# shared/clients/forecast_client.py
"""
Forecast Service Client for Inter-Service Communication

This client provides a high-level API for interacting with the Forecasting Service,
which generates demand predictions using Prophet ML algorithm, validates forecast accuracy,
and provides enterprise network demand aggregation for multi-location bakeries.

Key Capabilities:
- Forecast Generation: Single product, multi-day, batch forecasting
- Real-Time Predictions: On-demand predictions with custom features
- Forecast Validation: Compare predictions vs actual sales, track accuracy
- Analytics: Prediction performance metrics, historical accuracy trends
- Enterprise Aggregation: Network-wide demand forecasting for parent-child hierarchies
- Caching: Redis-backed caching for high-performance prediction serving

Backend Architecture:
- ATOMIC: /forecasting/forecasts (CRUD operations on forecast records)
- BUSINESS: /forecasting/operations/* (forecast generation, validation)
- ANALYTICS: /forecasting/analytics/* (performance metrics, accuracy trends)
- ENTERPRISE: /forecasting/enterprise/* (network demand aggregation)

Enterprise Features (NEW):
- Network demand aggregation across all child outlets for centralized production planning
- Child contribution tracking (each outlet's % of total network demand)
- Redis caching with 1-hour TTL for enterprise forecasts
- Subscription gating (requires Enterprise tier)

Usage Example:
    ```python
    from shared.clients import get_forecast_client
    from shared.config.base import get_settings
    from datetime import date, timedelta

    config = get_settings()
    client = get_forecast_client(config, calling_service_name="production")

    # Generate 7-day forecast for a product
    forecast = await client.generate_multi_day_forecast(
        tenant_id=tenant_id,
        inventory_product_id=product_id,
        forecast_date=date.today(),
        forecast_days=7,
        include_recommendations=True
    )

    # Batch forecast for multiple products
    batch_forecast = await client.generate_batch_forecast(
        tenant_id=tenant_id,
        inventory_product_ids=[product_id_1, product_id_2],
        forecast_date=date.today(),
        forecast_days=7
    )

    # Validate forecasts against actual sales
    validation = await client.validate_forecasts(
        tenant_id=tenant_id,
        date=date.today() - timedelta(days=1)
    )

    # Get predictions for a specific date (from cache or DB)
    predictions = await client.get_predictions_for_date(
        tenant_id=tenant_id,
        target_date=date.today()
    )
    ```

Service Architecture:
- Base URL: Configured via FORECASTING_SERVICE_URL environment variable
- Authentication: Uses BaseServiceClient with tenant_id header validation
- Error Handling: Returns None on errors, logs detailed error context
- Async: All methods are async and use httpx for HTTP communication
- Caching: 24-hour TTL for standard forecasts, 1-hour TTL for enterprise aggregations

ML Model Details:
- Algorithm: Facebook Prophet (time series forecasting)
- Features: 20+ temporal, weather, traffic, holiday, POI features
- Accuracy: 15-25% MAPE (Mean Absolute Percentage Error)
- Training: Weekly retraining via orchestrator automation
- Confidence Intervals: 95% confidence bounds (yhat_lower, yhat_upper)

Related Services:
- Production Service: Uses forecasts for production planning
- Procurement Service: Uses forecasts for ingredient ordering
- Orchestrator Service: Triggers daily forecast generation, displays network forecasts on enterprise dashboard
- Tenant Service: Validates hierarchy for enterprise aggregation
- Distribution Service: Network forecasts inform capacity planning

For more details, see services/forecasting/README.md
"""

from typing import Dict, Any, Optional, List
from datetime import date
import structlog
from .base_service_client import BaseServiceClient
from shared.config.base import BaseServiceSettings

logger = structlog.get_logger()


