from __future__ import annotations

import json
import shutil
from collections import Counter
from html import escape
from pathlib import Path
from typing import Any

from .owner_review import summarize_uncertainties
from .render import render_page
from .source import SourcePackage
from .util import load_json, project_root, utc_now, write_json, write_text
from .validation import validate_page


def validate_pilot_selection(
    selection_path: Path, source: SourcePackage
) -> tuple[dict[str, Any], list[int]]:
    selection = load_json(selection_path)
    cases = selection.get("cases") or []
    pages = [int(case["page"]) for case in cases]
    if len(pages) != 20 or len(set(pages)) != 20:
        raise ValueError("Production pilot selection must contain exactly 20 unique pages")
    ordered = sorted(pages)
    if ordered != list(range(ordered[0], ordered[0] + 20)):
        raise ValueError("Production pilot pages must be one consecutive physical range")
    evaluation_pages = {
        int(case["page"])
        for case in load_json(project_root() / "evaluation" / "selection.json")["cases"]
    }
    overlap = sorted(set(ordered) & evaluation_pages)
    if overlap:
        raise ValueError(f"Production pilot overlaps evaluation pages: {overlap}")
    for page in ordered:
        source.require_page(page)
    return selection, ordered


def _walk_interventions(node: Any) -> Counter[str]:
    found: Counter[str] = Counter()
    if isinstance(node, dict):
        kind = node.get("type")
        if kind in {"unclear", "illegible", "addition", "deletion", "correction"}:
            found[str(kind)] += 1
        for value in node.values():
            found.update(_walk_interventions(value))
    elif isinstance(node, list):
        for value in node:
            found.update(_walk_interventions(value))
    return found


def _dates(page: dict[str, Any]) -> list[dict[str, Any]]:
    dates: list[dict[str, Any]] = []
    for block in page["blocks"]:
        if block.get("date"):
            dates.append({"block_id": block["id"], **block["date"]})
        if block["type"] == "gap_notice":
            for endpoint in ("start_date", "end_date"):
                if block.get(endpoint):
                    dates.append(
                        {
                            "block_id": block["id"],
                            "range_endpoint": endpoint,
                            **block[endpoint],
                        }
                    )
    return dates


def _alignment(page: dict[str, Any]) -> dict[str, Any]:
    statuses: Counter[str] = Counter()
    matched = 0
    total = 0
    for block in page["blocks"]:
        evidence = block["evidence"]
        statuses[evidence["status"]] += 1
        matched += int(evidence["matched_tokens"])
        total += int(evidence["total_tokens"])
    return {
        "block_statuses": dict(sorted(statuses.items())),
        "matched_tokens": matched,
        "total_tokens": total,
        "note": "Exact normalized token alignment is secondary and may be partial or absent.",
    }


def _continuations(page: dict[str, Any]) -> dict[str, Any]:
    declarations: list[dict[str, Any]] = []
    for block in page["blocks"]:
        continuation = block["continuation"]
        if (
            continuation["from_previous"] != "no"
            or continuation["to_next"] != "no"
            or continuation["previous"]
            or continuation["next"]
        ):
            declarations.append({"block_id": block["id"], **continuation})
    return {"count": len(declarations), "declarations": declarations}


def _unusual_features(page: dict[str, Any]) -> list[str]:
    features: set[str] = set()
    block_types = Counter(block["type"] for block in page["blocks"])
    for block_type in ("letter", "list", "table", "annotation", "gap_notice", "separator"):
        if block_types[block_type]:
            features.add(f"{block_type}:{block_types[block_type]}")
    for kind, count in _walk_interventions(page["blocks"]).items():
        features.add(f"{kind}:{count}")
    for kind, count in Counter(note["kind"] for note in page["editorial_notes"]).items():
        features.add(f"editorial_note_{kind}:{count}")
    if len(page["blocks"]) > 3:
        features.add(f"multiple_blocks:{len(page['blocks'])}")
    return sorted(features)


