// animeidhentai.com — subbed/uncensored hentai episodes.
//
// The site is a Next.js app backed by a clean JSON API:
//   GET /api/browse?page=N        -> {videos:[28], total, pages}  (latest releases, paginated)
//   GET /api/search?q=Q&page=N    -> {videos:[8]}                 (matches titles AND tags)
// `browse` ignores sort/genre params, so genre/tag shortcuts route through `search` (which
// matches tag names well). Episode JSON carries everything we need for a card (title, tags,
// views, rating, duration, studio, release date) plus an `embedUrl` of the form
// `https://nhplayer.com/v/{embedId}/`.
//
// Playback: the watch page is client-rendered and yt-dlp cannot resolve nhplayer or the
// Cloudflare-fronted CDN behind it, so `video.url` is the (reachable) series page and the real
// MP4 is served through `/proxy/animeidhentai/{embedId}.mp4`, which replicates nhplayer's
// browser challenge and streams the signed CDN bytes (see src/proxies/animeidhentai.rs).

use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{
    Provider, build_proxy_url, report_provider_error, requester_or_default,
};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::requester::Requester;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait;
use serde_json::Value;

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

const CHANNEL_ID: &str = "animeidhentai";
const BASE_URL: &str = "https://animeidhentai.com";
const SITE_REFERER: &str = "https://animeidhentai.com/";

// Curated popular genres for the `categories` filter. The id doubles as the search term, since
// genre filtering is implemented by routing to `/api/search?q=<id>`.
const GENRES: &[&str] = &[
    "ahegao",
    "anal",
    "big breasts",
    "blowjob",
    "bondage",
    "censored",
    "creampie",
    "dark skin",
    "demon",
    "elf",
    "footjob",
    "futanari",
    "gangbang",
    "harem",
    "incest",
    "milf",
    "nakadashi",
    "netorare",
    "nurse",
    "paizuri",
    "schoolgirl",
    "teacher",
    "tentacle",
    "threesome",
    "toys",
    "uncensored",
    "virgin",
    "yuri",
];

#[derive(Debug, Clone)]
enum Target {
    Latest,
    Search(String),
}

#[derive(Debug, Clone)]
pub struct AnimeidhentaiProvider {}

impl AnimeidhentaiProvider {
    pub fn new() -> Self {
        Self {}
    }

    fn title_case(s: &str) -> String {
        s.split_whitespace()
            .map(|w| {
                let mut chars = w.chars();
                match chars.next() {
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                    None => String::new(),
                }
            })
            .collect::<Vec<_>>()
            .join(" ")
    }

