use common::error::exit_with_error;
use std::path::{Path, PathBuf};

const GATE_HEADER: &str = "// Generated by `gate init --harness opencode`. Safe to delete.";

pub(crate) const PLUGIN_TEMPLATE: &str = r#"// Generated by `gate init --harness opencode`. Safe to delete.
// Uses node:child_process (not Bun.spawnSync) for portability if opencode changes runtime.
import { spawnSync } from "node:child_process"

export const GatePlugin = async () => ({
  "tool.execute.before": async (
    input: { tool: string },
    output: { args: { command?: string } },
  ) => {
    if (input.tool !== "bash") return
    const cmd = output.args.command
    if (typeof cmd !== "string" || cmd.length === 0) return

    const result = spawnSync("gate", ["hook"], {
      input: JSON.stringify({ tool_name: "Bash", tool_input: { command: cmd } }),
      encoding: "utf8",
      timeout: 5000,
    })
    if (result.status !== 0 || !result.stdout) return
    try {
      const updated = JSON.parse(result.stdout)?.hookSpecificOutput?.updatedInput?.command
      if (typeof updated === "string" && updated.length > 0) {
        output.args.command = updated
      }
    } catch { /* malformed response — fail open to passthrough */ }
  },
})
"#;

pub fn run(scope: &str) {
    let path = match plugin_path(scope) {
        Ok(p) => p,
        Err(e) => exit_with_error(&e),
    };
    match try_write_plugin(&path) {
        Ok(WriteResult::AlreadyInstalled) => {
            println!(
                "gate opencode plugin already installed at {}",
                path.display()
            );
        }
        Ok(WriteResult::Written) => print_hint(&path, scope),
        Err(e) => exit_with_error(&e),
    }
}

enum WriteResult {
    Written,
    AlreadyInstalled,
}

pub(crate) fn plugin_path(scope: &str) -> Result<PathBuf, String> {
    match scope {
        "global" => {
            let home = std::env::var("HOME")
                .or_else(|_| std::env::var("USERPROFILE"))
                .map_err(|_| {
                    "cannot resolve home directory: set HOME or USERPROFILE".to_string()
                })?;
            Ok(PathBuf::from(home)
                .join(".config")
                .join("opencode")
                .join("plugin")
                .join("gate.ts"))
        }
        "project" => Ok(PathBuf::from(".opencode").join("plugin").join("gate.ts")),
        _ => Err(format!(
            "unsupported scope '{}'; use 'global' or 'project'",
            scope
        )),
    }
}

pub(crate) fn has_gate_header(content: &str) -> bool {
    content.lines().any(|line| line == GATE_HEADER)
}

fn try_write_plugin(path: &Path) -> Result<WriteResult, String> {
    if path.exists() {
        let existing = std::fs::read_to_string(path)
            .map_err(|e| format!("failed to read {}: {e}", path.display()))?;

        if existing == PLUGIN_TEMPLATE {
            return Ok(WriteResult::AlreadyInstalled);
        }

        if !has_gate_header(&existing) {
            return Err(format!(
                "refusing to overwrite {}: file exists without a gate header. \
                 Delete or rename it, then re-run `gate init --harness opencode`.",
                path.display()
            ));
        }
        // Has gate header but different content → upgrade (fall through to write)
    }

    write_atomic(path, PLUGIN_TEMPLATE)
        .map_err(|e| format!("failed to write {}: {e}", path.display()))?;

    Ok(WriteResult::Written)
}

fn print_hint(path: &Path, scope: &str) {
    println!("gate: opencode integration installed.");
    println!("  Plugin:   {}", path.display());
    if scope == "global" {
        println!("            (use --scope project for ./.opencode/plugin/gate.ts)");
    }
    println!();
    println!("  Restart your opencode session to load the plugin.");
    println!("  Run `gate config` to define which tools to intercept.");
}