def _attempt_failure_summary(page_root: Path) -> dict[str, Any]:
    model_failures: list[dict[str, Any]] = []
    canonical_failures: list[dict[str, Any]] = []
    for attempt in sorted(page_root.glob("attempt-*")):
        model_path = attempt / "model-validation.json"
        if model_path.is_file():
            report = load_json(model_path)
            if not report["valid"]:
                model_failures.append({"attempt": attempt.name, "errors": report["errors"]})
        canonical_path = attempt / "canonical-validation.json"
        if canonical_path.is_file():
            report = load_json(canonical_path)
            if not report["valid"]:
                canonical_failures.append(
                    {"attempt": attempt.name, "errors": report["errors"]}
                )
    initial_canonical = page_root / "canonical-validation.json"
    if initial_canonical.is_file():
        report = load_json(initial_canonical)
        if not report["valid"]:
            canonical_failures.insert(0, {"attempt": "initial", "errors": report["errors"]})
    return {"model": model_failures, "canonical": canonical_failures}


def _cli_usage(page_root: Path) -> dict[str, int]:
    totals: Counter[str] = Counter()
    for result_path in sorted(page_root.glob("attempt-*/backend-result.json")):
        usage = load_json(result_path).get("metadata", {}).get("usage") or {}
        for key, value in usage.items():
            if isinstance(value, int):
                totals[key] += value
    return dict(sorted(totals.items()))


