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

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"


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

def canonical_json(obj: Any) -> bytes:
    """Canonicalize a JSON-serializable object for signing.

    Rules: sort_keys=True, no whitespace, UTF-8 encoded, ensure_ascii=False.
    Matches the verifier's reconstruction path exactly.
    """
    return json.dumps(
        obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False
    ).encode("utf-8")


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


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

def load_synthetic_keypair():
    """Return (private_key, public_key, public_key_hex) for the 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(SYNTHETIC_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"
    )
    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="
    )

    # 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")


# ---------------------------------------------------------------------------
# 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

    cmd = [
        sys.executable,
        str(verifier_path),
        "--receipt",
        str(receipt_path),
        "--public-key-file",
        str(public_key_path),
        "--verbose",
    ]
    result = subprocess.run(
        cmd, capture_output=True, text=True, encoding="utf-8"
    )
    transcript = (
        f"$ {' '.join(cmd)}\n\n"
        f"{result.stdout}\n"
    )
    if result.stderr:
        transcript += f"\n[stderr]\n{result.stderr}\n"
    transcript += (
        f"\nExit code: {result.returncode}\n"
        f"\nNote: This is a SAMPLE evidence package using synthetic data "
        f"and a synthetic signer key.\n"
        f"In production, the signer key is the operator's, not VE's.\n"
    )
    transcript_path.write_text(transcript, encoding="utf-8", newline="\n")
    return result.returncode


# ---------------------------------------------------------------------------
# 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"
    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}")

        # 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] signing receipt: {receipt_path.name}")
        sign_receipt(receipt_path, private_key, public_key_hex)

        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)

    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())
