from __future__ import annotations

import json
from pathlib import Path
from typing import Annotated

import typer

from .backends import CodexExecBackend
from .config import load_profile
from .corpus import finalize_corpus, promote_reviewed_pilot, run_corpus_transcription
from .evaluation import compare_pages
from .owner_review import (
    approve_page,
    build_review_bundle,
    mark_needs_work,
    summarize_owner_review,
)
from .pilot import build_pilot_package, validate_pilot_selection
from .pipeline import run_batch, transcribe_page
from .reconciliation import reconcile_adjacent_pages
from .review import stage_agent_review
from .schema_tools import check_schemas
from .source import SourcePackage
from .util import load_json, project_root, utc_now, write_json
from .validation import validate_page, validate_reciprocal_pages


app = typer.Typer(no_args_is_help=True, help="Crowley diary transcription pipeline")
source_app = typer.Typer(no_args_is_help=True, help="Inspect the immutable OCR package")
schema_app = typer.Typer(no_args_is_help=True, help="Manage and validate schemas")
transcribe_app = typer.Typer(no_args_is_help=True, help="Run page transcription")
review_app = typer.Typer(no_args_is_help=True, help="Stage diagnostic agent reviews")
evaluate_app = typer.Typer(no_args_is_help=True, help="Compare candidates with diagnostics")
owner_review_app = typer.Typer(
    no_args_is_help=True,
    help="Build owner inspection artifacts and record explicit owner actions",
)
pilot_app = typer.Typer(
    no_args_is_help=True,
    help="Run and package the fixed 20-page consecutive production pilot",
)
corpus_app = typer.Typer(
    no_args_is_help=True,
    help="Run bounded parallel corpus-wide PASS 1 processing and deterministic aggregation",
)
app.add_typer(source_app, name="source")
app.add_typer(schema_app, name="schema")
app.add_typer(transcribe_app, name="transcribe")
app.add_typer(review_app, name="review")
app.add_typer(evaluate_app, name="evaluate")
app.add_typer(owner_review_app, name="owner-review")
app.add_typer(pilot_app, name="pilot")
app.add_typer(corpus_app, name="corpus")


def _emit(value: object) -> None:
    typer.echo(json.dumps(value, ensure_ascii=False, indent=2))


@source_app.command("verify")
def source_verify(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    output: Annotated[
        Path | None,
        typer.Option(help="Audit JSON destination"),
    ] = None,
) -> None:
    package = SourcePackage(source_root)
    report = package.verify()
    destination = output or project_root() / "artifacts" / "reports" / "source-audit.json"
    write_json(destination, report)
    _emit({"output": str(destination), **report["summary"], "status": report["status"]})
    if report["errors"]:
        raise typer.Exit(1)


@schema_app.command("check")
def schema_check() -> None:
    check_schemas()
    _emit({"status": "valid", "schema": str(project_root() / "schema" / "page.schema.json")})


@app.command("validate")
def validate_command(
    paths: Annotated[list[Path], typer.Argument(exists=True, dir_okay=False)],
    source_root: Annotated[
        Path | None,
        typer.Option(exists=True, file_okay=False),
    ] = None,
) -> None:
    package = SourcePackage(source_root) if source_root else None
    results = []
    invalid = False
    for path in paths:
        report = validate_page(load_json(path), package)
        results.append({"path": str(path), **report.as_dict()})
        invalid = invalid or not report.valid
    _emit({"results": results})
    if invalid:
        raise typer.Exit(1)


