use crate::config::ensure_parent_exists;
use crate::sandbox::SANDBOX_ENV_FLAG;
use crate::vault::{SECRET_RE, Vault};
use anyhow::Result;
use anyhow::anyhow;
use gman::SecretError;
use gman::providers::SupportedProvider;
use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
use gman::providers::azure_key_vault::AzureKeyVaultProvider;
use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
use gman::providers::gopass::GopassProvider;
use gman::providers::local::LocalProvider;
use gman::providers::one_password::OnePasswordProvider;
use indoc::formatdoc;
use inquire::validator::Validation;
use inquire::{Confirm, Password, PasswordDisplayMode, Select, Text, min_length, required};
use log::debug;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> Result<()> {
    let vault_password_file = local_provider
        .password_file
        .clone()
        .ok_or_else(|| anyhow!("Password file is not configured"))?;

    if vault_password_file.exists() {
        {
            let file_contents = std::fs::read_to_string(&vault_password_file)?;
            if !file_contents.trim().is_empty() {
                Ok(())
            } else {
                Err(anyhow!(
                    "The configured password file '{}' is empty. Please populate it with a password and try again.",
                    vault_password_file.display()
                ))
            }
        }
    } else {
        Err(anyhow!(
            "A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
        ))
    }
}

pub fn create_vault_password_file(vault: &mut Vault) -> Result<()> {
    let SupportedProvider::Local {
        provider_def: local_provider,
    } = &mut vault.provider
    else {
        return Ok(());
    };

    let vault_password_file = local_provider
        .password_file
        .clone()
        .ok_or_else(|| anyhow!("Password file is not configured"))?;

    if vault_password_file.exists() {
        {
            let file_contents = std::fs::read_to_string(&vault_password_file)?;
            if !file_contents.trim().is_empty() {
                debug!(
                    "create_vault_password_file was called but the password file already exists and is non-empty"
                );
                return Ok(());
            }
        }

        let ans = Confirm::new(
            format!(
                "The configured password file '{}' is empty. Create a password?",
                vault_password_file.display()
            )
            .as_str(),
        )
        .with_default(true)
        .prompt()?;

        if !ans {
            return Err(anyhow!(
                "The configured password file '{}' is empty. Please populate it with a password and try again.",
                vault_password_file.display()
            ));
        }

        let password = Password::new("Enter a password to encrypt all vault secrets:")
            .with_validator(required!())
            .with_validator(min_length!(10))
            .with_display_mode(PasswordDisplayMode::Masked)
            .prompt();

        match password {
            Ok(pw) => {
                std::fs::write(&vault_password_file, pw.as_bytes())?;
                set_password_file_permissions(&vault_password_file)?;
                println!(
                    "✓ Password file '{}' updated.",
                    vault_password_file.display()
                );
            }
            Err(_) => {
                return Err(anyhow!(
                    "Failed to read password from input. Password file not updated."
                ));
            }
        }
    } else {
        let ans = Confirm::new("No password file configured. Do you want to create one now?")
            .with_default(true)
            .prompt()?;

        if !ans {
            return Err(anyhow!(
                "A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
            ));
        }

        let password_file: PathBuf = Text::new("Enter the path to the password file to create:")
            .with_default(&vault_password_file.display().to_string())
            .with_validator(required!("Password file path is required"))
            .with_validator(|input: &str| {
                let path = PathBuf::from(input);
                if path.exists() {
                    Ok(Validation::Invalid(
                        "File already exists. Please choose a different path.".into(),
                    ))
                } else if let Some(parent) = path.parent() {
                    if !parent.exists() {
                        Ok(Validation::Invalid(
                            "Parent directory does not exist.".into(),
                        ))
                    } else {
                        Ok(Validation::Valid)
                    }
                } else {
                    Ok(Validation::Valid)
                }
            })
            .prompt()?
            .into();

        if password_file != vault_password_file {
            debug!(
                "{}",
                formatdoc!(
                    "
										The default password file path is '{}'.
										User chose to create file at a different path: '{}'.
										",
                    vault_password_file.display(),
                    password_file.display()
                )
            );
        }

        ensure_parent_exists(&password_file)?;

        let password = Password::new("Enter a password to encrypt all vault secrets:")
            .with_display_mode(PasswordDisplayMode::Masked)
            .with_validator(required!())
            .with_validator(min_length!(10))
            .prompt();

        match password {
            Ok(pw) => {
                std::fs::write(&password_file, pw.as_bytes())?;
                set_password_file_permissions(&password_file)?;
                local_provider.password_file = Some(password_file);
                println!(
                    "✓ Password file '{}' created.",
                    vault_password_file.display()
                );
            }
            Err(_) => {
                return Err(anyhow!(
                    "Failed to read password from input. Password file not created."
                ));
            }
        }
    }

    Ok(())
}

