use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, report_provider_error_background, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::requester::Requester;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait;
use base64::{engine::general_purpose, Engine};
use chrono::{DateTime, NaiveDate, Utc};
use error_chain::error_chain;
use futures::stream::{self, StreamExt};
use htmlentity::entity::{ICodedDataTrait, decode};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use regex::Regex;
use scraper::{ElementRef, Html, Selector};
use std::sync::{Arc, RwLock};
use std::time::Duration as StdDuration;
use std::{thread, vec};
use tokio::time::timeout;
use url::Url;
use wreq::Version;

pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
    crate::providers::ProviderChannelMetadata {
        group_id: "thai",
        tags: &["thai", "asian", "amateur"],
    };

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

const BASE_URL: &str = "https://www.thaiporntv.com";
const CHANNEL_ID: &str = "thaiporntv";
const USER_AGENT: &str =
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
const HTML_ACCEPT: &str =
    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
const CDN_BASE: &str = "https://web.techvids.top";

#[derive(Debug, Clone)]
pub struct ThaipornTvProvider {
    url: String,
    tags: Arc<RwLock<Vec<FilterOption>>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ArchiveMode {
    Latest,
    Popular,
    Commented,
    Rated,
    Longest,
}

#[derive(Debug, Clone)]
enum Target {
    Archive(ArchiveMode),
    Tag {
        slug: String,
    },
}

impl ThaipornTvProvider {
    pub fn new() -> Self {
        let provider = Self {
            url: BASE_URL.to_string(),
            tags: Arc::new(RwLock::new(vec![FilterOption {
                id: "all".to_string(),
                title: "All".to_string(),
            }])),
        };
        provider.spawn_initial_load();
        provider
    }

    fn spawn_initial_load(&self) {
        let url = self.url.clone();
        let tags = Arc::clone(&self.tags);

        thread::spawn(move || {
            let runtime = match tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
            {
                Ok(runtime) => runtime,
                Err(error) => {
                    report_provider_error_background(
                        CHANNEL_ID,
                        "spawn_initial_load.runtime_build",
                        &error.to_string(),
                    );
                    return;
                }
            };

            runtime.block_on(async move {
                if let Err(error) = Self::load_tags(&url, Arc::clone(&tags)).await {
                    report_provider_error_background(
                        CHANNEL_ID,
                        "load_tags",
                        &error.to_string(),
                    );
                }
            });
        });
    }

