//! Async worktree creation and component setup/teardown commands.
use crate::config::ProjectConfig;
use crate::slog;
use std::path::Path;
use std::process::Command;

use super::git::{
    branch_name, configure_sparse_checkout, create_worktree_sync, resolve_base_branch,
};
use super::{WorktreeInfo, worktree_path, write_mcp_json};

/// Create a git worktree for the given story.
///
/// - Creates the worktree at `{project_root}/.huskies/worktrees/{story_id}`
///   on branch `feature/story-{story_id}`.
/// - Writes `.mcp.json` in the worktree pointing to the MCP server at `port`.
/// - Runs setup commands from the config for each component **only on fresh
///   creation** — see below.
/// - If the worktree/branch already exists, reuses rather than errors.
///
/// **Idempotency on reuse:** when `wt_path` already exists, this function does
/// **not** re-run [`run_setup_commands`].  Setup commands typically include
/// destructive operations like `npm ci` (`rm -rf node_modules` then reinstall)
/// that, if run concurrently with another reuse from a different caller, leave
/// `node_modules` in a half-populated state (broken `.bin/*` symlinks pointing
/// at empty package directories).  This used to be rare and tolerable, but
/// after story 1066 added a 30-second periodic reconciler that calls
/// `reconcile_worktree_create` → `create_worktree`, every Coding story got a
/// destructive `npm ci` every 30s — racing the merge-gate's own frontend
/// build and producing the `sh: 1: tsc: not found` failure that bricked
/// story 1086 retries on 2026-05-15.
///
/// The reuse path now matches the documented contract of
/// `reconcile_worktree_create`: "no-op for stories whose worktree already
/// exists."  If a worktree is in a bad state and needs re-setup, the caller
/// must explicitly delete it and call `create_worktree` again.
pub async fn create_worktree(
    project_root: &Path,
    story_id: &str,
    config: &ProjectConfig,
    port: u16,
) -> Result<WorktreeInfo, String> {
    let wt_path = worktree_path(project_root, story_id);
    let branch = branch_name(story_id);
    let base_branch = resolve_base_branch(project_root, config.base_branch.as_deref());
    let root = project_root.to_path_buf();

    // Already exists — reuse without re-running destructive setup commands.
    // Sparse checkout is reconfigured (cheap, idempotent) and `.mcp.json` is
    // rewritten in case the server port changed across restarts.
    if wt_path.exists() {
        let wt_clone = wt_path.clone();
        tokio::task::spawn_blocking(move || configure_sparse_checkout(&wt_clone))
            .await
            .map_err(|e| format!("spawn_blocking: {e}"))??;
        write_mcp_json(&wt_path, port)?;
        return Ok(WorktreeInfo {
            path: wt_path,
            branch,
            base_branch,
        });
    }

    let wt = wt_path.clone();
    let br = branch.clone();

    tokio::task::spawn_blocking(move || create_worktree_sync(&root, &wt, &br))
        .await
        .map_err(|e| format!("spawn_blocking: {e}"))??;

    write_mcp_json(&wt_path, port)?;
    run_setup_commands(&wt_path, config).await;

    Ok(WorktreeInfo {
        path: wt_path,
        branch,
        base_branch,
    })
}

pub(crate) async fn run_setup_commands(wt_path: &Path, config: &ProjectConfig) {
    for component in &config.component {
        let cmd_dir = wt_path.join(&component.path);
        for cmd in &component.setup {
            if let Err(e) = run_shell_command(cmd, &cmd_dir).await {
                slog!("[worktree] setup warning for {}: {e}", component.name);
            }
        }
    }
}

pub(crate) async fn run_teardown_commands(
    wt_path: &Path,
    config: &ProjectConfig,
) -> Result<(), String> {
    for component in &config.component {
        let cmd_dir = wt_path.join(&component.path);
        for cmd in &component.teardown {
            // Best effort — don't fail teardown
            if let Err(e) = run_shell_command(cmd, &cmd_dir).await {
                slog!("[worktree] teardown warning for {}: {e}", component.name);
            }
        }
    }
    Ok(())
}

