# -*- coding: utf-8 -*-
"""CommitAttributionService Git 提交归因服务（Phase 7 Tier 1 BF）。

借鉴 zhikuncode-main CommitAttributionService.java：
- git blame 解析（行级作者 + 时间）
- git log 解析（最近 N 条提交，倒序）
- git shortlog 解析（按作者统计提交数）
- 提交摘要（commit + author + date + message 首行）

CLI 集成场景：
- `/git blame <file>` 命令展示行级归因表格
- `/git log [N]` 命令展示最近 N 条提交
- `/git stats` 命令展示作者贡献统计
- 自纠错循环中可调用 blame 判断"是谁修改的"

实现要点：
- 全部基于 subprocess 调用 git，无第三方依赖
- 失败一律返回空集合，不抛异常（CLI 容错）
- 超时 5 秒，避免大仓库卡死
"""
from __future__ import annotations

import subprocess
import threading
from dataclasses import dataclass, field
from typing import List, Optional

__all__ = [
    "CommitAttributionService",
    "BlameLine",
    "CommitInfo",
    "AuthorStats",
    "get_default_service",
]


@dataclass
class BlameLine:
    """git blame 单行结果。"""

    line_number: int
    commit_hash: str
    author: str
    author_date: str  # ISO 格式
    content: str = ""


@dataclass
class CommitInfo:
    """git log 单条提交。"""

    hash: str
    author: str
    email: str
    date: str  # ISO 格式
    message: str  # 提交消息首行


@dataclass
class AuthorStats:
    """作者提交统计。"""

    author: str
    email: str
    commit_count: int


