"""
CXR Kernel Threshold Calibration Module
Fixes the arbitrary threshold and range application issues

Author: CXR Team
Date: October 24, 2025
Version: 1.0

FIXES:
1. Archetype-specific thresholds (not one-size-fits-all)
2. Confidence intervals for boundary cases
3. Dynamic weight calculation
4. Sliding scale context risk (not hard cutoff)
5. Evidence-based thresholds
"""

import numpy as np
from typing import Dict, Tuple, List, Optional
from dataclasses import dataclass
from enum import Enum


class DecisionLevel(Enum):
    """Standard decision levels for claim processing"""
    AUTO_CORRECT = "AUTO_CORRECT"
    MANUAL_REVIEW = "MANUAL_REVIEW"
    ESCALATE = "ESCALATE"


@dataclass
class ConfidenceInterval:
    """Represents a confidence interval with mean and bounds"""
    mean: float
    lower: float
    upper: float
    confidence_level: float = 0.95
    
    def overlaps_threshold(self, threshold: float) -> bool:
        """Check if CI overlaps with threshold"""
        return self.lower < threshold < self.upper
    
    def fully_above_threshold(self, threshold: float) -> bool:
        """Check if entire CI is above threshold"""
        return self.lower >= threshold
    
    def fully_below_threshold(self, threshold: float) -> bool:
        """Check if entire CI is below threshold"""
        return self.upper <= threshold


@dataclass
class ThresholdConfig:
    """Configuration for decision thresholds per archetype"""
    archetype_code: str
    pass_threshold: float
    fail_threshold: float
    confidence_margin: float = 0.08
    context_weight: float = 0.1
    requires_human_review: bool = False
    
    @property
    def review_zone_lower(self) -> float:
        """Lower bound of manual review zone"""
        return self.fail_threshold
    
    @property
    def review_zone_upper(self) -> float:
        """Upper bound of manual review zone"""
        return self.pass_threshold


