//! Bootstrap helpers for IronClaw.
//!
//! The only setting that truly needs disk persistence before the database is
//! available is `DATABASE_URL` (chicken-and-egg: can't connect to DB without
//! it). Everything else is auto-detected or read from env vars.
//!
//! File: `~/.ironclaw/.env` (standard dotenvy format)

use std::path::PathBuf;

/// Path to the IronClaw-specific `.env` file: `~/.ironclaw/.env`.
pub fn ironclaw_env_path() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".ironclaw")
        .join(".env")
}

/// Load env vars from `~/.ironclaw/.env` (in addition to the standard `.env`).
///
/// Call this **after** `dotenvy::dotenv()` so that the standard `./.env`
/// takes priority over `~/.ironclaw/.env`. dotenvy never overwrites
/// existing env vars, so the effective priority is:
///
///   explicit env vars > `./.env` > `~/.ironclaw/.env`
///
/// If `~/.ironclaw/.env` doesn't exist but the legacy `bootstrap.json` does,
/// extracts `DATABASE_URL` from it and writes the `.env` file (one-time
/// upgrade from the old config format).
pub fn load_ironclaw_env() {
    let path = ironclaw_env_path();

    if !path.exists() {
        // One-time upgrade: extract DATABASE_URL from legacy bootstrap.json
        migrate_bootstrap_json_to_env(&path);
    }

    if path.exists() {
        let _ = dotenvy::from_path(&path);
    }
}

/// If `bootstrap.json` exists, pull `database_url` out of it and write `.env`.
fn migrate_bootstrap_json_to_env(env_path: &std::path::Path) {
    let ironclaw_dir = env_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."));
    let bootstrap_path = ironclaw_dir.join("bootstrap.json");

    if !bootstrap_path.exists() {
        return;
    }

    let content = match std::fs::read_to_string(&bootstrap_path) {
        Ok(c) => c,
        Err(_) => return,
    };

    // Minimal parse: just grab database_url from the JSON
    let parsed: serde_json::Value = match serde_json::from_str(&content) {
        Ok(v) => v,
        Err(_) => return,
    };

    if let Some(url) = parsed.get("database_url").and_then(|v| v.as_str()) {
        if let Some(parent) = env_path.parent()
            && let Err(e) = std::fs::create_dir_all(parent)
        {
            eprintln!("Warning: failed to create {}: {}", parent.display(), e);
            return;
        }
        if let Err(e) = std::fs::write(env_path, format!("DATABASE_URL=\"{}\"\n", url)) {
            eprintln!("Warning: failed to migrate bootstrap.json to .env: {}", e);
            return;
        }
        rename_to_migrated(&bootstrap_path);
        eprintln!(
            "Migrated DATABASE_URL from bootstrap.json to {}",
            env_path.display()
        );
    }
}

/// Write database bootstrap vars to `~/.ironclaw/.env`.
///
/// These settings form the chicken-and-egg layer: they must be available
/// from the filesystem (env vars) BEFORE any database connection, because
/// they determine which database to connect to. Everything else is stored
/// in the database itself.
///
/// Creates the parent directory if it doesn't exist.
/// Values are double-quoted so that `#` (common in URL-encoded passwords)
/// and other shell-special characters are preserved by dotenvy.
pub fn save_bootstrap_env(vars: &[(&str, &str)]) -> std::io::Result<()> {
    let path = ironclaw_env_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let mut content = String::new();
    for (key, value) in vars {
        // Escape backslashes and double quotes to prevent env var injection
        // (e.g. a value containing `"\nINJECTED="x` would break out of quotes).
        let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
        content.push_str(&format!("{}=\"{}\"\n", key, escaped));
    }
    std::fs::write(&path, &content)?;
    restrict_file_permissions(&path)?;
    Ok(())
}

/// Update or add a single variable in `~/.ironclaw/.env`, preserving existing content.
///
/// Unlike `save_bootstrap_env` (which overwrites the entire file), this
/// reads the current `.env`, replaces the line for `key` if it exists,
/// or appends it otherwise. Use this when writing a single bootstrap var
/// outside the wizard (which manages the full set via `save_bootstrap_env`).
pub fn upsert_bootstrap_var(key: &str, value: &str) -> std::io::Result<()> {
    let path = ironclaw_env_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
    let new_line = format!("{}=\"{}\"", key, escaped);
    let prefix = format!("{}=", key);

    let existing = std::fs::read_to_string(&path).unwrap_or_default();

    let mut found = false;
    let mut result = String::new();
    for line in existing.lines() {
        if line.starts_with(&prefix) {
            if !found {
                result.push_str(&new_line);
                result.push('\n');
                found = true;
            }
            // Skip duplicate lines for this key
            continue;
        }
        result.push_str(line);
        result.push('\n');
    }

    if !found {
        result.push_str(&new_line);
        result.push('\n');
    }

    std::fs::write(&path, result)?;
    restrict_file_permissions(&path)?;
    Ok(())
}

