use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Instant;

use anyhow::Result;
use async_trait::async_trait;
use regex::Regex;
use reqwest::Client;
use scraper::{Html, Selector};
use tracing::debug;

use crate::subtitle::{
    MediaQuery, Provider, ResolvedSubtitle, ResolvedSubtitleFormat, ResolvedSubtitleHeader,
    SubtitleCandidate, SubtitlePayload, SubtitleProvider,
};
use crate::types::ProviderStatus;

const SUBHD_BASE_URL: &str = "https://subhd.tv";

pub struct SubhdProvider {
    client: Client,
    enabled: AtomicBool,
}

impl SubhdProvider {
    #[must_use]
    pub fn new() -> Self {
        Self {
            client: Client::builder()
                .user_agent(
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                     AppleWebKit/537.36 (KHTML, like Gecko) \
                     Chrome/131.0.0.0 Safari/537.36",
                )
                .build()
                .expect("failed to build reqwest client"),
            enabled: AtomicBool::new(true),
        }
    }

    fn build_search_keyword(query: &MediaQuery) -> String {
        let mut keyword = query.title.clone();

        if let Some(season) = query.season {
            if let Some(episode) = query.episode {
                keyword = format!("{keyword} S{season:02}E{episode:02}");
            } else {
                keyword = format!("{keyword} S{season:02}");
            }
        }

        keyword
    }

    async fn search_detail_url(&self, keyword: &str) -> Result<Option<String>> {
        let html = self
            .client
            .get(format!("{SUBHD_BASE_URL}/search/{keyword}"))
            .send()
            .await?
            .text()
            .await?;

        let document = Html::parse_document(&html);
        let count_re = Regex::new(r"共\s*(\d+)\s*条").expect("invalid regex");
        let full_text = document.root_element().text().collect::<String>();

        if let Some(caps) = count_re.captures(&full_text) {
            let count: i32 = caps[1].parse().unwrap_or(0);
            if count < 1 {
                return Ok(None);
            }
        }

        // Find the first film link via the rounded-start image's parent anchor
        let img_selector = Selector::parse("img.rounded-start").expect("invalid selector");

        if let Some(img) = document.select(&img_selector).next() {
            // Walk up to find the parent <a>
            for ancestor in img.ancestors() {
                if let Some(el) = ancestor.value().as_element()
                    && el.name() == "a"
                    && let Some(href) = el.attr("href")
                {
                    let url = if href.starts_with("http") {
                        href.to_string()
                    } else {
                        format!("{SUBHD_BASE_URL}{href}")
                    };

                    return Ok(Some(url));
                }
            }
        }

        Ok(None)
    }

    async fn parse_subtitle_list(&self, detail_url: &str) -> Result<Vec<SubtitleCandidate>> {
        let html = self.client.get(detail_url).send().await?.text().await?;

        let document = Html::parse_document(&html);
        let item_selector = Selector::parse(".pt-2").expect("invalid selector");
        let link_selector = Selector::parse("a.link-dark").expect("invalid selector");
        let type_selector = Selector::parse(".text-secondary").expect("invalid selector");

        let mut candidates = vec![];

        for (idx, item) in document.select(&item_selector).enumerate() {
            let Some(link) = item.select(&link_selector).next() else {
                continue;
            };

            let title = link.text().collect::<String>().trim().to_string();
            if title.is_empty() {
                continue;
            }

            let Some(href) = link.value().attr("href") else {
                continue;
            };
            let sub_url = if href.starts_with("http") {
                href.to_string()
            } else {
                format!("{SUBHD_BASE_URL}{href}")
            };

            // Language / type info
            let lang = item
                .select(&type_selector)
                .next()
                .map(|el| el.text().collect::<String>().trim().to_string())
                .unwrap_or_default();

            let is_bilingual =
                lang.contains("双语") || (lang.contains("简") && lang.contains("英"));

            let payload = SubtitlePayload {
                file_name: title,
                url: sub_url,
            };

            candidates.push(SubtitleCandidate::new(
                "subhd".to_string(),
                lang,
                is_bilingual,
                payload,
                (10 - idx as i32).max(0),
            ));
        }

        Ok(candidates)
    }
}