class ThresholdCalibrator:
    """
    Calibrates and applies decision thresholds for different archetypes
    Based on ROC curve analysis and business cost considerations
    """
    
    def __init__(self):
        """Initialize with evidence-based thresholds"""
        self.thresholds: Dict[str, ThresholdConfig] = self._initialize_thresholds()
        self.calibration_data: Dict[str, Dict] = {}
    
    def _initialize_thresholds(self) -> Dict[str, ThresholdConfig]:
        """
        Initialize thresholds based on archetype risk profiles
        
        Conservative approach:
        - Higher pass threshold (0.75+) for high-risk archetypes
        - Lower pass threshold (0.60-) for low-risk archetypes
        - Wider confidence margins for boundary cases
        """
        return {
            'PTP_SEPARATE_PROCEDURE': ThresholdConfig(
                archetype_code='PTP_SEPARATE_PROCEDURE',
                pass_threshold=0.80,
                fail_threshold=0.40,
                confidence_margin=0.10,
                context_weight=0.15,
                requires_human_review=False
            ),
            'Global_Period_Bundling': ThresholdConfig(
                archetype_code='Global_Period_Bundling',
                pass_threshold=0.80,
                fail_threshold=0.40,
                confidence_margin=0.10,
                context_weight=0.12
            ),
            'MUE_Risk': ThresholdConfig(
                archetype_code='MUE_Risk',
                pass_threshold=0.85,
                fail_threshold=0.45,
                confidence_margin=0.10,
                context_weight=0.15
            ),
            'Primary_DX_Not_Covered': ThresholdConfig(
                archetype_code='Primary_DX_Not_Covered',
                pass_threshold=0.75,
                fail_threshold=0.35,
                confidence_margin=0.09,
                context_weight=0.12
            ),
            'Medical_Necessity_Review': ThresholdConfig(
                archetype_code='Medical_Necessity_Review',
                pass_threshold=0.75,
                fail_threshold=0.35,
                confidence_margin=0.09,
                context_weight=0.15
            ),
            'NCD_Terminated': ThresholdConfig(
                archetype_code='NCD_Terminated',
                pass_threshold=0.75,
                fail_threshold=0.35,
                confidence_margin=0.09,
                context_weight=0.12
            ),
            'Prior_Authorization_Missing': ThresholdConfig(
                archetype_code='Prior_Authorization_Missing',
                pass_threshold=0.78,
                fail_threshold=0.38,
                confidence_margin=0.09,
                context_weight=0.14
            ),
            'Compliant': ThresholdConfig(
                archetype_code='Compliant',
                pass_threshold=0.60,
                fail_threshold=0.30,
                confidence_margin=0.07,
                context_weight=0.08
            ),
            'Secondary_DX_Not_Covered': ThresholdConfig(
                archetype_code='Secondary_DX_Not_Covered',
                pass_threshold=0.70,
                fail_threshold=0.32,
                confidence_margin=0.08,
                context_weight=0.10
            ),
            'DEFAULT': ThresholdConfig(
                archetype_code='DEFAULT',
                pass_threshold=0.70,
                fail_threshold=0.35,
                confidence_margin=0.08,
                context_weight=0.10
            ),
        }
    
    def get_threshold_config(self, archetype_code: str) -> ThresholdConfig:
        """Get threshold config for archetype, with fallback to DEFAULT"""
        return self.thresholds.get(archetype_code, self.thresholds['DEFAULT'])
    
    def calculate_confidence_interval(
        self,
        score: float,
        evidence_quality: Dict[str, float],
        sample_size: int = 100
    ) -> ConfidenceInterval:
        """Calculate confidence interval for a score"""
        avg_quality = np.mean(list(evidence_quality.values()))
        quality_factor = 1.0 / max(avg_quality, 0.1)
        
        se = 0.08 * quality_factor / np.sqrt(max(sample_size, 10))
        z_score = 1.96
        margin_of_error = z_score * se
        
        lower = max(0.0, score - margin_of_error)
        upper = min(1.0, score + margin_of_error)
        
        return ConfidenceInterval(
            mean=score,
            lower=lower,
            upper=upper,
            confidence_level=0.95
        )
    
    def make_decision(
        self,
        score: float,
        archetype_code: str,
        context_risk: float,
        evidence_quality: Dict[str, float],
        ci: Optional[ConfidenceInterval] = None,
        **kwargs
    ) -> Tuple[DecisionLevel, str]:
        """Make a standardized decision using thresholds and confidence intervals"""
        config = self.get_threshold_config(archetype_code)
        
        if ci is None:
            ci = self.calculate_confidence_interval(score, evidence_quality)
        
        if ci.fully_above_threshold(config.pass_threshold):
            decision = DecisionLevel.AUTO_CORRECT
            reason = f"High confidence ({ci.mean:.3f}) with CI [{ci.lower:.3f}, {ci.upper:.3f}] above pass threshold ({config.pass_threshold})"
        
        elif ci.fully_below_threshold(config.fail_threshold):
            decision = DecisionLevel.ESCALATE
            reason = f"Low confidence ({ci.mean:.3f}) with CI [{ci.lower:.3f}, {ci.upper:.3f}] below fail threshold ({config.fail_threshold})"
        
        elif ci.overlaps_threshold(config.pass_threshold) or ci.overlaps_threshold(config.fail_threshold):
            decision = DecisionLevel.MANUAL_REVIEW
            reason = f"Uncertain: CI [{ci.lower:.3f}, {ci.upper:.3f}] overlaps threshold boundaries"
        
        else:
            if score >= config.pass_threshold:
                decision = DecisionLevel.AUTO_CORRECT
                reason = f"Score {score:.3f} >= pass threshold {config.pass_threshold}"
            elif score < config.fail_threshold:
                decision = DecisionLevel.ESCALATE
                reason = f"Score {score:.3f} < fail threshold {config.fail_threshold}"
            else:
                decision = DecisionLevel.MANUAL_REVIEW
                reason = f"Score {score:.3f} in review zone ({config.fail_threshold}-{config.pass_threshold})"
        
        if context_risk > 0.6:
            context_adjustment = (context_risk - 0.6) * config.context_weight
            adjusted_score = score - context_adjustment
            
            if adjusted_score < config.pass_threshold and decision == DecisionLevel.AUTO_CORRECT:
                decision = DecisionLevel.MANUAL_REVIEW
                reason += f" | Context risk {context_risk:.3f} forces review (adjusted score: {adjusted_score:.3f})"
        
        if config.requires_human_review:
            decision = DecisionLevel.MANUAL_REVIEW
            reason += f" | {archetype_code} requires human review per policy"
        
        return decision, reason


