"""
CalibrationGateReceipt - Constitutional ECE Worst-Slice Gating for High-Trust Profiles

Implements the second constitutional glyph (ECE) as a hard gate:
- High-trust profiles MUST have calibration within bounds to proceed
- ECE worst-slice > threshold → DENY for high-trust, ADVISORY for others
- Coverage outside band → DENY for high-trust
- Receipt emitted for every gate evaluation (audit trail)

The ECE worst-slice gate protects vulnerable cohorts by ensuring the system
doesn't act when its calibration is degraded for any subpopulation.
"""
from __future__ import annotations

import logging
import os
import hashlib
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Optional, Set

from pydantic import Field

from receipts.compat.pyd import BaseModel, ConfigDictLike, model_validator
from receipts.base import BaseReceipt, Domain
from receipts.jcs import canonicalize

logger = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# Gate Status Enum
# -----------------------------------------------------------------------------

class CalibrationGateStatus(str, Enum):
    """
    Status of the calibration gate evaluation.
    
    ALLOW: ECE within bounds and coverage acceptable; proceed with action
    HOLD: Calibration is marginal; action deferred for review
    DENY: ECE or coverage out of bounds for high-trust; block action
    ADVISORY_ONLY: Gate evaluated but profile doesn't require hard gating
    """
    ALLOW = "allow"
    HOLD = "hold"
    DENY = "deny"
    ADVISORY_ONLY = "advisory_only"


# -----------------------------------------------------------------------------
# High-Trust Profile Constants (shared with coherence_gate)
# -----------------------------------------------------------------------------

# Core profiles that REQUIRE calibration bounds to act (constitutional minimum)
CORE_HIGH_TRUST_PROFILES: Set[str] = {
    "clinical_control",
    "clinical_assist", 
    "governance_control",
}

# Extended high-trust profiles (spine-level enforcement)
EXTENDED_HIGH_TRUST_PROFILES: Set[str] = {
    "court-grade",
    "clinical-grade",
    "regulated",
    "strict",
}

# Default set used by calibration gate
HIGH_TRUST_PROFILES: Set[str] = CORE_HIGH_TRUST_PROFILES.copy()

# Environment override to enable extended profiles
if os.getenv("LOOM_EXTENDED_HIGH_TRUST", "").lower() in ("1", "true", "yes"):
    HIGH_TRUST_PROFILES = CORE_HIGH_TRUST_PROFILES | EXTENDED_HIGH_TRUST_PROFILES


# Default thresholds by profile
DEFAULT_ECE_MAX: Dict[str, float] = {
    "clinical_control": 0.03,   # Strict: tight calibration required
    "clinical_assist": 0.05,    # Moderate: allow some slack
    "governance_control": 0.04, # Middle ground
    "court-grade": 0.02,        # Court-grade needs tightest calibration
    "clinical-grade": 0.03,     # Same as clinical_control
    "regulated": 0.04,          # Regulated domains
    "strict": 0.03,             # Strict mode
    "regular": 0.10,            # Relaxed for regular profile
    "clinical_shadow": 1.0,     # Shadow mode is observation only
}

DEFAULT_COVERAGE_TARGET: float = 0.90
DEFAULT_COVERAGE_TOLERANCE: float = 0.02


# -----------------------------------------------------------------------------
# Gate Configuration
# -----------------------------------------------------------------------------

