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

use anyhow::{Result, bail};
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use tracing::{info, warn};

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

const ASSRT_API_URL: &str = "https://api.assrt.net/v1";

pub struct AssrtProvider {
    client: Client,
    token: String,
    enabled: AtomicBool,
    last_request: Mutex<Option<Instant>>,
}

// TODO: verify against actual API responses - remove unnecessary Options and
// #[serde(default)]
#[derive(Debug, Deserialize)]
struct AssrtSearchResponse {
    status: i32,
    sub: Option<AssrtSubList>,
}

#[derive(Debug, Deserialize)]
struct AssrtSubList {
    subs: Vec<AssrtSub>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct AssrtSub {
    id: i64,
    videoname: String,
    native_name: String,
    lang: Option<AssrtLang>,
    filelist: Option<Vec<AssrtFile>>,
}

#[derive(Debug, Deserialize)]
struct AssrtLang {
    desc: Option<String>,
}

#[derive(Debug, Deserialize)]
struct AssrtFile {
    url: String,
    fname: String,
}

// TODO: verify against actual API responses - remove unnecessary Options and
// #[serde(default)]
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct AssrtDetailResponse {
    status: i32,
    sub: Option<AssrtDetailSub>,
}

#[derive(Debug, Deserialize)]
struct AssrtDetailSub {
    subs: Vec<AssrtSubDetail>,
}

#[derive(Debug, Deserialize)]
struct AssrtSubDetail {
    #[serde(default)]
    filelist: Vec<AssrtFile>,
    lang: Option<AssrtLang>,
}

impl AssrtProvider {
    #[must_use]
    pub fn new(token: String) -> Self {
        Self {
            client: Client::new(),
            token,
            enabled: AtomicBool::new(true),
            last_request: Mutex::new(None),
        }
    }

    async fn api_get<T: serde::de::DeserializeOwned>(
        &self,
        path: &str,
        query: &[(&str, &str)],
    ) -> Result<T> {
        let mut params = vec![("token", self.token.as_str())];

        params.extend(query.iter().copied());

        Ok(self
            .client
            .get(format!("{ASSRT_API_URL}{path}"))
            .query(&params)
            .send()
            .await?
            .json()
            .await?)
    }

    /// Enforce 20-second rate limit
    async fn rate_limit(&self) {
        let wait = {
            let last = self.last_request.lock().unwrap();
            (*last).and_then(|last| {
                let elapsed = last.elapsed();

                if elapsed < Duration::from_secs(20) {
                    Some(Duration::from_secs(20) - elapsed)
                } else {
                    None
                }
            })
        };

        if let Some(wait) = wait {
            info!("rate limiting: waiting for {wait:?}");

            tokio::time::sleep(wait).await;
        }

        *self.last_request.lock().unwrap() = Some(Instant::now());
    }
}

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

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

    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 AssrtProvider {
    fn is_enabled(&self) -> bool {
        self.enabled.load(Ordering::Relaxed) && !self.token.is_empty()
    }

    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();
        let response = self.client.head(ASSRT_API_URL).send().await?;

        // The API returns various status codes depending on auth state, but any
        // response (including 403) proves the endpoint is reachable.
        if !response.status().is_success() && response.status() != 403 {
            bail!("unexpected status code: {}", response.status());
        }

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

    async fn search(&self, query: &MediaQuery) -> Result<Vec<SubtitleCandidate>> {
        self.rate_limit().await;

        let mut search_term = query.title.clone();

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

        let resp: AssrtSearchResponse = self.api_get("/sub/search", &[("q", &search_term)]).await?;

        if resp.status != 0 {
            warn!("search returned status {}", resp.status);
            return Ok(vec![]);
        }

        let subs = match resp.sub {
            Some(s) => s.subs,
            None => return Ok(vec![]),
        };

        let mut candidates = vec![];

        // For each search result, fetch detail to get file list
        for (idx, sub) in subs.iter().take(5).enumerate() {
            self.rate_limit().await;

            let detail_resp: AssrtDetailResponse = self
                .api_get("/sub/detail", &[("id", &sub.id.to_string())])
                .await?;

            if let Some(detail_sub) = detail_resp.sub {
                for detail in &detail_sub.subs {
                    let lang_desc = detail
                        .lang
                        .as_ref()
                        .and_then(|l| l.desc.clone())
                        .unwrap_or_default();

                    // Bilingual detection
                    let is_bilingual = lang_desc.contains("双语")
                        || (lang_desc.contains("中") && lang_desc.contains("英"));

                    for file in &detail.filelist {
                        let payload = SubtitlePayload {
                            file_name: file.fname.clone(),
                            url: file.url.clone(),
                        };

                        candidates.push(SubtitleCandidate::new(
                            "assrt".to_string(),
                            lang_desc.clone(),
                            is_bilingual,
                            payload,
                            (5 - idx as i32).max(0),
                        ));
                    }
                }
            }
        }

        Ok(candidates)
    }

    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 = AssrtProvider::new(String::new());
        let latency = provider.check_alive().await;

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

    #[test_with::env(ASSRT_TOKEN)]
    #[tokio::test]
    async fn search_movie_interstellar() {
        let provider = AssrtProvider::new(std::env::var("ASSRT_TOKEN").unwrap());
        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);
    }

    #[test_with::env(ASSRT_TOKEN)]
    #[tokio::test]
    async fn search_series_breaking_bad() {
        let provider = AssrtProvider::new(std::env::var("ASSRT_TOKEN").unwrap());
        let query = series_query("Breaking Bad", 1, 1);
        let results = provider.search(&query).await.unwrap();

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