use anyhow::{Context, Result, anyhow, bail};
use rust_embed::RustEmbed;
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use which::which;

mod mixins;

use crate::config::AppConfig;
use crate::config::Config;
use crate::config::VAULT_DATA_FILE_NAME;
use crate::config::paths;
use crate::sandbox::mixins::DiscoveredMixin;
use crate::utils::run_command_with_output;
use crate::vault::SECRET_RE;
use crate::vault::Vault;

const SBX_BINARY: &str = "sbx";
pub(crate) const SANDBOX_ENV_FLAG: &str = "IS_SANDBOX";
const SANDBOX_AGENT: &str = "coyote";

#[derive(RustEmbed)]
#[folder = "assets/sbx-kit/"]
struct EmbeddedKit;

pub fn launch(name: Option<String>, fresh: bool) -> Result<()> {
    ensure_sbx_installed()?;
    bail_if_nested()?;

    let name = resolve_name(name)?;
    let kit_path = resolve_kit_path()?;

    let config_path = paths::config_file();
    if !config_path.exists() {
        bail!("No Coyote config found. Run `coyote` on your host to complete setup first.");
    }
    let (config, config_content) = Config::load_from_file(&config_path)?;
    let bootstrap = AppConfig {
        vault_password_file: config.vault_password_file.clone(),
        secrets_provider: config.secrets_provider.clone(),
        ..AppConfig::default()
    };
    let vault = Vault::init(&bootstrap)?;
    let registered = sbx_registered_services()?;
    inject_llm_secret(&config_content, &vault, &registered)?;
    if !fresh {
        inject_mcp_secrets(&vault, &registered)?;
    }

    let discovered = mixins::discover()?;

    if sandbox_exists(&name)? {
        info!("Re-attaching to existing sandbox '{name}'");
    } else {
        mixins::log_discovery(&discovered, false);
        create_sandbox(&name, &kit_path, &discovered)?;
        if !fresh {
            copy_host_files(&name)?;
        }
    }

    exec_run(&name, &kit_path)
}

fn ensure_sbx_installed() -> Result<()> {
    which(SBX_BINARY).map_err(|_| {
        anyhow!(
            "`sbx` binary not found in PATH.\n\n\
             Install Docker Sandboxes:\n  https://docs.docker.com/ai/sandboxes/get-started/"
        )
    })?;

    Ok(())
}

fn bail_if_nested() -> Result<()> {
    if env::var_os(SANDBOX_ENV_FLAG).is_some() {
        bail!("Refusing to nest sandboxes: ${SANDBOX_ENV_FLAG} is set, already inside one");
    }

    Ok(())
}

fn resolve_name(name: Option<String>) -> Result<String> {
    if let Some(n) = name {
        let trimmed = n.trim();
        if !trimmed.is_empty() {
            let sanitized = sanitize_name(trimmed);
            if sanitized.is_empty() {
                bail!("Sandbox name '{trimmed}' sanitizes to an empty string");
            }

            return Ok(sanitized);
        }
    }

    let cwd = env::current_dir().context("Failed to determine current directory")?;
    let basename = cwd
        .file_name()
        .and_then(|s| s.to_str())
        .ok_or_else(|| anyhow!("Could not derive sandbox name from current directory"))?;
    let sanitized = sanitize_name(basename);
    if sanitized.is_empty() {
        bail!("Could not derive a valid sandbox name from '{basename}'; pass --sandbox <NAME>");
    }

    Ok(sanitized)
}

fn sanitize_name(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    let mut last_was_dash = false;
    for ch in input.chars() {
        let lower = ch.to_ascii_lowercase();
        if lower.is_ascii_alphanumeric() {
            out.push(lower);
            last_was_dash = false;
        } else if !last_was_dash {
            out.push('-');
            last_was_dash = true;
        }
    }

    out.trim_matches('-').to_string()
}

fn resolve_kit_path() -> Result<PathBuf> {
    if let Some(path) = paths::sandbox_kit_override() {
        if !path.exists() {
            bail!(
                "$COYOTE_SANDBOX_KIT is set but path does not exist: {}",
                path.display()
            );
        }

        debug!(
            "Using kit override from $COYOTE_SANDBOX_KIT: {}",
            path.display()
        );

        return Ok(path);
    }

    extract_embedded_kit()
}