/// Set restrictive file permissions (0o600) on Unix systems.
///
/// The `.env` file may contain database credentials and API keys,
/// so it should only be readable by the owner.
fn restrict_file_permissions(_path: &std::path::Path) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = std::fs::Permissions::from_mode(0o600);
        std::fs::set_permissions(_path, perms)?;
    }
    Ok(())
}

/// Write `DATABASE_URL` to `~/.ironclaw/.env`.
///
/// Convenience wrapper around `save_bootstrap_env` for single-value migration
/// paths. Prefer `save_bootstrap_env` for new code.
pub fn save_database_url(url: &str) -> std::io::Result<()> {
    save_bootstrap_env(&[("DATABASE_URL", url)])
}

/// One-time migration of legacy `~/.ironclaw/settings.json` into the database.
///
/// Only runs when a `settings.json` exists on disk AND the DB has no settings
/// yet. After the wizard writes directly to the DB, this path is only hit by
/// users upgrading from the old disk-only configuration.
///
/// After syncing, renames `settings.json` to `.migrated` so it won't trigger again.
pub async fn migrate_disk_to_db(
    store: &dyn crate::db::Database,
    user_id: &str,
) -> Result<(), MigrationError> {
    let ironclaw_dir = dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".ironclaw");
    let legacy_settings_path = ironclaw_dir.join("settings.json");

    if !legacy_settings_path.exists() {
        tracing::debug!("No legacy settings.json found, skipping disk-to-DB migration");
        return Ok(());
    }

    // If DB already has settings, this is not a first boot, the wizard already
    // wrote directly to the DB. Just clean up the stale file.
    let has_settings = store.has_settings(user_id).await.map_err(|e| {
        MigrationError::Database(format!("Failed to check existing settings: {}", e))
    })?;
    if has_settings {
        tracing::info!("DB already has settings, renaming stale settings.json");
        rename_to_migrated(&legacy_settings_path);
        return Ok(());
    }

    tracing::info!("Migrating disk settings to database...");

    // 1. Load and migrate settings.json
    let settings = crate::settings::Settings::load_from(&legacy_settings_path);
    let db_map = settings.to_db_map();
    if !db_map.is_empty() {
        store
            .set_all_settings(user_id, &db_map)
            .await
            .map_err(|e| {
                MigrationError::Database(format!("Failed to write settings to DB: {}", e))
            })?;
        tracing::info!("Migrated {} settings to database", db_map.len());
    }

    // 2. Write DATABASE_URL to ~/.ironclaw/.env
    if let Some(ref url) = settings.database_url {
        save_database_url(url)
            .map_err(|e| MigrationError::Io(format!("Failed to write .env: {}", e)))?;
        tracing::info!("Wrote DATABASE_URL to {}", ironclaw_env_path().display());
    }

    // 3. Migrate mcp-servers.json if it exists
    let mcp_path = ironclaw_dir.join("mcp-servers.json");
    if mcp_path.exists() {
        match std::fs::read_to_string(&mcp_path) {
            Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
                Ok(value) => {
                    store
                        .set_setting(user_id, "mcp_servers", &value)
                        .await
                        .map_err(|e| {
                            MigrationError::Database(format!(
                                "Failed to write MCP servers to DB: {}",
                                e
                            ))
                        })?;
                    tracing::info!("Migrated mcp-servers.json to database");

                    rename_to_migrated(&mcp_path);
                }
                Err(e) => {
                    tracing::warn!("Failed to parse mcp-servers.json: {}", e);
                }
            },
            Err(e) => {
                tracing::warn!("Failed to read mcp-servers.json: {}", e);
            }
        }
    }

    // 4. Migrate session.json if it exists
    let session_path = ironclaw_dir.join("session.json");
    if session_path.exists() {
        match std::fs::read_to_string(&session_path) {
            Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
                Ok(value) => {
                    store
                        .set_setting(user_id, "nearai.session_token", &value)
                        .await
                        .map_err(|e| {
                            MigrationError::Database(format!(
                                "Failed to write session to DB: {}",
                                e
                            ))
                        })?;
                    tracing::info!("Migrated session.json to database");

                    rename_to_migrated(&session_path);
                }
                Err(e) => {
                    tracing::warn!("Failed to parse session.json: {}", e);
                }
            },
            Err(e) => {
                tracing::warn!("Failed to read session.json: {}", e);
            }
        }
    }

    // 5. Rename settings.json to .migrated (don't delete, safety net)
    rename_to_migrated(&legacy_settings_path);

    // 6. Clean up old bootstrap.json if it exists (superseded by .env)
    let old_bootstrap = ironclaw_dir.join("bootstrap.json");
    if old_bootstrap.exists() {
        rename_to_migrated(&old_bootstrap);
        tracing::info!("Renamed old bootstrap.json to .migrated");
    }

    tracing::info!("Disk-to-DB migration complete");
    Ok(())
}

