//! Git command runner.
//!
//! Wraps `tokio::process::Command` to execute git operations on a local
//! repository path. All methods return structured results with proper
//! error handling for git-not-installed, not-a-repo, etc.

use crate::error::{ProjectError, Result};
use std::path::{Path, PathBuf};
use tokio::process::Command;

/// Executes git commands against a local repository.
#[derive(Clone, Debug)]
pub struct GitRunner {
    repo_path: PathBuf,
}

/// Parsed output of `git status --porcelain=v2 --branch`.
#[derive(Clone, Debug, Default)]
pub struct GitStatus {
    /// Current branch (empty if detached HEAD)
    pub branch: String,
    /// Whether there are uncommitted changes
    pub is_dirty: bool,
    /// Commits ahead of upstream
    pub ahead: u32,
    /// Commits behind upstream
    pub behind: u32,
}

/// Parsed output of `git worktree list --porcelain`.
#[derive(Clone, Debug)]
pub struct WorktreeEntry {
    /// Worktree filesystem path
    pub path: PathBuf,
    /// Branch checked out (None if detached)
    pub branch: Option<String>,
    /// Whether HEAD is detached
    pub is_detached: bool,
    /// Whether this is a bare repository worktree
    pub is_bare: bool,
}

impl GitRunner {
    /// Create a new runner for the given repository path.
    pub fn new(repo_path: impl Into<PathBuf>) -> Self {
        Self {
            repo_path: repo_path.into(),
        }
    }

    /// Path this runner operates on.
    pub fn repo_path(&self) -> &Path {
        &self.repo_path
    }

    /// Get the current branch name.
    ///
    /// Returns empty string if HEAD is detached.
    pub async fn current_branch(&self) -> Result<String> {
        let output = self.git(&["rev-parse", "--abbrev-ref", "HEAD"]).await?;
        let branch = output.trim().to_string();
        // rev-parse returns "HEAD" when detached
        if branch == "HEAD" {
            Ok(String::new())
        } else {
            Ok(branch)
        }
    }

    /// Get the current status (branch, dirty, ahead/behind).
    pub async fn status(&self) -> Result<GitStatus> {
        let output = self.git(&["status", "--porcelain=v2", "--branch"]).await?;
        parse_status(&output)
    }

    /// List remote names.
    pub async fn remotes(&self) -> Result<Vec<String>> {
        let output = self.git(&["remote"]).await?;
        Ok(output
            .lines()
            .map(|l| l.trim().to_string())
            .filter(|l| !l.is_empty())
            .collect())
    }

    /// Fetch from a remote (defaults to "origin").
    pub async fn fetch(&self, remote: Option<&str>) -> Result<()> {
        let remote = remote.unwrap_or("origin");
        self.git(&["fetch", remote, "--quiet"]).await?;
        Ok(())
    }

    /// List worktrees via `git worktree list --porcelain`.
    pub async fn worktree_list(&self) -> Result<Vec<WorktreeEntry>> {
        let output = self.git(&["worktree", "list", "--porcelain"]).await?;
        Ok(parse_worktree_list(&output))
    }

    /// Add a worktree at the given path for the given branch.
    pub async fn worktree_add(&self, path: &Path, branch: &str) -> Result<()> {
        self.git(&["worktree", "add", &path.to_string_lossy(), branch])
            .await?;
        Ok(())
    }

    /// Remove a worktree at the given path.
    pub async fn worktree_remove(&self, path: &Path, force: bool) -> Result<()> {
        let path_str = path.to_string_lossy();
        let mut args = vec!["worktree", "remove", &*path_str];
        if force {
            args.push("--force");
        }
        self.git(&args).await?;
        Ok(())
    }

    /// Checkout a branch.
    pub async fn checkout(&self, branch: &str) -> Result<()> {
        self.git(&["checkout", branch]).await?;
        Ok(())
    }

    /// Commit staged changes with the given message. Returns the commit hash.
    pub async fn commit(&self, message: &str) -> Result<String> {
        self.git(&["commit", "-m", message]).await?;
        let hash = self.git(&["rev-parse", "HEAD"]).await?;
        Ok(hash.trim().to_string())
    }

    /// Squash-merge from a source branch.
    pub async fn merge_squash(&self, source_branch: &str) -> Result<()> {
        self.git(&["merge", "--squash", source_branch]).await?;
        Ok(())
    }

    /// Hard-reset to a target ref.
    pub async fn reset_hard(&self, target: &str) -> Result<()> {
        self.git(&["reset", "--hard", target]).await?;
        Ok(())
    }

    // ========================================================================
    // Internal helpers
    // ========================================================================

    /// Execute a git command and return stdout on success.
    async fn git(&self, args: &[&str]) -> Result<String> {
        let output = Command::new("git")
            .args(args)
            .current_dir(&self.repo_path)
            .output()
            .await
            .map_err(|e| {
                if e.kind() == std::io::ErrorKind::NotFound {
                    ProjectError::Validation("git is not installed or not in PATH".into())
                } else {
                    ProjectError::Storage(e)
                }
            })?;

        if output.status.success() {
            Ok(String::from_utf8_lossy(&output.stdout).to_string())
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            Err(ProjectError::Validation(format!(
                "git {} failed: {}",
                args.first().unwrap_or(&""),
                stderr.trim()
            )))
        }
    }
}