@app.command("reconcile")
def reconcile_command(
    left: Annotated[Path, typer.Option(exists=True, dir_okay=False)],
    right: Annotated[Path, typer.Option(exists=True, dir_okay=False)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
) -> None:
    """Reconcile and rewrite one validated adjacent page pair in place."""

    package = SourcePackage(source_root)
    left_page = load_json(left)
    right_page = load_json(right)
    before = [validate_page(page, package) for page in (left_page, right_page)]
    if any(not report.valid for report in before):
        _emit(
            {
                "status": "invalid_input",
                "left_errors": before[0].errors,
                "right_errors": before[1].errors,
            }
        )
        raise typer.Exit(1)
    result = reconcile_adjacent_pages(left_page, right_page)
    after = [validate_page(page, package) for page in (left_page, right_page)]
    reciprocal = validate_reciprocal_pages([left_page, right_page])
    if any(not report.valid for report in after) or not reciprocal.valid:
        _emit(
            {
                "status": "failed",
                "reconciliation": result.as_dict(),
                "left_errors": after[0].errors,
                "right_errors": after[1].errors,
                "reciprocal_errors": reciprocal.errors,
            }
        )
        raise typer.Exit(1)
    write_json(left, left_page)
    write_json(right, right_page)
    _emit(
        {
            "status": "valid",
            "left": str(left),
            "right": str(right),
            "reconciliation": result.as_dict(),
        }
    )


@transcribe_app.command("page")
def transcribe_one(
    page: Annotated[int, typer.Option(min=1, max=295)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    profile_path: Annotated[Path | None, typer.Option(exists=True, dir_okay=False)] = None,
    force: Annotated[bool, typer.Option(help="Ignore an exact cache hit")] = False,
) -> None:
    package = SourcePackage(source_root)
    profile = load_profile(profile_path)
    run_id = utc_now().replace(":", "").replace("-", "").replace("Z", "Z-page")
    run_root = project_root() / "artifacts" / "runs" / run_id
    result = transcribe_page(
        source=package,
        page=page,
        run_id=run_id,
        run_root=run_root,
        profile=profile,
        backend=CodexExecBackend(),
        force=force,
    )
    _emit({"run_id": run_id, **result.as_dict()})
    if result.status == "failed":
        raise typer.Exit(1)


@transcribe_app.command("batch")
def transcribe_batch(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    selection: Annotated[
        Path,
        typer.Option(exists=True, dir_okay=False),
    ] = project_root() / "evaluation" / "selection.json",
    profile_path: Annotated[Path | None, typer.Option(exists=True, dir_okay=False)] = None,
    force: Annotated[bool, typer.Option(help="Ignore exact cache hits")] = False,
    allow_large_batch: Annotated[
        bool,
        typer.Option(help="Explicitly override the PoC page-count guard"),
    ] = False,
) -> None:
    package = SourcePackage(source_root)
    profile = load_profile(profile_path)
    run_id, results = run_batch(
        source=package,
        selection_path=selection,
        profile=profile,
        force=force,
        allow_large_batch=allow_large_batch,
    )
    _emit({"run_id": run_id, "results": [result.as_dict() for result in results]})
    if any(result.status == "failed" for result in results):
        raise typer.Exit(1)


@pilot_app.command("run")
def pilot_run(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    selection: Annotated[
        Path,
        typer.Option(exists=True, dir_okay=False),
    ] = project_root() / "pilot" / "selection.json",
    profile_path: Annotated[Path | None, typer.Option(exists=True, dir_okay=False)] = (
        project_root() / "config" / "pilot-luna-high.toml"
    ),
    run_id: Annotated[str | None, typer.Option(help="Stable ID required for resume")] = None,
    resume: Annotated[bool, typer.Option(help="Reuse valid pages already in this run")] = False,
) -> None:
    """Run exactly the selected consecutive pilot through subscription Codex."""

    package = SourcePackage(source_root)
    try:
        _, pages = validate_pilot_selection(selection, package)
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc
    if resume and not run_id:
        raise typer.BadParameter("--resume requires --run-id")
    resolved_id = run_id or utc_now().replace(":", "").replace("-", "").replace(
        "Z", "Z-pilot"
    )
    target = project_root() / "pilot" / "runs" / resolved_id
    batch_id, results = run_batch(
        source=package,
        selection_path=selection,
        profile=load_profile(profile_path),
        run_id=resolved_id,
        run_root=target,
        resume=resume,
    )
    report = build_pilot_package(
        run_root=target,
        source=package,
        selection_path=selection,
    )
    _emit(
        {
            "run_id": batch_id,
            "pages": pages,
            "run_root": str(target),
            "report": str(target / "review" / "report.md"),
            "review_index": str(target / "review" / "index.html"),
            "summary": report["summary"],
            "results": [result.as_dict() for result in results],
        }
    )
    manifest = load_json(target / "run-manifest.json")
    if manifest["status"] != "complete":
        raise typer.Exit(1)


@pilot_app.command("package")
def pilot_package(
    run_id: Annotated[str, typer.Option(min=1)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    selection: Annotated[
        Path,
        typer.Option(exists=True, dir_okay=False),
    ] = project_root() / "pilot" / "selection.json",
) -> None:
    """Regenerate deterministic renderings and report without invoking a model."""

    target = project_root() / "pilot" / "runs" / run_id
    if not target.is_dir():
        raise typer.BadParameter(f"Pilot run does not exist: {target}")
    report = build_pilot_package(
        run_root=target,
        source=SourcePackage(source_root),
        selection_path=selection,
    )
    _emit(
        {
            "run_id": run_id,
            "report": str(target / "review" / "report.md"),
            "review_index": str(target / "review" / "index.html"),
            "summary": report["summary"],
        }
    )


@corpus_app.command("promote-pilot")
def corpus_promote_pilot(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    owner: Annotated[str, typer.Option(min=1)] = "John Crowley",
) -> None:
    """Apply the owner's explicit approval to the preserved 20-page pilot."""

    result = promote_reviewed_pilot(source=SourcePackage(source_root), owner=owner)
    _emit(result)


@corpus_app.command("transcribe")
def corpus_transcribe(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    workers: Annotated[int, typer.Option(min=1, max=32)] = 4,
    shard_size: Annotated[int, typer.Option(min=1, max=20)] = 20,
    profile_path: Annotated[Path, typer.Option(exists=True, dir_okay=False)] = (
        project_root() / "config" / "pilot-luna-high.toml"
    ),
    run_id: Annotated[str | None, typer.Option(min=1)] = None,
    resume: Annotated[bool, typer.Option()] = False,
) -> None:
    """Process missing pages in sequential physical shards with bounded workers."""

    if resume and not run_id:
        raise typer.BadParameter("--resume requires --run-id")
    package = SourcePackage(source_root)
    promotion = promote_reviewed_pilot(source=package)
    resolved_id = run_id or utc_now().replace(":", "").replace("-", "").replace(
        "Z", "Z-corpus"
    )
    report = run_corpus_transcription(
        source=package,
        profile=load_profile(profile_path),
        workers=workers,
        shard_size=shard_size,
        run_id=resolved_id,
        resume=resume,
    )
    target = project_root() / "corpus" / "runs" / resolved_id
    _emit(
        {
            "run_id": resolved_id,
            "pilot_promotion": promotion,
            "coverage": report["coverage"],
            "execution": report["execution"],
            "report": str(target / "report" / "corpus-report.md"),
            "review_index": str(target / "review" / "index.html"),
        }
    )


@corpus_app.command("finalize")
def corpus_finalize(
    run_id: Annotated[str, typer.Option(min=1)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
) -> None:
    """Rerun deterministic aggregation after corpus model execution has stopped."""

    target = project_root() / "corpus" / "runs" / run_id
    if not target.is_dir():
        raise typer.BadParameter(f"Corpus run does not exist: {target}")
    report = finalize_corpus(run_root=target, source=SourcePackage(source_root))
    _emit(
        {
            "run_id": run_id,
            "coverage": report["coverage"],
            "report": str(target / "report" / "corpus-report.md"),
            "review_index": str(target / "review" / "index.html"),
        }
    )
@review_app.command("stage-agent")
def review_stage_agent(
    reviewed_json: Annotated[Path, typer.Option(exists=True, dir_okay=False)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    reviewer: Annotated[str, typer.Option()] = "Codex",
    note: Annotated[list[str] | None, typer.Option()] = None,
) -> None:
    package = SourcePackage(source_root)
    page = load_json(reviewed_json)
    page_id = page["page_id"]
    golden = project_root() / "evaluation" / "goldens" / f"{page_id}.json"
    edition = project_root() / "edition" / "pages" / f"{page_id}.json"
    staged = stage_agent_review(
        reviewed_path=reviewed_json,
        source=package,
        reviewer=reviewer,
        notes=note or [],
        golden_path=golden,
        edition_path=edition,
    )
    _emit(
        {
            "page_id": staged["page_id"],
            "status": staged["review"]["status"],
            "authority": staged["review"]["authority"],
            "diagnostic_fixture": str(golden),
            "edition_page": str(edition),
        }
    )


@evaluate_app.command("diff")
def evaluate_diff(
    run_id: Annotated[str, typer.Option()],
    output: Annotated[Path | None, typer.Option()] = None,
) -> None:
    candidates = project_root() / "artifacts" / "runs" / run_id / "candidates"
    goldens = project_root() / "evaluation" / "goldens"
    comparisons: list[dict[str, object]] = []
    for golden in sorted(goldens.glob("page-*.json")):
        candidate = candidates / golden.name
        if candidate.is_file():
            comparisons.append(compare_pages(candidate, golden))
    destination = output or project_root() / "artifacts" / "reports" / f"evaluation-{run_id}.json"
    write_json(
        destination,
        {
            "run_id": run_id,
            "created_at": utc_now(),
            "diagnostic_only": True,
            "comparisons": comparisons,
        },
    )
    _emit({"output": str(destination), "compared_pages": len(comparisons)})


@evaluate_app.command("indexed")
def evaluate_indexed(
    index: Annotated[Path, typer.Option(exists=True, dir_okay=False)] = (
        project_root() / "evaluation" / "candidate-index.json"
    ),
    output: Annotated[Path | None, typer.Option()] = None,
) -> None:
    """Compare all candidates named by an index with diagnostic fixtures."""

    candidate_index = load_json(index)
    comparisons: list[dict[str, object]] = []
    for record in candidate_index["candidates"]:
        candidate = project_root() / record["path"]
        golden = project_root() / "evaluation" / "goldens" / candidate.name
        if not candidate.is_file() or not golden.is_file():
            raise typer.BadParameter(
                f"Indexed pair is missing for page {record['page']}: {candidate}, {golden}"
            )
        comparisons.append(compare_pages(candidate, golden))
    destination = output or project_root() / "artifacts" / "reports" / "evaluation-indexed.json"
    write_json(
        destination,
        {
            "created_at": utc_now(),
            "candidate_index": str(index),
            "diagnostic_only": True,
            "comparisons": comparisons,
        },
    )
    _emit({"output": str(destination), "compared_pages": len(comparisons)})


@owner_review_app.command("build")
def owner_review_build(
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    output: Annotated[Path | None, typer.Option()] = None,
) -> None:
    """Generate static inspection artifacts without changing approval state."""

    package = SourcePackage(source_root)
    destination = output or project_root() / "evaluation" / "owner-review"
    _emit(build_review_bundle(source=package, output=destination))


@owner_review_app.command("approve-as-is")
def owner_review_approve_as_is(
    page: Annotated[int, typer.Option(min=1, max=295)],
    owner: Annotated[str, typer.Option(min=1)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    note: Annotated[str | None, typer.Option()] = None,
) -> None:
    """Explicitly approve the diagnostic copy as the owner edition."""

    try:
        result = approve_page(
            source=SourcePackage(source_root),
            page=page,
            owner=owner,
            note=note,
        )
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc
    _emit(result)


@owner_review_app.command("approve-corrected")
def owner_review_approve_corrected(
    page: Annotated[int, typer.Option(min=1, max=295)],
    owner: Annotated[str, typer.Option(min=1)],
    corrected_json: Annotated[Path, typer.Option(exists=True, dir_okay=False)],
    source_root: Annotated[Path, typer.Option(exists=True, file_okay=False)],
    note: Annotated[str | None, typer.Option()] = None,
) -> None:
    """Explicitly approve an owner-supplied corrected canonical JSON file."""

    try:
        result = approve_page(
            source=SourcePackage(source_root),
            page=page,
            owner=owner,
            corrected_json=corrected_json,
            note=note,
        )
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc
    _emit(result)


@owner_review_app.command("needs-work")
def owner_review_needs_work(
    page: Annotated[int, typer.Option(min=1, max=295)],
    owner: Annotated[str, typer.Option(min=1)],
    reason: Annotated[str, typer.Option(min=1)],
) -> None:
    """Record that a page remains unapproved and needs more work."""

    try:
        result = mark_needs_work(page=page, owner=owner, reason=reason)
    except ValueError as exc:
        raise typer.BadParameter(str(exc)) from exc
    _emit(result)


@owner_review_app.command("summary")
def owner_review_summary(
    output: Annotated[Path | None, typer.Option()] = None,
) -> None:
    """Summarize explicit owner decisions without treating diagnostics as truth."""

    report = summarize_owner_review()
    destination = output or (
        project_root() / "artifacts" / "reports" / "owner-review-summary.json"
    )
    write_json(destination, {"created_at": utc_now(), **report})
    _emit({"output": str(destination), **report})


if __name__ == "__main__":
    app()
