#!/usr/bin/env python3
"""Verifiability Score (V) - Receipt Trust Quantification

Computes V ∈ [0,1] for receipts and time windows, where:
- V = 1.0: Perfect verifiability (all proofs present, timely, signed, chained)
- V = 0.0: Zero verifiability (no proofs, unsigned, stale)

Formula:
    V = Σ(w_i · evidence_i) - Σ(penalty_j)
    Clamped to [0, 1]

Evidence Components (additive):
- signature_ok: JCS + Ed25519 signature valid
- chain_ok: Receipt chain continuity (prev_hash links)
- replay_ok: Replay bundle present and verifiable
- timely_ok: Receipt fresh (within window threshold)
- witnesses_ok: External witnesses/quorum present

Penalties (subtractive):
- redaction_without_receipt: Redacted fields without RedactionReceipt
- secret_flag: Secrets detected in receipt
- schema_warn: Schema validation warnings
- missing_proof: Expected proof missing (e.g., DIP for prophecy)

Usage:
    from receipts.verifiability import vscore_receipt, aggregate_window

    inputs = VInputs(
        signature_ok=True,
        chain_ok=True,
        replay_ok=True,
        timely_ok=True,
        witnesses_ok=False,
        penalties={"secret_flag": False}
    )

    weights = {"signature_ok": 0.25, "chain_ok": 0.20, ...}
    penalties = {"secret_flag": 0.10, ...}

    v = vscore_receipt(inputs, weights, penalties)
    # v = 0.85 (high verifiability)

    # Window aggregation
    window_v = aggregate_window([0.85, 0.90, 0.88, 0.92])
    # window_v = 0.89 (mean)

Configuration:
    Default weights/penalties in config/gatebook.yaml:
    ```yaml
    trustmark:
      vscore:
        weights:
          signature_ok: 0.25
          chain_ok: 0.20
          replay_ok: 0.20
          timely_ok: 0.15
          witnesses_ok: 0.10
        penalties:
          redaction_without_receipt: 0.05
          secret_flag: 0.10
          schema_warn: 0.05
          missing_proof: 0.10
    ```
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Dict, Iterable


@dataclass(frozen=True)
class VInputs:
    """Verifiability evidence for a single receipt."""

    signature_ok: bool  # JCS + Ed25519 valid
    chain_ok: bool      # prev_hash continuity
    replay_ok: bool     # Replay bundle verifiable
    timely_ok: bool     # Fresh (within window)
    witnesses_ok: bool  # External witnesses present
    penalties: Dict[str, bool]  # Fired penalties


def vscore_receipt(
    inputs: VInputs,
    weights: Dict[str, float],
    penalties: Dict[str, float]
) -> float:
    """Compute verifiability score for a single receipt.

    Args:
        inputs: Evidence and penalty flags
        weights: Evidence component weights (sum should be ≤1.0)
        penalties: Penalty magnitudes

    Returns:
        V-score in [0, 1]

    Examples:
        >>> inputs = VInputs(
        ...     signature_ok=True, chain_ok=True, replay_ok=True,
        ...     timely_ok=True, witnesses_ok=False,
        ...     penalties={"secret_flag": False}
        ... )
        >>> weights = {
        ...     "signature_ok": 0.25, "chain_ok": 0.20, "replay_ok": 0.20,
        ...     "timely_ok": 0.15, "witnesses_ok": 0.10
        ... }
        >>> penalties_cfg = {"secret_flag": 0.10}
        >>> vscore_receipt(inputs, weights, penalties_cfg)
        0.8
    """
    # Evidence (additive)
    score = 0.0
    score += weights.get("signature_ok", 0.0) * float(inputs.signature_ok)
    score += weights.get("chain_ok", 0.0) * float(inputs.chain_ok)
    score += weights.get("replay_ok", 0.0) * float(inputs.replay_ok)
    score += weights.get("timely_ok", 0.0) * float(inputs.timely_ok)
    score += weights.get("witnesses_ok", 0.0) * float(inputs.witnesses_ok)

    # Penalties (subtractive)
    for name, fired in inputs.penalties.items():
        if fired:
            penalty_amount = penalties.get(name, 0.0)
            score -= penalty_amount

    # Clamp to [0, 1]
    return max(0.0, min(1.0, round(score, 3)))


def timely_ok(ts_iso: str, timely_hours: int) -> bool:
    """Check if receipt timestamp is within timely window.

    Args:
        ts_iso: ISO 8601 timestamp (e.g., "2025-11-06T12:34:56Z")
        timely_hours: Maximum age in hours

    Returns:
        True if receipt age ≤ timely_hours

    Examples:
        >>> timely_ok("2025-11-06T12:00:00Z", 24)
        True  # If now is within 24 hours

        >>> timely_ok("2025-11-01T12:00:00Z", 24)
        False  # More than 24 hours old
    """
    try:
        # Handle both Z and +00:00 timezone formats
        ts = ts_iso.replace("Z", "+00:00")
        dt = datetime.fromisoformat(ts)

        # Ensure timezone-aware
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)

        age = datetime.now(timezone.utc) - dt
        return age <= timedelta(hours=timely_hours)

    except Exception:
        # Invalid timestamp format
        return False


def aggregate_window(vscores: Iterable[float]) -> float:
    """Aggregate receipt V-scores into window V-score.

    Currently uses simple mean. Future: weighted by importance,
    outlier detection, temporal decay, etc.

    Args:
        vscores: Individual receipt V-scores

    Returns:
        Window V-score in [0, 1]

    Examples:
        >>> aggregate_window([0.85, 0.90, 0.88, 0.92])
        0.888

        >>> aggregate_window([])
        0.0
    """
    scores = list(vscores)

    if not scores:
        return 0.0

    mean = sum(scores) / len(scores)

    # Clamp to [0, 1]
    return max(0.0, min(1.0, round(mean, 3)))


def vscore_to_status(v: float, thresholds: Dict[str, float]) -> str:
    """Convert V-score to status (PASS/WARN/FAIL).

    Args:
        v: V-score in [0, 1]
        thresholds: Status thresholds {"green": 0.85, "yellow": 0.70}

    Returns:
        Status string: "PASS", "WARN", or "FAIL"

    Examples:
        >>> vscore_to_status(0.90, {"green": 0.85, "yellow": 0.70})
        'PASS'

        >>> vscore_to_status(0.75, {"green": 0.85, "yellow": 0.70})
        'WARN'

        >>> vscore_to_status(0.65, {"green": 0.85, "yellow": 0.70})
        'FAIL'
    """
    green = thresholds.get("green", 0.85)
    yellow = thresholds.get("yellow", 0.70)

    if v >= green:
        return "PASS"
    elif v >= yellow:
        return "WARN"
    else:
        return "FAIL"


# Default configuration (fallback if gatebook missing)
DEFAULT_WEIGHTS = {
    "signature_ok": 0.25,
    "chain_ok": 0.20,
    "replay_ok": 0.20,
    "timely_ok": 0.15,
    "witnesses_ok": 0.10,
}

DEFAULT_PENALTIES = {
    "redaction_without_receipt": 0.05,
    "secret_flag": 0.10,
    "schema_warn": 0.05,
    "missing_proof": 0.10,
}

DEFAULT_THRESHOLDS = {
    "green": 0.85,   # PASS
    "yellow": 0.70,  # WARN
    "red": 0.00,     # FAIL
}