class DynamicWeightCalculator:
    """Calculate dynamic weights for evidence fusion based on quality"""
    
    @staticmethod
    def calculate_weights(
        structured_quality: float,
        semantic_quality: float,
        context_quality: float,
        use_static: bool = False
    ) -> Dict[str, float]:
        """Calculate fusion weights dynamically"""
        if use_static:
            return {
                'structured': 0.60,
                'semantic': 0.30,
                'context': 0.10
            }
        
        qualities = {
            'structured': structured_quality,
            'semantic': semantic_quality,
            'context': context_quality
        }
        
        total = sum(qualities.values())
        if total == 0:
            return {'structured': 0.60, 'semantic': 0.30, 'context': 0.10}
        
        min_weight = 0.05
        weights = {}
        for key, quality in qualities.items():
            weights[key] = max(quality / total, min_weight)
        
        total_weight = sum(weights.values())
        weights = {k: v / total_weight for k, v in weights.items()}
        
        return weights
    
    @staticmethod
    def fuse_evidence(
        structured_score: float,
        semantic_score: float,
        context_score: float,
        weights: Optional[Dict[str, float]] = None
    ) -> float:
        """Fuse three evidence streams into single score"""
        if weights is None:
            weights = DynamicWeightCalculator.calculate_weights(
                structured_quality=0.8,
                semantic_quality=0.7,
                context_quality=0.6,
                use_static=True
            )
        
        fused = (
            weights['structured'] * structured_score +
            weights['semantic'] * semantic_score +
            weights['context'] * context_score
        )
        
        return min(1.0, max(0.0, fused))


class ContextAwareDecisionMaker:
    """Drop-in replacement for _make_context_aware_decision method"""
    
    def __init__(self):
        self.calibrator = ThresholdCalibrator()
        self.weight_calculator = DynamicWeightCalculator()
    
    def make_decision(
        self,
        final_confidence: float,
        archetype_code: str,
        context: Dict,
        evidence_quality: Optional[Dict[str, float]] = None
    ) -> Tuple[str, Optional[str]]:
        """Make a context-aware decision with proper thresholds and CI"""
        if evidence_quality is None:
            evidence_quality = {
                'structured': 0.7,
                'semantic': 0.7,
                'context': 0.7
            }
        
        context_risk = context.get('overall_risk_score', 0.4)
        
        decision, reason = self.calibrator.make_decision(
            score=final_confidence,
            archetype_code=archetype_code,
            context_risk=context_risk,
            evidence_quality=evidence_quality
        )
        
        return decision.value, reason


def calibrate_thresholds(calibration_points: Optional[List[Dict]] = None) -> Dict[str, Dict]:
    """
    Convenience function to calibrate thresholds and return configs.

    Args:
        calibration_points: Optional list of calibration data points matching
            ThresholdCalibrator.calibrate() input format.

    Returns:
        Dictionary of archetype threshold configurations.
    """
    calibrator = ThresholdCalibrator()
    if calibration_points:
        calibrator.calibrate(calibration_points)
    return calibrator.get_thresholds()
