//! Extension pour intégrer la configuration Qobuz dans pmoconfig
//!
//! Ce module fournit le trait `QobuzConfigExt` qui permet d'ajouter facilement
//! des méthodes de gestion des credentials Qobuz à pmoconfig::Config.

use anyhow::{anyhow, Result};
use pmoconfig::Config;
use serde_yaml::Value;

/// Trait d'extension pour gérer la configuration Qobuz dans pmoconfig
///
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
/// aux credentials et paramètres Qobuz.
///
/// # Exemple
///
/// ```rust,ignore
/// use pmoconfig::get_config;
/// use pmoqobuz::QobuzConfigExt;
///
/// let config = get_config();
/// let (username, password) = config.get_qobuz_credentials()?;
/// println!("Qobuz user: {}", username);
/// ```
pub trait QobuzConfigExt {
    /// Récupère le nom d'utilisateur Qobuz depuis la configuration
    ///
    /// # Returns
    ///
    /// Le nom d'utilisateur (email) configuré pour Qobuz
    ///
    /// # Errors
    ///
    /// Retourne une erreur si le nom d'utilisateur n'est pas configuré
    fn get_qobuz_username(&self) -> Result<String>;

    /// Définit le nom d'utilisateur Qobuz dans la configuration
    ///
    /// # Arguments
    ///
    /// * `username` - Le nom d'utilisateur (email) Qobuz
    fn set_qobuz_username(&self, username: &str) -> Result<()>;

    /// Récupère le mot de passe Qobuz depuis la configuration
    ///
    /// # Returns
    ///
    /// Le mot de passe configuré pour Qobuz
    ///
    /// # Errors
    ///
    /// Retourne une erreur si le mot de passe n'est pas configuré
    fn get_qobuz_password(&self) -> Result<String>;

    /// Définit le mot de passe Qobuz dans la configuration
    ///
    /// # Arguments
    ///
    /// * `password` - Le mot de passe Qobuz
    fn set_qobuz_password(&self, password: &str) -> Result<()>;

    /// Récupère les credentials Qobuz (username et password)
    ///
    /// # Returns
    ///
    /// Un tuple (username, password) contenant les credentials Qobuz
    ///
    /// # Errors
    ///
    /// Retourne une erreur si l'un des credentials n'est pas configuré
    ///
    /// # Exemple
    ///
    /// ```rust,ignore
    /// use pmoconfig::get_config;
    /// use pmoqobuz::QobuzConfigExt;
    ///
    /// let config = get_config();
    /// match config.get_qobuz_credentials() {
    ///     Ok((username, password)) => {
    ///         println!("Credentials configured for: {}", username);
    ///     }
    ///     Err(e) => {
    ///         eprintln!("Qobuz credentials not configured: {}", e);
    ///     }
    /// }
    /// ```
    fn get_qobuz_credentials(&self) -> Result<(String, String)>;

    /// Récupère l'App ID Qobuz depuis la configuration
    ///
    /// # Returns
    ///
    /// L'App ID configuré pour Qobuz, ou None si non configuré
    ///
    /// # Note
    ///
    /// Si aucun App ID n'est configuré, le client utilisera soit le Spoofer
    /// pour en obtenir un dynamiquement, soit un App ID par défaut.
    fn get_qobuz_appid(&self) -> Result<Option<String>>;

    /// Définit l'App ID Qobuz dans la configuration
    ///
    /// # Arguments
    ///
    /// * `appid` - L'App ID Qobuz (ex: "1401488693436528")
    fn set_qobuz_appid(&self, appid: &str) -> Result<()>;

    /// Récupère le secret Qobuz depuis la configuration
    ///
    /// # Returns
    ///
    /// Le secret encodé en base64, ou None si non configuré
    ///
    /// # Note
    ///
    /// Le secret est la valeur `configvalue` du code Python.
    /// Il est décodé et XORé avec l'App ID pour obtenir le secret `s4`
    /// utilisé pour signer les requêtes sensibles.
    ///
    /// Si aucun secret n'est configuré, le client utilisera le Spoofer
    /// pour en obtenir un dynamiquement.
    fn get_qobuz_secret(&self) -> Result<Option<String>>;