fn extract_embedded_kit() -> Result<PathBuf> {
    let cache_root = paths::sbx_kit_dir();
    let new_hash = compute_kit_hash()?;
    let hash_file = paths::sbx_kit_hash_file();
    if let Ok(existing) = fs::read_to_string(&hash_file)
        && existing == new_hash
    {
        return Ok(cache_root);
    }

    if cache_root.exists() {
        fs::remove_dir_all(&cache_root)
            .with_context(|| format!("Failed to clear stale kit at {}", cache_root.display()))?;
    }
    fs::create_dir_all(&cache_root)
        .with_context(|| format!("Failed to create {}", cache_root.display()))?;

    for entry in EmbeddedKit::iter() {
        let file = EmbeddedKit::get(&entry)
            .ok_or_else(|| anyhow!("Embedded kit file missing during extraction: {entry}"))?;
        let dest = cache_root.join(entry.as_ref());
        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create {}", parent.display()))?;
        }

        fs::write(&dest, &file.data)
            .with_context(|| format!("Failed to write {}", dest.display()))?;
    }

    fs::write(&hash_file, &new_hash)
        .with_context(|| format!("Failed to write {}", hash_file.display()))?;
    debug!("Extracted embedded sbx-kit to {}", cache_root.display());

    Ok(cache_root)
}

fn compute_kit_hash() -> Result<String> {
    let mut hasher = Sha256::new();
    let mut entries: Vec<_> = EmbeddedKit::iter().collect();
    entries.sort();

    for entry in &entries {
        let file = EmbeddedKit::get(entry)
            .ok_or_else(|| anyhow!("Embedded kit file missing during hash: {entry}"))?;
        hasher.update(entry.as_bytes());
        hasher.update(b"\0");
        hasher.update(&file.data);
    }

    Ok(format!("{:x}", hasher.finalize()))
}

fn inject_llm_secret(
    config_content: &str,
    vault: &Vault,
    registered: &HashSet<String>,
) -> Result<()> {
    let value: serde_yaml::Value = serde_yaml::from_str(config_content)
        .context("Failed to parse config for LLM secret injection")?;

    let Some(clients) = value.get("clients").and_then(|v| v.as_sequence()) else {
        return Ok(());
    };

    for client in clients {
        let Some(api_key) = client.get("api_key").and_then(|v| v.as_str()) else {
            continue;
        };

        let Some(caps) = SECRET_RE.captures(api_key)? else {
            continue;
        };
        let secret_name = caps[1].to_string();

        let client_type = client.get("type").and_then(|v| v.as_str()).unwrap_or("");
        let client_name = client.get("name").and_then(|v| v.as_str());
        let service = provider_to_sbx_service(client_type, client_name);

        if registered.contains(&service) {
            eprintln!(
                "Secret for '{service}' already registered with sbx. \
                 To update it, run: sbx secret set -g --force {service}"
            );
            continue;
        }

        let secret_value = vault
            .get_secret(&secret_name, false)
            .with_context(|| format!("Failed to decrypt LLM api_key secret '{secret_name}'"))?;

        sbx_secret_set(&service, &secret_value)?;
    }

    Ok(())
}

fn find_secret_placeholder(value: &Value) -> Option<String> {
    match value {
        Value::String(s) => SECRET_RE
            .captures(s)
            .ok()
            .flatten()
            .map(|caps| caps[1].to_string()),
        Value::Object(map) => map.values().find_map(find_secret_placeholder),
        Value::Array(arr) => arr.iter().find_map(find_secret_placeholder),
        _ => None,
    }
}