def build_pilot_package(
    *, run_root: Path, source: SourcePackage, selection_path: Path
) -> dict[str, Any]:
    selection, selected_pages = validate_pilot_selection(selection_path, source)
    manifest_path = run_root / "run-manifest.json"
    if not manifest_path.is_file():
        raise ValueError(f"Missing pilot run manifest: {manifest_path}")
    manifest = load_json(manifest_path)
    if manifest.get("pages") != selected_pages:
        raise ValueError("Pilot run manifest pages do not match the selected 20-page range")
    results = {int(item["page"]): item for item in manifest.get("results", [])}
    reading_root = run_root / "renderings" / "reading"
    diplomatic_root = run_root / "renderings" / "diplomatic"
    page_reports: list[dict[str, Any]] = []
    alignment_page_statuses: Counter[str] = Counter()
    intervention_totals: Counter[str] = Counter()
    schema_failure_pages: set[int] = set()

    for page_number in selected_pages:
        page_id = f"page-{page_number:03d}"
        candidate_path = run_root / "candidates" / f"{page_id}.json"
        page_root = run_root / "pages" / page_id
        result = results.get(page_number, {"page": page_number, "status": "missing"})
        failure_summary = _attempt_failure_summary(page_root)
        final_attempt_number = int(result.get("attempt_count") or 1)
        final_attempt_root = page_root / f"attempt-{final_attempt_number:02d}"
        if failure_summary["model"] or failure_summary["canonical"]:
            schema_failure_pages.add(page_number)
        record: dict[str, Any] = {
            "physical_page": page_number,
            "model_run_status": result.get("status", "missing"),
            "repair_used": bool(result.get("repair_used", False)),
            "cache_hit": bool(result.get("cache_hit", False)),
            "resumed": bool(result.get("resumed", False)),
            "elapsed_seconds": result.get("elapsed_seconds", 0.0),
            "attempt_failures": failure_summary,
            "cli_reported_usage": _cli_usage(page_root),
            "source_image": str(source.image_path(page_number)),
            "candidate_json": str(candidate_path),
            "input_ocr": str(page_root / "current-ocr.txt"),
            "neighbor_context": str(page_root / "neighbor-context.json"),
            "raw_jsonl_events": str(final_attempt_root / "stdout.jsonl"),
            "stderr": str(final_attempt_root / "stderr.log"),
            "raw_final_response": str(final_attempt_root / "raw-final-response.txt"),
            "final_validation_report": str(page_root / "final-validation.json"),
        }
        if not candidate_path.is_file():
            record.update(
                {
                    "page_role": None,
                    "source_dates": [],
                    "validation": {"valid": False, "errors": result.get("errors", [])},
                    "uncertainties": [],
                    "alignment": None,
                    "continuations": None,
                    "unusual_structural_features": [],
                    "diplomatic_rendering": None,
                    "reading_rendering": None,
                }
            )
            page_reports.append(record)
            continue

        page = load_json(candidate_path)
        validation = validate_page(page, source)
        reading_path = reading_root / f"{page_id}.txt"
        diplomatic_path = diplomatic_root / f"{page_id}.txt"
        write_text(reading_path, render_page(page, diplomatic=False) + "\n")
        write_text(diplomatic_path, render_page(page, diplomatic=True) + "\n")
        alignment = _alignment(page)
        for status, count in alignment["block_statuses"].items():
            alignment_page_statuses[status] += count
        interventions = _walk_interventions(page["blocks"])
        intervention_totals.update(interventions)
        record.update(
            {
                "page_role": page["page_role"],
                "backend": page["provenance"]["backend"],
                "model": page["provenance"]["model"],
                "reasoning_effort": page["provenance"]["reasoning_effort"],
                "source_dates": _dates(page),
                "validation": validation.as_dict(),
                "review": page["review"],
                "editorial_notes": page["editorial_notes"],
                "uncertainties": summarize_uncertainties(page),
                "alignment": alignment,
                "continuations": _continuations(page),
                "unusual_structural_features": _unusual_features(page),
                "diplomatic_rendering": str(diplomatic_path),
                "reading_rendering": str(reading_path),
            }
        )
        page_reports.append(record)

    successes = [item for item in page_reports if item["model_run_status"] == "valid_candidate"]
    failures = [item for item in page_reports if item["model_run_status"] != "valid_candidate"]
    uncertain_pages = [item["physical_page"] for item in successes if item["uncertainties"]]
    reconciliation = load_json(run_root / "reconciliation.json")
    duplicate_report = load_json(run_root / "cross-page-duplicates.json")
    total_elapsed = sum(float(item.get("elapsed_seconds") or 0.0) for item in page_reports)
    usage_totals: Counter[str] = Counter()
    for item in page_reports:
        usage_totals.update(item["cli_reported_usage"])
    profile_counts = Counter(
        f"{item.get('model')}:{item.get('reasoning_effort')}"
        for item in successes
    )
    summary = {
        "pages_attempted": len(selected_pages),
        "first_pass_successes": sum(not item["repair_used"] for item in successes),
        "repaired_successes": sum(item["repair_used"] for item in successes),
        "failures": len(failures),
        "failed_pages": [item["physical_page"] for item in failures],
        "schema_or_invariant_failure_pages": sorted(schema_failure_pages),
        "pages_with_uncertain_readings": uncertain_pages,
        "continuation_links": sum(len(pair["linked"]) for pair in reconciliation["pairs"]),
        "unresolved_continuation_declarations": sum(
            len(pair["unresolved_left"]) + len(pair["unresolved_right"])
            for pair in reconciliation["pairs"]
        ),
        "duplicate_cross_page_findings": duplicate_report["findings"],
        "cache_hits": sum(item["cache_hit"] for item in page_reports),
        "resumed_pages": sum(item["resumed"] for item in page_reports),
        "recorded_elapsed_seconds": round(total_elapsed, 3),
        "mean_recorded_elapsed_seconds": round(total_elapsed / len(selected_pages), 3),
        "alignment_block_statuses": dict(sorted(alignment_page_statuses.items())),
        "intervention_counts": dict(sorted(intervention_totals.items())),
        "page_profile_counts": dict(sorted(profile_counts.items())),
        "cli_reported_usage_totals": dict(sorted(usage_totals.items())),
        "recurring_ocr_failure_patterns": [
            (
                f"Exact-token TSV alignment was partial for "
                f"{alignment_page_statuses.get('partial', 0)} of "
                f"{sum(alignment_page_statuses.values())} logical blocks; "
                f"{alignment_page_statuses.get('aligned', 0)} aligned fully and "
                f"{alignment_page_statuses.get('unaligned', 0)} were unaligned. "
                "This is expected when the candidate corrects OCR errors or preserves "
                "source typography that OCR lost."
            )
        ],
        "recurring_llm_failure_patterns": [
            (
                "No repeated response-schema failure occurred. Page 43's first "
                "schema-valid unreadable deletion triggered an overly strict "
                "deterministic invariant; the single repair and original response "
                "remain preserved, and the validator was relaxed after the pilot."
            ),
            (
                "Low-confidence readings are concentrated on pages 43, 48, and 59 "
                "and require owner comparison with the scan; they are not treated as errors."
            ),
        ],
        "new_schema_ambiguities": [
            (
                "Page 56 contains non-textual handwritten star/line/pointing marks. "
                "The candidate records them in a layout editorial note because the "
                "small canonical model has no dedicated graphical-mark node. Decide "
                "during owner review whether that is sufficient before corpus scale."
            )
        ],
        "scale_recommendation": "operationally_ready_but_wait_for_owner_review_before_corpus_scale",
    }
    report = {
        "created_at": utc_now(),
        "run_id": manifest["run_id"],
        "pilot_name": selection.get("name"),
        "physical_page_range": [selected_pages[0], selected_pages[-1]],
        "selection_reason": selection.get("description"),
        "authority": "diagnostic_only",
        "owner_review_status": "unapproved",
        "warning": (
            "Pilot candidates have not been owner reviewed and are not ground truth; "
            "no accuracy percentage is calculated."
        ),
        "execution": {
            "backend": manifest.get("backend"),
            "model": manifest.get("model"),
            "reasoning_effort": manifest.get("reasoning_effort"),
            "codex_version": manifest.get("codex_version"),
            "profiles": manifest.get("execution_profiles", []),
            "input_schema_sha256": manifest.get("input_schema_sha256"),
            "transcription_specification_sha256": manifest.get(
                "transcription_specification_sha256"
            ),
        },
        "summary": summary,
        "pages": page_reports,
    }
    review_root = run_root / "review"
    write_json(review_root / "report.json", report)
    write_text(review_root / "report.md", render_pilot_report(report))
    build_pilot_review_bundle(report=report, run_root=run_root, source=source)
    return report


