use std::path::Path;
use std::sync::Arc;

use anyhow::{Result, anyhow};
use futures::future::join_all;
use mediarium_ocr::CaptchaOcr;
use tracing::{error, info, warn};

use crate::subtitle::provider::SubtitleProvider;
use crate::subtitle::providers::assrt::AssrtProvider;
use crate::subtitle::providers::subhd::SubhdProvider;
use crate::subtitle::providers::xunlei::XunleiProvider;
use crate::subtitle::providers::zimiku::ZimukuProvider;
use crate::subtitle::types::{DownloadedSubtitle, MediaQuery, ResolvedSubtitle, SubtitleCandidate};
use crate::types::ProviderStatusEntry;

/// Hub that manages multiple subtitle providers.
pub struct SubtitleProviderHub {
    providers: Vec<Box<dyn SubtitleProvider>>,
}

impl SubtitleProviderHub {
    #[must_use]
    pub fn new() -> Self {
        Self { providers: vec![] }
    }

    #[must_use]
    pub fn configured(model_dir: &Path, assrt_token: &str) -> Self {
        let mut hub = Self::new();

        hub.register(Box::new(XunleiProvider::new()));
        hub.register(Box::new(SubhdProvider::new()));

        let zimuku_ocr = match CaptchaOcr::load(model_dir) {
            Ok(ocr) => Some(Arc::new(ocr)),
            Err(error) => {
                warn!(
                    "OCR model not available, zimuku captcha bypass disabled but provider remains enabled: {error}"
                );

                None
            }
        };

        hub.register(Box::new(ZimukuProvider::new(zimuku_ocr)));

        if !assrt_token.is_empty() {
            hub.register(Box::new(AssrtProvider::new(assrt_token.to_string())));
        }

        hub
    }

    pub fn register(&mut self, provider: Box<dyn SubtitleProvider>) {
        info!(
            "Registered subtitle provider: {} ({})",
            provider.key(),
            provider.name()
        );

        self.providers.push(provider);
    }

    fn find_provider(&self, provider_key: &str) -> Result<&dyn SubtitleProvider> {
        self.providers
            .iter()
            .find(|provider| provider.key() == provider_key)
            .map(|provider| provider.as_ref())
            .ok_or_else(|| anyhow!("Provider '{provider_key}' not found"))
    }

    /// Search all enabled providers concurrently and return merged results.
    pub async fn search(&self, query: &MediaQuery) -> Vec<SubtitleCandidate> {
        let futures: Vec<_> = self
            .providers
            .iter()
            .filter(|s| {
                if !s.is_enabled() || s.over_daily_limit() {
                    info!(
                        "Provider {} ({}) is disabled or over daily limit",
                        s.key(),
                        s.name()
                    );

                    false
                } else {
                    true
                }
            })
            .map(|s| async {
                let key = s.key().to_string();
                let name = s.name().to_string();
                let result = s.search(query).await;

                (key, name, result)
            })
            .collect();

        let results = join_all(futures).await;

        let mut all_candidates = vec![];

        for (key, name, result) in results {
            match result {
                Ok(candidates) => {
                    info!(
                        "Provider {} ({}) returned {} candidates",
                        key,
                        name,
                        candidates.len()
                    );

                    all_candidates.extend(candidates);
                }
                Err(e) => {
                    error!("Provider {} ({}) search failed: {}", key, name, e);
                }
            }
        }

        all_candidates
    }

    pub async fn check_all(&self) -> Vec<ProviderStatusEntry> {
        let futures: Vec<_> = self
            .providers
            .iter()
            .map(|s| async {
                let status = s.health_check().await;

                ProviderStatusEntry {
                    key: s.key().to_string(),
                    name: s.name().to_string(),
                    status,
                }
            })
            .collect();

        join_all(futures).await
    }

    pub async fn resolve(&self, candidate: &SubtitleCandidate) -> Result<ResolvedSubtitle> {
        let provider = self.find_provider(&candidate.provider)?;

        provider.resolve(candidate).await
    }

    pub async fn download(&self, resolved: &ResolvedSubtitle) -> Result<DownloadedSubtitle> {
        let provider = self.find_provider(&resolved.provider)?;

        provider.download(resolved).await
    }
}

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