"""
MetricsCollector for quality metrics and monitoring.

This module calculates quality metrics for the MCO PMSI coding pipeline
and provides alerting when thresholds are exceeded.

Validates: Requirements 18.1-18.9
"""

from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Optional
from sqlalchemy.orm import Session
from sqlalchemy import func

from pipeline_mco_pmsi.database.models import (
    StayDB,
    CodeDB,
    ClinicalFactDB,
    ValidationIssueDB,
    QuestionDB,
    TIMCorrectionDB,
)


@dataclass
class QualityMetrics:
    """Quality metrics for a set of stays."""
    
    # Requirement 18.1: Percentage of codes without evidence
    codes_without_evidence_pct: float = 0.0
    
    # Requirement 18.2: Percentage of negated diagnoses coded as affirmed
    negated_coded_as_affirmed_pct: float = 0.0
    
    # Requirement 18.3: Percentage of correct DP vs gold standard
    dp_accuracy_pct: Optional[float] = None
    
    # Requirement 18.4: Percentage of phantom DAS
    phantom_das_pct: float = 0.0
    
    # Requirement 18.5: Percentage of CCAM acts without explicit evidence
    ccam_without_evidence_pct: float = 0.0
    
    # Requirement 18.6: Percentage of stays validated in one click
    one_click_validation_pct: float = 0.0
    
    # Requirement 18.7: Percentage of relevant questions
    question_relevance_pct: Optional[float] = None
    
    # Additional metrics
    total_stays: int = 0
    total_codes: int = 0
    total_questions: int = 0
    calculation_date: datetime = field(default_factory=datetime.now)
    
    # Drift detection
    drift_detected: bool = False
    drift_metrics: List[str] = field(default_factory=list)


@dataclass
class MetricsThresholds:
    """Thresholds for quality metrics alerting."""
    
    # Requirement 18.9: Alert when metrics exceed thresholds
    max_codes_without_evidence_pct: float = 5.0
    max_negated_coded_as_affirmed_pct: float = 1.0
    min_dp_accuracy_pct: float = 80.0
    max_phantom_das_pct: float = 10.0
    max_ccam_without_evidence_pct: float = 2.0
    min_one_click_validation_pct: float = 50.0
    min_question_relevance_pct: float = 80.0