PILOT_REVIEW_CSS = """\
:root{color-scheme:light;--ink:#25231f;--paper:#f7f3ea;--green:#26362f;--line:#c8c1b5;--warn:#fff0bd;--blue:#17609a}
*{box-sizing:border-box}body{font:16px/1.45 system-ui,sans-serif;margin:0;color:var(--ink);background:var(--paper)}
header,main{padding:1rem 2rem}header{background:var(--green);color:#fff}header a{color:#bfe1ff}a{color:var(--blue)}
.warning{background:var(--warn);color:#4b3300;padding:.75rem;border-left:5px solid #c78a00}.status{display:flex;gap:.65rem;flex-wrap:wrap}
.badge{background:#e6eee9;color:#183026;border-radius:999px;padding:.25rem .65rem;font-size:.9rem}.warning .badge{background:#fff8df;color:#4b3300}
table{border-collapse:collapse;width:100%;background:#fff}th,td{border:1px solid var(--line);padding:.55rem;text-align:left;vertical-align:top}th{background:#e5e0d5;position:sticky;top:0}
.page-grid{display:grid;grid-template-columns:minmax(360px,46vw) minmax(0,1fr);gap:1.5rem}.image-pane{align-self:start;position:sticky;top:0}.image-pane img{width:100%;height:auto;background:#fff;border:1px solid var(--line)}
.evidence-pane{min-width:0}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:.7rem}.card{background:#fff;border:1px solid var(--line);padding:.7rem}.card strong{display:block}
pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#fff;border:1px solid var(--line);padding:.8rem;max-height:34rem;overflow:auto}details{margin:1rem 0}summary{cursor:pointer;font-weight:650}
.renderings{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.quiet{color:#625d55}code{overflow-wrap:anywhere}nav{display:flex;justify-content:space-between;gap:1rem}
@media(max-width:920px){header,main{padding:1rem}.page-grid,.renderings{grid-template-columns:1fr}.image-pane{position:static}th{position:static}}
"""


