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::parse_abbreviated_number;
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 error_chain::error_chain;
use scraper::{ElementRef, Html, Selector};
use wreq::Version;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "mainstream-tube",
        tags: &["tube", "hd", "studio", "exclusive"],
    };

const BASE_URL: &str = "https://www.thepornbunny.com";
const CHANNEL_ID: &str = "thepornbunny";
const FIREFOX_UA: &str =
    "Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0";
const HTML_ACCEPT: &str =
    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";

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

#[derive(Debug, Clone)]
pub struct ThepornbunnyProvider;

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

// All 20 categories exposed on thepornbunny.com/categories/
static CATEGORIES: &[(&str, &str)] = &[
    ("anal", "Anal"),
    ("asian", "Asian"),
    ("big-ass", "Big Ass"),
    ("big-tits", "Big Tits"),
    ("black", "Black"),
    ("cheating", "Cheating"),
    ("creampie", "Creampie"),
    ("deep-throat", "Deep Throat"),
    ("geek-nerd", "Geek / Nerd"),
    ("gym", "Gym"),
    ("latina", "Latina"),
    ("lesbian", "Lesbian"),
    ("massage", "Massage"),
    ("milf", "MILF"),
    ("outdoors", "Outdoors"),
    ("red-head", "Red Head"),
    ("shower", "Shower"),
    ("squirt", "Squirt"),
    ("teen18", "Teen18+"),
    ("threesome", "Threesome"),
];

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

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        let cat_options: Vec<FilterOption> = std::iter::once(FilterOption {
            id: "all".to_string(),
            title: "All".to_string(),
        })
        .chain(CATEGORIES.iter().map(|(slug, name)| FilterOption {
            id: format!("{BASE_URL}/category/{slug}/"),
            title: name.to_string(),
        }))
        .collect();

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "ThePornBunny".to_string(),
            description: "PornBunny HD porn videos with latest, top-rated, most-viewed, category, tag, studio, and search routing.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=thepornbunny.com"
                .to_string(),
            status: "active".to_string(),
            categories: CATEGORIES.iter().map(|(_, name)| name.to_string()).collect(),
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Browse PornBunny feeds.".to_string(),
                    systemImage: "list.number".to_string(),
                    colorName: "blue".to_string(),
                    options: vec![
                        FilterOption {
                            id: "new".to_string(),
                            title: "Latest".to_string(),
                        },
                        FilterOption {
                            id: "popular".to_string(),
                            title: "Most Viewed".to_string(),
                        },
                        FilterOption {
                            id: "rated".to_string(),
                            title: "Top Rated".to_string(),
                        },
                    ],
                    multiSelect: false,
                },
                ChannelOption {
                    id: "categories".to_string(),
                    title: "Categories".to_string(),
                    description: "Browse a PornBunny category archive.".to_string(),
                    systemImage: "square.grid.2x2".to_string(),
                    colorName: "orange".to_string(),
                    options: cat_options,
                    multiSelect: false,
                },
            ],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn selector(value: &str) -> Result<Selector> {
        Selector::parse(value)
            .map_err(|error| Error::from(format!("selector `{value}` parse failed: {error}")))
    }

    fn text_of(element: &ElementRef<'_>) -> String {
        element
            .text()
            .collect::<Vec<_>>()
            .join(" ")
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
    }

    fn normalize_url(url: &str) -> String {
        let trimmed = url.trim();
        if trimmed.is_empty() {
            return String::new();
        }
        if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
            return trimmed.to_string();
        }
        if trimmed.starts_with("//") {
            return format!("https:{trimmed}");
        }
        if trimmed.starts_with('/') {
            return format!("{BASE_URL}{trimmed}");
        }
        format!("{BASE_URL}/{}", trimmed.trim_start_matches("./"))
    }

    fn html_headers(referer: &str) -> Vec<(String, String)> {
        vec![
            ("User-Agent".to_string(), FIREFOX_UA.to_string()),
            ("Accept".to_string(), HTML_ACCEPT.to_string()),
            ("Referer".to_string(), referer.to_string()),
        ]
    }

    // Page 1: {base}/, page N: {base}/{N}/
    fn build_archive_page_url(base: &str, page: u16) -> String {
        if page <= 1 {
            return base.trim_end_matches('/').to_string() + "/";
        }
        format!("{}/{page}/", base.trim_end_matches('/'))
    }

    // Words joined with hyphens for search slug: "big ass" → "big-ass"
    fn build_search_slug(query: &str) -> String {
        query
            .split_whitespace()
            .map(str::to_ascii_lowercase)
            .collect::<Vec<_>>()
            .join("-")
    }

    fn build_target_url(target: &Target, page: u16) -> String {
        match target {
            Target::Latest => {
                Self::build_archive_page_url(&format!("{BASE_URL}/videos"), page)
            }
            Target::Popular => {
                Self::build_archive_page_url(&format!("{BASE_URL}/most-viewed"), page)
            }
            Target::TopRated => {
                Self::build_archive_page_url(&format!("{BASE_URL}/top-rated"), page)
            }
            Target::Search(query) => Self::build_archive_page_url(
                &format!("{BASE_URL}/search/{}", Self::build_search_slug(query)),
                page,
            ),
            Target::Archive(url) => Self::build_archive_page_url(url, page),
        }
    }

    async fn fetch_html(requester: &mut Requester, url: &str, referer: &str) -> Result<String> {
        requester
            .get_with_headers(url, Self::html_headers(referer), Some(Version::HTTP_11))
            .await
            .map_err(|error| Error::from(format!("request failed for {url}: {error}")))
    }

    fn parse_duration(text: &str) -> u32 {
        parse_time_to_seconds(text)
            .and_then(|v| u32::try_from(v).ok())
            .unwrap_or(0)
    }

    fn parse_views(text: &str) -> Option<u32> {
        parse_abbreviated_number(text.trim())
    }

    fn parse_rating(text: &str) -> Option<f32> {
        let digits: String = text
            .chars()
            .filter(|c| c.is_ascii_digit() || *c == '.')
            .collect();
        digits.parse::<f32>().ok()
    }

    fn parse_list_videos(html: &str, options: &ServerOptions) -> Result<Vec<VideoItem>> {
        let document = Html::parse_document(html);

        let card_sel = Self::selector("div.th.item")?;
        let link_sel = Self::selector("a.th-description")?;
        let img_sel = Self::selector("img.lazy-load[data-original]")?;
        let duration_sel = Self::selector("span.th-length")?;
        let rating_sel = Self::selector("span.th-rating")?;
        let views_sel = Self::selector("span.th-views")?;
        let source_sel = Self::selector("a.th-source")?;
        let model_sel = Self::selector("div.th-models a[href*=\"/pornstar/\"]")?;

        let mut items = Vec::new();

        for card in document.select(&card_sel) {
            let Some(link) = card.select(&link_sel).next() else {
                continue;
            };
            let href = link.value().attr("href").unwrap_or_default();
            let page_url = Self::normalize_url(href);
            if page_url.is_empty() {
                continue;
            }

            let title = Self::text_of(&link);
            if title.is_empty() {
                continue;
            }

            let Some(img) = card.select(&img_sel).next() else {
                continue;
            };
            let data_original = img.value().attr("data-original").unwrap_or_default();
            let thumb = Self::normalize_url(data_original);

            // Video ID from /images/thumb/{id}.webp
            let id = data_original
                .trim_end_matches(".webp")
                .rsplit('/')
                .next()
                .filter(|s| !s.is_empty())
                .unwrap_or_default()
                .to_string();
            if id.is_empty() {
                continue;
            }

            let duration = card
                .select(&duration_sel)
                .next()
                .map(|el| Self::parse_duration(&Self::text_of(&el)))
                .unwrap_or(0);

            let rating = card
                .select(&rating_sel)
                .next()
                .and_then(|el| Self::parse_rating(&Self::text_of(&el)));

            let views = card
                .select(&views_sel)
                .next()
                .and_then(|el| Self::parse_views(&Self::text_of(&el)));

            let studio_el = card.select(&source_sel).next();
            let uploader_name = studio_el
                .map(|el| Self::text_of(&el))
                .filter(|s| !s.is_empty());
            let uploader_url = studio_el
                .and_then(|el| el.value().attr("href"))
                .map(|href| Self::normalize_url(href))
                .filter(|s| !s.is_empty());
            let uploader_id = uploader_url.as_deref().and_then(|url| {
                let slug = url.trim_end_matches('/').rsplit('/').next()?;
                if slug.is_empty() {
                    None
                } else {
                    Some(format!("{CHANNEL_ID}:{slug}"))
                }
            });

            // Pornstar names become tags
            let tags: Vec<String> = card
                .select(&model_sel)
                .map(|el| Self::text_of(&el))
                .filter(|s| !s.is_empty())
                .collect();

            let slug = page_url
                .trim_end_matches('/')
                .rsplit('/')
                .next()
                .unwrap_or("")
                .to_string();
            let proxy_url = build_proxy_url(options, CHANNEL_ID, &slug);
            let format = VideoFormat::new(proxy_url, "1080".to_string(), "mp4".to_string());

            let mut item = VideoItem::new(
                id,
                title,
                page_url,
                CHANNEL_ID.to_string(),
                thumb,
                duration,
            );
            item.formats = Some(vec![format]);
            item.rating = rating;
            item.views = views;
            item.uploader = uploader_name;
            item.uploaderUrl = uploader_url;
            item.uploaderId = uploader_id;
            if !tags.is_empty() {
                item.tags = Some(tags);
            }
            items.push(item);
        }

        Ok(items)
    }

    fn resolve_sort_target(sort: &str) -> Target {
        match sort.trim().to_ascii_lowercase().as_str() {
            "popular" | "viewed" | "most_viewed" => Target::Popular,
            "rated" | "rating" | "top" => Target::TopRated,
            _ => Target::Latest,
        }
    }

    fn resolve_option_target(options: &ServerOptions, sort: &str) -> Target {
        if let Some(cat) = options.categories.as_deref() {
            if cat.starts_with(BASE_URL) && cat != "all" {
                return Target::Archive(cat.to_string());
            }
        }
        Self::resolve_sort_target(sort)
    }

    fn resolve_query_target(query: &str) -> Target {
        let trimmed = query.trim().trim_start_matches('@');

        // Prefix shortcuts: tag:blonde, studio:rk-prime, pornstar:mia-molotov, category:anal
        if let Some((kind, value)) = trimmed.split_once(':') {
            let slug = value.trim().replace(' ', "-").trim_matches('/').to_lowercase();
            if !slug.is_empty() {
                match kind.trim().to_ascii_lowercase().as_str() {
                    "tag" => return Target::Archive(format!("{BASE_URL}/tag/{slug}")),
                    "category" | "cat" => {
                        return Target::Archive(format!("{BASE_URL}/category/{slug}"))
                    }
                    "studio" => return Target::Archive(format!("{BASE_URL}/studio/{slug}")),
                    "pornstar" | "star" | "model" => {
                        return Target::Archive(format!("{BASE_URL}/pornstar/{slug}"))
                    }
                    _ => {}
                }
            }
        }

        // Direct category name match
        let normalized = trimmed.to_ascii_lowercase();
        for (slug, name) in CATEGORIES {
            if name.to_ascii_lowercase() == normalized || slug.replace('-', " ") == normalized {
                return Target::Archive(format!("{BASE_URL}/category/{slug}"));
            }
        }

        Target::Search(trimmed.to_string())
    }

    async fn fetch_target(
        &self,
        cache: VideoCache,
        target: Target,
        page: u16,
        per_page_limit: usize,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let source_url = Self::build_target_url(&target, page);

        if let Some((time, items)) = cache.get(&source_url) {
            if time.elapsed().unwrap_or_default().as_secs() < 300 {
                return Ok(items.into_iter().take(per_page_limit.max(1)).collect());
            }
        }

        let mut requester =
            requester_or_default(&options, CHANNEL_ID, "thepornbunny.fetch_target");
        let html = match Self::fetch_html(&mut requester, &source_url, &source_url).await {
            Ok(v) => v,
            Err(error) => {
                report_provider_error(
                    CHANNEL_ID,
                    "fetch_target.request",
                    &format!("url={source_url}; error={error}"),
                )
                .await;
                return Ok(vec![]);
            }
        };

        if html.trim().is_empty() {
            report_provider_error(
                CHANNEL_ID,
                "fetch_target.empty",
                &format!("url={source_url}"),
            )
            .await;
            return Ok(vec![]);
        }

        let items = Self::parse_list_videos(&html, &options)?;
        if items.is_empty() {
            return Ok(vec![]);
        }

        let limited: Vec<VideoItem> = items.iter().cloned().take(per_page_limit.max(1)).collect();
        cache.remove(&source_url);
        cache.insert(source_url, items);
        Ok(limited)
    }
}