pub fn prompt_provider_choice() -> Result<Option<SupportedProvider>> {
    let choices = vec![
        "local - encrypted file on this machine",
        "aws_secrets_manager - AWS Secrets Manager",
        "gcp_secret_manager - Google Cloud Secret Manager",
        "azure_key_vault - Azure Key Vault",
        "gopass - gopass password manager (requires the `gopass` CLI)",
        "one_password - 1Password (requires the `op` CLI)",
    ];
    let choice = Select::new("Which secrets provider would you like to use?", choices)
        .with_starting_cursor(0)
        .prompt()?;

    if choice.starts_with("local") {
        return Ok(None);
    }

    let provider = if choice.starts_with("aws_secrets_manager") {
        prompt_aws_provider()?
    } else if choice.starts_with("gcp_secret_manager") {
        prompt_gcp_provider()?
    } else if choice.starts_with("azure_key_vault") {
        prompt_azure_provider()?
    } else if choice.starts_with("gopass") {
        prompt_gopass_provider()?
    } else if choice.starts_with("one_password") {
        prompt_one_password_provider()?
    } else {
        return Err(anyhow!("unexpected provider choice: {choice}"));
    };

    Ok(Some(provider))
}

fn prompt_aws_provider() -> Result<SupportedProvider> {
    let aws_profile = Text::new("AWS profile name:")
        .with_default("default")
        .with_validator(required!())
        .with_help_message("From your ~/.aws/config and ~/.aws/credentials")
        .prompt()?;
    let aws_region = Text::new("AWS region:")
        .with_default("us-east-1")
        .with_validator(required!())
        .with_help_message("Where your secrets live (e.g. us-east-1, eu-west-2)")
        .prompt()?;

    advisory_preflight(
        "AWS",
        "aws",
        &["sts", "get-caller-identity", "--profile", &aws_profile],
    );

    Ok(SupportedProvider::AwsSecretsManager {
        provider_def: AwsSecretsManagerProvider {
            aws_profile: Some(aws_profile),
            aws_region: Some(aws_region),
        },
    })
}

fn prompt_gcp_provider() -> Result<SupportedProvider> {
    let gcp_project_id = Text::new("GCP project ID:")
        .with_validator(required!())
        .with_help_message("The project that hosts your Secret Manager secrets")
        .prompt()?;

    advisory_preflight(
        "GCP",
        "gcloud",
        &["auth", "application-default", "print-access-token"],
    );

    Ok(SupportedProvider::GcpSecretManager {
        provider_def: GcpSecretManagerProvider {
            gcp_project_id: Some(gcp_project_id),
        },
    })
}

fn prompt_azure_provider() -> Result<SupportedProvider> {
    let vault_name = Text::new("Azure Key Vault name:")
        .with_validator(required!())
        .with_help_message("Just the vault name; the https endpoint is auto-derived")
        .prompt()?;

    advisory_preflight("Azure", "az", &["account", "show"]);

    Ok(SupportedProvider::AzureKeyVault {
        provider_def: AzureKeyVaultProvider {
            vault_name: Some(vault_name),
        },
    })
}

fn prompt_gopass_provider() -> Result<SupportedProvider> {
    let store_raw = Text::new("gopass store (leave blank for default):").prompt()?;
    let store = match store_raw.trim() {
        "" => None,
        s => Some(s.to_string()),
    };

    required_cli_preflight("gopass", "gopass", "https://www.gopass.pw/");

    Ok(SupportedProvider::Gopass {
        provider_def: GopassProvider { store },
    })
}

fn prompt_one_password_provider() -> Result<SupportedProvider> {
    let vault_raw = Text::new("1Password vault (leave blank for default):").prompt()?;
    let vault = match vault_raw.trim() {
        "" => None,
        s => Some(s.to_string()),
    };

    let account_raw = Text::new("1Password account (leave blank for default):").prompt()?;
    let account = match account_raw.trim() {
        "" => None,
        s => Some(s.to_string()),
    };

    required_cli_preflight(
        "1Password CLI",
        "op",
        "https://developer.1password.com/docs/cli/",
    );

    Ok(SupportedProvider::OnePassword {
        provider_def: OnePasswordProvider { vault, account },
    })
}