class CommitAttributionService:
    """Git 提交归因服务。

    所有方法在非 git 仓库 / git 命令失败时返回空集合，不抛异常。
    """

    GIT_TIMEOUT = 5  # 秒

    def __init__(self, working_dir: str = ".") -> None:
        self._working_dir = working_dir
        self._lock = threading.Lock()

    def set_working_dir(self, path: str) -> None:
        with self._lock:
            self._working_dir = path

    # ------------------------------------------------------------------ public API

    def is_git_repo(self) -> bool:
        """检测工作目录是否为 git 仓库。"""
        try:
            result = self._run_git(["rev-parse", "--is-inside-work-tree"])
            return result.returncode == 0 and (result.stdout or "").strip() == "true"
        except (subprocess.SubprocessError, OSError):
            return False

    def blame(
        self,
        file_path: str,
        start: Optional[int] = None,
        end: Optional[int] = None,
    ) -> List[BlameLine]:
        """解析 git blame。

        Args:
            file_path: 相对工作目录的文件路径
            start: 起始行号（含，1-based）
            end: 结束行号（含，1-based）

        Returns:
            每行的归因信息列表；非 git 仓库或文件不存在返回 []
        """
        if not self.is_git_repo():
            return []

        args = ["blame", "--line-porcelain"]
        if start is not None and end is not None:
            args.extend(["-L", f"{start},{end}"])
        elif start is not None:
            args.extend(["-L", f"{start},+1"])
        args.append("--")
        args.append(file_path)

        try:
            result = self._run_git(args)
        except (subprocess.SubprocessError, OSError):
            return []
        if result.returncode != 0:
            return []

        return self._parse_blame_porcelain(result.stdout or "")

    def log(self, limit: int = 10) -> List[CommitInfo]:
        """解析 git log，返回最近 N 条提交（倒序）。"""
        if not self.is_git_repo():
            return []
        if limit <= 0:
            return []

        # 自定义分隔符便于解析
        sep = "\x1e"  # record separator
        fmt = sep.join(["%H", "%an", "%ae", "%aI", "%s"])
        args = ["log", f"--pretty=format:{fmt}", f"-n{limit}"]

        try:
            result = self._run_git(args)
        except (subprocess.SubprocessError, OSError):
            return []
        if result.returncode != 0:
            return []

        commits = []
        # 防护：解码失败时 stdout 可能为 None
        stdout = result.stdout or ""
        # 注意：不要用 str.splitlines()，因为它会把 \x1e (RS) 当作行分隔符
        for line in stdout.strip().split("\n"):
            if not line.strip():
                continue
            parts = line.split(sep)
            if len(parts) != 5:
                continue
            commits.append(CommitInfo(
                hash=parts[0],
                author=parts[1],
                email=parts[2],
                date=parts[3],
                message=parts[4],
            ))
        return commits

    def author_stats(self) -> List[AuthorStats]:
        """按作者分组统计提交数（git shortlog -sne）。"""
        if not self.is_git_repo():
            return []

        try:
            result = self._run_git(["shortlog", "-sne", "--all"])
        except (subprocess.SubprocessError, OSError):
            return []
        if result.returncode != 0:
            return []

        stats = []
        stdout = result.stdout or ""
        for line in stdout.strip().splitlines():
            line = line.strip()
            if not line:
                continue
            # 格式："<count>\t<name> <<email>>"
            try:
                # 拆分 count 和剩余部分
                count_str, rest = line.split("\t", 1) if "\t" in line else line.split(None, 1)
                count = int(count_str.strip())
                # 拆分 name 和 email
                if "<" in rest and ">" in rest:
                    name = rest[:rest.rindex("<")].strip()
                    email = rest[rest.rindex("<") + 1:rest.rindex(">")]
                else:
                    name = rest.strip()
                    email = ""
                stats.append(AuthorStats(author=name, email=email, commit_count=count))
            except (ValueError, IndexError):
                continue
        return stats

    def commit_summary(self, commit_hash: str) -> Optional[CommitInfo]:
        """获取指定 commit 的摘要信息。"""
        if not self.is_git_repo():
            return None

        sep = "\x1e"
        fmt = sep.join(["%H", "%an", "%ae", "%aI", "%s"])
        try:
            result = self._run_git(["show", "-s", f"--pretty=format:{fmt}", commit_hash])
        except (subprocess.SubprocessError, OSError):
            return None
        if result.returncode != 0:
            return None

        line = (result.stdout or "").strip()
        if not line:
            return None
        parts = line.split(sep)
        if len(parts) != 5:
            return None
        return CommitInfo(
            hash=parts[0],
            author=parts[1],
            email=parts[2],
            date=parts[3],
            message=parts[4],
        )

    # ------------------------------------------------------------------ 内部方法

    def _run_git(self, args: List[str]) -> subprocess.CompletedProcess:
        """执行 git 命令。

        关键：必须指定 encoding="utf-8"，否则 Windows 默认用 GBK 解码，
        遇到中文提交消息中的非 GBK 字符会抛 UnicodeDecodeError，
        且 result.stdout 会变成 None（subprocess 内部吞掉异常）。
        """
        return subprocess.run(
            ["git", *args],
            cwd=self._working_dir,
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=self.GIT_TIMEOUT,
        )

    @staticmethod
    def _parse_blame_porcelain(output: str) -> List[BlameLine]:
        """解析 git blame --line-porcelain 输出。

        格式：
            <sha> <orig-line> <final-line> [<content>]
            author <name>
            author-mail <email>
            author-time <unix-ts>
            summary <commit-subject>
            <tab>content

        每行内容前缀 <tab>。
        """
        lines = output.splitlines()
        result: List[BlameLine] = []
        current: dict = {}
        line_number = 0

        for line in lines:
            if not line:
                continue
            if line.startswith("\t"):
                # 行内容
                if current.get("hash"):
                    line_number += 1
                    result.append(BlameLine(
                        line_number=line_number,
                        commit_hash=current.get("hash", ""),
                        author=current.get("author", ""),
                        author_date=current.get("date", ""),
                        content=line[1:],  # 去掉 tab
                    ))
                current = {}
            elif line.startswith("author "):
                current["author"] = line[len("author "):]
            elif line.startswith("author-mail "):
                # 邮箱格式 <email>，去掉尖括号
                mail = line[len("author-mail "):]
                current["email"] = mail.strip("<>")
            elif line.startswith("author-time "):
                ts = line[len("author-time "):].strip()
                # 转 ISO 字符串（不引入 datetime 复杂解析，保留时间戳字符串）
                current["date"] = ts
            elif not line.startswith("author-") and not line.startswith("committer") \
                    and not line.startswith("summary") and not line.startswith("filename"):
                # 第一行 header：<sha> <orig-line> <final-line> [<content>]
                # 提取 sha
                parts = line.split()
                if parts and len(parts) >= 3 and current.get("hash") is None:
                    current["hash"] = parts[0]
        return result


# ------------------------------------------------------------------ 模块级单例

_default_service: Optional[CommitAttributionService] = None
_default_lock = threading.Lock()


def get_default_service() -> CommitAttributionService:
    """返回模块级默认 CommitAttributionService 单例。"""
    global _default_service
    if _default_service is None:
        with _default_lock:
            if _default_service is None:
                _default_service = CommitAttributionService()
    return _default_service
