"""
aicontract — Hotspot Analizi
Amara + Beatriz + Jin-ho + Dr. Yuki'nin toplantı kararı.

Git log'u parse ederek projenin "en kırılgan" noktalarını bulur.
Deterministik. Geçmiş değişmez. LLM yok.

Kullanım:
    import aicontract
    aicontract.hotspot(".")             # tüm proje
    aicontract.hotspot("auth.py")       # tek dosya
    aicontract.hotspot(".", top=10)     # en riskli 10
"""

import os
import re
import ast
import subprocess
from dataclasses import dataclass, field
from typing import Optional


ANSI = {
    "red": "\033[91m",
    "yellow": "\033[93m",
    "green": "\033[92m",
    "blue": "\033[94m",
    "bold": "\033[1m",
    "dim": "\033[2m",
    "reset": "\033[0m",
}


def _c(t, col):
    return f"{ANSI.get(col, '')}{t}{ANSI['reset']}"


# ── Veri yapıları ────────────────────────────────────────────────────


@dataclass
class FileHotspot:
    filepath: str
    change_count: int = 0  # kaç commit'te değişti
    bug_fix_count: int = 0  # kaç bug-fix commit'i
    authors: set = field(default_factory=set)  # kaç farklı kişi dokundu
    last_changed: str = ""  # son değişim tarihi
    co_changed_with: dict = field(default_factory=dict)  # birlikte değişen dosyalar
    risk_score: float = 0.0

    @property
    def risk_level(self) -> str:
        if self.risk_score >= 70:
            return "CRITICAL"
        if self.risk_score >= 40:
            return "HIGH"
        if self.risk_score >= 20:
            return "MEDIUM"
        return "LOW"


@dataclass
class FunctionHotspot:
    name: str
    filepath: str
    change_count: int = 0
    churn: int = 0  # toplam eklenen + silinen satır
    risk_score: float = 0.0

    @property
    def risk_level(self) -> str:
        if self.risk_score >= 70:
            return "CRITICAL"
        if self.risk_score >= 40:
            return "HIGH"
        if self.risk_score >= 20:
            return "MEDIUM"
        return "LOW"


@dataclass
class HotspotReport:
    project_dir: str
    file_hotspots: list[FileHotspot] = field(default_factory=list)
    function_hotspots: list[FunctionHotspot] = field(default_factory=list)
    total_commits: int = 0
    has_git: bool = False
    error: str = ""

    def top_files(self, n: int = 5) -> list[FileHotspot]:
        return sorted(self.file_hotspots, key=lambda x: x.risk_score, reverse=True)[:n]

    def top_functions(self, n: int = 5) -> list[FunctionHotspot]:
        return sorted(self.function_hotspots, key=lambda x: x.risk_score, reverse=True)[
            :n
        ]

    def __str__(self) -> str:
        if not self.has_git:
            return f"{_c('aicontract/hotspot', 'bold')} — Git repo bulunamadı: {self.project_dir}"

        lines = [
            f"\n{_c('aicontract/hotspot', 'bold')} — "
            f"{self.total_commits} commit · "
            f"{len(self.file_hotspots)} dosya analiz edildi"
        ]

        if self.file_hotspots:
            lines.append(f"\n{_c('En riskli dosyalar:', 'bold')}")
            for f in self.top_files():
                color = _risk_color(f.risk_level)
                lines.append(f"  {_c(f.risk_level, color):20} {f.filepath}")
                lines.append(
                    f"    değişim:{f.change_count}  bug-fix:{f.bug_fix_count}  "
                    f"yazar:{len(f.authors)}  skor:{f.risk_score:.0f}"
                )
                if f.co_changed_with:
                    top_co = sorted(
                        f.co_changed_with.items(), key=lambda x: x[1], reverse=True
                    )[:2]
                    co_str = ", ".join(f"{p}({c}x)" for p, c in top_co)
                    lines.append(f"    birlikte değişen: {_c(co_str, 'dim')}")

        if self.function_hotspots:
            lines.append(f"\n{_c('En riskli fonksiyonlar:', 'bold')}")
            for fn in self.top_functions():
                color = _risk_color(fn.risk_level)
                lines.append(
                    f"  {_c(fn.risk_level, color):20} "
                    f"{fn.filepath}::{_c(fn.name, 'blue')}"
                )
                lines.append(
                    f"    değişim:{fn.change_count}  churn:{fn.churn} satır  skor:{fn.risk_score:.0f}"
                )

        return "\n".join(lines)