#[async_trait]
impl Provider for ThepornbunnyProvider {
    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::<u16>().unwrap_or(1).max(1);
        let per_page_limit = per_page.parse::<usize>().unwrap_or(10).clamp(1, 60);

        let target = match query {
            Some(ref q) if !q.trim().is_empty() => Self::resolve_query_target(q.trim()),
            _ => Self::resolve_option_target(&options, &sort),
        };

        match self
            .fetch_target(cache, target, page, per_page_limit, options)
            .await
        {
            Ok(items) => items,
            Err(error) => {
                report_provider_error(
                    CHANNEL_ID,
                    "get_videos",
                    &format!("sort={sort}; page={page}; error={error}"),
                )
                .await;
                vec![]
            }
        }
    }

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builds_latest_url_page1() {
        assert_eq!(
            ThepornbunnyProvider::build_target_url(&Target::Latest, 1),
            "https://www.thepornbunny.com/videos/"
        );
    }

    #[test]
    fn builds_latest_url_page2() {
        assert_eq!(
            ThepornbunnyProvider::build_target_url(&Target::Latest, 2),
            "https://www.thepornbunny.com/videos/2/"
        );
    }

    #[test]
    fn builds_search_url() {
        assert_eq!(
            ThepornbunnyProvider::build_target_url(&Target::Search("big tits".to_string()), 1),
            "https://www.thepornbunny.com/search/big-tits/"
        );
    }

    #[test]
    fn resolves_tag_prefix() {
        match ThepornbunnyProvider::resolve_query_target("tag:blonde") {
            Target::Archive(url) => assert_eq!(url, "https://www.thepornbunny.com/tag/blonde"),
            _ => panic!("expected archive target"),
        }
    }

    #[test]
    fn resolves_studio_prefix() {
        match ThepornbunnyProvider::resolve_query_target("studio:rk-prime") {
            Target::Archive(url) => assert_eq!(url, "https://www.thepornbunny.com/studio/rk-prime"),
            _ => panic!("expected archive target"),
        }
    }

    #[test]
    fn resolves_category_name() {
        match ThepornbunnyProvider::resolve_query_target("Anal") {
            Target::Archive(url) => assert_eq!(url, "https://www.thepornbunny.com/category/anal"),
            _ => panic!("expected archive target"),
        }
    }
}