class CalibrationGateConfig(BaseModel):
    """Configuration for calibration gate thresholds and behavior."""
    
    model_config = ConfigDictLike(frozen=True)
    
    # Maximum ECE worst-slice allowed for high-trust profiles
    ece_max: float = Field(
        default=0.03,
        ge=0.0,
        le=1.0,
        description="Maximum ECE worst-slice threshold for ALLOW status",
    )
    
    # Coverage bounds
    coverage_target: float = Field(
        default=0.90,
        ge=0.0,
        le=1.0,
        description="Target coverage for calibration",
    )
    coverage_tolerance: float = Field(
        default=0.02,
        ge=0.0,
        le=0.5,
        description="Tolerance around coverage target",
    )
    
    # Profiles that require hard gating (DENY on failure)
    high_trust_profiles: Set[str] = Field(
        default_factory=lambda: HIGH_TRUST_PROFILES.copy(),
        description="Profiles where calibration failure results in DENY",
    )
    
    # Whether marginal calibration should result in HOLD vs ADVISORY
    treat_marginal_as_hold: bool = Field(
        default=True,
        description="If True, marginal calibration → HOLD; if False → ADVISORY",
    )
    
    # Margin for marginal state (within this % of threshold = marginal)
    marginal_margin: float = Field(
        default=0.2,
        ge=0.0,
        le=1.0,
        description="Within this factor of threshold is considered marginal",
    )
    
    # Version metadata (propagated to receipts/GateReceipts)
    gate_version: Optional[str] = Field(
        default=None,
        description="Gatebook/gate version tag for lineage/pin checks.",
    )
    policy_version: Optional[str] = Field(
        default=None,
        description="Policy version tag if distinct from gate_version.",
    )


def _resolve_versions(
    gate_version: Optional[str],
    policy_version: Optional[str],
) -> tuple[Optional[str], Optional[str]]:
    """
    Resolve gate/policy version from explicit config or environment.
    
    Falls back to LOOM_GATEBOOK_VERSION / LOOM_POLICY_VERSION to avoid losing
    lineage when the caller does not pass explicit versions.
    """
    env_gate = os.getenv("LOOM_GATEBOOK_VERSION")
    env_policy = os.getenv("LOOM_POLICY_VERSION")
    return gate_version or env_gate, policy_version or env_policy


def _default_version_token(
    ece_max: float,
    coverage_target: float,
    coverage_tolerance: float,
    high_trust_profiles: Set[str],
) -> str:
    """
    Deterministic version token derived from thresholds/profile set when no
    explicit gatebook/policy version is provided.
    """
    payload = {
        "ece_max": ece_max,
        "coverage_target": coverage_target,
        "coverage_tolerance": coverage_tolerance,
        "high_trust_profiles": sorted(high_trust_profiles),
    }
    return hashlib.blake2b(canonicalize(payload), digest_size=16).hexdigest()


def get_calibration_gate_config(
    profile: str,
    domain: Optional[str] = None,
    risk_band: Optional[str] = None,
    include_extended: bool = False,
    gate_version: Optional[str] = None,
    policy_version: Optional[str] = None,
) -> CalibrationGateConfig:
    """
    Get calibration gate configuration for a profile/domain/risk combination.
    
    Args:
        profile: Profile name to get config for
        domain: Optional domain context (clinical, governance, etc.)
        risk_band: Optional risk level (trivial, low, medium, sensitive, critical)
        include_extended: If True, include EXTENDED_HIGH_TRUST_PROFILES in high-trust set
    
    Returns:
        CalibrationGateConfig with appropriate thresholds
    """
    # Start with profile-specific ece_max
    ece_max = DEFAULT_ECE_MAX.get(profile, 0.05)
    
    # Tighten thresholds for critical risk bands
    if risk_band in ("critical", "sensitive"):
        ece_max = min(ece_max, 0.03)
    
    # Determine which profiles are high-trust for this config
    if include_extended:
        high_trust = CORE_HIGH_TRUST_PROFILES | EXTENDED_HIGH_TRUST_PROFILES
    else:
        high_trust = HIGH_TRUST_PROFILES
    
    # Clinical domain gets tighter defaults
    if domain == "clinical" and profile in high_trust:
        ece_max = min(ece_max, 0.03)
    
    resolved_gate_version, resolved_policy_version = _resolve_versions(gate_version, policy_version)
    if resolved_gate_version is None:
        resolved_gate_version = _default_version_token(
            ece_max,
            DEFAULT_COVERAGE_TARGET,
            DEFAULT_COVERAGE_TOLERANCE,
            high_trust,
        )
    if resolved_policy_version is None:
        resolved_policy_version = resolved_gate_version
    
    return CalibrationGateConfig(
        ece_max=ece_max,
        high_trust_profiles=high_trust,
        gate_version=resolved_gate_version,
        policy_version=resolved_policy_version,
    )


# -----------------------------------------------------------------------------
# CalibrationGateReceipt
# -----------------------------------------------------------------------------

