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::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};

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

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

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

#[derive(Debug, Clone)]
pub struct Tube8Provider {
    url: String,
}

#[derive(Debug, Clone)]
enum Target {
    Latest,
    MostViewed,
    TopRated,
    Search { query: String },
    Tag { slug: String },
    Category { slug: String },
    Channel { slug: String },
    Pornstar { slug: String },
}

impl Tube8Provider {
    pub fn new() -> Self {
        Self {
            url: BASE_URL.to_string(),
        }
    }

    fn build_channel(&self, _cv: ClientVersion) -> Channel {
        Channel {
            id: CHANNEL_ID.to_string(),
            name: "Tube8".to_string(),
            description:
                "Tube8 mainstream tube with latest, most-viewed, top-rated, search, and tag/channel/pornstar shortcuts. Playback uses a signed HLS proxy."
                    .to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube8.com".to_string(),
            status: "active".to_string(),
            categories: vec![],
            options: vec![ChannelOption {
                id: "sort".to_string(),
                title: "Sort".to_string(),
                description: "Browse Tube8 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: "rated".to_string(),
                        title: "Top Rated".to_string(),
                    },
                ],
                multiSelect: false,
            }],
            nsfw: true,
            cacheDuration: Some(1800),
            ytdlpCommand: Some("yt-dlp --impersonate chrome-120".to_string())
        }
    }

    fn selector(value: &str) -> Option<Selector> {
        Selector::parse(value).ok()
    }

    fn normalize_text(value: &str) -> String {
        decode(value.as_bytes())
            .to_string()
            .unwrap_or_else(|_| value.to_string())
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .trim()
            .to_string()
    }

    fn normalize_url(&self, value: &str) -> String {
        let trimmed = value.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}");
        }
        format!(
            "{}/{}",
            self.url.trim_end_matches('/'),
            trimmed.trim_start_matches('/')
        )
    }

    fn html_headers() -> Vec<(String, String)> {
        vec![
            (
                "User-Agent".to_string(),
                "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
                    .to_string(),
            ),
            (
                "Accept".to_string(),
                "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
                    .to_string(),
            ),
            ("Accept-Language".to_string(), "en-US,en;q=0.5".to_string()),
            ("Referer".to_string(), format!("{BASE_URL}/")),
        ]
    }

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

            for (prefix, kind) in [
                ("tag:", "tag"),
                ("category:", "category"),
                ("cat:", "category"),
                ("channel:", "channel"),
                ("pornstar:", "pornstar"),
                ("model:", "pornstar"),
            ] {
                if let Some(rest) = lower.strip_prefix(prefix) {
                    let slug = rest.trim().replace(' ', "-");
                    if !slug.is_empty() {
                        return match kind {
                            "tag" => Target::Tag { slug },
                            "category" => Target::Category { slug },
                            "channel" => Target::Channel { slug },
                            _ => Target::Pornstar { slug },
                        };
                    }
                }
            }

            return Target::Search { query: q.to_string() };
        }

        match sort.trim().to_ascii_lowercase().as_str() {
            "popular" | "viewed" | "most_viewed" | "mv" => Target::MostViewed,
            "rated" | "top" | "top_rated" | "tr" => Target::TopRated,
            _ => Target::Latest,
        }
    }

    fn build_url(&self, target: &Target, page: u16) -> String {
        match target {
            Target::Latest => {
                // Page 1 is the home page; page 2+ use /newest/page/N/
                if page > 1 {
                    format!("{}/newest/page/{page}/", self.url)
                } else {
                    format!("{}/", self.url)
                }
            }
            Target::MostViewed => {
                // Page 1: /mostviewed.html; page 2+: /most-viewed/page/N/
                if page > 1 {
                    format!("{}/most-viewed/page/{page}/", self.url)
                } else {
                    format!("{}/mostviewed.html/", self.url)
                }
            }
            Target::TopRated => {
                // Page 1: /top.html; page 2+: /top/page/N/
                if page > 1 {
                    format!("{}/top/page/{page}/", self.url)
                } else {
                    format!("{}/top.html/", self.url)
                }
            }
            Target::Search { query } => {
                let encoded: String =
                    form_urlencoded::byte_serialize(query.as_bytes()).collect();
                if page > 1 {
                    format!("{}/searches.html/?q={encoded}&page={page}", self.url)
                } else {
                    format!("{}/searches.html/?q={encoded}", self.url)
                }
            }
            Target::Tag { slug } => {
                if page > 1 {
                    format!("{}/porntags/{slug}/?page={page}", self.url)
                } else {
                    format!("{}/porntags/{slug}/", self.url)
                }
            }
            Target::Category { slug } => {
                if page > 1 {
                    format!("{}/cat/{slug}/?page={page}", self.url)
                } else {
                    format!("{}/cat/{slug}/", self.url)
                }
            }
            Target::Channel { slug } => {
                if page > 1 {
                    format!("{}/channel/{slug}/?page={page}", self.url)
                } else {
                    format!("{}/channel/{slug}/", self.url)
                }
            }
            Target::Pornstar { slug } => {
                if page > 1 {
                    format!("{}/pornstar/{slug}/?page={page}", self.url)
                } else {
                    format!("{}/pornstar/{slug}/", self.url)
                }
            }
        }
    }

    fn text_of(node: Option<ElementRef<'_>>) -> String {
        node.map(|v| Self::normalize_text(&v.text().collect::<String>()))
            .unwrap_or_default()
    }

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

        let Some(card_sel) = Self::selector("article.video-box.js_video-box") else {
            return vec![];
        };
        let link_sel = Self::selector("a[data-testid='plw_video_thumbnail_link']");
        let title_sel = Self::selector("a.video-title-text span");
        let thumb_sel = Self::selector("img.thumb-image");
        let duration_sel = Self::selector(".tm_video_duration span");
        let views_sel = Self::selector("span.info-views");
        let uploader_sel = Self::selector("a.author-title-text");
        let performer_sel = Self::selector("a.channel-performer");

        let mut items = Vec::new();

        for card in document.select(&card_sel) {
            let id = card
                .value()
                .attr("data-video-id")
                .map(|v| v.to_string())
                .filter(|v| !v.is_empty())
                .unwrap_or_default();
            if id.is_empty() {
                continue;
            }

            // Title - prefer aria-label on article; fall back to title link span
            let title = card
                .value()
                .attr("aria-label")
                .map(Self::normalize_text)
                .filter(|v| !v.is_empty())
                .or_else(|| {
                    title_sel
                        .as_ref()
                        .and_then(|s| card.select(s).next())
                        .map(|v| Self::normalize_text(&v.text().collect::<String>()))
                        .filter(|v| !v.is_empty())
                })
                .unwrap_or_default();
            if title.is_empty() {
                continue;
            }

            // Thumbnail
            let thumb = thumb_sel
                .as_ref()
                .and_then(|s| card.select(s).next())
                .and_then(|v| {
                    v.value()
                        .attr("data-src")
                        .or_else(|| v.value().attr("data-poster"))
                        .or_else(|| v.value().attr("src"))
                })
                .map(|v| self.normalize_url(v))
                .unwrap_or_default();

            // Preview flipbook clip from the thumbnail link's data-mediabook
            let preview = link_sel
                .as_ref()
                .and_then(|s| card.select(s).next())
                .and_then(|v| v.value().attr("data-mediabook"))
                .map(|v| v.replace("&amp;", "&"))
                .filter(|v| !v.is_empty() && !v.starts_with("data:"));

            // Duration
            let duration_text =
                Self::text_of(duration_sel.as_ref().and_then(|s| card.select(s).next()));
            let duration = parse_time_to_seconds(&duration_text).unwrap_or(0) as u32;

            // Views (first span.info-views) and rating (second span.info-views, contains "XX%")
            let all_views: Vec<_> = views_sel
                .as_ref()
                .map(|s| card.select(s).collect())
                .unwrap_or_default();
            let views = all_views
                .first()
                .map(|v| Self::normalize_text(&v.text().collect::<String>()))
                .and_then(|t| parse_abbreviated_number(&t))
                .map(|v| v as u32);
            let rating = all_views
                .get(1)
                .map(|v| {
                    Self::normalize_text(&v.text().collect::<String>()).replace('%', "")
                })
                .and_then(|v| v.parse::<f32>().ok());

            // Uploader name from article data attribute (most reliable); href from link
            let uploader_name = card
                .value()
                .attr("data-uploader-name")
                .map(Self::normalize_text)
                .filter(|v| !v.is_empty())
                .or_else(|| {
                    uploader_sel
                        .as_ref()
                        .and_then(|s| card.select(s).next())
                        .map(|v| Self::normalize_text(&v.text().collect::<String>()))
                        .filter(|v| !v.is_empty())
                });
            let uploader_href = uploader_sel
                .as_ref()
                .and_then(|s| card.select(s).next())
                .and_then(|v| v.value().attr("href"))
                .map(|v| self.normalize_url(v));
            // Namespaced uploader ID from article attribute
            let uploader_id = card
                .value()
                .attr("data-uploader-id")
                .filter(|v| !v.is_empty())
                .map(|v| format!("{CHANNEL_ID}:{v}"));

            // Performer tags from channel-performer links
            let mut tags: Vec<String> = Vec::new();
            if let Some(sel) = &performer_sel {
                for p in card.select(sel) {
                    let t = Self::normalize_text(&p.text().collect::<String>());
                    if !t.is_empty()
                        && !tags.iter().any(|x: &String| x.eq_ignore_ascii_case(&t))
                    {
                        tags.push(t);
                    }
                }
            }

            // Proxy URL resolves signed HLS via our redirect proxy
            let proxy_url = build_proxy_url(options, CHANNEL_ID, &id);

            let mut item = VideoItem::new(
                id.clone(),
                title,
                format!("https://www.tube8.com/porn-video/{id}/"),
                CHANNEL_ID.to_string(),
                thumb,
                duration,
            );
            item.views = views;
            if let Some(r) = rating {
                item = item.rating(r);
            }
            if let Some(name) = uploader_name {
                item = item.uploader(name);
            }
            if let Some(url) = uploader_href {
                item.uploaderUrl = Some(url);
            }
            if let Some(uid) = uploader_id {
                item.uploaderId = Some(uid);
            }
            if let Some(p) = preview {
                item = item.preview(p);
            }
            if !tags.is_empty() {
                item = item.tags(tags);
            }
            item = item.formats(vec![
                VideoFormat::m3u8(proxy_url, "auto".to_string(), "tube8".to_string())
                .ext("mp4".to_string())
                .protocol("m3u8_native".to_string())
                .video_ext("mp4".to_string())
            ]);

            items.push(item);
        }

        items
    }
}

