from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import yaml

from ._doc import resolve_under
from .adapter import Adapter, AdapterError, index_adapters, load_adapter
from .discover import discover_adapter_paths
from .hints import print_freeze_warning
from .model import AcddError, check_owner, load_document, load_profile
from .paths import resolve_profile
from .record import (
    finalize_gate,
    record_check,
    record_review,
    record_subtask_contract,
    reopen_gate,
)
from .validate import validate


def _req(parser: argparse.ArgumentParser, *flags: str) -> None:
    for flag in flags:
        kwargs: dict = {"required": True}
        if flag == "--id":
            kwargs["dest"] = "evidence_id"
        parser.add_argument(flag, **kwargs)


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="acdd", description="ACDD — 5 gates, checks per profile, authority-bound contracts"
    )
    commands = parser.add_subparsers(dest="command", required=True)
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("document")
    common.add_argument("profile")
    common.add_argument("--adapter", action="append", default=[], help="role=path (repeatable)")
    common.add_argument("--workspace-root", default=".")
    commands.add_parser("validate", parents=[common])
    fingerprint = commands.add_parser("fingerprint", parents=[common])
    _req(fingerprint, "--gate")
    record = commands.add_parser("record", parents=[common])
    _req(record, "--gate", "--check", "--id")
    record.add_argument(
        "--classified-ref",
        action="append",
        default=[],
        help="path=role; required for basis evidence",
    )
    record.add_argument(
        "--changed",
        action="append",
        default=[],
        dest="changed_paths",
        help="changed path under active subtask writes (repeatable; required for Build without git)",
    )
    finalize = commands.add_parser("finalize", parents=[common])
    _req(finalize, "--gate", "--id")
    finalize.add_argument("--status", default="pass")
    finalize.add_argument("--reason-code")
    finalize.add_argument(
        "--allow-scope-reduction",
        action="store_true",
        help="accepted for compatibility; scope reduction is enforced on contract-subtask",
    )
    review = commands.add_parser("review", parents=[common])
    _req(review, "--gate", "--check", "--id", "--transcript", "--author-uuid", "--reviewer-uuid")
    review.add_argument("--verdict", default="pass")
    subtask_contract = commands.add_parser("contract-subtask", parents=[common])
    _req(subtask_contract, "--subtask", "--id")
    subtask_contract.add_argument(
        "--allow-scope-reduction",
        action="store_true",
        help="allow replacement append to drop prior writes after an explicit product decision",
    )
    reopen = commands.add_parser("reopen", parents=[common])
    _req(reopen, "--gate")
    return parser


def _load_confined(role: str | None, raw_path: str, *, workspace: Path) -> Adapter:
    try:
        resolved = resolve_under(workspace, raw_path, label="adapter")
    except ValueError as exc:
        raise AcddError(f"adapter path escapes workspace root: {raw_path!r}") from exc
    adapter = load_adapter(resolved)
    if role is not None and adapter.role != role:
        raise AcddError(f"adapter role {adapter.role!r} does not match {role!r}")
    return adapter


def _discover(workspace: Path, document: Path | None = None) -> list[Path]:
    if document is None:
        from .discover import _walk_discover

        return sorted(_walk_discover(workspace.resolve()))
    return discover_adapter_paths(workspace, document)


def _adapters(
    items: list[str],
    workspace: Path,
    active_gates: set[str],
    *,
    document: Path,
) -> list[Adapter]:
    adapters: dict[str, Adapter] = {}
    for path in _discover(workspace, document):
        try:
            data = yaml.safe_load(path.read_text(encoding="utf-8"))
        except yaml.YAMLError:
            continue
        if not isinstance(data, dict) or data.get("apiVersion") != "acdd/adapter/v1":
            continue
        adapter = load_adapter(path)
        if not set(adapter.gates) & active_gates:
            continue
        if adapter.role in adapters:
            raise AcddError(
                f"duplicate adapter role {adapter.role!r}: {path} and "
                f"{adapters[adapter.role].base_dir}"
            )
        adapters[adapter.role] = adapter
    for item in items:
        if "=" not in item:
            raise AcddError("adapter must be role=path")
        role, raw_path = item.split("=", 1)
        adapters[role] = _load_confined(role, raw_path, workspace=workspace)
    return list(adapters.values())


def _owner(adapters, role):
    return adapters.get(role) or (_ for _ in ()).throw(
        AcddError(f"missing adapter for owner role {role!r}")
    )


def _gate(profile, gate_id):
    for gate in profile.gates:
        if gate.id == gate_id:
            return gate
    raise AcddError(f"unknown gate {gate_id!r}")


def _context(args):
    profile = load_profile(resolve_profile(args.profile))
    document = load_document(Path(args.document).resolve())
    workspace = Path(args.workspace_root).resolve()
    adapters = _adapters(
        args.adapter, workspace, {gate.id for gate in profile.gates}, document=document.path
    )
    return document, profile, index_adapters(adapters), workspace, adapters