fn advisory_preflight(label: &str, cli: &str, args: &[&str]) {
    match Command::new(cli).args(args).output() {
        Ok(out) if out.status.success() => {
            println!("✓ {label} authentication check succeeded.");
        }
        Ok(out) => {
            let stderr = String::from_utf8_lossy(&out.stderr);
            eprintln!("⚠️  {label} preflight returned non-zero:");
            if !stderr.trim().is_empty() {
                eprintln!("    {}", stderr.trim());
            }
            eprintln!("    Setup will continue. Fix authentication before using --add-secret etc.");
        }
        Err(_) => {
            eprintln!(
                "⚠️  `{cli}` CLI not found on PATH. Coyote will still try the {label} SDK directly via standard credentials (env vars, instance metadata, service-account JSON, etc.)."
            );
        }
    }
}

fn required_cli_preflight(label: &str, cli: &str, install_url: &str) {
    match Command::new(cli).arg("--version").output() {
        Ok(out) if out.status.success() => {
            println!("✓ {label} is installed and reachable.");
        }
        Ok(_) => {
            eprintln!(
                "⚠️  `{cli} --version` returned non-zero. Your {label} install may be broken — verify before using the vault."
            );
        }
        Err(_) => {
            eprintln!("⚠️  `{cli}` not found on PATH.");
            eprintln!(
                "    The {label} secrets provider requires it. Install from {install_url} before running --add-secret etc."
            );
        }
    }
}

pub fn interpolate_secrets(content: &str, vault: &Vault) -> Result<(String, Vec<String>)> {
    if env::var_os(SANDBOX_ENV_FLAG).is_some() {
        return Ok((content.to_string(), vec![]));
    }
    interpolate_secrets_with(content, vault.auth_hint(), |name| {
        vault.get_secret(name, false)
    })
}

fn interpolate_secrets_with<F>(
    content: &str,
    auth_hint: Option<&'static str>,
    mut get_secret: F,
) -> Result<(String, Vec<String>)>
where
    F: FnMut(&str) -> Result<String>,
{
    let mut missing_secrets = vec![];
    let mut fatal_error: Option<anyhow::Error> = None;

    let parsed_content: String = content
        .lines()
        .map(|line| {
            if line.trim_start().starts_with('#') || fatal_error.is_some() {
                return line.to_string();
            }

            SECRET_RE
                .replace_all(line, |caps: &fancy_regex::Captures<'_>| {
                    if fatal_error.is_some() {
                        return String::new();
                    }

                    let name = caps[1].trim();
                    match get_secret(name) {
                        Ok(s) => s,
                        Err(e) => match e.downcast_ref::<SecretError>() {
                            Some(SecretError::NotFound { .. }) => {
                                missing_secrets.push(name.to_string());
                                String::new()
                            }
                            Some(SecretError::AuthFailed { .. }) => {
                                let base =
                                    format!("Failed to fetch secret '{name}' from vault: {e}");
                                let msg = match auth_hint {
                                    Some(hint) => format!("{base}\n\nHint: {hint}"),
                                    None => base,
                                };
                                fatal_error = Some(anyhow!("{msg}"));
                                String::new()
                            }
                            _ => {
                                fatal_error = Some(anyhow!(
                                    "Failed to fetch secret '{name}' from vault: {e}"
                                ));
                                String::new()
                            }
                        },
                    }
                })
                .to_string()
        })
        .collect::<Vec<_>>()
        .join("\n");

    if let Some(err) = fatal_error {
        return Err(err);
    }

    Ok((parsed_content, missing_secrets))
}

#[cfg(unix)]
fn set_password_file_permissions(path: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(|e| {
        anyhow!(
            "Failed to set 0600 permissions on '{}': {e}",
            path.display()
        )
    })
}

