#!/usr/bin/env python3
"""Offline verifier for TELOS public governance receipt chains.

WHAT THE SIGNATURE GUARANTEES
    Each Ed25519 signature covers only the receipt's signed_payload: runtime
    commit, event attributes, scores, signals, and fixture hashes. Changing a
    signed field makes the signature fail.

WHAT THE CHAIN GUARANTEES
    The outer ledger entry_hash is recomputed from the outer record, and each
    subsequent prev_hash must equal the prior record's recomputed entry hash.
    This makes deletion, reordering, or mutation evident when the chain is
    verified from genesis through head.

WHAT THIS DOES NOT GUARANTEE ON ITS OWN
    - Issuer identity. Pin the separately obtained key fingerprint with
      --expect-key.
    - The exact issued chain head file. Pin its separately obtained SHA-256
      with --expect-sha256. In chain mode this always pins the final receipt.
    - That an observed action was correct, safe, compliant, or production
      calibrated. These receipts use a public synthetic fixture.

Uses the repository's dependency-free RFC 8032 verifier. No network access is used.
"""

from __future__ import annotations

import argparse
import base64
import copy
import hashlib
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from ed25519_pure import verify as ed25519_verify

DEFAULT_PATH = Path("receipts")
RECEIPT_GLOB = "receipt_*.json"
GENESIS_HASH = "0" * 64
EXPECTED_PROOF_TYPE = "telos.public.governance_receipt.v1"
EXPECTED_CANONICALIZATION = (
    "utf8-json;sort_keys=true;separators=(',',':');"
    "ensure_ascii=false;sha256;ed25519"
)


class VerificationInputError(ValueError):
    """Raised when receipt input cannot be safely parsed."""


def _bool(value: bool) -> str:
    return "true" if value else "false"


