use async_trait::async_trait;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::Deserialize;

use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, build_proxy_url, report_provider_error, requester_or_default, strip_url_scheme};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "ai",
        tags: &["ai", "hentai", "generated", "anime", "community"],
    };

const CHANNEL_ID: &str = "hotbunny";
const BASE_URL: &str = "https://hotbunny.ai";
const CDN_BASE: &str = "https://assets.hotbunny.ai";
const REFERER: &str = "https://hotbunny.ai/";
// Fetch at most this many posts per call so pagination stays cheap.
const MAX_FETCH: usize = 200;

// Category label → API CUID, hardcoded from GET /api/categories (stable IDs).
const CATEGORIES: &[(&str, &str)] = &[
    ("cmiys6yak0b1dpb0q3a9ijs8z", "Anal"),
    ("cmir7ft9r00bonq0ryhbjj0zf", "BDSM"),
    ("cmiyslxmt0b1gpb0qjbjiu1my", "Big Breasts"),
    ("cmiyszx1l0b1kpb0qxk71aql9", "Blondes"),
    ("cmiysraox0b1ipb0q4plrax1b", "Exhibitionism"),
    ("cmiyt45ce0b1mpb0qgb2s3q4c", "Feet"),
    ("cmiysyf0q0b1jpb0q6f41m9l2", "Furry"),
    ("cmiysfqo10b1epb0qczzjshi7", "Futanari"),
    ("cmiydl9et0000om0q9768i2ta", "Gay"),
    ("cmiysoegl0b1hpb0qd9vzsn7q", "Group"),
    ("cmiys0rlx0b19pb0q2f7iis14", "Hardcore"),
    ("cmiys3wix0b1bpb0qsmiud4bu", "Lesbian"),
    ("cmiye8qa40001om0qasn0d2iu", "Monsters"),
    ("cmiys1rcy0b1apb0qfb4lx9ib", "Oral"),
    ("cmiyepzos0000pb0qx74evwyx", "Soft"),
];

// ---------------------------------------------------------------------------
// API response types
// ---------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
struct FeedResponse {
    posts: Vec<HotbunnyPost>,
    #[serde(rename = "nextCursor", default)]
    _next_cursor: Option<String>,
}

#[derive(Debug, Deserialize)]
struct HotbunnyPost {
    id: String,
    #[serde(rename = "imageUrl", default)]
    image_url: String,
    #[serde(rename = "authorName", default)]
    author_name: String,
    #[serde(default)]
    likes: u64,
    #[serde(rename = "sharedVideos", default)]
    shared_videos: Vec<SharedVideo>,
    #[serde(default)]
    width: u32,
    #[serde(default)]
    height: u32,
    #[serde(default)]
    galleries: Vec<Gallery>,
}

#[derive(Debug, Deserialize)]
struct SharedVideo {
    #[serde(rename = "videoUrl", default)]
    video_url: String,
}

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

// ---------------------------------------------------------------------------
// Target routing
// ---------------------------------------------------------------------------

enum Target {
    Hot,
    Popular,
    Recent,
    Category(String),
    Search(String),
    Author(String),
}

// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------