/// Rename a file to `<name>.migrated` as a safety net.
fn rename_to_migrated(path: &std::path::Path) {
    let mut migrated = path.as_os_str().to_owned();
    migrated.push(".migrated");
    if let Err(e) = std::fs::rename(path, &migrated) {
        tracing::warn!("Failed to rename {} to .migrated: {}", path.display(), e);
    }
}

/// Errors that can occur during disk-to-DB migration.
#[derive(Debug, thiserror::Error)]
pub enum MigrationError {
    #[error("Database error: {0}")]
    Database(String),
    #[error("IO error: {0}")]
    Io(String),
}

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

    #[test]
    fn test_save_and_load_database_url() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // Write in the quoted format that save_database_url uses
        let url = "postgres://localhost:5432/ironclaw_test";
        std::fs::write(&env_path, format!("DATABASE_URL=\"{}\"\n", url)).unwrap();

        // Verify the content is a valid dotenv line (quoted)
        let content = std::fs::read_to_string(&env_path).unwrap();
        assert_eq!(
            content,
            "DATABASE_URL=\"postgres://localhost:5432/ironclaw_test\"\n"
        );

        // Verify dotenvy can parse it (strips quotes automatically)
        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();
        assert_eq!(parsed.len(), 1);
        assert_eq!(parsed[0].0, "DATABASE_URL");
        assert_eq!(parsed[0].1, url);
    }

    #[test]
    fn test_save_database_url_with_hash_in_password() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // URLs with # in the password are common (URL-encoded special chars).
        // Without quoting, dotenvy treats # as a comment delimiter.
        let url = "postgres://user:p%23ss@localhost:5432/ironclaw";
        std::fs::write(&env_path, format!("DATABASE_URL=\"{}\"\n", url)).unwrap();

        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();
        assert_eq!(parsed.len(), 1);
        assert_eq!(parsed[0].0, "DATABASE_URL");
        assert_eq!(parsed[0].1, url);
    }

    #[test]
    fn test_save_database_url_creates_parent_dirs() {
        let dir = tempdir().unwrap();
        let nested = dir.path().join("deep").join("nested");
        let env_path = nested.join(".env");

        // Parent doesn't exist yet
        assert!(!nested.exists());

        // The global function uses a fixed path, so we test the logic directly
        std::fs::create_dir_all(&nested).unwrap();
        std::fs::write(&env_path, "DATABASE_URL=postgres://test\n").unwrap();

        assert!(env_path.exists());
        let content = std::fs::read_to_string(&env_path).unwrap();
        assert!(content.contains("DATABASE_URL=postgres://test"));
    }

    #[test]
    fn test_save_bootstrap_env_escapes_quotes() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // A malicious URL attempting to inject a second env var
        let malicious = r#"http://evil.com"