def _pilot_list(items: list[str], *, empty: str = "None recorded") -> str:
    if not items:
        return f'<p class="quiet">{escape(empty)}</p>'
    return "<ul>" + "".join(f"<li>{escape(item)}</li>" for item in items) + "</ul>"


def _pilot_dates(dates: list[dict[str, Any]]) -> str:
    if not dates:
        return '<p class="quiet">No source date recorded on this page.</p>'
    items = []
    for date in dates:
        normalized = date.get("date_normalized") or "not normalized"
        status = date.get("normalization_status") or "—"
        items.append(
            f"{date.get('date_source', '—')} → {normalized} ({status})"
        )
    return _pilot_list(items)


def _pilot_run_href(path: str, run_root: Path) -> str:
    relative = Path(path).resolve().relative_to(run_root.resolve())
    return "../../" + relative.as_posix()


def _pilot_page_html(
    *,
    report: dict[str, Any],
    page: dict[str, Any],
    run_root: Path,
    previous_page: int | None,
    next_page: int | None,
) -> str:
    number = page["physical_page"]
    page_id = f"page-{number:03d}"
    candidate_path = Path(page["candidate_json"])
    candidate_text = (
        json.dumps(load_json(candidate_path), ensure_ascii=False, indent=2)
        if candidate_path.is_file()
        else "No candidate was produced."
    )
    ocr_path = Path(page["input_ocr"])
    ocr = ocr_path.read_text(encoding="utf-8", errors="replace") if ocr_path.is_file() else ""
    raw_path = Path(page["raw_final_response"])
    raw_response = (
        raw_path.read_text(encoding="utf-8", errors="replace")
        if raw_path.is_file()
        else "No raw final response was preserved."
    )
    reading_path = Path(page["reading_rendering"]) if page["reading_rendering"] else None
    diplomatic_path = (
        Path(page["diplomatic_rendering"]) if page["diplomatic_rendering"] else None
    )
    reading = (
        reading_path.read_text(encoding="utf-8", errors="replace")
        if reading_path and reading_path.is_file()
        else "No reading rendering was produced."
    )
    diplomatic = (
        diplomatic_path.read_text(encoding="utf-8", errors="replace")
        if diplomatic_path and diplomatic_path.is_file()
        else "No diplomatic rendering was produced."
    )
    notes = [note["text"] for note in page.get("editorial_notes", [])]
    notes.extend((page.get("review") or {}).get("notes", []))
    alignment = page.get("alignment") or {}
    continuation = page.get("continuations") or {"count": 0, "declarations": []}
    validation = page.get("validation") or {"valid": False, "errors": ["Missing result"]}
    navigation = []
    if previous_page is not None:
        navigation.append(f'<a href="page-{previous_page:03d}.html">← page {previous_page}</a>')
    else:
        navigation.append("<span></span>")
    navigation.append('<a href="../index.html">pilot checklist</a>')
    if next_page is not None:
        navigation.append(f'<a href="page-{next_page:03d}.html">page {next_page} →</a>')
    else:
        navigation.append("<span></span>")
    links = [
        ("candidate JSON", f"../data/candidates/{page_id}.json"),
        ("raw response", f"../data/raw-responses/{page_id}.txt"),
        ("raw OCR", f"../data/ocr/{page_id}.txt"),
    ]
    for label, key in (
        ("context", "neighbor_context"),
        ("raw JSONL events", "raw_jsonl_events"),
        ("stderr", "stderr"),
        ("validation JSON", "final_validation_report"),
    ):
        path = page.get(key)
        if path and Path(path).is_file():
            links.append((label, _pilot_run_href(path, run_root)))
    artifact_links = " · ".join(
        f'<a href="{escape(href)}">{escape(label)}</a>' for label, href in links
    )
    continuation_json = json.dumps(continuation, ensure_ascii=False, indent=2)
    validation_json = json.dumps(validation, ensure_ascii=False, indent=2)
    failure_json = json.dumps(page.get("attempt_failures", {}), ensure_ascii=False, indent=2)
    return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>{page_id} pilot review</title><link rel="stylesheet" href="../assets/pilot-review.css"></head>
