//! Squash-merge orchestration: rebase agent work onto master and run post-merge gates.

#![allow(unused_imports, dead_code)]
use std::path::Path;
use std::process::Command;
use std::sync::Mutex;

use super::super::gates::run_project_tests;
use super::{MergeReport, MergeResult};
use crate::config::ProjectConfig;

/// Global lock ensuring only one squash-merge runs at a time.
///
/// The merge pipeline uses a shared `.huskies/merge_workspace` directory and
/// temporary `merge-queue/{story_id}` branches.  If two merges run concurrently,
/// the second call's initial cleanup destroys the first call's branch mid-flight,
/// causing `git cherry-pick merge-queue/…` to fail with "bad revision".
static MERGE_LOCK: Mutex<()> = Mutex::new(());

/// Returns `true` when no squash-merge is currently running, i.e. the merge
/// lock is free.
///
/// Used by the build-directory GC pass (story 1199) to decide whether
/// `.huskies/merge_workspace` is safe to reclaim. This is a best-effort,
/// momentary check — the lock is not held across the reclaim itself, so a
/// merge that starts immediately afterward can still race with GC. That's
/// acceptable: the GC pass tolerates races and skips on error rather than
/// failing the whole pass.
pub(crate) fn merge_lock_is_free() -> bool {
    MERGE_LOCK.try_lock().is_ok()
}

/// Resolve the base branch for `project_root` from config, or auto-detect it.
fn resolve_base_branch(project_root: &Path) -> String {
    let configured = crate::config::ProjectConfig::load(project_root)
        .ok()
        .and_then(|c| c.base_branch);
    crate::worktree::resolve_base_branch(project_root, configured.as_deref())
}

