#!/usr/bin/env python3
"""Stdlib HMAC receipt-chain verifier for the public probe demo.

Verifies ReceiptEngine-compatible receipts (canonical JSON + HMAC-SHA256 +
prev_receipt_hash linking) using only the Python standard library.

Secrets are NEVER embedded. Provide the shared secret via:
  --secret / --secret-file / RECEIPTENGINE_HMAC_KEY / HUMMBL_SIGNING_SECRET

Use --structure-only to check schema + hash linking without HMAC (useful for
the live public probe when you do not hold the worker signing key).

This verifies probe/fixture receipts only. It does not settle KRINEIA naming
and is not public-key non-repudiation.
"""

from __future__ import annotations

import argparse
import hashlib
import hmac
import json
import os
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any

REQUIRED_FIELDS = (
    "receipt_id",
    "agent_id",
    "sequence_id",
    "prev_receipt_hash",
    "timestamp",
    "action_type",
    "payload",
    "law_checks",
    "violations",
    "evidence_grade",
    "signature",
)


@dataclass(frozen=True)
class CheckResult:
    ok: bool
    code: str
    detail: str


def canonical_json(receipt: dict[str, Any]) -> str:
    body = {key: value for key, value in receipt.items() if key != "signature"}
    return json.dumps(body, sort_keys=True, separators=(",", ":"))


def receipt_hash(receipt: dict[str, Any]) -> str:
    return hashlib.sha256(canonical_json(receipt).encode("utf-8")).hexdigest()