<body><header><nav>{''.join(f'<span>{item}</span>' for item in navigation)}</nav><h1>Pilot {page_id}</h1>
<p class="warning"><strong>Unapproved diagnostic material.</strong> The scan is authoritative. Viewing this page does not approve or change any artifact.</p>
<div class="status"><span class="badge">{escape(page.get('model_run_status') or 'missing')}</span><span class="badge">validation: {'valid' if validation.get('valid') else 'failed'}</span><span class="badge">repair: {'yes' if page.get('repair_used') else 'no'}</span><span class="badge">owner review: {escape(report['owner_review_status'])}</span></div></header>
<main class="page-grid"><section class="image-pane"><h2>Authoritative scan</h2><a href="../images/{page_id}.png"><img src="../images/{page_id}.png" alt="Scanned source {page_id}"></a></section>
<section class="evidence-pane"><div class="cards">
<div class="card"><strong>Page role</strong>{escape(str(page.get('page_role') or '—'))}</div>
<div class="card"><strong>Model profile</strong>{escape(str(page.get('model') or '—'))} / {escape(str(page.get('reasoning_effort') or '—'))}</div>
<div class="card"><strong>TSV evidence</strong>{alignment.get('matched_tokens', 0)} / {alignment.get('total_tokens', 0)} tokens</div>
<div class="card"><strong>Continuations</strong>{continuation.get('count', 0)} declaration(s)</div></div>
<h2>Source dates</h2>{_pilot_dates(page.get('source_dates', []))}
<h2>Uncertainties</h2>{_pilot_list(page.get('uncertainties', []))}
<h2>Editorial and review notes</h2>{_pilot_list(notes)}
<h2>Deterministic renderings</h2><div class="renderings"><section><h3>Reading</h3><pre>{escape(reading)}</pre></section><section><h3>Diplomatic</h3><pre>{escape(diplomatic)}</pre></section></div>
<h2>Raw OCR</h2><pre>{escape(ocr)}</pre>
<details><summary>Model candidate JSON</summary><pre>{escape(candidate_text)}</pre></details>
<details><summary>Raw final model response</summary><pre>{escape(raw_response)}</pre></details>
<details><summary>Validation and repair details</summary><h3>Final validation</h3><pre>{escape(validation_json)}</pre><h3>Attempt failures</h3><pre>{escape(failure_json)}</pre></details>
<details><summary>Reconciled continuation details</summary><pre>{escape(continuation_json)}</pre></details>
<h2>Preserved artifacts</h2><p>{artifact_links}</p>
<p class="quiet">Candidate authority: <code>{escape(str((page.get('review') or {}).get('authority', 'diagnostic_only')))}</code>. No owner-approved pilot edition exists.</p>
</section></main></body></html>"""


def _pilot_index_html(report: dict[str, Any]) -> str:
    rows = []
    for page in report["pages"]:
        number = page["physical_page"]
        dates = "; ".join(item["date_source"] for item in page["source_dates"]) or "—"
        features = ", ".join(page["unusual_structural_features"]) or "—"
        continuation = (page.get("continuations") or {}).get("count", 0)
        rows.append(
            "<tr>"
            f'<td><a href="pages/page-{number:03d}.html">{number}</a></td>'
            f"<td>{escape(str(page.get('page_role') or '—'))}</td>"
            f"<td>{escape(dates)}</td>"
            f"<td>{escape(page['model_run_status'])}</td>"
            f"<td>{'yes' if page['repair_used'] else 'no'}</td>"
            f"<td>{'yes' if page['validation']['valid'] else 'no'}</td>"
            f"<td>{len(page['uncertainties'])}</td>"
            f"<td>{continuation}</td>"
            f"<td>{escape(features)}</td>"
            "</tr>"
        )
    first, last = report["physical_page_range"]
    return f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>Crowley diary pilot review</title><link rel="stylesheet" href="assets/pilot-review.css"></head><body>
<header><h1>Crowley diary production-pilot review</h1>
<p>Physical pages {first}–{last}. Select a page number to compare its scan, OCR, candidate, renderings, notes, and uncertainties.</p>
<p class="warning"><strong>All 20 pages are unapproved.</strong> These model candidates are diagnostic aids, not ground truth. Opening this bundle never changes review status.</p></header>
<main><p><a href="report.md">narrative report</a> · <a href="report.json">machine-readable report</a></p>
<table><thead><tr><th>Page</th><th>Role</th><th>Source date(s)</th><th>Run</th><th>Repair</th><th>Valid</th><th>Uncertainties</th><th>Continuations</th><th>Features</th></tr></thead><tbody>{''.join(rows)}</tbody></table></main></body></html>"""


