//! Target registry and adapter trait.
//! Each provider implements `TargetAdapter`, and this module dispatches based on CLI-selected target.

#[cfg(feature = "github")]
mod github;

#[cfg(feature = "vercel")]
mod vercel;

#[cfg(feature = "fly")]
mod fly;

#[cfg(not(any(feature = "github", feature = "vercel", feature = "fly")))]
compile_error!("No targets enabled. Enable at least one of: \"github\", \"vercel\", \"fly\".");

use anyhow::Result;
use clap::ValueEnum;
use std::collections::HashMap;
use std::fmt;

/// Supported remote targets (feature-gated).
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum Target {
    #[cfg(feature = "github")]
    Github,
    #[cfg(feature = "vercel")]
    Vercel,
    #[cfg(feature = "fly")]
    Fly,
}

impl fmt::Display for Target {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            #[cfg(feature = "github")]
            Target::Github => "github",
            #[cfg(feature = "vercel")]
            Target::Vercel => "vercel",
            #[cfg(feature = "fly")]
            Target::Fly => "fly",
        };
        write!(f, "{}", s)
    }
}

pub struct PushOptions {
    pub repo: Option<String>,    // for GitHub
    pub project: Option<String>, // for Vercel
    pub app: Option<String>,     // for Fly.io
    pub env: Option<String>,     // cred environment name
}

#[allow(async_fn_in_trait)] // Async in trait is crate-internal; we accept the bound
/// Common interface every target must implement.
/// Defaults are conservative: unsupported operations return errors unless overridden.
pub trait TargetAdapter {
    /// Human-readable target name.
    fn name(&self) -> &str;

    /// Push secrets to the target. Default errors out for non-hosting targets.
    async fn push(
        &self,
        _secrets: &HashMap<String, String>,
        _auth_token: &str,
        _options: &PushOptions,
    ) -> Result<()> {
        anyhow::bail!(
            "Target '{}' is not a hosting platform; you cannot push secrets to it.",
            self.name()
        );
    }

    /// Delete secrets from the target. Default errors out for non-hosting targets.
    async fn delete(
        &self,
        _keys: &[String],
        _auth_token: &str,
        _options: &PushOptions,
    ) -> Result<()> {
        anyhow::bail!(
            "Target '{}' is not a hosting platform; you cannot prune secrets from it.",
            self.name()
        );
    }

    #[allow(dead_code)]
    /// Optionally generate a new API key/token for an environment.
    async fn generate(&self, _env: &str, _auth_token: &str) -> Result<(String, String)> {
        anyhow::bail!(
            "Target '{}' does not support API key generation.",
            self.name()
        );
    }

    /// Revoke a specific secret/key value at the target.
    async fn revoke_secret(
        &self,
        _key_name: &str,
        _key_value: &str,
        _auth_token: &str,
    ) -> Result<()> {
        anyhow::bail!(
            "Target '{}' does not support API key revocation.",
            self.name()
        );
    }

    /// Revoke the authentication token used for this target (if supported).
    async fn revoke_auth_token(&self, _auth_token: &str) -> Result<()> {
        Ok(()) // Default to ok (allows local logout)
    }
}

/// Concrete wrapper to erase target-specific types while dispatching calls.
pub enum TargetWrapper {
    #[cfg(feature = "github")]
    Github(github::Github),
    #[cfg(feature = "vercel")]
    Vercel(vercel::Vercel),
    #[cfg(feature = "fly")]
    Fly(fly::Fly),
}

impl TargetAdapter for TargetWrapper {
    fn name(&self) -> &str {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.name(),
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.name(),
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.name(),
        }
    }

    async fn push(
        &self,
        secrets: &HashMap<String, String>,
        auth_token: &str,
        options: &PushOptions,
    ) -> Result<()> {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.push(secrets, auth_token, options).await,
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.push(secrets, auth_token, options).await,
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.push(secrets, auth_token, options).await,
        }
    }

    async fn delete(&self, keys: &[String], auth_token: &str, options: &PushOptions) -> Result<()> {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.delete(keys, auth_token, options).await,
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.delete(keys, auth_token, options).await,
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.delete(keys, auth_token, options).await,
        }
    }

    async fn generate(&self, env: &str, auth_token: &str) -> Result<(String, String)> {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.generate(env, auth_token).await,
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.generate(env, auth_token).await,
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.generate(env, auth_token).await,
        }
    }

    async fn revoke_secret(&self, key_name: &str, key_value: &str, auth_token: &str) -> Result<()> {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.revoke_secret(key_name, key_value, auth_token).await,
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.revoke_secret(key_name, key_value, auth_token).await,
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.revoke_secret(key_name, key_value, auth_token).await,
        }
    }

    async fn revoke_auth_token(&self, auth_token: &str) -> Result<()> {
        match self {
            #[cfg(feature = "github")]
            Self::Github(p) => p.revoke_auth_token(auth_token).await,
            #[cfg(feature = "vercel")]
            Self::Vercel(p) => p.revoke_auth_token(auth_token).await,
            #[cfg(feature = "fly")]
            Self::Fly(p) => p.revoke_auth_token(auth_token).await,
        }
    }
}

pub fn get(name: Target) -> Option<TargetWrapper> {
    match name {
        #[cfg(feature = "github")]
        Target::Github => Some(TargetWrapper::Github(github::Github)),
        #[cfg(feature = "vercel")]
        Target::Vercel => Some(TargetWrapper::Vercel(vercel::Vercel)),
        #[cfg(feature = "fly")]
        Target::Fly => Some(TargetWrapper::Fly(fly::Fly)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockTarget;
    impl TargetAdapter for MockTarget {
        fn name(&self) -> &str {
            "mock_target"
        }
        async fn generate(&self, _env: &str, _token: &str) -> Result<(String, String)> {
            Ok(("KEY".to_string(), "VAL".to_string()))
        }
    }

    #[cfg(feature = "github")]
    #[test]
    fn test_factory_returns_github() {
        let p = get(Target::Github);
        assert!(p.is_some());
        assert_eq!(p.unwrap().name(), "github");
    }

    #[cfg(feature = "vercel")]
    #[test]
    fn test_factory_returns_vercel() {
        let p = get(Target::Vercel);
        assert!(p.is_some());
        assert_eq!(p.unwrap().name(), "vercel");
    }

    #[cfg(feature = "fly")]
    #[test]
    fn test_factory_returns_fly() {
        let p = get(Target::Fly);
        assert!(p.is_some());
        assert_eq!(p.unwrap().name(), "fly");
    }

    #[tokio::test]
    async fn test_trait_defaults_prevent_invalid_usage() {
        let p = MockTarget;
        let secrets = HashMap::new();
        let options = PushOptions {
            repo: None,
            project: None,
            app: None,
            env: None,
        };

        let push_result = p.push(&secrets, "token", &options).await;
        assert!(push_result.is_err());
        assert!(
            push_result
                .unwrap_err()
                .to_string()
                .contains("not a hosting platform")
        );

        let delete_result = p.delete(&[], "token", &options).await;
        assert!(delete_result.is_err());
        assert!(
            delete_result
                .unwrap_err()
                .to_string()
                .contains("not a hosting platform")
        );
    }

    #[cfg(feature = "github")]
    #[tokio::test]
    async fn test_target_wrapper_dispatch() {
        let p = get(Target::Github).unwrap();
        assert_eq!(p.name(), "github");
    }
}