INJECTED="pwned"#;
        let mut content = String::new();
        let escaped = malicious.replace('\\', "\\\\").replace('"', "\\\"");
        content.push_str(&format!("LLM_BASE_URL=\"{}\"\n", escaped));
        std::fs::write(&env_path, &content).unwrap();

        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();

        // Must parse as exactly one variable, not two
        assert_eq!(parsed.len(), 1, "injection must not create extra vars");
        assert_eq!(parsed[0].0, "LLM_BASE_URL");
        // The value should contain the original malicious content (unescaped by dotenvy)
        assert!(
            parsed[0].1.contains("INJECTED"),
            "value should contain the literal injection attempt, not execute it"
        );
    }

    #[test]
    fn test_ironclaw_env_path() {
        let path = ironclaw_env_path();
        assert!(path.ends_with(".ironclaw/.env"));
    }

    #[test]
    fn test_migrate_bootstrap_json_to_env() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");
        let bootstrap_path = dir.path().join("bootstrap.json");

        // Write a legacy bootstrap.json
        let bootstrap_json = serde_json::json!({
            "database_url": "postgres://localhost/ironclaw_upgrade",
            "database_pool_size": 5,
            "secrets_master_key_source": "keychain",
            "onboard_completed": true
        });
        std::fs::write(
            &bootstrap_path,
            serde_json::to_string_pretty(&bootstrap_json).unwrap(),
        )
        .unwrap();

        assert!(!env_path.exists());
        assert!(bootstrap_path.exists());

        // Run the migration
        migrate_bootstrap_json_to_env(&env_path);

        // .env should now exist with DATABASE_URL
        assert!(env_path.exists());
        let content = std::fs::read_to_string(&env_path).unwrap();
        assert_eq!(
            content,
            "DATABASE_URL=\"postgres://localhost/ironclaw_upgrade\"\n"
        );

        // bootstrap.json should be renamed to .migrated
        assert!(!bootstrap_path.exists());
        assert!(dir.path().join("bootstrap.json.migrated").exists());
    }

    #[test]
    fn test_migrate_bootstrap_json_no_database_url() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");
        let bootstrap_path = dir.path().join("bootstrap.json");

        // bootstrap.json with no database_url
        let bootstrap_json = serde_json::json!({
            "onboard_completed": false
        });
        std::fs::write(
            &bootstrap_path,
            serde_json::to_string_pretty(&bootstrap_json).unwrap(),
        )
        .unwrap();

        migrate_bootstrap_json_to_env(&env_path);

        // .env should NOT be created
        assert!(!env_path.exists());
        // bootstrap.json should remain (no migration happened)
        assert!(bootstrap_path.exists());
    }

    #[test]
    fn test_migrate_bootstrap_json_missing() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // No bootstrap.json at all
        migrate_bootstrap_json_to_env(&env_path);

        // Nothing should happen
        assert!(!env_path.exists());
    }

    #[test]
    fn test_save_bootstrap_env_multiple_vars() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join("nested").join(".env");

        std::fs::create_dir_all(env_path.parent().unwrap()).unwrap();

        let vars = [
            ("DATABASE_BACKEND", "libsql"),
            ("LIBSQL_PATH", "/home/user/.ironclaw/ironclaw.db"),
        ];

        // Write manually to the temp path (save_bootstrap_env uses the global path)
        let mut content = String::new();
        for (key, value) in &vars {
            content.push_str(&format!("{}=\"{}\"\n", key, value));
        }
        std::fs::write(&env_path, &content).unwrap();

        // Verify dotenvy can parse all entries
        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();
        assert_eq!(parsed.len(), 2);
        assert_eq!(
            parsed[0],
            ("DATABASE_BACKEND".to_string(), "libsql".to_string())
        );
        assert_eq!(
            parsed[1],
            (
                "LIBSQL_PATH".to_string(),
                "/home/user/.ironclaw/ironclaw.db".to_string()
            )
        );
    }

    #[test]
    fn test_save_bootstrap_env_overwrites_previous() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // Write initial content
        std::fs::write(&env_path, "DATABASE_URL=\"postgres://old\"\n").unwrap();

        // Overwrite with new vars (simulating save_bootstrap_env behavior)
        let content = "DATABASE_BACKEND=\"libsql\"\nLIBSQL_PATH=\"/new/path.db\"\n";
        std::fs::write(&env_path, content).unwrap();

        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();
        // Old DATABASE_URL should be gone
        assert_eq!(parsed.len(), 2);
        assert!(parsed.iter().all(|(k, _)| k != "DATABASE_URL"));
    }

    #[test]
    fn test_onboard_completed_round_trips_through_env() {
        let dir = tempdir().unwrap();
        let env_path = dir.path().join(".env");

        // Simulate what the wizard writes: bootstrap vars + ONBOARD_COMPLETED
        let vars = [
            ("DATABASE_BACKEND", "libsql"),
            ("ONBOARD_COMPLETED", "true"),
        ];
        let mut content = String::new();
        for (key, value) in &vars {
            let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
            content.push_str(&format!("{}=\"{}\"\n", key, escaped));
        }
        std::fs::write(&env_path, &content).unwrap();

        // Verify dotenvy parses ONBOARD_COMPLETED correctly
        let parsed: Vec<(String, String)> = dotenvy::from_path_iter(&env_path)
            .unwrap()
            .filter_map(|r| r.ok())
            .collect();
        assert_eq!(parsed.len(), 2);
        let onboard = parsed.iter().find(|(k, _)| k == "ONBOARD_COMPLETED");
        assert!(onboard.is_some(), "ONBOARD_COMPLETED must be present");
        assert_eq!(onboard.unwrap().1, "true");
    }
}