impl Default for SubhdProvider {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl Provider for SubhdProvider {
    fn key(&self) -> &'static str {
        "subhd"
    }

    fn name(&self) -> &'static str {
        "SubHD"
    }

    async fn health_check(&self) -> ProviderStatus {
        if !self.is_enabled() {
            return ProviderStatus::Unhealthy;
        }

        match self.check_alive().await {
            Ok(latency_ms) => ProviderStatus::Healthy { latency_ms },
            Err(_) => ProviderStatus::Unhealthy,
        }
    }
}

#[async_trait]
impl SubtitleProvider for SubhdProvider {
    fn is_enabled(&self) -> bool {
        self.enabled.load(Ordering::Relaxed)
    }

    fn download_client(&self) -> &Client {
        &self.client
    }

    fn over_daily_limit(&self) -> bool {
        false
    }

    async fn check_alive(&self) -> Result<u64> {
        let start = Instant::now();
        self.client.head(SUBHD_BASE_URL).send().await?;

        Ok(start.elapsed().as_millis() as u64)
    }

    async fn search(&self, query: &MediaQuery) -> Result<Vec<SubtitleCandidate>> {
        let keyword = Self::build_search_keyword(query);

        let Some(detail_url) = self.search_detail_url(&keyword).await? else {
            debug!("no results for '{keyword}'");

            return Ok(vec![]);
        };

        self.parse_subtitle_list(&detail_url).await
    }

    async fn resolve(&self, candidate: &SubtitleCandidate) -> Result<ResolvedSubtitle> {
        let format = ResolvedSubtitleFormat::from_file_name(&candidate.payload.file_name);

        Ok(ResolvedSubtitle {
            provider: self.key().to_string(),
            header: ResolvedSubtitleHeader {
                file_name: candidate.payload.file_name.clone(),
                format,
                language: candidate.language.clone(),
                is_bilingual: candidate.is_bilingual,
            },
            download_url: candidate.payload.url.clone(),
        })
    }
}

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

    fn movie_query(title: &str) -> MediaQuery {
        MediaQuery {
            title: title.to_string(),
            season: None,
            episode: None,
            file_path: String::new(),
        }
    }

    fn series_query(title: &str, season: i32, episode: i32) -> MediaQuery {
        MediaQuery {
            title: title.to_string(),
            season: Some(season),
            episode: Some(episode),
            file_path: String::new(),
        }
    }

    #[tokio::test]
    async fn check_alive() {
        let provider = SubhdProvider::new();
        let latency = provider.check_alive().await;

        assert!(latency.is_ok(), "check_alive should succeed");
    }

    #[tokio::test]
    async fn search_movie_interstellar() {
        let provider = SubhdProvider::new();
        let query = movie_query("Interstellar");
        let results = provider.search(&query).await.unwrap();

        assert!(!results.is_empty(), "Interstellar should have subtitles");
        insta::assert_yaml_snapshot!(results);
    }

    #[tokio::test]
    async fn search_by_imdb_id() {
        let provider = SubhdProvider::new();
        let query = MediaQuery {
            title: "The Dark Knight".to_string(),
            season: None,
            episode: None,
            file_path: String::new(),
        };
        let results = provider.search(&query).await.unwrap();

        assert!(!results.is_empty(), "Dark Knight should have subtitles");
        insta::assert_yaml_snapshot!(results);
    }

    #[tokio::test]
    async fn search_series_game_of_thrones() {
        let provider = SubhdProvider::new();
        let query = series_query("Game of Thrones", 1, 1);
        let results = provider.search(&query).await.unwrap();

        assert!(!results.is_empty(), "Game of Thrones should have subtitles");
        insta::assert_yaml_snapshot!(results);
    }
}