def _risk_color(level: str) -> str:
    return {"CRITICAL": "red", "HIGH": "red", "MEDIUM": "yellow", "LOW": "green"}.get(
        level, "dim"
    )


# ── Git log parsing ──────────────────────────────────────────────────

BUG_FIX_KEYWORDS = re.compile(
    r"\b(fix|bug|patch|hotfix|error|crash|broken|revert|issue|"
    r"hata|düzelt|kır|sorun|çözüm)\b",
    re.IGNORECASE,
)


def _run_git(cmd: list[str], cwd: str) -> Optional[str]:
    try:
        result = subprocess.run(
            ["git"] + cmd,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=15,
        )
        if result.returncode == 0:
            return result.stdout
        return None
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return None


def _is_git_repo(path: str) -> bool:
    return _run_git(["rev-parse", "--git-dir"], path) is not None


def _parse_log_for_files(project_dir: str) -> tuple[dict, int]:
    """
    Her dosya için:
    - Kaç commit'te değişti
    - Kaç bug-fix commit'i
    - Hangi yazarlar dokundu
    - Son değişim tarihi
    - Aynı commit'te hangi dosyalarla birlikte değişti
    """
    # Format: commit_hash | author | date | subject \n dosya1 \n dosya2 ...
    log = _run_git(
        ["log", "--name-only", "--pretty=format:COMMIT|%H|%an|%ad|%s", "--date=short"],
        project_dir,
    )
    if log is None:
        return {}, 0

    file_stats: dict[str, FileHotspot] = {}
    total_commits = 0
    current_commit = None
    current_files = []

    def flush_commit():
        if not current_commit or not current_files:
            return
        # Co-change: bu commit'teki tüm py dosyaları birbirine bağlı
        py_files = [f for f in current_files if f.endswith(".py")]
        for filepath in py_files:
            if filepath not in file_stats:
                file_stats[filepath] = FileHotspot(filepath=filepath)
            hs = file_stats[filepath]
            hs.change_count += 1
            hs.authors.add(current_commit["author"])
            hs.last_changed = hs.last_changed or current_commit["date"]
            if current_commit["is_bugfix"]:
                hs.bug_fix_count += 1
            # Co-change
            for other in py_files:
                if other != filepath:
                    hs.co_changed_with[other] = hs.co_changed_with.get(other, 0) + 1

    for line in log.splitlines():
        if line.startswith("COMMIT|"):
            flush_commit()
            _, _, author, date, subject = line.split("|", 4)
            current_commit = {
                "author": author,
                "date": date,
                "is_bugfix": bool(BUG_FIX_KEYWORDS.search(subject)),
            }
            current_files = []
            total_commits += 1
        elif line.strip() and current_commit is not None:
            current_files.append(line.strip())

    flush_commit()
    return file_stats, total_commits


def _parse_log_for_functions(filepath: str, project_dir: str) -> list[FunctionHotspot]:
    """
    Bir dosyadaki fonksiyonların churn (satır değişimi) geçmişi.
    git log -L :fn_name:filepath ile fonksiyon bazlı log alır.
    """
    # Önce mevcut fonksiyon isimlerini AST'den çek
    full_path = os.path.join(project_dir, filepath)
    if not os.path.exists(full_path):
        return []

    try:
        with open(full_path) as f:
            source = f.read()
        tree = ast.parse(source)
        fn_names = [
            n.name
            for n in ast.walk(tree)
            if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
        ]
    except Exception:
        return []

    results = []
    for fn_name in fn_names[:20]:  # çok fazla git çağrısı olmasın
        log = _run_git(["log", "--oneline", f"-L:{fn_name}:{filepath}"], project_dir)
        if log is None:
            continue

        commit_lines = [
            line for line in log.splitlines() if re.match(r"^[0-9a-f]{7}", line)
        ]
        added = len(re.findall(r"^\+[^+]", log, re.MULTILINE))
        removed = len(re.findall(r"^-[^-]", log, re.MULTILINE))

        if commit_lines:
            results.append(
                FunctionHotspot(
                    name=fn_name,
                    filepath=filepath,
                    change_count=len(commit_lines),
                    churn=added + removed,
                )
            )

    return results


