//! Git integration — context gathering, commit, push, branch management.
//!
//! Used both by the harness (auto-commit, branch-per-task) and as a tool
//! the LLM can call directly.

use std::path::Path;
use tokio::process::Command;

/// Run a git command in the working directory. Returns stdout on success.
async fn git(working_dir: &Path, args: &[&str]) -> Result<String, String> {
    let output = tokio::time::timeout(
        std::time::Duration::from_secs(30),
        Command::new("git")
            .args(args)
            .current_dir(working_dir)
            .output(),
    )
    .await
    .map_err(|_| "git command timed out".to_string())?
    .map_err(|e| format!("Failed to run git: {}", e))?;

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();

    if output.status.success() {
        Ok(stdout)
    } else {
        Err(format!("{}\n{}", stdout, stderr).trim().to_string())
    }
}

/// Check if the working directory is a git repo.
pub async fn is_git_repo(working_dir: &Path) -> bool {
    git(working_dir, &["rev-parse", "--is-inside-work-tree"])
        .await
        .is_ok()
}

/// Get the current branch name.
pub async fn current_branch(working_dir: &Path) -> Result<String, String> {
    git(working_dir, &["rev-parse", "--abbrev-ref", "HEAD"])
        .await
        .map(|s| s.trim().to_string())
}

/// Get git status (short format).
pub async fn status(working_dir: &Path) -> Result<String, String> {
    git(working_dir, &["status", "--short"]).await
}

/// Get recent commits (one-line format).
pub async fn log_oneline(working_dir: &Path, count: usize) -> Result<String, String> {
    let n = format!("-{}", count);
    git(working_dir, &["log", "--oneline", &n]).await
}

/// Get diff of staged and unstaged changes.
pub async fn diff(working_dir: &Path) -> Result<String, String> {
    // Unstaged changes
    let unstaged = git(working_dir, &["diff"]).await.unwrap_or_default();
    // Staged changes
    let staged = git(working_dir, &["diff", "--cached"]).await.unwrap_or_default();

    let mut result = String::new();
    if !staged.is_empty() {
        result.push_str("=== Staged changes ===\n");
        result.push_str(&staged);
    }
    if !unstaged.is_empty() {
        if !result.is_empty() {
            result.push('\n');
        }
        result.push_str("=== Unstaged changes ===\n");
        result.push_str(&unstaged);
    }
    if result.is_empty() {
        result.push_str("No changes");
    }
    Ok(result)
}

/// Get diff stat (summary of changes).
pub async fn diff_stat(working_dir: &Path) -> Result<String, String> {
    git(working_dir, &["diff", "--stat", "HEAD"]).await
}

/// Get blame for a file (or a range of lines).
pub async fn blame(
    working_dir: &Path,
    file: &str,
    start_line: Option<u32>,
    end_line: Option<u32>,
) -> Result<String, String> {
    let mut args = vec!["blame", "--date=short"];
    let range;
    if let (Some(start), Some(end)) = (start_line, end_line) {
        range = format!("-L {},{}", start, end);
        args.push(&range);
    }
    args.push(file);
    git(working_dir, &args).await
}

/// Stage all changes and commit.
pub async fn commit(working_dir: &Path, message: &str) -> Result<String, String> {
    // Stage all tracked changes + new files
    git(working_dir, &["add", "-A"]).await?;

    // Check if there's anything to commit
    let status = git(working_dir, &["status", "--porcelain"]).await?;
    if status.trim().is_empty() {
        return Ok("Nothing to commit".to_string());
    }

    git(working_dir, &["commit", "-m", message]).await
}

/// Push to remote.
pub async fn push(working_dir: &Path) -> Result<String, String> {
    // Try regular push first
    match git(working_dir, &["push"]).await {
        Ok(out) => Ok(out),
        Err(e) => {
            // If no upstream, set it
            if e.contains("no upstream") || e.contains("has no upstream") {
                let branch = current_branch(working_dir).await?;
                git(working_dir, &["push", "-u", "origin", &branch]).await
            } else {
                Err(e)
            }
        }
    }
}

