use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::parse_abbreviated_number;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoItem};

use async_trait::async_trait;
use error_chain::error_chain;
use htmlentity::entity::{ICodedDataTrait, decode};
use scraper::{ElementRef, Html, Selector};

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

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

const FIREFOX_UA: &str =
    "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.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)
        }
    }
}

// Static category list — xhamster has 600+ categories; this covers the mainstream ones
const CATEGORIES: &[(&str, &str)] = &[
    ("18-year-old", "18 Year Old"),
    ("amateur", "Amateur"),
    ("anal", "Anal"),
    ("asian", "Asian"),
    ("bbw", "BBW"),
    ("bdsm", "BDSM"),
    ("big-ass", "Big Ass"),
    ("big-tits", "Big Tits"),
    ("bisexual", "Bisexual"),
    ("blonde", "Blonde"),
    ("blowjob", "Blowjob"),
    ("bondage", "Bondage"),
    ("brunette", "Brunette"),
    ("creampie", "Creampie"),
    ("cumshot", "Cumshot"),
    ("ebony", "Ebony"),
    ("fetish", "Fetish"),
    ("gay", "Gay"),
    ("granny", "Granny"),
    ("hardcore", "Hardcore"),
    ("hentai", "Hentai"),
    ("homemade", "Homemade"),
    ("indian", "Indian"),
    ("interracial", "Interracial"),
    ("japanese", "Japanese"),
    ("latina", "Latina"),
    ("lesbian", "Lesbian"),
    ("massage", "Massage"),
    ("masturbation", "Masturbation"),
    ("mature", "Mature"),
    ("milf", "MILF"),
    ("old-young", "Old & Young"),
    ("orgasm", "Orgasm"),
    ("pov", "POV"),
    ("public", "Public"),
    ("russian", "Russian"),
    ("shemale", "Shemale"),
    ("small-tits", "Small Tits"),
    ("squirt", "Squirt"),
    ("teen", "Teen"),
    ("threesome", "Threesome"),
    ("toys", "Toys"),
    ("vintage", "Vintage"),
    ("webcam", "Webcam"),
];