    fn build_channel(&self, _cv: ClientVersion) -> Channel {
        let mut cat_options: Vec<FilterOption> = vec![FilterOption {
            id: "all".to_string(),
            title: "All".to_string(),
        }];
        for genre in GENRES {
            cat_options.push(FilterOption {
                id: genre.to_string(),
                title: Self::title_case(genre),
            });
        }

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "AnimeIDHentai".to_string(),
            description:
                "AnimeIDHentai — latest subbed and uncensored hentai episodes with keyword and genre search."
                    .to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=animeidhentai.com".to_string(),
            status: "active".to_string(),
            categories: GENRES.iter().map(|g| Self::title_case(g)).collect(),
            options: vec![ChannelOption {
                id: "categories".to_string(),
                title: "Genres".to_string(),
                description: "Browse a hentai genre.".to_string(),
                systemImage: "square.grid.2x2".to_string(),
                colorName: "pink".to_string(),
                options: cat_options,
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    /// Map a query (with optional `genre:`/`tag:`/`cat:` prefix) onto a fetch target.
    fn resolve_query_target(query: &str) -> Target {
        let trimmed = query.trim().trim_start_matches('#').trim();
        if let Some((kind, value)) = trimmed.split_once(':') {
            let value = value.trim();
            if !value.is_empty() {
                match kind.trim().to_ascii_lowercase().as_str() {
                    "genre" | "tag" | "cat" | "category" => {
                        return Target::Search(value.to_string());
                    }
                    _ => {}
                }
            }
        }
        Target::Search(trimmed.to_string())
    }

    fn target_url(target: &Target, page: u32) -> String {
        match target {
            Target::Latest => format!("{BASE_URL}/api/browse?page={page}"),
            Target::Search(q) => {
                let encoded = percent_encoding::utf8_percent_encode(
                    q,
                    percent_encoding::NON_ALPHANUMERIC,
                )
                .to_string();
                format!("{BASE_URL}/api/search?q={encoded}&page={page}")
            }
        }
    }

    fn abs_url(path: &str) -> String {
        if path.starts_with("http://") || path.starts_with("https://") {
            path.to_string()
        } else if path.starts_with('/') {
            format!("{BASE_URL}{path}")
        } else {
            format!("{BASE_URL}/{path}")
        }
    }

    fn pick_image(v: &Value) -> String {
        for key in ["thumb", "backdrop", "featureImage", "cover"] {
            if let Some(s) = v[key].as_str() {
                if !s.is_empty() {
                    return Self::abs_url(s);
                }
            }
        }
        String::new()
    }

    fn extract_embed_id(embed_url: &str) -> Option<String> {
        let after = embed_url.split("/v/").nth(1)?;
        let id = after.trim_matches('/').split('/').next()?.split('?').next()?;
        (!id.is_empty()).then(|| id.to_string())
    }

    fn build_item(v: &Value, options: &ServerOptions) -> Option<VideoItem> {
        let slug = v["slug"].as_str().filter(|s| !s.is_empty())?;
        let embed_id = Self::extract_embed_id(v["embedUrl"].as_str().unwrap_or_default())?;

        let series_slug = v["titleSlug"].as_str().unwrap_or(slug);
        let base_title = v["title"].as_str().unwrap_or_default().trim().to_string();
        let ep = v["ep"].as_u64().unwrap_or(0);
        let title = if base_title.is_empty() {
            slug.replace('-', " ")
        } else if ep >= 1 {
            format!("{base_title} Episode {ep}")
        } else {
            base_title
        };

        let url = format!("{BASE_URL}/series/{series_slug}");
        let thumb = Self::pick_image(v);
        let duration = v["duration"]
            .as_str()
            .and_then(parse_time_to_seconds)
            .and_then(|s| u32::try_from(s).ok())
            .unwrap_or(0);

        let mut item = VideoItem::new(
            slug.to_string(),
            title,
            url,
            CHANNEL_ID.to_string(),
            thumb,
            duration,
        );
        item = item.aspect_ratio(16.0 / 9.0);

        if let Some(views) = v["views"].as_u64() {
            item.views = Some(views.min(u32::MAX as u64) as u32);
        }
        if let Some(rating) = v["rating"].as_f64() {
            // Site rating is 0-10; Hot Tub clients use a 0-100 scale.
            item.rating = Some(((rating * 10.0) as f32).clamp(0.0, 100.0));
        }

        let tags: Vec<String> = v["tags"]
            .as_array()
            .map(|arr| {
                arr.iter()
                    .filter_map(|t| t.as_str().map(str::to_string))
                    .collect()
            })
            .unwrap_or_default();
        item = item.tags(tags);

        if let Some(brand) = v["brand"].as_str().filter(|s| !s.is_empty()) {
            item = item.uploader(brand.to_string());
        }

        if let Some(released) = v["releasedAt"].as_str() {
            if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(released) {
                let ts = dt.timestamp();
                if ts > 0 {
                    item = item.uploaded_at(ts as u64);
                }
            }
        }

        let quality = v["quality"].as_str().unwrap_or_default().to_string();
        let proxy_url = build_proxy_url(options, CHANNEL_ID, &format!("{embed_id}.mp4"));
        let label = if quality.is_empty() {
            "mp4".to_string()
        } else {
            quality.clone()
        };
        let mut fmt = VideoFormat::new(proxy_url, label, "mp4".to_string()).ext("mp4".to_string());
        if let Some(height) = quality
            .chars()
            .filter(|c| c.is_ascii_digit())
            .collect::<String>()
            .parse::<u32>()
            .ok()
            .filter(|h| *h > 0)
        {
            fmt = fmt.height(height);
        }
        item = item.formats(vec![fmt]);

        Some(item)
    }

    async fn fetch_videos(requester: &mut Requester, url: &str) -> Option<Vec<Value>> {
        let resp = requester
            .get_raw_with_headers(
                url,
                vec![
                    ("Referer".to_string(), SITE_REFERER.to_string()),
                    ("Accept".to_string(), "application/json".to_string()),
                ],
            )
            .await
            .ok()?;
        if !resp.status().is_success() {
            return None;
        }
        let body = resp.text().await.ok()?;
        let json: Value = serde_json::from_str(&body).ok()?;
        json["videos"].as_array().cloned()
    }

    async fn fetch_target(
        &self,
        cache: VideoCache,
        target: Target,
        page: u32,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let url = Self::target_url(&target, page);

        if let Some((ts, cached)) = cache.get(&url) {
            if ts.elapsed().unwrap_or_default().as_secs() < 300 {
                return cached.clone();
            }
        }

        let mut requester =
            requester_or_default(&options, CHANNEL_ID, "fetch_target.missing_requester");
        let Some(videos) = Self::fetch_videos(&mut requester, &url).await else {
            report_provider_error(CHANNEL_ID, "fetch_target.request", &format!("url={url}")).await;
            return vec![];
        };

        let items: Vec<VideoItem> = videos
            .iter()
            .filter_map(|v| Self::build_item(v, &options))
            .collect();

        if !items.is_empty() {
            cache.insert(url, items.clone());
        }
        items
    }
}

#[async_trait]
impl Provider for AnimeidhentaiProvider {
    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::<u32>().unwrap_or(1).max(1);

        let target = match query {
            Some(q) if !q.trim().is_empty() => Self::resolve_query_target(q.trim()),
            _ => match options.categories.as_deref() {
                Some(cat) if !cat.is_empty() && cat != "all" => Target::Search(cat.to_string()),
                _ => Target::Latest,
            },
        };

        self.fetch_target(cache, target, page, options).await
    }

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