"""
Graph freshness detection via git SHA tracking.

Pure git — works on GitHub, code.amazon.com, GitLab, any git host.
Detects ALL changes: our commits, teammates' commits, rebases, merges.
"""

from __future__ import annotations

import logging
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .graph_store import GraphStore

logger = logging.getLogger(__name__)


class GitError(Exception):
    """Git command failed."""


@dataclass
class FreshnessResult:
    stale: bool
    changed_files: list[str] = field(default_factory=list)
    commits_behind: int = 0
    last_indexed: str | None = None
    current_head: str | None = None
    suggest_full_rebuild: bool = False
    reason: str = ""


def check_freshness(graph_store: GraphStore) -> FreshnessResult:
    """
    Pure git — works on GitHub, code.amazon.com, GitLab, any git host.
    Detects ALL changes: our commits, teammates' commits, rebases, merges.
    """
    repo_root_str = graph_store.get_meta("repo_root")
    if not repo_root_str:
        return FreshnessResult(stale=True, suggest_full_rebuild=True,
                               reason="No repo_root in graph metadata")

    repo_root = Path(repo_root_str)
    if not repo_root.is_dir():
        return FreshnessResult(stale=True, reason=f"repo not found: {repo_root}")

    # Check if it's a git repo
    if not (repo_root / ".git").exists() and not _is_git_worktree(repo_root):
        return _mtime_freshness(graph_store, repo_root)

    last_commit = graph_store.get_meta("last_indexed_commit")

    if not last_commit:
        return FreshnessResult(stale=True, suggest_full_rebuild=True,
                               reason="Never indexed")

    # What's HEAD now?
    try:
        current_head = _git(repo_root, ["rev-parse", "HEAD"]).strip()
    except GitError as e:
        logger.warning(f"Git rev-parse failed: {e}")
        return FreshnessResult(stale=True, reason=f"git error: {e}")

    if current_head == last_commit:
        return FreshnessResult(stale=False)

    # Edge case: force push / rebase removed our baseline commit
    try:
        ancestor_check = subprocess.run(
            ["git", "merge-base", "--is-ancestor", last_commit, current_head],
            cwd=repo_root, capture_output=True, timeout=5
        )
        if ancestor_check.returncode != 0:
            return FreshnessResult(
                stale=True,
                suggest_full_rebuild=True,
                current_head=current_head,
                last_indexed=last_commit,
                reason=f"Base commit {last_commit[:8]} rebased away — incremental impossible"
            )
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return FreshnessResult(
            stale=True,
            suggest_full_rebuild=True,
            current_head=current_head,
            reason="git merge-base failed"
        )

    # Normal case: what files changed?
    try:
        changed_output = _git(repo_root, [
            "diff", "--name-only", f"{last_commit}..{current_head}"
        ])
        changed_files = [f for f in changed_output.strip().split("\n") if f]

        commit_count_str = _git(repo_root, [
            "rev-list", "--count", f"{last_commit}..{current_head}"
        ]).strip()
        commit_count = int(commit_count_str) if commit_count_str else 0
    except (GitError, ValueError) as e:
        logger.warning(f"Git diff/rev-list failed: {e}")
        return FreshnessResult(
            stale=True,
            suggest_full_rebuild=True,
            current_head=current_head,
            last_indexed=last_commit,
            reason=f"git error: {e}"
        )

    return FreshnessResult(
        stale=True,
        changed_files=changed_files,
        commits_behind=commit_count,
        last_indexed=last_commit,
        current_head=current_head,
        suggest_full_rebuild=(len(changed_files) > 100 or commit_count > 50),
    )


def _git(cwd: Path, args: list[str], timeout: int = 5) -> str:
    """Bare git command. No hosting API. Works on any git repo."""
    try:
        result = subprocess.run(
            ["git"] + args,
            cwd=cwd, capture_output=True, text=True, timeout=timeout
        )
    except FileNotFoundError:
        raise GitError("git not found on PATH")
    except subprocess.TimeoutExpired:
        raise GitError(f"git {' '.join(args)}: timed out after {timeout}s")

    if result.returncode != 0:
        raise GitError(f"git {' '.join(args)}: {result.stderr.strip()}")
    return result.stdout


def _is_git_worktree(path: Path) -> bool:
    """Check if path is a git worktree (has .git file pointing to main repo)."""
    git_file = path / ".git"
    if git_file.is_file():
        try:
            content = git_file.read_text().strip()
            return content.startswith("gitdir:")
        except Exception:
            pass
    return False


def _mtime_freshness(graph_store: GraphStore, repo_root: Path) -> FreshnessResult:
    """
    Fallback: check file mtimes vs last_indexed timestamp.
    Lower accuracy than git (catches file saves, misses renames/deletes).
    """
    from .parser import LANGUAGE_MAP

    last_indexed_str = graph_store.get_meta("last_full_index")
    last_indexed_ts = float(last_indexed_str) if last_indexed_str else 0.0

    changed = []
    file_count = 0
    max_files = 10000  # cap to avoid walking huge non-git dirs

    for ext in LANGUAGE_MAP:
        for f in repo_root.rglob(f"*{ext}"):
            file_count += 1
            if file_count > max_files:
                return FreshnessResult(
                    stale=True,
                    suggest_full_rebuild=True,
                    reason=f"mtime scan: too many files (>{max_files})"
                )
            try:
                if f.stat().st_mtime > last_indexed_ts:
                    changed.append(str(f.relative_to(repo_root)))
            except OSError:
                continue

    return FreshnessResult(
        stale=bool(changed),
        changed_files=changed,
        reason="mtime-based (no git)"
    )
