#!/usr/bin/env python3
"""Gate A operator CLI for the breakdown layer.

Gate A definition: ref derivation/generation may proceed only when
``run`` exits 0, meaning there are no unresolved coverage BLOCKs. This build
does not enforce Gate A downstream; later generation remains operator-invoked
by procedure.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path


_REPO_ROOT = Path(__file__).resolve().parents[3]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.core.paths import ProjectPaths  # noqa: E402
from recoil.pipeline._lib.breakdown_coverage_validator import (  # noqa: E402
    verify_coverage,
    write_coverage_report,
)
from recoil.pipeline._lib.breakdown_extract import (  # noqa: E402
    BreakdownExtractError,
    extract_mention_ledger,
)
from recoil.pipeline._lib.breakdown_propose import (  # noqa: E402
    BreakdownProposalError,
    approve_proposal,
    propose_bible_diff,
    reject_proposal,
)
from recoil.pipeline._lib.prose_validator import Severity  # noqa: E402


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("command", nargs="?", choices=["run"], help="run S1 -> coverage -> S2")
    parser.add_argument("--project", help="project slug for run")
    parser.add_argument("--episode", type=int, help="episode number for run")
    parser.add_argument("--approve", metavar="PROPOSAL_FILE", help="apply an approved proposal")
    parser.add_argument(
        "--tombstone-ok",
        default="",
        help="comma-separated zero-based tombstone suggestion indexes to accept with --approve",
    )
    parser.add_argument("--reject", metavar="PROPOSAL_FILE", help="mark a proposal rejected")
    args = parser.parse_args(argv)

    actions = sum(bool(value) for value in (args.command, args.approve, args.reject))
    if actions != 1:
        parser.error("choose exactly one action: run, --approve, or --reject")

    try:
        if args.command == "run":
            if not args.project or args.episode is None:
                parser.error("run requires --project and --episode")
            return _run_gate(args.project, args.episode)
        if args.approve:
            return _approve(args.approve, args.tombstone_ok)
        if args.reject:
            reject_proposal(Path(args.reject))
            print(f"proposal rejected: {args.reject}")
            return 0
    except BreakdownExtractError as exc:
        print(f"breakdown extract failed: {exc}", file=sys.stderr)
        return 1
    except BreakdownProposalError as exc:
        print(f"breakdown gate failed: {exc}", file=sys.stderr)
        if "stale proposal" in str(exc):
            return 2
        return 1

    parser.error("unreachable action state")
    return 1


def _run_gate(project: str, episode: int) -> int:
    paths = ProjectPaths.for_project(project)
    ledger = extract_mention_ledger(project, episode)
    bible = _read_json_object(paths.global_bible_path)
    tombstones = _load_tombstones(paths.episode_breakdown_dir(episode) / "tombstones.json")
    results = verify_coverage(ledger, bible, tombstones=tombstones)
    report_path = write_coverage_report(results, ledger, paths.episode_breakdown_dir(episode))
    blocks = [result for result in results if result.severity is Severity.BLOCK]
    warns = [result for result in results if result.severity is Severity.WARN]

    proposal_path: Path | None = None
    if blocks:
        _proposal, proposal_path = propose_bible_diff(project, episode, ledger, bible, blocks)

    print(f"coverage_report={report_path}")
    if proposal_path:
        print(f"proposal={proposal_path}")
    else:
        print("proposal=None")
    print(f"BLOCK={len(blocks)} WARN={len(warns)}")
    return 0 if not blocks else 3


def _approve(proposal_file: str, tombstone_ok: str) -> int:
    indices = _parse_tombstone_indices(tombstone_ok)
    outcome = approve_proposal(Path(proposal_file), tombstone_indices=indices)
    if outcome.get("applied"):
        print(f"proposal approved: {proposal_file}")
    else:
        print(
            f"proposal NOT applied (residual BLOCKs — bible untouched): {proposal_file}"
        )
    print(f"coverage_report={outcome['report_path']}")
    print(f"accepted_tombstones={len(outcome['accepted_tombstones'])}")
    print(f"BLOCK={outcome['block_count']} WARN={outcome['warn_count']}")
    return 0 if outcome["block_count"] == 0 else 3


def _parse_tombstone_indices(raw: str) -> list[int]:
    if not raw.strip():
        return []
    indices: list[int] = []
    for part in raw.split(","):
        token = part.strip()
        if not token:
            continue
        try:
            indices.append(int(token))
        except ValueError as exc:
            raise BreakdownProposalError(f"invalid tombstone index: {token!r}") from exc
    return indices


def _read_json_object(path: Path) -> dict:
    data = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(data, dict):
        raise BreakdownProposalError(f"expected JSON object: {path}")
    return data


def _load_tombstones(path: Path) -> list[dict]:
    if not path.is_file():
        return []
    data = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(data, list):
        raise BreakdownProposalError(f"tombstones file must contain a list: {path}")
    return [item for item in data if isinstance(item, dict)]


if __name__ == "__main__":
    raise SystemExit(main())