fn write_atomic(path: &Path, content: &str) -> anyhow::Result<()> {
    let parent = path
        .parent()
        .ok_or_else(|| anyhow::anyhow!("plugin path has no parent directory"))?;
    std::fs::create_dir_all(parent)?;
    let file_name = path
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or_else(|| anyhow::anyhow!("plugin path has no filename"))?;
    let tmp_path = parent.join(format!("{file_name}.gate_tmp"));
    std::fs::write(&tmp_path, content)?;
    std::fs::rename(&tmp_path, path)?;
    Ok(())
}

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

    // ── template invariants ──────────────────────────────────────────────────

    #[test]
    fn template_has_gate_header() {
        assert!(has_gate_header(PLUGIN_TEMPLATE));
    }

    #[test]
    fn template_contains_snake_case_json_shape() {
        // These strings are the literal keys the plugin sends to gate hook
        assert!(PLUGIN_TEMPLATE.contains("tool_name"));
        assert!(PLUGIN_TEMPLATE.contains("tool_input"));
        // JS object literal syntax uses unquoted key: { command: cmd }
        assert!(PLUGIN_TEMPLATE.contains("command: cmd"));
    }

    #[test]
    fn template_reads_hook_response_correctly() {
        assert!(PLUGIN_TEMPLATE.contains("hookSpecificOutput"));
        assert!(PLUGIN_TEMPLATE.contains("updatedInput"));
        assert!(PLUGIN_TEMPLATE.contains("output.args.command"));
    }

    // ── has_gate_header ────────────────────────────────────────────────────

    #[test]
    fn header_detected_in_first_line() {
        let content =
            "// Generated by `gate init --harness opencode`. Safe to delete.\nsome more\n";
        assert!(has_gate_header(content));
    }

    #[test]
    fn header_detected_in_later_line() {
        let content = "line one\n// Generated by `gate init --harness opencode`. Safe to delete.\n";
        assert!(has_gate_header(content));
    }

    #[test]
    fn header_not_detected_in_random_content() {
        assert!(!has_gate_header("random content\nno header here\n"));
        assert!(!has_gate_header(""));
        assert!(!has_gate_header("// some other comment\n"));
    }

    // ── plugin_path ──────────────────────────────────────────────────────────

    #[test]
    fn plugin_path_project_is_relative() {
        let path = plugin_path("project").unwrap();
        assert_eq!(path, PathBuf::from(".opencode/plugin/gate.ts"));
    }

    #[test]
    fn plugin_path_global_uses_home() {
        let original_home = std::env::var("HOME").ok();
        unsafe { std::env::set_var("HOME", "/test/home") };
        let path = plugin_path("global").unwrap();
        match original_home {
            Some(h) => unsafe { std::env::set_var("HOME", h) },
            None => unsafe { std::env::remove_var("HOME") },
        }
        assert_eq!(
            path,
            PathBuf::from("/test/home/.config/opencode/plugin/gate.ts")
        );
    }

    #[test]
    fn plugin_path_invalid_scope_errors() {
        assert!(plugin_path("invalid").is_err());
        assert!(plugin_path("").is_err());
    }

    // ── try_write_plugin ─────────────────────────────────────────────────────

    #[test]
    fn write_creates_file_and_parent_dirs() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("nested/plugin/gate.ts");
        let result = try_write_plugin(&path).unwrap();
        assert!(matches!(result, WriteResult::Written));
        assert!(path.exists());
        assert_eq!(std::fs::read_to_string(&path).unwrap(), PLUGIN_TEMPLATE);
    }

    #[test]
    fn idempotent_returns_already_installed() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("gate.ts");
        try_write_plugin(&path).unwrap();
        let result = try_write_plugin(&path).unwrap();
        assert!(matches!(result, WriteResult::AlreadyInstalled));
        // File content unchanged
        assert_eq!(std::fs::read_to_string(&path).unwrap(), PLUGIN_TEMPLATE);
    }

    #[test]
    fn upgrade_overwrites_file_with_gate_header() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("gate.ts");
        let old = "// Generated by `gate init --harness opencode`. Safe to delete.\nold content\n";
        std::fs::write(&path, old).unwrap();
        let result = try_write_plugin(&path).unwrap();
        assert!(matches!(result, WriteResult::Written));
        assert_eq!(std::fs::read_to_string(&path).unwrap(), PLUGIN_TEMPLATE);
    }

    #[test]
    fn clobber_guard_rejects_file_without_header() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("gate.ts");
        std::fs::write(&path, "user authored content\n").unwrap();
        let result = try_write_plugin(&path);
        assert!(result.is_err());
        // Original file must be untouched
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "user authored content\n"
        );
    }

    #[test]
    fn atomic_write_leaves_no_tmp_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("gate.ts");
        write_atomic(&path, "content").unwrap();
        let tmp = dir.path().join("gate.ts.gate_tmp");
        assert!(
            !tmp.exists(),
            "tmp file must not remain after successful write"
        );
    }

    // ── harness gate regression (OPENCODE env var) ────────────────────────────

    #[test]
    fn is_agent_harness_true_when_opencode_set() {
        let saved = std::env::var("OPENCODE").ok();
        unsafe { std::env::set_var("OPENCODE", "1") };
        let result = common::harness::is_agent_harness();
        match saved {
            Some(v) => unsafe { std::env::set_var("OPENCODE", v) },
            None => unsafe { std::env::remove_var("OPENCODE") },
        }
        assert!(result, "OPENCODE env var must trigger harness gate");
    }
}
