"""
Safety Stock Calculator

Calculates dynamic safety stock based on demand variability,
lead time, and service level targets.
"""

import math
import statistics
from decimal import Decimal
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
import logging

logger = logging.getLogger(__name__)


@dataclass
class SafetyStockResult:
    """Result of safety stock calculation"""
    safety_stock_quantity: Decimal
    service_level: float
    z_score: float
    demand_std_dev: float
    lead_time_days: int
    calculation_method: str
    confidence: str  # 'high', 'medium', 'low'
    reasoning_data: dict


@dataclass
class DemandHistory:
    """Historical demand data for an ingredient"""
    ingredient_id: str
    daily_demands: List[float]  # Historical daily demands
    mean_demand: float
    std_dev: float
    coefficient_of_variation: float


class SafetyStockCalculator:
    """
    Calculates safety stock using statistical methods.

    Formula: Safety Stock = Z × σ × √L
    where:
    - Z = service level z-score (e.g., 1.96 for 97.5%)
    - σ = demand standard deviation
    - L = lead time in days

    This accounts for demand variability during lead time.
    """

    # Z-scores for common service levels
    SERVICE_LEVEL_Z_SCORES = {
        0.50: 0.00,   # 50% - no buffer (not recommended)
        0.80: 0.84,   # 80% service level
        0.85: 1.04,   # 85% service level
        0.90: 1.28,   # 90% service level
        0.95: 1.65,   # 95% service level
        0.975: 1.96,  # 97.5% service level
        0.99: 2.33,   # 99% service level
        0.995: 2.58,  # 99.5% service level
        0.999: 3.09   # 99.9% service level
    }

    def __init__(self, default_service_level: float = 0.95):
        """
        Initialize safety stock calculator.

        Args:
            default_service_level: Default target service level (0-1)
        """
        self.default_service_level = default_service_level

    def calculate_safety_stock(
        self,
        demand_std_dev: float,
        lead_time_days: int,
        service_level: Optional[float] = None
    ) -> SafetyStockResult:
        """
        Calculate safety stock using standard formula.

        Safety Stock = Z × σ × √L

        Args:
            demand_std_dev: Standard deviation of daily demand
            lead_time_days: Supplier lead time in days
            service_level: Target service level (uses default if None)

        Returns:
            SafetyStockResult with calculation details
        """
        if service_level is None:
            service_level = self.default_service_level

        # Get z-score for service level
        z_score = self._get_z_score(service_level)

        # Calculate safety stock
        if lead_time_days <= 0 or demand_std_dev <= 0:
            return SafetyStockResult(
                safety_stock_quantity=Decimal('0'),
                service_level=service_level,
                z_score=z_score,
                demand_std_dev=demand_std_dev,
                lead_time_days=lead_time_days,
                calculation_method='zero_due_to_invalid_inputs',
                confidence='low',
                reasoning_data={
                    'type': 'error_lead_time_invalid',
                    'parameters': {
                        'lead_time_days': lead_time_days,
                        'demand_std_dev': demand_std_dev
                    }
                }
            )

        # Safety Stock = Z × σ × √L
        safety_stock = z_score * demand_std_dev * math.sqrt(lead_time_days)

        # Determine confidence
        confidence = self._determine_confidence(demand_std_dev, lead_time_days)

        return SafetyStockResult(
            safety_stock_quantity=Decimal(str(round(safety_stock, 2))),
            service_level=service_level,
            z_score=z_score,
            demand_std_dev=demand_std_dev,
            lead_time_days=lead_time_days,
            calculation_method='statistical_z_score',
            confidence=confidence,
            reasoning_data={
                'type': 'statistical_z_score',
                'calculation_method': 'statistical_z_score',
                'parameters': {
                    'service_level': round(service_level * 100, 1),
                    'z_score': round(z_score, 2),
                    'demand_std_dev': round(demand_std_dev, 2),
                    'lead_time_days': lead_time_days,
                    'safety_stock': round(safety_stock, 2)
                },
                'confidence': confidence
            }
        )

    def calculate_from_demand_history(
        self,
        daily_demands: List[float],
        lead_time_days: int,
        service_level: Optional[float] = None
    ) -> SafetyStockResult:
        """
        Calculate safety stock from historical demand data.

        Args:
            daily_demands: List of historical daily demands
            lead_time_days: Supplier lead time in days
            service_level: Target service level

        Returns:
            SafetyStockResult with calculation details
        """
        if not daily_demands or len(daily_demands) < 2:
            logger.warning("Insufficient demand history for safety stock calculation")
            return SafetyStockResult(
                safety_stock_quantity=Decimal('0'),
                service_level=service_level or self.default_service_level,
                z_score=0.0,
                demand_std_dev=0.0,
                lead_time_days=lead_time_days,
                calculation_method='insufficient_data',
                confidence='low',
                reasoning_data={
                    'type': 'error_insufficient_data',
                    'parameters': {
                        'data_points': len(daily_demands) if daily_demands else 0,
                        'min_required': 2
                    }
                }
            )

        # Calculate standard deviation
        demand_std_dev = statistics.stdev(daily_demands)

        return self.calculate_safety_stock(
            demand_std_dev=demand_std_dev,
            lead_time_days=lead_time_days,
            service_level=service_level
        )

    def calculate_with_lead_time_variability(
        self,
        demand_mean: float,
        demand_std_dev: float,
        lead_time_mean: int,
        lead_time_std_dev: int,
        service_level: Optional[float] = None
    ) -> SafetyStockResult:
        """
        Calculate safety stock considering both demand AND lead time variability.

        More accurate formula:
        SS = Z × √(L_mean × σ_demand² + μ_demand² × σ_lead_time²)

        Args:
            demand_mean: Mean daily demand
            demand_std_dev: Standard deviation of daily demand
            lead_time_mean: Mean lead time in days
            lead_time_std_dev: Standard deviation of lead time
            service_level: Target service level

        Returns:
            SafetyStockResult with calculation details
        """
        if service_level is None:
            service_level = self.default_service_level

        z_score = self._get_z_score(service_level)

        # Calculate combined variance
        variance = (
            lead_time_mean * (demand_std_dev ** 2) +
            (demand_mean ** 2) * (lead_time_std_dev ** 2)
        )

        safety_stock = z_score * math.sqrt(variance)

        confidence = 'high' if lead_time_std_dev > 0 else 'medium'

        return SafetyStockResult(
            safety_stock_quantity=Decimal(str(round(safety_stock, 2))),
            service_level=service_level,
            z_score=z_score,
            demand_std_dev=demand_std_dev,
            lead_time_days=lead_time_mean,
            calculation_method='statistical_with_lead_time_variability',
            confidence=confidence,
            reasoning_data={
                'type': 'advanced_variability',
                'calculation_method': 'statistical_with_lead_time_variability',
                'parameters': {
                    'service_level': round(service_level * 100, 1),
                    'z_score': round(z_score, 2),
                    'demand_mean': round(demand_mean, 2),
                    'demand_std_dev': round(demand_std_dev, 2),
                    'lead_time_mean': lead_time_mean,
                    'lead_time_std_dev': round(lead_time_std_dev, 1),
                    'safety_stock': round(safety_stock, 2)
                },
                'confidence': confidence
            }
        )

    def calculate_using_fixed_percentage(
        self,
        average_demand: float,
        lead_time_days: int,
        percentage: float = 0.20
    ) -> SafetyStockResult:
        """
        Calculate safety stock as percentage of lead time demand.

        Simple method: Safety Stock = % × (Average Daily Demand × Lead Time)

        Args:
            average_demand: Average daily demand
            lead_time_days: Supplier lead time in days
            percentage: Safety stock percentage (default 20%)

        Returns:
            SafetyStockResult with calculation details
        """
        lead_time_demand = average_demand * lead_time_days
        safety_stock = lead_time_demand * percentage

        return SafetyStockResult(
            safety_stock_quantity=Decimal(str(round(safety_stock, 2))),
            service_level=0.0,  # Not based on service level
            z_score=0.0,
            demand_std_dev=0.0,
            lead_time_days=lead_time_days,
            calculation_method='fixed_percentage',
            confidence='low',
            reasoning_data={
                'type': 'fixed_percentage',
                'calculation_method': 'fixed_percentage',
                'parameters': {
                    'percentage': round(percentage * 100, 0),
                    'average_demand': round(average_demand, 2),
                    'lead_time_days': lead_time_days,
                    'lead_time_demand': round(lead_time_demand, 2),
                    'safety_stock': round(safety_stock, 2)
                },
                'confidence': 'low'
            }
        )

    def calculate_batch_safety_stock(
        self,
        ingredients_data: List[Dict]
    ) -> Dict[str, SafetyStockResult]:
        """
        Calculate safety stock for multiple ingredients.

        Args:
            ingredients_data: List of dicts with ingredient data

        Returns:
            Dictionary mapping ingredient_id to SafetyStockResult
        """
        results = {}

        for data in ingredients_data:
            ingredient_id = data['ingredient_id']

            if 'daily_demands' in data:
                # Use historical data
                result = self.calculate_from_demand_history(
                    daily_demands=data['daily_demands'],
                    lead_time_days=data['lead_time_days'],
                    service_level=data.get('service_level')
                )
            elif 'demand_std_dev' in data:
                # Use provided std dev
                result = self.calculate_safety_stock(
                    demand_std_dev=data['demand_std_dev'],
                    lead_time_days=data['lead_time_days'],
                    service_level=data.get('service_level')
                )
            else:
                # Fallback to percentage method
                result = self.calculate_using_fixed_percentage(
                    average_demand=data.get('average_demand', 0),
                    lead_time_days=data['lead_time_days'],
                    percentage=data.get('safety_percentage', 0.20)
                )

            results[ingredient_id] = result

        logger.info(f"Calculated safety stock for {len(results)} ingredients")

        return results

    def analyze_demand_history(
        self,
        daily_demands: List[float]
    ) -> DemandHistory:
        """
        Analyze demand history to extract statistics.

        Args:
            daily_demands: List of historical daily demands

        Returns:
            DemandHistory with statistics
        """
        if not daily_demands:
            return DemandHistory(
                ingredient_id="unknown",
                daily_demands=[],
                mean_demand=0.0,
                std_dev=0.0,
                coefficient_of_variation=0.0
            )

        mean_demand = statistics.mean(daily_demands)
        std_dev = statistics.stdev(daily_demands) if len(daily_demands) >= 2 else 0.0
        cv = (std_dev / mean_demand) if mean_demand > 0 else 0.0

        return DemandHistory(
            ingredient_id="unknown",
            daily_demands=daily_demands,
            mean_demand=mean_demand,
            std_dev=std_dev,
            coefficient_of_variation=cv
        )

    def _get_z_score(self, service_level: float) -> float:
        """
        Get z-score for service level.

        Args:
            service_level: Target service level (0-1)

        Returns:
            Z-score
        """
        # Find closest service level
        if service_level in self.SERVICE_LEVEL_Z_SCORES:
            return self.SERVICE_LEVEL_Z_SCORES[service_level]

        # Interpolate or use closest
        levels = sorted(self.SERVICE_LEVEL_Z_SCORES.keys())

        for i, level in enumerate(levels):
            if service_level <= level:
                return self.SERVICE_LEVEL_Z_SCORES[level]

        # Use highest if beyond range
        return self.SERVICE_LEVEL_Z_SCORES[levels[-1]]

    def _determine_confidence(
        self,
        demand_std_dev: float,
        lead_time_days: int
    ) -> str:
        """
        Determine confidence level of calculation.

        Args:
            demand_std_dev: Demand standard deviation
            lead_time_days: Lead time in days

        Returns:
            Confidence level
        """
        if demand_std_dev == 0:
            return 'low'  # No variability in data

        if lead_time_days < 3:
            return 'high'  # Short lead time, easier to manage
        elif lead_time_days < 7:
            return 'medium'
        else:
            return 'medium'  # Long lead time, more uncertainty

    def recommend_service_level(
        self,
        ingredient_category: str,
        is_critical: bool = False
    ) -> float:
        """
        Recommend service level based on ingredient characteristics.

        Args:
            ingredient_category: Category of ingredient
            is_critical: Whether ingredient is business-critical

        Returns:
            Recommended service level
        """
        # Critical ingredients: very high service level
        if is_critical:
            return 0.99

        # Perishables: moderate service level (to avoid waste)
        if ingredient_category.lower() in ['dairy', 'meat', 'produce', 'fresh']:
            return 0.90

        # Standard ingredients: high service level
        return 0.95

    def export_to_dict(self, result: SafetyStockResult) -> Dict:
        """
        Export result to dictionary for API response.

        Args:
            result: SafetyStockResult

        Returns:
            Dictionary representation
        """
        return {
            'safety_stock_quantity': float(result.safety_stock_quantity),
            'service_level': result.service_level,
            'z_score': result.z_score,
            'demand_std_dev': result.demand_std_dev,
            'lead_time_days': result.lead_time_days,
            'calculation_method': result.calculation_method,
            'confidence': result.confidence,
            'reasoning_data': result.reasoning_data
        }