/// Install a pre-commit git hook in an agent worktree.
///
/// Creates `{wt_path}/.git-hooks/pre-commit` containing a shell script that
/// runs `script/check` and aborts the commit if it fails. Configures the
/// worktree's `core.hooksPath` (via `git config --worktree`) so only this
/// worktree uses the per-worktree hooks directory.
pub fn install_pre_commit_hook(wt_path: &Path) -> Result<(), String> {
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    let hooks_dir = wt_path.join(".git-hooks");
    std::fs::create_dir_all(&hooks_dir).map_err(|e| format!("create .git-hooks dir: {e}"))?;

    let hook = "#!/bin/sh\n\
        #\n\
        # Pre-commit hook installed by huskies.\n\
        # Runs script/check (fmt-check, clippy, cargo check, source-map-check)\n\
        # before every commit. Aborts if any gate fails.\n\
        #\n\
        # Emergency bypass: git commit --no-verify  (see AGENT.md — avoid this)\n\
        \n\
        REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\n\
        \n\
        printf '[pre-commit] Running script/check ...\\n'\n\
        OUTPUT=$(\"$REPO_ROOT/script/check\" 2>&1)\n\
        STATUS=$?\n\
        \n\
        if [ \"$STATUS\" -ne 0 ]; then\n\
            printf '\\n=== PRE-COMMIT HOOK FAILED ===\\n\\n'\n\
            printf '%s\\n' \"$OUTPUT\"\n\
            printf '\\nFix the issues above, then re-validate with:\\n'\n\
            printf '    script/check\\n'\n\
            printf '\\nEmergency bypass (see AGENT.md -- avoid this):\\n'\n\
            printf '    git commit --no-verify\\n\\n'\n\
            exit 1\n\
        fi\n";

    let hook_path = hooks_dir.join("pre-commit");
    std::fs::write(&hook_path, hook).map_err(|e| format!("write pre-commit hook: {e}"))?;

    #[cfg(unix)]
    std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))
        .map_err(|e| format!("chmod pre-commit hook: {e}"))?;

    // `git config --worktree` requires `extensions.worktreeConfig = true` on
    // the repository config; enable it idempotently before the per-worktree
    // hooks-path set below.  Writing without `--worktree` targets the main
    // GIT_DIR/config which all worktrees share, so a single call is enough.
    let _ = std::process::Command::new("git")
        .args(["config", "extensions.worktreeConfig", "true"])
        .current_dir(wt_path)
        .output();

    // Point git at the per-worktree hooks dir so only this worktree uses
    // these hooks (not the main repo or other worktrees).
    slog!(
        "[worktree-hook] starting git config --worktree core.hooksPath for '{}'",
        wt_path.display()
    );
    let output = std::process::Command::new("git")
        .args(["config", "--worktree", "core.hooksPath", ".git-hooks"])
        .current_dir(wt_path)
        .output()
        .map_err(|e| format!("git config --worktree core.hooksPath: {e}"))?;

    slog!(
        "[worktree-hook] finished git config --worktree core.hooksPath for '{}' status={}",
        wt_path.display(),
        output.status
    );
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!(
            "git config --worktree core.hooksPath failed: {stderr}"
        ));
    }

    Ok(())
}