fn inject_mcp_secrets(vault: &Vault, registered: &HashSet<String>) -> Result<()> {
    let mcp_path = paths::mcp_config_file();
    if !mcp_path.exists() {
        return Ok(());
    }

    let content = fs::read_to_string(&mcp_path)
        .with_context(|| format!("Failed to read {}", mcp_path.display()))?;
    let mcp: Value = serde_json::from_str(&content)
        .with_context(|| format!("Failed to parse {}", mcp_path.display()))?;

    let Some(servers) = mcp.get("mcpServers").and_then(|v| v.as_object()) else {
        return Ok(());
    };

    for (server_name, server_config) in servers {
        let Some(secret_name) = find_secret_placeholder(server_config) else {
            continue;
        };

        if registered.contains(server_name.as_str()) {
            eprintln!(
                "Secret for '{server_name}' already registered with sbx. \
                 To update it, run: sbx secret set -g --force {server_name}"
            );
            continue;
        }

        let secret_value = vault.get_secret(&secret_name, false).with_context(|| {
            format!(
                "Secret '{secret_name}' referenced by MCP server '{server_name}' not found \
                 in vault. Add it with: coyote --add-secret {secret_name}"
            )
        })?;

        sbx_secret_set(server_name, &secret_value)?;
    }

    Ok(())
}

fn provider_to_sbx_service(provider_type: &str, client_name: Option<&str>) -> String {
    match provider_type {
        "claude" => "anthropic".to_string(),
        "openai" => "openai".to_string(),
        "gemini" | "vertexai" => "google".to_string(),
        "openai-compatible" => client_name.unwrap_or("openai-compatible").to_string(),
        other => client_name.unwrap_or(other).to_string(),
    }
}

fn sbx_registered_services() -> Result<HashSet<String>> {
    let (success, stdout, _) = run_command_with_output(SBX_BINARY, &["secret", "ls"], None)
        .context("Failed to run `sbx secret ls`")?;

    if !success {
        return Ok(HashSet::new());
    }

    Ok(stdout
        .lines()
        .skip(1)
        .filter_map(|line| {
            let mut parts = line.split_whitespace();
            let scope = parts.next()?;
            let _kind = parts.next()?;
            let name = parts.next()?;
            if scope == "(global)" {
                Some(name.to_string())
            } else {
                None
            }
        })
        .collect())
}

fn sbx_secret_set(service: &str, secret_value: &str) -> Result<()> {
    let mut child = Command::new(SBX_BINARY)
        .args(["secret", "set", "-g", service])
        .stdin(Stdio::piped())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()
        .context("Failed to spawn `sbx secret set -g`")?;

    if let Some(mut stdin_handle) = child.stdin.take() {
        stdin_handle
            .write_all(secret_value.as_bytes())
            .context("Failed to write secret to `sbx secret set -g` stdin")?;
    }

    let status = child
        .wait()
        .context("Failed to wait for `sbx secret set -g`")?;

    if !status.success() {
        bail!("`sbx secret set -g {service}` exited with {status}");
    }

    Ok(())
}

fn sandbox_exists(name: &str) -> Result<bool> {
    let (success, stdout, stderr) =
        run_command_with_output(SBX_BINARY, &["ls"], None).context("Failed to run `sbx ls`")?;
    if !success {
        bail!("`sbx ls` failed: {stderr}");
    }

    Ok(stdout
        .lines()
        .skip(1)
        .any(|line| line.split_whitespace().next() == Some(name)))
}

fn create_sandbox(name: &str, kit_path: &Path, mixins: &[DiscoveredMixin]) -> Result<()> {
    info!("Creating sandbox '{name}'");
    let args = build_create_args(name, kit_path, mixins)?;
    debug!("sbx {}", args.join(" "));
    let status = Command::new(SBX_BINARY)
        .args(&args)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .context("Failed to spawn `sbx create`")?;

    if !status.success() {
        bail!("`sbx create` exited with {status}");
    }

    Ok(())
}

fn build_create_args(
    name: &str,
    kit_path: &Path,
    mixins: &[DiscoveredMixin],
) -> Result<Vec<String>> {
    let kit_str = kit_path
        .to_str()
        .ok_or_else(|| anyhow!("Kit path is not valid UTF-8: {}", kit_path.display()))?;

    let mut args = vec![
        "create".to_string(),
        "--name".to_string(),
        name.to_string(),
        "--kit".to_string(),
        kit_str.to_string(),
    ];

    for mixin in mixins {
        let mixin_kit = mixin.kit_path()?;
        let mixin_str = mixin_kit
            .to_str()
            .ok_or_else(|| anyhow!("Mixin kit path is not valid UTF-8: {}", mixin_kit.display()))?
            .to_string();
        args.push("--kit".to_string());
        args.push(mixin_str);
    }

    args.push(SANDBOX_AGENT.to_string());
    args.push(".".to_string());

    Ok(args)
}

