//! Low-level synchronous git operations for worktree management.
use crate::slog;
use std::path::Path;
use std::process::Command;

pub(crate) fn branch_name(story_id: &str) -> String {
    format!("feature/story-{story_id}")
}

/// Detect the current branch of the project root (the base branch worktrees fork from).
pub(crate) fn detect_base_branch(project_root: &Path) -> String {
    Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .current_dir(project_root)
        .output()
        .ok()
        .and_then(|o| {
            if o.status.success() {
                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
            } else {
                None
            }
        })
        .unwrap_or_else(|| "master".to_string())
}

/// Resolve the base branch to use for `project_root`.
///
/// Returns `configured` (typically `ProjectConfig.base_branch`) when set;
/// otherwise auto-detects the repository's default branch via
/// [`detect_base_branch`]. This is the single shared resolver for base-branch
/// fallback — production call sites must use it rather than hardcoding
/// `"master"` directly.
pub(crate) fn resolve_base_branch(project_root: &Path, configured: Option<&str>) -> String {
    configured
        .map(str::to_string)
        .unwrap_or_else(|| detect_base_branch(project_root))
}

/// Placeholder for worktree isolation of `.huskies/work/`.
///
/// Previous approaches (sparse checkout, skip-worktree) all leaked state
/// from worktrees back to the main checkout's config/index. For now this
/// is a no-op — merge conflicts from pipeline file moves are handled at
/// merge time by the mergemaster (squash merge ignores work/ diffs).
pub(crate) fn configure_sparse_checkout(_wt_path: &Path) -> Result<(), String> {
    Ok(())
}

/// Create a git worktree at `wt_path` on `branch`, pruning stale references first.
pub(crate) fn create_worktree_sync(
    project_root: &Path,
    wt_path: &Path,
    branch: &str,
) -> Result<(), String> {
    // Ensure the parent directory exists
    if let Some(parent) = wt_path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| format!("Create worktree dir: {e}"))?;
    }

    // Prune stale worktree references (e.g. directories deleted externally)
    let _ = Command::new("git")
        .args(["worktree", "prune"])
        .current_dir(project_root)
        .output();

    // Try to create branch. If it already exists that's fine.
    let _ = Command::new("git")
        .args(["branch", branch])
        .current_dir(project_root)
        .output();

    // Create worktree
    let output = Command::new("git")
        .args(["worktree", "add", &wt_path.to_string_lossy(), branch])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("git worktree add: {e}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // If it says already checked out, that's fine
        if stderr.contains("already checked out") || stderr.contains("already exists") {
            return Ok(());
        }
        return Err(format!("git worktree add failed: {stderr}"));
    }

    // Enable sparse checkout to exclude pipeline files from the worktree.
    // This prevents .huskies/work/ changes from ending up in feature branches,
    // which cause rename/delete merge conflicts when merging back to master.
    configure_sparse_checkout(wt_path)?;

    Ok(())
}

/// Remove a git worktree directory and delete its branch.
pub(crate) fn remove_worktree_sync(
    project_root: &Path,
    wt_path: &Path,
    branch: &str,
) -> Result<(), String> {
    // Remove worktree
    let output = Command::new("git")
        .args(["worktree", "remove", "--force", &wt_path.to_string_lossy()])
        .current_dir(project_root)
        .output()
        .map_err(|e| format!("git worktree remove: {e}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        if stderr.contains("not a working tree") {
            // Orphaned directory: git doesn't recognise it as a worktree.
            // Remove the directory directly and prune stale git metadata.
            slog!(
                "[worktree] orphaned worktree detected, removing directory: {}",
                wt_path.display()
            );
            if let Err(e) = std::fs::remove_dir_all(wt_path) {
                slog!("[worktree] failed to remove orphaned directory: {e}");
            }
            let _ = Command::new("git")
                .args(["worktree", "prune"])
                .current_dir(project_root)
                .output();
        } else {
            slog!("[worktree] remove warning: {stderr}");
        }
    }

    // Delete branch (best effort, force-delete to handle unmerged branches)
    let _ = Command::new("git")
        .args(["branch", "-D", branch])
        .current_dir(project_root)
        .output();

    Ok(())
}