#[cfg(not(unix))]
fn set_password_file_permissions(_path: &Path) -> Result<()> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Error;
    use std::cell::RefCell;

    fn not_found(name: &str) -> Error {
        Error::new(SecretError::NotFound {
            key: name.to_string(),
            provider: "test",
        })
    }

    fn auth_failed() -> Error {
        Error::new(SecretError::AuthFailed {
            provider: "test",
            source: anyhow!("auth failure"),
        })
    }

    struct Calls(RefCell<Vec<String>>);

    impl Calls {
        fn new() -> Self {
            Self(RefCell::new(Vec::new()))
        }

        fn record(&self, name: &str) {
            self.0.borrow_mut().push(name.to_string());
        }

        fn snapshot(&self) -> Vec<String> {
            self.0.borrow().clone()
        }
    }

    #[test]
    fn interpolates_single_secret_per_line() {
        let (out, missing) =
            interpolate_secrets_with("api_key={{API_KEY}}", None, |name| match name {
                "API_KEY" => Ok("sk-12345".to_string()),
                other => panic!("unexpected lookup: {other}"),
            })
            .unwrap();

        assert_eq!(out, "api_key=sk-12345");
        assert!(missing.is_empty());
    }

    #[test]
    fn regex_matches_each_secret_independently_when_one_per_line() {
        let calls = Calls::new();
        let (out, missing) = interpolate_secrets_with("{{ONE}}\nmiddle\n{{TWO}}", None, |name| {
            calls.record(name);
            Ok(name.to_lowercase())
        })
        .unwrap();

        assert_eq!(calls.snapshot(), vec!["ONE".to_string(), "TWO".to_string()]);
        assert_eq!(out, "one\nmiddle\ntwo");
        assert!(missing.is_empty());
    }

    #[test]
    fn skips_comment_lines() {
        let calls = Calls::new();

        let (out, missing) =
            interpolate_secrets_with("# api_key={{NEVER_FETCHED}}\nreal={{S}}", None, |name| {
                calls.record(name);
                Ok("v".to_string())
            })
            .unwrap();

        assert_eq!(out, "# api_key={{NEVER_FETCHED}}\nreal=v");
        assert!(missing.is_empty());
        assert_eq!(calls.snapshot(), vec!["S".to_string()]);
    }

    #[test]
    fn missing_secrets_become_empty_strings_and_are_reported() {
        let (out, missing) = interpolate_secrets_with(
            "a={{HAVE}}\nb={{MISSING_1}}\nc={{MISSING_2}}",
            None,
            |name| match name {
                "HAVE" => Ok("present".to_string()),
                missing => Err(not_found(missing)),
            },
        )
        .unwrap();

        assert_eq!(out, "a=present\nb=\nc=");
        assert_eq!(
            missing,
            vec!["MISSING_1".to_string(), "MISSING_2".to_string()]
        );
    }

    #[test]
    fn interpolates_multiple_secrets_on_same_line() {
        let calls = Calls::new();

        let (out, missing) = interpolate_secrets_with("url={{URL}} key={{KEY}}", None, |name| {
            calls.record(name);
            match name {
                "URL" => Ok("https://example.test".to_string()),
                "KEY" => Ok("sk-12345".to_string()),
                other => panic!("unexpected lookup: {other}"),
            }
        })
        .unwrap();

        assert_eq!(calls.snapshot(), vec!["URL".to_string(), "KEY".to_string()]);
        assert_eq!(out, "url=https://example.test key=sk-12345");
        assert!(missing.is_empty());
    }

    #[test]
    fn regex_rejects_braces_in_secret_names() {
        let calls = Calls::new();

        let (out, missing) =
            interpolate_secrets_with("literal {{ {NOT_A_NAME} }} text", None, |name| {
                calls.record(name);
                Ok(format!("got-{name}"))
            })
            .unwrap();

        assert!(
            calls.snapshot().is_empty(),
            "name with embedded braces must not match"
        );
        assert_eq!(out, "literal {{ {NOT_A_NAME} }} text");
        assert!(missing.is_empty());
    }

    #[test]
    fn fatal_failure_short_circuits_remaining_lines() {
        let calls = Calls::new();

        let result =
            interpolate_secrets_with("a={{S1}}\nb={{S2}}\nc={{S3}}\nd={{S4}}", None, |name| {
                calls.record(name);
                match name {
                    "S1" => Ok("first".to_string()),
                    "S2" => Err(auth_failed()),
                    other => Ok(format!("late-{other}")),
                }
            });

        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("S2"),
            "error should name the offending secret, got: {err}"
        );
        assert_eq!(
            calls.snapshot(),
            vec!["S1".to_string(), "S2".to_string()],
            "lookups must stop at the failing secret - S3 and S4 should never be fetched"
        );
    }

    #[test]
    fn auth_failure_appends_hint_when_provided() {
        let result = interpolate_secrets_with(
            "k={{K}}",
            Some("run `coyote --authenticate` to reauth"),
            |_| Err(auth_failed()),
        );

        let err = result.unwrap_err().to_string();

        assert!(err.contains("Hint:"), "expected hint in error, got: {err}");
        assert!(
            err.contains("coyote --authenticate"),
            "expected hint contents, got: {err}"
        );
    }
}