pub(crate) fn run_squash_merge(
    project_root: &Path,
    branch: &str,
    story_id: &str,
) -> Result<MergeResult, String> {
    // Acquire the merge lock so concurrent calls don't clobber each other.
    let _lock = MERGE_LOCK
        .lock()
        .map_err(|e| format!("Merge lock poisoned: {e}"))?;

    // ── Pre-flight: verify the branch has commits ahead of base ──────────────
    // A zero-commit branch produces an empty squash and a silent "nothing to
    // commit" failure.  Catch it early with a grep-able error before any merge
    // work starts.
    let base_branch = resolve_base_branch(project_root);

    let ahead_out = Command::new("git")
        .args(["rev-list", "--count", &format!("{base_branch}..{branch}")])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("Failed to count commits ahead: {e}"))?;

    if ahead_out.status.success() {
        let ahead: u64 = String::from_utf8_lossy(&ahead_out.stdout)
            .trim()
            .parse()
            .unwrap_or(1); // parse failure → don't false-positive; let merge proceed
        if ahead == 0 {
            return Ok(MergeResult::NoCommits {
                output: format!(
                    "{story_id}: no commits to merge — feature branch '{branch}' \
                     has 0 commits ahead of '{base_branch}'"
                ),
            });
        }
    }

    let mut all_output = String::new();
    let merge_branch = format!("merge-queue/{story_id}");
    let merge_wt_path = project_root.join(".huskies").join("merge_workspace");

    // Ensure we start clean: remove any leftover merge workspace.
    cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);

    // ── Create merge-queue branch at current HEAD ─────────────────
    all_output.push_str(&format!(
        "=== Creating merge-queue branch '{merge_branch}' ===\n"
    ));
    let create_branch = Command::new("git")
        .args(["branch", &merge_branch])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("Failed to create merge-queue branch: {e}"))?;
    if !create_branch.status.success() {
        let stderr = String::from_utf8_lossy(&create_branch.stderr);
        all_output.push_str(&format!("Branch creation failed: {stderr}\n"));
        return Err(format!("Failed to create merge-queue branch: {stderr}"));
    }

    // ── Create temporary worktree ─────────────────────────────────
    all_output.push_str("=== Creating temporary merge worktree ===\n");
    let wt_str = merge_wt_path.to_string_lossy().to_string();
    let create_wt = Command::new("git")
        .args(["worktree", "add", &wt_str, &merge_branch])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("Failed to create merge worktree: {e}"))?;
    if !create_wt.status.success() {
        let stderr = String::from_utf8_lossy(&create_wt.stderr);
        all_output.push_str(&format!("Worktree creation failed: {stderr}\n"));
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Err(format!("Failed to create merge worktree: {stderr}"));
    }

    // ── Squash-merge in the temporary worktree ────────────────────
    all_output.push_str(&format!("=== git merge --squash {branch} ===\n"));
    let merge = Command::new("git")
        .args(["merge", "--squash", branch])
        .current_dir(&merge_wt_path)
        .output()
        .map_err(|e| format!("Failed to run git merge: {e}"))?;

    let merge_stdout = String::from_utf8_lossy(&merge.stdout).to_string();
    let merge_stderr = String::from_utf8_lossy(&merge.stderr).to_string();
    all_output.push_str(&merge_stdout);
    all_output.push_str(&merge_stderr);
    all_output.push('\n');

    if !merge.status.success() {
        all_output.push_str(
            "=== Conflicts detected — aborting merge. Use `start_agent mergemaster` \
             to invoke LLM-driven conflict resolution. ===\n",
        );
        let details =
            format!("Merge conflicts in branch '{branch}':\n{merge_stdout}{merge_stderr}");
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Ok(MergeResult::Conflict {
            details: Some(details),
            output: all_output,
        });
    }

    // ── Commit in the temporary worktree ──────────────────────────
    all_output.push_str("=== git commit ===\n");
    // Include human-readable name and item type when the CRDT is available.
    // Falls back to the bare ID when running outside the server (e.g. in tests).
    let story_label = crate::crdt_state::read_item(story_id)
        .map(|item| {
            let type_str = item.item_type().map(|t| t.as_str()).unwrap_or("story");
            format!(" {} {}", type_str, item.name())
        })
        .unwrap_or_default();
    let commit_msg = format!("huskies: merge {story_id}{story_label}");
    let commit = Command::new("git")
        .args(["commit", "-m", &commit_msg])
        .current_dir(&merge_wt_path)
        .output()
        .map_err(|e| format!("Failed to run git commit: {e}"))?;

    let commit_stdout = String::from_utf8_lossy(&commit.stdout).to_string();
    let commit_stderr = String::from_utf8_lossy(&commit.stderr).to_string();
    all_output.push_str(&commit_stdout);
    all_output.push_str(&commit_stderr);
    all_output.push('\n');

    if !commit.status.success() {
        if commit_stderr.contains("nothing to commit")
            || commit_stdout.contains("nothing to commit")
        {
            // Bug 777: "nothing to commit" after a clean squash means the feature
            // branch's content is already on `base_branch` (idempotent retry after
            // a previous successful merge).  Return success so the caller advances
            // the story to `5_done` instead of overwriting that state with
            // `merge_failure`.  The pre-flight `ahead == 0` check above already
            // catches genuinely empty feature branches, so reaching this point
            // implies "ahead > 0 but already merged".
            all_output
                .push_str("=== Nothing to commit — feature branch already merged into base ===\n");
            cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
            return Ok(MergeResult::Success {
                conflicts_resolved: false,
                conflict_details: None,
                gate_output: all_output,
            });
        }
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Ok(MergeResult::Other {
            output: all_output,
            conflict_details: None,
        });
    }

    // ── Bug 226: Verify the commit contains real code changes ─────
    // If the merge only brought in .huskies/ files (pipeline file moves),
    // there are no actual code changes to land on master.  Abort.
    {
        let diff_check = Command::new("git")
            .args(["diff", "--name-only", "HEAD~1..HEAD"])
            .current_dir(&merge_wt_path)
            .output()
            .map_err(|e| format!("Failed to check merge diff: {e}"))?;
        let changed_files = String::from_utf8_lossy(&diff_check.stdout);
        let has_code_changes = changed_files
            .lines()
            .any(|f| !f.starts_with(".huskies/work/"));
        if !has_code_changes {
            all_output.push_str(
                "=== Merge commit contains only .huskies/ file moves, no code changes ===\n",
            );
            cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
            return Ok(MergeResult::Other {
                output: all_output,
                conflict_details: Some(
                    "Feature branch has no code changes outside .huskies/ — only \
                     pipeline file moves were found."
                        .to_string(),
                ),
            });
        }
    }

    // ── Run component setup from project.toml (same as worktree creation) ──────────
    {
        let config = ProjectConfig::load(&merge_wt_path).unwrap_or_default();
        if !config.component.is_empty() {
            all_output.push_str("=== component setup (merge worktree) ===\n");
        }
        for component in &config.component {
            let cmd_dir = merge_wt_path.join(&component.path);
            for cmd in &component.setup {
                all_output.push_str(&format!("--- {}: {cmd} ---\n", component.name));
                match Command::new("sh")
                    .args(["-c", cmd])
                    .current_dir(&cmd_dir)
                    .output()
                {
                    Ok(out) => {
                        all_output.push_str(&String::from_utf8_lossy(&out.stdout));
                        all_output.push_str(&String::from_utf8_lossy(&out.stderr));
                        all_output.push('\n');
                        if !out.status.success() {
                            all_output.push_str(&format!(
                                "=== setup warning: '{}' failed: {cmd} ===\n",
                                component.name
                            ));
                        }
                    }
                    Err(e) => {
                        all_output.push_str(&format!(
                            "=== setup warning: failed to run '{cmd}': {e} ===\n"
                        ));
                    }
                }
            }
        }
    }

    // ── Quality gates in merge workspace (BEFORE fast-forward) ────
    // Run gates in the merge worktree so that failures abort before master moves.
    all_output.push_str("=== Running quality gates before fast-forward ===\n");
    match run_merge_quality_gates(&merge_wt_path) {
        Ok(outcome) if outcome.passed => {
            all_output.push_str(&outcome.output);
            all_output.push('\n');
            all_output.push_str("=== Quality gates passed ===\n");
        }
        Ok(outcome) => {
            all_output.push_str(&outcome.output);
            all_output.push('\n');
            all_output.push_str(
                "=== Quality gates FAILED — aborting fast-forward, master unchanged ===\n",
            );
            cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
            return Ok(MergeResult::GateFailure {
                output: all_output,
                failure_kind: outcome.failure_kind,
            });
        }
        Err(e) => {
            all_output.push_str(&format!("Gate check error: {e}\n"));
            cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
            return Ok(MergeResult::GateFailure {
                output: all_output,
                failure_kind: None,
            });
        }
    }

    // ── Cherry-pick the squash commit onto master ──────────────────
    // We cherry-pick instead of fast-forward so that concurrent filesystem
    // watcher commits on master (e.g. pipeline file moves) don't block the
    // merge.  Cherry-pick applies the diff of the squash commit cleanly on
    // top of master's current HEAD.
    all_output.push_str(&format!(
        "=== Cherry-picking squash commit from {merge_branch} onto master ===\n"
    ));
    let cp = Command::new("git")
        .args(["cherry-pick", &merge_branch])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("Failed to cherry-pick merge-queue commit: {e}"))?;

    let cp_stdout = String::from_utf8_lossy(&cp.stdout).to_string();
    let cp_stderr = String::from_utf8_lossy(&cp.stderr).to_string();
    all_output.push_str(&cp_stdout);
    all_output.push_str(&cp_stderr);
    all_output.push('\n');

    if !cp.status.success() {
        // Abort the cherry-pick so master is left clean.
        let _ = Command::new("git")
            .args(["cherry-pick", "--abort"])
            .current_dir(project_root)
            .output();
        all_output.push_str("=== Cherry-pick failed — aborting, master unchanged ===\n");
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Ok(MergeResult::Other {
            output: all_output,
            conflict_details: Some(format!(
                "Cherry-pick of squash commit failed (conflict with master?):\n{cp_stderr}"
            )),
        });
    }

    // ── Verify code landed on the correct branch ──────────────────
    // Guard against the cherry-pick silently landing on the wrong branch
    // (e.g. a merge-queue branch from a concurrent merge).  If the current
    // branch is not the base branch, or the HEAD commit has no code diff,
    // treat the merge as failed so the story stays in the merge stage.
    let current_branch = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .current_dir(project_root)
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();

    if current_branch != base_branch {
        all_output.push_str(&format!(
            "=== VERIFICATION FAILED: expected branch '{base_branch}' but HEAD is on \
             '{current_branch}'. Cherry-pick landed on wrong branch. ===\n"
        ));
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Ok(MergeResult::Other {
            output: all_output,
            conflict_details: Some(format!(
                "Cherry-pick landed on '{current_branch}' instead of '{base_branch}'"
            )),
        });
    }

    // Verify HEAD commit has actual code changes (not an empty cherry-pick).
    // Exclude .huskies/work/ (pipeline file moves) but keep .huskies/project.toml
    // and other config files which are legitimate deliverables.
    let diff_stat = Command::new("git")
        .args([
            "diff",
            "--stat",
            "HEAD~1..HEAD",
            "--",
            ".",
            ":(exclude).huskies/work",
        ])
        .current_dir(project_root)
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();

    if diff_stat.is_empty() {
        all_output.push_str(
            "=== VERIFICATION FAILED: cherry-pick produced no code changes on master. ===\n",
        );
        cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
        return Ok(MergeResult::Other {
            output: all_output,
            conflict_details: Some(
                "Cherry-pick commit contains no code changes (empty diff)".to_string(),
            ),
        });
    }

    all_output.push_str(&format!(
        "=== Verified: cherry-pick landed on '{base_branch}' with code changes ===\n"
    ));

    // ── Regen source-map.json on master after cherry-pick ─────────
    // Run deterministically on project_root (now on master). Skip the commit
    // when regen produces no diff (idempotent case). Failure is non-fatal.
    {
        let map_path = project_root.join(".huskies").join("source-map.json");
        let old_content = std::fs::read_to_string(&map_path).ok();
        match source_map_gen::regenerate_source_map(project_root, &map_path) {
            Err(e) => {
                all_output.push_str(&format!(
                    "=== source-map regen failed (non-fatal): {e} ===\n"
                ));
            }
            Ok(()) => {
                let new_content = std::fs::read_to_string(&map_path).ok();
                if old_content != new_content {
                    all_output.push_str("=== source-map.json changed — committing on master ===\n");
                    let _ = Command::new("git")
                        .args(["add", ".huskies/source-map.json"])
                        .current_dir(project_root)
                        .output();
                    match Command::new("git")
                        .args(["commit", "-m", "huskies: regen source-map.json"])
                        .current_dir(project_root)
                        .output()
                    {
                        Ok(c) if c.status.success() => {
                            all_output.push_str("=== source-map.json committed on master ===\n");
                        }
                        Ok(c) => {
                            let stderr = String::from_utf8_lossy(&c.stderr);
                            all_output.push_str(&format!(
                                "=== source-map commit failed (non-fatal): {stderr} ===\n"
                            ));
                        }
                        Err(e) => {
                            all_output.push_str(&format!(
                                "=== source-map commit error (non-fatal): {e} ===\n"
                            ));
                        }
                    }
                } else {
                    all_output
                        .push_str("=== source-map.json unchanged — no follow-up commit ===\n");
                }
            }
        }
    }

    // ── Clean up ──────────────────────────────────────────────────
    cleanup_merge_workspace(project_root, &merge_wt_path, &merge_branch);
    all_output.push_str("=== Merge-queue cleanup complete ===\n");

    Ok(MergeResult::Success {
        conflicts_resolved: false,
        conflict_details: None,
        gate_output: all_output,
    })
}

