use std::fmt::{self, Display};
use std::time::Instant;

use anyhow::{Result, anyhow};
use async_trait::async_trait;
use reqwest::{Client, RequestBuilder, Url};
use serde::{Deserialize, Serialize};

use super::config::MTeamConfig;
use crate::site::capabilities::SiteCapabilities;
use crate::site::provider::SiteProvider;
use crate::site::query::MediaResourceQuery;
use crate::site::types::{ResolvedSiteDownload, ResourceCandidate, ResourcePayload};
use crate::types::{Provider, ProviderStatus};

const MTEAM_PROVIDER_KEY: &str = "mteam";
const MTEAM_PROVIDER_NAME: &str = "M-Team";

pub struct MTeamProvider<'a> {
    config: MTeamConfig<'a>,
    client: Client,
}

impl<'a> MTeamProvider<'a> {
    pub fn new(config: MTeamConfig<'a>) -> Result<Self> {
        if config.api_key.trim().is_empty() {
            return Err(anyhow!("mteam provider requires an api key"));
        }

        Url::parse(config.api_base_url)
            .map_err(|error| anyhow!("invalid mteam api base url: {error}"))?;
        Url::parse(config.web_base_url)
            .map_err(|error| anyhow!("invalid mteam web base url: {error}"))?;

        let mut builder = Client::builder();

        if let Some(user_agent) = config.user_agent {
            builder = builder.user_agent(user_agent);
        }

        Ok(Self {
            config,
            client: builder.build()?,
        })
    }

    async fn execute_api<T>(
        &self,
        endpoint: &str,
        body: &impl Serialize,
    ) -> Result<MTeamApiResponse<T>>
    where
        T: for<'de> Deserialize<'de>,
    {
        let response = self
            .request_api(endpoint)
            .json(body)
            .send()
            .await?
            .error_for_status()?;
        let payload = response.json::<MTeamApiResponse<T>>().await?;

        if !payload.is_success() {
            return Err(anyhow!(
                "mteam api error: code={} message={}",
                payload.code,
                payload.message
            ));
        }

        Ok(payload)
    }

    async fn execute_form_api<T>(
        &self,
        endpoint: &str,
        form: &[(&str, &str)],
    ) -> Result<MTeamApiResponse<T>>
    where
        T: for<'de> Deserialize<'de>,
    {
        let response = self
            .request_api(endpoint)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .form(form)
            .send()
            .await?
            .error_for_status()?;
        let payload = response.json::<MTeamApiResponse<T>>().await?;

        if !payload.is_success() {
            return Err(anyhow!(
                "mteam api error: code={} message={}",
                payload.code,
                payload.message
            ));
        }

        Ok(payload)
    }

    fn request_api(&self, endpoint: &str) -> RequestBuilder {
        self.client
            .post(api_url(self.config.api_base_url, endpoint))
            .header("Accept", "application/json")
            .header("x-api-key", self.config.api_key)
    }

    fn build_search_request(&self, query: Option<&MediaResourceQuery>) -> MTeamSearchRequest {
        let (keyword, page_number, page_size, categories) = if let Some(query) = query {
            let page_size = query.pagination.limit.max(1);
            let page_number = query.pagination.offset / page_size + 1;

            (
                query.title.clone(),
                page_number,
                page_size,
                query.categories.clone(),
            )
        } else {
            (String::new(), 1, 20, vec![])
        };

        MTeamSearchRequest {
            mode: "normal".to_string(),
            categories,
            keyword,
            page_number,
            page_size,
        }
    }

    async fn fetch_candidates(
        &self,
        query: Option<&MediaResourceQuery>,
    ) -> Result<Vec<ResourceCandidate>> {
        let response = self
            .execute_api::<MTeamSearchData>(
                "/api/torrent/search",
                &self.build_search_request(query),
            )
            .await?;

        Ok(parse_search_candidates(
            MTEAM_PROVIDER_KEY,
            self.config.web_base_url,
            response.data,
        ))
    }
}

#[async_trait]
impl<'a> Provider for MTeamProvider<'a> {
    fn key(&self) -> &str {
        MTEAM_PROVIDER_KEY
    }

    fn name(&self) -> &str {
        MTEAM_PROVIDER_NAME
    }

    async fn health_check(&self) -> ProviderStatus {
        let start = Instant::now();
        let response = self
            .execute_api::<serde_json::Value>("/api/member/profile", &serde_json::json!({}))
            .await;

        match response {
            Ok(_) => ProviderStatus::Healthy {
                latency_ms: start.elapsed().as_millis() as u64,
            },
            Err(_) => ProviderStatus::Unhealthy,
        }
    }
}

#[async_trait]
impl<'a> SiteProvider for MTeamProvider<'a> {
    async fn search_resources(&self, query: &MediaResourceQuery) -> Result<Vec<ResourceCandidate>> {
        self.fetch_candidates(Some(query)).await
    }

    async fn latest_resources(&self) -> Result<Vec<ResourceCandidate>> {
        self.fetch_candidates(None).await
    }

