# ================================================================
# services/forecasting/app/services/performance_monitoring_service.py
# ================================================================
"""
Performance Monitoring Service

Monitors forecast accuracy over time and triggers actions when
performance degrades below acceptable thresholds.
"""

from typing import Dict, Any, List, Optional, Tuple
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, func, desc
from datetime import datetime, timedelta, timezone
import structlog
import uuid

from app.models.validation_run import ValidationRun
from app.models.predictions import ModelPerformanceMetric
from app.models.forecasts import Forecast
from shared.database.exceptions import DatabaseError

logger = structlog.get_logger()


class PerformanceMonitoringService:
    """Service for monitoring forecast performance and triggering improvements"""

    # Configurable thresholds
    MAPE_WARNING_THRESHOLD = 20.0  # Warning if MAPE > 20%
    MAPE_CRITICAL_THRESHOLD = 30.0  # Critical if MAPE > 30%
    MAPE_TREND_THRESHOLD = 5.0  # Alert if MAPE increases by > 5% over period
    MIN_SAMPLES_FOR_ALERT = 5  # Minimum validations before alerting
    TREND_LOOKBACK_DAYS = 30  # Days to analyze for trends

    def __init__(self, db_session: AsyncSession):
        self.db = db_session

    async def get_accuracy_summary(
        self,
        tenant_id: uuid.UUID,
        days: int = 30
    ) -> Dict[str, Any]:
        """
        Get accuracy summary for recent period

        Args:
            tenant_id: Tenant identifier
            days: Number of days to analyze

        Returns:
            Summary with overall metrics and trends
        """
        try:
            start_date = datetime.now(timezone.utc) - timedelta(days=days)

            # Get recent validation runs
            query = (
                select(ValidationRun)
                .where(
                    and_(
                        ValidationRun.tenant_id == tenant_id,
                        ValidationRun.status == "completed",
                        ValidationRun.started_at >= start_date,
                        ValidationRun.forecasts_with_actuals > 0  # Only runs with actual data
                    )
                )
                .order_by(desc(ValidationRun.started_at))
            )

            result = await self.db.execute(query)
            runs = result.scalars().all()

            if not runs:
                return {
                    "status": "no_data",
                    "message": f"No validation runs found in last {days} days",
                    "period_days": days
                }

            # Calculate summary statistics
            total_forecasts = sum(r.total_forecasts_evaluated for r in runs)
            total_with_actuals = sum(r.forecasts_with_actuals for r in runs)

            mape_values = [r.overall_mape for r in runs if r.overall_mape is not None]
            mae_values = [r.overall_mae for r in runs if r.overall_mae is not None]
            rmse_values = [r.overall_rmse for r in runs if r.overall_rmse is not None]

            avg_mape = sum(mape_values) / len(mape_values) if mape_values else None
            avg_mae = sum(mae_values) / len(mae_values) if mae_values else None
            avg_rmse = sum(rmse_values) / len(rmse_values) if rmse_values else None

            # Determine health status
            health_status = "healthy"
            if avg_mape and avg_mape > self.MAPE_CRITICAL_THRESHOLD:
                health_status = "critical"
            elif avg_mape and avg_mape > self.MAPE_WARNING_THRESHOLD:
                health_status = "warning"

            return {
                "status": "ok",
                "period_days": days,
                "validation_runs": len(runs),
                "total_forecasts_evaluated": total_forecasts,
                "total_forecasts_with_actuals": total_with_actuals,
                "coverage_percentage": round(
                    (total_with_actuals / total_forecasts * 100) if total_forecasts > 0 else 0, 2
                ),
                "average_metrics": {
                    "mape": round(avg_mape, 2) if avg_mape else None,
                    "mae": round(avg_mae, 2) if avg_mae else None,
                    "rmse": round(avg_rmse, 2) if avg_rmse else None,
                    "accuracy_percentage": round(100 - avg_mape, 2) if avg_mape else None
                },
                "health_status": health_status,
                "thresholds": {
                    "warning": self.MAPE_WARNING_THRESHOLD,
                    "critical": self.MAPE_CRITICAL_THRESHOLD
                }
            }

        except Exception as e:
            logger.error(
                "Failed to get accuracy summary",
                tenant_id=tenant_id,
                error=str(e)
            )
            raise DatabaseError(f"Failed to get accuracy summary: {str(e)}")

    async def detect_performance_degradation(
        self,
        tenant_id: uuid.UUID,
        lookback_days: int = 30
    ) -> Dict[str, Any]:
        """
        Detect if forecast performance is degrading over time

        Args:
            tenant_id: Tenant identifier
            lookback_days: Days to analyze for trends

        Returns:
            Degradation analysis with recommendations
        """
        try:
            logger.info(
                "Detecting performance degradation",
                tenant_id=tenant_id,
                lookback_days=lookback_days
            )

            start_date = datetime.now(timezone.utc) - timedelta(days=lookback_days)

            # Get validation runs ordered by time
            query = (
                select(ValidationRun)
                .where(
                    and_(
                        ValidationRun.tenant_id == tenant_id,
                        ValidationRun.status == "completed",
                        ValidationRun.started_at >= start_date,
                        ValidationRun.forecasts_with_actuals > 0
                    )
                )
                .order_by(ValidationRun.started_at)
            )

            result = await self.db.execute(query)
            runs = list(result.scalars().all())

            if len(runs) < self.MIN_SAMPLES_FOR_ALERT:
                return {
                    "status": "insufficient_data",
                    "message": f"Need at least {self.MIN_SAMPLES_FOR_ALERT} validation runs",
                    "runs_found": len(runs)
                }

            # Split into first half and second half
            midpoint = len(runs) // 2
            first_half = runs[:midpoint]
            second_half = runs[midpoint:]

            # Calculate average MAPE for each half
            first_half_mape = sum(
                r.overall_mape for r in first_half if r.overall_mape
            ) / len([r for r in first_half if r.overall_mape])

            second_half_mape = sum(
                r.overall_mape for r in second_half if r.overall_mape
            ) / len([r for r in second_half if r.overall_mape])

            mape_change = second_half_mape - first_half_mape
            mape_change_percentage = (mape_change / first_half_mape * 100) if first_half_mape > 0 else 0

            # Determine if degradation is significant
            is_degrading = mape_change > self.MAPE_TREND_THRESHOLD
            severity = "none"

            if is_degrading:
                if mape_change > self.MAPE_TREND_THRESHOLD * 2:
                    severity = "high"
                elif mape_change > self.MAPE_TREND_THRESHOLD:
                    severity = "medium"

            # Get products with worst performance
            poor_products = await self._identify_poor_performers(tenant_id, lookback_days)

            result = {
                "status": "analyzed",
                "period_days": lookback_days,
                "samples_analyzed": len(runs),
                "is_degrading": is_degrading,
                "severity": severity,
                "metrics": {
                    "first_period_mape": round(first_half_mape, 2),
                    "second_period_mape": round(second_half_mape, 2),
                    "mape_change": round(mape_change, 2),
                    "mape_change_percentage": round(mape_change_percentage, 2)
                },
                "poor_performers": poor_products,
                "recommendations": []
            }

            # Add recommendations
            if is_degrading:
                result["recommendations"].append({
                    "action": "retrain_models",
                    "priority": "high" if severity == "high" else "medium",
                    "reason": f"MAPE increased by {abs(mape_change):.1f}% over {lookback_days} days"
                })

            if poor_products:
                result["recommendations"].append({
                    "action": "retrain_poor_performers",
                    "priority": "high",
                    "reason": f"{len(poor_products)} products with MAPE > {self.MAPE_CRITICAL_THRESHOLD}%",
                    "products": poor_products[:10]  # Top 10 worst
                })

            logger.info(
                "Performance degradation analysis complete",
                tenant_id=tenant_id,
                is_degrading=is_degrading,
                severity=severity,
                poor_performers=len(poor_products)
            )

            return result

        except Exception as e:
            logger.error(
                "Failed to detect performance degradation",
                tenant_id=tenant_id,
                error=str(e)
            )
            raise DatabaseError(f"Failed to detect degradation: {str(e)}")

    async def _identify_poor_performers(
        self,
        tenant_id: uuid.UUID,
        lookback_days: int
    ) -> List[Dict[str, Any]]:
        """Identify products/locations with poor accuracy"""
        try:
            start_date = datetime.now(timezone.utc) - timedelta(days=lookback_days)

            # Get recent performance metrics grouped by product
            query = select(
                ModelPerformanceMetric.inventory_product_id,
                func.avg(ModelPerformanceMetric.mape).label('avg_mape'),
                func.avg(ModelPerformanceMetric.mae).label('avg_mae'),
                func.count(ModelPerformanceMetric.id).label('sample_count')
            ).where(
                and_(
                    ModelPerformanceMetric.tenant_id == tenant_id,
                    ModelPerformanceMetric.created_at >= start_date,
                    ModelPerformanceMetric.mape.isnot(None)
                )
            ).group_by(
                ModelPerformanceMetric.inventory_product_id
            ).having(
                func.avg(ModelPerformanceMetric.mape) > self.MAPE_CRITICAL_THRESHOLD
            ).order_by(
                desc(func.avg(ModelPerformanceMetric.mape))
            ).limit(20)

            result = await self.db.execute(query)
            poor_performers = []

            for row in result.fetchall():
                poor_performers.append({
                    "inventory_product_id": str(row.inventory_product_id),
                    "avg_mape": round(row.avg_mape, 2),
                    "avg_mae": round(row.avg_mae, 2),
                    "sample_count": row.sample_count,
                    "requires_retraining": True
                })

            return poor_performers

        except Exception as e:
            logger.error("Failed to identify poor performers", error=str(e))
            return []

    async def check_model_age(
        self,
        tenant_id: uuid.UUID,
        max_age_days: int = 30
    ) -> Dict[str, Any]:
        """
        Check if models are outdated and need retraining

        Args:
            tenant_id: Tenant identifier
            max_age_days: Maximum acceptable model age

        Returns:
            Analysis of model ages
        """
        try:
            # Get distinct models used in recent forecasts
            cutoff_date = datetime.now(timezone.utc) - timedelta(days=7)

            query = select(
                Forecast.model_id,
                Forecast.model_version,
                Forecast.inventory_product_id,
                func.max(Forecast.created_at).label('last_used'),
                func.count(Forecast.id).label('forecast_count')
            ).where(
                and_(
                    Forecast.tenant_id == tenant_id,
                    Forecast.created_at >= cutoff_date
                )
            ).group_by(
                Forecast.model_id,
                Forecast.model_version,
                Forecast.inventory_product_id
            )

            result = await self.db.execute(query)

            models_info = []
            outdated_count = 0

            for row in result.fetchall():
                # Check age against training service (would need to query training service)
                # For now, assume models older than max_age_days need retraining
                models_info.append({
                    "model_id": row.model_id,
                    "model_version": row.model_version,
                    "inventory_product_id": str(row.inventory_product_id),
                    "last_used": row.last_used.isoformat(),
                    "forecast_count": row.forecast_count
                })

            return {
                "status": "analyzed",
                "models_in_use": len(models_info),
                "outdated_models": outdated_count,
                "max_age_days": max_age_days,
                "models": models_info[:20]  # Top 20
            }

        except Exception as e:
            logger.error("Failed to check model age", error=str(e))
            raise DatabaseError(f"Failed to check model age: {str(e)}")

    async def generate_performance_report(
        self,
        tenant_id: uuid.UUID,
        days: int = 30
    ) -> Dict[str, Any]:
        """
        Generate comprehensive performance report

        Args:
            tenant_id: Tenant identifier
            days: Analysis period

        Returns:
            Complete performance report with recommendations
        """
        try:
            logger.info(
                "Generating performance report",
                tenant_id=tenant_id,
                days=days
            )

            # Get all analyses
            summary = await self.get_accuracy_summary(tenant_id, days)
            degradation = await self.detect_performance_degradation(tenant_id, days)
            model_age = await self.check_model_age(tenant_id)

            # Compile recommendations
            all_recommendations = []

            if summary.get("health_status") == "critical":
                all_recommendations.append({
                    "priority": "critical",
                    "action": "immediate_review",
                    "reason": f"Overall MAPE is {summary['average_metrics']['mape']}%",
                    "details": "Forecast accuracy is critically low"
                })

            all_recommendations.extend(degradation.get("recommendations", []))

            report = {
                "generated_at": datetime.now(timezone.utc).isoformat(),
                "tenant_id": str(tenant_id),
                "analysis_period_days": days,
                "summary": summary,
                "degradation_analysis": degradation,
                "model_age_analysis": model_age,
                "recommendations": all_recommendations,
                "requires_action": len(all_recommendations) > 0
            }

            logger.info(
                "Performance report generated",
                tenant_id=tenant_id,
                health_status=summary.get("health_status"),
                recommendations_count=len(all_recommendations)
            )

            return report

        except Exception as e:
            logger.error(
                "Failed to generate performance report",
                tenant_id=tenant_id,
                error=str(e)
            )
            raise DatabaseError(f"Failed to generate report: {str(e)}")