class CalibrationGateReceipt(BaseReceipt):
    """
    Receipt emitted for every calibration gate evaluation.
    
    This is the constitutional record of whether ECE/coverage gating allowed
    or blocked an action. Even advisory evaluations emit receipts for audit.
    
    Fields:
        envelope_id: ID of the decision envelope being gated
        episode_id: Optional episode context
        ece_worst: Worst-slice ECE value
        ece_aggregate: Aggregate ECE (for comparison)
        coverage: Coverage value
        n_samples: Number of calibration samples
        status: Gate decision (allow/hold/deny/advisory_only)
        profile: Profile that was evaluated
        domain: Domain of the action
        risk_band: Risk level of the action
        thresholds: Thresholds that were applied
        reason: Human-readable explanation of decision
        worst_slice_group: Which subgroup had worst ECE (if available)
    """
    
    receipt_type: str = "CalibrationGateReceipt"
    domain: str = Field(default=Domain.GOVERNANCE.value)
    
    # Core identification
    envelope_id: str = Field(description="ID of the decision envelope being gated")
    episode_id: Optional[str] = Field(default=None, description="Optional episode context")
    
    # Calibration metrics
    ece_worst: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Worst-slice Expected Calibration Error",
    )
    ece_aggregate: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Aggregate ECE (overall calibration)",
    )
    coverage: Optional[float] = Field(
        default=None,
        ge=0.0,
        le=1.0,
        description="Coverage (fraction of predictions with confidence intervals)",
    )
    n_samples: Optional[int] = Field(
        default=None,
        ge=0,
        description="Number of calibration samples",
    )
    
    # Worst slice info
    worst_slice_group: Optional[str] = Field(
        default=None,
        description="Identifier of the subgroup with worst ECE",
    )
    
    # Gate decision
    status: CalibrationGateStatus = Field(
        description="Gate decision: allow|hold|deny|advisory_only",
    )
    
    # Context
    profile: str = Field(description="Profile being evaluated")
    action_domain: Optional[str] = Field(
        default=None,
        description="Domain of the action (clinical, governance, etc.)",
    )
    risk_band: Optional[str] = Field(
        default=None,
        description="Risk level (trivial, low, medium, sensitive, critical)",
    )
    
    # Version metadata
    gate_version: Optional[str] = Field(
        default=None,
        description="Gatebook/gate version tag for this evaluation.",
    )
    policy_version: Optional[str] = Field(
        default=None,
        description="Policy version tag if distinct from gate_version.",
    )
    
    # Thresholds applied
    thresholds: Dict[str, float] = Field(
        default_factory=dict,
        description="Thresholds used for this evaluation",
    )
    
    # Explanation
    reason: str = Field(
        default="",
        description="Human-readable explanation of gate decision",
    )
    
    # Marginal state
    is_marginal: bool = Field(
        default=False,
        description="Whether calibration is marginal (close to threshold)",
    )
    
    @model_validator(mode="after")
    def _validate_status_consistency(self) -> "CalibrationGateReceipt":
        """Ensure status is consistent with profile and metrics."""
        if (
            self.status == CalibrationGateStatus.ALLOW
            and self.profile in HIGH_TRUST_PROFILES
            and self.ece_worst is None
        ):
            logger.warning(
                f"CalibrationGateReceipt ALLOW for high-trust profile {self.profile} "
                f"but ece_worst is None - this may indicate a bug"
            )
        return self
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        try:
            data = self.model_dump(mode="python")
        except AttributeError:
            data = self.dict()  # Pydantic v1 fallback
        data["status"] = self.status.value if isinstance(self.status, Enum) else self.status
        return data


# -----------------------------------------------------------------------------
# Re-exports
# -----------------------------------------------------------------------------

__all__ = [
    "CalibrationGateStatus",
    "CalibrationGateConfig",
    "CalibrationGateReceipt",
    "CORE_HIGH_TRUST_PROFILES",
    "EXTENDED_HIGH_TRUST_PROFILES",
    "HIGH_TRUST_PROFILES",
    "DEFAULT_ECE_MAX",
    "DEFAULT_COVERAGE_TARGET",
    "DEFAULT_COVERAGE_TOLERANCE",
    "get_calibration_gate_config",
]