fn copy_host_files(name: &str) -> Result<()> {
    let config_dir = paths::config_dir();

    if config_dir.exists() {
        let sandbox_config_dir = "/home/agent/.config/coyote";
        ensure_sandbox_dir(name, sandbox_config_dir)?;
        let dest = format!("{name}:{sandbox_config_dir}/");
        for entry in fs::read_dir(&config_dir)
            .with_context(|| format!("Failed to read {}", config_dir.display()))?
        {
            let entry = entry?;
            let path = entry.path();
            if path.file_name().is_some_and(|n| n == VAULT_DATA_FILE_NAME) {
                continue;
            }
            sbx_cp(&path.display().to_string(), &dest)?;
        }
        chown_agent_recursive(name, sandbox_config_dir)?;
    } else {
        debug!(
            "Skipping config copy: {} does not exist",
            config_dir.display()
        );
    }

    let oauth_tokens_dir = paths::oauth_tokens_dir();
    if oauth_tokens_dir.exists() {
        let sandbox_cache_dir = "/home/agent/.cache";
        let sandbox_oauth_dir = "/home/agent/.cache/coyote/oauth";
        ensure_sandbox_dir(name, sandbox_oauth_dir)?;
        let dest = format!("{name}:{sandbox_oauth_dir}/");
        for entry in fs::read_dir(&oauth_tokens_dir)
            .with_context(|| format!("Failed to read {}", oauth_tokens_dir.display()))?
        {
            let entry = entry?;
            let path = entry.path();
            sbx_cp(&path.display().to_string(), &dest)?;
        }
        chown_agent_recursive(name, sandbox_cache_dir)?;
    } else {
        debug!(
            "Skipping OAuth token copy: {} does not exist",
            oauth_tokens_dir.display()
        );
    }

    Ok(())
}

fn ensure_sandbox_dir(sandbox: &str, dir: &str) -> Result<()> {
    let dir_q = shell_words::quote(dir);
    let cmd = format!("sudo mkdir -p {dir_q} && sudo chown agent:agent {dir_q}");

    debug!("sbx exec {sandbox}: {cmd}");

    let status = Command::new(SBX_BINARY)
        .args(["exec", sandbox, "sh", "-c", &cmd])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .context("Failed to spawn `sbx exec` to prepare destination directory")?;

    if !status.success() {
        bail!("Preparing sandbox directory '{dir}' failed: sbx exec exited with {status}");
    }

    Ok(())
}

fn sbx_cp(src: &str, dest: &str) -> Result<()> {
    debug!("sbx cp {src} {dest}");
    let status = Command::new(SBX_BINARY)
        .args(["cp", src, dest])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .context("Failed to spawn `sbx cp`")?;

    if !status.success() {
        bail!("`sbx cp {src} {dest}` exited with {status}");
    }

    Ok(())
}

fn exec_run(name: &str, kit_path: &Path) -> Result<()> {
    let kit_str = kit_path
        .to_str()
        .ok_or_else(|| anyhow!("Kit path is not valid UTF-8: {}", kit_path.display()))?;
    debug!("sbx run --name {name} --kit {kit_str}");
    let status = Command::new(SBX_BINARY)
        .args(["run", "--name", name, "--kit", kit_str])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .context("Failed to spawn `sbx run`")?;

    if !status.success() {
        bail!("`sbx run` exited with {status}");
    }

    Ok(())
}

