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

use color_eyre::eyre::Result;

/// Load environment variables from a .env file.
/// Format: KEY=VALUE (one per line), # comments, empty lines skipped.
pub fn load_env(path: &Path) -> Result<HashMap<String, String>> {
    let mut vars = HashMap::new();
    if !path.exists() {
        return Ok(vars);
    }
    if let Err(e) = crate::fs_secure::restrict_path(path, 0o600) {
        tracing::warn!("failed to secure env file {}: {e}", path.display());
    }
    let content = std::fs::read_to_string(path)?;
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        if let Some((key, value)) = trimmed.split_once('=') {
            let key = key.trim().to_string();
            let value = value
                .trim()
                .trim_matches('"')
                .trim_matches('\'')
                .to_string();
            vars.insert(key, value);
        }
    }
    Ok(vars)
}

/// Set a key in the `.env` file. Creates the file if it doesn't exist.
/// Updates existing keys in place, appends new ones at the end.
pub fn set_env_value(path: &Path, key: &str, value: &str) -> Result<()> {
    let mut lines: Vec<String> = if path.exists() {
        std::fs::read_to_string(path)?
            .lines()
            .map(String::from)
            .collect()
    } else {
        Vec::new()
    };

    let prefix = format!("{key}=");
    let new_line = format!("{key}={value}");
    let mut found = false;

    for line in &mut lines {
        let trimmed = line.trim();
        if trimmed.starts_with(&prefix) {
            line.clone_from(&new_line);
            found = true;
            break;
        }
    }

    if !found {
        // Add a blank line separator if the file is non-empty and doesn't end with one.
        if !lines.is_empty() && !lines.last().is_some_and(|l| l.trim().is_empty()) {
            lines.push(String::new());
        }
        lines.push(new_line);
    }

    crate::fs_secure::write_file(path, lines.join("\n") + "\n", 0o600)?;
    Ok(())
}

/// Remove a key from the `.env` file. No-op if the file or key is absent.
pub fn remove_env_value(path: &Path, key: &str) -> Result<()> {
    if !path.exists() {
        return Ok(());
    }
    let prefix = format!("{key}=");
    let kept: Vec<String> = std::fs::read_to_string(path)?
        .lines()
        .filter(|line| !line.trim_start().starts_with(&prefix))
        .map(String::from)
        .collect();
    crate::fs_secure::write_file(path, kept.join("\n") + "\n", 0o600)?;
    Ok(())
}

/// Apply .env credentials to the web config.
///
/// Reads `WEB_PASSWORD` and `WEB_SESSION_SECRET` from the env map.
/// `WEB_SESSION_SECRET` is hex-encoded (64 chars = 32 bytes); invalid or
/// missing values are treated as "no secret yet" — the caller is
/// responsible for generating one and persisting it.
pub fn apply_web_credentials(web: &mut super::WebConfig, env: &HashMap<String, String>) {
    if let Some(val) = env.get("WEB_PASSWORD") {
        web.password.clone_from(val);
    }
    if let Some(val) = env.get("WEB_SESSION_SECRET")
        && let Ok(bytes) = hex::decode(val.trim())
        && bytes.len() == 32
    {
        web.session_secret = bytes;
    }
}

/// Apply `.env`-stored credentials to the shrink config.
///
/// Reads `SHRINK_API_KEY`. The `#[serde(skip)]` on `ShrinkConfig.api_key`
/// guarantees the key never round-trips through `config.toml`, so this
/// is the only path the secret reaches `AppConfig`.
pub fn apply_shrink_credentials(shrink: &mut super::ShrinkConfig, env: &HashMap<String, String>) {
    if let Some(val) = env.get("SHRINK_API_KEY") {
        shrink.api_key = val.trim().to_string();
    }
}

/// Ensure `web.session_secret` is set, generating and persisting a fresh
/// 32-byte secret to `.env` on first run.
///
/// This is intentionally separated from [`apply_web_credentials`] so callers
/// can decide whether to materialise a secret (server start) or not (config
/// validation, dry runs).
pub fn ensure_session_secret(web: &mut super::WebConfig, env_path: &Path) -> Result<()> {
    use rand::RngExt;
    if web.session_secret.len() == 32 {
        return Ok(());
    }
    let mut bytes = [0u8; 32];
    rand::rng().fill(&mut bytes);
    set_env_value(env_path, "WEB_SESSION_SECRET", &hex::encode(bytes))?;
    web.session_secret = bytes.to_vec();
    Ok(())
}