def build_pilot_review_bundle(
    *, report: dict[str, Any], run_root: Path, source: SourcePackage
) -> Path:
    """Build a static, read-only inspection bundle for an unapproved pilot run."""

    review_root = run_root / "review"
    write_text(review_root / "assets" / "pilot-review.css", PILOT_REVIEW_CSS)
    for directory in (
        review_root / "images",
        review_root / "data" / "candidates",
        review_root / "data" / "ocr",
        review_root / "data" / "raw-responses",
        review_root / "pages",
    ):
        directory.mkdir(parents=True, exist_ok=True)
    pages = report["pages"]
    for position, page in enumerate(pages):
        number = page["physical_page"]
        page_id = f"page-{number:03d}"
        shutil.copyfile(source.image_path(number), review_root / "images" / f"{page_id}.png")
        for source_path, destination in (
            (Path(page["candidate_json"]), review_root / "data" / "candidates" / f"{page_id}.json"),
            (Path(page["input_ocr"]), review_root / "data" / "ocr" / f"{page_id}.txt"),
            (Path(page["raw_final_response"]), review_root / "data" / "raw-responses" / f"{page_id}.txt"),
        ):
            if source_path.is_file():
                shutil.copyfile(source_path, destination)
        previous_page = pages[position - 1]["physical_page"] if position else None
        next_page = (
            pages[position + 1]["physical_page"] if position + 1 < len(pages) else None
        )
        write_text(
            review_root / "pages" / f"{page_id}.html",
            _pilot_page_html(
                report=report,
                page=page,
                run_root=run_root,
                previous_page=previous_page,
                next_page=next_page,
            ),
        )
    index_path = review_root / "index.html"
    write_text(index_path, _pilot_index_html(report))
    return index_path


