use std::collections::HashMap;
use std::path::Path;

use fabro_static::EnvVars;
use fabro_types::settings::ServerNamespace;
use fabro_vault::Vault;
use tracing::warn;

use crate::jwt_auth::{AuthMode, resolve_auth_mode_with_lookup, validate_auth_configuration};
use crate::migrations;
use crate::server_secrets::ServerSecrets;

pub(crate) fn resolve_startup(
    env_path: &Path,
    env_entries: HashMap<String, String>,
    settings: &ServerNamespace,
    vault: &Vault,
) -> anyhow::Result<(AuthMode, ServerSecrets)> {
    let server_secrets = ServerSecrets::load(env_path, env_entries)?;
    let auth_secret_lookup = |name: &str| match name {
        EnvVars::GITHUB_APP_CLIENT_SECRET => vault.get(name).map(str::to_string),
        _ => server_secrets.get(name),
    };
    let auth_mode = resolve_auth_mode_with_lookup(settings, auth_secret_lookup)?;
    Ok((auth_mode, server_secrets))
}

pub fn migrate_startup_vault(vault_path: impl AsRef<Path>) {
    let vault_path = vault_path.as_ref();
    match migrations::migrate_legacy_vault_file(vault_path) {
        Ok(report) if report.changed() => {
            let backup_path = report
                .backup_path
                .as_ref()
                .map_or_else(|| "<none>".to_string(), |path| path.display().to_string());
            warn!(
                migrated_entries = report.migrated_entries,
                skipped_entries = report.skipped_entries,
                backup_path = %backup_path,
                removal_deadline = migrations::LEGACY_VAULT_REMOVAL_DEADLINE,
                "Migrated legacy vault file"
            );
        }
        Ok(_) => {}
        Err(err) => {
            warn!(
                error = %err,
                removal_deadline = migrations::LEGACY_VAULT_REMOVAL_DEADLINE,
                "Legacy vault migration failed; continuing with normal vault load"
            );
        }
    }
}

pub fn validate_startup(
    env_path: &Path,
    env_entries: HashMap<String, String>,
    settings: &ServerNamespace,
    vault: &Vault,
) -> anyhow::Result<()> {
    resolve_startup(env_path, env_entries, settings, vault).map(|_| ())
}

