"""Staleness detection -- git-aware claim-time safety check.

Compares the git SHA captured at enqueue time against the current HEAD.
Classifies the overlap between changed files and the task's file manifest:

    clean       -- No commits since queue time. Safe to proceed.
    trivial     -- Commits exist but none touch the manifest files. Proceed + note.
    uncertain   -- Some manifest files changed. Escalate to orchestrator.
    conflicted  -- Heavy overlap (>=3 files or >50% of manifest). Escalate to human.

Usage::

    sha = capture_sha(repo_dir)              # at enqueue time
    result = assess_staleness(               # at claim time
        sha_at_queue=sha,
        file_manifest=["src/foo.py"],
        repo_dir=Path("."),
    )
    if result.level in (StalenessLevel.UNCERTAIN, StalenessLevel.CONFLICTED):
        # bounce the task
        ...
"""

from __future__ import annotations

import logging
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional

from cohort.pipeline.pipeline_task import StalenessLevel

logger = logging.getLogger(__name__)


@dataclass
class StalenessResult:
    level: StalenessLevel
    sha_at_queue: Optional[str]
    sha_current: Optional[str]
    files_changed_since_queue: List[str] = field(default_factory=list)
    overlapping_files: List[str] = field(default_factory=list)
    report: str = ""


def capture_sha(repo_dir: Optional[Path] = None) -> Optional[str]:
    """Return the current HEAD SHA, or None if git is unavailable."""
    try:
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            cwd=str(repo_dir or Path(".")),
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return result.stdout.strip()
    except Exception as exc:
        logger.debug("capture_sha failed: %s", exc)
    return None


def _changed_files(sha_at_queue: str, repo_dir: Path) -> List[str]:
    """Return list of files changed between sha_at_queue and HEAD."""
    try:
        result = subprocess.run(
            ["git", "diff", "--name-only", sha_at_queue, "HEAD"],
            cwd=str(repo_dir),
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode == 0:
            return [f.strip() for f in result.stdout.splitlines() if f.strip()]
    except Exception as exc:
        logger.debug("_changed_files failed: %s", exc)
    return []


def _current_sha(repo_dir: Path) -> Optional[str]:
    return capture_sha(repo_dir)


def assess_staleness(
    sha_at_queue: Optional[str],
    file_manifest: List[str],
    repo_dir: Optional[Path] = None,
) -> StalenessResult:
    """Assess how stale a task is relative to the current repo state.

    Parameters
    ----------
    sha_at_queue:
        Git SHA captured when the task was enqueued.
    file_manifest:
        Files the task intends to touch.
    repo_dir:
        Root of the git repository. Defaults to cwd.
    """
    repo_dir = repo_dir or Path(".")
    sha_current = _current_sha(repo_dir)

    # No git info available -- treat as clean, don't block
    if sha_at_queue is None or sha_current is None:
        return StalenessResult(
            level=StalenessLevel.CLEAN,
            sha_at_queue=sha_at_queue,
            sha_current=sha_current,
            report="Git SHA unavailable; staleness check skipped.",
        )

    # Same SHA -- nothing changed
    if sha_at_queue == sha_current:
        return StalenessResult(
            level=StalenessLevel.CLEAN,
            sha_at_queue=sha_at_queue,
            sha_current=sha_current,
            report="No commits since task was queued.",
        )

    changed = _changed_files(sha_at_queue, repo_dir)
    manifest_set = {Path(f).as_posix() for f in file_manifest}
    changed_set = {Path(f).as_posix() for f in changed}
    overlap = sorted(manifest_set & changed_set)

    if not overlap:
        report = (
            f"{len(changed)} file(s) changed since queue; "
            "none overlap with task manifest."
        )
        return StalenessResult(
            level=StalenessLevel.TRIVIAL,
            sha_at_queue=sha_at_queue,
            sha_current=sha_current,
            files_changed_since_queue=changed,
            overlapping_files=[],
            report=report,
        )

    overlap_ratio = len(overlap) / max(len(manifest_set), 1)
    is_conflicted = len(overlap) >= 3 or overlap_ratio > 0.5

    level = StalenessLevel.CONFLICTED if is_conflicted else StalenessLevel.UNCERTAIN
    report = (
        f"{len(overlap)} manifest file(s) changed since queue "
        f"({overlap_ratio:.0%} overlap): {', '.join(overlap)}"
    )

    return StalenessResult(
        level=level,
        sha_at_queue=sha_at_queue,
        sha_current=sha_current,
        files_changed_since_queue=changed,
        overlapping_files=overlap,
        report=report,
    )
