use anyhow::{bail, Result};
use std::process::Command;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;

/// Wrapper for invoking the git CLI (network/auth paths).
pub struct GitCli;

impl GitCli {
    pub fn new() -> Self {
        Self
    }

    /// Get the current branch name via `git rev-parse --abbrev-ref HEAD`.
    pub fn current_branch(&self, repo_path: &str) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "rev-parse", "--abbrev-ref", "HEAD"])
            .output()?;

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

        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    }

    /// Stage a file via `git add <file>`.
    pub fn stage_file(&self, repo_path: &str, file: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "add", "--", file])
            .output()?;

        if !output.status.success() {
            bail!(
                "git add failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(())
    }

    /// Unstage a file via `git restore --staged <file>`.
    pub fn unstage_file(&self, repo_path: &str, file: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "restore", "--staged", "--", file])
            .output()?;

        if !output.status.success() {
            bail!(
                "git restore --staged failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }

        Ok(())
    }

    /// Show diff for a single path with configurable context.
    pub fn diff(
        &self,
        repo_path: &str,
        path: &str,
        staged: bool,
        context: usize,
    ) -> Result<String> {
        // Check if file is untracked (new file)
        // For untracked files, use --no-index to show full file as new
        let status_output = Command::new("git")
            .args(["-C", repo_path, "status", "--porcelain=v2", "--", path])
            .output()?;
        
        let is_untracked = if status_output.status.success() {
            let status_str = String::from_utf8_lossy(&status_output.stdout);
            // Untracked files start with "? " in porcelain v2
            status_str.trim().starts_with("? ")
        } else {
            false
        };
        
        if is_untracked {
            // For untracked files, show full file content as a new file diff
            let file_path = Path::new(repo_path).join(path);
            if !file_path.exists() {
                return Ok(String::new());
            }
            
            let mut cmd = Command::new("git");
            cmd.args(["-C", repo_path, "diff", "--no-index"]);
            let ctx = format!("--unified={}", context);
            cmd.arg(ctx);
            cmd.arg("/dev/null");
            cmd.arg(&file_path);
            
            let output = cmd.output()?;
            // --no-index returns exit code 1 for new files (which is normal)
            // We still want to capture the output
            if output.status.code() == Some(1) || output.status.success() {
                Ok(String::from_utf8_lossy(&output.stdout).into_owned())
            } else {
                bail!(
                    "git diff --no-index failed (exit {}): {}",
                    output.status,
                    String::from_utf8_lossy(&output.stderr)
                );
            }
        } else {
            // For tracked files, use normal diff
            let mut cmd = Command::new("git");
            cmd.args(["-C", repo_path, "diff"]);
            if staged {
                cmd.arg("--cached");
            }
            let ctx = format!("--unified={}", context);
            cmd.arg(ctx);
            cmd.arg("--");
            cmd.arg(path);

            let output = cmd.output()?;
            if !output.status.success() {
                bail!(
                    "git diff failed (exit {}): {}",
                    output.status,
                    String::from_utf8_lossy(&output.stderr)
                );
            }

            Ok(String::from_utf8_lossy(&output.stdout).into_owned())
        }
    }

    /// Get last commit information (for amend operations)
    pub fn last_commit_info(&self, repo_path: &str) -> Result<crate::app::state::LastCommitInfo> {
        // Get author name, email, message, date, and hash
        // Use null separator to handle multi-line commit messages
        let output = Command::new("git")
            .args([
                "-C", repo_path,
                "log", "-1", "--format=%an%n%ae%n%ai%n%H%n%h%n%s%n%b"
            ])
            .output()?;
        
        if !output.status.success() {
            bail!(
                "git log failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        
        let text = String::from_utf8_lossy(&output.stdout);
        let lines: Vec<&str> = text.lines().collect();
        
        if lines.len() < 6 {
            bail!("Invalid commit info format");
        }
        
        let author_name = lines[0].to_string();
        let author_email = lines[1].to_string();
        let date = lines[2].to_string();
        let hash = lines[3].to_string();
        let short_hash = lines[4].to_string();
        let subject = lines.get(5).unwrap_or(&"").to_string();
        let body = if lines.len() > 6 {
            lines[6..].join("\n").trim().to_string()
        } else {
            String::new()
        };
        
        // Combine subject and body for full message
        let message = if body.is_empty() {
            subject
        } else {
            format!("{}\n\n{}", subject, body)
        };
        
        Ok(crate::app::state::LastCommitInfo {
            author_name,
            author_email,
            message,
            date,
            hash,
            short_hash,
        })
    }

    /// Commit staged changes with a message; set `amend` to amend last commit.
    /// If `author_name` and `author_email` are provided, use `--author` flag.
    pub fn commit(&self, repo_path: &str, message: &str, amend: bool, author_name: Option<&str>, author_email: Option<&str>) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "commit", "-m", message]);
        if amend {
            cmd.arg("--amend");
        }
        // Add --author if both name and email are provided
        if let (Some(name), Some(email)) = (author_name, author_email) {
            if !name.is_empty() && !email.is_empty() {
                cmd.arg("--author");
                cmd.arg(format!("{} <{}>", name, email));
            }
        }
        let output = cmd.output()?;
        if !output.status.success() {
            bail!(
                "git commit failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Push current branch. If `force_with_lease` true, use safer force push.
    pub fn push(&self, repo_path: &str, force_with_lease: bool) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "push"]);
        if force_with_lease {
            cmd.arg("--force-with-lease");
        }
        let output = cmd.output()?;
        if !output.status.success() {
            bail!(
                "git push failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Cherry-pick a commit hash.
    pub fn cherry_pick(&self, repo_path: &str, hash: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "cherry-pick", hash])
            .output()?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            // Extract the main error message (first line) for better readability
            let error_msg = stderr
                .lines()
                .find(|line| line.starts_with("error:"))
                .map(|line| line.trim_start_matches("error:").trim())
                .unwrap_or_else(|| stderr.trim());
            
            // Check if it's the common "local changes would be overwritten" error
            if stderr.contains("your local changes would be overwritten") {
                bail!(
                    "Cannot cherry-pick: you have uncommitted changes that would be overwritten. Commit or stash your changes first."
                );
            }
            
            bail!(
                "git cherry-pick failed (exit {}): {}",
                output.status,
                error_msg
            );
        }
        Ok(())
    }

    /// Revert a commit hash.
    pub fn revert(&self, repo_path: &str, hash: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "revert", hash])
            .output()?;
        if !output.status.success() {
            bail!(
                "git revert failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Continue an in-progress rebase with optional supplied message/author (editorless reword).
    pub fn rebase_continue(
        &self,
        repo_path: &str,
        message: Option<&str>,
        author_name: Option<&str>,
        author_email: Option<&str>,
    ) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "rebase", "--continue"]);

        // Author overrides via environment.
        if let Some(name) = author_name {
            cmd.env("GIT_AUTHOR_NAME", name);
            cmd.env("GIT_COMMITTER_NAME", name);
        }
        if let Some(email) = author_email {
            cmd.env("GIT_AUTHOR_EMAIL", email);
            cmd.env("GIT_COMMITTER_EMAIL", email);
        }

        if let Some(msg) = message {
            // Write message to a temp file and point GIT_EDITOR to cat it into the buffer.
            let mut tempfile = NamedTempFile::new()?;
            use std::io::Write;
            tempfile.write_all(msg.as_bytes())?;
            let path = tempfile.path().to_string_lossy().to_string();
            let editor_cmd = format!("cat {} > ", path);
            cmd.env("GIT_EDITOR", editor_cmd);
        }

        let output = cmd.output()?;
        if !output.status.success() {
            bail!(
                "git rebase --continue failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Abort an in-progress rebase.
    pub fn rebase_abort(&self, repo_path: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "rebase", "--abort"])
            .output()?;
        if !output.status.success() {
            bail!(
                "git rebase --abort failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Skip current step in an in-progress rebase.
    pub fn rebase_skip(&self, repo_path: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "rebase", "--skip"])
            .output()?;
        if !output.status.success() {
            bail!(
                "git rebase --skip failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Reset to a target with a mode (soft/mixed/hard).
    pub fn reset(&self, repo_path: &str, mode: &str, target: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "reset", mode, target])
            .output()?;
        if !output.status.success() {
            bail!(
                "git reset {} {} failed (exit {}): {}",
                mode,
                target,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Fetch remote refs without updating working tree (dry-run).
    pub fn fetch_dry_run(&self, repo_path: &str, remote: &str) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "fetch", "--dry-run", remote])
            .output()?;
        if !output.status.success() {
            bail!(
                "git fetch --dry-run failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
    }

    /// Get ahead/behind against a remote branch.
    pub fn ahead_behind_remote(&self, repo_path: &str, branch: &str) -> Result<(i32, i32)> {
        let rev = format!("origin/{}", branch);
        let output = Command::new("git")
            .args(["-C", repo_path, "rev-list", "--left-right", "--count", &format!("{}...{}", rev, branch)])
            .output()?;
        if !output.status.success() {
            bail!(
                "git rev-list --left-right --count failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        let text = String::from_utf8_lossy(&output.stdout);
        let parts: Vec<&str> = text.trim().split_whitespace().collect();
        if parts.len() >= 2 {
            let behind = parts[0].parse::<i32>().unwrap_or(0);
            let ahead = parts[1].parse::<i32>().unwrap_or(0);
            Ok((ahead, behind))
        } else {
            bail!("unexpected rev-list output: {}", text);
        }
    }

    /// List merged local branches relative to base (excludes base, main, master, current).
    pub fn merged_local_branches(&self, repo_path: &str, base: &str) -> Result<Vec<String>> {
        let output = Command::new("git")
            .args(["-C", repo_path, "branch", "--merged", base])
            .output()?;
        if !output.status.success() {
            bail!(
                "git branch --merged failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        let current = self.current_branch(repo_path).unwrap_or_default();
        let mut branches = Vec::new();
        for line in String::from_utf8_lossy(&output.stdout).lines() {
            let name = line.trim().trim_start_matches('*').trim().to_string();
            if name.is_empty() {
                continue;
            }
            if name == base || name == "main" || name == "master" || name == current {
                continue;
            }
            branches.push(name);
        }
        Ok(branches)
    }

    /// Delete a local branch (safe; no force).
    pub fn delete_local_branch(&self, repo_path: &str, branch: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "branch", "-d", branch])
            .output()?;
        if !output.status.success() {
            bail!(
                "git branch -d {} failed (exit {}): {}",
                branch,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Remote prune stale tracking branches.
    pub fn remote_prune(&self, repo_path: &str, remote: &str) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "remote", "prune", remote])
            .output()?;
        if !output.status.success() {
            bail!(
                "git remote prune {} failed (exit {}): {}",
                remote,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
    }

    /// Get remote URL.
    pub fn remote_url(&self, repo_path: &str, remote: &str) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "remote", "get-url", remote])
            .output()?;
        if !output.status.success() {
            bail!(
                "git remote get-url {} failed (exit {}): {}",
                remote,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    }

    /// Set remote URL.
    pub fn set_remote_url(&self, repo_path: &str, remote: &str, url: &str) -> Result<()> {
        let output = Command::new("git")
            .args(["-C", repo_path, "remote", "set-url", remote, url])
            .output()?;
        if !output.status.success() {
            bail!(
                "git remote set-url {} failed (exit {}): {}",
                remote,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Log brief (oneline, decorate, graph, relative date).
    pub fn log_brief(&self, repo_path: &str, limit: usize) -> Result<String> {
        let output = Command::new("git")
            .args([
                "-C",
                repo_path,
                "log",
                "--oneline",
                "--decorate",
                "--graph",
                "--date=relative",
                &format!("-{}", limit),
            ])
            .output()?;
        if !output.status.success() {
            bail!(
                "git log brief failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
    }

    /// Log with stats.
    pub fn log_stats(&self, repo_path: &str, limit: usize) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "log", "--stat", &format!("-{}", limit)])
            .output()?;
        if !output.status.success() {
            bail!(
                "git log --stat failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
    }

    fn run_with_timeout(mut cmd: Command, timeout_secs: u64) -> Result<std::process::Output> {
        use std::io::Read;
        use std::thread::sleep;
        use std::time::{Duration, Instant};
        let start = Instant::now();
        let mut child = cmd
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()?;
        loop {
            if let Some(status) = child.try_wait()? {
                let mut stdout = Vec::new();
                let mut stderr = Vec::new();
                if let Some(mut out) = child.stdout.take() {
                    let _ = out.read_to_end(&mut stdout);
                }
                if let Some(mut err) = child.stderr.take() {
                    let _ = err.read_to_end(&mut stderr);
                }
                return Ok(std::process::Output { status, stdout, stderr });
            }
            if start.elapsed() > Duration::from_secs(timeout_secs) {
                let _ = child.kill();
                let _ = child.wait();
                bail!("git command timed out after {}s", timeout_secs);
            }
            sleep(Duration::from_millis(200));
        }
    }

    /// Pull with ff-only (default true) with timeout safeguard.
    pub fn pull_ff_only(&self, repo_path: &str, ff_only: bool, timeout_secs: u64) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "pull"]);
        if ff_only {
            cmd.arg("--ff-only");
        }
        let output = Self::run_with_timeout(cmd, timeout_secs)?;
        if !output.status.success() {
            bail!(
                "git pull failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Pull with rebase and optional autostash with timeout safeguard.
    pub fn pull_rebase(&self, repo_path: &str, autostash: bool, timeout_secs: u64) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "pull", "--rebase"]);
        if autostash {
            cmd.arg("--autostash");
        }
        let output = Self::run_with_timeout(cmd, timeout_secs)?;
        if !output.status.success() {
            bail!(
                "git pull --rebase failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Apply a patch to index (staged) or working tree. If reverse=true, applies in reverse.
    pub fn apply_patch(&self, repo_path: &str, patch: &str, to_index: bool, reverse: bool) -> Result<()> {
        let mut cmd = Command::new("git");
        cmd.args(["-C", repo_path, "apply"]);
        if to_index {
            cmd.arg("--cached");
        }
        if reverse {
            cmd.arg("--reverse");
        }
        use std::io::Write;
        use std::process::Stdio;
        let mut child = cmd
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;
        if let Some(mut stdin) = child.stdin.take() {
            stdin.write_all(patch.as_bytes())?;
        }
        let output = child.wait_with_output()?;
        if !output.status.success() {
            bail!(
                "git apply failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Read interactive rebase todo file (if present). Returns lines and path used.
    pub fn read_rebase_todo(&self, repo_path: &str) -> Result<(Vec<String>, String)> {
        let candidates = [
            PathBuf::from(repo_path).join(".git").join("rebase-merge").join("git-rebase-todo"),
            PathBuf::from(repo_path).join(".git").join("rebase-apply").join("git-rebase-todo"),
        ];
        for path in candidates.iter() {
            if path.exists() {
                let content = fs::read_to_string(path)?;
                let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
                return Ok((lines, path.to_string_lossy().to_string()));
            }
        }
        bail!("no rebase todo found");
    }

    /// Start interactive rebase against a base (upstream) commit. Returns the todo path.
    ///
    /// We force the sequence editor to a helper that exits non-zero after dumping
    /// the todo. Git stops immediately, leaving git-rebase-todo for us to rewrite.
    /// If the todo exists, we treat it as success regardless of exit status.
    pub fn start_rebase_interactive(&self, repo_path: &str, base: &str) -> Result<String> {
        let status = Command::new("git")
            .env("GIT_SEQUENCE_EDITOR", "sh -c 'cat \"$1\"; exit 1' --")
            .args(["-C", repo_path, "rebase", "-i", base])
            .status()
            .map_err(|e| anyhow::anyhow!("failed to spawn git rebase -i: {e}"))?;

        let candidates = [
            PathBuf::from(repo_path).join(".git").join("rebase-merge").join("git-rebase-todo"),
            PathBuf::from(repo_path).join(".git").join("rebase-apply").join("git-rebase-todo"),
        ];
        for path in candidates.iter() {
            if path.exists() {
                return Ok(path.to_string_lossy().to_string());
            }
        }

        bail!(
            "git rebase -i {} did not produce git-rebase-todo (exit {})",
            base,
            status
        );
    }

    /// Start interactive rebase from root.
    pub fn start_rebase_interactive_root(&self, repo_path: &str) -> Result<String> {
        let status = Command::new("git")
            .env("GIT_SEQUENCE_EDITOR", "sh -c 'cat \"$1\"; exit 1' --")
            .args(["-C", repo_path, "rebase", "-i", "--root"])
            .status()
            .map_err(|e| anyhow::anyhow!("failed to spawn git rebase -i --root: {e}"))?;

        let candidates = [
            PathBuf::from(repo_path).join(".git").join("rebase-merge").join("git-rebase-todo"),
            PathBuf::from(repo_path).join(".git").join("rebase-apply").join("git-rebase-todo"),
        ];
        for path in candidates.iter() {
            if path.exists() {
                return Ok(path.to_string_lossy().to_string());
            }
        }

        bail!(
            "git rebase -i --root did not produce git-rebase-todo (exit {})",
            status
        );
    }

    /// Return the root commit (first commit hash).
    pub fn root_commit(&self, repo_path: &str) -> Result<String> {
        let output = Command::new("git")
            .args(["-C", repo_path, "rev-list", "--max-parents=0", "HEAD"])
            .output()?;
        if !output.status.success() {
            bail!(
                "git rev-list failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(String::from_utf8_lossy(&output.stdout)
            .lines()
            .next()
            .unwrap_or("")
            .to_string())
    }

    /// List PRs from a GitHub-style remote via ls-remote pull/*/head.
    pub fn list_pull_requests(&self, repo_path: &str, remote: &str) -> Result<Vec<(String, String)>> {
        let output = Command::new("git")
            .args(["-C", repo_path, "ls-remote", "--refs", remote, "pull/*/head"])
            .output()?;
        if !output.status.success() {
            bail!(
                "git ls-remote failed (exit {}): {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        let mut prs = Vec::new();
        for line in String::from_utf8_lossy(&output.stdout).lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2 {
                if let Some(refname) = parts.get(1) {
                    if let Some(num) = refname.trim().split('/').nth(2) {
                        prs.push((num.to_string(), refname.to_string()));
                    }
                }
            }
        }
        Ok(prs)
    }

    /// Fetch PR head into local branch refs/pr/<num>.
    pub fn fetch_pr(&self, repo_path: &str, remote: &str, pr: &str) -> Result<()> {
        let target = format!("pull/{}/head:pr/{}", pr, pr);
        let output = Command::new("git")
            .args(["-C", repo_path, "fetch", remote, &target])
            .output()?;
        if !output.status.success() {
            bail!(
                "git fetch {} failed (exit {}): {}",
                target,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }

    /// Checkout local PR branch refs/pr/<num>.
    pub fn checkout_pr_branch(&self, repo_path: &str, pr: &str) -> Result<()> {
        let branch = format!("pr/{}", pr);
        let output = Command::new("git")
            .args(["-C", repo_path, "checkout", &branch])
            .output()?;
        if !output.status.success() {
            bail!(
                "git checkout {} failed (exit {}): {}",
                branch,
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        Ok(())
    }
}