    async fn resolve(&self, candidate: &ResourceCandidate) -> Result<ResolvedSiteDownload> {
        let response = self
            .execute_form_api::<String>(
                "/api/torrent/genDlToken",
                &[("id", candidate.payload.resource_key.as_str())],
            )
            .await?;

        Ok(ResolvedSiteDownload {
            file_name: Some(format!("{}.torrent", candidate.payload.resource_key)),
            download_url: response.data,
            headers: vec![],
        })
    }

    fn capabilities(&self) -> SiteCapabilities {
        SiteCapabilities::SEARCH
            | SiteCapabilities::DETAIL_LOOKUP
            | SiteCapabilities::DIRECT_DOWNLOAD
            | SiteCapabilities::LOGIN_REQUIRED
            | SiteCapabilities::API_TOKEN
    }
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct MTeamSearchRequest {
    mode: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    categories: Vec<String>,
    #[serde(skip_serializing_if = "String::is_empty")]
    keyword: String,
    page_number: usize,
    page_size: usize,
}

#[derive(Debug, Deserialize)]
struct MTeamApiResponse<T> {
    code: MTeamApiCode,
    message: String,
    data: T,
}

impl<T> MTeamApiResponse<T> {
    fn is_success(&self) -> bool {
        match &self.code {
            MTeamApiCode::String(value) => value == "0",
            MTeamApiCode::Number(value) => *value == 0,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum MTeamApiCode {
    String(String),
    Number(i64),
}

impl Display for MTeamApiCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::String(value) => f.write_str(value),
            Self::Number(value) => write!(f, "{value}"),
        }
    }
}

#[derive(Debug, Deserialize)]
struct MTeamSearchData {
    data: Vec<MTeamTorrent>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MTeamTorrent {
    id: String,
    name: String,
    #[serde(default)]
    small_descr: String,
    #[serde(default, rename = "labelsNew")]
    labels: Vec<String>,
}

fn parse_search_candidates(
    provider_key: &str,
    web_base_url: &str,
    payload: MTeamSearchData,
) -> Vec<ResourceCandidate> {
    payload
        .data
        .into_iter()
        .map(|torrent| {
            let detail_url = Some(format!(
                "{}/detail/{}",
                web_base_url.trim_end_matches('/'),
                torrent.id
            ));
            let title = if torrent.small_descr.trim().is_empty() {
                torrent.name
            } else {
                format!("{} {}", torrent.name, torrent.small_descr.trim())
            };

            ResourceCandidate {
                provider: provider_key.to_string(),
                title,
                detail_url,
                labels: torrent.labels,
                payload: ResourcePayload {
                    resource_key: torrent.id,
                    torrent_url: None,
                },
            }
        })
        .collect()
}

fn api_url(base_url: &str, endpoint: &str) -> String {
    format!("{}{}", base_url.trim_end_matches('/'), endpoint)
}

#[cfg(test)]
mod tests {
    use super::{MTeamConfig, MTeamSearchData, MTeamTorrent, api_url, parse_search_candidates};
    use crate::site::{MediaQueryMediaType, MediaResourceQuery, ReleaseFilters};
    use crate::{ExternalIds, Pagination};

    fn test_query() -> MediaResourceQuery {
        MediaResourceQuery {
            media_type: MediaQueryMediaType::Movie,
            title: "Interstellar".to_string(),
            original_title: None,
            year: Some(2014),
            season: None,
            episode: None,
            external_ids: ExternalIds {
                imdb_id: Some("tt0816692".to_string()),
                ..ExternalIds::default()
            },
            categories: vec!["419".to_string()],
            keywords: vec![],
            release_filters: ReleaseFilters::default(),
            pagination: Pagination {
                offset: 20,
                limit: 20,
            },
        }
    }

    #[test]
    fn search_request_uses_title_keyword_and_pagination() {
        let provider = super::MTeamProvider::new(MTeamConfig {
            api_key: "test-api-key",
            ..MTeamConfig::default()
        })
        .unwrap();
        let request = provider.build_search_request(Some(&test_query()));

        assert_eq!(request.mode, "normal");
        assert_eq!(request.keyword, "Interstellar");
        assert_eq!(request.categories, vec!["419".to_string()]);
        assert_eq!(request.page_number, 2);
        assert_eq!(request.page_size, 20);
    }

    #[test]
    fn parse_search_candidates_builds_detail_urls() {
        let candidates = parse_search_candidates(
            "mteam",
            "https://kp.m-team.cc",
            MTeamSearchData {
                data: vec![MTeamTorrent {
                    id: "12345".to_string(),
                    name: "Interstellar".to_string(),
                    small_descr: "2014 1080p BluRay".to_string(),
                    labels: vec!["中字".to_string(), "4k".to_string()],
                }],
            },
        );

        assert_eq!(candidates.len(), 1);
        assert_eq!(candidates[0].provider, "mteam");
        assert_eq!(candidates[0].payload.resource_key, "12345");
        assert_eq!(
            candidates[0].detail_url.as_deref(),
            Some("https://kp.m-team.cc/detail/12345")
        );
        assert_eq!(
            candidates[0].labels,
            vec!["中字".to_string(), "4k".to_string()]
        );
    }

    #[test]
    fn api_url_trims_base_trailing_slash() {
        assert_eq!(
            api_url("https://api.m-team.cc/", "/api/torrent/search"),
            "https://api.m-team.cc/api/torrent/search"
        );
    }
}
