use crate::api::ClientVersion;
use crate::providers::Provider;
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::requester::Requester;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use crate::{DbPool, db};

use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use error_chain::error_chain;
use futures::stream::{self, StreamExt};
use htmlentity::entity::{ICodedDataTrait, decode};
use serde::Deserialize;
use std::collections::HashSet;
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::vec;
use titlecase::Titlecase;
use wreq::Version;
use wreq_util::Emulation;

// How long a cached listing/search entry is considered usable at all.
const HARD_TTL_SECS: u64 = 60 * 60 * 24;
// Past this age we still answer instantly from cache/DB but trigger a
// background refresh so the next request gets fresh data / renewed signed URLs.
const SOFT_TTL_SECS: u64 = 60 * 60;

#[derive(Debug, Deserialize)]
struct PlayerSecureConfig {
    en: String,
    iv: String,
    uri: String,
}

#[derive(Debug, Deserialize)]
struct PlayerApiSource {
    src: String,
    #[serde(default)]
    label: String,
}

#[derive(Debug, Deserialize, Default)]
struct PlayerApiData {
    #[serde(default)]
    sources: Vec<PlayerApiSource>,
}

#[derive(Debug, Deserialize)]
struct PlayerApiResponse {
    status: bool,
    #[serde(default)]
    data: Option<PlayerApiData>,
}

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "hentai-animation",
        tags: &["hentai", "anime", "curated"],
    };

error_chain! {
    foreign_links {
        Io(std::io::Error);
        HttpRequest(wreq::Error);
        Json(serde_json::Error);
    }
    errors {
        Parse(msg: String) {
            description("parse error")
            display("parse error: {}", msg)
        }
    }
}

#[derive(Debug, Clone)]
pub struct HentaihavenProvider {
    url: String,
    categories: Arc<RwLock<Vec<FilterOption>>>,
}

impl HentaihavenProvider {
    pub fn new() -> Self {
        let provider = Self {
            url: "https://hentaihaven.xxx".to_string(),
            categories: Arc::new(RwLock::new(vec![])),
        };
        provider
    }

    fn build_channel(&self, clientversion: ClientVersion) -> Channel {
        let _ = clientversion;
        Channel {
            id: "hentaihaven".to_string(),
            name: "Hentai Haven".to_string(),
            description: "Watch Free Hentai Videos HD!".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=hentaihaven.xxx".to_string(),
            status: "active".to_string(),
            categories: self
                .categories
                .read()
                .map(|categories| categories.iter().map(|c| c.title.clone()).collect())
                .unwrap_or_else(|e| {
                    crate::providers::report_provider_error_background(
                        "hentaihaven",
                        "build_channel.categories_read",
                        &e.to_string(),
                    );
                    vec![]
                }),
            options: vec![],
            nsfw: true,
            cacheDuration: None,
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
        if let Ok(mut vec) = target.write() {
            if !vec.iter().any(|x| x.id == item.id) {
                vec.push(item);
            }
        }
    }

    async fn get(
        &self,
        cache: VideoCache,
        page: u8,
        sort: &str,
        options: ServerOptions,
        pool: DbPool,
    ) -> Result<Vec<VideoItem>> {
        let _ = sort;
        let video_url = format!("{}/hentai/page/{}/", self.url, page);

        // Fast path: a usable in-memory entry exists. Answer immediately; once it
        // is older than the soft TTL, kick a background refresh so the next caller
        // sees fresher data without anyone waiting on it now.
        if let Some((time, items)) = cache.get(&video_url) {
            let age = time.elapsed().unwrap_or_default().as_secs();
            if age < HARD_TTL_SECS && !items.is_empty() {
                if age >= SOFT_TTL_SECS {
                    let requester = crate::providers::requester_or_default(
                        &options,
                        module_path!(),
                        "missing_requester",
                    );
                    self.spawn_refresh(requester, pool, cache, video_url, None, false);
                }
                return Ok(items);
            }
        }

        // Fetch the listing page (a single cheap request) to learn which episode
        // URLs belong on this page and in what order.
        let mut requester =
            crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
        let text = match Self::get_with_retry(&mut requester, &video_url, 3).await {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "hentaihaven",
                    "get.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(cache
                    .get(&video_url)
                    .map(|(_, items)| items)
                    .unwrap_or_default());
            }
        };
        let urls = Self::parse_listing_urls(&text);
        if urls.is_empty() {
            return Ok(cache
                .get(&video_url)
                .map(|(_, items)| items)
                .unwrap_or_default());
        }