def render_pilot_report(report: dict[str, Any]) -> str:
    summary = report["summary"]
    lines = [
        "# Crowley diary 20-page production pilot",
        "",
        f"Run: `{report['run_id']}`  ",
        f"Physical pages: **{report['physical_page_range'][0]}–{report['physical_page_range'][1]}**  ",
        f"Authority: `{report['authority']}`; owner status: `{report['owner_review_status']}`",
        "",
        report["warning"],
        "",
        "## Range selection",
        "",
        report.get("selection_reason") or "",
        "",
        "## Operational summary",
        "",
        f"- Attempted: {summary['pages_attempted']}",
        f"- First-pass successes: {summary['first_pass_successes']}",
        f"- Repaired successes: {summary['repaired_successes']}",
        f"- Failures: {summary['failures']} ({summary['failed_pages']})",
        f"- Schema/invariant failure pages: {summary['schema_or_invariant_failure_pages']}",
        f"- Pages with uncertainty: {summary['pages_with_uncertain_readings']}",
        f"- Reconciled continuation links: {summary['continuation_links']}",
        f"- Duplicate cross-page findings: {len(summary['duplicate_cross_page_findings'])}",
        f"- Cache hits / resumed pages: {summary['cache_hits']} / {summary['resumed_pages']}",
        f"- Recorded execution time: {summary['recorded_elapsed_seconds']} s total; {summary['mean_recorded_elapsed_seconds']} s/page mean",
        f"- Page execution profiles: {summary['page_profile_counts']}",
        f"- CLI-reported token usage: {summary['cli_reported_usage_totals']}",
        f"- Exact TSV block alignment: {summary['alignment_block_statuses']}",
        f"- Inline interventions: {summary['intervention_counts']}",
        f"- Scale recommendation: `{summary['scale_recommendation']}`",
        "",
        "## Page checklist",
        "",
        "| Page | Profile | Role | Dates | Run | Repair | Valid | Uncertainty | TSV matched | Continuation | Features | Artifacts |",
        "|---:|---|---|---|---|:---:|:---:|---:|---:|---:|---|---|",
    ]
    for page in report["pages"]:
        dates = "; ".join(date["date_source"] for date in page["source_dates"]) or "—"
        alignment = page["alignment"] or {"matched_tokens": 0, "total_tokens": 0}
        continuation = page["continuations"] or {"count": 0}
        artifacts = f"[candidate]({page['candidate_json']})"
        if page["reading_rendering"]:
            artifacts += (
                f" · [reading]({page['reading_rendering']})"
                f" · [diplomatic]({page['diplomatic_rendering']})"
                f" · [scan]({page['source_image']})"
                f" · [OCR]({page['input_ocr']})"
                f" · [context]({page['neighbor_context']})"
                f" · [events]({page['raw_jsonl_events']})"
                f" · [raw response]({page['raw_final_response']})"
            )
        lines.append(
            "| {physical_page} | {profile} | {page_role} | {dates} | {model_run_status} | "
            "{repair} | {valid} | {uncertain} | {matched}/{total} | {continuation} | "
            "{features} | {artifacts} |".format(
                **page,
                profile=(
                    f"{page.get('model', '—')} / {page.get('reasoning_effort', '—')}"
                ),
                dates=dates.replace("|", "\\|"),
                repair="yes" if page["repair_used"] else "no",
                valid="yes" if page["validation"]["valid"] else "no",
                uncertain=len(page["uncertainties"]),
                matched=alignment["matched_tokens"],
                total=alignment["total_tokens"],
                continuation=continuation["count"],
                features=", ".join(page["unusual_structural_features"]) or "—",
                artifacts=artifacts,
            )
        )
    lines.extend(
        [
            "",
            "## Recorded patterns and open questions",
            "",
            "OCR/TSV patterns:",
            "",
            *[f"- {item}" for item in summary["recurring_ocr_failure_patterns"]],
            "",
            "LLM patterns:",
            "",
            *([f"- {item}" for item in summary["recurring_llm_failure_patterns"]] or ["- None recorded automatically; owner visual review remains required."]),
            "",
            "Schema ambiguities:",
            "",
            *([f"- {item}" for item in summary["new_schema_ambiguities"]] or ["- None identified by deterministic validation."]),
            "",
        ]
    )
    return "\n".join(lines)
