"""Read a project's last committed ``generated/`` snapshots from git (the Changed lens).

The Changed lens (spec §4.10) needs no new schema — it diffs a project's **two most
recent committed ``generated/`` snapshots** and reports what moved. Git is the only
honest record of "what the last snapshot was" without inventing a second state file,
so this module shells ``git log`` + ``git show`` against the project's working tree.

It is strictly **read-only**: it never checks out, stages, or writes — it reads blobs
out of history by object name. A project that isn't a git repo, or whose ``generated/``
has fewer than two commits, simply yields fewer snapshots and the lens says so
honestly (no guess, no blank). Kept out of ``lenses.py`` so that module stays pure
(generated/ -> view-model); the git side is isolated here, like the spine isolates
git in its provider.
"""

from __future__ import annotations

import json
import subprocess
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from mgf.fedres import DiskProvider

#: The generated files the Changed lens diffs (state + roadmap + agent suggestions).
_DIFFED_FILES: tuple[str, ...] = ("status.json", "roadmap.json", "suggestions.json")


@dataclass(frozen=True)
class Snapshot:
    """One committed ``generated/`` state: the commit + its parseable payloads."""

    commit: str
    files: dict[str, Any]  # filename -> parsed JSON payload (unparseable files dropped)


def _git(root: str, *args: str) -> str | None:
    """Run a read-only git command in ``root``; None on any failure (not a repo, etc.)."""
    try:
        out = subprocess.run(  # noqa: S603
            ["git", "-C", root, *args],  # noqa: S607
            capture_output=True,
            text=True,
            check=True,
            timeout=5,
        )
    except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
        return None
    return out.stdout


def _generated_relpath(root: str) -> str | None:
    """The project's ``generated/`` dir, repo-root-relative (honours a sub-dir project)."""
    prefix = _git(root, "rev-parse", "--show-prefix")
    if prefix is None:
        return None
    # --show-prefix is "" at the repo top, else "sub/dir/" (keeps its trailing slash).
    return prefix.strip() + "fed-resources/generated"


def _commits(root: str, relpath: str) -> list[str]:
    """Commit hashes that touched ``generated/``, newest first."""
    out = _git(root, "log", "--format=%H", "--", relpath)
    if not out:
        return []
    return [line.strip() for line in out.splitlines() if line.strip()]


def _show_json(root: str, commit: str, relpath: str, name: str) -> Any | None:
    """Read one file's payload from a commit by object name; None if absent/unparseable."""
    out = _git(root, "show", f"{commit}:{relpath}/{name}")
    if out is None:
        return None
    try:
        return json.loads(out)
    except json.JSONDecodeError:
        return None


def generated_snapshots(provider: DiskProvider, limit: int = 2) -> list[Snapshot]:
    """Up to ``limit`` most-recent committed ``generated/`` snapshots, newest first.

    Empty when the project isn't a git repo or has no committed ``generated/``; the
    Changed lens turns ``len < 2`` into its honest "not enough history" message.
    """
    root = str(provider.project_root)
    relpath = _generated_relpath(root)
    if relpath is None:
        return []
    snaps: list[Snapshot] = []
    for commit in _commits(root, relpath)[:limit]:
        files: dict[str, Any] = {}
        for name in _DIFFED_FILES:
            payload = _show_json(root, commit, relpath, name)
            if isinstance(payload, dict):
                files[name] = payload
        snaps.append(Snapshot(commit=commit, files=files))
    return snaps