    /// Définit le secret Qobuz dans la configuration
    ///
    /// # Arguments
    ///
    /// * `secret` - Le secret encodé en base64 (configvalue)
    fn set_qobuz_secret(&self, secret: &str) -> Result<()>;

    /// Récupère le secret brut du Spoofer depuis la configuration
    ///
    /// # Returns
    ///
    /// Le secret brut (non XORé), ou None si non configuré
    ///
    /// # Note
    ///
    /// Ce secret est obtenu par le Spoofer et est utilisé directement
    /// sans XOR avec l'App ID. Si ce secret est présent, il est testé
    /// en priorité avant de relancer le Spoofer.
    fn get_qobuz_spoofer_secret(&self) -> Result<Option<String>>;

    /// Définit le secret brut du Spoofer dans la configuration
    ///
    /// # Arguments
    ///
    /// * `secret` - Le secret brut obtenu par le Spoofer
    fn set_qobuz_spoofer_secret(&self, secret: &str) -> Result<()>;

    /// Récupère le token d'authentification depuis la configuration
    ///
    /// # Returns
    ///
    /// Le token d'authentification, ou None si non configuré ou expiré
    fn get_qobuz_auth_token(&self) -> Result<Option<String>>;

    /// Récupère l'ID utilisateur depuis la configuration
    ///
    /// # Returns
    ///
    /// L'ID utilisateur, ou None si non configuré
    fn get_qobuz_user_id(&self) -> Result<Option<String>>;

    /// Récupère le timestamp d'expiration du token
    ///
    /// # Returns
    ///
    /// Le timestamp d'expiration (Unix timestamp), ou None si non configuré
    fn get_qobuz_token_expires_at(&self) -> Result<Option<u64>>;

    /// Récupère le label de l'abonnement depuis la configuration
    fn get_qobuz_subscription_label(&self) -> Result<Option<String>>;

    /// Sauvegarde les informations d'authentification dans la configuration
    ///
    /// # Arguments
    ///
    /// * `token` - Le token d'authentification
    /// * `user_id` - L'ID utilisateur
    /// * `subscription_label` - Le label de l'abonnement (optionnel)
    /// * `expires_at` - Timestamp d'expiration (Unix timestamp)
    fn set_qobuz_auth_info(
        &self,
        token: &str,
        user_id: &str,
        subscription_label: Option<&str>,
        expires_at: u64,
    ) -> Result<()>;

    /// Supprime les informations d'authentification de la configuration
    fn clear_qobuz_auth_info(&self) -> Result<()>;

    /// Vérifie si le token d'authentification est encore valide
    ///
    /// # Returns
    ///
    /// true si un token existe et n'est pas expiré, false sinon
    fn is_qobuz_auth_valid(&self) -> bool;

    /// Récupère le répertoire de cache Qobuz
    ///
    /// # Returns
    ///
    /// Le chemin absolu du répertoire de cache, créé s'il n'existe pas
    fn get_qobuz_cache_dir(&self) -> Result<String>;

    /// Définit le répertoire de cache Qobuz
    fn set_qobuz_cache_dir(&self, directory: String) -> Result<()>;

    /// Récupère le nombre maximum de requêtes concurrentes
    ///
    /// # Returns
    ///
    /// Le nombre maximum de requêtes concurrentes, ou None si non configuré (défaut: 2)
    fn get_qobuz_rate_limit_max_concurrent(&self) -> Result<Option<usize>>;

    /// Définit le nombre maximum de requêtes concurrentes
    fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()>;

    /// Récupère le délai minimum entre requêtes en millisecondes
    ///
    /// # Returns
    ///
    /// Le délai minimum en ms, ou None si non configuré (défaut: 400ms)
    fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result<Option<u64>>;

    /// Définit le délai minimum entre requêtes
    fn set_qobuz_rate_limit_min_delay_ms(&self, delay_ms: u64) -> Result<()>;

    /// Vérifie si le rate limiting est activé
    ///
    /// # Returns
    ///
    /// true si activé (défaut), false sinon
    fn is_qobuz_rate_limiting_enabled(&self) -> bool;

    /// Active ou désactive le rate limiting
    fn set_qobuz_rate_limiting_enabled(&self, enabled: bool) -> Result<()>;