def verify_signature(receipt: dict[str, Any], secret: bytes) -> bool:
    expected = hmac.new(
        secret,
        canonical_json(receipt).encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    signature = receipt.get("signature", "")
    if not isinstance(signature, str):
        return False
    return hmac.compare_digest(signature, expected)


def load_bytes(source: str) -> bytes:
    if source == "-":
        return sys.stdin.buffer.read()
    if source.startswith("https://") or source.startswith("http://"):
        request = urllib.request.Request(
            source,
            headers={"User-Agent": "hummbl-verify-receipt/1.0"},
        )
        with urllib.request.urlopen(request, timeout=30) as response:  # noqa: S310
            return response.read()
    path = Path(source)
    return path.read_bytes()


def extract_receipts(payload: Any) -> list[dict[str, Any]]:
    if isinstance(payload, list):
        if not payload:
            raise ValueError("receipt list is empty")
        if not all(isinstance(item, dict) for item in payload):
            raise ValueError("receipt list must contain objects")
        return payload
    if not isinstance(payload, dict):
        raise ValueError("payload must be a JSON object or array")

    if isinstance(payload.get("receipts"), list):
        receipts = payload["receipts"]
        if not receipts:
            raise ValueError("receipts array is empty")
        if not all(isinstance(item, dict) for item in receipts):
            raise ValueError("receipts array must contain objects")
        return receipts

    live = payload.get("live_response")
    if isinstance(live, dict) and isinstance(live.get("receipt"), dict):
        return [live["receipt"]]

    if isinstance(payload.get("receipt"), dict):
        return [payload["receipt"]]

    if "receipt_id" in payload and "signature" in payload:
        return [payload]

    raise ValueError(
        "could not find receipts (expected receipts[], receipt, "
        "live_response.receipt, or a bare receipt object)"
    )


def check_structure(receipt: dict[str, Any], index: int) -> list[CheckResult]:
    results: list[CheckResult] = []
    missing = [field for field in REQUIRED_FIELDS if field not in receipt]
    if missing:
        results.append(
            CheckResult(
                False,
                "schema",
                f"receipt[{index}] missing fields: {', '.join(missing)}",
            )
        )
        return results
    if not isinstance(receipt["payload"], dict):
        results.append(
            CheckResult(False, "schema", f"receipt[{index}].payload must be an object")
        )
    if not isinstance(receipt["law_checks"], list):
        results.append(
            CheckResult(False, "schema", f"receipt[{index}].law_checks must be a list")
        )
    if not isinstance(receipt["violations"], list):
        results.append(
            CheckResult(False, "schema", f"receipt[{index}].violations must be a list")
        )
    if not isinstance(receipt["signature"], str) or len(receipt["signature"]) != 64:
        results.append(
            CheckResult(
                False,
                "schema",
                f"receipt[{index}].signature must be a 64-char hex digest",
            )
        )
    if not results:
        results.append(CheckResult(True, "schema", f"receipt[{index}] fields present"))
    return results


def verify_chain(
    receipts: list[dict[str, Any]],
    *,
    secret: bytes | None,
    structure_only: bool,
) -> list[CheckResult]:
    results: list[CheckResult] = []
    if not receipts:
        return [CheckResult(False, "empty", "no receipts to verify")]

    prev_hash = ""
    for index, receipt in enumerate(receipts):
        structural = check_structure(receipt, index)
        results.extend(structural)
        if any(not item.ok for item in structural):
            break

        linked = receipt.get("prev_receipt_hash") == prev_hash
        results.append(
            CheckResult(
                linked,
                "chain",
                (
                    f"receipt[{index}] prev_receipt_hash matches prior hash"
                    if linked
                    else (
                        f"receipt[{index}] prev_receipt_hash mismatch "
                        f"(expected {prev_hash!r}, got {receipt.get('prev_receipt_hash')!r})"
                    )
                ),
            )
        )
        if not linked:
            break

        if structure_only:
            prev_hash = receipt_hash(receipt)
            continue

        if secret is None:
            results.append(
                CheckResult(
                    False,
                    "secret",
                    "HMAC secret required (set --secret or RECEIPTENGINE_HMAC_KEY); "
                    "use --structure-only to skip signature checks",
                )
            )
            break

        signed = verify_signature(receipt, secret)
        results.append(
            CheckResult(
                signed,
                "hmac",
                (
                    f"receipt[{index}] HMAC-SHA256 signature valid"
                    if signed
                    else f"receipt[{index}] HMAC-SHA256 signature mismatch"
                ),
            )
        )
        if not signed:
            break
        prev_hash = receipt_hash(receipt)
    return results

def resolve_secret(args: argparse.Namespace) -> bytes | None:
    if args.structure_only:
        return None
    if args.secret is not None:
        return args.secret.encode("utf-8")
    if args.secret_file is not None:
        return Path(args.secret_file).read_bytes().strip()
    for env_name in ("RECEIPTENGINE_HMAC_KEY", "HUMMBL_SIGNING_SECRET"):
        value = os.environ.get(env_name)
        if value:
            return value.encode("utf-8")
    return None


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "source",
        help="Path, URL, or '-' for stdin. Accepts probe JSON, evidence JSON, "
        "or a receipts[] fixture.",
    )
    parser.add_argument(
        "--secret",
        help="HMAC shared secret (UTF-8). Prefer env var in shells that log argv.",
    )
    parser.add_argument(
        "--secret-file",
        help="Read HMAC secret bytes from a file (trailing newline stripped).",
    )
    parser.add_argument(
        "--structure-only",
        action="store_true",
        help="Validate schema + prev_receipt_hash linking without HMAC.",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Emit machine-readable results.",
    )
    args = parser.parse_args(argv)

    try:
        raw = load_bytes(args.source)
        payload = json.loads(raw.decode("utf-8"))
        receipts = extract_receipts(payload)
    except (OSError, urllib.error.URLError, json.JSONDecodeError, ValueError) as exc:
        print(f"FAIL load: {exc}", file=sys.stderr)
        return 2

    secret = resolve_secret(args)
    if not args.structure_only and secret is None:
        print(
            "FAIL secret: provide --secret / --secret-file / RECEIPTENGINE_HMAC_KEY "
            "or pass --structure-only",
            file=sys.stderr,
        )
        return 2

    results = verify_chain(
        receipts,
        secret=secret,
        structure_only=args.structure_only,
    )
    ok = all(item.ok for item in results)

    if args.json:
        print(
            json.dumps(
                {
                    "ok": ok,
                    "source": args.source,
                    "structure_only": args.structure_only,
                    "receipt_count": len(receipts),
                    "checks": [
                        {"ok": item.ok, "code": item.code, "detail": item.detail}
                        for item in results
                    ],
                    "assurance": (
                        "HMAC verifies integrity/authenticity inside a shared-secret "
                        "trust domain; not public-key non-repudiation. "
                        "Public probe scope is the cited surface/timestamp only."
                    ),
                },
                indent=2,
                sort_keys=True,
            )
        )
    else:
        mode = "structure-only" if args.structure_only else "hmac+chain"
        print(f"{'PASS' if ok else 'FAIL'} ({mode}) receipts={len(receipts)}")
        for item in results:
            mark = "ok" if item.ok else "FAIL"
            print(f"  [{mark}] {item.code}: {item.detail}")
        print(
            "Note: HMAC is shared-secret authenticity, not public-key non-repudiation."
        )

    return 0 if ok else 1


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