def _calculate_file_risk(hs: FileHotspot, max_changes: int, max_bugfix: int) -> float:
    """
    Risk skoru formülü (Beatriz + Rashid onayı):
      - Değişim sıklığı: %40
      - Bug-fix oranı:   %35
      - Yazar sayısı:    %15  (çok kişi = koordinasyon riski)
      - Co-change:       %10  (birçok dosyayla birlikte değişiyorsa köklü)
    """
    if max_changes == 0:
        return 0.0

    change_score = (hs.change_count / max_changes) * 40
    bugfix_ratio = hs.bug_fix_count / max(hs.change_count, 1)
    bugfix_score = bugfix_ratio * 35
    author_score = min(len(hs.authors) / 5, 1.0) * 15
    co_score = min(len(hs.co_changed_with) / 10, 1.0) * 10

    return round(change_score + bugfix_score + author_score + co_score, 1)


def _calculate_fn_risk(fn: FunctionHotspot, max_changes: int, max_churn: int) -> float:
    if max_changes == 0:
        return 0.0
    change_score = (fn.change_count / max(max_changes, 1)) * 60
    churn_score = (fn.churn / max(max_churn, 1)) * 40
    return round(change_score + churn_score, 1)


# ── Ana API ──────────────────────────────────────────────────────────


def hotspot(
    path: str = ".",
    *,
    top: int = 5,
    include_functions: bool = True,
    silent: bool = False,
) -> HotspotReport:
    """
    Projenin Git geçmişinden risk haritası çıkar.

    En çok değişen, en çok bug-fix alan, en çok yazarın
    dokunduğu dosya ve fonksiyonları bulur.

    Args:
        path:               Proje dizini veya tek dosya
        top:                Kaç hotspot gösterilsin
        include_functions:  Fonksiyon bazlı analiz de yap (yavaşlatır)
        silent:             Terminale basma

    Döner:
        HotspotReport

    Kullanım:
        aicontract.hotspot(".")
        aicontract.hotspot("auth.py", top=3)
        report = aicontract.hotspot(".", silent=True)
        for f in report.top_files(10): print(f.filepath, f.risk_score)
    """
    # Proje kökünü bul
    if os.path.isfile(path):
        project_dir = os.path.dirname(os.path.abspath(path))
        filter_file = os.path.basename(path)
    else:
        project_dir = os.path.abspath(path)
        filter_file = None

    report = HotspotReport(project_dir=project_dir)

    if not _is_git_repo(project_dir):
        report.has_git = False
        report.error = "Git repo bulunamadı"
        if not silent:
            print(f"{_c('aicontract/hotspot', 'bold')} — Git repo yok: {project_dir}")
        return report

    report.has_git = True

    # Dosya bazlı analiz
    file_stats, total_commits = _parse_log_for_files(project_dir)
    report.total_commits = total_commits

    if filter_file:
        file_stats = {
            k: v
            for k, v in file_stats.items()
            if k == filter_file or k.endswith(filter_file)
        }

    if file_stats:
        max_changes = max(hs.change_count for hs in file_stats.values())
        max_bugfix = max(hs.bug_fix_count for hs in file_stats.values()) or 1
        for hs in file_stats.values():
            hs.risk_score = _calculate_file_risk(hs, max_changes, max_bugfix)
        report.file_hotspots = sorted(
            file_stats.values(), key=lambda x: x.risk_score, reverse=True
        )[: top * 2]

    # Fonksiyon bazlı analiz (en riskli dosyalar için)
    if include_functions and report.file_hotspots:
        all_fn_hotspots = []
        top_files = report.top_files(min(top, 3))  # en riskli 3 dosya
        for file_hs in top_files:
            fns = _parse_log_for_functions(file_hs.filepath, project_dir)
            all_fn_hotspots.extend(fns)

        if all_fn_hotspots:
            max_fn_changes = max(f.change_count for f in all_fn_hotspots) or 1
            max_churn = max(f.churn for f in all_fn_hotspots) or 1
            for fn in all_fn_hotspots:
                fn.risk_score = _calculate_fn_risk(fn, max_fn_changes, max_churn)
            report.function_hotspots = sorted(
                all_fn_hotspots, key=lambda x: x.risk_score, reverse=True
            )[:top]

    if not silent:
        print(report)

    return report
