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

use anyhow::{Result, bail};
use async_trait::async_trait;
use regex::Regex;
use reqwest::Client;
use serde::Deserialize;
#[cfg(test)]
use serde::Serialize;
use sha1::{Digest, Sha1};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tracing::debug;

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

const XUNLEI_API_URL: &str = "http://sub.xmp.sandai.net:8000/subxl";
const TEST_CID: &str = "FB4E2AFF106112136DFC5ACC7339EB29D1EC0CF8";
const CID_BUFFER_SIZE: usize = 0x5000; // 20480 bytes
const CID_MIN_FILE_SIZE: u64 = 0xF000; // 61440 bytes

/// Lone surrogate escapes (`\uD800`-`\uDFFF`) that the XunLei API sometimes
/// returns.
static SURROGATE_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\\u[dD][89a-fA-F][0-9a-fA-F]{2}").unwrap());

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

#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
struct XunleiResponse {
    sublist: Vec<XunleiSub>,
}

#[derive(Debug, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
struct XunleiSub {
    #[serde(default)]
    scid: String,
    #[serde(default)]
    sname: String,
    #[serde(default)]
    language: String,
    #[serde(default)]
    surl: String,
    #[serde(default)]
    svote: i64,
}

impl XunleiProvider {
    #[must_use]
    pub fn new() -> Self {
        Self {
            client: Client::new(),
            enabled: AtomicBool::new(true),
        }
    }

    /// Calculate XunLei CID (Content ID) from a video file.
    ///
    /// Uses SHA1 hash of three 20480-byte segments sampled from the file:
    /// start, one-third, and end.
    async fn calculate_cid(file_path: &str) -> Result<String> {
        let mut file = File::open(file_path).await?;
        let file_length = file.seek(SeekFrom::End(0)).await?;

        if file_length < CID_MIN_FILE_SIZE {
            bail!("File too small for CID calculation (< {CID_MIN_FILE_SIZE} bytes)");
        }

        let positions = [0u64, file_length / 3, file_length - CID_BUFFER_SIZE as u64];

        let mut hasher = Sha1::new();
        let mut buffer = vec![0u8; CID_BUFFER_SIZE];

        for &position in &positions {
            file.seek(SeekFrom::Start(position)).await?;
            file.read_exact(&mut buffer).await?;
            hasher.update(&buffer);
        }

        Ok(format!("{:X}", hasher.finalize()))
    }

    /// Fetch and parse a XunLei API response.
    ///
    /// The API may return lone surrogate escapes (`\uD800`-`\uDFFF`) which are
    /// invalid Unicode; replace them with `\uFFFD` so `serde_json` can parse.
    async fn fetch_json(&self, url: &str) -> Result<XunleiResponse> {
        let text = self.client.get(url).send().await?.text().await?;
        let text = SURROGATE_RE.replace_all(&text, r"\uFFFD");

        Ok(serde_json::from_str(&text)?)
    }
}

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

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

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

    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 XunleiProvider {
    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();
        let url = format!("{XUNLEI_API_URL}/{TEST_CID}.json");
        self.fetch_json(&url).await?;

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

    async fn search(&self, query: &MediaQuery) -> Result<Vec<SubtitleCandidate>> {
        if query.file_path.is_empty() {
            bail!("file path is empty, cannot calculate CID");
        }

        let cid = match Self::calculate_cid(&query.file_path).await {
            Ok(cid) => cid,
            Err(e) => {
                debug!("failed to calculate CID: {e}");

                return Ok(vec![]);
            }
        };

        let url = format!("{XUNLEI_API_URL}/{cid}.json");
        let resp = self.fetch_json(&url).await?;

        let mut candidates = vec![];

        for (idx, sub) in resp.sublist.iter().enumerate() {
            if sub.scid.is_empty() {
                continue;
            }

            let is_bilingual = sub.language.contains("双语") || sub.language.contains('&');

            let payload = SubtitlePayload {
                file_name: sub.sname.clone(),
                url: sub.surl.clone(),
            };

            candidates.push(SubtitleCandidate::new(
                "xunlei".to_string(),
                sub.language.clone(),
                is_bilingual,
                payload,
                sub.svote as i32 + (resp.sublist.len() as i32 - 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::*;

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

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

    #[tokio::test]
    async fn search_by_known_cid() {
        let provider = XunleiProvider::new();
        let url = format!("{XUNLEI_API_URL}/{TEST_CID}.json");
        let resp = provider.fetch_json(&url).await.unwrap();

        assert!(!resp.sublist.is_empty(), "test CID should return subtitles");
        insta::assert_yaml_snapshot!(resp.sublist);
    }
}
