"""
Forecast Repository
Repository for forecast operations
"""

from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text, desc, func
from sqlalchemy.exc import IntegrityError
from datetime import datetime, timedelta, date, timezone
import structlog

from .base import ForecastingBaseRepository
from app.models.forecasts import Forecast
from shared.database.exceptions import DatabaseError, ValidationError

logger = structlog.get_logger()


class ForecastRepository(ForecastingBaseRepository):
    """Repository for forecast operations"""
    
    def __init__(self, session: AsyncSession, cache_ttl: Optional[int] = 600):
        # Forecasts are relatively stable, medium cache time (10 minutes)
        super().__init__(Forecast, session, cache_ttl)
    
    async def create_forecast(self, forecast_data: Dict[str, Any]) -> Forecast:
        """
        Create a new forecast with validation.

        Handles duplicate forecast race condition gracefully:
        If a forecast already exists for the same (tenant, product, date, location),
        it will be updated instead of creating a duplicate.
        """
        try:
            # Validate forecast data
            validation_result = self._validate_forecast_data(
                forecast_data,
                ["tenant_id", "inventory_product_id", "location", "forecast_date",
                 "predicted_demand", "confidence_lower", "confidence_upper", "model_id"]
            )

            if not validation_result["is_valid"]:
                raise ValidationError(f"Invalid forecast data: {validation_result['errors']}")

            # Set default values
            if "confidence_level" not in forecast_data:
                forecast_data["confidence_level"] = 0.8
            if "algorithm" not in forecast_data:
                forecast_data["algorithm"] = "prophet"
            if "business_type" not in forecast_data:
                forecast_data["business_type"] = "individual"

            # Try to create forecast
            try:
                forecast = await self.create(forecast_data)

                logger.info("Forecast created successfully",
                           forecast_id=forecast.id,
                           tenant_id=forecast.tenant_id,
                           inventory_product_id=forecast.inventory_product_id,
                           forecast_date=forecast.forecast_date.isoformat())

                return forecast

            except IntegrityError as ie:
                # Handle unique constraint violation (duplicate forecast)
                error_msg = str(ie).lower()
                if "unique constraint" in error_msg or "duplicate" in error_msg or "uq_forecast_tenant_product_date_location" in error_msg:
                    logger.warning("Forecast already exists (race condition), updating instead",
                                  tenant_id=forecast_data.get("tenant_id"),
                                  inventory_product_id=forecast_data.get("inventory_product_id"),
                                  forecast_date=str(forecast_data.get("forecast_date")))

                    # Rollback the failed insert
                    await self.session.rollback()

                    # Fetch the existing forecast
                    existing_forecast = await self.get_existing_forecast(
                        tenant_id=forecast_data["tenant_id"],
                        inventory_product_id=forecast_data["inventory_product_id"],
                        forecast_date=forecast_data["forecast_date"],
                        location=forecast_data["location"]
                    )

                    if existing_forecast:
                        # Update existing forecast with new prediction data
                        update_data = {
                            "predicted_demand": forecast_data["predicted_demand"],
                            "confidence_lower": forecast_data["confidence_lower"],
                            "confidence_upper": forecast_data["confidence_upper"],
                            "confidence_level": forecast_data.get("confidence_level", 0.8),
                            "model_id": forecast_data["model_id"],
                            "model_version": forecast_data.get("model_version"),
                            "algorithm": forecast_data.get("algorithm", "prophet"),
                            "processing_time_ms": forecast_data.get("processing_time_ms"),
                            "features_used": forecast_data.get("features_used"),
                            "weather_temperature": forecast_data.get("weather_temperature"),
                            "weather_precipitation": forecast_data.get("weather_precipitation"),
                            "weather_description": forecast_data.get("weather_description"),
                        }

                        updated_forecast = await self.update(str(existing_forecast.id), update_data)

                        logger.info("Existing forecast updated after duplicate detection",
                                   forecast_id=updated_forecast.id,
                                   tenant_id=updated_forecast.tenant_id,
                                   inventory_product_id=updated_forecast.inventory_product_id)

                        return updated_forecast
                    else:
                        # This shouldn't happen, but log it
                        logger.error("Duplicate forecast detected but not found in database")
                        raise DatabaseError("Duplicate forecast detected but not found")
                else:
                    # Different integrity error, re-raise
                    raise

        except ValidationError:
            raise
        except IntegrityError as ie:
            # Re-raise integrity errors that weren't handled above
            logger.error("Database integrity error creating forecast",
                        tenant_id=forecast_data.get("tenant_id"),
                        error=str(ie))
            raise DatabaseError(f"Database integrity error: {str(ie)}")
        except Exception as e:
            logger.error("Failed to create forecast",
                        tenant_id=forecast_data.get("tenant_id"),
                        inventory_product_id=forecast_data.get("inventory_product_id"),
                        error=str(e))
            raise DatabaseError(f"Failed to create forecast: {str(e)}")

    async def get_existing_forecast(
        self,
        tenant_id: str,
        inventory_product_id: str,
        forecast_date: datetime,
        location: str
    ) -> Optional[Forecast]:
        """Get an existing forecast by unique key (tenant, product, date, location)"""
        try:
            query = select(Forecast).where(
                and_(
                    Forecast.tenant_id == tenant_id,
                    Forecast.inventory_product_id == inventory_product_id,
                    Forecast.forecast_date == forecast_date,
                    Forecast.location == location
                )
            )
            result = await self.session.execute(query)
            return result.scalar_one_or_none()
        except Exception as e:
            logger.error("Failed to get existing forecast", error=str(e))
            return None
    
    async def get_forecasts_by_date_range(
        self,
        tenant_id: str,
        start_date: date,
        end_date: date,
        inventory_product_id: str = None,
        location: str = None
    ) -> List[Forecast]:
        """Get forecasts within a date range"""
        try:
            filters = {"tenant_id": tenant_id}
            
            if inventory_product_id:
                filters["inventory_product_id"] = inventory_product_id
            if location:
                filters["location"] = location
            
            # Convert dates to datetime for comparison
            start_datetime = datetime.combine(start_date, datetime.min.time())
            end_datetime = datetime.combine(end_date, datetime.max.time())
            
            return await self.get_by_date_range(
                tenant_id, start_datetime, end_datetime
            )
            
        except Exception as e:
            logger.error("Failed to get forecasts by date range",
                        tenant_id=tenant_id,
                        start_date=start_date,
                        end_date=end_date,
                        error=str(e))
            raise DatabaseError(f"Failed to get forecasts: {str(e)}")
    
    async def get_latest_forecast_for_product(
        self,
        tenant_id: str,
        inventory_product_id: str,
        location: str = None
    ) -> Optional[Forecast]:
        """Get the most recent forecast for a product"""
        try:
            filters = {
                "tenant_id": tenant_id,
                "inventory_product_id": inventory_product_id
            }
            if location:
                filters["location"] = location
            
            forecasts = await self.get_multi(
                filters=filters,
                limit=1,
                order_by="forecast_date",
                order_desc=True
            )
            
            return forecasts[0] if forecasts else None
            
        except Exception as e:
            logger.error("Failed to get latest forecast for product",
                        tenant_id=tenant_id,
                        inventory_product_id=inventory_product_id,
                        error=str(e))
            raise DatabaseError(f"Failed to get latest forecast: {str(e)}")
    
    async def get_forecasts_for_date(
        self,
        tenant_id: str,
        forecast_date: date,
        inventory_product_id: str = None
    ) -> List[Forecast]:
        """Get all forecasts for a specific date"""
        try:
            # Convert date to datetime range
            start_datetime = datetime.combine(forecast_date, datetime.min.time())
            end_datetime = datetime.combine(forecast_date, datetime.max.time())
            
            return await self.get_by_date_range(
                tenant_id, start_datetime, end_datetime
            )
            
        except Exception as e:
            logger.error("Failed to get forecasts for date",
                        tenant_id=tenant_id,
                        forecast_date=forecast_date,
                        error=str(e))
            raise DatabaseError(f"Failed to get forecasts for date: {str(e)}")
    
    async def get_forecast_accuracy_metrics(
        self,
        tenant_id: str,
        inventory_product_id: str = None,
        days_back: int = 30
    ) -> Dict[str, Any]:
        """Get forecast accuracy metrics"""
        try:
            cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_back)
            
            # Build base query conditions
            conditions = ["tenant_id = :tenant_id", "forecast_date >= :cutoff_date"]
            params = {
                "tenant_id": tenant_id,
                "cutoff_date": cutoff_date
            }
            
            if inventory_product_id:
                conditions.append("inventory_product_id = :inventory_product_id")
                params["inventory_product_id"] = inventory_product_id
            
            query_text = f"""
                SELECT 
                    COUNT(*) as total_forecasts,
                    AVG(predicted_demand) as avg_predicted_demand,
                    MIN(predicted_demand) as min_predicted_demand,
                    MAX(predicted_demand) as max_predicted_demand,
                    AVG(confidence_upper - confidence_lower) as avg_confidence_interval,
                    AVG(processing_time_ms) as avg_processing_time_ms,
                    COUNT(DISTINCT inventory_product_id) as unique_products,
                    COUNT(DISTINCT model_id) as unique_models
                FROM forecasts 
                WHERE {' AND '.join(conditions)}
            """
            
            result = await self.session.execute(text(query_text), params)
            row = result.fetchone()
            
            if row and row.total_forecasts > 0:
                return {
                    "total_forecasts": int(row.total_forecasts),
                    "avg_predicted_demand": float(row.avg_predicted_demand or 0),
                    "min_predicted_demand": float(row.min_predicted_demand or 0),
                    "max_predicted_demand": float(row.max_predicted_demand or 0),
                    "avg_confidence_interval": float(row.avg_confidence_interval or 0),
                    "avg_processing_time_ms": float(row.avg_processing_time_ms or 0),
                    "unique_products": int(row.unique_products or 0),
                    "unique_models": int(row.unique_models or 0),
                    "period_days": days_back
                }
            
            return {
                "total_forecasts": 0,
                "avg_predicted_demand": 0.0,
                "min_predicted_demand": 0.0,
                "max_predicted_demand": 0.0,
                "avg_confidence_interval": 0.0,
                "avg_processing_time_ms": 0.0,
                "unique_products": 0,
                "unique_models": 0,
                "period_days": days_back
            }
            
        except Exception as e:
            logger.error("Failed to get forecast accuracy metrics",
                        tenant_id=tenant_id,
                        error=str(e))
            return {
                "total_forecasts": 0,
                "avg_predicted_demand": 0.0,
                "min_predicted_demand": 0.0,
                "max_predicted_demand": 0.0,
                "avg_confidence_interval": 0.0,
                "avg_processing_time_ms": 0.0,
                "unique_products": 0,
                "unique_models": 0,
                "period_days": days_back
            }
    
    async def get_demand_trends(
        self,
        tenant_id: str,
        inventory_product_id: str,
        days_back: int = 30
    ) -> Dict[str, Any]:
        """Get demand trends for a product"""
        try:
            cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_back)
            
            query_text = """
                SELECT 
                    DATE(forecast_date) as date,
                    AVG(predicted_demand) as avg_demand,
                    MIN(predicted_demand) as min_demand,
                    MAX(predicted_demand) as max_demand,
                    COUNT(*) as forecast_count
                FROM forecasts 
                WHERE tenant_id = :tenant_id 
                AND inventory_product_id = :inventory_product_id
                AND forecast_date >= :cutoff_date
                GROUP BY DATE(forecast_date)
                ORDER BY date DESC
            """
            
            result = await self.session.execute(text(query_text), {
                "tenant_id": tenant_id,
                "inventory_product_id": inventory_product_id,
                "cutoff_date": cutoff_date
            })
            
            trends = []
            for row in result.fetchall():
                trends.append({
                    "date": row.date.isoformat() if row.date else None,
                    "avg_demand": float(row.avg_demand),
                    "min_demand": float(row.min_demand),
                    "max_demand": float(row.max_demand),
                    "forecast_count": int(row.forecast_count)
                })
            
            # Calculate overall trend direction
            if len(trends) >= 2:
                recent_avg = sum(t["avg_demand"] for t in trends[:7]) / min(7, len(trends))
                older_avg = sum(t["avg_demand"] for t in trends[-7:]) / min(7, len(trends[-7:]))
                trend_direction = "increasing" if recent_avg > older_avg else "decreasing"
            else:
                trend_direction = "stable"
            
            return {
                "inventory_product_id": inventory_product_id,
                "period_days": days_back,
                "trends": trends,
                "trend_direction": trend_direction,
                "total_data_points": len(trends)
            }
            
        except Exception as e:
            logger.error("Failed to get demand trends",
                        tenant_id=tenant_id,
                        inventory_product_id=inventory_product_id,
                        error=str(e))
            return {
                "inventory_product_id": inventory_product_id,
                "period_days": days_back,
                "trends": [],
                "trend_direction": "unknown",
                "total_data_points": 0
            }
    
    async def get_model_usage_statistics(self, tenant_id: str) -> Dict[str, Any]:
        """Get statistics about model usage"""
        try:
            # Get model usage counts
            model_query = text("""
                SELECT 
                    model_id,
                    algorithm,
                    COUNT(*) as usage_count,
                    AVG(predicted_demand) as avg_prediction,
                    MAX(forecast_date) as last_used,
                    COUNT(DISTINCT inventory_product_id) as products_covered
                FROM forecasts 
                WHERE tenant_id = :tenant_id
                GROUP BY model_id, algorithm
                ORDER BY usage_count DESC
            """)
            
            result = await self.session.execute(model_query, {"tenant_id": tenant_id})
            
            model_stats = []
            for row in result.fetchall():
                model_stats.append({
                    "model_id": row.model_id,
                    "algorithm": row.algorithm,
                    "usage_count": int(row.usage_count),
                    "avg_prediction": float(row.avg_prediction),
                    "last_used": row.last_used.isoformat() if row.last_used else None,
                    "products_covered": int(row.products_covered)
                })
            
            # Get algorithm distribution
            algorithm_query = text("""
                SELECT algorithm, COUNT(*) as count 
                FROM forecasts 
                WHERE tenant_id = :tenant_id
                GROUP BY algorithm
            """)
            
            algorithm_result = await self.session.execute(algorithm_query, {"tenant_id": tenant_id})
            algorithm_distribution = {row.algorithm: row.count for row in algorithm_result.fetchall()}
            
            return {
                "model_statistics": model_stats,
                "algorithm_distribution": algorithm_distribution,
                "total_unique_models": len(model_stats)
            }
            
        except Exception as e:
            logger.error("Failed to get model usage statistics",
                        tenant_id=tenant_id,
                        error=str(e))
            return {
                "model_statistics": [],
                "algorithm_distribution": {},
                "total_unique_models": 0
            }
    
    async def cleanup_old_forecasts(self, days_old: int = 90) -> int:
        """Clean up old forecasts"""
        return await self.cleanup_old_records(days_old=days_old)
    
    async def get_forecast_summary(self, tenant_id: str) -> Dict[str, Any]:
        """Get comprehensive forecast summary for a tenant"""
        try:
            # Get basic statistics
            basic_stats = await self.get_statistics_by_tenant(tenant_id)
            
            # Get accuracy metrics
            accuracy_metrics = await self.get_forecast_accuracy_metrics(tenant_id)
            
            # Get model usage
            model_usage = await self.get_model_usage_statistics(tenant_id)
            
            # Get recent activity
            recent_forecasts = await self.get_recent_records(tenant_id, hours=24)
            
            return {
                "tenant_id": tenant_id,
                "basic_statistics": basic_stats,
                "accuracy_metrics": accuracy_metrics,
                "model_usage": model_usage,
                "recent_activity": {
                    "forecasts_last_24h": len(recent_forecasts),
                    "latest_forecast": recent_forecasts[0].forecast_date.isoformat() if recent_forecasts else None
                }
            }
            
        except Exception as e:
            logger.error("Failed to get forecast summary",
                        tenant_id=tenant_id,
                        error=str(e))
            return {"error": f"Failed to get forecast summary: {str(e)}"}
    
    async def get_forecasts_by_date(
        self,
        tenant_id: str,
        forecast_date: date,
        inventory_product_id: str = None
    ) -> List[Forecast]:
        """
        Get all forecasts for a specific date.
        Used for forecast validation against actual sales.

        Args:
            tenant_id: Tenant UUID
            forecast_date: Date to get forecasts for
            inventory_product_id: Optional product filter

        Returns:
            List of forecasts for the date
        """
        try:
            query = select(Forecast).where(
                and_(
                    Forecast.tenant_id == tenant_id,
                    func.date(Forecast.forecast_date) == forecast_date
                )
            )

            if inventory_product_id:
                query = query.where(Forecast.inventory_product_id == inventory_product_id)

            result = await self.session.execute(query)
            forecasts = result.scalars().all()

            logger.info("Retrieved forecasts by date",
                       tenant_id=tenant_id,
                       forecast_date=forecast_date.isoformat(),
                       count=len(forecasts))

            return list(forecasts)

        except Exception as e:
            logger.error("Failed to get forecasts by date",
                        tenant_id=tenant_id,
                        forecast_date=forecast_date.isoformat(),
                        error=str(e))
            raise DatabaseError(f"Failed to get forecasts: {str(e)}")

    async def bulk_create_forecasts(self, forecasts_data: List[Dict[str, Any]]) -> List[Forecast]:
        """Bulk create multiple forecasts"""
        try:
            created_forecasts = []

            for forecast_data in forecasts_data:
                # Validate each forecast
                validation_result = self._validate_forecast_data(
                    forecast_data,
                    ["tenant_id", "inventory_product_id", "location", "forecast_date",
                     "predicted_demand", "confidence_lower", "confidence_upper", "model_id"]
                )

                if not validation_result["is_valid"]:
                    logger.warning("Skipping invalid forecast data",
                                  errors=validation_result["errors"],
                                  data=forecast_data)
                    continue

                forecast = await self.create(forecast_data)
                created_forecasts.append(forecast)

            logger.info("Bulk created forecasts",
                       requested_count=len(forecasts_data),
                       created_count=len(created_forecasts))

            return created_forecasts

        except Exception as e:
            logger.error("Failed to bulk create forecasts",
                        requested_count=len(forecasts_data),
                        error=str(e))
            raise DatabaseError(f"Bulk forecast creation failed: {str(e)}")