"""
kinlyze/git.py
────────────────────────────────────────────────────────────────────────────────
Reads local Git history — stdlib only, zero external dependencies.

KEY DESIGN: ONE git log call for the entire analysis window.
Parsing happens in Python. This is ~500x faster than calling
`git log -- <file>` per file on a medium-sized repo.
────────────────────────────────────────────────────────────────────────────────
"""

import os
import re
import subprocess
from collections import defaultdict
from datetime import datetime, timezone
from typing import Callable, Dict, List, Optional

from .exceptions import (
    GitCommandError, GitNotFoundError, NotARepoError, EmptyRepoError
)

# ── Skip lists ────────────────────────────────────────────────────────────────

SKIP_EXTENSIONS = frozenset({
    ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico",
    ".woff", ".woff2", ".ttf", ".eot", ".otf",
    ".zip", ".tar", ".gz", ".bz2", ".7z",
    ".exe", ".dll", ".so", ".dylib", ".class", ".pyc", ".pyo",
    ".pdf", ".doc", ".docx", ".xls", ".xlsx",
    ".lock", ".sum", ".map", ".db", ".sqlite",
    ".DS_Store",
})

SKIP_DIRS = frozenset({
    "node_modules", "vendor", ".git", "__pycache__",
    "dist", "build", ".next", ".nuxt", "coverage",
    ".venv", "venv", "env", ".cache", ".idea", ".vscode",
    "bower_components", "jspm_packages",
})

CRITICAL_PATTERNS = frozenset({
    "payment", "billing", "stripe", "checkout", "invoice", "subscription",
    "auth", "authn", "authz", "security", "password", "token",
    "oauth", "jwt", "session", "credential", "secret", "encrypt",
    "migration", "schema", "model", "database",
    "settings", "config",
    "core", "engine", "pipeline", "processor",
    "router", "middleware", "gateway",
    "deploy", "dockerfile",
})

COMMIT_SEP = "<<KINLYZE>>"


# ── Git runner ────────────────────────────────────────────────────────────────

def _run(args: List[str], cwd: str) -> str:
    try:
        result = subprocess.run(
            ["git"] + args,
            cwd=cwd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=180,
        )
    except FileNotFoundError:
        raise GitNotFoundError()
    except subprocess.TimeoutExpired:
        raise GitCommandError(" ".join(args[:2]), "Timed out after 180s — try --days with a smaller window.")

    if result.returncode != 0:
        raise GitCommandError(" ".join(args[:2]), result.stderr)

    return result.stdout


# ── Repo validation ───────────────────────────────────────────────────────────

def is_git_repo(path: str) -> bool:
    try:
        _run(["rev-parse", "--git-dir"], path)
        return True
    except (GitCommandError, GitNotFoundError):
        return False


def get_repo_root(path: str) -> str:
    return _run(["rev-parse", "--show-toplevel"], path).strip()


def get_repo_info(repo_root: str) -> Dict:
    """Name, branch, total commits, first/last dates."""
    # Check for any commits at all
    try:
        total_str = _run(["rev-list", "--count", "HEAD"], repo_root).strip()
        total = int(total_str)
    except (GitCommandError, ValueError):
        total = 0

    if total == 0:
        raise EmptyRepoError()

    try:
        branch = _run(["rev-parse", "--abbrev-ref", "HEAD"], repo_root).strip()
    except GitCommandError:
        branch = "unknown"

    try:
        last_str  = _run(["log", "--format=%ai", "--max-count=1"], repo_root).strip()
        first_str = _run(["log", "--reverse", "--format=%ai", "--max-count=1"], repo_root).strip()
        last_date  = _parse_date(last_str)
        first_date = _parse_date(first_str)
    except GitCommandError:
        last_date = first_date = None

    return {
        "name":          os.path.basename(repo_root),
        "branch":        branch,
        "total_commits": total,
        "first_commit":  first_date,
        "last_commit":   last_date,
    }


# ── File helpers ──────────────────────────────────────────────────────────────

def should_analyze(path: str) -> bool:
    parts = path.replace("\\", "/").split("/")
    for part in parts[:-1]:
        if part.lower() in SKIP_DIRS:
            return False
    _, ext = os.path.splitext(path)
    return ext.lower() not in SKIP_EXTENSIONS and bool(parts[-1])


def is_critical_file(file_path: str) -> bool:
    lower = file_path.lower()
    return any(p in lower for p in CRITICAL_PATTERNS)


