"""Git 操作封装：仓库检测、staged 文件、diff 获取、智能截断、commit 执行。"""

from __future__ import annotations

import subprocess
from dataclasses import dataclass


class GitError(Exception):
    """Git 操作相关错误。"""


# ---------------------------------------------------------------------------
# 数据类
# ---------------------------------------------------------------------------

@dataclass
class FileChange:
    """单个文件的变更信息。"""
    path: str
    status: str          # A/M/D/R 等
    insertions: int = 0
    deletions: int = 0


@dataclass
class DiffResult:
    """diff 结果。"""
    files: list[FileChange]
    diff_content: str     # 完整或截断后的 diff
    stat_summary: str     # git diff --stat 的输出
    truncated: bool       # 是否被截断


# ---------------------------------------------------------------------------
# 底层 Git 命令执行
# ---------------------------------------------------------------------------

def _run_git(*args: str, cwd: str | None = None) -> str:
    """执行 git 命令并返回 stdout。失败时抛出 GitError。"""
    cmd = ["git"] + list(args)
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            encoding="utf-8",
            errors="replace",
            cwd=cwd,
            timeout=30,
        )
    except FileNotFoundError:
        raise GitError("未找到 git 命令，请确保已安装 Git")
    except subprocess.TimeoutExpired:
        raise GitError(f"git 命令超时: {' '.join(cmd)}")

    if result.returncode != 0:
        stderr = result.stderr.strip()
        raise GitError(f"git 命令执行失败: {stderr}")

    return result.stdout


# ---------------------------------------------------------------------------
# 公开 API
# ---------------------------------------------------------------------------

def is_git_repo(cwd: str | None = None) -> bool:
    """检测当前目录是否为 Git 仓库。"""
    try:
        _run_git("rev-parse", "--is-inside-work-tree", cwd=cwd)
        return True
    except GitError:
        return False


def has_unstaged_changes(cwd: str | None = None) -> bool:
    """检测工作区是否有未暂存的修改。"""
    output = _run_git("diff", "--name-only", cwd=cwd)
    return bool(output.strip())


def has_untracked_files(cwd: str | None = None) -> bool:
    """检测是否有未跟踪的新文件。"""
    output = _run_git("ls-files", "--others", "--exclude-standard", cwd=cwd)
    return bool(output.strip())


def add_all(cwd: str | None = None) -> str:
    """执行 git add -A，将所有变更添加到暂存区。"""
    return _run_git("add", "-A", cwd=cwd)


def get_staged_files(cwd: str | None = None) -> list[FileChange]:
    """获取暂存区的文件列表及变更状态。"""
    # --name-status 输出格式: "M\tfile.py"
    output = _run_git("diff", "--cached", "--name-status", cwd=cwd)
    files = []
    for line in output.strip().splitlines():
        if not line.strip():
            continue
        parts = line.split("\t", 1)
        if len(parts) == 2:
            status, path = parts
            files.append(FileChange(path=path, status=status))
    return files


def get_staged_diff(cwd: str | None = None) -> str:
    """获取暂存区的完整 diff 内容。"""
    return _run_git("diff", "--cached", cwd=cwd)


def get_staged_stat(cwd: str | None = None) -> str:
    """获取暂存区的 stat 摘要。"""
    return _run_git("diff", "--cached", "--stat", cwd=cwd)


def get_staged_numstat(cwd: str | None = None) -> list[FileChange]:
    """获取暂存区每个文件的增删行数。"""
    output = _run_git("diff", "--cached", "--numstat", cwd=cwd)
    files = []
    for line in output.strip().splitlines():
        if not line.strip():
            continue
        parts = line.split("\t")
        if len(parts) == 3:
            ins_str, del_str, path = parts
            ins = int(ins_str) if ins_str != "-" else 0
            dels = int(del_str) if del_str != "-" else 0
            files.append(FileChange(path=path, status="M", insertions=ins, deletions=dels))
    return files


def build_diff_result(
    max_diff_lines: int = 500,
    max_diff_chars: int = 15000,
    cwd: str | None = None,
) -> DiffResult:
    """构建 diff 结果，超过阈值时进行智能截断。

    同时按行数和字符数两个维度截断，取更严格的限制，
    避免因长行导致 prompt 超出模型输入上限。

    Args:
        max_diff_lines: diff 行数阈值，超过则截断。
        max_diff_chars: diff 字符数阈值，超过则截断。
        cwd: 工作目录。

    Returns:
        DiffResult 包含文件列表、diff 内容、stat 摘要、是否截断。
    """
    files = get_staged_files(cwd=cwd)
    stat_summary = get_staged_stat(cwd=cwd)
    full_diff = get_staged_diff(cwd=cwd)

    # 合并增删行数信息
    numstat_files = get_staged_numstat(cwd=cwd)
    numstat_map = {f.path: f for f in numstat_files}
    for f in files:
        if f.path in numstat_map:
            f.insertions = numstat_map[f.path].insertions
            f.deletions = numstat_map[f.path].deletions

    diff_lines = full_diff.splitlines()
    truncated_by_lines = len(diff_lines) > max_diff_lines

    if truncated_by_lines:
        diff_lines = diff_lines[:max_diff_lines]

    # 按字符数截断：逐行累加，超出预算时停止
    char_count = 0
    kept_lines = []
    truncated_by_chars = False
    for line in diff_lines:
        if char_count + len(line) + 1 > max_diff_chars:  # +1 for newline
            truncated_by_chars = True
            break
        kept_lines.append(line)
        char_count += len(line) + 1

    truncated = truncated_by_lines or truncated_by_chars

    if truncated:
        truncated_diff = "\n".join(kept_lines)
        diff_content = (
            f"{truncated_diff}\n\n"
            f"... (diff 内容过长，已截断，原始共 {len(full_diff.splitlines())} 行 / "
            f"{len(full_diff)} 字符) ...\n\n"
            f"完整文件变更摘要:\n{stat_summary}"
        )
    else:
        diff_content = full_diff

    return DiffResult(
        files=files,
        diff_content=diff_content,
        stat_summary=stat_summary,
        truncated=truncated,
    )


def commit(message: str, cwd: str | None = None) -> str:
    """执行 git commit。返回 git 输出。"""
    return _run_git("commit", "-m", message, cwd=cwd)


def commit_files(files: list[str], message: str, cwd: str | None = None) -> str:
    """对指定文件执行 git commit（用于智能拆分提交）。

    先 reset HEAD 取消全部暂存，再 add 指定文件，最后 commit。
    """
    # 先取消所有暂存
    _run_git("reset", "HEAD", cwd=cwd)
    # 暂存指定文件
    _run_git("add", "--", *files, cwd=cwd)
    # 提交
    result = _run_git("commit", "-m", message, cwd=cwd)
    return result
