#!/usr/bin/env python3
"""Local operator commands for the source-only receipt witness kit.

Only ``serve`` exposes a network listener, and that surface is read-only.
Key generation, receipt intake, checkpoint issuance, and candidate export are
local commands against the operator's persistent data directory.
"""

from __future__ import annotations

import argparse
import json
import os
import stat
from pathlib import Path
from typing import Any, Mapping

from bulla.experimental.receipt_witness import (
    OperatorManifestConfig,
    ReceiptWitness,
    make_witness_server,
)
from bulla.identity import LocalEd25519Signer
from bulla.receipt_parser import parse_action_receipt_json

from candidate import (
    DATA_MEMBERS,
    _RELEASE_CARGO,
    _RELEASE_ISSUER,
    canonical,
    sha256,
    strict_json,
    write_candidate,
)

CONFIG_PROFILE = "bulla.receipt-witness-operator-config/0.1"
CONFIG_FIELDS = {"profile", "log_id", "endpoint", "source_kit_root"}
DATA_FILES = {
    "key": "operator-key.json",
    "config": "operator-config.json",
    "source": "source-closure.json",
    "retention": "retention-policy.json",
    "privacy": "privacy-policy.json",
    "control": "control-disclosure.json",
    "log": "log.jsonl",
    "store": "store.jsonl",
    "checkpoints": "checkpoints.jsonl",
}


class OperatorError(ValueError):
    pass


def _read_object(path: Path, label: str, *, canonical_required: bool = True) -> Mapping[str, Any]:
    raw = path.read_bytes()
    value = strict_json(raw, label)
    if not isinstance(value, Mapping):
        raise OperatorError(f"{label} must contain a JSON object")
    if canonical_required and raw != canonical(value):
        raise OperatorError(f"{label} must use the exact canonical JSON presentation")
    return value


def _write_private(path: Path, payload: bytes) -> None:
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    try:
        os.write(descriptor, payload)
    finally:
        os.close(descriptor)
    if stat.S_IMODE(path.stat().st_mode) != 0o600:
        raise OperatorError("operator key file must have mode 0600")


def initialize(
    data: Path,
    *,
    log_id: str,
    endpoint: str,
    source_closure: Path,
    retention_policy: Path,
    privacy_policy: Path,
    control_disclosure: Path,
) -> dict[str, Any]:
    data = Path(data)
    if data.exists() and any(data.iterdir()):
        raise OperatorError("operator data directory must be absent or empty")
    if not log_id or log_id.strip() != log_id:
        raise OperatorError("log ID must be non-empty and have no surrounding whitespace")
    source = _read_object(source_closure, "source closure")
    if (
        set(source) != {"profile", "root", "members"}
        or source["profile"] != "bulla.receipt-witness-source-closure/0.1"
    ):
        raise OperatorError("source closure uses an unsupported profile")
    policies = {
        "retention": (retention_policy, "glyph.witness-retention-policy/0.1"),
        "privacy": (privacy_policy, "glyph.witness-privacy-policy/0.1"),
        "control": (control_disclosure, "glyph.witness-control-disclosure/0.1"),
    }
    policy_bytes: dict[str, bytes] = {}
    for label, (path, profile) in policies.items():
        document = _read_object(path, f"{label} document", canonical_required=False)
        if document.get("profile") != profile:
            raise OperatorError(f"{label} document uses an unsupported profile")
        policy_bytes[label] = canonical(document)

    config = OperatorManifestConfig(
        endpoint=endpoint,
        source_kit_root=source["root"],
        retention_policy_sha256=sha256(policy_bytes["retention"]),
        privacy_policy_sha256=sha256(policy_bytes["privacy"]),
        control_disclosure_sha256=sha256(policy_bytes["control"]),
    )
    signer = LocalEd25519Signer.generate()
    data.mkdir(parents=True, exist_ok=True)
    _write_private(data / DATA_FILES["key"], canonical(signer.to_keyfile_dict()))
    (data / DATA_FILES["config"]).write_bytes(
        canonical(
            {
                "profile": CONFIG_PROFILE,
                "log_id": log_id,
                "endpoint": config.endpoint,
                "source_kit_root": config.source_kit_root,
            }
        )
    )
    (data / DATA_FILES["source"]).write_bytes(canonical(source))
    for label in ("retention", "privacy", "control"):
        (data / DATA_FILES[label]).write_bytes(policy_bytes[label])
    for label in ("log", "store", "checkpoints"):
        (data / DATA_FILES[label]).touch(mode=0o600)
    witness = load_witness(data)
    return {
        "status": "INITIALIZED",
        "operator_did": signer.issuer,
        "log_id": log_id,
        "endpoint": endpoint,
        "source_kit_root": source["root"],
        "manifest": witness.manifest(),
    }