// ============================================================================
// Parsers
// ============================================================================

/// Parse `git status --porcelain=v2 --branch` output.
fn parse_status(output: &str) -> Result<GitStatus> {
    let mut status = GitStatus::default();

    for line in output.lines() {
        if let Some(rest) = line.strip_prefix("# branch.head ") {
            status.branch = rest.trim().to_string();
            if status.branch == "(detached)" {
                status.branch = String::new();
            }
        } else if let Some(rest) = line.strip_prefix("# branch.ab ") {
            // Format: "+N -M"
            for part in rest.split_whitespace() {
                if let Some(ahead) = part.strip_prefix('+') {
                    status.ahead = ahead.parse().unwrap_or(0);
                } else if let Some(behind) = part.strip_prefix('-') {
                    status.behind = behind.parse().unwrap_or(0);
                }
            }
        } else if line.starts_with('1')
            || line.starts_with('2')
            || line.starts_with('u')
            || line.starts_with('?')
        {
            // Any tracked/untracked change means dirty
            status.is_dirty = true;
        }
    }

    Ok(status)
}

/// Parse `git worktree list --porcelain` output.
///
/// Porcelain format outputs blocks separated by blank lines:
/// ```text
/// worktree /path/to/main
/// HEAD abc123
/// branch refs/heads/main
///
/// worktree /path/to/feature
/// HEAD def456
/// branch refs/heads/feature
/// ```
fn parse_worktree_list(output: &str) -> Vec<WorktreeEntry> {
    let mut entries = Vec::new();
    let mut current_path: Option<PathBuf> = None;
    let mut current_branch: Option<String> = None;
    let mut is_detached = false;
    let mut is_bare = false;

    for line in output.lines() {
        if line.is_empty() {
            // End of block — flush
            if let Some(path) = current_path.take() {
                entries.push(WorktreeEntry {
                    path,
                    branch: current_branch.take(),
                    is_detached,
                    is_bare,
                });
            }
            is_detached = false;
            is_bare = false;
        } else if let Some(rest) = line.strip_prefix("worktree ") {
            current_path = Some(PathBuf::from(rest));
        } else if let Some(rest) = line.strip_prefix("branch ") {
            // Strip refs/heads/ prefix
            current_branch = Some(rest.strip_prefix("refs/heads/").unwrap_or(rest).to_string());
        } else if line == "detached" {
            is_detached = true;
        } else if line == "bare" {
            is_bare = true;
        }
    }

    // Flush last block (output may not end with blank line)
    if let Some(path) = current_path.take() {
        entries.push(WorktreeEntry {
            path,
            branch: current_branch.take(),
            is_detached,
            is_bare,
        });
    }

    entries
}

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

    #[test]
    fn test_parse_status_clean() {
        let output = "# branch.head main\n# branch.ab +0 -0\n";
        let status = parse_status(output).unwrap();
        assert_eq!(status.branch, "main");
        assert!(!status.is_dirty);
        assert_eq!(status.ahead, 0);
        assert_eq!(status.behind, 0);
    }

    #[test]
    fn test_parse_status_dirty_ahead_behind() {
        let output = "\
# branch.head feature
# branch.ab +3 -1
1 .M N... 100644 100644 100644 abc123 def456 src/main.rs
? new_file.txt
";
        let status = parse_status(output).unwrap();
        assert_eq!(status.branch, "feature");
        assert!(status.is_dirty);
        assert_eq!(status.ahead, 3);
        assert_eq!(status.behind, 1);
    }

    #[test]
    fn test_parse_status_detached() {
        let output = "# branch.head (detached)\n";
        let status = parse_status(output).unwrap();
        assert_eq!(status.branch, "");
    }

    #[test]
    fn test_parse_worktree_list() {
        let output = "\
worktree /home/user/project
HEAD abc123def456
branch refs/heads/main

worktree /home/user/project-feature
HEAD 789012345678
branch refs/heads/feature

worktree /home/user/project-detached
HEAD aabbccdd
detached

";
        let entries = parse_worktree_list(output);
        assert_eq!(entries.len(), 3);

        assert_eq!(entries[0].path, PathBuf::from("/home/user/project"));
        assert_eq!(entries[0].branch, Some("main".to_string()));
        assert!(!entries[0].is_detached);

        assert_eq!(entries[1].path, PathBuf::from("/home/user/project-feature"));
        assert_eq!(entries[1].branch, Some("feature".to_string()));
        assert!(!entries[1].is_detached);

        assert_eq!(
            entries[2].path,
            PathBuf::from("/home/user/project-detached")
        );
        assert!(entries[2].branch.is_none());
        assert!(entries[2].is_detached);
    }

    #[test]
    fn test_parse_worktree_list_no_trailing_newline() {
        let output = "worktree /repo\nHEAD abc\nbranch refs/heads/main";
        let entries = parse_worktree_list(output);
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].branch, Some("main".to_string()));
    }

    #[test]
    fn test_parse_worktree_list_bare() {
        let output = "worktree /repo.git\nHEAD abc\nbare\n\n";
        let entries = parse_worktree_list(output);
        assert_eq!(entries.len(), 1);
        assert!(entries[0].is_bare);
    }
}