/// Create and switch to a new branch.
pub async fn create_branch(working_dir: &Path, branch_name: &str) -> Result<String, String> {
    git(working_dir, &["checkout", "-b", branch_name]).await
}

/// Switch to an existing branch.
pub async fn checkout(working_dir: &Path, branch_name: &str) -> Result<String, String> {
    git(working_dir, &["checkout", branch_name]).await
}

/// Build a git context string for the system prompt.
/// Includes branch, status, and recent commits.
pub async fn gather_context(working_dir: &Path) -> Option<String> {
    if !is_git_repo(working_dir).await {
        return None;
    }

    let mut ctx = String::from("# Git-kontekst\n\n");

    if let Ok(branch) = current_branch(working_dir).await {
        ctx.push_str(&format!("Branch: {}\n", branch));
    }

    if let Ok(st) = status(working_dir).await {
        let st = st.trim();
        if st.is_empty() {
            ctx.push_str("Status: ren (ingen endringer)\n");
        } else {
            let line_count = st.lines().count();
            if line_count > 20 {
                // Summarize if too many changes
                ctx.push_str(&format!("Status: {} endrede filer\n", line_count));
            } else {
                ctx.push_str(&format!("Status:\n{}\n", st));
            }
        }
    }

    if let Ok(log) = log_oneline(working_dir, 10).await {
        let log = log.trim();
        if !log.is_empty() {
            ctx.push_str(&format!("\nSiste commits:\n{}\n", log));
        }
    }

    Some(ctx)
}

/// Generate a commit message based on the diff.
/// Returns a short summary suitable for a commit message.
pub async fn auto_commit_message(working_dir: &Path) -> Result<String, String> {
    let stat = match git(working_dir, &["diff", "--cached", "--stat"]).await {
        Ok(s) if !s.trim().is_empty() => s,
        _ => git(working_dir, &["diff", "--stat", "HEAD"]).await?,
    };

    if stat.trim().is_empty() {
        return Err("No changes to describe".to_string());
    }

    // Extract file list from stat
    let files: Vec<&str> = stat
        .lines()
        .filter(|l| l.contains('|'))
        .map(|l| l.split('|').next().unwrap_or("").trim())
        .collect();

    if files.is_empty() {
        return Ok("Oppdatering".to_string());
    }

    // Build a descriptive message
    if files.len() == 1 {
        Ok(format!("Oppdater {}", files[0]))
    } else if files.len() <= 5 {
        Ok(format!("Oppdater {} filer: {}", files.len(), files.join(", ")))
    } else {
        // Group by directory
        let dirs: std::collections::HashSet<&str> = files
            .iter()
            .filter_map(|f| f.rsplit_once('/').map(|(d, _)| d))
            .collect();
        if dirs.len() <= 3 {
            let dirs_str: Vec<&&str> = dirs.iter().collect();
            Ok(format!(
                "Oppdater {} filer i {}",
                files.len(),
                dirs_str.iter().map(|d| **d).collect::<Vec<_>>().join(", ")
            ))
        } else {
            Ok(format!("Oppdater {} filer", files.len()))
        }
    }
}

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

    #[tokio::test]
    async fn test_is_git_repo() {
        // The synops repo itself should be a git repo
        let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
        assert!(is_git_repo(&repo).await);
    }

    #[tokio::test]
    async fn test_current_branch() {
        let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
        let branch = current_branch(&repo).await;
        assert!(branch.is_ok());
        assert!(!branch.unwrap().is_empty());
    }

    #[tokio::test]
    async fn test_gather_context() {
        let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
        let ctx = gather_context(&repo).await;
        assert!(ctx.is_some());
        let ctx = ctx.unwrap();
        assert!(ctx.contains("Branch:"));
        assert!(ctx.contains("Siste commits:"));
    }

    #[tokio::test]
    async fn test_not_git_repo() {
        assert!(!is_git_repo(Path::new("/tmp")).await);
    }
}