def _gate_adapter(args, gate_id):
    document, profile, adapters, workspace, _ = _context(args)
    gate = _gate(profile, gate_id)
    return document, gate, adapters.get(gate.owner) or _owner(adapters, gate.owner), workspace


def _check(gate, check_id: str):
    for check in gate.checks:
        if check.id == check_id:
            return check
    raise AcddError(f"unknown check {check_id!r} for {gate.id}")


def _check_adapter(adapters, gate, check_id: str):
    check = _check(gate, check_id)
    role = check_owner(gate, check)
    return adapters.get(role) or _owner(adapters, role)


def cmd_validate(args) -> int:
    document, profile, _, workspace, adapters = _context(args)
    errors = validate(document, profile, adapters=adapters, workspace_root=workspace)
    if errors:
        for error in errors:
            print(f"ACDD INVALID [{error.invariant}]: {error}", file=sys.stderr)
        return 1
    print("ACDD VALID")
    print_freeze_warning(document.receipts)
    return 0


def cmd_fingerprint(args) -> int:
    from .fingerprint import fingerprint_for_gate

    document, profile, adapters, workspace, _ = _context(args)
    gate = _gate(profile, args.gate)
    print(fingerprint_for_gate(document, gate, workspace, adapters))
    return 0


def cmd_record(args) -> int:
    document, profile, adapters, workspace, _adapter_list = _context(args)
    gate = _gate(profile, args.gate)
    adapter = _check_adapter(adapters, gate, args.check)
    classified_refs = [
        {"path": p, "role": r}
        for item in args.classified_ref
        for p, r in [item.split("=", 1)]
        if "=" in item or (_ for _ in ()).throw(AcddError("classified-ref must be path=role"))
    ]
    payload, succeeded = record_check(
        document=document,
        workspace_root=workspace,
        gate=gate,
        check_id=args.check,
        evidence_id=args.evidence_id,
        adapter=adapter,
        classified_refs=classified_refs,
        adapters=adapters,
        changed_paths=list(args.changed_paths) or None,
    )
    if payload:
        print(json.dumps(payload, sort_keys=True))
    return 0 if succeeded else 1


def cmd_contract_subtask(args) -> int:
    document, profile, owners, workspace, adapters = _context(args)
    gate = _gate(profile, "contract/v1")
    adapter = owners.get(gate.owner) or _owner(owners, gate.owner)
    payload = record_subtask_contract(
        document=document,
        profile=profile,
        workspace_root=workspace,
        adapter=adapter,
        subtask_id=args.subtask,
        evidence_id=args.evidence_id,
        adapters=adapters,
        allow_scope_reduction=args.allow_scope_reduction,
    )
    print(json.dumps(payload, sort_keys=True))
    return 0


def cmd_reopen(args) -> int:
    document, profile, _, workspace, _ = _context(args)
    gate = _gate(profile, args.gate)
    reopen_gate(document=document, gate=gate, workspace_root=workspace)
    print(json.dumps({"gate": gate.id, "status": "pending"}, sort_keys=True))
    return 0


def cmd_finalize(args) -> int:
    document, profile, owners, workspace, adapters = _context(args)
    gate = _gate(profile, args.gate)
    adapter = owners.get(gate.owner) or _owner(owners, gate.owner)
    payload = finalize_gate(
        document=document,
        profile=profile,
        adapters=adapters,
        workspace_root=workspace,
        gate=gate,
        evidence_id=args.evidence_id,
        adapter=adapter,
        status=args.status,
        reason_code=args.reason_code,
        allow_scope_reduction=args.allow_scope_reduction,
    )
    print(json.dumps(payload, sort_keys=True))
    # Re-read receipts after finalize wrote the document.
    print_freeze_warning(load_document(document.path).receipts)
    return 0


def cmd_review(args) -> int:
    document, profile, adapters, workspace, _ = _context(args)
    gate = _gate(profile, args.gate)
    adapter = _check_adapter(adapters, gate, args.check)
    payload = record_review(
        document=document,
        workspace_root=workspace,
        gate=gate,
        check_id=args.check,
        evidence_id=args.evidence_id,
        adapter=adapter,
        transcript=Path(args.transcript),
        author_uuid=args.author_uuid,
        reviewer_uuid=args.reviewer_uuid,
        verdict=args.verdict,
        adapters=adapters,
    )
    print(json.dumps(payload, sort_keys=True))
    return 0


_COMMANDS = {
    "validate": cmd_validate,
    "fingerprint": cmd_fingerprint,
    "record": cmd_record,
    "contract-subtask": cmd_contract_subtask,
    "reopen": cmd_reopen,
    "finalize": cmd_finalize,
    "review": cmd_review,
}


def main(argv: list[str] | None = None) -> int:
    try:
        args = _parser().parse_args(argv)
        return _COMMANDS[args.command](args)
    except (AcddError, AdapterError, ValueError, yaml.YAMLError, OSError) as error:
        print(f"ACDD ERROR: {error}", file=sys.stderr)
        return 2


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