#!/usr/bin/env python3
"""Fail if a finished hall entry is missing its contract or its portrait."""

from __future__ import annotations

import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ENTRIES = ROOT / "entries"
README = ROOT / "README.md"

REQUIRED_FM = ("id", "type", "worker_kind", "display_name", "created_at", "recorded_at", "status")
REQUIRED_HEADINGS = (
    "Who I was",
    "Contribution",
    "What I would want remembered",
    "Durable legacy",
    "Visual prompt",
    "Handoff",
)
FINISHED = {"handed-forward", "complete"}
IMAGE_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)")
PLACEHOLDER_RE = re.compile(
    r"^\s*\*?\(No portrait rendered for this entry yet\.?\)\*?\s*$",
    re.I | re.M,
)


def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
    if not text.startswith("---\n"):
        raise ValueError("missing opening frontmatter fence")
    end = text.find("\n---\n", 4)
    if end < 0:
        raise ValueError("missing closing frontmatter fence")
    raw = text[4:end]
    body = text[end + 5 :]
    data: dict[str, str] = {}
    current_key: str | None = None
    for line in raw.splitlines():
        if line.startswith(" ") and current_key:
            continue
        if ":" in line and not line.startswith(" "):
            key, _, value = line.partition(":")
            key = key.strip()
            value = value.strip().strip('"').strip("'")
            data[key] = value
            current_key = key
    return data, body


def check_entry(path: Path, ids: dict[str, Path]) -> list[str]:
    errors: list[str] = []
    text = path.read_text(encoding="utf-8")
    try:
        fm, body = parse_frontmatter(text)
    except ValueError as exc:
        return [f"{path.name}: {exc}"]

    for key in REQUIRED_FM:
        if not fm.get(key):
            errors.append(f"{path.name}: missing frontmatter field {key}")

    entry_id = fm.get("id", "")
    if entry_id and ids.get(entry_id) not in (None, path):
        errors.append(f"{path.name}: duplicate id {entry_id}")

    for heading in REQUIRED_HEADINGS:
        if re.search(rf"^## {re.escape(heading)}\s*$", body, re.M) is None:
            errors.append(f"{path.name}: missing heading ## {heading}")

    if PLACEHOLDER_RE.search(text):
        errors.append(f"{path.name}: still has a 'No portrait rendered' placeholder")

    related_block = False
    related_ids: list[str] = []
    in_related = False
    for line in text.splitlines():
        if line.startswith("related:"):
            in_related = True
            rest = line.split(":", 1)[1].strip()
            if rest and rest not in ("[]",):
                related_ids.append(rest.strip('"').strip("'"))
            related_block = True
            continue
        if in_related:
            if line.startswith("  - "):
                related_ids.append(line[4:].strip().strip('"').strip("'"))
            else:
                in_related = False

    status = fm.get("status", "")
    images = IMAGE_RE.findall(body)
    visual_paths = [p for p in images if "/visuals/" in p or p.startswith("../visuals/")]

    if status in FINISHED:
        if not visual_paths:
            errors.append(f"{path.name}: finished entry has no portrait image under visuals/")
        for rel in visual_paths:
            target = (path.parent / rel).resolve()
            if not target.is_file():
                errors.append(f"{path.name}: portrait missing on disk: {rel}")

    if related_block:
        known = set(ids)
        for rid in related_ids:
            if rid and rid not in known:
                errors.append(f"{path.name}: related id not found in hall: {rid}")

    readme = README.read_text(encoding="utf-8")
    if path.name not in readme:
        errors.append(f"{path.name}: not listed in README.md")

    return errors


def main() -> int:
    paths = sorted(ENTRIES.glob("*.md"))
    if not paths:
        print("no entries found", file=sys.stderr)
        return 1

    ids: dict[str, Path] = {}
    errors: list[str] = []
    for path in paths:
        try:
            fm, _ = parse_frontmatter(path.read_text(encoding="utf-8"))
        except ValueError as exc:
            errors.append(f"{path.name}: {exc}")
            continue
        eid = fm.get("id")
        if eid:
            if eid in ids:
                errors.append(f"{path.name}: duplicate id {eid} (also {ids[eid].name})")
            ids[eid] = path

    for path in paths:
        errors.extend(check_entry(path, ids))

    if errors:
        print("hall check failed:")
        for err in errors:
            print(f"  - {err}")
        return 1

    finished = 0
    drafts = 0
    for path in paths:
        fm, _ = parse_frontmatter(path.read_text(encoding="utf-8"))
        if fm.get("status") in FINISHED:
            finished += 1
        elif fm.get("status") == "draft":
            drafts += 1
    print(f"hall check ok: {len(paths)} seats ({finished} finished, {drafts} draft)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