def _key(path: Path) -> LocalEd25519Signer:
    mode = stat.S_IMODE(path.stat().st_mode)
    if mode & 0o077:
        raise OperatorError("operator key file must not be group- or world-readable")
    document = _read_object(path, "operator key")
    signer = LocalEd25519Signer.from_keyfile_dict(dict(document))
    if document.get("did") != signer.verification_method:
        raise OperatorError("operator key file DID does not match its secret key")
    return signer


def load_witness(data: Path) -> ReceiptWitness:
    data = Path(data)
    missing = [name for name in DATA_FILES.values() if not (data / name).is_file()]
    if missing:
        raise OperatorError(f"operator data directory is incomplete: {sorted(missing)}")
    config = _read_object(data / DATA_FILES["config"], "operator config")
    if set(config) != CONFIG_FIELDS or config["profile"] != CONFIG_PROFILE:
        raise OperatorError("operator config fields or profile changed")
    source = _read_object(data / DATA_FILES["source"], "source closure")
    if source.get("root") != config["source_kit_root"]:
        raise OperatorError("operator config does not bind its source closure")
    retention = (data / DATA_FILES["retention"]).read_bytes()
    privacy = (data / DATA_FILES["privacy"]).read_bytes()
    control = (data / DATA_FILES["control"]).read_bytes()
    for raw, label in ((retention, "retention"), (privacy, "privacy"), (control, "control")):
        value = strict_json(raw, f"{label} document")
        if raw != canonical(value):
            raise OperatorError(f"{label} document changed from canonical form")
    manifest_config = OperatorManifestConfig(
        endpoint=config["endpoint"],
        source_kit_root=config["source_kit_root"],
        retention_policy_sha256=sha256(retention),
        privacy_policy_sha256=sha256(privacy),
        control_disclosure_sha256=sha256(control),
    )
    return ReceiptWitness(
        operator_signer=_key(data / DATA_FILES["key"]),
        log_id=config["log_id"],
        log_path=data / DATA_FILES["log"],
        store_path=data / DATA_FILES["store"],
        checkpoint_path=data / DATA_FILES["checkpoints"],
        manifest_config=manifest_config,
    )


def _release_attestations(witness: ReceiptWitness) -> tuple[str, str]:
    found: dict[str, str] = {}
    for record in witness.intake_records():
        raw = witness.receipt_bytes(record["attestation_hash"])
        if raw is None:
            raise OperatorError("intake record has no retained receipt bytes")
        document = parse_action_receipt_json(raw).to_dict()
        action = document.get("action") or {}
        subject = action.get("subject") or {}
        if action.get("type") == "package.release" and subject.get("package") == "bulla":
            version = subject.get("version")
            release = _RELEASE_CARGO.get(version)
            if (
                release is not None
                and sha256(raw) == release["byte_sha256"]
                and document["hashes"]["attestation"] == release["attestation"]
                and (document.get("signature") or {}).get("issuer") == _RELEASE_ISSUER
            ):
                found[version] = record["attestation_hash"]
    if set(found) != {"0.47.1", "0.48.0"}:
        raise OperatorError(
            "candidate requires the exact published Bulla 0.47.1 and 0.48.0 release receipts"
        )
    return found["0.47.1"], found["0.48.0"]