/// Apply .env credentials to server configs.
/// For each server with id "foo", looks for `FOO_SASL_USER`, `FOO_SASL_PASS`, `FOO_PASSWORD`.
pub fn apply_credentials(
    servers: &mut HashMap<String, super::ServerConfig>,
    env: &HashMap<String, String>,
) {
    for (id, server) in servers.iter_mut() {
        let prefix = id.to_uppercase();
        let mut key = String::with_capacity(prefix.len() + 10);
        let mut get = |suffix: &str| -> Option<String> {
            key.clear();
            key.push_str(&prefix);
            key.push_str(suffix);
            env.get(&key).cloned()
        };
        if let Some(val) = get("_SASL_USER") {
            server.sasl_user = Some(val);
        }
        if let Some(val) = get("_SASL_PASS") {
            server.sasl_pass = Some(val);
        }
        if let Some(val) = get("_PASSWORD") {
            server.password = Some(val);
        }
    }
}

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

    #[test]
    fn parse_env_file() {
        let dir = std::env::temp_dir().join("repartee_test_env");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join(".env");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(f, "# Comment").unwrap();
        writeln!(f, "SASL_PASS=secret123").unwrap();
        writeln!(f, "SERVER_PASS=\"quoted value\"").unwrap();
        writeln!(f).unwrap();

        let vars = load_env(&path).unwrap();
        assert_eq!(vars.get("SASL_PASS").unwrap(), "secret123");
        assert_eq!(vars.get("SERVER_PASS").unwrap(), "quoted value");

        std::fs::remove_dir_all(&dir).unwrap();
    }

    #[test]
    fn load_env_missing_file() {
        let path = std::env::temp_dir().join("repartee_test_nonexistent/.env");
        let vars = load_env(&path).unwrap();
        assert!(vars.is_empty());
    }

    #[test]
    fn apply_credentials_to_servers() {
        let mut servers = HashMap::new();
        servers.insert(
            "libera".to_string(),
            super::super::ServerConfig {
                label: "Libera".to_string(),
                address: "irc.libera.chat".to_string(),
                port: 6697,
                tls: true,
                tls_verify: true,
                autoconnect: false,
                channels: vec![],
                nick: None,
                username: None,
                realname: None,
                password: None,
                sasl_user: None,
                sasl_pass: None,
                bind_ip: None,
                encoding: None,
                auto_reconnect: None,
                reconnect_delay: None,
                reconnect_max_retries: None,
                autosendcmd: None,
                sasl_mechanism: None,
                client_cert_path: None,
            },
        );

        let mut env = HashMap::new();
        env.insert("LIBERA_SASL_USER".to_string(), "myuser".to_string());
        env.insert("LIBERA_SASL_PASS".to_string(), "mypass".to_string());

        apply_credentials(&mut servers, &env);

        let server = servers.get("libera").unwrap();
        assert_eq!(server.sasl_user.as_deref(), Some("myuser"));
        assert_eq!(server.sasl_pass.as_deref(), Some("mypass"));
        assert!(server.password.is_none());
    }

    #[test]
    fn set_env_value_creates_new_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".env");

        set_env_value(&path, "WEB_PASSWORD", "secret").unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("WEB_PASSWORD=secret"));
    }

    #[test]
    fn set_env_value_updates_existing_key() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".env");
        std::fs::write(&path, "FOO=old\nWEB_PASSWORD=old\nBAR=keep\n").unwrap();

        set_env_value(&path, "WEB_PASSWORD", "new").unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("WEB_PASSWORD=new"));
        assert!(content.contains("FOO=old"));
        assert!(content.contains("BAR=keep"));
        assert!(!content.contains("WEB_PASSWORD=old"));
    }

    #[test]
    fn remove_env_value_deletes_key() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".env");
        std::fs::write(&path, "FOO=a\nLIBERA_PASSWORD=secret\nBAR=b\n").unwrap();

        remove_env_value(&path, "LIBERA_PASSWORD").unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("FOO=a"));
        assert!(content.contains("BAR=b"));
        assert!(!content.contains("LIBERA_PASSWORD"));
    }

    #[test]
    fn remove_env_value_missing_key_is_ok() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".env");
        std::fs::write(&path, "FOO=a\n").unwrap();
        remove_env_value(&path, "NOPE").unwrap();
        assert!(std::fs::read_to_string(&path).unwrap().contains("FOO=a"));
    }

    #[test]
    fn set_env_value_appends_new_key() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".env");
        std::fs::write(&path, "EXISTING=value\n").unwrap();

        set_env_value(&path, "NEW_KEY", "new_value").unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("EXISTING=value"));
        assert!(content.contains("NEW_KEY=new_value"));
    }
}
