"""
verify.py — Ed25519 receipt verification for Headless Oracle V5.

Usage:
    from headless_oracle import verify, OracleClient

    client = OracleClient()
    receipt = client.get_demo("XNYS")
    result = verify(receipt)
    if not result.valid:
        raise RuntimeError(f"Receipt invalid: {result.reason}")
    if receipt["status"] != "OPEN":
        raise RuntimeError("Market is not open — halting")
"""

from __future__ import annotations

import json
import time
from dataclasses import dataclass
from typing import Any, Literal

import httpx
from nacl.signing import VerifyKey
from nacl.encoding import HexEncoder
from nacl.exceptions import BadSignatureError

# Fields that must be present for a basic market receipt.
_REQUIRED_MARKET = {"receipt_id", "issued_at", "expires_at", "issuer", "mic",
                    "status", "source", "receipt_mode", "schema_version",
                    "public_key_id", "signature"}

# Fields that must be present for a health receipt.
_REQUIRED_HEALTH = {"receipt_id", "issued_at", "expires_at", "issuer",
                    "status", "source", "public_key_id", "signature"}

# The exact set of fields included in the health receipt's canonical signing payload.
# The /v5/health response also contains unsigned envelope fields (exchange_count,
# supported_mics, data_coverage, edge_case_count_current_year) which must be
# excluded before signature verification. Source: /v5/keys → health_fields.
_HEALTH_SIGNED_FIELDS = {"expires_at", "issued_at", "issuer", "public_key_id",
                         "receipt_id", "source", "status"}

FailReason = Literal[
    "MISSING_FIELDS",
    "EXPIRED",
    "UNKNOWN_KEY",
    "INVALID_SIGNATURE",
    "KEY_FETCH_FAILED",
    "INVALID_KEY_FORMAT",
]


@dataclass
class VerifyResult:
    valid: bool
    reason: FailReason | None = None


def _canonical_payload(receipt: dict[str, Any]) -> bytes:
    """Reconstruct the canonical signing payload (signed fields only, sorted alphabetically).

    Market receipts: all fields except 'signature' are signed.
    Health receipts (source=SYSTEM, no mic): only _HEALTH_SIGNED_FIELDS are signed.
    The /v5/health response includes unsigned envelope fields (exchange_count, etc.)
    that must be excluded; otherwise the signature will not verify.
    """
    is_health = receipt.get("source") == "SYSTEM" and "mic" not in receipt
    if is_health:
        payload = {k: v for k, v in receipt.items() if k in _HEALTH_SIGNED_FIELDS}
    else:
        payload = {k: v for k, v in receipt.items() if k != "signature"}
    return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")


def verify(
    receipt: dict[str, Any],
    *,
    public_key: str | None = None,
    keys_url: str = "https://headlessoracle.com/v5/keys",
    now: float | None = None,
    timeout: float = 10.0,
) -> VerifyResult:
    """
    Verify a signed Headless Oracle receipt.

    Args:
        receipt:    The receipt dict (parsed JSON) from any Oracle endpoint.
        public_key: Hex-encoded Ed25519 public key. If omitted, fetched from keys_url.
        keys_url:   Override for the key registry URL (useful for staging/self-hosted).
        now:        Unix timestamp to use as "now" (for testing). Defaults to real time.
        timeout:    HTTP timeout in seconds when fetching the public key.

    Returns:
        VerifyResult(valid=True) on success.
        VerifyResult(valid=False, reason=<code>) on failure.

    Fail reasons:
        MISSING_FIELDS    — receipt is missing required fields
        EXPIRED           — receipt.expires_at is in the past
        UNKNOWN_KEY       — public_key_id not found in key registry
        INVALID_SIGNATURE — signature does not match canonical payload
        KEY_FETCH_FAILED  — could not retrieve public key from registry
        INVALID_KEY_FORMAT — public key is not valid hex or wrong length
    """
    # ── 1. Required fields ────────────────────────────────────────────────────
    is_health = receipt.get("source") == "SYSTEM" and "mic" not in receipt
    required = _REQUIRED_HEALTH if is_health else _REQUIRED_MARKET
    if not required.issubset(receipt.keys()):
        return VerifyResult(valid=False, reason="MISSING_FIELDS")

    # ── 2. TTL check ──────────────────────────────────────────────────────────
    try:
        from datetime import datetime, timezone
        expires_at = datetime.fromisoformat(receipt["expires_at"].replace("Z", "+00:00"))
        current_ts = now if now is not None else time.time()
        current_dt = datetime.fromtimestamp(current_ts, tz=timezone.utc)
        if current_dt > expires_at:
            return VerifyResult(valid=False, reason="EXPIRED")
    except (ValueError, KeyError):
        return VerifyResult(valid=False, reason="MISSING_FIELDS")

    # ── 3. Resolve public key ─────────────────────────────────────────────────
    hex_key = public_key
    if hex_key is None:
        try:
            resp = httpx.get(keys_url, timeout=timeout)
            resp.raise_for_status()
            keys_data = resp.json()
            key_id = receipt["public_key_id"]
            # Registry format: { keys: [{ key_id, public_key, ... }] }
            keys_list = keys_data.get("keys", [])
            match = next((k for k in keys_list if k.get("key_id") == key_id), None)
            if match is None:
                return VerifyResult(valid=False, reason="UNKNOWN_KEY")
            hex_key = match["public_key"]
        except Exception:
            return VerifyResult(valid=False, reason="KEY_FETCH_FAILED")

    # ── 4. Decode public key ──────────────────────────────────────────────────
    try:
        verify_key = VerifyKey(hex_key.encode(), encoder=HexEncoder)
    except Exception:
        return VerifyResult(valid=False, reason="INVALID_KEY_FORMAT")

    # ── 5. Verify signature ───────────────────────────────────────────────────
    try:
        sig_bytes = bytes.fromhex(receipt["signature"])
        payload = _canonical_payload(receipt)
        verify_key.verify(payload, sig_bytes)
    except (BadSignatureError, ValueError):
        return VerifyResult(valid=False, reason="INVALID_SIGNATURE")

    return VerifyResult(valid=True)