#[derive(Debug, Clone)]
enum Target {
    Newest,
    MostViewed,
    Best,
    Search(String),
    Category(String),
    Channel(String),
}

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

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

    fn build_channel(&self, _cv: ClientVersion) -> Channel {
        let mut cat_options = vec![FilterOption {
            id: "all".to_string(),
            title: "All".to_string(),
        }];
        for (slug, label) in CATEGORIES {
            cat_options.push(FilterOption {
                id: slug.to_string(),
                title: label.to_string(),
            });
        }

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "xHamster".to_string(),
            description:
                "xHamster — free porn with newest, most viewed, category, channel, and search routing."
                    .to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=xhamster.com".to_string(),
            status: "active".to_string(),
            categories: CATEGORIES
                .iter()
                .map(|(_, label)| label.to_string())
                .collect(),
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Browse xHamster by sort order.".to_string(),
                    systemImage: "list.number".to_string(),
                    colorName: "blue".to_string(),
                    options: vec![
                        FilterOption {
                            id: "new".to_string(),
                            title: "Newest".to_string(),
                        },
                        FilterOption {
                            id: "popular".to_string(),
                            title: "Most Viewed".to_string(),
                        },
                        FilterOption {
                            id: "best".to_string(),
                            title: "Best".to_string(),
                        },
                    ],
                    multiSelect: false,
                },
                ChannelOption {
                    id: "categories".to_string(),
                    title: "Categories".to_string(),
                    description: "Browse an xHamster 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(|e| Error::from(format!("selector `{value}` parse failed: {e}")))
    }

    fn decode_html(text: &str) -> String {
        decode(text.as_bytes())
            .to_string()
            .unwrap_or_else(|_| text.to_string())
    }

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

    fn normalize_key(s: &str) -> String {
        s.trim()
            .replace(['-', '_'], " ")
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .to_ascii_lowercase()
    }

    fn target_url(target: &Target, page: u16) -> String {
        let base = match target {
            Target::Newest => format!("{BASE_URL}/newest"),
            Target::MostViewed => format!("{BASE_URL}/most-viewed"),
            Target::Best => format!("{BASE_URL}/best"),
            Target::Search(q) => {
                let encoded = q.trim().replace(' ', "+");
                format!("{BASE_URL}/search/{encoded}")
            }
            Target::Category(slug) => format!("{BASE_URL}/categories/{slug}"),
            Target::Channel(slug) => format!("{BASE_URL}/channels/{slug}"),
        };
        if page <= 1 {
            base
        } else {
            format!("{base}?page={page}")
        }
    }

    fn parse_views(text: &str) -> Option<u32> {
        let cleaned = text
            .replace("views", "")
            .replace("view", "")
            .replace([',', ' '], "");
        parse_abbreviated_number(cleaned.trim())
    }

    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()),
        ]
    }

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

        let card_sel = Self::selector("div[data-video-type=\"video\"]")?;
        let thumb_link_sel = Self::selector("a[data-role=\"thumb-link\"]")?;
        let img_sel = Self::selector("img[data-role=\"thumb-preview-img\"]")?;
        let dur_sel = Self::selector("div[data-role=\"video-duration\"]")?;
        let title_sel = Self::selector("a.video-thumb-info__name")?;
        let uploader_name_sel = Self::selector("a.video-uploader__name")?;
        let uploader_link_sel = Self::selector("a[data-role=\"video-uploader-link\"]")?;
        let views_sel = Self::selector("div.video-thumb-views")?;

        let mut items = Vec::new();

        for card in document.select(&card_sel) {
            let id = match card.value().attr("data-video-id") {
                Some(v) if !v.is_empty() => v.to_string(),
                _ => continue,
            };

            let thumb_link = match card.select(&thumb_link_sel).next() {
                Some(el) => el,
                None => continue,
            };
            let href = thumb_link.value().attr("href").unwrap_or_default();
            if href.is_empty() {
                continue;
            }
            let page_url = if href.starts_with("https://") {
                href.to_string()
            } else {
                format!("{BASE_URL}{href}")
            };

            let preview = thumb_link
                .value()
                .attr("data-previewvideo")
                .or_else(|| thumb_link.value().attr("data-previewvideo-fallback"))
                .map(str::to_string);

            // srcset holds a smaller 526x298 thumb; fall back to src for the large one
            let thumb = card
                .select(&img_sel)
                .next()
                .and_then(|el| {
                    el.value()
                        .attr("srcset")
                        .or_else(|| el.value().attr("src"))
                })
                .map(|v| {
                    // srcset may have descriptor suffix like " 1w"; take first whitespace token
                    v.split_whitespace().next().unwrap_or(v).to_string()
                })
                .unwrap_or_default();

            let duration = card
                .select(&dur_sel)
                .next()
                .map(|el| Self::text_of(&el))
                .and_then(|text| parse_time_to_seconds(&text))
                .and_then(|v| u32::try_from(v).ok())
                .unwrap_or(0);

            let title = card
                .select(&title_sel)
                .next()
                .and_then(|el| el.value().attr("title"))
                .map(Self::decode_html)
                .filter(|v| !v.trim().is_empty())
                .or_else(|| {
                    // fallback: aria-label on thumb link
                    thumb_link
                        .value()
                        .attr("aria-label")
                        .map(Self::decode_html)
                        .filter(|v| !v.is_empty())
                })
                .unwrap_or_default();
            if title.is_empty() {
                continue;
            }

            let uploader_name_el = card.select(&uploader_name_sel).next();
            let uploader_link_el = card.select(&uploader_link_sel).next();

            let uploader = uploader_name_el
                .as_ref()
                .map(|el| Self::decode_html(&Self::text_of(el)))
                .filter(|v| !v.is_empty());

            let uploader_url = uploader_link_el
                .and_then(|el| el.value().attr("href"))
                .map(|v| {
                    if v.starts_with("https://") {
                        v.to_string()
                    } else {
                        format!("{BASE_URL}{v}")
                    }
                })
                .filter(|v| !v.is_empty());

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

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

            item.views = views;
            item.preview = preview;
            item.uploader = uploader;
            item.uploaderUrl = uploader_url.clone();

            if let Some(url) = &uploader_url {
                let slug = url
                    .trim_end_matches('/')
                    .rsplit('/')
                    .next()
                    .unwrap_or_default();
                if !slug.is_empty() {
                    let kind = if url.contains("/channels/") {
                        "channel"
                    } else if url.contains("/pornstars/") {
                        "pornstar"
                    } else {
                        "creator"
                    };
                    item.uploaderId = Some(format!("{CHANNEL_ID}:{kind}:{slug}"));
                }
            }

            items.push(item);
        }

        Ok(items)
    }

    fn resolve_query_target(&self, query: &str) -> Target {
        let trimmed = query.trim();

        if let Some((kind, value)) = trimmed.split_once(':') {
            let slug = value.trim().replace(' ', "-").to_ascii_lowercase();
            if !slug.is_empty() {
                match kind.trim().to_ascii_lowercase().as_str() {
                    "cat" | "category" => return Target::Category(slug),
                    "channel" | "channels" => return Target::Channel(slug),
                    _ => {}
                }
            }
        }

        // Check static category list by label or slug
        let normalized = Self::normalize_key(trimmed);
        for (slug, label) in CATEGORIES {
            if Self::normalize_key(label) == normalized || Self::normalize_key(slug) == normalized {
                return Target::Category(slug.to_string());
            }
        }

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

    fn resolve_sort_target(sort: &str) -> Target {
        match sort.trim().to_ascii_lowercase().as_str() {
            "popular" | "viewed" | "most_viewed" | "mostviewed" => Target::MostViewed,
            "best" => Target::Best,
            _ => Target::Newest,
        }
    }

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

    async fn fetch_target(
        &self,
        cache: VideoCache,
        target: Target,
        page: u16,
        per_page: usize,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let url = Self::target_url(&target, page);
        let cache_key = format!("{url}#per={per_page}");

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

        let mut requester =
            requester_or_default(&options, CHANNEL_ID, "xhamster.fetch_target");

        let html = requester
            .get_with_headers(&url, Self::html_headers(&url), None)
            .await
            .map_err(|e| Error::from(format!("request failed for {url}: {e}")))?;

        if html.trim().is_empty() {
            return Err(Error::from(format!("empty response for {url}")));
        }

        let all = Self::parse_list_page(&html)?;
        let items: Vec<VideoItem> = all.into_iter().take(per_page.max(1)).collect();

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

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

        let target = match query {
            Some(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, options)
            .await
        {
            Ok(items) => items,
            Err(e) => {
                report_provider_error(CHANNEL_ID, "get_videos", &e.to_string()).await;
                vec![]
            }
        }
    }

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

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

    #[test]
    fn parses_target_url_pagination() {
        assert_eq!(
            XhamsterProvider::target_url(&Target::Newest, 1),
            "https://xhamster.com/newest"
        );
        assert_eq!(
            XhamsterProvider::target_url(&Target::Newest, 2),
            "https://xhamster.com/newest?page=2"
        );
        assert_eq!(
            XhamsterProvider::target_url(&Target::MostViewed, 3),
            "https://xhamster.com/most-viewed?page=3"
        );
        assert_eq!(
            XhamsterProvider::target_url(&Target::Search("big ass".to_string()), 1),
            "https://xhamster.com/search/big+ass"
        );
        assert_eq!(
            XhamsterProvider::target_url(&Target::Category("amateur".to_string()), 2),
            "https://xhamster.com/categories/amateur?page=2"
        );
        assert_eq!(
            XhamsterProvider::target_url(&Target::Channel("vip4k".to_string()), 1),
            "https://xhamster.com/channels/vip4k"
        );
    }

    #[test]
    fn resolves_category_by_label_and_slug() {
        let p = XhamsterProvider::new();
        assert!(matches!(
            p.resolve_query_target("amateur"),
            Target::Category(s) if s == "amateur"
        ));
        assert!(matches!(
            p.resolve_query_target("Big Ass"),
            Target::Category(s) if s == "big-ass"
        ));
        assert!(matches!(
            p.resolve_query_target("Old & Young"),
            Target::Category(s) if s == "old-young"
        ));
    }

    #[test]
    fn resolves_explicit_shortcuts() {
        let p = XhamsterProvider::new();
        assert!(matches!(
            p.resolve_query_target("cat:milf"),
            Target::Category(s) if s == "milf"
        ));
        assert!(matches!(
            p.resolve_query_target("channel:vip4k"),
            Target::Channel(s) if s == "vip4k"
        ));
    }

    #[test]
    fn falls_through_to_search() {
        let p = XhamsterProvider::new();
        assert!(matches!(
            p.resolve_query_target("some unknown query"),
            Target::Search(_)
        ));
    }

    #[test]
    fn parses_listing_card() {
        let p = XhamsterProvider::new();
        let html = r#"
        <html><body>
        <div data-video-type="video" data-video-id="12345678">
          <a data-role="thumb-link"
             href="https://xhamster.com/videos/test-video-xh12345"
             data-previewvideo="https://thumb-v1.xhcdn.com/a/abc/012/345/678/526x298.t.mp4"
             aria-label="Test Video Title">
            <img data-role="thumb-preview-img"
                 src="https://ic-vt-nss.xhcdn.com/a/abc/012/345/678/1280x720.1.jpg"
                 srcset="https://ic-vt-nss.xhcdn.com/a/xyz/012/345/678/526x298.1.jpg"/>
            <div data-role="video-duration">
              <div>10:30</div>
            </div>
          </a>
          <div class="container-f146a video-thumb-info">
            <a class="video-thumb-info__name" title="Test Video Title"
               href="https://xhamster.com/videos/test-video-xh12345">
              Test Video Title
            </a>
            <div class="video-thumb-uploader">
              <div class="video-uploader-data">
                <a data-role="video-uploader-link"
                   href="https://xhamster.com/channels/testchannel">TestChannel</a>
                <a class="video-uploader__name"
                   href="https://xhamster.com/channels/testchannel">TestChannel</a>
                <div class="video-thumb-views">1.2M views</div>
              </div>
            </div>
          </div>
        </div>
        </body></html>
        "#;

        let items = XhamsterProvider::parse_list_page(html).expect("parse should succeed");
        assert_eq!(items.len(), 1);
        let item = &items[0];
        assert_eq!(item.id, "12345678");
        assert_eq!(item.title, "Test Video Title");
        assert_eq!(
            item.url,
            "https://xhamster.com/videos/test-video-xh12345"
        );
        assert!(item.thumb.contains("526x298") || item.thumb.contains("1280x720"));
        assert_eq!(item.duration, 630);
        assert_eq!(item.views, Some(1200000));
        assert_eq!(item.uploader.as_deref(), Some("TestChannel"));
        assert_eq!(
            item.uploaderUrl.as_deref(),
            Some("https://xhamster.com/channels/testchannel")
        );
        assert_eq!(
            item.uploaderId.as_deref(),
            Some("xhamster:channel:testchannel")
        );
        assert_eq!(
            item.preview.as_deref(),
            Some("https://thumb-v1.xhcdn.com/a/abc/012/345/678/526x298.t.mp4")
        );
    }
}