pub fn validate_startup_configuration(settings: &ServerNamespace) -> anyhow::Result<()> {
    validate_auth_configuration(settings)
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::path::{Path, PathBuf};

    use fabro_config::{ServerSettingsBuilder, envfile};
    use fabro_static::EnvVars;
    use fabro_types::settings::ServerNamespace;
    use fabro_vault::{SecretStore, SecretType, Vault};

    use super::validate_startup;
    use crate::migrations;

    fn resolved_settings(auth_methods: &[&str]) -> ServerNamespace {
        ServerSettingsBuilder::from_toml(&format!(
            r#"
_version = 1

[server.auth]
methods = [{}]

[server.auth.github]
allowed_usernames = ["octocat"]

[server.integrations.github]
client_id = "Iv1.test"
"#,
            auth_methods
                .iter()
                .map(|method| format!("\"{method}\""))
                .collect::<Vec<_>>()
                .join(", ")
        ))
        .unwrap()
        .server
    }

    fn empty_vault(dir: &tempfile::TempDir) -> Vault {
        Vault::load(dir.path().join("secrets.json")).unwrap()
    }

    fn env_path(dir: &tempfile::TempDir) -> PathBuf {
        dir.path().join("server.env")
    }

    async fn test_secret_store(dir: &tempfile::TempDir) -> SecretStore {
        let database = fabro_db::Database::connect(dir.path().join("fabro.db"))
            .await
            .unwrap();
        database.migrate().await.unwrap();
        SecretStore::new(database.clone_pool())
    }

    #[expect(
        clippy::disallowed_methods,
        reason = "test helper scans a temporary directory after startup migration completes"
    )]
    fn migration_backups(dir: &Path) -> Vec<PathBuf> {
        std::fs::read_dir(dir)
            .unwrap()
            .map(|entry| entry.unwrap().path())
            .filter(|path| {
                path.file_name()
                    .and_then(|name| name.to_str())
                    .is_some_and(|name| {
                        name.contains("optional-server-env-secrets-to-vault-migration")
                    })
            })
            .collect()
    }

    #[test]
    fn validate_startup_accepts_configured_secrets() {
        let dir = tempfile::tempdir().unwrap();
        let vault = empty_vault(&dir);
        let env = HashMap::from([
            (
                EnvVars::SESSION_SECRET.to_string(),
                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
            ),
            (
                EnvVars::FABRO_DEV_TOKEN.to_string(),
                "fabro_dev_abababababababababababababababababababababababababababababababab"
                    .to_string(),
            ),
        ]);
        let settings = resolved_settings(&["dev-token"]);

        assert!(
            validate_startup(
                dir.path().join("server.env").as_path(),
                env,
                &settings,
                &vault,
            )
            .is_ok()
        );
    }

    #[test]
    fn validate_startup_rejects_missing_secrets() {
        let dir = tempfile::tempdir().unwrap();
        let settings = resolved_settings(&["dev-token"]);
        let vault = empty_vault(&dir);

        assert!(
            validate_startup(
                dir.path().join("server.env").as_path(),
                HashMap::new(),
                &settings,
                &vault,
            )
            .is_err()
        );
    }

    #[test]
    fn validate_startup_requires_github_client_secret_from_vault() {
        let dir = tempfile::tempdir().unwrap();
        let env = HashMap::from([
            (
                EnvVars::SESSION_SECRET.to_string(),
                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
            ),
            (
                EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
                "server-env-client-secret".to_string(),
            ),
        ]);
        let settings = resolved_settings(&["github"]);
        let vault = empty_vault(&dir);

        let err = validate_startup(
            dir.path().join("server.env").as_path(),
            env,
            &settings,
            &vault,
        )
        .expect_err("github client secret in server.env should not satisfy startup");

        assert!(err.to_string().contains("GITHUB_APP_CLIENT_SECRET"));
    }

    #[test]
    fn validate_startup_accepts_github_client_secret_from_vault() {
        let dir = tempfile::tempdir().unwrap();
        let env = HashMap::from([(
            EnvVars::SESSION_SECRET.to_string(),
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
        )]);
        let settings = resolved_settings(&["github"]);
        let mut vault = empty_vault(&dir);
        vault
            .set(
                EnvVars::GITHUB_APP_CLIENT_SECRET,
                "vault-client-secret",
                SecretType::Token,
                None,
            )
            .unwrap();

        validate_startup(
            dir.path().join("server.env").as_path(),
            env,
            &settings,
            &vault,
        )
        .expect("github client secret in vault should satisfy startup");
    }

    #[tokio::test]
    async fn migrate_optional_secrets_moves_server_env_secrets_to_store() {
        let dir = tempfile::tempdir().unwrap();
        let server_env_path = env_path(&dir);
        envfile::write_env_file(
            &server_env_path,
            &HashMap::from([
                (
                    EnvVars::SESSION_SECRET.to_string(),
                    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
                ),
                (
                    EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
                    "legacy-client-secret".to_string(),
                ),
                (
                    EnvVars::GITHUB_APP_PRIVATE_KEY.to_string(),
                    "legacy-private-key".to_string(),
                ),
                (EnvVars::OPENAI_API_KEY.to_string(), "sk-legacy".to_string()),
            ]),
        )
        .unwrap();
        let store = test_secret_store(&dir).await;

        migrations::migrate_optional_server_env_secrets_to_store(
            &store,
            &server_env_path,
            &HashMap::new(),
        )
        .await
        .expect("legacy optional secrets should migrate");

        let client_secret = store
            .get(EnvVars::GITHUB_APP_CLIENT_SECRET)
            .await
            .unwrap()
            .expect("client secret should be stored");
        assert_eq!(client_secret.value, "legacy-client-secret");
        assert_eq!(client_secret.secret_type, SecretType::Token);
        let private_key = store
            .get(EnvVars::GITHUB_APP_PRIVATE_KEY)
            .await
            .unwrap()
            .expect("private key should be stored");
        assert_eq!(private_key.value, "legacy-private-key");
        assert_eq!(private_key.secret_type, SecretType::File);
        let openai_key = store
            .get(EnvVars::OPENAI_API_KEY)
            .await
            .unwrap()
            .expect("openai key should be stored");
        assert_eq!(openai_key.value, "sk-legacy");

        let server_env = envfile::read_env_file(&server_env_path).unwrap();
        assert!(server_env.contains_key(EnvVars::SESSION_SECRET));
        assert!(!server_env.contains_key(EnvVars::GITHUB_APP_CLIENT_SECRET));
        assert!(!server_env.contains_key(EnvVars::GITHUB_APP_PRIVATE_KEY));
        assert!(!server_env.contains_key(EnvVars::OPENAI_API_KEY));
        assert_eq!(migration_backups(dir.path()).len(), 1);
    }

    #[tokio::test]
    async fn migrate_optional_secrets_prefers_process_env_and_preserves_conflicting_server_env() {
        let dir = tempfile::tempdir().unwrap();
        let server_env_path = env_path(&dir);
        envfile::write_env_file(
            &server_env_path,
            &HashMap::from([(
                EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
                "file-client-secret".to_string(),
            )]),
        )
        .unwrap();
        let env_entries = HashMap::from([(
            EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
            "process-client-secret".to_string(),
        )]);
        let store = test_secret_store(&dir).await;

        migrations::migrate_optional_server_env_secrets_to_store(
            &store,
            &server_env_path,
            &env_entries,
        )
        .await
        .expect("process env secret should migrate");

        let client_secret = store
            .get(EnvVars::GITHUB_APP_CLIENT_SECRET)
            .await
            .unwrap()
            .expect("client secret should be stored");
        assert_eq!(client_secret.value, "process-client-secret");
        let server_env = envfile::read_env_file(&server_env_path).unwrap();
        assert_eq!(
            server_env
                .get(EnvVars::GITHUB_APP_CLIENT_SECRET)
                .map(String::as_str),
            Some("file-client-secret")
        );
        assert!(migration_backups(dir.path()).is_empty());
    }

    #[tokio::test]
    async fn migrate_optional_secrets_keeps_existing_stored_secret_and_removes_matching_server_env()
    {
        let dir = tempfile::tempdir().unwrap();
        let server_env_path = env_path(&dir);
        envfile::write_env_file(
            &server_env_path,
            &HashMap::from([(
                EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
                "vault-client-secret".to_string(),
            )]),
        )
        .unwrap();
        let store = test_secret_store(&dir).await;
        store
            .set(
                EnvVars::GITHUB_APP_CLIENT_SECRET,
                "vault-client-secret",
                SecretType::Token,
                None,
            )
            .await
            .unwrap();

        migrations::migrate_optional_server_env_secrets_to_store(
            &store,
            &server_env_path,
            &HashMap::new(),
        )
        .await
        .expect("redundant server env secret should be cleaned up");

        let client_secret = store
            .get(EnvVars::GITHUB_APP_CLIENT_SECRET)
            .await
            .unwrap()
            .expect("client secret should be stored");
        assert_eq!(client_secret.value, "vault-client-secret");
        let server_env = envfile::read_env_file(&server_env_path).unwrap();
        assert!(!server_env.contains_key(EnvVars::GITHUB_APP_CLIENT_SECRET));
        assert_eq!(migration_backups(dir.path()).len(), 1);
    }

    #[tokio::test]
    async fn migrate_optional_secrets_migrated_github_client_secret_satisfies_startup() {
        let dir = tempfile::tempdir().unwrap();
        let server_env_path = env_path(&dir);
        envfile::write_env_file(
            &server_env_path,
            &HashMap::from([
                (
                    EnvVars::SESSION_SECRET.to_string(),
                    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
                ),
                (
                    EnvVars::GITHUB_APP_CLIENT_SECRET.to_string(),
                    "legacy-client-secret".to_string(),
                ),
            ]),
        )
        .unwrap();
        let settings = resolved_settings(&["github"]);
        let store = test_secret_store(&dir).await;

        migrations::migrate_optional_server_env_secrets_to_store(
            &store,
            &server_env_path,
            &HashMap::new(),
        )
        .await
        .expect("legacy github client secret should migrate");

        let vault = store.snapshot().await.unwrap().into_vault();
        validate_startup(&server_env_path, HashMap::new(), &settings, &vault)
            .expect("migrated github client secret should satisfy startup");
    }
}
