//! Client factory for creating extension clients from configuration

use std::sync::Arc;

use tracing::{info, warn};

use super::traits::{DistributionClient, SourceClient};
use super::vault_resolver::VaultResolver;
use super::{ForgejoClient, GitHubClient};
use crate::config::Config;
use crate::error::{RegistryError, Result};
use crate::gitea::GiteaClient as GiteaClientImpl;
use crate::oci::OciClient as OciClientImpl;

/// Factory for creating extension clients
pub struct ClientFactory;

impl ClientFactory {
    /// Create all configured clients from configuration, resolving any vault://
    /// token references.
    ///
    /// When `vault_url` is `Some`, any `token_path` starting with `vault://` is
    /// resolved via the vault-service HTTP API with a 5-minute in-memory
    /// cache. When `vault_url` is `None`, vault:// paths cause an error.
    pub async fn create_from_config(
        config: &Config,
        vault_url: Option<&str>,
    ) -> Result<(Vec<Arc<dyn SourceClient>>, Vec<Arc<dyn DistributionClient>>)> {
        let resolver = vault_url.map(|u| VaultResolver::new(u.to_string()));

        let mut source_clients: Vec<Arc<dyn SourceClient>> = Vec::new();
        let mut distribution_clients: Vec<Arc<dyn DistributionClient>> = Vec::new();

        // Create Gitea clients (source-based)
        for gitea_config in &config.sources.gitea {
            let token = resolve_token(&gitea_config.token_path, resolver.as_ref()).await?;
            let client = GiteaClientImpl::new(gitea_config, token)?;
            source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
            info!("Registered Gitea client: {}", gitea_config.url);
        }

        // Create Forgejo clients (source-based)
        for forgejo_config in &config.sources.forgejo {
            let token = resolve_token(&forgejo_config.token_path, resolver.as_ref()).await?;
            let client = ForgejoClient::new(forgejo_config, token)?;
            source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
            info!("Registered Forgejo client: {}", forgejo_config.url);
        }

        // Create GitHub clients (source-based)
        for github_config in &config.sources.github {
            // GitHub tokens are optional; a missing file is treated as unauthenticated
            // access
            let token = resolve_token_optional(&github_config.token_path, resolver.as_ref()).await;
            if token.is_none() {
                warn!(
                    "GitHub client '{}' has no token — API rate limits apply",
                    github_config.organization
                );
            }
            let client = GitHubClient::new(github_config, token)?;
            source_clients.push(Arc::new(client) as Arc<dyn SourceClient>);
            info!("Registered GitHub client: {}", github_config.organization);
        }

        // Create OCI clients (distribution-based)
        for oci_config in &config.distributions.oci {
            let auth_token = match &oci_config.auth_token_path {
                Some(path) => Some(resolve_token(path, resolver.as_ref()).await?),
                None => None,
            };
            let client = OciClientImpl::new(oci_config, auth_token)?;
            distribution_clients.push(Arc::new(client) as Arc<dyn DistributionClient>);
            info!("Registered OCI client: {}", oci_config.registry);
        }

        if source_clients.is_empty() && distribution_clients.is_empty() {
            return Err(RegistryError::Config(
                "No backends configured (gitea, forgejo, github, or oci required)".to_string(),
            ));
        }

        info!(
            "Initialized {} source clients and {} distribution clients",
            source_clients.len(),
            distribution_clients.len()
        );

        Ok((source_clients, distribution_clients))
    }
}

/// Resolve a token path to its plaintext value.
///
/// Paths prefixed with `vault://` are resolved via VaultResolver (requires
/// resolver to be Some). All other paths are read from the filesystem.
async fn resolve_token(path: &str, resolver: Option<&VaultResolver>) -> Result<String> {
    if path.starts_with("vault://") {
        let resolver = resolver.ok_or_else(|| {
            RegistryError::Config(format!(
                "token_path '{}' is a vault:// reference but no vault_url was provided",
                path
            ))
        })?;
        return resolver
            .try_resolve(path)
            .await
            .expect("try_resolve always returns Some for vault:// prefixed paths")
            .map_err(|e| {
                RegistryError::Config(format!("Vault resolution failed for '{}': {}", path, e))
            });
    }

    std::fs::read_to_string(path)
        .map(|s| s.trim().to_string())
        .map_err(|e| RegistryError::Config(format!("Failed to read token from '{}': {}", path, e)))
}

/// Resolve a token path, returning `None` on any failure.
///
/// Used for optional tokens (e.g., GitHub) where unauthenticated access is
/// acceptable.
async fn resolve_token_optional(path: &str, resolver: Option<&VaultResolver>) -> Option<String> {
    resolve_token(path, resolver).await.ok()
}