/// Migrate filesystem artifacts for story IDs that were rewritten from slug form
/// (`664_story_my_feature`) to numeric-only form (`664`).
///
/// For each `(old_id, new_id)` pair (as returned by
/// `crdt_state::migrate_story_ids_to_numeric`), this function:
/// 1. Moves the git worktree directory via `git worktree move` when it exists.
/// 2. Renames the git branch from `feature/story-{old_id}` to `feature/story-{new_id}`.
/// 3. Renames the log directory at `.huskies/logs/{old_id}` when it exists.
///
/// All steps are best-effort: failures are logged but do not abort the migration.
/// Operations are skipped when the destination path already exists (idempotent).
pub fn migrate_slug_paths(project_root: &Path, migrations: &[(String, String)]) {
    for (old_id, new_id) in migrations {
        // ── Worktree directory ──────────────────────────────────────────────
        let old_wt = super::worktree_path(project_root, old_id);
        let new_wt = super::worktree_path(project_root, new_id);

        if old_wt.exists() && !new_wt.exists() {
            let out = Command::new("git")
                .args([
                    "worktree",
                    "move",
                    &old_wt.to_string_lossy(),
                    &new_wt.to_string_lossy(),
                ])
                .current_dir(project_root)
                .output();
            match out {
                Ok(o) if o.status.success() => {
                    slog!("[migrate] Moved worktree {old_id} → {new_id}");
                }
                Ok(o) => {
                    let stderr = String::from_utf8_lossy(&o.stderr);
                    slog!(
                        "[migrate] git worktree move failed for {old_id}: {stderr}; \
                         falling back to directory rename"
                    );
                    if let Err(e) = std::fs::rename(&old_wt, &new_wt) {
                        slog!("[migrate] Directory rename for worktree {old_id} failed: {e}");
                    } else {
                        slog!("[migrate] Renamed worktree directory {old_id} → {new_id}");
                    }
                }
                Err(e) => {
                    slog!("[migrate] git worktree move error for {old_id}: {e}");
                }
            }
        }

        // ── Git branch ──────────────────────────────────────────────────────
        let old_branch = branch_name(old_id);
        let new_branch = branch_name(new_id);
        let out = Command::new("git")
            .args(["branch", "-m", &old_branch, &new_branch])
            .current_dir(project_root)
            .output();
        match out {
            Ok(o) if o.status.success() => {
                slog!("[migrate] Renamed branch {old_branch} → {new_branch}");
            }
            Ok(o) => {
                let stderr = String::from_utf8_lossy(&o.stderr);
                // Branch may not exist (story already merged/archived) — log at debug level.
                slog!("[migrate] Branch rename skipped for {old_id}: {stderr}");
            }
            Err(e) => {
                slog!("[migrate] Branch rename error for {old_id}: {e}");
            }
        }

        // ── Log directory ───────────────────────────────────────────────────
        let old_log = project_root.join(".huskies").join("logs").join(old_id);
        let new_log = project_root.join(".huskies").join("logs").join(new_id);
        if old_log.exists() && !new_log.exists() {
            if let Err(e) = std::fs::rename(&old_log, &new_log) {
                slog!("[migrate] Log directory rename for {old_id} failed: {e}");
            } else {
                slog!("[migrate] Moved log directory {old_id} → {new_id}");
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    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");
    }

    #[test]
    fn branch_name_format() {
        assert_eq!(branch_name("42_my_story"), "feature/story-42_my_story");
        assert_eq!(branch_name("1_test"), "feature/story-1_test");
    }

    #[test]
    fn numeric_id_branch_name_uses_number_only() {
        assert_eq!(branch_name("664"), "feature/story-664");
        assert_eq!(branch_name("730"), "feature/story-730");
    }

    #[test]
    fn numeric_id_worktree_path_uses_number_only() {
        let project_root = Path::new("/home/user/my-project");
        let path = super::super::worktree_path(project_root, "664");
        assert_eq!(
            path,
            Path::new("/home/user/my-project/.huskies/worktrees/664")
        );
    }

    #[test]
    fn detect_base_branch_returns_branch_in_git_repo() {
        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 branch = detect_base_branch(&project_root);
        assert!(!branch.is_empty());
    }

    #[test]
    fn detect_base_branch_falls_back_to_master_for_non_git_dir() {
        let tmp = TempDir::new().unwrap();
        let branch = detect_base_branch(tmp.path());
        assert_eq!(branch, "master");
    }

    #[test]
    fn configure_sparse_checkout_is_noop() {
        let tmp = TempDir::new().unwrap();
        assert!(configure_sparse_checkout(tmp.path()).is_ok());
    }

    #[test]
    fn create_worktree_after_stale_reference() {
        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 wt_path = tmp.path().join("my-worktree");
        let branch = "feature/test-stale";

        // First creation should succeed
        create_worktree_sync(&project_root, &wt_path, branch).unwrap();
        assert!(wt_path.exists());

        // Simulate external deletion (e.g., rm -rf by another agent)
        fs::remove_dir_all(&wt_path).unwrap();
        assert!(!wt_path.exists());

        // Second creation should succeed despite stale git reference.
        // Without `git worktree prune`, this fails with "already checked out"
        // or "already exists".
        let result = create_worktree_sync(&project_root, &wt_path, branch);
        assert!(
            result.is_ok(),
            "Expected worktree creation to succeed after stale reference, got: {:?}",
            result.err()
        );
        assert!(wt_path.exists());
    }

    #[test]
    fn worktree_has_all_files_including_work() {
        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);

        // Create a tracked file under .huskies/work/ on the initial branch
        let work_dir = project_root.join(".huskies").join("work");
        fs::create_dir_all(&work_dir).unwrap();
        fs::write(work_dir.join("test_story.md"), "# Test").unwrap();
        Command::new("git")
            .args(["add", "."])
            .current_dir(&project_root)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", "add work file"])
            .current_dir(&project_root)
            .output()
            .unwrap();

        let wt_path = tmp.path().join("my-worktree");
        let branch = "feature/test-sparse";
        create_worktree_sync(&project_root, &wt_path, branch).unwrap();

        // Worktree should have all files including .huskies/work/
        assert!(wt_path.join(".huskies").join("work").exists());
        assert!(wt_path.join(".git").exists());

        // Main checkout must NOT be affected by worktree creation.
        assert!(
            work_dir.exists(),
            ".huskies/work/ must still exist in the main checkout"
        );
    }

    #[test]
    fn remove_worktree_sync_removes_orphaned_directory() {
        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);

        // Create a directory that looks like a worktree but isn't registered with git
        let wt_path = project_root
            .join(".huskies")
            .join("worktrees")
            .join("orphan");
        fs::create_dir_all(&wt_path).unwrap();
        fs::write(wt_path.join("some_file.txt"), "stale").unwrap();
        assert!(wt_path.exists());

        // git worktree remove will fail with "not a working tree",
        // but the fallback should rm -rf the directory
        remove_worktree_sync(&project_root, &wt_path, "feature/orphan").unwrap();
        assert!(!wt_path.exists());
    }

    #[test]
    fn remove_worktree_sync_cleans_up_directory() {
        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 wt_path = project_root
            .join(".huskies")
            .join("worktrees")
            .join("test_rm");
        create_worktree_sync(&project_root, &wt_path, "feature/test-rm").unwrap();
        assert!(wt_path.exists());

        remove_worktree_sync(&project_root, &wt_path, "feature/test-rm").unwrap();
        assert!(!wt_path.exists());
    }
}
