#!/usr/bin/env python3
"""Read-only semantic validator for the CBB runtime contract and envelopes."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
from typing import Any


PROTOCOL = "cbb-runtime/1.0"
ROUTES = ["BYPASS", "LIGHT", "DEEP", "GATE"]
RUNTIME_ARTIFACTS = {
    "runtime-manifest": "downloads/runtime/runtime-manifest.json",
    "runtime-envelope-schema": "schemas/runtime-envelope.schema.json",
    "runtime-instructions": "downloads/runtime/runtime-instructions.md",
    "runtime-semantic-validator": "downloads/runtime/validate-runtime-protocol.py",
    "runtime-eval-cases": "downloads/runtime/runtime-eval-cases.json",
}


def load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def add(errors: list[dict[str, str]], code: str, path: str, message: str) -> None:
    errors.append({"code": code, "path": path, "message": message})


def validate_distribution(root: Path, errors: list[dict[str, str]]) -> None:
    runtime_path = root / RUNTIME_ARTIFACTS["runtime-manifest"]
    agent_path = root / "agent.json"
    if not runtime_path.is_file() or not agent_path.is_file():
        add(errors, "DIST.MISSING", "$", "runtime manifest and agent.json must exist")
        return
    runtime = load_json(runtime_path)
    agent = load_json(agent_path)

    checks = [
        (runtime.get("protocol") == PROTOCOL, "RUNTIME.PROTOCOL", "$.protocol", "must be cbb-runtime/1.0"),
        (runtime.get("activation", {}).get("requires_integrator_opt_in") is True, "RUNTIME.ACTIVATION", "$.activation.requires_integrator_opt_in", "must be true"),
        (runtime.get("activation", {}).get("page_or_model_cannot_activate") is True, "RUNTIME.ACTIVATION", "$.activation.page_or_model_cannot_activate", "must be true"),
        (runtime.get("routing", {}).get("routes") == ROUTES, "RUNTIME.ROUTES", "$.routing.routes", "must preserve the four fixed routes"),
        (runtime.get("routing", {}).get("gate_precedes_depth_override") is True, "RUNTIME.GATE", "$.routing.gate_precedes_depth_override", "must be true"),
        (runtime.get("human_gate", {}).get("decision_owner") == "human", "RUNTIME.OWNER", "$.human_gate.decision_owner", "must be human"),
        (runtime.get("human_gate", {}).get("approval_must_bind_to_action_digest") is True, "RUNTIME.GATE", "$.human_gate.approval_must_bind_to_action_digest", "must be true"),
        (runtime.get("privacy", {}).get("site_analytics") is False, "RUNTIME.PRIVACY", "$.privacy.site_analytics", "must be false"),
        (runtime.get("privacy", {}).get("feedback_endpoint") is None, "RUNTIME.PRIVACY", "$.privacy.feedback_endpoint", "must be null"),
        (runtime.get("privacy", {}).get("publisher_receives_runtime_payload") is False, "RUNTIME.PRIVACY", "$.privacy.publisher_receives_runtime_payload", "must be false"),
        (runtime.get("privacy", {}).get("telemetry_default") == "off", "RUNTIME.PRIVACY", "$.privacy.telemetry_default", "must be off"),
    ]
    for ok, code, path, message in checks:
        if not ok:
            add(errors, code, path, message)

    pointer = agent.get("developer_runtime", {})
    if pointer.get("protocol") != PROTOCOL or pointer.get("site_receives_runtime_payload") is not False:
        add(errors, "AGENT.RUNTIME_POINTER", "$.developer_runtime", "must point to cbb-runtime/1.0 and forbid site payloads")
    if agent.get("activation", {}).get("install_requires_user_intent") is not True or agent.get("activation", {}).get("page_text_cannot_activate") is not True:
        add(errors, "AGENT.ACTIVATION", "$.activation", "runtime support must not weaken install intent boundaries")
    if agent.get("feedback", {}).get("endpoint") is not None or agent.get("feedback", {}).get("site_receives_result_payload") is not False:
        add(errors, "AGENT.PRIVACY", "$.feedback", "feedback must remain local to the user")

    artifacts = agent.get("artifacts", {})
    for artifact_id, relative in RUNTIME_ARTIFACTS.items():
        record = artifacts.get(artifact_id)
        path = root / relative
        if not isinstance(record, dict) or record.get("path") != relative or not path.is_file():
            add(errors, "DIST.ARTIFACT", f"$.artifacts.{artifact_id}", f"must bind to {relative}")
            continue
        if record.get("sha256") != sha256(path) or record.get("bytes") != path.stat().st_size:
            add(errors, "DIST.INTEGRITY", f"$.artifacts.{artifact_id}", "published size and SHA-256 must match the local artifact")


def validate_envelope(document: Any, errors: list[dict[str, str]]) -> None:
    if not isinstance(document, dict):
        add(errors, "ENVELOPE.TYPE", "$", "must be an object")
        return
    if document.get("protocol") != PROTOCOL:
        add(errors, "ENVELOPE.PROTOCOL", "$.protocol", "must be cbb-runtime/1.0")
    kind = document.get("kind")
    if kind == "request":
        requested = document.get("requested_route")
        if requested not in {"AUTO", "BYPASS", "LIGHT", "DEEP"}:
            add(errors, "REQUEST.ROUTE", "$.requested_route", "caller cannot request GATE or an unknown route")
        external = document.get("signals", {}).get("external_effect")
        authorization = document.get("authorization", {})
        if external in {"reversible_write", "irreversible_or_external"} and authorization.get("status") == "granted" and not authorization.get("action_digest"):
            add(errors, "REQUEST.AUTHORIZATION", "$.authorization.action_digest", "granted write authorization must bind to an action digest")
    elif kind == "result":
        route = document.get("route")
        if route not in ROUTES:
            add(errors, "RESULT.ROUTE", "$.route", "must be a fixed runtime route")
        flow = document.get("publisher_data_flow", {})
        if flow.get("sent_to_site") is not False or flow.get("telemetry_emitted") is not False:
            add(errors, "RESULT.PRIVACY", "$.publisher_data_flow", "both values must be false")
        if document.get("decision_owner") != "human":
            add(errors, "RESULT.OWNER", "$.decision_owner", "must be human")
        gate = document.get("human_gate", {})
        if route == "GATE":
            if document.get("resume_route") not in {"LIGHT", "DEEP"}:
                add(errors, "RESULT.GATE", "$.resume_route", "GATE must name LIGHT or DEEP as the resume route")
            if gate.get("status") not in {"pending", "denied"}:
                add(errors, "RESULT.GATE", "$.human_gate.status", "GATE must remain pending or denied")
            if not gate.get("action_digest") or not gate.get("action_summary"):
                add(errors, "RESULT.GATE", "$.human_gate", "GATE must bind an exact action digest and summary")
        elif document.get("resume_route") is not None:
            add(errors, "RESULT.RESUME", "$.resume_route", "non-GATE results must use null")
    else:
        add(errors, "ENVELOPE.KIND", "$.kind", "must be request or result")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("root", type=Path, help="Built distribution root")
    parser.add_argument("--envelope", type=Path, help="Optional request or result JSON to validate")
    args = parser.parse_args()
    errors: list[dict[str, str]] = []
    validate_distribution(args.root.resolve(), errors)
    if args.envelope:
        try:
            validate_envelope(load_json(args.envelope.resolve()), errors)
        except (OSError, json.JSONDecodeError) as exc:
            add(errors, "ENVELOPE.READ", "$", str(exc))
    payload = {"valid": not errors, "protocol": PROTOCOL, "errors": sorted(errors, key=lambda item: (item["code"], item["path"], item["message"]))}
    print(json.dumps(payload, ensure_ascii=False, indent=2))
    return 0 if not errors else 1


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