//! Hub for coordinating multiple site providers.

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

use super::provider::SiteProvider;
use super::query::MediaResourceQuery;
use super::types::{ResolvedSiteDownload, ResourceCandidate};
use crate::types::ProviderStatusEntry;

/// Hub that manages multiple site providers.
pub struct SiteProviderHub {
    providers: Vec<Box<dyn SiteProvider>>,
}

impl SiteProviderHub {
    /// Create an empty provider hub.
    #[must_use]
    pub fn new() -> Self {
        Self { providers: vec![] }
    }

    /// Register a site provider.
    pub fn register(&mut self, provider: Box<dyn SiteProvider>) {
        info!(
            "Registered site provider: {} ({})",
            provider.key(),
            provider.name()
        );

        self.providers.push(provider);
    }

    /// Search selected providers concurrently and return merged results.
    pub async fn search_resources(
        &self,
        provider_keys: &[&str],
        query: &MediaResourceQuery,
    ) -> Vec<ResourceCandidate> {
        let providers: Vec<_> = provider_keys
            .iter()
            .filter_map(|key| match self.find_provider(key) {
                Ok(provider) => Some(provider),
                Err(error) => {
                    error!("Site provider {} lookup failed: {}", key, error);

                    None
                }
            })
            .collect();

        let futures: Vec<_> = providers
            .into_iter()
            .map(|provider| async {
                let key = provider.key().to_string();
                let name = provider.name().to_string();
                let result = provider.search_resources(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!(
                        "Site provider {} ({}) returned {} candidates",
                        key,
                        name,
                        candidates.len()
                    );

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

        all_candidates
    }

    /// Fetch the latest resources from a specific provider.
    ///
    /// # Errors
    ///
    /// Returns an error when the provider cannot be found, is disabled, or the
    /// provider-specific latest listing fails.
    pub async fn latest_resources_from(
        &self,
        provider_key: &str,
    ) -> Result<Vec<ResourceCandidate>> {
        let provider = self.find_provider(provider_key)?;

        provider.latest_resources().await
    }

    /// Check health information for all registered providers.
    pub async fn check_all(&self) -> Vec<ProviderStatusEntry> {
        let futures: Vec<_> = self
            .providers
            .iter()
            .map(|provider| async {
                let status = provider.health_check().await;

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

        join_all(futures).await
    }

    /// Resolve a candidate via the provider that produced it.
    ///
    /// # Errors
    ///
    /// Returns an error when the provider cannot be found or resolution fails.
    pub async fn resolve(&self, candidate: &ResourceCandidate) -> Result<ResolvedSiteDownload> {
        let provider = self.find_provider(&candidate.provider)?;

        provider.resolve(candidate).await
    }

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

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