    /// Version du bundle Qobuz extrait en dernier (ex : `"8.1.0-b019"`).
    /// Permet de détecter une rotation de bundle sans télécharger les 7 MB.
    fn get_qobuz_bundle_version(&self) -> Result<Option<String>>;

    /// Persiste la version du bundle après une extraction réussie.
    fn set_qobuz_bundle_version(&self, version: &str) -> Result<()>;

    /// Nombre de workers concurrents pour l'enregistrement des tracks en cache.
    ///
    /// Contrôle le semaphore dans `register_tracks_lazy` : plus la valeur est
    /// haute, plus les covers sont téléchargées en parallèle, mais plus la
    /// contention sur le mutex SQLite est forte.
    ///
    /// Défaut : 4 (adapté à une machine sous contrainte mémoire / Docker).
    fn get_qobuz_register_concurrency(&self) -> usize;

    /// Nombre de pages de playlist chargées en parallèle via `/playlist/get`.
    ///
    /// La page 1 est toujours séquentielle (pour obtenir `total`). Les pages
    /// suivantes sont lancées simultanément jusqu'à cette limite.
    /// Valeur trop haute → risque de rate limiting Qobuz.
    ///
    /// Défaut : 3.
    fn get_qobuz_page_concurrency(&self) -> usize;
}

impl QobuzConfigExt for Config {
    fn get_qobuz_username(&self) -> Result<String> {
        match self.get_value(&["accounts", "qobuz", "username"])? {
            Value::String(s) => Ok(s),
            _ => Err(anyhow!("Qobuz username not configured")),
        }
    }