#[async_trait]
impl Provider for Tube8Provider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        _db_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 target = Self::target_from_request(query.as_deref(), &sort);
        let url = self.build_url(&target, page);

        let old_items = match cache.get(&url) {
            Some((time, items)) if time.elapsed().unwrap_or_default().as_secs() < 300 => {
                return items.clone();
            }
            Some((_, items)) => items.clone(),
            None => vec![],
        };

        let mut requester = requester_or_default(&options, CHANNEL_ID, "get_videos");
        let text = match requester
            .get_with_headers(&url, Self::html_headers(), None)
            .await
        {
            Ok(v) => v,
            Err(e) => {
                report_provider_error(
                    CHANNEL_ID,
                    "get_videos.request",
                    &format!("url={url}; error={e}"),
                )
                .await;
                return old_items;
            }
        };

        let items = self.parse_items(&text, &options);
        if items.is_empty() {
            return old_items;
        }

        cache.remove(&url);
        cache.insert(url, items.clone());
        items
    }

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

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

    fn provider() -> Tube8Provider {
        Tube8Provider::new()
    }

    #[test]
    fn resolves_sort_targets() {
        assert!(matches!(
            Tube8Provider::target_from_request(None, "new"),
            Target::Latest
        ));
        assert!(matches!(
            Tube8Provider::target_from_request(None, "popular"),
            Target::MostViewed
        ));
        assert!(matches!(
            Tube8Provider::target_from_request(None, "rated"),
            Target::TopRated
        ));
    }

    #[test]
    fn resolves_prefix_shortcuts() {
        let p = provider();
        let _ = p;
        assert!(matches!(
            Tube8Provider::target_from_request(Some("tag:lesbian"), "new"),
            Target::Tag { slug } if slug == "lesbian"
        ));
        assert!(matches!(
            Tube8Provider::target_from_request(Some("channel:brazzers"), "new"),
            Target::Channel { slug } if slug == "brazzers"
        ));
        assert!(matches!(
            Tube8Provider::target_from_request(Some("pornstar:mia khalifa"), "new"),
            Target::Pornstar { slug } if slug == "mia-khalifa"
        ));
        assert!(matches!(
            Tube8Provider::target_from_request(Some("cat:teens"), "new"),
            Target::Category { slug } if slug == "teens"
        ));
    }

    #[test]
    fn builds_latest_pages() {
        let p = provider();
        assert_eq!(p.build_url(&Target::Latest, 1), "https://www.tube8.com/");
        assert_eq!(
            p.build_url(&Target::Latest, 2),
            "https://www.tube8.com/newest/page/2/"
        );
    }

    #[test]
    fn builds_search_pages() {
        let p = provider();
        let t = Target::Search { query: "teen creampie".to_string() };
        assert_eq!(
            p.build_url(&t, 1),
            "https://www.tube8.com/searches.html/?q=teen+creampie"
        );
        assert_eq!(
            p.build_url(&t, 2),
            "https://www.tube8.com/searches.html/?q=teen+creampie&page=2"
        );
    }

    #[test]
    fn parses_video_cards() {
        let p = provider();
        let html = r#"
        <article class="video-box pc js_video-box js-pop"
             data-video-id="12345"
             data-uploader-id="999"
             data-uploader-name="TestChannel"
             aria-label="Test Video Title">
            <a href="/porn-video/12345/" data-testid="plw_video_thumbnail_link"
               data-mediabook="https://ev-ph.t8cdn.com/videos/test_fb.mp4?validfrom=1&validto=2">
                <div class="thumb-image-container">
                    <img class="thumb-image js_lazy"
                         data-src="https://ei-ph.t8cdn.com/videos/test/thumb.jpg"
                         src="data:image/png;base64,xxx"/>
                </div>
                <div class="video-properties">
                    <div class="video-duration tm_video_duration"><span>08:30</span></div>
                </div>
            </a>
            <div class="thumb-info-wrapper">
                <a class="author-title-text" href="/channel/testchannel/">TestChannel</a>
                <a class="channel-performer" href="/pornstar/jane-doe/">Jane Doe</a>
                <span class='info-views'>12.3K</span>
                <span class='info-views'>87%</span>
            </div>
        </article>
        "#;

        let opts = ServerOptions {
            client_version: None,
            featured: None,
            category: None,
            sites: None,
            filter: None,
            language: None,
            public_url_base: None,
            requester: None,
            network: None,
            stars: None,
            categories: None,
            duration: None,
            sort: None,
            sexuality: None,
        };
        let items = p.parse_items(html, &opts);
        assert_eq!(items.len(), 1);
        let item = &items[0];
        assert_eq!(item.id, "12345");
        assert_eq!(item.title, "Test Video Title");
        assert_eq!(item.thumb, "https://ei-ph.t8cdn.com/videos/test/thumb.jpg");
        assert_eq!(item.duration, 510);
        assert_eq!(item.views, Some(12300));
        assert_eq!(item.rating, Some(87.0));
        assert_eq!(item.uploader.as_deref(), Some("TestChannel"));
        assert!(item
            .tags
            .as_ref()
            .is_some_and(|t| t.iter().any(|v| v == "Jane Doe")));
        assert!(item
            .preview
            .as_ref()
            .is_some_and(|p| p.contains("_fb.mp4")));
    }
}
