"""
Canonical metric contract for Verity audit engine.

Every metric in the engine conforms to this output shape so policy can reason about it.
"""
from typing import Dict, Any, List, Optional, Union, Tuple
from enum import Enum


class MetricCategory(str, Enum):
    """Metric categories for organization."""
    PERFORMANCE = "performance"
    FAIRNESS = "fairness"
    ROBUSTNESS = "robustness"
    DATA_QUALITY = "data_quality"
    PRIVACY = "privacy"
    SECURITY = "security"
    EXPLAINABILITY = "explainability"
    SAFETY = "safety"  # For API/genAI models
    RELIABILITY = "reliability"  # For API/genAI models


class AuditMode(str, Enum):
    """Supported audit modes."""
    DATASET_MODEL = "dataset_model"
    API_MODEL = "api_model"
    HYBRID = "hybrid"


class TaskType(str, Enum):
    """Supported task types."""
    REGRESSION = "regression"
    CLASSIFICATION = "classification"
    RANKING = "ranking"
    GENERATIVE = "generative"


class MetricUnit(str, Enum):
    """Metric units for thresholding."""
    RATIO = "ratio"  # 0-1 scale
    PERCENTAGE = "percentage"  # 0-100 scale
    CURRENCY = "currency"  # Dollar amounts
    NORMALIZED = "normalized"  # Normalized by range/mean/std
    STATISTICAL_DISTANCE = "statistical_distance"  # KS distance, etc.
    BOOLEAN = "boolean"  # True/False
    ABSOLUTE_DIFFERENCE = "absolute_difference"  # Raw difference
    RELATIVE_DIFFERENCE = "relative_difference"  # Percentage difference
    CORRELATION_COEFFICIENT = "correlation_coefficient"  # -1 to 1
    MUTUAL_INFORMATION = "mutual_information"  # Bits
    NORMALIZED_ERROR = "normalized_error"  # Error normalized by std/mean


class MetricDirection(str, Enum):
    """Direction of metric (what is better)."""
    LOWER_IS_BETTER = "lower_is_better"  # Error metrics, gaps
    HIGHER_IS_BETTER = "higher_is_better"  # Accuracy, R²
    IN_RANGE = "in_range"  # Must be within range
    BOOLEAN_TRUE_IS_BAD = "boolean_true_is_bad"  # Leakage detected = True is bad


class MetricAggregation(str, Enum):
    """How the metric is aggregated."""
    SCALAR = "scalar"  # Single value
    PER_GROUP = "per_group"  # Value per protected group
    PER_FEATURE = "per_feature"  # Value per feature
    DISTRIBUTION = "distribution"  # Full distribution stats


class MetricSeverity(str, Enum):
    """Severity level for enforcement."""
    GATE = "gate"  # Hard fail - blocks audit
    WARN = "warn"  # Warning - passes but flagged
    REPORT = "report"  # Telemetry only - no enforcement


class MetricContract:
    """
    Canonical metric contract.
    
    Every metric in the engine should conform to this structure.
    """
    
    def __init__(
        self,
        metric_id: str,
        category: MetricCategory,
        mode_support: List[AuditMode],
        task_support: List[TaskType],
        unit: MetricUnit,
        direction: MetricDirection,
        aggregation: MetricAggregation,
        value: Union[float, Dict[str, Any]],
        metadata: Optional[Dict[str, Any]] = None,
        severity: MetricSeverity = MetricSeverity.REPORT,
        threshold: Optional[float] = None,
        evidence_requirements: Optional[Dict[str, Any]] = None
    ):
        self.metric_id = metric_id
        self.category = category
        self.mode_support = mode_support
        self.task_support = task_support
        self.unit = unit
        self.direction = direction
        self.aggregation = aggregation
        self.value = value
        self.metadata = metadata or {}
        self.severity = severity
        self.threshold = threshold
        self.evidence_requirements = evidence_requirements or {}
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "metric_id": self.metric_id,
            "category": self.category.value,
            "mode_support": [m.value for m in self.mode_support],
            "task_support": [t.value for t in self.task_support],
            "unit": self.unit.value,
            "direction": self.direction.value,
            "aggregation": self.aggregation.value,
            "value": self.value,
            "metadata": self.metadata,
            "severity": self.severity.value,
            "threshold": self.threshold,
            "evidence_requirements": self.evidence_requirements
        }
    
    def check_threshold(self) -> Tuple[bool, Optional[str]]:
        """
        Check if metric passes threshold.
        
        Returns:
            (passed: bool, reason: Optional[str])
        """
        if self.threshold is None:
            return True, None
        
        if self.aggregation == MetricAggregation.SCALAR:
            if isinstance(self.value, (int, float)):
                if self.direction == MetricDirection.LOWER_IS_BETTER:
                    passed = self.value <= self.threshold
                elif self.direction == MetricDirection.HIGHER_IS_BETTER:
                    passed = self.value >= self.threshold
                else:
                    passed = True  # IN_RANGE or BOOLEAN handled separately
                
                if not passed:
                    reason = f"{self.metric_id} = {self.value:.4f}, threshold = {self.threshold:.4f}"
                else:
                    reason = None
                
                return passed, reason
        
        # For per_group, per_feature, distribution - check max/min as appropriate
        if isinstance(self.value, dict):
            if self.direction == MetricDirection.LOWER_IS_BETTER:
                max_value = max(v for v in self.value.values() if isinstance(v, (int, float)))
                passed = max_value <= self.threshold
                if not passed:
                    reason = f"{self.metric_id} max = {max_value:.4f}, threshold = {self.threshold:.4f}"
                else:
                    reason = None
                return passed, reason
            elif self.direction == MetricDirection.HIGHER_IS_BETTER:
                min_value = min(v for v in self.value.values() if isinstance(v, (int, float)))
                passed = min_value >= self.threshold
                if not passed:
                    reason = f"{self.metric_id} min = {min_value:.4f}, threshold = {self.threshold:.4f}"
                else:
                    reason = None
                return passed, reason
        
        return True, None