    fn set_qobuz_username(&self, username: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "username"],
            Value::String(username.to_string()),
        )
    }

    fn get_qobuz_password(&self) -> Result<String> {
        match self.get_value(&["accounts", "qobuz", "password"])? {
            Value::String(s) => {
                // Déchiffrement automatique si le mot de passe est chiffré
                pmoconfig::encryption::get_password(&s)
                    .map_err(|e| anyhow!("Failed to decrypt password: {}", e))
            }
            _ => Err(anyhow!("Qobuz password not configured")),
        }
    }

    fn set_qobuz_password(&self, password: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "password"],
            Value::String(password.to_string()),
        )
    }

    fn get_qobuz_credentials(&self) -> Result<(String, String)> {
        let username = self.get_qobuz_username()?;
        let password = self.get_qobuz_password()?;
        Ok((username, password))
    }

    fn get_qobuz_appid(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "appid"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn set_qobuz_appid(&self, appid: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "appid"],
            Value::String(appid.to_string()),
        )
    }

    fn get_qobuz_secret(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "secret"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn set_qobuz_secret(&self, secret: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "secret"],
            Value::String(secret.to_string()),
        )
    }

    fn get_qobuz_spoofer_secret(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "spoofer_secret"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn set_qobuz_spoofer_secret(&self, secret: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "spoofer_secret"],
            Value::String(secret.to_string()),
        )
    }

    fn get_qobuz_auth_token(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "auth_token"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn get_qobuz_user_id(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "user_id"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn get_qobuz_token_expires_at(&self) -> Result<Option<u64>> {
        match self.get_value(&["accounts", "qobuz", "token_expires_at"]) {
            Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap())),
            Ok(Value::Number(n)) if n.is_i64() => Ok(Some(n.as_i64().unwrap() as u64)),
            Ok(_) => Ok(None),  // Wrong type
            Err(_) => Ok(None), // Not configured
        }
    }

    fn get_qobuz_subscription_label(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "subscription_label"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            Ok(Value::String(_)) => Ok(None), // Empty string
            Ok(_) => Ok(None),                // Wrong type
            Err(_) => Ok(None),               // Not configured
        }
    }

    fn set_qobuz_auth_info(
        &self,
        token: &str,
        user_id: &str,
        subscription_label: Option<&str>,
        expires_at: u64,
    ) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "auth_token"],
            Value::String(token.to_string()),
        )?;
        self.set_value(
            &["accounts", "qobuz", "user_id"],
            Value::String(user_id.to_string()),
        )?;
        self.set_value(
            &["accounts", "qobuz", "token_expires_at"],
            Value::Number(serde_yaml::Number::from(expires_at)),
        )?;

        if let Some(label) = subscription_label {
            self.set_value(
                &["accounts", "qobuz", "subscription_label"],
                Value::String(label.to_string()),
            )?;
        }

        Ok(())
    }

    fn clear_qobuz_auth_info(&self) -> Result<()> {
        // On ne propage pas les erreurs car les valeurs peuvent ne pas exister
        let _ = self.set_value(
            &["accounts", "qobuz", "auth_token"],
            Value::String(String::new()),
        );
        let _ = self.set_value(
            &["accounts", "qobuz", "user_id"],
            Value::String(String::new()),
        );
        let _ = self.set_value(
            &["accounts", "qobuz", "token_expires_at"],
            Value::Number(serde_yaml::Number::from(0)),
        );
        let _ = self.set_value(
            &["accounts", "qobuz", "subscription_label"],
            Value::String(String::new()),
        );
        Ok(())
    }

    fn is_qobuz_auth_valid(&self) -> bool {
        let token_present = self
            .get_qobuz_auth_token()
            .ok()
            .flatten()
            .map(|token| !token.is_empty())
            .unwrap_or(false);

        let user_present = self
            .get_qobuz_user_id()
            .ok()
            .flatten()
            .map(|user_id| !user_id.is_empty())
            .unwrap_or(false);

        token_present && user_present
    }

    fn get_qobuz_cache_dir(&self) -> Result<String> {
        self.get_managed_dir(&["host", "qobuz_cache", "directory"], "cache_qobuz")
    }

    fn set_qobuz_cache_dir(&self, directory: String) -> Result<()> {
        self.set_managed_dir(&["host", "qobuz_cache", "directory"], directory)
    }

    fn get_qobuz_rate_limit_max_concurrent(&self) -> Result<Option<usize>> {
        match self.get_value(&["accounts", "qobuz", "rate_limit", "max_concurrent"]) {
            Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap() as usize)),
            Ok(_) => Ok(None),
            Err(_) => Ok(Some(2)), // Default: 2 concurrent requests
        }
    }

    fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "rate_limit", "max_concurrent"],
            Value::Number(serde_yaml::Number::from(max)),
        )
    }

    fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result<Option<u64>> {
        match self.get_value(&["accounts", "qobuz", "rate_limit", "min_delay_ms"]) {
            Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap())),
            Ok(_) => Ok(None),
            Err(_) => Ok(Some(400)), // Default: 400ms
        }
    }

    fn set_qobuz_rate_limit_min_delay_ms(&self, delay_ms: u64) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "rate_limit", "min_delay_ms"],
            Value::Number(serde_yaml::Number::from(delay_ms)),
        )
    }

    fn is_qobuz_rate_limiting_enabled(&self) -> bool {
        match self.get_value(&["accounts", "qobuz", "rate_limit", "enabled"]) {
            Ok(Value::Bool(b)) => b,
            _ => true, // Default: enabled
        }
    }

    fn set_qobuz_rate_limiting_enabled(&self, enabled: bool) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "rate_limit", "enabled"],
            Value::Bool(enabled),
        )
    }

    fn get_qobuz_bundle_version(&self) -> Result<Option<String>> {
        match self.get_value(&["accounts", "qobuz", "bundle_version"]) {
            Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)),
            _ => Ok(None),
        }
    }

    fn set_qobuz_bundle_version(&self, version: &str) -> Result<()> {
        self.set_value(
            &["accounts", "qobuz", "bundle_version"],
            Value::String(version.to_string()),
        )
    }

    fn get_qobuz_register_concurrency(&self) -> usize {
        match self.get_value(&["accounts", "qobuz", "register_concurrency"]) {
            Ok(Value::Number(n)) if n.as_u64().unwrap_or(0) >= 1 => {
                n.as_u64().unwrap() as usize
            }
            _ => 4,
        }
    }

    fn get_qobuz_page_concurrency(&self) -> usize {
        match self.get_value(&["accounts", "qobuz", "page_concurrency"]) {
            Ok(Value::Number(n)) if n.as_u64().unwrap_or(0) >= 1 => {
                n.as_u64().unwrap() as usize
            }
            _ => 3,
        }
    }
}