pub(crate) fn cleanup_merge_workspace(
    project_root: &Path,
    merge_wt_path: &Path,
    merge_branch: &str,
) {
    let wt_str = merge_wt_path.to_string_lossy().to_string();
    let _ = Command::new("git")
        .args(["worktree", "remove", "--force", &wt_str])
        .current_dir(project_root)
        .output();
    // If the directory still exists (e.g. it was a plain directory from a
    // previous failed run, not a registered git worktree), remove it so
    // the next `git worktree add` can succeed.
    if merge_wt_path.exists() {
        let _ = std::fs::remove_dir_all(merge_wt_path);
    }
    let _ = Command::new("git")
        .args(["branch", "-D", merge_branch])
        .current_dir(project_root)
        .output();
}

fn run_merge_quality_gates(
    project_root: &Path,
) -> Result<crate::agents::gates::GateOutcome, String> {
    use crate::agents::gates::GateOutcome;

    let mut all_output = String::new();
    let mut all_passed = true;

    let script_test = project_root.join("script").join("test");

    if script_test.exists() {
        // Delegate entirely to script/test — it is the single source of truth
        // for the full test suite (clippy, cargo tests, frontend builds, etc.).
        let (success, output) = run_project_tests(project_root)?;
        all_output.push_str(&output);
        if !success {
            all_passed = false;
        }
        return if all_passed {
            Ok(GateOutcome::pass(all_output))
        } else {
            Ok(GateOutcome::fail(all_output))
        };
    }

    // No script/test — fall back to cargo gates for Rust projects.
    let cargo_toml = project_root.join("Cargo.toml");
    if cargo_toml.exists() {
        let clippy = Command::new("cargo")
            .args(["clippy", "--all-targets", "--all-features"])
            .current_dir(project_root)
            .output()
            .map_err(|e| format!("Failed to run cargo clippy: {e}"))?;

        all_output.push_str("=== cargo clippy ===\n");
        let clippy_out = format!(
            "{}{}",
            String::from_utf8_lossy(&clippy.stdout),
            String::from_utf8_lossy(&clippy.stderr)
        );
        all_output.push_str(&clippy_out);
        all_output.push('\n');

        if !clippy.status.success() {
            all_passed = false;
        }

        let (test_success, test_out) = run_project_tests(project_root)?;
        all_output.push_str(&test_out);
        if !test_success {
            all_passed = false;
        }
    }

    if all_passed {
        Ok(GateOutcome::pass(all_output))
    } else {
        Ok(GateOutcome::fail(all_output))
    }
}

#[cfg(test)]
mod tests_advanced;
#[cfg(test)]
mod tests_basic;
#[cfg(test)]
mod tests_changelog;