def _reject_duplicate_pairs(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise VerificationInputError(f"duplicate JSON key: {key}")
        result[key] = value
    return result


def canonical_payload_bytes(signed_payload: Dict[str, Any]) -> bytes:
    """Re-derive the exact UTF-8 bytes covered by the signature."""
    return json.dumps(
        signed_payload,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")


def actual_entry_hash(receipt: Dict[str, Any]) -> str:
    """Recompute the outer ledger hash without trusting stored entry_hash."""
    outer = {key: value for key, value in receipt.items() if key != "entry_hash"}
    canonical = json.dumps(outer, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _load_exactly_one(path: Path) -> Dict[str, Any]:
    raw = path.read_bytes()
    lines = [line for line in raw.decode("utf-8").splitlines() if line.strip()]
    if len(lines) != 1:
        raise VerificationInputError(
            f"{path}: expected exactly one JSON record, found {len(lines)}"
        )
    try:
        receipt = json.loads(lines[0], object_pairs_hook=_reject_duplicate_pairs)
    except (json.JSONDecodeError, UnicodeDecodeError) as exc:
        raise VerificationInputError(f"{path}: invalid UTF-8 JSON: {exc}") from exc
    if not isinstance(receipt, dict):
        raise VerificationInputError(f"{path}: receipt must be a JSON object")
    return {"path": path, "raw": raw, "receipt": receipt}


def load_receipts(path: Path) -> Dict[str, Any]:
    """Load either one receipt file or one ordered receipt directory."""
    if path.is_file():
        return {"mode": "single", "items": [_load_exactly_one(path)]}
    if not path.is_dir():
        raise VerificationInputError(f"receipt path does not exist: {path}")
    files = sorted(path.glob(RECEIPT_GLOB))
    if not files:
        raise VerificationInputError(f"no {RECEIPT_GLOB} files found in {path}")
    return {"mode": "chain", "items": [_load_exactly_one(file) for file in files]}


def _verify_cryptographic_core(receipt: Dict[str, Any]) -> Dict[str, Any]:
    result = {
        "schema_valid": False,
        "canonical_payload_match": False,
        "payload_hash_valid": False,
        "signature_valid": False,
        "public_key": "",
        "key_fingerprint": "",
        "signed_payload_sha256": "",
        "error": "",
    }
    try:
        expected_outer = {
            "event",
            "timestamp",
            "sequence",
            "prev_hash",
            "data",
            "entry_hash",
        }
        if set(receipt) != expected_outer:
            raise VerificationInputError("outer receipt schema mismatch")
        if set(receipt["data"]) != {"governance_proof"}:
            raise VerificationInputError("outer data schema mismatch")

        proof = receipt["data"]["governance_proof"]
        expected_proof = {
            "type",
            "canonicalization",
            "signed_payload",
            "canonical_payload_b64",
            "signed_payload_sha256",
            "signing_payload_sha256",
            "verdict_signature",
            "public_key",
        }
        if not isinstance(proof, dict) or set(proof) != expected_proof:
            raise VerificationInputError("governance proof schema mismatch")
        if proof["type"] != EXPECTED_PROOF_TYPE:
            raise VerificationInputError("unexpected proof type")
        if proof["canonicalization"] != EXPECTED_CANONICALIZATION:
            raise VerificationInputError("unexpected canonicalization")

        payload = proof["signed_payload"]
        if not isinstance(payload, dict):
            raise VerificationInputError("signed_payload must be an object")
        canonical = canonical_payload_bytes(payload)
        carried = base64.b64decode(proof["canonical_payload_b64"], validate=True)
        result["canonical_payload_match"] = carried == canonical

        digest = hashlib.sha256(canonical).digest()
        digest_hex = digest.hex()
        result["signed_payload_sha256"] = digest_hex
        result["payload_hash_valid"] = (
            proof["signed_payload_sha256"] == digest_hex
            and proof["signing_payload_sha256"] == digest_hex
        )

        public_key = proof["public_key"]
        public_key_bytes = bytes.fromhex(public_key)
        signature = bytes.fromhex(proof["verdict_signature"])
        result["public_key"] = public_key
        result["key_fingerprint"] = hashlib.sha256(public_key_bytes).hexdigest()
        result["signature_valid"] = ed25519_verify(signature, digest, public_key_bytes)

        result["schema_valid"] = True
    except (KeyError, TypeError, ValueError, VerificationInputError) as exc:
        result["error"] = str(exc)
    return result


def evaluate_loaded(
    loaded: Dict[str, Any],
    expect_key: Optional[str] = None,
    expect_sha256: Optional[str] = None,
    tamper: bool = False,
) -> Dict[str, Any]:
    """Evaluate a loaded chain or single receipt without network access."""
    mode = loaded["mode"]
    items = copy.deepcopy(loaded["items"])
    if tamper:
        if mode != "chain" or len(items) < 3:
            raise VerificationInputError(
                "--tamper requires a chain directory containing at least 3 receipts"
            )
        target_index = 1
        payload = items[target_index]["receipt"]["data"]["governance_proof"][
            "signed_payload"
        ]
        old_value = payload["scores"]["fidelity"]
        payload["scores"]["fidelity"] = old_value + 0.01
        tamper_info = {
            "index": target_index,
            "file": str(items[target_index]["path"]),
            "field": "signed_payload.scores.fidelity",
            "old_value": old_value,
            "new_value": payload["scores"]["fidelity"],
        }
    else:
        tamper_info = None

    records: List[Dict[str, Any]] = []
    prior_actual_hash: Optional[str] = None
    prior_sequence: Optional[int] = None

    for index, item in enumerate(items):
        receipt = item["receipt"]
        crypto = _verify_cryptographic_core(receipt)
        try:
            computed_entry_hash = actual_entry_hash(receipt)
        except (TypeError, ValueError) as exc:
            computed_entry_hash = ""
            crypto["error"] = crypto["error"] or str(exc)
        stored_entry_hash = receipt.get("entry_hash", "")
        entry_hash_valid = computed_entry_hash == stored_entry_hash

        if mode == "chain":
            sequence = receipt.get("sequence")
            if index == 0:
                sequence_valid = sequence == 1
                link_valid = receipt.get("prev_hash") == GENESIS_HASH
                expected_prev_hash = GENESIS_HASH
            else:
                sequence_valid = (
                    isinstance(sequence, int)
                    and isinstance(prior_sequence, int)
                    and sequence == prior_sequence + 1
                )
                expected_prev_hash = prior_actual_hash or ""
                link_valid = receipt.get("prev_hash") == expected_prev_hash
        else:
            sequence = receipt.get("sequence")
            sequence_valid = True
            link_valid = None
            expected_prev_hash = None

        record_valid = all(
            (
                crypto["schema_valid"],
                crypto["canonical_payload_match"],
                crypto["payload_hash_valid"],
                crypto["signature_valid"],
                entry_hash_valid,
                sequence_valid,
            )
        ) and (link_valid is not False)

        records.append(
            {
                "index": index + 1,
                "path": str(item["path"]),
                "file_sha256": hashlib.sha256(item["raw"]).hexdigest(),
                "sequence": sequence,
                "prev_hash": receipt.get("prev_hash", ""),
                "expected_prev_hash": expected_prev_hash,
                "stored_entry_hash": stored_entry_hash,
                "actual_entry_hash": computed_entry_hash,
                "entry_hash_valid": entry_hash_valid,
                "sequence_valid": sequence_valid,
                "link_valid": link_valid,
                "valid": record_valid,
                **crypto,
            }
        )
        prior_actual_hash = computed_entry_hash
        prior_sequence = sequence if isinstance(sequence, int) else None

    fingerprints = {
        record["key_fingerprint"]
        for record in records
        if record["key_fingerprint"]
    }
    key_consistent = len(fingerprints) == 1 and all(
        bool(record["key_fingerprint"]) for record in records
    )
    actual_key = next(iter(fingerprints)) if len(fingerprints) == 1 else ""
    expected_key_valid = (
        True
        if expect_key is None
        else key_consistent and actual_key.lower() == expect_key.strip().lower()
    )

    head = records[-1]
    expected_head_sha256_valid = (
        True
        if expect_sha256 is None
        else head["file_sha256"].lower() == expect_sha256.strip().lower()
    )
    overall_valid = (
        all(record["valid"] for record in records)
        and key_consistent
        and expected_key_valid
        and expected_head_sha256_valid
    )
    return {
        "mode": mode,
        "records": records,
        "tamper": tamper_info,
        "key_consistent": key_consistent,
        "key_fingerprint": actual_key,
        "expected_key_supplied": expect_key is not None,
        "expected_key_valid": expected_key_valid,
        "expected_head_sha256_supplied": expect_sha256 is not None,
        "expected_head_sha256_valid": expected_head_sha256_valid,
        "chain_head_file": head["path"],
        "chain_head_entry_hash": head["actual_entry_hash"],
        "chain_head_file_sha256": head["file_sha256"],
        "valid": overall_valid,
    }


def print_result(result: Dict[str, Any]) -> None:
    print("TELOS public governance receipt verification")
    print(f"mode={result['mode']}")
    print(f"receipt_count={len(result['records'])}")
    if result["tamper"]:
        tamper = result["tamper"]
        print("tamper_demo=true")
        print(f"tamper_target={tamper['file']}")
        print(f"tamper_field={tamper['field']}")
        print(f"tamper_old_value={tamper['old_value']}")
        print(f"tamper_new_value={tamper['new_value']}")

    for record in result["records"]:
        prefix = f"receipt_{record['index']:02d}"
        print(f"{prefix}_file={record['path']}")
        print(f"{prefix}_sequence={record['sequence']}")
        print(f"{prefix}_schema_valid={_bool(record['schema_valid'])}")
        print(
            f"{prefix}_canonical_payload_match="
            f"{_bool(record['canonical_payload_match'])}"
        )
        print(f"{prefix}_payload_hash_valid={_bool(record['payload_hash_valid'])}")
        print(f"{prefix}_signature_valid={_bool(record['signature_valid'])}")
        print(f"{prefix}_entry_hash_valid={_bool(record['entry_hash_valid'])}")
        print(f"{prefix}_sequence_valid={_bool(record['sequence_valid'])}")
        if record["link_valid"] is None:
            print(f"{prefix}_prev_hash_matches_prior_actual_hash=not_checked_single_file")
        else:
            print(
                f"{prefix}_prev_hash_matches_prior_actual_hash="
                f"{_bool(record['link_valid'])}"
            )
        print(f"{prefix}_valid={_bool(record['valid'])}")
        if record["error"]:
            print(f"{prefix}_error={record['error']}")

    print(f"signing_key_consistent={_bool(result['key_consistent'])}")
    print(f"signing_key_fingerprint_sha256={result['key_fingerprint']}")
    if result["expected_key_supplied"]:
        print(f"expected_key_valid={_bool(result['expected_key_valid'])}")
    else:
        print("expected_key_valid=not_supplied")
    print(f"chain_head_file={result['chain_head_file']}")
    print(f"chain_head_entry_hash={result['chain_head_entry_hash']}")
    print(f"chain_head_file_sha256={result['chain_head_file_sha256']}")
    if result["expected_head_sha256_supplied"]:
        print(
            "expected_chain_head_sha256_valid="
            f"{_bool(result['expected_head_sha256_valid'])}"
        )
    else:
        print("expected_chain_head_sha256_valid=not_supplied")
    print("RESULT:", "VALID" if result["valid"] else "INVALID")


def run(
    path: Path,
    expect_key: Optional[str] = None,
    expect_sha256: Optional[str] = None,
    tamper: bool = False,
) -> int:
    try:
        loaded = load_receipts(path)
        result = evaluate_loaded(loaded, expect_key, expect_sha256, tamper)
    except (OSError, VerificationInputError) as exc:
        print(f"INPUT_ERROR: {exc}", file=sys.stderr)
        return 2
    print_result(result)
    return 0 if result["valid"] else 1


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Verify a TELOS public governance receipt chain offline."
    )
    parser.add_argument(
        "path",
        nargs="?",
        type=Path,
        default=DEFAULT_PATH,
        help="receipt directory (default: receipts) or one receipt file",
    )
    parser.add_argument(
        "--expect-key",
        default=None,
        help="expected signing-key fingerprint (SHA-256 of public key)",
    )
    parser.add_argument(
        "--expect-sha256",
        default=None,
        help="expected chain-head receipt SHA-256 (or selected file in single mode)",
    )
    parser.add_argument(
        "--tamper",
        action="store_true",
        help="mutate receipt 02 in memory; signature and downstream link must fail",
    )
    args = parser.parse_args()
    raise SystemExit(run(args.path, args.expect_key, args.expect_sha256, args.tamper))


if __name__ == "__main__":
    main()
