"""从 git history 重建认知事实

解析 git log，从 commit messages + 修改文件列表中提取认知事实，
标记 source='rebuild'，confidence=0.6（低于 hook 的 0.9）。

用途：cognitive.db 丢失/损坏后恢复，或新机器克隆项目后快速获得历史认知。
"""

from __future__ import annotations

import json
import logging
import re
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

from .db_cog import CogDatabase

logger = logging.getLogger(__name__)

MERGE_PATTERN = re.compile(r"^Merge\s+(branch|pull request|remote)", re.IGNORECASE)

# git log 自定义分隔符
_COMMIT_SEP = "__COMMIT__"


@dataclass
class RebuildStats:
    """重建统计"""

    commits_processed: int = 0
    commits_skipped: int = 0
    facts_created: int = 0
    facts_duplicate: int = 0
    errors: int = 0


def rebuild_from_git(
    db_cog: CogDatabase,
    repo_path: Path,
    collection: str,
    *,
    since: str | None = None,
    limit: int = 500,
    dry_run: bool = False,
) -> RebuildStats:
    """解析 git log 并提取认知事实。

    Args:
        db_cog: 已连接的认知数据库实例
        repo_path: git 仓库根目录
        collection: 集合名称
        since: git log 时间过滤（如 "2025-01-01"、"3 months ago"）
        limit: 最大处理 commit 数（默认 500）
        dry_run: 只统计不写入

    Returns:
        RebuildStats
    """
    stats = RebuildStats()

    commits = _parse_git_log(repo_path, since=since, limit=limit)

    for commit in commits:
        if MERGE_PATTERN.match(commit["message"]):
            stats.commits_skipped += 1
            continue

        try:
            facts = _commit_to_facts(commit, collection)

            for fact in facts:
                if dry_run:
                    stats.facts_created += 1
                    continue

                # 去重：忽略 session_id，只看 assertion + collection
                # （避免 hook fact 和 rebuild fact 语义重复）
                if _fact_exists_any_session(
                    db_cog, fact["assertion"], collection,
                ):
                    stats.facts_duplicate += 1
                    continue

                result = db_cog.insert_fact(fact)
                if result is not None:
                    stats.facts_created += 1
                else:
                    stats.errors += 1

            stats.commits_processed += 1
        except Exception as e:
            logger.warning(
                "处理 commit %s 失败: %s", commit.get("hash", "?")[:8], e
            )
            stats.errors += 1

    return stats


def _parse_git_log(
    repo_path: Path,
    since: str | None = None,
    limit: int = 500,
) -> list[dict]:
    """执行 git log 并解析输出。

    使用 --name-only 获取修改文件列表（比 --diff 快得多）。

    Returns:
        list of dict: hash, message, author, date, files
    """
    cmd = [
        "git", "-C", str(repo_path),
        "log",
        f"--max-count={limit}",
        f"--format={_COMMIT_SEP}%H%n%s%n%an%n%aI",
        "--name-only",
        "--no-merges",
    ]
    if since:
        cmd.append(f"--since={since}")

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=60,
        )
        if result.returncode != 0:
            logger.warning("git log 失败: %s", result.stderr.strip())
            return []
    except (subprocess.TimeoutExpired, FileNotFoundError) as e:
        logger.warning("git log 执行失败: %s", e)
        return []

    return _parse_log_output(result.stdout)


def _parse_log_output(output: str) -> list[dict]:
    """解析自定义 git log 格式。"""
    commits = []
    blocks = output.split(_COMMIT_SEP)

    for block in blocks:
        block = block.strip()
        if not block:
            continue

        lines = block.split("\n")
        if len(lines) < 4:
            continue

        commit_hash = lines[0].strip()
        message = lines[1].strip()
        author = lines[2].strip()
        date = lines[3].strip()

        # 剩余行是 --name-only 输出的文件路径
        files = [
            line.strip()
            for line in lines[4:]
            if line.strip()
        ]

        commits.append({
            "hash": commit_hash,
            "message": message,
            "author": author,
            "date": date,
            "files": files,
        })

    return commits


def _commit_to_facts(commit: dict, collection: str) -> list[dict]:
    """将一个 commit 转换为 fact 列表。

    策略：
    1. 每个 commit → 1 个 commit 级 fact
    2. 每个修改文件 → 1 个文件级 fact（超过 20 个文件时跳过）
    """
    facts = []
    message = commit["message"]

    # Commit 级 fact
    facts.append(_make_rebuild_fact(
        assertion=f"Committed: {message[:100]}",
        category="code",
        collection=collection,
        scope_files=commit["files"][:10],
    ))

    # 文件级 fact（太多文件时跳过，避免噪音）
    if len(commit["files"]) <= 20:
        for file_path in commit["files"]:
            facts.append(_make_rebuild_fact(
                assertion=f"Modified {file_path} — {message[:60]}",
                category="code",
                collection=collection,
                scope_files=[file_path],
            ))

    return facts


def _make_rebuild_fact(
    assertion: str,
    category: str,
    collection: str,
    scope_files: list[str] | None = None,
    scope_entities: list[str] | None = None,
) -> dict:
    """构造 rebuild 专用 fact dict。

    与 hook fact 的区别：
    - source='rebuild'（非 'hook'）
    - confidence=0.6（低于 hook 的 0.9，因为是重建数据）
    """
    return {
        "assertion": assertion,
        "category": category,
        "collection": collection,
        "session_id": None,
        "scope_files": json.dumps(scope_files or []),
        "scope_modules": "[]",
        "scope_entities": json.dumps(scope_entities or []),
        "confidence": 0.6,
        "source": "rebuild",
        "status": "active",
    }


def _fact_exists_any_session(
    db_cog: CogDatabase, assertion: str, collection: str,
) -> bool:
    """检查 fact 是否已存在（忽略 session_id）。

    与 db_cog.fact_exists 不同，此函数只按 assertion + collection 去重，
    不考虑 session_id，避免 hook fact（有 session_id）和 rebuild fact（无 session_id）
    之间产生语义重复。
    """
    row = db_cog.conn.execute(
        "SELECT 1 FROM facts WHERE assertion = ? AND collection = ? LIMIT 1",
        (assertion, collection),
    ).fetchone()
    return row is not None