        // Serve whatever we have already resolved (from the DB) right away, then
        // refresh the entire listing in the background.
        let db_items = Self::items_from_db(&urls, &pool);
        if !db_items.is_empty() {
            cache.insert(video_url.clone(), db_items.clone());
            self.spawn_refresh(requester, pool, cache, video_url, Some(urls), false);
            return Ok(db_items);
        }

        // Cold start: nothing cached for any item yet, resolve synchronously this
        // one time so the first ever request is not empty.
        let items = self.resolve_urls(urls, &requester, pool).await;
        if !items.is_empty() {
            cache.insert(video_url.clone(), items.clone());
            return Ok(items);
        }
        Ok(cache
            .get(&video_url)
            .map(|(_, items)| items)
            .unwrap_or_default())
    }

    async fn query(
        &self,
        cache: VideoCache,
        page: u8,
        query: &str,
        options: ServerOptions,
        pool: DbPool,
    ) -> Result<Vec<VideoItem>> {
        if page > 1 {
            return Ok(vec![]);
        }
        let video_url = format!("{}/?s={}", self.url, query.replace(" ", "+"));

        if let Some((time, items)) = cache.get(&video_url) {
            let age = time.elapsed().unwrap_or_default().as_secs();
            if age < HARD_TTL_SECS && !items.is_empty() {
                if age >= SOFT_TTL_SECS {
                    let requester = crate::providers::requester_or_default(
                        &options,
                        module_path!(),
                        "missing_requester",
                    );
                    self.spawn_refresh(requester, pool, cache, video_url, None, true);
                }
                return Ok(items);
            }
        }

        let mut requester =
            crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
        let text = match Self::get_with_retry(&mut requester, &video_url, 3).await {
            Ok(text) => text,
            Err(e) => {
                crate::providers::report_provider_error(
                    "hentaihaven",
                    "query.request",
                    &format!("url={video_url}; error={e}"),
                )
                .await;
                return Ok(cache
                    .get(&video_url)
                    .map(|(_, items)| items)
                    .unwrap_or_default());
            }
        };
        let urls = Self::parse_search_urls(&text);
        if urls.is_empty() {
            return Ok(cache
                .get(&video_url)
                .map(|(_, items)| items)
                .unwrap_or_default());
        }

        let db_items = Self::items_from_db(&urls, &pool);
        if !db_items.is_empty() {
            cache.insert(video_url.clone(), db_items.clone());
            self.spawn_refresh(requester, pool, cache, video_url, Some(urls), true);
            return Ok(db_items);
        }

        let items = self.resolve_urls(urls, &requester, pool).await;
        if !items.is_empty() {
            cache.insert(video_url.clone(), items.clone());
            return Ok(items);
        }
        Ok(cache
            .get(&video_url)
            .map(|(_, items)| items)
            .unwrap_or_default())
    }

    fn extract_segment_url(seg: &str) -> Option<String> {
        seg.split("a href=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .map(|s| s.to_string())
    }

    /// Extract the ordered list of episode page URLs from a listing page.
    fn parse_listing_urls(html: &str) -> Vec<String> {
        if html.is_empty() || html.contains("404 Not Found") {
            return vec![];
        }
        let block = match html.split("previouspostslink").next().and_then(|s| {
            s.split("vraven_manga_list").nth(1).or_else(|| {
                s.find(r#"<div class="page-content-listing item-big_thumbnail">"#)
                    .map(|idx| &s[idx..])
            })
        }) {
            Some(b) => b,
            None => {
                crate::providers::report_provider_error_background(
                    "hentaihaven",
                    "parse_listing.block",
                    "Failed to get block from listing html",
                );
                return vec![];
            }
        };
        block
            .split("id=\"manga-item-")
            .skip(1)
            .filter_map(Self::extract_segment_url)
            .collect()
    }

    /// Extract the ordered list of result URLs from a search page.
    fn parse_search_urls(html: &str) -> Vec<String> {
        if html.is_empty() || html.contains("404 Not Found") {
            return vec![];
        }
        let block = match html
            .split("<footer")
            .next()
            .and_then(|s| s.split("content-area").nth(1))
        {
            Some(b) => b,
            None => {
                crate::providers::report_provider_error_background(
                    "hentaihaven",
                    "parse_search.block",
                    "Failed to get block from search html",
                );
                return vec![];
            }
        };
        block
            .split("c-tabs-item__content col-6 col-md-12")
            .skip(1)
            .filter_map(Self::extract_segment_url)
            .collect()
    }

    /// Build a response from already-resolved items stored in the DB, preserving
    /// the order of `urls`. Items not yet in the DB are simply skipped.
    fn items_from_db(urls: &[String], pool: &DbPool) -> Vec<VideoItem> {
        let mut conn = match pool.get() {
            Ok(conn) => conn,
            Err(_) => return vec![],
        };
        urls.iter()
            .filter_map(|url| match db::get_video(&mut conn, url.clone()) {
                Ok(Some(json)) => VideoItem::from(json).ok(),
                _ => None,
            })
            .collect()
    }

    /// Resolve each episode page URL into a full `VideoItem`, persisting every
    /// success to the DB. On failure we fall back to any stored copy so a
    /// transient error does not drop the item from the page.
    async fn resolve_urls(
        &self,
        urls: Vec<String>,
        requester: &Requester,
        pool: DbPool,
    ) -> Vec<VideoItem> {
        stream::iter(urls.into_iter().map(|url| {
            let provider = self.clone();
            let mut req = requester.clone();
            let pool = pool.clone();
            async move {
                match provider.fetch_video_item(&url, &mut req).await {
                    Ok(item) => {
                        if let Ok(mut conn) = pool.get() {
                            let new_len = item.formats.as_ref().map_or(0, |f| f.len());
                            let old_item = db::get_video(&mut conn, url.clone())
                                .ok()
                                .flatten()
                                .and_then(|json| VideoItem::from(json).ok());
                            let old_len = old_item
                                .as_ref()
                                .and_then(|o| o.formats.as_ref())
                                .map_or(0, |f| f.len());
                            if new_len >= old_len {
                                let _ = db::upsert_video(
                                    &mut conn,
                                    &url,
                                    &serde_json::to_string(&item).unwrap_or_default(),
                                );
                                Some(item)
                            } else {
                                // A partial refresh resolved fewer episodes than we
                                // already have (likely a transient outage) — keep the
                                // richer stored copy rather than degrading it.
                                old_item.or(Some(item))
                            }
                        } else {
                            Some(item)
                        }
                    }
                    Err(e) => {
                        eprintln!("Hentai Haven Provider: Failed to resolve {url}: {e}");
                        if let Ok(mut conn) = pool.get() {
                            if let Ok(Some(cached)) = db::get_video(&mut conn, url.clone()) {
                                if let Ok(item) = VideoItem::from(cached) {
                                    return Some(item);
                                }
                            }
                        }
                        None
                    }
                }
            }
        }))
        .buffered(2)
        .filter_map(|item| async move { item })
        .collect::<Vec<_>>()
        .await
    }

    /// Per-listing in-flight guard so we never run two background refreshes for
    /// the same page concurrently.
    fn refresh_in_flight() -> &'static Mutex<HashSet<String>> {
        static SET: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
        SET.get_or_init(|| Mutex::new(HashSet::new()))
    }

    fn try_begin_refresh(key: &str) -> bool {
        match Self::refresh_in_flight().lock() {
            Ok(mut set) => set.insert(key.to_string()),
            Err(_) => false,
        }
    }

    fn end_refresh(key: &str) {
        if let Ok(mut set) = Self::refresh_in_flight().lock() {
            set.remove(key);
        }
    }

    /// Spawn a non-blocking refresh of a listing/search page. `urls` may be
    /// supplied when the caller already fetched the listing; otherwise the
    /// refresh re-fetches it itself.
    fn spawn_refresh(
        &self,
        requester: Requester,
        pool: DbPool,
        cache: VideoCache,
        key: String,
        urls: Option<Vec<String>>,
        search: bool,
    ) {
        if !Self::try_begin_refresh(&key) {
            return;
        }
        let provider = self.clone();
        tokio::spawn(async move {
            provider
                .refresh(requester, pool, cache, key.clone(), urls, search)
                .await;
            Self::end_refresh(&key);
        });
    }

    async fn refresh(
        &self,
        mut requester: Requester,
        pool: DbPool,
        cache: VideoCache,
        key: String,
        urls: Option<Vec<String>>,
        search: bool,
    ) {
        let urls = match urls {
            Some(urls) => urls,
            None => match Self::get_with_retry(&mut requester, &key, 3).await {
                Ok(text) => {
                    if search {
                        Self::parse_search_urls(&text)
                    } else {
                        Self::parse_listing_urls(&text)
                    }
                }
                Err(e) => {
                    crate::providers::report_provider_error_background(
                        "hentaihaven",
                        "refresh.request",
                        &format!("url={key}; error={e}"),
                    );
                    return;
                }
            },
        };
        if urls.is_empty() {
            return;
        }
        let items = self.resolve_urls(urls, &requester, pool).await;
        if !items.is_empty() {
            cache.insert(key, items);
        }
    }

    async fn fetch_video_item(
        &self,
        video_url: &str,
        requester: &mut Requester,
    ) -> Result<VideoItem> {
        let html = Self::get_with_retry(requester, video_url, 3)
            .await
            .map_err(|e| Error::from(format!("Failed to fetch video page: {}", e)))?;

        let mut title = html
            .split("<h1>")
            .nth(1)
            .and_then(|s| s.split("</h1>").next())
            .ok_or_else(|| ErrorKind::Parse(format!("video title: {video_url}")))?
            .trim()
            .to_string();
        title = decode(title.as_bytes())
            .to_string()
            .unwrap_or(title)
            .titlecase();
        let id = video_url
            .split('/')
            .nth(4)
            .and_then(|s| s.split('.').next())
            .ok_or_else(|| ErrorKind::Parse(format!("video id: {video_url}")))?
            .to_string();
        let thumb = html
            .split("og:image\" content=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .unwrap_or("")
            .to_string();
        let raw_tags: Vec<FilterOption> = html
            .split("Genre(s)")
            .nth(1)
            .unwrap_or_default()
            .split("Release")
            .nth(0)
            .unwrap_or_default()
            .split("a href=\"")
            .skip(1)
            .map(|tag_block| {
                let id = tag_block
                    .split("\"")
                    .nth(1)
                    .and_then(|s| s.split('"').next())
                    .unwrap_or("")
                    .to_string();
                let title = tag_block
                    .split('>')
                    .nth(1)
                    .and_then(|s| s.split('<').next())
                    .map(|s| {
                        decode(s.as_bytes())
                            .to_string()
                            .unwrap_or(s.to_string())
                            .titlecase()
                    })
                    .unwrap_or("".to_string());
                FilterOption {
                    id: id.to_ascii_lowercase().replace(" ", "+"),
                    title: title.clone(),
                }
            })
            .collect::<Vec<FilterOption>>();
        for tag in &raw_tags {
            Self::push_unique(&self.categories, tag.clone());
        }
        let tags = raw_tags.into_iter().map(|t| t.title).collect();
        let views = html
            .split("Viewed")
            .last()
            .and_then(|s| s.split("summary-content\">").nth(1))
            .and_then(|s| s.split(" Total").nth(0))
            .map(|s| s.trim().parse::<u32>().unwrap_or(0))
            .unwrap_or(0);
        let episode_block = html
            .split("manga-chapters-holder")
            .nth(1)
            .unwrap_or_default()
            .split("vraven_read")
            .nth(0)
            .unwrap_or_default();
        let episodes: Vec<(String, String)> = episode_block
            .split("wp-manga-chapter")
            .skip(1)
            .filter_map(|episode| {
                let href = episode
                    .split("a href=\"")
                    .nth(1)
                    .and_then(|s| s.split('"').next())?
                    .to_string();
                let title = episode
                    .split("<div>")
                    .nth(1)
                    .and_then(|s| s.split('<').next())
                    .unwrap_or_default()
                    .trim()
                    .to_string();
                Some((title, href))
            })
            .collect();

        let formats: Vec<VideoFormat> = stream::iter(episodes.into_iter().map(|(title, href)| {
            let requester = requester.clone();
            let provider = self.clone();
            async move { provider.resolve_episode_format(title, href, requester).await }
        }))
        .buffered(1)
        .filter_map(|result| async move {
            match result {
                Ok(format) => Some(format),
                Err(e) => {
                    eprintln!("Hentai Haven Provider: Failed to resolve episode format: {e}");
                    None
                }
            }
        })
        .collect::<Vec<_>>()
        .await;
        if formats.is_empty() {
            return Err(Error::from(format!("No formats found for video URL: {}", video_url)));
        }
        if formats.len() > 1 {
            title = format!("{} ({} Episodes)", title, formats.len());
        }

        Ok(
            VideoItem::new(id, title, video_url.to_string(), "hentaihaven".into(), thumb, 0)
                .formats(formats)
                .tags(tags)
                .views(views)
                .aspect_ratio(0.715),
        )
    }

    async fn resolve_episode_format(
        &self,
        title: String,
        href: String,
        mut requester: Requester,
    ) -> Result<VideoFormat> {
        let episode_html = Self::get_with_retry(&mut requester, &href, 4)
            .await
            .map_err(|e| Error::from(format!("Failed to fetch episode page {href}: {e}")))?;

        let player_url = episode_html
            .split("iframe src=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .map(|s| s.replace("&amp;", "&"))
            .ok_or_else(|| ErrorKind::Parse(format!("player iframe url: {href}")))?;

        let player_html = Self::get_with_retry(&mut requester, &player_url, 4)
            .await
            .map_err(|e| Error::from(format!("Failed to fetch player page {player_url}: {e}")))?;

        let token = player_html
            .split("x-secure-token\" content=\"")
            .nth(1)
            .and_then(|s| s.split('"').next())
            .ok_or_else(|| ErrorKind::Parse(format!("secure token: {href}")))?;

        let config = Self::decode_secure_token(token)?;
        let api_base = if config.uri.starts_with("//") {
            format!("https:{}", config.uri)
        } else {
            config.uri.clone()
        };
        let api_url = format!("{api_base}api.php");

        let body = Self::build_player_api_body(&config.en, &config.iv);

        let text = Self::post_ajax_with_retry(
            &api_url,
            &body,
            vec![
                ("Content-Type", "application/x-www-form-urlencoded"),
                ("Accept", "*/*"),
                ("Accept-Language", "en-US,en;q=0.5"),
                ("Referer", player_url.as_str()),
                ("Origin", self.url.as_str()),
                ("Sec-Fetch-Dest", "empty"),
                ("Sec-Fetch-Mode", "cors"),
                ("Sec-Fetch-Site", "same-origin"),
                ("X-Requested-With", "XMLHttpRequest"),
            ],
            4,
        )
        .await
        .map_err(|e| Error::from(format!("Failed to call player api {api_url}: {e}")))?;

        let api_response: PlayerApiResponse = serde_json::from_str(&text)
            .map_err(|e| Error::from(format!("Failed to parse player api body {api_url}: {e}")))?;
        if !api_response.status {
            return Err(Error::from(format!("player api returned status=false for {href}")));
        }
        let source = api_response
            .data
            .and_then(|d| d.sources.into_iter().next())
            .ok_or_else(|| ErrorKind::Parse(format!("no sources in player api response: {href}")))?;

        let quality = if source.label.trim().is_empty() {
            "auto".to_string()
        } else {
            source.label.to_ascii_lowercase()
        };

        Ok(
            VideoFormat::new(source.src, quality, "m3u8".to_string())
                .format_id(title.clone())
                .http_header(
                    "User-Agent".to_string(),
                    "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"
                        .to_string(),
                )
                .http_header("Referer".to_string(), self.url.clone())
                .http_header("Origin".to_string(), self.url.clone())
                .format_note(title),
        )
    }

    async fn get_with_retry(
        requester: &mut Requester,
        url: &str,
        attempts: u32,
    ) -> std::result::Result<String, String> {
        let mut last_err = String::new();
        for attempt in 0..attempts {
            if attempt > 0 {
                let backoff_ms = 500u64 * (1u64 << (attempt - 1).min(3));
                tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
            }
            match requester.get(url, Some(Version::HTTP_2)).await {
                Ok(text) => return Ok(text),
                Err(e) => last_err = e.to_string(),
            }
        }
        Err(last_err)
    }

    fn ajax_client() -> &'static wreq::Client {
        static CLIENT: OnceLock<wreq::Client> = OnceLock::new();
        CLIENT.get_or_init(|| {
            wreq::Client::builder()
                .cert_verification(false)
                .emulation(Emulation::Chrome137)
                .build()
                .expect("Failed to build hentaihaven AJAX client")
        })
    }

    async fn post_ajax_with_retry(
        url: &str,
        body: &str,
        headers: Vec<(&str, &str)>,
        attempts: u32,
    ) -> std::result::Result<String, String> {
        let mut last_err = String::new();
        for attempt in 0..attempts {
            if attempt > 0 {
                let backoff_ms = 500u64 * (1u64 << (attempt - 1).min(3));
                tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
            }
            let mut request = Self::ajax_client()
                .post(url)
                .version(Version::HTTP_2)
                .body(body.to_string());
            for (key, value) in headers.iter() {
                request = request.header(*key, *value);
            }
            match request.send().await {
                Ok(response) => {
                    let status = response.status();
                    match response.text().await {
                        Ok(text) if status.is_success() => return Ok(text),
                        Ok(_) => last_err = format!("status {status}"),
                        Err(e) => last_err = e.to_string(),
                    }
                }
                Err(e) => last_err = e.to_string(),
            }
        }
        Err(last_err)
    }

    fn build_player_api_body(en: &str, iv: &str) -> String {
        let mut serializer = url::form_urlencoded::Serializer::new(String::new());
        serializer
            .append_pair("action", "zarat_get_data_player_ajax")
            .append_pair("a", en)
            .append_pair("b", iv);
        serializer.finish()
    }

    fn decode_secure_token(token: &str) -> Result<PlayerSecureConfig> {
        let stripped = token.strip_prefix("sha512-").unwrap_or(token);
        let mut data = Self::rot13(stripped);
        data = Self::decode_base64_layer(&data)?;
        data = Self::rot13(&data);
        data = Self::decode_base64_layer(&data)?;
        data = Self::rot13(&data);
        data = Self::decode_base64_layer(&data)?;
        serde_json::from_str(&data)
            .map_err(|e| Error::from(format!("Failed to parse secure token json: {e}")))
    }

    fn decode_base64_layer(value: &str) -> Result<String> {
        let mut normalized = value.trim().to_string();
        while normalized.len() % 4 != 0 {
            normalized.push('=');
        }
        let bytes = STANDARD
            .decode(normalized)
            .map_err(|e| Error::from(format!("base64 decode failed: {e}")))?;
        String::from_utf8(bytes).map_err(|e| Error::from(format!("utf8 decode failed: {e}")))
    }

    fn rot13(input: &str) -> String {
        input
            .chars()
            .map(|c| match c {
                'A'..='Z' => (((c as u8 - b'A' + 13) % 26) + b'A') as char,
                'a'..='z' => (((c as u8 - b'a' + 13) % 26) + b'a') as char,
                other => other,
            })
            .collect()
    }
}

#[async_trait]
impl Provider for HentaihavenProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        _per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let page = page.parse::<u8>().unwrap_or(1);

        let res = match query {
            Some(q) => self.to_owned().query(cache, page, &q, options, pool).await,
            None => self.get(cache, page, &sort, options, pool).await,
        };

        res.unwrap_or_else(|e| {
            eprintln!("hentai haven error: {e}");
            vec![]
        })
    }

    fn get_channel(&self, v: ClientVersion) -> Option<Channel> {
        Some(self.build_channel(v))
    }
}