def get_module_path(file_path: str) -> str:
    parts = file_path.replace("\\", "/").split("/")
    return "/".join(parts[:-1]) if len(parts) > 1 else "root"


def get_tracked_files(repo_root: str) -> List[str]:
    try:
        out = _run(["ls-files"], repo_root)
        return [f.strip() for f in out.splitlines() if f.strip() and should_analyze(f.strip())]
    except GitCommandError:
        return []


def get_contributors(repo_root: str) -> List[Dict]:
    """All-time contributors — single pass via git shortlog."""
    try:
        out = _run(["shortlog", "-sne", "--all", "--no-merges"], repo_root)
    except GitCommandError:
        return []

    contributors = []
    for line in out.strip().splitlines():
        m = re.match(r"\s*(\d+)\s+(.+?)\s+<(.+?)>", line)
        if m:
            contributors.append({
                "commits": int(m.group(1)),
                "name":    m.group(2).strip(),
                "email":   m.group(3).lower().strip(),
            })
    return contributors


# ── Bulk commit loader — the core performance optimisation ────────────────────

def _parse_bulk_log(raw: str) -> Dict[str, List[Dict]]:
    """
    Parse raw output of `git log --format=<<KINLYZE>>\\n%H\\n%ae\\n%an\\n%ai --name-only`
    into {file_path: [commit_dict, ...]}

    State machine: SEEK → SHA → EMAIL → NAME → DATE → FILES → repeat
    """
    file_commits: Dict[str, List[Dict]] = defaultdict(list)
    commit: Dict = {}
    state = "seek"

    for raw_line in raw.splitlines():
        line = raw_line.rstrip()

        if line == COMMIT_SEP:
            commit = {}
            state  = "sha"
            continue

        if state == "sha":
            commit["sha"]   = line[:8]
            state = "email"
        elif state == "email":
            commit["email"] = line.lower().strip()
            state = "name"
        elif state == "name":
            commit["name"]  = line.strip()
            state = "date"
        elif state == "date":
            commit["date"]  = _parse_date(line)
            state = "files"
        elif state == "files":
            if not line:
                continue   # blank separator between commit and next marker
            if should_analyze(line) and commit.get("email"):
                file_commits[line].append({
                    "sha":   commit.get("sha",  ""),
                    "email": commit["email"],
                    "name":  commit.get("name",  ""),
                    "date":  commit.get("date"),
                })

    return dict(file_commits)


def load_commits_bulk(
    repo_root: str,
    since_days: int = 365,
    progress_cb: Optional[Callable[[str], None]] = None,
) -> Dict[str, List[Dict]]:
    """
    ONE git log call covering the full analysis window.
    Returns {file_path: [commits]} for every analyzable file touched.
    """
    fmt = f"{COMMIT_SEP}%n%H%n%ae%n%an%n%ai"

    if progress_cb:
        progress_cb(f"Running git log (last {since_days} days)...")

    try:
        raw = _run([
            "log",
            f"--since={since_days} days ago",
            f"--format={fmt}",
            "--name-only",
            "--no-merges",
            "--diff-filter=AM",
        ], repo_root)
    except GitCommandError as e:
        # Non-fatal — return empty rather than crashing
        return {}

    if progress_cb:
        progress_cb("Parsing commit history...")

    return _parse_bulk_log(raw)


def load_commits_window(
    repo_root: str,
    since_days: int,
    until_days: int = 0,
) -> Dict[str, List[Dict]]:
    """
    Commits in a specific window [until_days ago, since_days ago].
    Used for trend comparison (recent 90d vs prior 90d).
    """
    fmt = f"{COMMIT_SEP}%n%H%n%ae%n%an%n%ai"
    args = [
        "log",
        f"--since={since_days} days ago",
        f"--format={fmt}",
        "--name-only",
        "--no-merges",
        "--diff-filter=AM",
    ]
    if until_days > 0:
        args.insert(2, f"--until={until_days} days ago")

    try:
        raw = _run(args, repo_root)
        return _parse_bulk_log(raw)
    except GitCommandError:
        return {}


# ── Date parsing ──────────────────────────────────────────────────────────────

def _parse_date(date_str: str) -> Optional[datetime]:
    if not date_str:
        return None
    date_str = date_str.strip()
    try:
        # Normalize "2024-01-15 14:30:00 +0530" → "2024-01-15 14:30:00+0530"
        date_str = re.sub(r"(\d{2}:\d{2}:\d{2}) ([+-]\d{4})$", r"\1\2", date_str)
        dt = datetime.fromisoformat(date_str)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except (ValueError, AttributeError):
        return None