def export(data: Path, destination: Path) -> str:
    witness = load_witness(data)
    if len(witness) != 2 or len(witness.checkpoint_history()) != 2:
        raise OperatorError("candidate exercise requires exactly two intakes and two checkpoints")
    attestation_a, attestation_b = _release_attestations(witness)
    intake_a = witness.intake_record(attestation_a)
    intake_b = witness.intake_record(attestation_b)
    checkpoints = witness.checkpoint_history()
    inclusion_a = witness.inclusion_at(attestation_a, checkpoints[0].tree_size)
    inclusion_b = witness.inclusion_at(attestation_b, checkpoints[1].tree_size)
    if None in (intake_a, intake_b, inclusion_a, inclusion_b):
        raise OperatorError("candidate exercise is missing intake or inclusion data")
    if intake_a["index"] != 0 or intake_b["index"] != 1:
        raise OperatorError("release receipts must be ingested in 0.47.1 then 0.48.0 order")
    consistency = witness.adjacent_consistency(
        checkpoints[0].checkpoint_hash, checkpoints[1].checkpoint_hash
    )
    if consistency is None:
        raise OperatorError("candidate exercise lacks adjacent consistency evidence")
    source = (Path(data) / DATA_FILES["source"]).read_bytes()
    documents = {
        "operator-manifest.json": canonical(witness.manifest()),
        "source-closure.json": source,
        "retention-policy.json": (Path(data) / DATA_FILES["retention"]).read_bytes(),
        "privacy-policy.json": (Path(data) / DATA_FILES["privacy"]).read_bytes(),
        "control-disclosure.json": (Path(data) / DATA_FILES["control"]).read_bytes(),
        "receipt-a.json": witness.receipt_bytes(attestation_a),
        "receipt-b.json": witness.receipt_bytes(attestation_b),
        "intake-a.json": canonical(intake_a),
        "intake-b.json": canonical(intake_b),
        "checkpoint-a.json": canonical(checkpoints[0].to_dict()),
        "checkpoint-b.json": canonical(checkpoints[1].to_dict()),
        "inclusion-a.json": canonical(inclusion_a),
        "inclusion-b.json": canonical(inclusion_b),
        "consistency-a-b.json": canonical(consistency),
    }
    if set(documents) != set(DATA_MEMBERS) or any(value is None for value in documents.values()):
        raise OperatorError("candidate exporter did not close its exact member set")
    return write_candidate(destination, signer=witness.operator_signer, documents=documents)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)

    init = subparsers.add_parser("init", help="generate a new operator key and persistent state")
    init.add_argument("--data", type=Path, required=True)
    init.add_argument("--log-id", required=True)
    init.add_argument("--endpoint", required=True)
    init.add_argument("--source-closure", type=Path, default=Path(__file__).with_name("source-closure.json"))
    init.add_argument("--retention-policy", type=Path, required=True)
    init.add_argument("--privacy-policy", type=Path, required=True)
    init.add_argument("--control-disclosure", type=Path, required=True)

    intake = subparsers.add_parser("intake", help="locally verify and retain one exact receipt")
    intake.add_argument("--data", type=Path, required=True)
    intake.add_argument("--receipt", type=Path, required=True)

    checkpoint = subparsers.add_parser("checkpoint", help="explicitly issue one signed checkpoint")
    checkpoint.add_argument("--data", type=Path, required=True)

    serve = subparsers.add_parser("serve", help="serve the read-only public surface")
    serve.add_argument("--data", type=Path, required=True)
    serve.add_argument("--host", default="127.0.0.1")
    serve.add_argument("--port", type=int, default=8080)

    export_parser = subparsers.add_parser("export", help="export a closed NOT_COUNTED candidate")
    export_parser.add_argument("--data", type=Path, required=True)
    export_parser.add_argument("--output", type=Path, required=True)

    args = parser.parse_args()
    if args.command == "init":
        result = initialize(
            args.data,
            log_id=args.log_id,
            endpoint=args.endpoint,
            source_closure=args.source_closure,
            retention_policy=args.retention_policy,
            privacy_policy=args.privacy_policy,
            control_disclosure=args.control_disclosure,
        )
    elif args.command == "intake":
        result = load_witness(args.data).intake(args.receipt.read_bytes())
    elif args.command == "checkpoint":
        result = load_witness(args.data).checkpoint().to_dict()
    elif args.command == "export":
        result = {
            "candidate_root": export(args.data, args.output),
            "candidate_status": "VALID_CANDIDATE",
            "counter_status": "NOT_COUNTED",
        }
    elif args.command == "serve":
        server = make_witness_server(load_witness(args.data), host=args.host, port=args.port)
        print(json.dumps({"status": "SERVING_READ_ONLY", "address": server.server_address}))
        server.serve_forever()
        return 0
    else:  # pragma: no cover
        raise OperatorError(f"unsupported command: {args.command}")
    print(json.dumps(result, indent=2))
    return 0


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