"""Phase 3 version bumper: rewrite TOC + versions.json + CHANGE_LOG.md, commit, tag.

Decides patch vs minor based on a small heuristic over the size of the proposed
fix. apply_bump is transactional: on any failure mid-flight, it restores the
pre-call state of the working tree (and removes any commit/tag it created)
before re-raising.
"""
import json
import re
import subprocess
from pathlib import Path

PATCH_BUMP_FILES_THRESHOLD = 3
PATCH_BUMP_LINES_THRESHOLD = 50

_TOC_VERSION_RE = re.compile(r"^(## Version:\s*)([0-9]+\.[0-9]+\.[0-9]+)\s*$", re.MULTILINE)


def decide_bump_kind(files_changed: int, diff_lines: int) -> str:
    if files_changed > PATCH_BUMP_FILES_THRESHOLD or diff_lines > PATCH_BUMP_LINES_THRESHOLD:
        return "minor"
    return "patch"


def parse_version(s: str) -> tuple[int, int, int]:
    parts = s.strip().split(".")
    if len(parts) != 3:
        raise ValueError(f"not semver: {s!r}")
    try:
        return (int(parts[0]), int(parts[1]), int(parts[2]))
    except ValueError as exc:
        raise ValueError(f"not semver: {s!r}") from exc


def next_version(current: str, kind: str) -> str:
    major, minor, patch = parse_version(current)
    if kind == "patch":
        return f"{major}.{minor}.{patch + 1}"
    if kind == "minor":
        return f"{major}.{minor + 1}.0"
    raise ValueError(f"unknown bump kind: {kind!r}")


def read_current_version(repo_root: Path) -> str:
    toc = repo_root / "CritMatic.toc"
    if not toc.exists():
        raise FileNotFoundError(f"{toc} does not exist")
    text = toc.read_text(encoding="utf-8")
    m = _TOC_VERSION_RE.search(text)
    if not m:
        raise ValueError("no `## Version:` line found in CritMatic.toc")
    return m.group(2)


def _git(repo: Path, *args: str) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", "-C", str(repo), *args],
        capture_output=True, text=True, check=False,
    )


def _tag_exists(repo: Path, tag: str) -> bool:
    return _git(repo, "rev-parse", "--verify", tag).returncode == 0


def _resolve_unique_tag(repo: Path, base: str) -> str:
    if not _tag_exists(repo, base):
        return base
    suffix = 1
    while _tag_exists(repo, f"{base}-{suffix}"):
        suffix += 1
    return f"{base}-{suffix}"


def _rewrite_toc(repo: Path, new_version: str) -> None:
    toc = repo / "CritMatic.toc"
    text = toc.read_text(encoding="utf-8")
    new_text, n = _TOC_VERSION_RE.subn(rf"\g<1>{new_version}", text, count=1)
    if n != 1:
        raise ValueError("could not substitute Version line")
    toc.write_text(new_text, encoding="utf-8")


def _rewrite_versions_json(repo: Path, new_version: str) -> None:
    p = repo / "versions.json"
    data = json.loads(p.read_text(encoding="utf-8"))
    data["critmatic_version"] = new_version
    p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def _prepend_changelog(repo: Path, new_version: str, body: str, today_mmddyyyy: str) -> None:
    p = repo / "CHANGE_LOG.md"
    if not p.is_file():
        raise FileNotFoundError(f"{p} is not a file")
    existing = p.read_text(encoding="utf-8")
    block = f"## [v{new_version}-release] - {today_mmddyyyy}\n- Auto-fix: {body}\n\n"
    lines = existing.splitlines(keepends=True)
    insert_at = 0
    for i, line in enumerate(lines):
        if line.startswith("# "):
            insert_at = i + 1
            if insert_at < len(lines) and lines[insert_at].strip() == "":
                insert_at += 1
            break
    new_text = "".join(lines[:insert_at]) + block + "".join(lines[insert_at:])
    p.write_text(new_text, encoding="utf-8")


def apply_bump(
    *,
    repo_root: Path,
    new_version: str,
    changelog_body: str,
    today_mmddyyyy: str,
) -> str:
    toc = repo_root / "CritMatic.toc"
    versions = repo_root / "versions.json"
    changelog = repo_root / "CHANGE_LOG.md"
    snapshots = {p: p.read_text(encoding="utf-8") for p in (toc, versions, changelog) if p.is_file()}

    committed = False
    tag: str | None = None

    try:
        _rewrite_toc(repo_root, new_version)
        _rewrite_versions_json(repo_root, new_version)
        _prepend_changelog(repo_root, new_version, changelog_body, today_mmddyyyy)

        _git(repo_root, "add", "CritMatic.toc", "versions.json", "CHANGE_LOG.md")
        commit = _git(repo_root, "commit", "-m", f"chore(release): v{new_version}")
        if commit.returncode != 0:
            raise RuntimeError(f"git commit failed: {commit.stderr.strip()}")
        committed = True

        tag = _resolve_unique_tag(repo_root, f"v{new_version}")
        tag_proc = _git(repo_root, "tag", "-a", tag, "-m", f"Release {tag}")
        if tag_proc.returncode != 0:
            raise RuntimeError(f"git tag failed: {tag_proc.stderr.strip()}")

        return tag
    except Exception:
        if tag is not None and _tag_exists(repo_root, tag):
            _git(repo_root, "tag", "-d", tag)
        if committed:
            _git(repo_root, "reset", "--hard", "HEAD~1")
        for p, content in snapshots.items():
            p.write_text(content, encoding="utf-8")
        raise