class ForecastServiceClient(BaseServiceClient):
    """Client for communicating with the forecasting service"""

    def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"):
        super().__init__(calling_service_name, config)

    def get_service_base_path(self) -> str:
        return "/api/v1"

    # ================================================================
    # ATOMIC: Forecast CRUD Operations
    # ================================================================

    async def get_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
        """Get forecast details by ID"""
        return await self.get(f"forecasting/forecasts/{forecast_id}", tenant_id=tenant_id)

    async def list_forecasts(
        self,
        tenant_id: str,
        inventory_product_id: Optional[str] = None,
        start_date: Optional[date] = None,
        end_date: Optional[date] = None,
        limit: int = 50,
        offset: int = 0
    ) -> Optional[List[Dict[str, Any]]]:
        """List forecasts for a tenant with optional filters"""
        params = {"limit": limit, "offset": offset}
        if inventory_product_id:
            params["inventory_product_id"] = inventory_product_id
        if start_date:
            params["start_date"] = start_date.isoformat()
        if end_date:
            params["end_date"] = end_date.isoformat()

        return await self.get("forecasting/forecasts", tenant_id=tenant_id, params=params)

    async def delete_forecast(self, tenant_id: str, forecast_id: str) -> Optional[Dict[str, Any]]:
        """Delete a forecast"""
        return await self.delete(f"forecasting/forecasts/{forecast_id}", tenant_id=tenant_id)

    # ================================================================
    # BUSINESS: Forecasting Operations
    # ================================================================

    async def generate_single_forecast(
        self,
        tenant_id: str,
        inventory_product_id: str,
        forecast_date: date,
        include_recommendations: bool = False
    ) -> Optional[Dict[str, Any]]:
        """Generate a single product forecast"""
        data = {
            "inventory_product_id": inventory_product_id,
            "forecast_date": forecast_date.isoformat(),
            "include_recommendations": include_recommendations
        }
        return await self.post("forecasting/operations/single", data=data, tenant_id=tenant_id)

    async def generate_multi_day_forecast(
        self,
        tenant_id: str,
        inventory_product_id: str,
        forecast_date: date,
        forecast_days: int = 7,
        include_recommendations: bool = False
    ) -> Optional[Dict[str, Any]]:
        """Generate multiple daily forecasts for the specified period"""
        data = {
            "inventory_product_id": inventory_product_id,
            "forecast_date": forecast_date.isoformat(),
            "forecast_days": forecast_days,
            "include_recommendations": include_recommendations
        }
        return await self.post("forecasting/operations/multi-day", data=data, tenant_id=tenant_id)

    async def generate_batch_forecast(
        self,
        tenant_id: str,
        inventory_product_ids: List[str],
        forecast_date: date,
        forecast_days: int = 1
    ) -> Optional[Dict[str, Any]]:
        """Generate forecasts for multiple products in batch"""
        data = {
            "inventory_product_ids": inventory_product_ids,
            "forecast_date": forecast_date.isoformat(),
            "forecast_days": forecast_days
        }
        return await self.post("forecasting/operations/batch", data=data, tenant_id=tenant_id)

    async def generate_realtime_prediction(
        self,
        tenant_id: str,
        inventory_product_id: str,
        model_id: str,
        features: Dict[str, Any],
        model_path: Optional[str] = None,
        confidence_level: float = 0.8
    ) -> Optional[Dict[str, Any]]:
        """Generate real-time prediction"""
        data = {
            "inventory_product_id": inventory_product_id,
            "model_id": model_id,
            "features": features,
            "confidence_level": confidence_level
        }
        if model_path:
            data["model_path"] = model_path

        return await self.post("forecasting/operations/realtime", data=data, tenant_id=tenant_id)

    async def validate_predictions(
        self,
        tenant_id: str,
        start_date: date,
        end_date: date
    ) -> Optional[Dict[str, Any]]:
        """Validate predictions against actual sales data"""
        params = {
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat()
        }
        return await self.post("forecasting/operations/validate-predictions", params=params, tenant_id=tenant_id)

    async def validate_forecasts(
        self,
        tenant_id: str,
        date: date
    ) -> Optional[Dict[str, Any]]:
        """
        Validate forecasts for a specific date against actual sales.
        Calculates MAPE, RMSE, MAE and identifies products with poor accuracy.

        Args:
            tenant_id: Tenant UUID
            date: Date to validate (validates this single day)

        Returns:
            Dict with overall metrics and poor accuracy products list
        """
        from datetime import datetime, timezone

        # Convert date to datetime with timezone for start/end of day
        start_datetime = datetime.combine(date, datetime.min.time()).replace(tzinfo=timezone.utc)
        end_datetime = datetime.combine(date, datetime.max.time()).replace(tzinfo=timezone.utc)

        # Call the new validation endpoint
        result = await self.post(
            "forecasting/validation/validate-yesterday",
            params={"orchestration_run_id": None},
            tenant_id=tenant_id
        )

        if not result:
            return None

        # Transform the new response format to match the expected format
        overall_metrics = result.get("overall_metrics", {})

        # Get poor accuracy products from the result
        poor_accuracy_products = result.get("poor_accuracy_products", [])

        return {
            "overall_mape": overall_metrics.get("mape", 0),
            "overall_rmse": overall_metrics.get("rmse", 0),
            "overall_mae": overall_metrics.get("mae", 0),
            "overall_r2_score": overall_metrics.get("r2_score", 0),
            "overall_accuracy_percentage": overall_metrics.get("accuracy_percentage", 0),
            "products_validated": result.get("forecasts_with_actuals", 0),
            "poor_accuracy_products": poor_accuracy_products,
            "validation_run_id": result.get("validation_run_id"),
            "forecasts_evaluated": result.get("forecasts_evaluated", 0),
            "forecasts_with_actuals": result.get("forecasts_with_actuals", 0),
            "forecasts_without_actuals": result.get("forecasts_without_actuals", 0)
        }

    async def get_forecast_statistics(
        self,
        tenant_id: str,
        start_date: Optional[date] = None,
        end_date: Optional[date] = None
    ) -> Optional[Dict[str, Any]]:
        """Get forecast statistics"""
        params = {}
        if start_date:
            params["start_date"] = start_date.isoformat()
        if end_date:
            params["end_date"] = end_date.isoformat()

        return await self.get("forecasting/operations/statistics", tenant_id=tenant_id, params=params)

    async def clear_prediction_cache(self, tenant_id: str) -> Optional[Dict[str, Any]]:
        """Clear prediction cache"""
        return await self.delete("forecasting/operations/cache", tenant_id=tenant_id)

    # ================================================================
    # ANALYTICS: Forecasting Analytics
    # ================================================================

    async def get_predictions_performance(
        self,
        tenant_id: str,
        start_date: Optional[date] = None,
        end_date: Optional[date] = None
    ) -> Optional[Dict[str, Any]]:
        """Get predictions performance analytics"""
        params = {}
        if start_date:
            params["start_date"] = start_date.isoformat()
        if end_date:
            params["end_date"] = end_date.isoformat()

        return await self.get("forecasting/analytics/predictions-performance", tenant_id=tenant_id, params=params)

    # ================================================================
    # ML INSIGHTS: Dynamic Rules Generation
    # ================================================================

    async def trigger_rules_generation(
        self,
        tenant_id: str,
        product_ids: Optional[List[str]] = None,
        lookback_days: int = 90,
        min_samples: int = 10
    ) -> Optional[Dict[str, Any]]:
        """
        Trigger dynamic business rules learning for demand forecasting.

        Args:
            tenant_id: Tenant UUID
            product_ids: Specific product IDs to analyze. If None, analyzes all products
            lookback_days: Days of historical data to analyze (30-365)
            min_samples: Minimum samples required for rule learning (5-100)

        Returns:
            Dict with rules generation results including insights posted
        """
        data = {
            "product_ids": product_ids,
            "lookback_days": lookback_days,
            "min_samples": min_samples
        }
        return await self.post("forecasting/ml/insights/generate-rules", data=data, tenant_id=tenant_id)

    async def trigger_demand_insights_internal(
        self,
        tenant_id: str
    ) -> Optional[Dict[str, Any]]:
        """
        Trigger demand forecasting insights for a tenant (internal service use only).

        This method calls the internal endpoint which is protected by x-internal-service header.
        Used by demo-session service after cloning to generate AI insights from seeded data.

        Args:
            tenant_id: Tenant ID to trigger insights for

        Returns:
            Dict with trigger results or None if failed
        """
        try:
            result = await self._make_request(
                method="POST",
                endpoint=f"forecasting/internal/ml/generate-demand-insights",
                tenant_id=tenant_id,
                data={"tenant_id": tenant_id},
                headers={"x-internal-service": "demo-session"}
            )

            if result:
                logger.info(
                    "Demand insights triggered successfully via internal endpoint",
                    tenant_id=tenant_id,
                    insights_posted=result.get("insights_posted", 0)
                )
            else:
                logger.warning(
                    "Demand insights internal endpoint returned no result",
                    tenant_id=tenant_id
                )

            return result

        except Exception as e:
            logger.error(
                "Failed to trigger demand insights",
                tenant_id=tenant_id,
                error=str(e)
            )
            return None

    # ================================================================
    # Legacy/Compatibility Methods (deprecated)
    # ================================================================

    async def generate_forecasts(
        self,
        tenant_id: str,
        forecast_days: int = 7,
        inventory_product_ids: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        """
        COMPATIBILITY: Orchestrator-friendly method to generate forecasts

        This method is called by the orchestrator service and generates batch forecasts
        for either specified products or all products.

        Args:
            tenant_id: Tenant UUID
            forecast_days: Number of days to forecast (default 7)
            inventory_product_ids: Optional list of product IDs. If None, forecasts all products.

        Returns:
            Dict with forecast results
        """
        from datetime import datetime

        # If no product IDs specified, let the backend handle it
        if not inventory_product_ids:
            # Call the batch operation endpoint to forecast all products
            # The forecasting service will handle fetching all products internally
            data = {
                "batch_name": f"orchestrator-batch-{datetime.now().strftime('%Y%m%d')}",
                "inventory_product_ids": [],  # Empty list will trigger fetching all products
                "forecast_days": forecast_days
            }
            return await self.post("forecasting/operations/batch", data=data, tenant_id=tenant_id)

        # Otherwise use the standard batch forecast
        return await self.generate_batch_forecast(
            tenant_id=tenant_id,
            inventory_product_ids=inventory_product_ids,
            forecast_date=datetime.now().date(),
            forecast_days=forecast_days
        )

    async def get_aggregated_forecast(
        self,
        parent_tenant_id: str,
        start_date: date,
        end_date: date,
        product_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Get aggregated forecast for enterprise tenant and all children.

        This method calls the enterprise forecasting aggregation endpoint which
        combines demand forecasts across the parent tenant and all child tenants
        in the network. Used for centralized production planning.

        Args:
            parent_tenant_id: The parent tenant (central bakery) UUID
            start_date: Start date for forecast range
            end_date: End date for forecast range
            product_id: Optional product ID to filter forecasts

        Returns:
            Aggregated forecast data including:
            - total_demand: Sum of all child demands
            - child_contributions: Per-child demand breakdown
            - forecast_date_range: Date range for the forecast
            - cached: Whether data was served from Redis cache
        """
        params = {
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat()
        }
        if product_id:
            params["product_id"] = product_id

        # Use _make_request directly because the base_service_client adds /tenants/{tenant_id}/ prefix
        # Gateway route is: /api/v1/tenants/{tenant_id}/forecasting/enterprise/{path}
        # So we need the full path without tenant_id parameter to avoid double prefixing
        return await self._make_request(
            "GET",
            f"tenants/{parent_tenant_id}/forecasting/enterprise/aggregated",
            params=params
        )

    async def create_forecast(
        self,
        tenant_id: str,
        model_id: str,
        start_date: str,
        end_date: str,
        product_ids: Optional[List[str]] = None,
        include_confidence_intervals: bool = True,
        **kwargs
    ) -> Optional[Dict[str, Any]]:
        """
        DEPRECATED: Use generate_single_forecast or generate_batch_forecast instead
        Legacy method for backward compatibility
        """
        # Map to new batch forecast operation
        if product_ids:
            return await self.generate_batch_forecast(
                tenant_id=tenant_id,
                inventory_product_ids=product_ids,
                forecast_date=date.fromisoformat(start_date),
                forecast_days=1
            )
        return None


# Backward compatibility alias
def create_forecast_client(config: BaseServiceSettings, service_name: str = "unknown") -> ForecastServiceClient:
    """Create a forecast service client (backward compatibility)"""
    return ForecastServiceClient(config, service_name)