pub(crate) async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> {
    let cmd = cmd.to_string();
    let cwd = cwd.to_path_buf();

    tokio::task::spawn_blocking(move || {
        slog!("[worktree] Running: {cmd} in {}", cwd.display());
        let output = Command::new("sh")
            .args(["-c", &cmd])
            .current_dir(&cwd)
            .output()
            .map_err(|e| format!("Run '{cmd}': {e}"))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(format!("Command '{cmd}' failed: {stderr}"));
        }
        Ok(())
    })
    .await
    .map_err(|e| format!("spawn_blocking: {e}"))?
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{ComponentConfig, WatcherConfig};
    use std::fs;
    use std::process::Command;
    use tempfile::TempDir;

    fn init_git_repo(dir: &Path) {
        Command::new("git")
            .args(["init"])
            .current_dir(dir)
            .output()
            .expect("git init");
        Command::new("git")
            .args(["commit", "--allow-empty", "-m", "init"])
            .current_dir(dir)
            .output()
            .expect("git commit");
    }

    fn empty_config() -> ProjectConfig {
        ProjectConfig {
            component: vec![],
            agent: vec![],
            watcher: WatcherConfig::default(),
            disk_watch: Default::default(),
            default_qa: "server".to_string(),
            default_coder_model: None,
            max_coders: None,
            max_retries: 2,
            base_branch: None,
            rate_limit_notifications: true,
            web_ui_status_consumer: true,
            matrix_status_consumer: true,
            slack_status_consumer: true,
            discord_status_consumer: true,
            whatsapp_status_consumer: true,
            timezone: None,
            rendezvous: None,
            trusted_keys: Vec::new(),
            crdt_require_token: false,
            crdt_tokens: Vec::new(),
            max_mesh_peers: 3,
            gateway_url: None,
            gateway_project: None,
            status_push_enabled: true,
            merge_failure_block_threshold: 3,
            gc_min_free_gb: 0,
        }
    }

    fn failing_setup_config() -> ProjectConfig {
        ProjectConfig {
            component: vec![ComponentConfig {
                name: "broken-build".to_string(),
                path: ".".to_string(),
                setup: vec!["exit 1".to_string()],
                teardown: vec![],
            }],
            ..empty_config()
        }
    }

    #[tokio::test]
    async fn run_shell_command_succeeds_for_echo() {
        let tmp = TempDir::new().unwrap();
        let result = run_shell_command("echo hello", tmp.path()).await;
        assert!(result.is_ok(), "Expected success: {:?}", result.err());
    }

    #[tokio::test]
    async fn run_shell_command_fails_for_nonzero_exit() {
        let tmp = TempDir::new().unwrap();
        let result = run_shell_command("exit 1", tmp.path()).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("failed"));
    }

    #[tokio::test]
    async fn run_setup_commands_no_components_succeeds() {
        let tmp = TempDir::new().unwrap();
        // Should complete without panic
        run_setup_commands(tmp.path(), &empty_config()).await;
    }

    #[tokio::test]
    async fn run_setup_commands_runs_each_command_successfully() {
        let tmp = TempDir::new().unwrap();
        let config = ProjectConfig {
            component: vec![ComponentConfig {
                name: "test".to_string(),
                path: ".".to_string(),
                setup: vec!["echo setup_ok".to_string()],
                teardown: vec![],
            }],
            ..empty_config()
        };
        // Should complete without panic
        run_setup_commands(tmp.path(), &config).await;
    }

    #[tokio::test]
    async fn run_setup_commands_ignores_failures() {
        let tmp = TempDir::new().unwrap();
        let config = ProjectConfig {
            component: vec![ComponentConfig {
                name: "test".to_string(),
                path: ".".to_string(),
                setup: vec!["exit 1".to_string()],
                teardown: vec![],
            }],
            ..empty_config()
        };
        // Setup command failures are non-fatal — should not panic or propagate
        run_setup_commands(tmp.path(), &config).await;
    }

    #[tokio::test]
    async fn run_teardown_commands_ignores_failures() {
        let tmp = TempDir::new().unwrap();
        let config = ProjectConfig {
            component: vec![ComponentConfig {
                name: "test".to_string(),
                path: ".".to_string(),
                setup: vec![],
                teardown: vec!["exit 1".to_string()],
            }],
            ..empty_config()
        };
        // Teardown failures are best-effort — should not propagate
        assert!(run_teardown_commands(tmp.path(), &config).await.is_ok());
    }

    #[tokio::test]
    async fn create_worktree_fresh_creates_dir_and_mcp_json() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        let info = create_worktree(&project_root, "42_fresh_test", &empty_config(), 3001)
            .await
            .unwrap();

        assert!(info.path.exists());
        assert!(info.path.join(".mcp.json").exists());
        let mcp = fs::read_to_string(info.path.join(".mcp.json")).unwrap();
        assert!(mcp.contains("3001"));
        assert_eq!(info.branch, "feature/story-42_fresh_test");
    }

    #[tokio::test]
    async fn create_worktree_reuses_existing_path_and_updates_port() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        // First creation
        let _info1 = create_worktree(&project_root, "43_reuse_test", &empty_config(), 3001)
            .await
            .unwrap();
        // Second call — worktree already exists, reuse path, update port
        let info2 = create_worktree(&project_root, "43_reuse_test", &empty_config(), 3002)
            .await
            .unwrap();

        let mcp = fs::read_to_string(info2.path.join(".mcp.json")).unwrap();
        assert!(
            mcp.contains("3002"),
            "MCP json should be updated to new port"
        );
    }

    #[tokio::test]
    async fn create_worktree_succeeds_despite_setup_failure() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        // Even though setup commands fail, create_worktree must succeed
        // so the agent can start and fix the problem itself.
        let result = create_worktree(
            &project_root,
            "172_setup_fail",
            &failing_setup_config(),
            3001,
        )
        .await;
        assert!(
            result.is_ok(),
            "create_worktree must succeed even if setup commands fail: {:?}",
            result.err()
        );
        let info = result.unwrap();
        assert!(info.path.exists());
        assert!(info.path.join(".mcp.json").exists());
    }

    #[tokio::test]
    async fn create_worktree_reuse_does_not_rerun_setup_commands() {
        // Regression for the 2026-05-15 1086 outage: the reuse path used to
        // re-run setup commands (including destructive `npm ci`).  Combined
        // with story 1066's 30-second periodic reconciler, this fired
        // `npm ci` against every Coding story every 30s and caused
        // `tsc: not found` gate failures.  The reuse path must now be a
        // no-op for setup commands.
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        // First creation — no setup commands, should succeed
        create_worktree(&project_root, "173_reuse_no_setup", &empty_config(), 3001)
            .await
            .unwrap();

        // Second call — worktree exists.  Setup commands are configured to
        // FAIL (`exit 1`); if the reuse path were still running them, the
        // failure log would surface — but more importantly, this test
        // documents that the reuse path is expected to NEVER reach
        // `run_setup_commands` and therefore can never produce a setup
        // failure regardless of how broken the setup config is.
        let result = create_worktree(
            &project_root,
            "173_reuse_no_setup",
            &failing_setup_config(),
            3002,
        )
        .await;
        assert!(
            result.is_ok(),
            "reuse must succeed and must not run setup commands: {:?}",
            result.err()
        );
    }

    #[tokio::test]
    async fn create_worktree_reuse_does_not_create_setup_marker_file() {
        // Stronger version of the above: assert that on reuse, a setup
        // command that would have created a marker file does NOT run.
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("my-project");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        // First creation — no setup, so no marker yet.
        let info = create_worktree(&project_root, "174_reuse_marker", &empty_config(), 3001)
            .await
            .unwrap();
        let marker = info.path.join("__setup_ran__");
        assert!(!marker.exists(), "no marker after empty-setup creation");

        // Second call with a setup command that WOULD create the marker if
        // run.  The reuse path must not run it.
        let cfg = ProjectConfig {
            component: vec![ComponentConfig {
                name: "marker".to_string(),
                path: ".".to_string(),
                setup: vec!["touch __setup_ran__".to_string()],
                teardown: vec![],
            }],
            ..empty_config()
        };
        create_worktree(&project_root, "174_reuse_marker", &cfg, 3002)
            .await
            .unwrap();

        assert!(
            !marker.exists(),
            "reuse path must not run setup commands; marker file was created"
        );
    }

    /// Regression: before the fix, `install_pre_commit_hook` was called directly
    /// from an async context, monopolising a tokio worker thread per call.  With
    /// `worker_threads=2` and two concurrent installs the executor would stall.
    /// After the fix the installs run on `spawn_blocking`'s thread pool, leaving
    /// both worker threads free to drive async tasks like the sleep below.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn install_pre_commit_hook_does_not_block_tokio_executor() {
        let tmp = TempDir::new().unwrap();

        // Helper: create a git repo and a linked worktree inside tmp.
        let setup = |repo: &str, wt: &str, branch: &str| -> std::path::PathBuf {
            let repo_path = tmp.path().join(repo);
            fs::create_dir_all(&repo_path).unwrap();
            init_git_repo(&repo_path);
            Command::new("git")
                .args(["config", "extensions.worktreeConfig", "true"])
                .current_dir(&repo_path)
                .output()
                .unwrap();
            let wt_path = tmp.path().join(wt);
            Command::new("git")
                .args(["worktree", "add", wt_path.to_str().unwrap(), "-b", branch])
                .current_dir(&repo_path)
                .output()
                .unwrap();
            wt_path
        };

        let wt1 = setup("repo1", "wt1", "feature/noblock-1");
        let wt2 = setup("repo2", "wt2", "feature/noblock-2");

        // Both hook installs run on the blocking pool — executor threads stay free.
        let h1 = tokio::task::spawn_blocking(move || install_pre_commit_hook(&wt1));
        let h2 = tokio::task::spawn_blocking(move || install_pre_commit_hook(&wt2));

        let start = std::time::Instant::now();
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        let elapsed = start.elapsed();

        h1.await.unwrap().expect("hook install 1 must succeed");
        h2.await.unwrap().expect("hook install 2 must succeed");

        assert!(
            elapsed < std::time::Duration::from_millis(500),
            "tokio executor was starved; 100 ms sleep took {elapsed:?}"
        );
    }

    #[test]
    fn install_pre_commit_hook_creates_executable_hook_and_sets_hookspath() {
        let tmp = TempDir::new().unwrap();
        let project_root = tmp.path().join("main-repo");
        fs::create_dir_all(&project_root).unwrap();
        init_git_repo(&project_root);

        // Enable per-worktree config so git config --worktree works.
        Command::new("git")
            .args(["config", "extensions.worktreeConfig", "true"])
            .current_dir(&project_root)
            .output()
            .expect("enable extensions.worktreeConfig");

        // Create a linked worktree to simulate what huskies does for agents.
        let wt_path = tmp.path().join("linked-wt");
        let out = Command::new("git")
            .args([
                "worktree",
                "add",
                wt_path.to_str().unwrap(),
                "-b",
                "feature/hook-test",
            ])
            .current_dir(&project_root)
            .output()
            .expect("git worktree add");
        assert!(
            out.status.success(),
            "git worktree add failed: {}",
            String::from_utf8_lossy(&out.stderr)
        );

        install_pre_commit_hook(&wt_path).expect("install_pre_commit_hook must succeed");

        // Hook file must exist.
        let hook_path = wt_path.join(".git-hooks").join("pre-commit");
        assert!(hook_path.exists(), "pre-commit hook must be created");

        // Hook must be executable.
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = fs::metadata(&hook_path).unwrap().permissions().mode();
            assert!(mode & 0o111 != 0, "pre-commit hook must be executable");
        }

        // Hook content must reference script/check and --no-verify.
        let content = fs::read_to_string(&hook_path).unwrap();
        assert!(
            content.contains("script/check"),
            "hook must invoke script/check; got:\n{content}"
        );
        assert!(
            content.contains("--no-verify"),
            "hook must mention --no-verify bypass; got:\n{content}"
        );

        // git config core.hooksPath for the worktree must be .git-hooks.
        let cfg_out = Command::new("git")
            .args(["config", "--worktree", "core.hooksPath"])
            .current_dir(&wt_path)
            .output()
            .expect("git config --worktree core.hooksPath");
        assert!(
            cfg_out.status.success(),
            "git config --worktree core.hooksPath lookup failed"
        );
        let value = String::from_utf8_lossy(&cfg_out.stdout).trim().to_string();
        assert_eq!(
            value, ".git-hooks",
            "core.hooksPath must be set to .git-hooks"
        );
    }
}
