"""Evidence Fabric Sample Package -- sign-and-verify build script.

SPDX-License-Identifier: MIT

Generates a synthetic Ed25519 keypair, signs the sample receipt + manifest,
computes artifact hashes, writes public_key.json, runs the verifier, and
captures a transcript.

Usage:
  python sign_and_verify.py              # default: sign + verify + transcript
  python sign_and_verify.py --sign       # sign only (no verify)
  python sign_and_verify.py --verify     # verify only (assume signatures present)
  python sign_and_verify.py --help

This is a BUILD-TIME script. It must be run once per package to populate
the PLACEHOLDER fields in receipt.json and manifest.json with real
cryptographic signatures.

The synthetic private key (SYNTHETIC_PRIVATE_KEY_HEX below) is intentionally
hard-coded so that anyone, anywhere, can re-derive the public key and
re-sign the artifacts. This is appropriate ONLY because the data is
synthetic. In production, the operator generates and protects their own key.

Dependencies: `cryptography` (>=41.0). Python 3.9+.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict


# ---------------------------------------------------------------------------
# Synthetic key material (SAMPLE-ONLY; do not use in production)
# ---------------------------------------------------------------------------

SYNTHETIC_PRIVATE_KEY_HEX = (
    "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
)
SYNTHETIC_SIGNER_KEY_ID = "vke_synthetic_2026-05"

# A second synthetic keypair, present only so the published keyring has a
# retired entry to exercise historical verification across key rotation.
# Nothing in this package is signed with it.
SYNTHETIC_RETIRED_PRIVATE_KEY_HEX = (
    "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"
)
SYNTHETIC_RETIRED_SIGNER_KEY_ID = "vke_synthetic_2026-01"

SYNTHETIC_SESSION_ID = "sess_synthetic_clinic_001"
SYNTHETIC_TENANT_ID = "tenant_synthetic_demo"
JCS_CANONICALIZATION_VERSION = "rfc8785-jcs-v1"
CHAIN_HEAD_SENTINEL = "0" * 64
# Manifest/transcript fixed-point ceiling. Two passes settle a deterministic
# run; more than that means something in the verifier's output is not
# reproducible and the manifest must not pretend otherwise.
MAX_TRANSCRIPT_PASSES = 4


# ---------------------------------------------------------------------------
# Canonical-JSON helper (must match verifier's canonicalization)
# ---------------------------------------------------------------------------

def canonical_json(obj: Any) -> bytes:
    """Canonicalize under the frozen legacy format `ve-canonical-json-v1`.

    Rules: sort_keys=True, no whitespace, UTF-8 encoded, ensure_ascii=False.
    Used for the artifacts that carry no `canonicalization_version` field --
    receipt.json and manifest.json -- so their historical signatures stay
    reproducible byte-for-byte.
    """
    return json.dumps(
        obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False
    ).encode("utf-8")


def canonical_json_jcs(obj: Any) -> bytes:
    """Canonicalize under RFC 8785 JCS, for artifacts stamped rfc8785-jcs-v1."""
    try:
        import rfc8785
    except ImportError:
        sys.stderr.write(
            "ERROR: the `rfc8785` package is required to sign the session export.\n"
            "       Install: pip install rfc8785\n"
        )
        sys.exit(2)
    return rfc8785.dumps(obj)


def sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


# ---------------------------------------------------------------------------
# Key derivation
# ---------------------------------------------------------------------------

def load_synthetic_keypair(private_key_hex: str = SYNTHETIC_PRIVATE_KEY_HEX):
    """Return (private_key, public_key, public_key_hex) for a synthetic key."""
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import (
            Ed25519PrivateKey,
        )
        from cryptography.hazmat.primitives.serialization import (
            Encoding,
            PublicFormat,
        )
    except ImportError:
        sys.stderr.write(
            "ERROR: the `cryptography` package is required.\n"
            "       Install: pip install cryptography\n"
        )
        sys.exit(2)

    private_bytes = bytes.fromhex(private_key_hex)
    private_key = Ed25519PrivateKey.from_private_bytes(private_bytes)
    public_key = private_key.public_key()
    public_bytes = public_key.public_bytes(
        Encoding.Raw, PublicFormat.Raw
    )
    return private_key, public_key, public_bytes.hex()


# ---------------------------------------------------------------------------
# Signing
# ---------------------------------------------------------------------------

def sign_receipt(receipt_path: Path, private_key, public_key_hex: str) -> Dict[str, Any]:
    """Sign the receipt in place. Returns the signed receipt dict."""
    with receipt_path.open("r", encoding="utf-8") as f:
        receipt = json.load(f)

    # Fill input/policy/payload/response hashes with derived placeholders.
    # These are not load-bearing for verification (verification signs the
    # whole receipt body); they are illustrative of where real hashes go.
    receipt["request"]["input_hash_sha256"] = sha256_hex(
        b"synthetic_clinical_input_487_tokens"
    )
    # The session export binds every receipt to its session AND its tenant, so
    # a receipt has to carry both or it cannot be admitted to a bound export.
    # session_id is authored in receipt.json; the tenant is stamped from the
    # same constant the chain events use, so the fixture and the binding
    # contract cannot drift apart.
    receipt["request"]["session_id"] = SYNTHETIC_SESSION_ID
    receipt["request"]["tenant_id"] = SYNTHETIC_TENANT_ID
    receipt["policy"]["policy_hash_sha256"] = sha256_hex(
        canonical_json(receipt["policy"]["rules_fired"])
    )
    receipt["provider_routing"]["upstream_payload_hash_sha256"] = sha256_hex(
        b"synthetic_redacted_payload_after_policy"
    )
    receipt["response"]["response_hash_sha256"] = sha256_hex(
        b"synthetic_response_312_tokens"
    )
    receipt["verification"]["transparency_log_inclusion_proof"] = (
        "c2FtcGxlX2luY2x1c2lvbl9wcm9vZl9zeW50aGV0aWM="
    )
    # The field is schema-present and roadmap-backed: no transparency log
    # operates behind this sample, so the value is a synthetic placeholder and
    # the sibling status field says so in the artifact itself.
    receipt["verification"]["transparency_log_inclusion_proof_status"] = "roadmap"

    # Strip the signature field for canonicalization
    receipt["verification"]["signature"] = ""
    canonical = canonical_json(receipt)
    signature_bytes = private_key.sign(canonical)
    signature_hex = "ed25519:" + signature_bytes.hex()
    receipt["verification"]["signature"] = signature_hex

    with receipt_path.open("w", encoding="utf-8", newline="\n") as f:
        json.dump(receipt, f, indent=2, ensure_ascii=False)
        f.write("\n")

    return receipt


def sign_manifest(
    manifest_path: Path,
    package_dir: Path,
    private_key,
    public_key_hex: str,
) -> Dict[str, Any]:
    """Compute artifact hashes, sign manifest, write back."""
    with manifest_path.open("r", encoding="utf-8") as f:
        manifest = json.load(f)

    # Fill each artifact's sha256 + bytes
    for artifact in manifest["artifacts"]:
        artifact_path = package_dir / artifact["path"]
        if artifact_path.exists():
            data = artifact_path.read_bytes()
            artifact["sha256"] = sha256_hex(data)
            artifact["bytes"] = len(data)
        else:
            artifact["sha256"] = "MISSING_FILE"
            artifact["bytes"] = 0

    manifest["signer_public_key_hex"] = public_key_hex

    # Strip signature for canonicalization
    manifest["manifest_signature"] = ""
    canonical = canonical_json(manifest)
    signature_bytes = private_key.sign(canonical)
    manifest["manifest_signature"] = "ed25519:" + signature_bytes.hex()

    with manifest_path.open("w", encoding="utf-8", newline="\n") as f:
        json.dump(manifest, f, indent=2, ensure_ascii=False)
        f.write("\n")

    return manifest


def write_public_key_file(public_key_hex: str, out_path: Path) -> None:
    public_key_doc = {
        "alg": "ed25519",
        "signer_key_id": SYNTHETIC_SIGNER_KEY_ID,
        "public_key_hex": public_key_hex,
        "synthetic": True,
        "synthetic_notice": (
            "This is a SYNTHETIC public key (matching the synthetic private key "
            "in sign_and_verify.py) for sample-package verification only. "
            "In production, the public key is the operator's and is distributed "
            "via the operator's gateway, not via this sample."
        ),
    }
    with out_path.open("w", encoding="utf-8", newline="\n") as f:
        json.dump(public_key_doc, f, indent=2, ensure_ascii=False)
        f.write("\n")


def write_public_keyring_file(
    public_key_hex: str, retired_public_key_hex: str, out_path: Path
) -> None:
    """Write the public-only keyring: one active key, one retired key.

    The keyring exists so `--public-keyring-file` can be exercised against this
    package. It is an externally pinned input: the verifier checks its shape and
    binds each signed object to the key that object names, but the keyring's own
    authenticity comes from wherever the auditor pinned it, not from this file.
    """
    keyring = {
        "schema_version": "1.0.0",
        "keys": [
            {
                "alg": "ed25519",
                "key_id": SYNTHETIC_RETIRED_SIGNER_KEY_ID,
                "public_key_hex": retired_public_key_hex,
                "status": "retired",
                "valid_from": "2026-01-01T00:00:00Z",
                "valid_until": "2026-05-01T00:00:00Z",
            },
            {
                "alg": "ed25519",
                "key_id": SYNTHETIC_SIGNER_KEY_ID,
                "public_key_hex": public_key_hex,
                "status": "active",
                "valid_from": "2026-05-01T00:00:00Z",
                "valid_until": None,
            },
        ],
    }
    with out_path.open("w", encoding="utf-8", newline="\n") as f:
        json.dump(keyring, f, indent=2, ensure_ascii=False)
        f.write("\n")


def sign_session_export(
    export_path: Path, receipt: Dict[str, Any], private_key
) -> Dict[str, Any]:
    """Build and sign the synthetic three-event session export.

    Every event and the envelope declare `rfc8785-jcs-v1`, so this fixture
    exercises the JCS path end to end. EVERY payload carries a float
    (`elapsed_ms`) on purpose: JCS writes 12.0 as 12 where the legacy format
    keeps the trailing ".0", so no event can pass under the wrong
    canonicalization. A fixture of integers and strings alone would
    canonicalize identically under both schemes and would verify even against
    a verifier that ignored `canonicalization_version` entirely -- a gate that
    can never fail is not a gate.
    """
    payloads = [
        {
            "event_type": "request_received",
            "request_id": "req_synthetic_0001",
            "received_utc": "2026-05-18T14:32:11.234Z",
            "elapsed_ms": 0.0,
            "input_token_count": 487,
            "data_class_count": 4,
        },
        {
            "event_type": "policy_applied",
            "policy_version": "v2026.05.18-h2",
            "elapsed_ms": 12.0,
            "rule_count": 3,
            "policy_hash_sha256": receipt["policy"]["policy_hash_sha256"],
        },
        {
            "event_type": "response_recorded",
            "elapsed_ms": 1847.0,
            "completion_token_count": 312,
            "latency_ms": 1847.0,
            "response_hash_sha256": receipt["response"]["response_hash_sha256"],
        },
    ]

    events = []
    prev_event_hash = CHAIN_HEAD_SENTINEL
    for sequence, payload in enumerate(payloads):
        event = {
            "sequence": sequence,
            "session_id": SYNTHETIC_SESSION_ID,
            "tenant_id": SYNTHETIC_TENANT_ID,
            "canonicalization_version": JCS_CANONICALIZATION_VERSION,
            "signing_key_id": SYNTHETIC_SIGNER_KEY_ID,
            "prev_event_hash": prev_event_hash,
            "payload": payload,
            "metadata_hash": sha256_hex(canonical_json_jcs(payload)),
        }
        event["event_signature"] = "ed25519:" + private_key.sign(
            canonical_json_jcs(event)
        ).hex()
        events.append(event)
        # The next link commits to this event minus its own signature.
        body_for_hash = {k: v for k, v in event.items() if k != "event_signature"}
        prev_event_hash = sha256_hex(canonical_json_jcs(body_for_hash))

    tip_for_hash = {k: v for k, v in events[-1].items() if k != "event_signature"}
    bundle = {
        "package_id": "pkg_synthetic_session_export_v1",
        "schema_version": "1.0.0",
        "canonicalization_version": JCS_CANONICALIZATION_VERSION,
        "synthetic": True,
        "synthetic_notice": (
            "SYNTHETIC session export. The three chain events and this envelope are "
            "signed with the synthetic key vke_synthetic_2026-05, whose private half is "
            "exposed in sign_and_verify.py so anyone can re-derive and re-sign. The "
            "embedded receipt is the same synthetic receipt shipped as receipt.json and "
            "carries no canonicalization_version, so it verifies under the frozen legacy "
            "format while the chain verifies under RFC 8785 JCS."
        ),
        "session_id": SYNTHETIC_SESSION_ID,
        "tenant_id": SYNTHETIC_TENANT_ID,
        "signing_key_id": SYNTHETIC_SIGNER_KEY_ID,
        "manifest_file": "manifest.json",
        "export_generated_utc": "2026-05-18T14:35:00.000Z",
        "chain": {
            # The chain WRAPPER carries the tenant as well as the session. The
            # verifier's binding contract requires every layer that can carry
            # identity to carry it (see verifier_cli.py's module docstring): a
            # wrapper that named only the session could not honestly be reported
            # as declaring the export's tenant.
            "session_id": SYNTHETIC_SESSION_ID,
            "tenant_id": SYNTHETIC_TENANT_ID,
            "count": len(events),
            "events": events,
            "anchor": {
                "algorithm": "sha256-rfc8785-jcs-v1",
                "event_count": len(events),
                "last_event_hash": sha256_hex(canonical_json_jcs(tip_for_hash)),
            },
        },
        "receipts": [receipt],
    }
    bundle["signature"] = "ed25519:" + private_key.sign(canonical_json_jcs(bundle)).hex()

    with export_path.open("w", encoding="utf-8", newline="\n") as f:
        json.dump(bundle, f, indent=2, ensure_ascii=False)
        f.write("\n")

    return bundle


# ---------------------------------------------------------------------------
# Verification (runs verifier_cli.py via subprocess and captures transcript)
# ---------------------------------------------------------------------------

def run_verifier_and_capture(
    package_dir: Path, transcript_path: Path
) -> int:
    verifier_path = package_dir / "verifier_cli.py"
    receipt_path = package_dir / "receipt.json"
    public_key_path = package_dir / "public_key.json"

    if not verifier_path.exists():
        sys.stderr.write(
            f"ERROR: verifier not found at {verifier_path}\n"
        )
        return 2

    runs = [
        ["--receipt", "receipt.json", "--public-key-file", "public_key.json"],
        ["--manifest", "manifest.json", "--public-key-file", "public_key.json"],
        ["--chain", "session_export.json", "--public-key-file", "public_key.json"],
        ["--session-export", "session_export.json", "--public-key-file", "public_key.json"],
        ["--session-export", "session_export.json", "--public-keyring-file", "public_keyring.json"],
        ["--self-test"],
    ]
    sections = []
    worst = 0
    for run_args in runs:
        result = subprocess.run(
            [sys.executable, verifier_path.name, *run_args],
            cwd=package_dir,
            capture_output=True,
            text=True,
            encoding="utf-8",
        )
        worst = max(worst, result.returncode)
        section = f"$ python verifier_cli.py {' '.join(run_args)}\n\n{result.stdout}"
        if result.stderr:
            section += f"\n[stderr]\n{result.stderr}"
        section += f"\nExit code: {result.returncode}\n"
        sections.append(section)

    transcript = (
        "\n" + "-" * 72 + "\n\n"
    ).join(sections)
    transcript += (
        f"\nNote: This is a SAMPLE evidence package using synthetic data "
        f"and a synthetic signer key.\n"
        f"This sample does not establish any production signing arrangement.\n"
    )
    transcript_path.write_text(transcript, encoding="utf-8", newline="\n")
    return worst


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> int:
    parser = argparse.ArgumentParser(
        description="Sign + verify the Evidence Fabric sample package."
    )
    parser.add_argument(
        "--sign", action="store_true", help="Sign only (skip verification)"
    )
    parser.add_argument(
        "--verify", action="store_true", help="Verify only (skip signing)"
    )
    args = parser.parse_args()

    do_sign = args.sign or not args.verify
    do_verify = args.verify or not args.sign

    package_dir = Path(__file__).parent.resolve()
    receipt_path = package_dir / "receipt.json"
    manifest_path = package_dir / "manifest.json"
    public_key_path = package_dir / "public_key.json"
    keyring_path = package_dir / "public_keyring.json"
    export_path = package_dir / "session_export.json"
    transcript_path = package_dir / "verifier_transcript.txt"

    if do_sign:
        print(f"[sign] loading synthetic keypair: {SYNTHETIC_SIGNER_KEY_ID}")
        private_key, _public_key, public_key_hex = load_synthetic_keypair()
        print(f"[sign] public_key_hex: {public_key_hex}")
        retired_public_key_hex = load_synthetic_keypair(
            SYNTHETIC_RETIRED_PRIVATE_KEY_HEX
        )[2]

        # Order matters: public_key.json + receipt.json BOTH must be
        # at their final byte content BEFORE manifest is signed, because
        # manifest.json embeds SHA-256 of each artifact. Writing them after
        # the manifest signature would invalidate the manifest's artifact
        # hashes (caught by cycle-1 self-review 2026-05-19).
        print(f"[sign] writing public key: {public_key_path.name}")
        write_public_key_file(public_key_hex, public_key_path)

        print(f"[sign] writing public keyring: {keyring_path.name}")
        write_public_keyring_file(public_key_hex, retired_public_key_hex, keyring_path)

        print(f"[sign] signing receipt: {receipt_path.name}")
        receipt = sign_receipt(receipt_path, private_key, public_key_hex)

        print(f"[sign] signing session export: {export_path.name} (3-event JCS chain + embedded receipt)")
        sign_session_export(export_path, receipt, private_key)

    if do_sign and not do_verify:
        # Sign-only: the manifest commits to the transcript already on disk.
        # That is correct here because the transcript is deterministic and the
        # shipped one is already at the fixed point below, so --sign reproduces
        # the same manifest bytes.
        print(f"[sign] signing manifest: {manifest_path.name} (last; embeds hashes of all sibling artifacts)")
        sign_manifest(manifest_path, package_dir, private_key, public_key_hex)
        return 0

    if do_sign and do_verify:
        # The transcript is BOTH an output of the verifier run and an input to
        # the manifest that the same run verifies. Signing once cannot be
        # correct: the manifest would commit to a transcript the run it records
        # had not produced yet, and the package would ship claiming to manifest
        # every artifact while quietly excluding this one.
        #
        # Iterate to the fixed point instead: sign the manifest over the
        # transcript currently on disk, run the verifier against exactly that
        # pair (run_verifier_and_capture rewrites the transcript only after all
        # six runs finish, so the run sees the manifested bytes), and stop when
        # the run reproduces the transcript it was verified against. At that
        # point the manifest's hash and the file on disk are the same bytes.
        # With deterministic self-test keys this converges and then stays
        # converged, which is what makes the byte-reproducibility claim true for
        # all eleven files instead of ten.
        on_disk = transcript_path.read_bytes() if transcript_path.exists() else None
        rc = 1
        for attempt in range(1, MAX_TRANSCRIPT_PASSES + 1):
            print(f"[sign] signing manifest (pass {attempt}): {manifest_path.name} "
                  f"(last; embeds hashes of all sibling artifacts, transcript included)")
            sign_manifest(manifest_path, package_dir, private_key, public_key_hex)
            print(f"[verify] running verifier; transcript -> {transcript_path.name}")
            rc = run_verifier_and_capture(package_dir, transcript_path)
            produced = transcript_path.read_bytes()
            if produced == on_disk:
                print(f"[verify] transcript fixed point reached on pass {attempt}: "
                      f"the manifest commits to the bytes this run produced")
                break
            on_disk = produced
        else:
            sys.stderr.write(
                f"ERROR: the verifier transcript did not settle within "
                f"{MAX_TRANSCRIPT_PASSES} passes; something in the run is "
                f"nondeterministic and the manifest cannot commit to it.\n"
            )
            return 2
        print(f"[verify] verifier exit code: {rc}")
        return rc

    if do_verify:
        print(f"[verify] running verifier; transcript -> {transcript_path.name}")
        rc = run_verifier_and_capture(package_dir, transcript_path)
        print(f"[verify] verifier exit code: {rc}")
        return rc

    return 0


if __name__ == "__main__":
    sys.exit(main())
