"""Контрольные точки перед прогоном (R06).

У владельца выключился ноутбук посреди работы исполнителя — полтора часа пропали
целиком: коммита не было, дерево пустое. Поэтому `chekan run` перед каждым запуском
снимает снимок рабочей копии.

Одного `git stash create` мало. Он возвращает висячий коммит: на него никто не
ссылается, и первый же `git gc` его съедает. Точка протухает молча — хуже, чем её
отсутствие, потому что на неё рассчитывают. Поэтому снимок сразу закрепляется
ссылкой `refs/chekan/checkpoints/<прогон>/<время>`: пока ссылка есть, объект
живёт.

Рабочая копия при этом не трогается: `stash create` только пишет объекты, ничего
не откатывая. Прогон продолжается ровно с тем деревом, которое было.
"""

from __future__ import annotations

from datetime import datetime
from pathlib import Path

from .store import STATE_DIR, git

REF_PREFIX = "refs/chekan/checkpoints"

NO_GIT = "контрольная точка: репозитория нет, точек не будет"
CLEAN = "контрольная точка: снимать нечего — рабочая копия чистая"


def in_git(root: Path) -> bool:
    return bool(git(root, "rev-parse", "--git-dir"))


def _free_ref(root: Path, slug: str) -> str:
    stamp = datetime.now().strftime("%Y%m%dT%H%M%S")
    base = f"{REF_PREFIX}/{slug}/{stamp}"
    ref, n = base, 2
    while git(root, "rev-parse", "--verify", "--quiet", ref):
        ref = f"{base}-{n}"
        n += 1
    return ref


def capture(root: Path, slug: str) -> dict:
    """Снять точку и закрепить её ссылкой.

    Вырожденные случаи не роняют прогон и не врут: их видно словами, а `status`
    отличает их от настоящей точки.
    """
    if not in_git(root):
        return {"status": "nogit", "text": NO_GIT}
    sha = git(root, "stash", "create")
    if not sha:
        # Чистое дерево и репозиторий без коммитов дают пустой ответ одинаково:
        # в обоих случаях снимать нечего.
        return {"status": "clean", "text": CLEAN}
    ref = _free_ref(root, slug)
    git(root, "update-ref", "-m", "chekan checkpoint", ref, sha)
    if git(root, "rev-parse", "--verify", "--quiet", ref) != sha:
        # Молчать здесь нельзя: незакреплённый снимок выглядит как точка, но
        # исчезает при первой сборке мусора.
        return {
            "status": "unpinned",
            "sha": sha,
            "text": (
                f"контрольная точка: снимок {sha[:8]} снят, но ссылку {ref} закрепить "
                "не удалось — точка не переживёт `git gc`"
            ),
        }
    return {
        "status": "saved",
        "sha": sha,
        "ref": ref,
        "text": f"контрольная точка: {ref} → {sha[:8]}",
    }


def points(root: Path, slug: str | None = None) -> list[tuple[str, str]]:
    """Закреплённые точки, свежая первой: пары (ссылка, коммит)."""
    if not in_git(root):
        return []
    pattern = f"{REF_PREFIX}/{slug}" if slug else REF_PREFIX
    listing = git(
        root, "for-each-ref", "--sort=-creatordate", "--sort=-refname",
        "--format=%(refname) %(objectname)", pattern,
    )
    out: list[tuple[str, str]] = []
    for line in listing.splitlines():
        parts = line.split()
        if len(parts) == 2:
            out.append((parts[0], parts[1]))
    return out


def latest(root: Path, slug: str | None = None) -> tuple[str, str] | None:
    found = points(root, slug)
    return found[0] if found else None


def restore_command(ref: str) -> str:
    """Команда восстановления дословно — её копируют и выполняют как есть."""
    return f"git stash apply {ref}"


def dirty_paths(root: Path, limit: int = 12) -> list[str]:
    """Что не сохранено. Каталог прогона исключён: он меняется от каждой команды."""
    status = git(root, "status", "--porcelain=v1")
    rows = [ln for ln in status.splitlines() if ln.strip() and f"{STATE_DIR}/" not in ln]
    return rows[:limit]
