use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;

use anyhow::{Context, Result};
use tokio::process::Command;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteRef {
    pub sha: String,
    pub name: String,
}

pub fn parse_ls_remote(output: &str) -> Vec<RemoteRef> {
    output
        .lines()
        .filter_map(|line| {
            let (sha, refname) = line.split_once('\t')?;
            Some(RemoteRef {
                sha: sha.trim().to_string(),
                name: refname.trim().to_string(),
            })
        })
        .collect()
}

pub fn refs_by_branch(refs: &[RemoteRef]) -> HashMap<&str, &str> {
    refs.iter()
        .filter_map(|r| {
            r.name
                .strip_prefix("refs/heads/")
                .map(|branch| (branch, r.sha.as_str()))
        })
        .collect()
}

pub async fn ls_remote(dir: &Path) -> Result<Vec<RemoteRef>> {
    let output = Command::new("git")
        .args(["ls-remote", "origin"])
        .current_dir(dir)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git ls-remote")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git ls-remote failed: {}", stderr.trim());
    }

    let stdout = String::from_utf8(output.stdout)?;
    Ok(parse_ls_remote(&stdout))
}

pub async fn fetch_branch(dir: &Path, branch: &str) -> Result<()> {
    let output = Command::new("git")
        .args(["fetch", "origin", branch])
        .current_dir(dir)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git fetch")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git fetch origin {} failed: {}", branch, stderr.trim());
    }
    Ok(())
}

pub async fn reset_to_remote(dir: &Path, branch: &str) -> Result<()> {
    let status = Command::new("git")
        .args(["checkout", branch])
        .current_dir(dir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .output()
        .await
        .context("failed to run git checkout")?;

    if !status.status.success() {
        // Branch might not exist locally yet
        let output = Command::new("git")
            .args(["checkout", "-B", branch, &format!("origin/{}", branch)])
            .current_dir(dir)
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .output()
            .await
            .context("failed to create local branch")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("git checkout -b {} failed: {}", branch, stderr.trim());
        }
    }

    let output = Command::new("git")
        .args(["reset", "--hard", &format!("origin/{}", branch)])
        .current_dir(dir)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git reset")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git reset --hard origin/{} failed: {}", branch, stderr.trim());
    }
    Ok(())
}

pub async fn git_root(dir: &Path) -> Result<PathBuf> {
    let output = Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(dir)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git rev-parse")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git rev-parse failed: {}", stderr.trim());
    }

    let stdout = String::from_utf8(output.stdout)?;
    Ok(PathBuf::from(stdout.trim()))
}

pub async fn prune_worktrees(repo_dir: &Path) -> Result<()> {
    let output = Command::new("git")
        .args(["worktree", "prune"])
        .current_dir(repo_dir)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git worktree prune")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git worktree prune failed: {}", stderr.trim());
    }
    Ok(())
}

pub async fn add_worktree(repo_dir: &Path, path: &Path, branch: &str) -> Result<()> {
    let output = Command::new("git")
        .args([
            "worktree",
            "add",
            "--force",
            &path.to_string_lossy(),
            branch,
        ])
        .current_dir(repo_dir)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git worktree add")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git worktree add {} failed: {}", branch, stderr.trim());
    }
    Ok(())
}

pub async fn commit_subject(dir: &Path, sha: &str) -> Result<String> {
    let output = Command::new("git")
        .args(["log", "-1", "--format=%s", sha])
        .current_dir(dir)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .context("failed to run git log")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git log {} failed: {}", sha, stderr.trim());
    }

    let stdout = String::from_utf8(output.stdout)?;
    Ok(stdout.trim().to_string())
}