pub struct HotbunnyProvider;

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

    fn resolve_target(sort: &str, query: Option<&str>, options: &ServerOptions) -> Target {
        if let Some(q) = query.map(str::trim).filter(|q| !q.is_empty()) {
            let lower = q.to_ascii_lowercase();

            // uploader:/author: prefix
            let suffix_after_colon = |prefix: &str| -> Option<&str> {
                lower.strip_prefix(prefix).map(|_| {
                    let colon = q.find(':').unwrap_or(0);
                    q[colon + 1..].trim()
                })
            };

            if suffix_after_colon("uploader:").is_some() || suffix_after_colon("author:").is_some() {
                let colon = q.find(':').unwrap_or(0);
                return Target::Author(q[colon + 1..].trim().to_string());
            }

            // cat:/category: prefix or #tag
            let keyword = if lower.starts_with("cat:") || lower.starts_with("category:") {
                let colon = q.find(':').unwrap_or(0);
                q[colon + 1..].trim()
            } else {
                q.trim_start_matches('#')
            };

            // Match keyword against a known category label
            if let Some((cat_id, _)) = CATEGORIES
                .iter()
                .find(|(_, label)| label.eq_ignore_ascii_case(keyword))
            {
                return Target::Category(cat_id.to_string());
            }

            return Target::Search(keyword.to_string());
        }

        // categories filter option
        if let Some(cat) = options.categories.as_deref().filter(|c| *c != "all" && !c.trim().is_empty()) {
            if let Some((cat_id, _)) = CATEGORIES
                .iter()
                .find(|(_, label)| label.eq_ignore_ascii_case(cat))
            {
                return Target::Category(cat_id.to_string());
            }
        }

        match sort {
            "popular" => Target::Popular,
            "recent" | "new" => Target::Recent,
            _ => Target::Hot,
        }
    }

    fn api_url(target: &Target, limit: usize) -> String {
        match target {
            Target::Hot => format!("{BASE_URL}/api/post/feed?limit={limit}"),
            Target::Popular => format!("{BASE_URL}/api/post/feed/popular?limit={limit}"),
            Target::Recent => format!("{BASE_URL}/api/post/feed/recents?limit={limit}"),
            Target::Category(id) => format!("{BASE_URL}/api/post/category/{id}?limit={limit}"),
            Target::Search(q) => {
                let encoded = utf8_percent_encode(q, NON_ALPHANUMERIC).to_string();
                format!("{BASE_URL}/api/post/search?query={encoded}&limit={limit}")
            }
            Target::Author(name) => {
                let encoded = utf8_percent_encode(name, NON_ALPHANUMERIC).to_string();
                format!("{BASE_URL}/api/post/author/{encoded}/all?limit={limit}")
            }
        }
    }

    // Returns (posts, cdn_cookie) — cdn_cookie is the img_auth JWT the API
    // sets via Set-Cookie for Domain=.hotbunny.ai; clients need it in the
    // Cookie header when fetching from assets.hotbunny.ai directly.
    async fn fetch_posts(&self, url: &str, options: &ServerOptions) -> (Vec<HotbunnyPost>, Option<String>) {
        let mut requester = requester_or_default(options, CHANNEL_ID, "fetch_posts");
        let headers = vec![
            ("Referer".to_string(), REFERER.to_string()),
            ("Accept".to_string(), "application/json".to_string()),
        ];
        let text = match requester.get_with_headers(url, headers, None).await {
            Ok(t) => t,
            Err(e) => {
                report_provider_error(CHANNEL_ID, "fetch_posts", &format!("{url}: {e}")).await;
                return (vec![], None);
            }
        };

        // The domain cookie fix in store_response_cookies now scopes img_auth to
        // .hotbunny.ai, so this lookup also covers assets.hotbunny.ai.
        let cdn_cookie = requester.cookie_header_for_url(CDN_BASE);

        match serde_json::from_str::<FeedResponse>(&text) {
            Ok(resp) => (resp.posts, cdn_cookie),
            Err(e) => {
                report_provider_error(CHANNEL_ID, "fetch_posts", &format!("parse: {e}")).await;
                (vec![], cdn_cookie)
            }
        }
    }

    fn build_item(post: HotbunnyPost, options: &ServerOptions, cdn_cookie: Option<&str>) -> VideoItem {
        let title = match post.galleries.first().map(|g| g.label.trim()) {
            Some(label) if !label.is_empty() => format!("{label} by {}", post.author_name),
            _ => format!("by {}", post.author_name),
        };

        let page_url = format!("{BASE_URL}/post/{}", post.id);

        let thumb = if !post.image_url.is_empty() {
            let cdn_path = strip_url_scheme(&format!("{CDN_BASE}/{}", post.image_url));
            build_proxy_url(options, "hotbunny-thumb", &cdn_path)
        } else {
            String::new()
        };

        let mut item = VideoItem::new(
            post.id,
            title,
            page_url,
            CHANNEL_ID.to_string(),
            thumb,
            0,
        );

        if post.likes > 0 {
            item.views = Some(post.likes as u32);
        }
        if post.width > 0 && post.height > 0 {
            item.aspectRatio = Some(post.width as f32 / post.height as f32);
        }

        if !post.author_name.is_empty() {
            let slug = post.author_name.to_ascii_lowercase();
            item.uploader = Some(post.author_name.clone());
            item.uploaderUrl = Some(format!("{BASE_URL}/u/{slug}"));
            item.uploaderId = Some(format!("{CHANNEL_ID}:{slug}"));
        }

        // First shared video becomes a playable format
        if let Some(sv) = post.shared_videos.into_iter().find(|v| !v.video_url.is_empty()) {
            let cdn_url = format!("{CDN_BASE}/{}", sv.video_url);
            let mut fmt = VideoFormat::new(cdn_url, "auto".to_string(), "mp4".to_string());
            fmt.add_http_header("Referer".to_string(), REFERER.to_string());
            if let Some(cookie) = cdn_cookie {
                fmt.add_http_header("Cookie".to_string(), cookie.to_string());
            }
            item.formats = Some(vec![fmt]);
        }

        item
    }

    fn build_channel(&self) -> Channel {
        let sort_options = vec![
            FilterOption { id: "feed".to_string(), title: "Hot".to_string() },
            FilterOption { id: "popular".to_string(), title: "Most Liked".to_string() },
            FilterOption { id: "recent".to_string(), title: "Recent".to_string() },
        ];

        let category_options: Vec<FilterOption> = CATEGORIES
            .iter()
            .map(|(_, label)| FilterOption {
                id: label.to_string(),
                title: label.to_string(),
            })
            .collect();

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "HotBunny".to_string(),
            description: "AI-generated hentai images and short clips from HotBunny.ai community creators.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=hotbunny.ai".to_string(),
            status: "active".to_string(),
            categories: CATEGORIES.iter().map(|(_, l)| l.to_string()).collect(),
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Feed ordering".to_string(),
                    systemImage: "arrow.up.arrow.down".to_string(),
                    colorName: "blue".to_string(),
                    options: sort_options,
                    multiSelect: false,
                },
                ChannelOption {
                    id: "categories".to_string(),
                    title: "Category".to_string(),
                    description: "Browse by content category".to_string(),
                    systemImage: "tag".to_string(),
                    colorName: "orange".to_string(),
                    options: category_options,
                    multiSelect: false,
                },
            ],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }
}

#[async_trait]
impl Provider for HotbunnyProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = cache;
        let _ = pool;

        let page = page.parse::<usize>().unwrap_or(1).max(1);
        let per_page = per_page.parse::<usize>().unwrap_or(20).clamp(1, 50);
        let total_limit = (page * per_page).min(MAX_FETCH);

        let normalized = query.as_deref().map(str::trim).filter(|q| !q.is_empty());
        let target = Self::resolve_target(&sort, normalized, &options);

        let url = Self::api_url(&target, total_limit);
        let (posts, cdn_cookie) = self.fetch_posts(&url, &options).await;
        if posts.is_empty() {
            return vec![];
        }

        let start = (page - 1) * per_page;
        if start >= posts.len() {
            return vec![];
        }

        posts
            .into_iter()
            .skip(start)
            .take(per_page)
            .map(|p| Self::build_item(p, &options, cdn_cookie.as_deref()))
            .collect()
    }

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