fn chown_agent_recursive(sandbox: &str, path: &str) -> Result<()> {
    let path_q = shell_words::quote(path);
    let cmd = format!("sudo chown -R agent:agent {path_q}");

    debug!("sbx exec {sandbox}: {cmd}");

    let status = Command::new(SBX_BINARY)
        .args(["exec", sandbox, "sh", "-c", &cmd])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .context("Failed to spawn `sbx exec` to chown copied files")?;

    if !status.success() {
        bail!("Chowning '{path}' in sandbox failed: sbx exec exited with {status}");
    }

    Ok(())
}

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

    #[test]
    fn sanitize_name_lowercases() {
        assert_eq!(sanitize_name("Foo"), "foo");
    }

    #[test]
    fn sanitize_name_replaces_non_alphanumeric() {
        assert_eq!(sanitize_name("hello world!"), "hello-world");
    }

    #[test]
    fn sanitize_name_collapses_dash_runs() {
        assert_eq!(sanitize_name("a___b"), "a-b");
    }

    #[test]
    fn sanitize_name_trims_dashes() {
        assert_eq!(sanitize_name("---hi---"), "hi");
    }

    #[test]
    fn sanitize_name_handles_mixed_input() {
        assert_eq!(sanitize_name("My Project (v2)"), "my-project-v2");
    }

    #[test]
    fn sanitize_name_all_invalid_yields_empty() {
        assert_eq!(sanitize_name("///"), "");
    }

    #[test]
    fn resolve_name_uses_explicit_arg() {
        let n = resolve_name(Some("explicit-name".to_string())).unwrap();
        assert_eq!(n, "explicit-name");
    }

    #[test]
    fn resolve_name_sanitizes_explicit_arg() {
        let n = resolve_name(Some("My Sandbox!".to_string())).unwrap();
        assert_eq!(n, "my-sandbox");
    }

    #[test]
    fn resolve_name_rejects_empty_after_sanitize() {
        let err = resolve_name(Some("///".to_string()));
        assert!(err.is_err());
    }

    #[test]
    fn resolve_name_falls_back_to_cwd_when_none() {
        let n = resolve_name(None).unwrap();
        assert!(!n.is_empty());
        assert!(n.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'));
    }

    #[test]
    fn compute_kit_hash_is_deterministic() {
        let h1 = compute_kit_hash().unwrap();
        let h2 = compute_kit_hash().unwrap();

        assert_eq!(h1, h2);
        assert_eq!(h1.len(), 64);
    }

    #[test]
    fn build_create_args_emits_base_kit_before_mixins() {
        let kit = PathBuf::from("/cache/sbx-kit");
        let unique = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let dir_a = env::temp_dir().join(format!("coyote-mixin-a-{unique}"));
        let dir_b = env::temp_dir().join(format!("coyote-mixin-b-{unique}"));
        fs::create_dir_all(&dir_a).unwrap();
        fs::create_dir_all(&dir_b).unwrap();

        let mixins = vec![
            DiscoveredMixin {
                path: dir_a.clone(),
                label: "user".into(),
                install_count: 0,
                domain_count: 0,
            },
            DiscoveredMixin {
                path: dir_b.clone(),
                label: "sql".into(),
                install_count: 0,
                domain_count: 0,
            },
        ];

        let args = build_create_args("my-box", &kit, &mixins).unwrap();

        assert_eq!(
            args,
            vec![
                "create".to_string(),
                "--name".to_string(),
                "my-box".to_string(),
                "--kit".to_string(),
                "/cache/sbx-kit".to_string(),
                "--kit".to_string(),
                dir_a.display().to_string(),
                "--kit".to_string(),
                dir_b.display().to_string(),
                "coyote".to_string(),
                ".".to_string(),
            ]
        );

        let _ = fs::remove_dir_all(&dir_a);
        let _ = fs::remove_dir_all(&dir_b);
    }

    #[test]
    fn build_create_args_with_no_mixins_omits_mixin_kits() {
        let kit = PathBuf::from("/cache/sbx-kit");
        let args = build_create_args("box", &kit, &[]).unwrap();
        assert_eq!(
            args,
            vec![
                "create".to_string(),
                "--name".to_string(),
                "box".to_string(),
                "--kit".to_string(),
                "/cache/sbx-kit".to_string(),
                "coyote".to_string(),
                ".".to_string(),
            ]
        );
    }
}