    fn build_channel(&self, _clientversion: ClientVersion) -> Channel {
        let tags = self.tags.read().map(|value| value.clone()).unwrap_or_default();

        Channel {
            id: CHANNEL_ID.to_string(),
            name: "THAIPornTV".to_string(),
            description: "Free Thai Porn Videos & Asian Sex Tube.".to_string(),
            premium: false,
            favicon: "https://www.google.com/s2/favicons?sz=64&domain=thaiporntv.com".to_string(),
            status: "active".to_string(),
            categories: tags.iter().map(|value| value.title.clone()).collect(),
            options: vec![
                ChannelOption {
                    id: "sort".to_string(),
                    title: "Sort".to_string(),
                    description: "Sort the videos".to_string(),
                    systemImage: "list.number".to_string(),
                    colorName: "blue".to_string(),
                    options: vec![
                        FilterOption {
                            id: "new".to_string(),
                            title: "Most Recent".to_string(),
                        },
                        FilterOption {
                            id: "popular".to_string(),
                            title: "Most Viewed".to_string(),
                        },
                        FilterOption {
                            id: "commented".to_string(),
                            title: "Most Commented".to_string(),
                        },
                        FilterOption {
                            id: "rated".to_string(),
                            title: "Top Rated".to_string(),
                        },
                        FilterOption {
                            id: "longest".to_string(),
                            title: "Longest".to_string(),
                        },
                    ],
                    multiSelect: false,
                },
                ChannelOption {
                    id: "filter".to_string(),
                    title: "Tags".to_string(),
                    description: "Video tags.".to_string(),
                    systemImage: "tag.fill".to_string(),
                    colorName: "green".to_string(),
                    options: tags,
                    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 collapse_whitespace(text: &str) -> String {
        text.split_whitespace().collect::<Vec<_>>().join(" ")
    }

    fn decode_html_entities(text: &str) -> String {
        decode(text.as_bytes())
            .to_string()
            .unwrap_or_else(|_| text.to_string())
            .replace('\u{a0}', " ")
            .trim()
            .to_string()
    }

    fn absolute_url(&self, value: &str) -> String {
        if value.starts_with("http://") || value.starts_with("https://") {
            return value.to_string();
        }
        format!(
            "{}/{}",
            self.url.trim_end_matches('/'),
            value.trim_start_matches('/')
        )
    }

    fn html_headers(&self, referer: &str) -> Vec<(String, String)> {
        vec![
            ("Referer".to_string(), referer.to_string()),
            ("User-Agent".to_string(), USER_AGENT.to_string()),
            ("Accept".to_string(), HTML_ACCEPT.to_string()),
            ("Accept-Language".to_string(), "en-US,en;q=0.9".to_string()),
        ]
    }

    async fn fetch_html(
        &self,
        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 push_unique(target: &Arc<RwLock<Vec<FilterOption>>>, item: FilterOption) {
        if item.id.is_empty() || item.title.is_empty() {
            return;
        }
        if let Ok(mut values) = target.write() {
            if !values
                .iter()
                .any(|existing| existing.id == item.id || existing.title == item.title)
            {
                values.push(item);
            }
        }
    }

    async fn load_tags(base_url: &str, tags: Arc<RwLock<Vec<FilterOption>>>) -> Result<()> {
        let mut requester = Requester::new();
        let provider = Self {
            url: base_url.to_string(),
            tags: Arc::clone(&tags),
        };
        let html = provider.fetch_html(
            &mut requester,
            &format!("{}/tags/", base_url),
            &format!("{}/", base_url),
        ).await?;
        let document = Html::parse_document(&html);
        // Tag cards are <a class="group block relative ..."> links with /tags/ in href
        // html5ever handles unquoted href attributes correctly
        let a_selector = Self::selector("a.group[href*='/tags/']")?;
        let h2_selector = Self::selector("h2")?;
        for element in document.select(&a_selector) {
            let Some(href) = element.value().attr("href") else {
                continue;
            };
            // Skip pagination and root tag page
            if href.ends_with("/tags/") || href.contains("/page/") {
                continue;
            }
            // Extract title from the h2 inside the card
            let title = element.select(&h2_selector).next()
                .map(|h| Self::collapse_whitespace(&h.text().collect::<String>()))
                .unwrap_or_default();
            if title.is_empty() {
                continue;
            }
            Self::push_unique(
                &tags,
                FilterOption {
                    id: href.to_string(),
                    title,
                },
            );
        }
        Ok(())
    }

    fn archive_from_sort(sort: &str) -> ArchiveMode {
        match sort {
            "popular" => ArchiveMode::Popular,
            "commented" => ArchiveMode::Commented,
            "rated" => ArchiveMode::Rated,
            "longest" => ArchiveMode::Longest,
            _ => ArchiveMode::Latest,
        }
    }

    fn resolve_target(&self, options: &ServerOptions, sort: &str, query: Option<&str>) -> Target {
        // Query: check for tag shortcut first
        if let Some(q) = query {
            if let Some(target) = self.find_tag_target_in_options(q) {
                return target;
            }
        }
        // Filter option: check for tag shortcut
        if let Some(value) = options.filter.as_deref() {
            if let Some(target) = self.find_tag_target_in_options(value) {
                return target;
            }
        }
        Target::Archive(Self::archive_from_sort(sort))
    }

    fn find_tag_target_in_options(&self, value: &str) -> Option<Target> {
        let normalized = value.trim().to_lowercase();
        let tags = self.tags.read().ok()?;
        let option = tags.iter().find(|item| {
            item.id.eq_ignore_ascii_case(value) || item.title.trim().to_lowercase() == normalized
        })?;
        self.target_from_filter_id(&option.id)
    }

    fn target_from_filter_id(&self, id: &str) -> Option<Target> {
        if id.contains("/tags/") {
            let url = Url::parse(&self.absolute_url(id)).ok()?;
            let segments: Vec<_> = url
                .path_segments()?
                .filter(|s| !s.is_empty())
                .collect();
            let slug = segments.last()?.to_string();
            if slug == "tags" {
                return None;
            }
            return Some(Target::Tag { slug });
        }
        None
    }

    fn build_url_for_target(&self, target: &Target, page: u32) -> String {
        match target {
            Target::Archive(mode) => self.build_archive_url(*mode, page),
            Target::Tag { slug } => self.build_tag_url(slug, page),
        }
    }

    fn build_archive_url(&self, mode: ArchiveMode, page: u32) -> String {
        let base_path = match mode {
            ArchiveMode::Latest => "recent",
            ArchiveMode::Popular => "popular",
            ArchiveMode::Commented => "commented",
            ArchiveMode::Rated => "rated",
            ArchiveMode::Longest => "longest",
        };
        if page <= 1 {
            format!("{}/{}/", self.url, base_path)
        } else {
            format!("{}/{}/page/{}/", self.url, base_path, page)
        }
    }

    fn build_tag_url(&self, slug: &str, page: u32) -> String {
        let encoded_slug = utf8_percent_encode(slug, NON_ALPHANUMERIC).to_string();
        if page <= 1 {
            format!("{}/tags/{}/", self.url, encoded_slug)
        } else {
            format!("{}/tags/{}/page/{}/", self.url, encoded_slug, page)
        }
    }

    fn decode_data_enc(encoded_data: &str) -> Result<Vec<VideoFormat>> {
        let cleaned_data = encoded_data.replace("-", "+").replace("_", "/");
        let padding = (4 - cleaned_data.len() % 4) % 4;
        let padded_data = format!("{}{}", cleaned_data, "=".repeat(padding));

        let decoded_bytes = general_purpose::STANDARD.decode(&padded_data)
            .map_err(|e| Error::from(format!("Base64 decode failed: {e}")))?;

        if decoded_bytes.is_empty() {
            return Err(Error::from("Decoded bytes are empty"));
        }

        let key = decoded_bytes[0];
        let decrypted_bytes: Vec<u8> = decoded_bytes[1..]
            .iter()
            .map(|&b| b ^ key)
            .collect();

        let json_str = String::from_utf8(decrypted_bytes)
            .map_err(|e| Error::from(format!("UTF-8 decode failed: {e}")))?;

        let formats_json: serde_json::Value = serde_json::from_str(&json_str)
            .map_err(|e| Error::from(format!("JSON parse failed: {e}")))?;

        let Some(formats_array) = formats_json.as_array() else {
            return Err(Error::from("JSON is not an array"));
        };

        let mut formats = Vec::new();
        for format_val in formats_array {
            let Some(u) = format_val.get("u").and_then(|v| v.as_str()) else { continue; };
            let Some(q) = format_val.get("q").and_then(|v| v.as_str()) else { continue; };

            let mut format = VideoFormat::new(
                u.to_string(),
                q.to_string(),
                "application/x-mpegURL".to_string(),
            );
            format.add_http_header("Referer".to_string(), BASE_URL.to_string());
            formats.push(format);
        }

        Ok(formats)
    }

    fn parse_card(&self, card: ElementRef<'_>) -> Option<VideoItem> {
        // Selectors for the Tailwind-based redesign
        let playthumb_sel = Self::selector("a.playthumb").ok()?;
        let img_sel = Self::selector("img").ok()?;
        let title_sel = Self::selector("a.text-brand-pink").ok()?;
        let tag_sel = Self::selector("a[href*='/tags/']").ok()?;
        let date_sel = Self::selector("span.ml-auto").ok()?;

        let link = card.select(&playthumb_sel).next()?;
        let href = link.value().attr("href")?;
        let data_id = link.value().attr("data-id").unwrap_or("");

        // ID: numeric part from data-id (xn88-39688 → 39688) or from URL
        let id = if !data_id.is_empty() {
            data_id.rsplit('-').next().unwrap_or(data_id).to_string()
        } else {
            let re = Regex::new(r"-(\d+)/$").unwrap();
            re.captures(href)?.get(1)?.as_str().to_string()
        };

        let url = if href.starts_with("http") {
            href.to_string()
        } else {
            self.absolute_url(href)
        };

        let thumb = card.select(&img_sel).next()
            .and_then(|e| e.value().attr("src"))
            .map(|s| if s.starts_with("http") { s.to_string() } else { self.absolute_url(s) })
            .unwrap_or_default();

        // Preview GIF from CDN
        let preview = if !data_id.is_empty() {
            Some(format!("{CDN_BASE}/2/4/7/9/preview/{data_id}_preview.gif"))
        } else {
            None
        };

        let title = card.select(&title_sel).next()
            .map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
            .filter(|t| !t.is_empty())
            .or_else(|| {
                link.value().attr("aria-label")
                    .map(|s| {
                        let s = s.strip_prefix("Watch ").unwrap_or(s);
                        let s = s.strip_suffix(" video").unwrap_or(s);
                        Self::decode_html_entities(s)
                    })
            })
            .unwrap_or_default();

        // Duration is in a font-mono div inside the thumbnail overlay
        let card_html = card.html();
        let dur_re = Regex::new(r"font-mono[^>]+>(\d+:\d+(?::\d+)?)<").unwrap();
        let duration_text = dur_re.captures(&card_html)
            .and_then(|c| c.get(1))
            .map(|m| m.as_str().to_string())
            .unwrap_or_default();
        let duration = parse_duration_mm_ss(&duration_text);

        // Views from the fa-eye span
        let views_re = Regex::new(r"fa-eye[^>]+></i>\s*(\d[\d,]*)").unwrap();
        let views = views_re.captures(&card_html)
            .and_then(|c| c.get(1))
            .and_then(|m| m.as_str().replace(',', "").parse::<u32>().ok());

        // Upload date from the ml-auto span
        let uploaded_at = card.select(&date_sel).next()
            .map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
            .and_then(|s| NaiveDate::parse_from_str(s.trim(), "%d %b %Y").ok())
            .and_then(|date| {
                date.and_hms_opt(0, 0, 0)
                    .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc).timestamp() as u64)
            });

        // Tags from /tags/ links in the card (these are simple text-only links in cards)
        let tags: Vec<String> = card.select(&tag_sel)
            .map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
            .filter(|s| !s.is_empty())
            .collect();

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

        if let Some(views) = views { item = item.views(views); }
        if let Some(uploaded_at) = uploaded_at { item = item.uploaded_at(uploaded_at); }
        if let Some(preview) = preview { item = item.preview(preview); }
        if !tags.is_empty() { item = item.tags(tags); }

        Some(item)
    }

    fn get_video_items_from_html(&self, html: String) -> Result<Vec<VideoItem>> {
        let document = Html::parse_document(&html);
        // Cards use class "group flex flex-col"; ad cards additionally have "ad-container"
        let card_selector = Self::selector("div.group:not(.ad-container)")?;

        let mut items = Vec::new();
        for card in document.select(&card_selector) {
            if let Some(item) = self.parse_card(card) {
                items.push(item);
            }
        }
        Ok(items)
    }

    async fn enrich_video(&self, mut item: VideoItem, options: &ServerOptions) -> Result<VideoItem> {
        let mut requester = requester_or_default(options, CHANNEL_ID, "enrich_video");
        let detail_fetch = timeout(
            StdDuration::from_secs(6),
            self.fetch_html(&mut requester, &item.url, &item.url),
        )
        .await
        .map_err(|_| Error::from(format!("detail request timed out for {}", item.url)))??;

        let document = Html::parse_document(&detail_fetch);
        let video_element_selector = Self::selector("video[data-enc]")?;

        if let Some(video_element) = document.select(&video_element_selector).next() {
            if let Some(data_enc) = video_element.value().attr("data-enc") {
                match Self::decode_data_enc(data_enc) {
                    Ok(formats) => {
                        item.formats = Some(formats);
                    },
                    Err(e) => {
                        report_provider_error_background(
                            CHANNEL_ID,
                            "decode_data_enc",
                            &format!("url={}; error={}", item.url, e),
                        );
                    }
                }
            }
        }

        Ok(item)
    }

    async fn fetch_items_for_url(
        &self,
        cache: VideoCache,
        url: String,
        per_page_limit: usize,
        enrich_details: bool,
        options: &ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        if let Some((time, items)) = cache.get(&url) {
            if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
                return Ok(items.clone());
            }
        }

        let mut requester = requester_or_default(options, CHANNEL_ID, "fetch_items_for_url");
        let html = timeout(
            StdDuration::from_secs(10),
            self.fetch_html(&mut requester, &url, &url),
        )
        .await
        .map_err(|_| Error::from(format!("list request timed out for {url}")))??;

        let list_items = self.get_video_items_from_html(html)?;

        if list_items.is_empty() {
            return Ok(vec![]);
        }

        let limited_items = list_items
            .into_iter()
            .take(per_page_limit.max(1))
            .collect::<Vec<_>>();

        if !enrich_details {
            cache.insert(url, limited_items.clone());
            return Ok(limited_items);
        }

        let items = stream::iter(limited_items.into_iter().map(|item| {
            let provider = self.clone();
            let options = options.clone();
            async move { provider.enrich_video(item, &options).await }
        }))
        .buffer_unordered(4)
        .collect::<Vec<_>>()
        .await;

        let mut final_items = Vec::new();
        for item_result in items {
            match item_result {
                Ok(item) => final_items.push(item),
                Err(e) => report_provider_error_background(CHANNEL_ID, "enrich_video_stream", &e.to_string()),
            }
        }

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

        Ok(final_items)
    }

    async fn get(
        &self,
        cache: VideoCache,
        page: u32,
        sort: &str,
        query: Option<&str>,
        per_page_limit: usize,
        options: ServerOptions,
    ) -> Result<Vec<VideoItem>> {
        let target = self.resolve_target(&options, sort, query);
        let url = self.build_url_for_target(&target, page);
        self.fetch_items_for_url(cache, url, per_page_limit, page <= 1, &options)
            .await
    }
}

/// Parse "MM:SS" or "HH:MM:SS" into total seconds.
fn parse_duration_mm_ss(text: &str) -> u32 {
    let parts: Vec<u32> = text
        .split(':')
        .filter_map(|p| p.trim().parse().ok())
        .collect();
    match parts.as_slice() {
        [h, m, s] => h * 3600 + m * 60 + s,
        [m, s] => m * 60 + s,
        [s] => *s,
        _ => 0,
    }
}

#[async_trait]
impl Provider for ThaipornTvProvider {
    async fn get_videos(
        &self,
        cache: VideoCache,
        pool: DbPool,
        sort: String,
        query: Option<String>,
        page: String,
        per_page: String,
        options: ServerOptions,
    ) -> Vec<VideoItem> {
        let _ = pool;
        let page = page.parse::<u32>().unwrap_or(1);
        let per_page_limit = per_page.parse::<usize>().unwrap_or(30);
        let query_ref = query.as_deref().filter(|q| !q.trim().is_empty());

        let result = self.get(cache, page, &sort, query_ref, per_page_limit, options).await;

        match result {
            Ok(videos) => videos,
            Err(error) => {
                report_provider_error(CHANNEL_ID, "get_videos", &error.to_string()).await;
                vec![]
            }
        }
    }

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::util::cache::VideoCache;
    use crate::util::requester::Requester;

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

    #[test]
    fn builds_archive_urls() {
        let provider = provider();
        assert_eq!(
            provider.build_archive_url(ArchiveMode::Latest, 1),
            "https://www.thaiporntv.com/recent/"
        );
        assert_eq!(
            provider.build_archive_url(ArchiveMode::Latest, 2),
            "https://www.thaiporntv.com/recent/page/2/"
        );
        assert_eq!(
            provider.build_archive_url(ArchiveMode::Popular, 1),
            "https://www.thaiporntv.com/popular/"
        );
        assert_eq!(
            provider.build_archive_url(ArchiveMode::Popular, 3),
            "https://www.thaiporntv.com/popular/page/3/"
        );
    }

    #[test]
    fn builds_tag_urls() {
        let provider = provider();
        assert_eq!(
            provider.build_tag_url("blowjob", 1),
            "https://www.thaiporntv.com/tags/blowjob/"
        );
        assert_eq!(
            provider.build_tag_url("thai-massage", 2),
            "https://www.thaiporntv.com/tags/thai%2Dmassage/page/2/"
        );
    }

    #[test]
    fn decodes_data_enc_correctly() {
        let encoded = "C1BwKX4pMSljf397eDEkJHxuaSV_bmhjfWJveCV_ZHskZjh-MyQ6PT4zVD8zO3slZjh-MyknKXopMSk_Mzt7KScpYykxbWpneG52Vg";
        let formats = ThaipornTvProvider::decode_data_enc(encoded).unwrap();
        assert_eq!(formats.len(), 1);
        assert_eq!(formats[0].url, "https://web.techvids.top/m3u8/1658_480p.m3u8");
        // Verify format fields via JSON serialization (quality and http_headers are private)
        let json = serde_json::to_value(&formats[0]).unwrap();
        assert_eq!(json["quality"], "480p");
        assert_eq!(json["http_headers"]["Referer"], BASE_URL);
    }

    #[test]
    fn parses_duration() {
        assert_eq!(parse_duration_mm_ss("50:47"), 3047);
        assert_eq!(parse_duration_mm_ss("1:05:30"), 3930);
        assert_eq!(parse_duration_mm_ss("12:04"), 724);
    }

    #[tokio::test]
    #[ignore]
    async fn fetches_and_parses_archive() {
        let provider = provider();
        let options = ServerOptions {
            client_version: None,
            featured: None,
            category: None,
            sites: None,
            filter: None,
            language: None,
            public_url_base: Some("http://127.0.0.1:18080".to_string()),
            requester: Some(Requester::new()),
            network: None,
            stars: None,
            categories: None,
            duration: None,
            sort: Some("new".to_string()),
            sexuality: None,
        };
        let videos = provider.get(VideoCache::new(), 1, "new", None, 10, options).await.unwrap();
        assert!(!videos.is_empty());
    }
}