class MetricsCollector:
    """
    Collects and calculates quality metrics for the MCO PMSI coding pipeline.
    
    Validates: Requirements 18.1-18.9
    """
    
    def __init__(
        self,
        session: Session,
        thresholds: Optional[MetricsThresholds] = None
    ):
        """
        Initialize the MetricsCollector.
        
        Args:
            session: SQLAlchemy database session
            thresholds: Optional custom thresholds for alerting
        """
        self.session = session
        self.thresholds = thresholds or MetricsThresholds()
    
    def calculate_metrics(
        self,
        stay_ids: Optional[List[int]] = None,
        start_date: Optional[datetime] = None,
        end_date: Optional[datetime] = None,
        gold_standard: Optional[Dict[int, str]] = None
    ) -> QualityMetrics:
        """
        Calculate quality metrics for a set of stays.
        
        Args:
            stay_ids: Optional list of stay IDs to calculate metrics for
            start_date: Optional start date for filtering stays
            end_date: Optional end date for filtering stays
            gold_standard: Optional dict mapping stay_id to correct DP code
        
        Returns:
            QualityMetrics with calculated values
        
        Validates: Requirement 18.1-18.7
        """
        # Build query for stays
        query = self.session.query(StayDB)
        
        if stay_ids:
            query = query.filter(StayDB.id.in_(stay_ids))
        if start_date:
            query = query.filter(StayDB.created_at >= start_date)
        if end_date:
            query = query.filter(StayDB.created_at <= end_date)
        
        stays = query.all()
        
        if not stays:
            return QualityMetrics()
        
        metrics = QualityMetrics(
            total_stays=len(stays),
            calculation_date=datetime.now()
        )
        
        # Calculate each metric
        metrics.codes_without_evidence_pct = self._calculate_codes_without_evidence(stays)
        metrics.negated_coded_as_affirmed_pct = self._calculate_negated_coded_as_affirmed(stays)
        metrics.phantom_das_pct = self._calculate_phantom_das(stays)
        metrics.ccam_without_evidence_pct = self._calculate_ccam_without_evidence(stays)
        metrics.one_click_validation_pct = self._calculate_one_click_validation(stays)
        
        # Calculate DP accuracy if gold standard provided
        if gold_standard:
            metrics.dp_accuracy_pct = self._calculate_dp_accuracy(stays, gold_standard)
        
        # Calculate question relevance if feedback available
        metrics.question_relevance_pct = self._calculate_question_relevance(stays)
        
        # Count total codes and questions
        metrics.total_codes = sum(len(stay.codes) for stay in stays)
        metrics.total_questions = sum(len(stay.questions) for stay in stays)
        
        return metrics
    
    def _calculate_codes_without_evidence(self, stays: List[StayDB]) -> float:
        """
        Calculate percentage of codes without evidence.
        
        Validates: Requirement 18.1
        """
        total_codes = 0
        codes_without_evidence = 0
        
        for stay in stays:
            for code in stay.codes:
                total_codes += 1
                if not code.evidences or len(code.evidences) == 0:
                    codes_without_evidence += 1
        
        if total_codes == 0:
            return 0.0
        
        return (codes_without_evidence / total_codes) * 100.0
    
    def _calculate_negated_coded_as_affirmed(self, stays: List[StayDB]) -> float:
        """
        Calculate percentage of negated diagnoses coded as affirmed.
        
        Validates: Requirement 18.2
        """
        total_diagnostic_codes = 0
        negated_coded_as_affirmed = 0
        
        for stay in stays:
            # Get all facts for this stay
            facts_by_text = {}
            for fact in stay.facts:
                if fact.type == "diagnostic":
                    facts_by_text[fact.text.lower()] = fact
            
            # Check codes against facts
            for code in stay.codes:
                if code.type in ["dp", "dr", "das"]:
                    total_diagnostic_codes += 1
                    
                    # Check if any evidence text matches a negated fact
                    for evidence in code.evidences:
                        fact_text = evidence.text.lower()
                        if fact_text in facts_by_text:
                            fact = facts_by_text[fact_text]
                            if fact.qualifier_certainty == "nié":
                                negated_coded_as_affirmed += 1
                                break
        
        if total_diagnostic_codes == 0:
            return 0.0
        
        return (negated_coded_as_affirmed / total_diagnostic_codes) * 100.0
    
    def _calculate_dp_accuracy(
        self,
        stays: List[StayDB],
        gold_standard: Dict[int, str]
    ) -> float:
        """
        Calculate percentage of correct DP vs gold standard.
        
        Validates: Requirement 18.3
        """
        total_stays_with_gold = 0
        correct_dp = 0
        
        for stay in stays:
            if stay.id not in gold_standard:
                continue
            
            total_stays_with_gold += 1
            expected_dp = gold_standard[stay.id]
            
            # Find proposed DP
            dp_code = next((c for c in stay.codes if c.type == "dp"), None)
            
            if dp_code and dp_code.code == expected_dp:
                correct_dp += 1
        
        if total_stays_with_gold == 0:
            return 0.0
        
        return (correct_dp / total_stays_with_gold) * 100.0
    
    def _calculate_phantom_das(self, stays: List[StayDB]) -> float:
        """
        Calculate percentage of phantom DAS (DAS without supporting evidence).
        
        Validates: Requirement 18.4
        """
        total_das = 0
        phantom_das = 0
        
        for stay in stays:
            for code in stay.codes:
                if code.type == "das":
                    total_das += 1
                    
                    # Check if DAS has weak or no evidence
                    if not code.evidences or len(code.evidences) == 0:
                        phantom_das += 1
                    elif code.confidence < 0.5:
                        phantom_das += 1
        
        if total_das == 0:
            return 0.0
        
        return (phantom_das / total_das) * 100.0
    
    def _calculate_ccam_without_evidence(self, stays: List[StayDB]) -> float:
        """
        Calculate percentage of CCAM acts without explicit evidence.
        
        Validates: Requirement 18.5
        """
        total_ccam = 0
        ccam_without_evidence = 0
        
        for stay in stays:
            for code in stay.codes:
                if code.type == "ccam":
                    total_ccam += 1
                    if not code.evidences or len(code.evidences) == 0:
                        ccam_without_evidence += 1
        
        if total_ccam == 0:
            return 0.0
        
        return (ccam_without_evidence / total_ccam) * 100.0
    
    def _calculate_one_click_validation(self, stays: List[StayDB]) -> float:
        """
        Calculate percentage of stays validated in one click (no TIM corrections).
        
        Validates: Requirement 18.6
        """
        total_validated_stays = 0
        one_click_stays = 0
        
        for stay in stays:
            # Check if stay has been validated (has at least one accepted code)
            has_accepted_codes = any(code.status == "accepted" for code in stay.codes)
            
            if has_accepted_codes:
                total_validated_stays += 1
                
                # Check if there are no TIM corrections for any code in this stay
                code_ids = [code.id for code in stay.codes]
                corrections = self.session.query(TIMCorrectionDB).filter(
                    TIMCorrectionDB.original_code_id.in_(code_ids)
                ).count()
                
                if corrections == 0:
                    one_click_stays += 1
        
        if total_validated_stays == 0:
            return 0.0
        
        return (one_click_stays / total_validated_stays) * 100.0
    
    def _calculate_question_relevance(self, stays: List[StayDB]) -> Optional[float]:
        """
        Calculate percentage of relevant questions based on TIM feedback.
        
        Validates: Requirement 18.7
        """
        total_questions_with_feedback = 0
        relevant_questions = 0
        
        for stay in stays:
            for question in stay.questions:
                # Check if question has feedback (relevance_score field)
                if hasattr(question, 'relevance_score') and question.relevance_score is not None:
                    total_questions_with_feedback += 1
                    if question.relevance_score >= 0.7:  # Consider relevant if score >= 0.7
                        relevant_questions += 1
        
        if total_questions_with_feedback == 0:
            return None  # No feedback available
        
        return (relevant_questions / total_questions_with_feedback) * 100.0
    
    def detect_drift(
        self,
        current_metrics: QualityMetrics,
        baseline_metrics: QualityMetrics,
        drift_threshold: float = 10.0
    ) -> bool:
        """
        Detect quality drift by comparing current metrics to baseline.
        
        Args:
            current_metrics: Current quality metrics
            baseline_metrics: Baseline metrics to compare against
            drift_threshold: Percentage change threshold for drift detection
        
        Returns:
            True if drift detected, False otherwise
        
        Validates: Requirement 18.8
        """
        drift_detected = False
        drift_metrics = []
        
        # Check each metric for significant change
        metrics_to_check = [
            ("codes_without_evidence_pct", "increase"),
            ("negated_coded_as_affirmed_pct", "increase"),
            ("dp_accuracy_pct", "decrease"),
            ("phantom_das_pct", "increase"),
            ("ccam_without_evidence_pct", "increase"),
            ("one_click_validation_pct", "decrease"),
        ]
        
        for metric_name, direction in metrics_to_check:
            current_value = getattr(current_metrics, metric_name, None)
            baseline_value = getattr(baseline_metrics, metric_name, None)
            
            if current_value is None or baseline_value is None:
                continue
            
            if baseline_value == 0:
                continue
            
            change_pct = abs((current_value - baseline_value) / baseline_value) * 100.0
            
            if change_pct >= drift_threshold:
                if direction == "increase" and current_value > baseline_value:
                    drift_detected = True
                    drift_metrics.append(metric_name)
                elif direction == "decrease" and current_value < baseline_value:
                    drift_detected = True
                    drift_metrics.append(metric_name)
        
        current_metrics.drift_detected = drift_detected
        current_metrics.drift_metrics = drift_metrics
        
        return drift_detected
    
    def check_thresholds(self, metrics: QualityMetrics) -> List[str]:
        """
        Check if metrics exceed configured thresholds and generate alerts.
        
        Args:
            metrics: Quality metrics to check
        
        Returns:
            List of alert messages for exceeded thresholds
        
        Validates: Requirement 18.9
        """
        alerts = []
        
        # Check codes without evidence
        if metrics.codes_without_evidence_pct > self.thresholds.max_codes_without_evidence_pct:
            alerts.append(
                f"ALERT: Codes without evidence ({metrics.codes_without_evidence_pct:.1f}%) "
                f"exceeds threshold ({self.thresholds.max_codes_without_evidence_pct:.1f}%)"
            )
        
        # Check negated coded as affirmed
        if metrics.negated_coded_as_affirmed_pct > self.thresholds.max_negated_coded_as_affirmed_pct:
            alerts.append(
                f"ALERT: Negated diagnoses coded as affirmed ({metrics.negated_coded_as_affirmed_pct:.1f}%) "
                f"exceeds threshold ({self.thresholds.max_negated_coded_as_affirmed_pct:.1f}%)"
            )
        
        # Check DP accuracy
        if metrics.dp_accuracy_pct is not None and metrics.dp_accuracy_pct < self.thresholds.min_dp_accuracy_pct:
            alerts.append(
                f"ALERT: DP accuracy ({metrics.dp_accuracy_pct:.1f}%) "
                f"below threshold ({self.thresholds.min_dp_accuracy_pct:.1f}%)"
            )
        
        # Check phantom DAS
        if metrics.phantom_das_pct > self.thresholds.max_phantom_das_pct:
            alerts.append(
                f"ALERT: Phantom DAS ({metrics.phantom_das_pct:.1f}%) "
                f"exceeds threshold ({self.thresholds.max_phantom_das_pct:.1f}%)"
            )
        
        # Check CCAM without evidence
        if metrics.ccam_without_evidence_pct > self.thresholds.max_ccam_without_evidence_pct:
            alerts.append(
                f"ALERT: CCAM without evidence ({metrics.ccam_without_evidence_pct:.1f}%) "
                f"exceeds threshold ({self.thresholds.max_ccam_without_evidence_pct:.1f}%)"
            )
        
        # Check one-click validation
        if metrics.one_click_validation_pct < self.thresholds.min_one_click_validation_pct:
            alerts.append(
                f"ALERT: One-click validation rate ({metrics.one_click_validation_pct:.1f}%) "
                f"below threshold ({self.thresholds.min_one_click_validation_pct:.1f}%)"
            )
        
        # Check question relevance
        if metrics.question_relevance_pct is not None and metrics.question_relevance_pct < self.thresholds.min_question_relevance_pct:
            alerts.append(
                f"ALERT: Question relevance ({metrics.question_relevance_pct:.1f}%) "
                f"below threshold ({self.thresholds.min_question_relevance_pct:.1f}%)"
            )
        
        return alerts
