"""
Monte Carlo precipitation simulation framework.

This module provides a high-level interface for running Monte Carlo
precipitation simulations with multiple methods and comprehensive analysis.
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Union
import logging

from ..engines.analytical import ParameterManifest, MonthlyParams
from ..engines.simulation import SimulationEngine
from .analysis import DroughtAnalyzer, ExtremeEventAnalyzer, SeasonalAnalyzer


class PrecipitationSimulator:
    """
    High-level precipitation simulator for water resources applications.
    
    This class provides a complete workflow for:
    - Running Monte Carlo simulations with pre-computed parameters
    - Multiple simulation methods (WGEN, bootstrap variants)
    - Comprehensive analysis and visualization
    
    The simulator is decoupled from data loading and works with ParameterManifest
    objects produced by the AnalyticalEngine.
    
    Example:
        >>> # Using pre-computed parameters
        >>> sim = pg.PrecipitationSimulator(parameters=parameter_manifest)
        >>> results = sim.run_monte_carlo_simulation(n_realizations=100)
        >>> analysis = sim.analyze_results()
        
        >>> # Using historical data for bootstrap methods
        >>> sim = pg.PrecipitationSimulator(parameters=parameter_manifest, 
        ...                                historical_data=historical_series)
        >>> results = sim.run_monte_carlo_simulation(methods=['wgen', 'bootstrap_doy'])
    """
    
    def __init__(self, parameters: ParameterManifest, 
                 historical_data: Optional[pd.Series] = None):
        """
        Initialize the precipitation simulator with pre-computed parameters.
        
        This constructor enables parameter-only simulation, decoupling the simulator
        from GHCN data requirements. The simulator can operate with just a 
        ParameterManifest, making it suitable for scenarios where parameters
        come from external sources, manual input, or pre-computed analyses.
        
        Args:
            parameters: ParameterManifest containing monthly WGEN parameters.
                       Must include parameters for all 12 months.
            historical_data: Optional historical precipitation data for bootstrap methods.
                           If None, only WGEN simulation will be available.
        
        Raises:
            ValueError: If parameters are invalid or missing required months
        """
        self.parameters = parameters
        self.historical_data = historical_data
        self.results = {}
        
        # Set up logging
        self.logger = logging.getLogger(__name__)
        
        # Validate parameters
        self._validate_parameters()
        
        # Document parameter source in metadata if not already present
        self._document_parameter_source()
        
        self.logger.info("PrecipitationSimulator initialized with parameter-based approach")
        if historical_data is not None:
            self.logger.info(f"Historical data available: {len(historical_data)} days")
        else:
            self.logger.info("Parameter-only mode: bootstrap methods not available")
    
    def _validate_parameters(self) -> None:
        """Validate the parameter manifest."""
        if not self.parameters:
            raise ValueError("ParameterManifest is required")
        
        if not self.parameters.overall_parameters:
            raise ValueError("ParameterManifest must contain overall_parameters")
        
        # Check that we have parameters for all months
        missing_months = []
        for month in range(1, 13):
            if month not in self.parameters.overall_parameters:
                missing_months.append(month)
        
        if missing_months:
            raise ValueError(f"Missing parameters for months: {missing_months}")
        
        self.logger.info("Parameter validation completed successfully")
        
    def _document_parameter_source(self) -> None:
        """
        Document parameter source in metadata for traceability and workflow integration.
        
        Ensures that simulation metadata includes comprehensive information about where
        parameters originated, supporting requirement 1.5 for parameter source documentation
        and enabling seamless workflow integration.
        """
        if not hasattr(self.parameters, 'metadata') or self.parameters.metadata is None:
            self.parameters.metadata = {}
        
        # Add parameter source documentation if not present
        if 'parameter_source' not in self.parameters.metadata:
            if self.historical_data is not None:
                self.parameters.metadata['parameter_source'] = 'Derived from historical data'
                self.parameters.metadata['parameter_derivation'] = 'AnalyticalEngine calculation'
            else:
                self.parameters.metadata['parameter_source'] = 'External parameter input'
                self.parameters.metadata['parameter_derivation'] = 'Pre-computed parameters'
        
        # Add simulation mode documentation
        if 'simulation_mode' not in self.parameters.metadata:
            if self.historical_data is not None:
                self.parameters.metadata['simulation_mode'] = 'Full (WGEN + Bootstrap methods available)'
                self.parameters.metadata['available_methods'] = ['wgen', 'bootstrap_doy', 'block_bootstrap']
            else:
                self.parameters.metadata['simulation_mode'] = 'Parameter-only (WGEN only)'
                self.parameters.metadata['available_methods'] = ['wgen']
        
        # Add workflow integration metadata
        self.parameters.metadata.update({
            'simulator_initialized': datetime.now().isoformat(),
            'workflow_integration_version': '1.0',
            'parameter_validation_passed': True,
            'supports_trend_mode': self.parameters.trend_analysis is not None,
            'historical_data_available': self.historical_data is not None,
            'historical_data_length': len(self.historical_data) if self.historical_data is not None else 0
        })
        
        # Preserve station information for workflow continuity
        if hasattr(self.historical_data, 'attrs') and self.historical_data is not None:
            source_attrs = self.historical_data.attrs
            if 'station_name' in source_attrs:
                self.parameters.metadata['station_name'] = source_attrs['station_name']
            if 'station_id' in source_attrs:
                self.parameters.metadata['station_id'] = source_attrs['station_id']
        
        # Add parameter quality assessment
        self.parameters.metadata['parameter_quality_assessment'] = self._assess_parameter_quality_for_simulation()
    
    def _assess_parameter_quality_for_simulation(self) -> Dict[str, Any]:
        """Assess parameter quality specifically for simulation purposes."""
        assessment = {
            'overall_quality': 'good',
            'warnings': [],
            'recommendations': []
        }
        
        # Check parameter ranges and consistency
        issues = 0
        for month, params in self.parameters.overall_parameters.items():
            if not (0 <= params.p_ww <= 1):
                assessment['warnings'].append(f"Month {month}: P(W|W) out of valid range")
                issues += 1
            if not (0 <= params.p_wd <= 1):
                assessment['warnings'].append(f"Month {month}: P(W|D) out of valid range")
                issues += 1
            if params.alpha <= 0 or params.beta <= 0:
                assessment['warnings'].append(f"Month {month}: Invalid Gamma parameters")
                issues += 1
        
        # Assess overall quality
        if issues == 0:
            assessment['overall_quality'] = 'excellent'
        elif issues <= 2:
            assessment['overall_quality'] = 'good'
        elif issues <= 5:
            assessment['overall_quality'] = 'fair'
            assessment['recommendations'].append("Consider reviewing parameter calculation")
        else:
            assessment['overall_quality'] = 'poor'
            assessment['recommendations'].append("Parameters may need recalculation")
        
        # Add data adequacy assessment
        if 'total_data_points' in self.parameters.metadata:
            data_points = self.parameters.metadata['total_data_points']
            if data_points < 365 * 5:
                assessment['warnings'].append("Limited data for parameter estimation (<5 years)")
            elif data_points >= 365 * 20:
                assessment['recommendations'].append("Excellent data coverage for reliable parameters")
        
        return assessment
        
    def get_parameters_summary(self) -> pd.DataFrame:
        """
        Get a summary of the loaded parameters in DataFrame format.
        
        Returns:
            DataFrame with monthly parameters for easy viewing
        """
        rows = []
        for month in range(1, 13):
            params = self.parameters.overall_parameters[month]
            rows.append({
                'month': month,
                'p_ww': params.p_ww,
                'p_wd': params.p_wd,
                'alpha': params.alpha,
                'beta': params.beta,
                'mean_precip': params.alpha * params.beta  # Gamma distribution mean
            })
        
        return pd.DataFrame(rows).set_index('month')
    
    def validate_parameters(self) -> Dict[str, Any]:
        """
        Validate the loaded parameters for simulation readiness.
        
        Returns:
            Dictionary with validation results and warnings
        """
        validation = {
            'is_valid': True,
            'warnings': [],
            'errors': []
        }
        
        for month in range(1, 13):
            params = self.parameters.overall_parameters[month]
            
            # Check transition probabilities
            if not (0 <= params.p_ww <= 1):
                validation['errors'].append(f"Month {month}: p_ww={params.p_ww} not in [0,1]")
                validation['is_valid'] = False
            
            if not (0 <= params.p_wd <= 1):
                validation['errors'].append(f"Month {month}: p_wd={params.p_wd} not in [0,1]")
                validation['is_valid'] = False
            
            # Check Gamma parameters
            if params.alpha <= 0:
                validation['errors'].append(f"Month {month}: alpha={params.alpha} must be positive")
                validation['is_valid'] = False
            
            if params.beta <= 0:
                validation['errors'].append(f"Month {month}: beta={params.beta} must be positive")
                validation['is_valid'] = False
            
            # Check for reasonable parameter ranges
            if params.alpha > 10:
                validation['warnings'].append(f"Month {month}: alpha={params.alpha} unusually large (>10)")
            
            if params.beta > 100:
                validation['warnings'].append(f"Month {month}: beta={params.beta} unusually large (>100)")
        
        return validation
    
    def run_monte_carlo_simulation(self, 
                                 start_date: str = '2026-01-01', 
                                 end_date: str = '2035-12-31',
                                 n_realizations: int = 100, 
                                 methods: Optional[List[str]] = None, 
                                 seed: int = 42,
                                 trend_mode: bool = False) -> Dict[str, pd.DataFrame]:
        """
        Run Monte Carlo simulation with comprehensive error handling and performance optimization.
        
        This method implements intelligent method selection, robust error handling with graceful
        degradation, and performance optimization through caching and efficient computation.
        
        Args:
            start_date: Simulation start date (YYYY-MM-DD)
            end_date: Simulation end date (YYYY-MM-DD)
            n_realizations: Number of Monte Carlo realizations
            methods: List of simulation methods ('wgen', 'bootstrap_doy', 'block_bootstrap')
            seed: Random seed for reproducibility
            trend_mode: Enable non-stationary trend projection (requires trend analysis in parameters)
            
        Returns:
            Dictionary of simulation results by method
            
        Raises:
            ValueError: If parameters not available or invalid method specified
            SimulationError: If simulation fails and no recovery is possible
        """
        from ..utils.error_handling import robust_operation, performance_optimizer
        from ..utils.exceptions import SimulationError, StateError, BoundsError
        
        # Validate inputs with error recovery
        try:
            self._validate_simulation_inputs(n_realizations, start_date, end_date, methods, trend_mode)
        except Exception as e:
            self.logger.error(f"Input validation failed: {e}")
            # Apply graceful degradation for invalid inputs
            n_realizations = min(max(1, n_realizations), 1000)  # Clamp to reasonable range
            if methods and not all(method in ['wgen', 'bootstrap_doy', 'block_bootstrap'] for method in methods):
                self.logger.warning("Invalid methods specified, using intelligent selection")
                methods = None
        
        if methods is None:
            # Intelligent method selection based on available data
            if self.historical_data is None:
                methods = ['wgen']
                self.logger.info("No historical data available, using WGEN method only")
            else:
                methods = ['wgen', 'bootstrap_doy', 'block_bootstrap']
                self.logger.info("Historical data available, using all simulation methods")
        else:
            self.logger.info(f"Using user-specified methods: {methods}")
        
        # Validate that bootstrap methods have historical data
        bootstrap_methods = ['bootstrap_doy', 'block_bootstrap']
        requested_bootstrap = [m for m in methods if m in bootstrap_methods]
        
        if requested_bootstrap and self.historical_data is None:
            self.logger.warning(f"Bootstrap methods {requested_bootstrap} require historical data, removing from methods")
            methods = [method for method in methods if method not in bootstrap_methods]
            if not methods:
                self.logger.info("No valid methods remaining, defaulting to WGEN")
                methods = ['wgen']
        
        # Set up date range with validation
        try:
            dates = pd.date_range(start=start_date, end=end_date, freq='D')
            if len(dates) == 0:
                raise ValueError("Date range is empty")
        except Exception as e:
            self.logger.error(f"Date range creation failed: {e}")
            # Use fallback date range
            dates = pd.date_range(start='2026-01-01', periods=365, freq='D')
            self.logger.warning(f"Using fallback date range: {dates[0]} to {dates[-1]}")
        
        n_years = len(dates) / 365.25
        
        self.logger.info(f"Running {n_realizations} realizations for {n_years:.1f} years ({len(dates)} days)")
        if trend_mode:
            self.logger.info("Trend mode enabled - using non-stationary parameters")
        
        results = {}
        simulation_metadata = {
            'n_realizations': n_realizations,
            'date_range': (dates[0].isoformat(), dates[-1].isoformat()),
            'methods_requested': methods.copy(),
            'trend_mode': trend_mode,
            'seed': seed,
            'simulation_start_time': datetime.now().isoformat()
        }
        
        # Run each simulation method with error handling
        for method in methods:
            method_start_time = datetime.now()
            self.logger.info(f"Running {method} simulation...")
            
            try:
                # Use performance optimizer for caching
                cache_key = f"simulation_{method}_{len(dates)}_{n_realizations}_{seed}_{trend_mode}"
                
                @robust_operation(
                    operation_name=f"{method}_simulation",
                    fallback_value=None,
                    error_types=(SimulationError, StateError, BoundsError, ValueError, RuntimeError),
                    log_errors=True,
                    raise_on_failure=False
                )
                def run_method_simulation():
                    if method == 'wgen':
                        return self._simulate_wgen_robust(dates, n_realizations, seed, trend_mode)
                    elif method == 'bootstrap_doy':
                        return self._simulate_bootstrap_doy_robust(dates, n_realizations, seed)
                    elif method == 'block_bootstrap':
                        return self._simulate_block_bootstrap_robust(dates, n_realizations, seed)
                    else:
                        raise ValueError(f"Unknown simulation method: {method}")
                
                # Execute simulation with caching
                method_results = performance_optimizer.cached_computation(
                    cache_key, run_method_simulation
                )
                
                if method_results is not None:
                    results[method] = method_results
                    method_time = (datetime.now() - method_start_time).total_seconds()
                    self.logger.info(f"{method} simulation completed in {method_time:.2f}s")
                    
                    # Add method-specific metadata
                    simulation_metadata[f'{method}_completion_time'] = method_time
                    simulation_metadata[f'{method}_success'] = True
                else:
                    self.logger.error(f"{method} simulation failed completely")
                    simulation_metadata[f'{method}_success'] = False
                    
                    # Try to create minimal fallback results
                    fallback_results = self._create_fallback_simulation_results(dates, n_realizations)
                    if fallback_results is not None:
                        results[f"{method}_fallback"] = fallback_results
                        self.logger.warning(f"Using fallback results for {method}")
            
            except Exception as e:
                self.logger.error(f"Catastrophic failure in {method} simulation: {e}")
                simulation_metadata[f'{method}_error'] = str(e)
                simulation_metadata[f'{method}_success'] = False
                
                # Continue with other methods
                continue
        
        # Validate that we have at least some results
        if not results:
            error_msg = "All simulation methods failed, no results available"
            self.logger.error(error_msg)
            
            # Create emergency fallback results
            emergency_results = self._create_fallback_simulation_results(dates, min(10, n_realizations))
            if emergency_results is not None:
                results['emergency_fallback'] = emergency_results
                self.logger.warning("Using emergency fallback simulation results")
            else:
                raise SimulationError(
                    error_msg,
                    details=simulation_metadata,
                    guidance="Check parameter manifest validity and reduce simulation complexity"
                )
        
        # Store results and metadata
        self.results = results
        simulation_metadata['total_simulation_time'] = (
            datetime.now() - datetime.fromisoformat(simulation_metadata['simulation_start_time'])
        ).total_seconds()
        simulation_metadata['methods_completed'] = list(results.keys())
        simulation_metadata['success_rate'] = len(results) / len(methods) if methods else 0
        
        # Add metadata to parameter manifest
        if not hasattr(self.parameters, 'metadata'):
            self.parameters.metadata = {}
        self.parameters.metadata['last_simulation'] = simulation_metadata
        
        self.logger.info(f"Monte Carlo simulation completed with {len(results)} method(s)")
        return results
    
    def _validate_simulation_inputs(self, n_realizations: int, start_date: str, 
                                  end_date: str, methods: Optional[List[str]], 
                                  trend_mode: bool) -> None:
        """Validate simulation inputs with comprehensive checks."""
        if n_realizations <= 0:
            raise ValueError(f"n_realizations must be positive, got {n_realizations}")
        
        if n_realizations > 10000:
            self.logger.warning(f"Large number of realizations ({n_realizations}) may impact performance")
        
        # Validate date format and range
        try:
            start_dt = pd.to_datetime(start_date)
            end_dt = pd.to_datetime(end_date)
            if start_dt >= end_dt:
                raise ValueError(f"start_date ({start_date}) must be before end_date ({end_date})")
        except Exception as e:
            raise ValueError(f"Invalid date format: {e}")
        
        # Validate methods
        if methods is not None:
            valid_methods = ['wgen', 'bootstrap_doy', 'block_bootstrap']
            invalid_methods = [m for m in methods if m not in valid_methods]
            if invalid_methods:
                raise ValueError(f"Invalid methods: {invalid_methods}. Valid methods: {valid_methods}")
        
        # Validate trend mode
        if trend_mode and (not hasattr(self.parameters, 'trend_analysis') or 
                          self.parameters.trend_analysis is None):
            raise ValueError("Trend mode requires trend_analysis in parameter manifest")
    
    def _select_methods_intelligently(self) -> List[str]:
        """Select simulation methods based on available data and capabilities."""
        if self.historical_data is None:
            return ['wgen']
        else:
            # Check data quality for bootstrap methods
            if len(self.historical_data) < 365:
                self.logger.warning("Limited historical data, using WGEN only")
                return ['wgen']
            else:
                return ['wgen', 'bootstrap_doy', 'block_bootstrap']
    
    def _simulate_wgen_robust(self, dates: pd.DatetimeIndex, n_realizations: int, 
                            seed: int, trend_mode: bool = False) -> pd.DataFrame:
        """
        Robust WGEN simulation with comprehensive error handling.
        """
        try:
            return self._simulate_wgen(dates, n_realizations, seed, trend_mode)
        except Exception as e:
            self.logger.error(f"WGEN simulation failed: {e}")
            
            # Try with simplified parameters
            try:
                self.logger.info("Attempting WGEN simulation with simplified parameters")
                # Disable trend mode if it was causing issues
                return self._simulate_wgen(dates, n_realizations, seed, False)
            except Exception as e2:
                self.logger.error(f"Simplified WGEN simulation also failed: {e2}")
                raise SimulationError(
                    f"WGEN simulation failed: {e}",
                    details={'original_error': str(e), 'simplified_error': str(e2)},
                    guidance="Check parameter manifest validity and ensure all monthly parameters are present"
                )
    
    def _simulate_bootstrap_doy_robust(self, dates: pd.DatetimeIndex, 
                                     n_realizations: int, seed: int) -> pd.DataFrame:
        """
        Robust bootstrap day-of-year simulation with error handling.
        """
        try:
            return self._simulate_bootstrap_doy(dates, n_realizations, seed)
        except Exception as e:
            self.logger.error(f"Bootstrap DOY simulation failed: {e}")
            
            # Try with reduced complexity
            try:
                self.logger.info("Attempting bootstrap DOY with reduced realizations")
                reduced_realizations = min(10, n_realizations)
                return self._simulate_bootstrap_doy(dates, reduced_realizations, seed)
            except Exception as e2:
                self.logger.error(f"Reduced bootstrap DOY simulation also failed: {e2}")
                raise SimulationError(
                    f"Bootstrap DOY simulation failed: {e}",
                    details={'original_error': str(e), 'reduced_error': str(e2)},
                    guidance="Check historical data quality and completeness"
                )
    
    def _simulate_block_bootstrap_robust(self, dates: pd.DatetimeIndex, 
                                       n_realizations: int, seed: int) -> pd.DataFrame:
        """
        Robust block bootstrap simulation with error handling.
        """
        try:
            return self._simulate_block_bootstrap(dates, n_realizations, seed)
        except Exception as e:
            self.logger.error(f"Block bootstrap simulation failed: {e}")
            
            # Try with smaller block size
            try:
                self.logger.info("Attempting block bootstrap with smaller blocks")
                # This would require modifying the bootstrap method to accept block size
                # For now, just try with reduced realizations
                reduced_realizations = min(10, n_realizations)
                return self._simulate_block_bootstrap(dates, reduced_realizations, seed)
            except Exception as e2:
                self.logger.error(f"Reduced block bootstrap simulation also failed: {e2}")
                raise SimulationError(
                    f"Block bootstrap simulation failed: {e}",
                    details={'original_error': str(e), 'reduced_error': str(e2)},
                    guidance="Check historical data length and quality for bootstrap resampling"
                )
    
    def _create_fallback_simulation_results(self, dates: pd.DatetimeIndex, 
                                          n_realizations: int) -> Optional[pd.DataFrame]:
        """
        Create minimal fallback simulation results when all methods fail.
        """
        try:
            self.logger.info("Creating fallback simulation results")
            
            # Use simple exponential distribution based on overall mean
            if hasattr(self.parameters, 'overall_parameters') and self.parameters.overall_parameters:
                # Calculate average beta parameter across all months
                avg_beta = np.mean([
                    params.beta for params in self.parameters.overall_parameters.values()
                ])
            else:
                avg_beta = 5.0  # Default fallback
            
            # Generate simple synthetic data
            np.random.seed(42)  # Fixed seed for reproducible fallback
            synthetic_data = np.random.exponential(avg_beta, (len(dates), n_realizations))
            
            # Apply wet/dry pattern (simple approach)
            wet_prob = 0.3  # Default wet day probability
            wet_pattern = np.random.random((len(dates), n_realizations)) < wet_prob
            synthetic_data = synthetic_data * wet_pattern
            
            result_df = pd.DataFrame(synthetic_data, index=dates)
            
            # Add metadata to indicate this is fallback data
            result_df.attrs = {
                'source': 'fallback_simulation',
                'warning': 'This is synthetic fallback data due to simulation failures',
                'avg_beta_used': avg_beta,
                'wet_probability_used': wet_prob
            }
            
            return result_df
            
        except Exception as e:
            self.logger.error(f"Failed to create fallback simulation results: {e}")
            return None
    
    def _simulate_wgen(self, dates: pd.DatetimeIndex, n_realizations: int, 
                      seed: int, trend_mode: bool = False) -> pd.DataFrame:
        """
        WGEN-style simulation using the SimulationEngine for proper stateful simulation.
        
        This method leverages the existing SimulationEngine which handles:
        - Proper Markov chain state transitions
        - Gamma distribution sampling
        - Trend projection (if enabled)
        - Comprehensive error handling
        """
        results = np.zeros((len(dates), n_realizations))
        
        for realization in range(n_realizations):
            # Create a new simulation engine for each realization
            engine = SimulationEngine(
                parameters=self.parameters,
                trend_mode=trend_mode,
                random_seed=seed + realization  # Different seed per realization
            )
            
            # Initialize the engine
            engine.initialize(
                start_date=dates[0].to_pydatetime(),
                initial_wet_state=False
            )
            
            # Generate precipitation for each day
            for i in range(len(dates)):
                precip = engine.step()
                results[i, realization] = precip
        
        return pd.DataFrame(results, index=dates)
    
    def _simulate_bootstrap_doy(self, dates: pd.DatetimeIndex, n_realizations: int, seed: int) -> pd.DataFrame:
        """Day-of-year bootstrap simulation."""
        rng = np.random.default_rng(seed)
        
        # Create pools by day of year
        doy_pools = {}
        for date, value in self.historical_data.dropna().items():
            doy = (date.month, date.day)
            if doy not in doy_pools:
                doy_pools[doy] = []
            doy_pools[doy].append(value)
        
        sims = np.zeros((len(dates), n_realizations))
        
        for i, date in enumerate(dates):
            doy = (date.month, date.day)
            
            # Get pool for this day of year, fallback to month if needed
            if doy in doy_pools:
                pool = doy_pools[doy]
            else:
                # Fallback to all days in the month
                pool = []
                for key, values in doy_pools.items():
                    if key[0] == date.month:
                        pool.extend(values)
                
                if not pool:
                    pool = list(self.historical_data.dropna().values)
            
            # Sample from pool
            sims[i, :] = rng.choice(pool, size=n_realizations, replace=True)
        
        return pd.DataFrame(sims, index=dates)
    
    def _simulate_block_bootstrap(self, dates: pd.DatetimeIndex, n_realizations: int, 
                                seed: int, block_length: int = 7) -> pd.DataFrame:
        """Block bootstrap simulation preserving temporal correlation."""
        rng = np.random.default_rng(seed)
        historical_values = self.historical_data.dropna().values
        n_historical = len(historical_values)
        
        sims = np.zeros((len(dates), n_realizations))
        
        for real in range(n_realizations):
            simulated = []
            
            while len(simulated) < len(dates):
                # Random starting position
                start_idx = rng.integers(0, max(1, n_historical - block_length + 1))
                block = historical_values[start_idx:start_idx + block_length]
                simulated.extend(block)
            
            sims[:, real] = simulated[:len(dates)]
        
        return pd.DataFrame(sims, index=dates)
    
    def analyze_results(self, save_plots: bool = True, output_dir: str = 'output') -> Dict:
        """
        Comprehensive analysis of simulation results.
        
        Args:
            save_plots: Whether to save analysis plots
            output_dir: Directory to save outputs
            
        Returns:
            Dictionary with analysis results
            
        Raises:
            ValueError: If no simulation results available
        """
        if not self.results:
            raise ValueError("No simulation results available. Run run_monte_carlo_simulation() first.")
        
        Path(output_dir).mkdir(exist_ok=True)
        
        analysis = {}
        
        # Get historical statistics for comparison if available
        hist_annual = None
        if self.historical_data is not None:
            hist_annual = self.historical_data.resample('YE').sum()
            self.logger.info("=== SIMULATION ANALYSIS ===")
            self.logger.info(f"Historical annual mean: {hist_annual.mean():.1f} mm")
            self.logger.info(f"Historical annual std: {hist_annual.std():.1f} mm")
        else:
            self.logger.info("=== SIMULATION ANALYSIS (No Historical Comparison) ===")
        
        for method, sims in self.results.items():
            self.logger.info(f"\n--- {method.upper()} METHOD ---")
            
            # Annual statistics
            annual_sims = sims.resample('YE').sum()
            annual_means = annual_sims.mean(axis=1)
            annual_stds = annual_sims.std(axis=1)
            
            self.logger.info(f"Simulated annual mean: {annual_means.mean():.1f} ± {annual_means.std():.1f} mm")
            self.logger.info(f"Simulated annual std: {annual_stds.mean():.1f} ± {annual_stds.std():.1f} mm")
            
            # Monthly statistics
            monthly_sims = sims.resample('ME').sum()
            
            # Wet day statistics
            wet_day_freq = (sims > 0.1).mean(axis=1).resample('YE').mean()
            self.logger.info(f"Wet day frequency: {wet_day_freq.mean():.3f} ± {wet_day_freq.std():.3f}")
            
            # Extreme events (95th percentile)
            daily_95th = sims[sims > 0].quantile(0.95, axis=1)
            self.logger.info(f"95th percentile daily: {daily_95th.mean():.1f} ± {daily_95th.std():.1f} mm")
            
            analysis[method] = {
                'annual_totals': annual_sims,
                'monthly_totals': monthly_sims,
                'annual_means': annual_means,
                'annual_stds': annual_stds,
                'wet_day_freq': wet_day_freq,
                'daily_95th': daily_95th
            }
        
        # Create comprehensive plots
        if save_plots:
            self._create_analysis_plots(analysis, output_dir, hist_annual)
        
        return analysis
    
    def plot_simulation_comparison(self, 
                                  historical_data: pd.Series,
                                  realization_index: int = 0,
                                  save_path: Optional[str] = None,
                                  show_plot: bool = True,
                                  figsize: tuple = (15, 10)) -> Optional[str]:
        """
        Create a comprehensive 4-panel comparison between synthetic and historical precipitation.
        
        Creates four comparison charts:
        1. Annual precipitation distribution (histogram)
        2. Monthly climatology (line chart)
        3. Daily precipitation time series (sample year)
        4. Monthly wet day frequency (line chart)
        
        Args:
            historical_data: Historical precipitation time series for comparison
            realization_index: Which synthetic realization to use for comparison (default: 0)
            save_path: Optional path to save the plot
            show_plot: Whether to display the plot
            figsize: Figure size as (width, height)
            
        Returns:
            Path where plot was saved (if save_path provided)
            
        Raises:
            ValueError: If no simulation results are available
            
        Example:
            >>> simulator = pg.PrecipitationSimulator(parameters=manifest)
            >>> results = simulator.run_monte_carlo_simulation(n_realizations=5)
            >>> simulator.plot_simulation_comparison(
            ...     historical_data=historical_precip,
            ...     save_path='comparison.png'
            ... )
        """
        if not self.results:
            raise ValueError("No simulation results available. Run run_monte_carlo_simulation() first.")
        
        # Get the first available method results
        method_name = list(self.results.keys())[0]
        synthetic_data = self.results[method_name]
        
        if realization_index >= synthetic_data.shape[1]:
            raise ValueError(f"Realization index {realization_index} not available. "
                           f"Only {synthetic_data.shape[1]} realizations exist.")
        
        try:
            import matplotlib.pyplot as plt
            import numpy as np
        except ImportError:
            raise ImportError("matplotlib is required for plotting. Install with: pip install matplotlib")
        
        # Extract data for comparison
        synthetic_series = synthetic_data.iloc[:, realization_index]
        
        # Create the 4-panel figure
        fig, axes = plt.subplots(2, 2, figsize=figsize)
        fig.suptitle('Synthetic vs Historical Precipitation Comparison', fontsize=16, fontweight='bold')
        
        # Panel 1: Annual precipitation distribution
        hist_annual = historical_data.groupby(historical_data.index.year).sum()
        
        # For synthetic data, calculate annual totals across all realizations
        synthetic_annual_all = []
        for col in synthetic_data.columns:
            annual_totals = synthetic_data[col].groupby(synthetic_data.index.year).sum()
            synthetic_annual_all.extend(annual_totals.values)
        
        axes[0, 0].hist(hist_annual, bins=20, alpha=0.7, label='Historical', density=True, 
                       color='steelblue', edgecolor='navy')
        axes[0, 0].hist(synthetic_annual_all, bins=20, alpha=0.7, label='Synthetic', density=True,
                       color='lightcoral', edgecolor='darkred')
        axes[0, 0].set_xlabel('Annual Precipitation (mm)')
        axes[0, 0].set_ylabel('Density')
        axes[0, 0].set_title('Annual Precipitation Distribution')
        axes[0, 0].legend()
        axes[0, 0].grid(True, alpha=0.3)
        
        # Panel 2: Monthly climatology (mean daily precipitation by month)
        hist_monthly = historical_data.groupby(historical_data.index.month).mean()
        syn_monthly = synthetic_series.groupby(synthetic_series.index.month).mean()
        
        months = range(1, 13)
        month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                      'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        
        axes[0, 1].plot(months, hist_monthly, 'o-', label='Historical', linewidth=2, 
                       color='steelblue', markersize=6)
        axes[0, 1].plot(months, syn_monthly, 's-', label='Synthetic', linewidth=2,
                       color='lightcoral', markersize=6)
        axes[0, 1].set_xlabel('Month')
        axes[0, 1].set_ylabel('Mean Daily Precipitation (mm)')
        axes[0, 1].set_title('Monthly Climatology')
        axes[0, 1].legend()
        axes[0, 1].set_xticks(months)
        axes[0, 1].set_xticklabels(month_names)
        axes[0, 1].grid(True, alpha=0.3)
        
        # Panel 3: Daily precipitation time series (sample year)
        sample_days = 365  # Show one year
        hist_sample = historical_data.iloc[-sample_days:] if len(historical_data) >= sample_days else historical_data
        syn_sample = synthetic_series.iloc[:sample_days] if len(synthetic_series) >= sample_days else synthetic_series
        
        axes[1, 0].plot(range(len(hist_sample)), hist_sample, alpha=0.7, 
                       label=f'Historical (recent {len(hist_sample)} days)', color='steelblue')
        axes[1, 0].plot(range(len(syn_sample)), syn_sample, alpha=0.7, 
                       label=f'Synthetic (first {len(syn_sample)} days)', color='lightcoral')
        axes[1, 0].set_xlabel('Day')
        axes[1, 0].set_ylabel('Daily Precipitation (mm)')
        axes[1, 0].set_title('Daily Precipitation Time Series (Sample Period)')
        axes[1, 0].legend()
        axes[1, 0].grid(True, alpha=0.3)
        
        # Panel 4: Wet day frequency by month
        wet_threshold = getattr(self.parameters.metadata, 'wet_day_threshold', 0.254)
        
        hist_wet_freq = (historical_data > wet_threshold).groupby(historical_data.index.month).mean()
        syn_wet_freq = (synthetic_series > wet_threshold).groupby(synthetic_series.index.month).mean()
        
        axes[1, 1].plot(months, hist_wet_freq, 'o-', label='Historical', linewidth=2,
                       color='steelblue', markersize=6)
        axes[1, 1].plot(months, syn_wet_freq, 's-', label='Synthetic', linewidth=2,
                       color='lightcoral', markersize=6)
        axes[1, 1].set_xlabel('Month')
        axes[1, 1].set_ylabel('Wet Day Frequency')
        axes[1, 1].set_title('Monthly Wet Day Frequency')
        axes[1, 1].legend()
        axes[1, 1].set_xticks(months)
        axes[1, 1].set_xticklabels(month_names)
        axes[1, 1].grid(True, alpha=0.3)
        
        plt.tight_layout()
        
        # Save plot if requested
        if save_path:
            plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')
            self.logger.info(f"Simulation comparison plot saved to: {save_path}")
        
        # Show plot if requested
        if show_plot:
            plt.show()
        else:
            plt.close()
        
        return save_path if save_path else None
    
    def _create_analysis_plots(self, analysis: Dict, output_dir: str, hist_annual: Optional[pd.Series] = None):
        """Create comprehensive analysis plots."""
        
        # 1. Annual precipitation comparison
        fig, axes = plt.subplots(2, 2, figsize=(15, 12))
        
        # Annual totals comparison
        ax = axes[0, 0]
        methods = list(analysis.keys())
        annual_data = [analysis[method]['annual_totals'].values.flatten() for method in methods]
        
        if hist_annual is not None:
            ax.boxplot([hist_annual.values] + annual_data, 
                      labels=['Historical'] + [m.title() for m in methods])
        else:
            ax.boxplot(annual_data, labels=[m.title() for m in methods])
        
        ax.set_ylabel('Annual Precipitation (mm)')
        ax.set_title('Annual Precipitation Distribution')
        ax.grid(True, alpha=0.3)
        
        # Monthly climatology
        ax = axes[0, 1]
        
        if self.historical_data is not None:
            hist_monthly_clim = self.historical_data.groupby(self.historical_data.index.month).mean()
            months = range(1, 13)
            ax.plot(months, hist_monthly_clim.values, 'ko-', label='Historical', linewidth=2)
        
        for method in methods:
            monthly_sims = analysis[method]['monthly_totals']
            monthly_clim = monthly_sims.groupby(monthly_sims.index.month).mean().mean(axis=1)
            ax.plot(months, monthly_clim.values, 'o-', label=method.title(), alpha=0.7)
        
        ax.set_xlabel('Month')
        ax.set_ylabel('Mean Monthly Precipitation (mm)')
        ax.set_title('Monthly Climatology')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        # Wet day frequency by month
        ax = axes[1, 0]
        
        if self.historical_data is not None:
            hist_wet_freq = (self.historical_data > 0.1).groupby(self.historical_data.index.month).mean()
            ax.plot(months, hist_wet_freq.values, 'ko-', label='Historical', linewidth=2)
        
        for method in methods:
            sims = self.results[method]
            wet_freq = (sims > 0.1).groupby(sims.index.month).mean().mean(axis=1)
            ax.plot(months, wet_freq.values, 'o-', label=method.title(), alpha=0.7)
        
        ax.set_xlabel('Month')
        ax.set_ylabel('Wet Day Frequency')
        ax.set_title('Seasonal Wet Day Frequency')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        # Annual time series (first 10 realizations)
        ax = axes[1, 1]
        years = analysis[methods[0]]['annual_totals'].index.year
        
        for i in range(min(10, analysis[methods[0]]['annual_totals'].shape[1])):
            ax.plot(years, analysis[methods[0]]['annual_totals'].iloc[:, i], 
                   alpha=0.3, color='blue')
        
        if hist_annual is not None:
            ax.plot(hist_annual.index.year, hist_annual.values, 'ko-', 
                   label='Historical', alpha=0.7)
        
        ax.set_xlabel('Year')
        ax.set_ylabel('Annual Precipitation (mm)')
        ax.set_title(f'{methods[0].title()} - Sample Realizations')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.savefig(f'{output_dir}/precipitation_analysis.png', dpi=300, bbox_inches='tight')
        plt.show()
        
        self.logger.info(f"Analysis plots saved to {output_dir}/")
    
    def export_results(self, output_dir: str = 'output', 
                      include_sample_realizations: bool = True,
                      n_sample_realizations: int = 10) -> None:
        """
        Export simulation results and analysis to files.
        
        Args:
            output_dir: Directory to save outputs
            include_sample_realizations: Whether to save sample realizations
            n_sample_realizations: Number of sample realizations to save
        """
        if not self.results:
            raise ValueError("No simulation results available")
        
        Path(output_dir).mkdir(exist_ok=True)
        
        # Save parameters summary
        params_summary = self.get_parameters_summary()
        params_summary.to_csv(f'{output_dir}/monthly_parameters.csv')
        
        # Save full parameter manifest as JSON
        with open(f'{output_dir}/parameter_manifest.json', 'w') as f:
            f.write(self.parameters.to_json())
        
        # Save results
        for method, sims in self.results.items():
            # Save sample realizations
            if include_sample_realizations:
                sample_sims = sims.iloc[:, :n_sample_realizations]
                sample_sims.to_csv(f'{output_dir}/{method}_sample_realizations.csv')
            
            # Save annual statistics
            annual_stats = sims.resample('YE').sum()
            annual_summary = pd.DataFrame({
                'mean': annual_stats.mean(axis=1),
                'std': annual_stats.std(axis=1),
                'min': annual_stats.min(axis=1),
                'max': annual_stats.max(axis=1),
                'p10': annual_stats.quantile(0.1, axis=1),
                'p90': annual_stats.quantile(0.9, axis=1)
            })
            annual_summary.to_csv(f'{output_dir}/{method}_annual_statistics.csv')
        
        self.logger.info(f"Results exported to {output_dir}/")
    
    def get_water_resources_analysis(self) -> Dict:
        """
        Get specialized water resources analysis.
        
        Returns:
            Dictionary with drought, extreme event, and seasonal analysis
        """
        if not self.results:
            raise ValueError("No simulation results available")
        
        analysis = {}
        
        for method, sims in self.results.items():
            # Drought analysis
            drought_analyzer = DroughtAnalyzer()
            drought_stats = drought_analyzer.analyze(sims)
            
            # Extreme event analysis
            extreme_analyzer = ExtremeEventAnalyzer()
            extreme_stats = extreme_analyzer.analyze(sims)
            
            # Seasonal analysis
            seasonal_analyzer = SeasonalAnalyzer()
            seasonal_stats = seasonal_analyzer.analyze(sims)
            
            analysis[method] = {
                'drought': drought_stats,
                'extreme_events': extreme_stats,
                'seasonal': seasonal_stats
            }
        
        return analysis
    
    def get_synthetic_data_for_analysis(self, method: str = 'wgen', 
                                      realization: int = 0) -> pd.Series:
        """
        Get a single realization of synthetic data formatted for AnalyticalEngine analysis.
        
        This method provides synthetic precipitation data in the same format as historical
        data, enabling seamless analysis with AnalyticalEngine. The returned Series includes
        metadata attributes that preserve simulation information for workflow integration.
        
        Args:
            method: Simulation method to extract data from ('wgen', 'bootstrap_doy', etc.)
            realization: Realization number to extract (0-based index)
            
        Returns:
            pandas Series with synthetic precipitation data and metadata attributes
            
        Raises:
            ValueError: If method or realization not available
            
        Example:
            >>> # After simulation
            >>> simulator = pg.PrecipitationSimulator(parameters=manifest)
            >>> results = simulator.run_monte_carlo_simulation(n_realizations=100)
            >>> 
            >>> # Get synthetic data for analysis
            >>> synthetic_series = simulator.get_synthetic_data_for_analysis('wgen', 0)
            >>> 
            >>> # Analyze with AnalyticalEngine (works identically to historical data)
            >>> engine = pg.AnalyticalEngine(synthetic_series)
            >>> engine.initialize()
            >>> engine.print_annual_precipitation_statistics()
        """
        if not self.results:
            raise ValueError("No simulation results available. Run run_monte_carlo_simulation() first.")
        
        if method not in self.results:
            available_methods = list(self.results.keys())
            raise ValueError(f"Method '{method}' not available. Available methods: {available_methods}")
        
        sim_data = self.results[method]
        
        if realization >= sim_data.shape[1]:
            raise ValueError(f"Realization {realization} not available. Available realizations: 0-{sim_data.shape[1]-1}")
        
        # Extract single realization
        synthetic_series = sim_data.iloc[:, realization].copy()
        
        # Add comprehensive metadata attributes for workflow integration
        synthetic_series.attrs = {
            'data_type': 'synthetic_precipitation',
            'simulation_method': method,
            'realization_number': realization,
            'total_realizations': sim_data.shape[1],
            'simulation_date': datetime.now().isoformat(),
            'workflow_ready': True,
            'source': f'PrecipitationSimulator ({method})',
            'original_units': 'mm',
            'wet_day_threshold': self.parameters.metadata.get('wet_day_threshold_mm', 0.0254),
            
            # Preserve parameter source information
            'parameter_source': self.parameters.metadata.get('parameter_source', 'Unknown'),
            'parameter_derivation': self.parameters.metadata.get('parameter_derivation', 'Unknown'),
            
            # Station information (if available from parameters)
            'station_name': self.parameters.metadata.get('station_name', f'Synthetic Station ({method})'),
            'station_id': self.parameters.metadata.get('station_id', f'SYN_{method.upper()}_{realization:03d}'),
            
            # Simulation metadata
            'simulation_parameters': {
                'start_date': synthetic_series.index.min().isoformat(),
                'end_date': synthetic_series.index.max().isoformat(),
                'simulation_years': len(synthetic_series) / 365.25,
                'trend_mode_used': self.parameters.trend_analysis is not None
            },
            
            # Quality indicators
            'data_completeness': 1.0,  # Synthetic data is always complete
            'validation_status': 'synthetic_data',
            'analysis_ready': True
        }
        
        # Add coordinates if available
        if 'latitude' in self.parameters.metadata:
            synthetic_series.attrs['latitude'] = self.parameters.metadata['latitude']
        if 'longitude' in self.parameters.metadata:
            synthetic_series.attrs['longitude'] = self.parameters.metadata['longitude']
        
        return synthetic_series
    
    def analyze_ensemble_with_analytical_engine(self, wet_day_threshold: float = 0.0254,
                                              save_plots: bool = True,
                                              output_dir: str = 'ensemble_analysis') -> Dict[str, Any]:
        """
        Comprehensive ensemble analysis using AnalyticalEngine for each realization.
        
        This method demonstrates the synthetic data analysis parity by using the same
        AnalyticalEngine that works with historical data to analyze synthetic realizations.
        It provides ensemble statistics, parameter consistency analysis, and validation
        against the original parameters used for simulation.
        
        Args:
            wet_day_threshold: Wet day threshold for analysis (mm)
            save_plots: Whether to save analysis plots
            output_dir: Directory to save analysis outputs
            
        Returns:
            Dictionary containing comprehensive ensemble analysis results
            
        Example:
            >>> # After simulation
            >>> simulator = pg.PrecipitationSimulator(parameters=manifest)
            >>> results = simulator.run_monte_carlo_simulation(n_realizations=50)
            >>> 
            >>> # Comprehensive ensemble analysis
            >>> ensemble_analysis = simulator.analyze_ensemble_with_analytical_engine()
            >>> print(f"Parameter consistency: {ensemble_analysis['parameter_consistency']['overall_score']:.2f}")
        """
        from ..engines.analytical import AnalyticalEngine
        from pathlib import Path
        import numpy as np
        
        if not self.results:
            raise ValueError("No simulation results available. Run run_monte_carlo_simulation() first.")
        
        if save_plots:
            Path(output_dir).mkdir(parents=True, exist_ok=True)
        
        print("=== ENSEMBLE ANALYSIS WITH ANALYTICAL ENGINE ===")
        print("Analyzing synthetic realizations using the same AnalyticalEngine used for historical data...")
        print()
        
        ensemble_analysis = {
            'analysis_date': datetime.now().isoformat(),
            'wet_day_threshold': wet_day_threshold,
            'methods_analyzed': list(self.results.keys()),
            'method_results': {},
            'parameter_consistency': {},
            'ensemble_statistics': {}
        }
        
        # Analyze each simulation method
        for method_name, sim_data in self.results.items():
            print(f"--- {method_name.upper()} METHOD ENSEMBLE ANALYSIS ---")
            
            n_realizations = sim_data.shape[1]
            sample_size = min(20, n_realizations)  # Analyze up to 20 realizations in detail
            
            print(f"Total realizations: {n_realizations}")
            print(f"Detailed analysis sample: {sample_size} realizations")
            
            # Analyze sample of realizations with AnalyticalEngine
            realization_analyses = []
            parameter_estimates = []
            
            for i in range(sample_size):
                # Get synthetic data for this realization
                synthetic_series = self.get_synthetic_data_for_analysis(method_name, i)
                
                # Analyze with AnalyticalEngine (identical to historical data analysis)
                engine = AnalyticalEngine(synthetic_series, wet_day_threshold=wet_day_threshold)
                engine.initialize()
                
                # Calculate parameters (same process as historical data)
                monthly_params = engine.calculate_monthly_parameters()
                
                # Get annual statistics
                annual_stats = engine.get_annual_precipitation_statistics()
                
                realization_analyses.append({
                    'realization': i,
                    'monthly_parameters': monthly_params,
                    'annual_statistics': annual_stats,
                    'engine_state': engine.get_state()
                })
                
                parameter_estimates.append(monthly_params)
            
            # Calculate ensemble parameter statistics
            ensemble_params = self._calculate_ensemble_parameter_statistics(parameter_estimates)
            
            # Parameter consistency analysis
            consistency_analysis = self._analyze_parameter_consistency(
                parameter_estimates, self.parameters.overall_parameters
            )
            
            method_results = {
                'n_realizations': n_realizations,
                'sample_size': sample_size,
                'realization_analyses': realization_analyses,
                'ensemble_parameters': ensemble_params,
                'consistency_analysis': consistency_analysis
            }
            
            ensemble_analysis['method_results'][method_name] = method_results
            
            # Print summary
            print(f"Parameter consistency score: {consistency_analysis['overall_score']:.2f}/100")
            print(f"Best month consistency: {consistency_analysis['best_month']} (score: {consistency_analysis['monthly_scores'][consistency_analysis['best_month']]:.1f})")
            print(f"Worst month consistency: {consistency_analysis['worst_month']} (score: {consistency_analysis['monthly_scores'][consistency_analysis['worst_month']]:.1f})")
            print()
        
        # Cross-method parameter comparison
        if len(self.results) > 1:
            print("--- CROSS-METHOD PARAMETER COMPARISON ---")
            cross_method_analysis = self._compare_method_parameters(ensemble_analysis['method_results'])
            ensemble_analysis['cross_method_comparison'] = cross_method_analysis
            
            print(f"Method parameter correlation: {cross_method_analysis['average_correlation']:.3f}")
            print(f"Most consistent parameter: {cross_method_analysis['most_consistent_parameter']}")
            print(f"Least consistent parameter: {cross_method_analysis['least_consistent_parameter']}")
            print()
        
        # Overall ensemble statistics
        print("--- OVERALL ENSEMBLE STATISTICS ---")
        overall_stats = self._calculate_overall_ensemble_statistics(ensemble_analysis['method_results'])
        ensemble_analysis['ensemble_statistics'] = overall_stats
        
        print(f"Average parameter recovery rate: {overall_stats['average_recovery_rate']:.1f}%")
        print(f"Parameter estimation stability: {overall_stats['estimation_stability']}")
        print(f"Recommended method: {overall_stats['recommended_method']}")
        print()
        
        # Generate ensemble analysis plots
        if save_plots:
            print("--- GENERATING ENSEMBLE ANALYSIS PLOTS ---")
            plot_paths = self._create_ensemble_analysis_plots(ensemble_analysis, output_dir)
            ensemble_analysis['plot_paths'] = plot_paths
            
            for plot_type, path in plot_paths.items():
                print(f"  {plot_type}: {path}")
            print()
        
        return ensemble_analysis
    
    def _calculate_ensemble_parameter_statistics(self, parameter_estimates: List[Dict]) -> Dict[str, Any]:
        """Calculate statistics across parameter estimates from multiple realizations."""
        import numpy as np
        
        # Organize parameters by month and type
        monthly_stats = {}
        
        for month in range(1, 13):
            month_params = {
                'p_ww': [params[month].p_ww for params in parameter_estimates],
                'p_wd': [params[month].p_wd for params in parameter_estimates],
                'alpha': [params[month].alpha for params in parameter_estimates],
                'beta': [params[month].beta for params in parameter_estimates]
            }
            
            monthly_stats[month] = {}
            for param_name, values in month_params.items():
                monthly_stats[month][param_name] = {
                    'mean': np.mean(values),
                    'std': np.std(values),
                    'min': np.min(values),
                    'max': np.max(values),
                    'cv': np.std(values) / np.mean(values) if np.mean(values) != 0 else 0
                }
        
        return monthly_stats
    
    def _analyze_parameter_consistency(self, parameter_estimates: List[Dict], 
                                     original_parameters: Dict) -> Dict[str, Any]:
        """Analyze consistency between estimated and original parameters."""
        import numpy as np
        
        monthly_scores = {}
        param_biases = {}
        
        for month in range(1, 13):
            original = original_parameters[month]
            estimates = [params[month] for params in parameter_estimates]
            
            # Calculate biases for each parameter
            biases = {
                'p_ww': [est.p_ww - original.p_ww for est in estimates],
                'p_wd': [est.p_wd - original.p_wd for est in estimates],
                'alpha': [est.alpha - original.alpha for est in estimates],
                'beta': [est.beta - original.beta for est in estimates]
            }
            
            param_biases[month] = biases
            
            # Calculate consistency score for this month (0-100)
            month_score = 100.0
            
            for param_name, bias_values in biases.items():
                mean_bias = np.mean(bias_values)
                std_bias = np.std(bias_values)
                
                # Penalize based on mean bias and variability
                if param_name in ['p_ww', 'p_wd']:
                    # Transition probabilities: stricter tolerance
                    month_score -= min(25, abs(mean_bias) * 500)  # Up to 25 points for bias
                    month_score -= min(15, std_bias * 300)        # Up to 15 points for variability
                else:
                    # Gamma parameters: more lenient
                    month_score -= min(20, abs(mean_bias) * 10)   # Up to 20 points for bias
                    month_score -= min(10, std_bias * 5)          # Up to 10 points for variability
            
            monthly_scores[month] = max(0, month_score)
        
        overall_score = np.mean(list(monthly_scores.values()))
        best_month = max(monthly_scores.keys(), key=lambda k: monthly_scores[k])
        worst_month = min(monthly_scores.keys(), key=lambda k: monthly_scores[k])
        
        return {
            'overall_score': overall_score,
            'monthly_scores': monthly_scores,
            'parameter_biases': param_biases,
            'best_month': best_month,
            'worst_month': worst_month,
            'consistency_rating': self._rate_consistency(overall_score)
        }
    
    def _rate_consistency(self, score: float) -> str:
        """Rate parameter consistency based on score."""
        if score >= 90:
            return "Excellent"
        elif score >= 80:
            return "Good"
        elif score >= 70:
            return "Fair"
        elif score >= 60:
            return "Poor"
        else:
            return "Very Poor"
    
    def _compare_method_parameters(self, method_results: Dict[str, Any]) -> Dict[str, Any]:
        """Compare parameters across different simulation methods."""
        import numpy as np
        from itertools import combinations
        
        methods = list(method_results.keys())
        if len(methods) < 2:
            return {'message': 'Need at least 2 methods for comparison'}
        
        # Extract ensemble parameters for each method
        method_params = {}
        for method in methods:
            ensemble_params = method_results[method]['ensemble_parameters']
            method_params[method] = ensemble_params
        
        # Calculate correlations between methods
        correlations = {}
        param_names = ['p_ww', 'p_wd', 'alpha', 'beta']
        
        for param_name in param_names:
            param_correlations = []
            
            for method1, method2 in combinations(methods, 2):
                values1 = []
                values2 = []
                
                for month in range(1, 13):
                    values1.append(method_params[method1][month][param_name]['mean'])
                    values2.append(method_params[method2][month][param_name]['mean'])
                
                corr = np.corrcoef(values1, values2)[0, 1]
                param_correlations.append(corr)
            
            correlations[param_name] = np.mean(param_correlations)
        
        # Find most and least consistent parameters
        most_consistent = max(correlations.keys(), key=lambda k: correlations[k])
        least_consistent = min(correlations.keys(), key=lambda k: correlations[k])
        
        return {
            'parameter_correlations': correlations,
            'average_correlation': np.mean(list(correlations.values())),
            'most_consistent_parameter': most_consistent,
            'least_consistent_parameter': least_consistent
        }
    
    def _calculate_overall_ensemble_statistics(self, method_results: Dict[str, Any]) -> Dict[str, Any]:
        """Calculate overall ensemble statistics across all methods."""
        import numpy as np
        
        # Calculate average parameter recovery rate
        recovery_rates = []
        stability_ratings = []
        
        for method, results in method_results.items():
            consistency = results['consistency_analysis']
            recovery_rates.append(consistency['overall_score'])
            
            # Stability based on parameter variability
            ensemble_params = results['ensemble_parameters']
            cv_values = []
            for month in range(1, 13):
                for param_name in ['p_ww', 'p_wd', 'alpha', 'beta']:
                    cv_values.append(ensemble_params[month][param_name]['cv'])
            
            avg_cv = np.mean(cv_values)
            stability_ratings.append(100 - min(100, avg_cv * 1000))  # Convert CV to stability score
        
        avg_recovery = np.mean(recovery_rates)
        avg_stability = np.mean(stability_ratings)
        
        # Determine stability rating
        if avg_stability >= 90:
            stability_rating = "Excellent"
        elif avg_stability >= 80:
            stability_rating = "Good"
        elif avg_stability >= 70:
            stability_rating = "Fair"
        else:
            stability_rating = "Poor"
        
        # Recommend best method
        method_scores = {}
        for method, results in method_results.items():
            score = results['consistency_analysis']['overall_score']
            method_scores[method] = score
        
        recommended_method = max(method_scores.keys(), key=lambda k: method_scores[k])
        
        return {
            'average_recovery_rate': avg_recovery,
            'average_stability_score': avg_stability,
            'estimation_stability': stability_rating,
            'method_scores': method_scores,
            'recommended_method': recommended_method
        }
    
    def _create_ensemble_analysis_plots(self, ensemble_analysis: Dict[str, Any], 
                                      output_dir: str) -> Dict[str, str]:
        """Create comprehensive ensemble analysis plots."""
        import matplotlib.pyplot as plt
        import numpy as np
        from pathlib import Path
        
        plot_paths = {}
        
        # 1. Parameter consistency analysis plot
        fig, axes = plt.subplots(2, 2, figsize=(15, 12))
        
        methods = list(ensemble_analysis['method_results'].keys())
        
        # Monthly consistency scores
        ax = axes[0, 0]
        months = range(1, 13)
        
        for method in methods:
            consistency = ensemble_analysis['method_results'][method]['consistency_analysis']
            scores = [consistency['monthly_scores'][month] for month in months]
            ax.plot(months, scores, 'o-', label=f'{method.title()}', linewidth=2)
        
        ax.set_xlabel('Month')
        ax.set_ylabel('Consistency Score')
        ax.set_title('Monthly Parameter Consistency Scores')
        ax.legend()
        ax.grid(True, alpha=0.3)
        ax.set_ylim(0, 100)
        
        # Parameter bias analysis (first method)
        if methods:
            method = methods[0]
            consistency = ensemble_analysis['method_results'][method]['consistency_analysis']
            param_biases = consistency['parameter_biases']
            
            ax = axes[0, 1]
            param_names = ['p_ww', 'p_wd', 'alpha', 'beta']
            
            for i, param_name in enumerate(param_names):
                biases_all_months = []
                for month in range(1, 13):
                    biases_all_months.extend(param_biases[month][param_name])
                
                ax.boxplot([biases_all_months], positions=[i], widths=0.6)
            
            ax.set_xticks(range(len(param_names)))
            ax.set_xticklabels(param_names)
            ax.set_ylabel('Parameter Bias')
            ax.set_title(f'Parameter Bias Distribution ({method.title()})')
            ax.grid(True, alpha=0.3)
            ax.axhline(y=0, color='r', linestyle='--', alpha=0.7)
        
        # Cross-method parameter correlation (if multiple methods)
        if len(methods) > 1 and 'cross_method_comparison' in ensemble_analysis:
            ax = axes[1, 0]
            cross_method = ensemble_analysis['cross_method_comparison']
            param_correlations = cross_method['parameter_correlations']
            
            param_names = list(param_correlations.keys())
            correlations = list(param_correlations.values())
            
            bars = ax.bar(param_names, correlations, alpha=0.7)
            ax.set_ylabel('Correlation')
            ax.set_title('Cross-Method Parameter Correlations')
            ax.grid(True, alpha=0.3)
            ax.set_ylim(0, 1)
            
            # Color bars based on correlation strength
            for bar, corr in zip(bars, correlations):
                if corr > 0.8:
                    bar.set_color('green')
                elif corr > 0.6:
                    bar.set_color('orange')
                else:
                    bar.set_color('red')
        
        # Overall method comparison
        ax = axes[1, 1]
        overall_stats = ensemble_analysis['ensemble_statistics']
        method_scores = overall_stats['method_scores']
        
        methods_list = list(method_scores.keys())
        scores = list(method_scores.values())
        
        bars = ax.bar(methods_list, scores, alpha=0.7)
        ax.set_ylabel('Overall Score')
        ax.set_title('Method Performance Comparison')
        ax.grid(True, alpha=0.3)
        ax.set_ylim(0, 100)
        
        # Color bars and add score labels
        for bar, score in zip(bars, scores):
            if score > 80:
                bar.set_color('green')
            elif score > 60:
                bar.set_color('orange')
            else:
                bar.set_color('red')
            
            # Add score label on bar
            ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 1,
                   f'{score:.1f}', ha='center', va='bottom')
        
        plt.tight_layout()
        plot_path = Path(output_dir) / 'ensemble_parameter_analysis.png'
        plt.savefig(plot_path, dpi=300, bbox_inches='tight')
        plt.close()
        plot_paths['parameter_analysis'] = str(plot_path)
        
        # 2. Parameter distribution plots
        if methods:
            method = methods[0]  # Use first method for detailed analysis
            ensemble_params = ensemble_analysis['method_results'][method]['ensemble_parameters']
            
            fig, axes = plt.subplots(2, 2, figsize=(15, 12))
            param_names = ['p_ww', 'p_wd', 'alpha', 'beta']
            
            for i, param_name in enumerate(param_names):
                ax = axes[i // 2, i % 2]
                
                # Extract parameter values across months
                means = [ensemble_params[month][param_name]['mean'] for month in range(1, 13)]
                stds = [ensemble_params[month][param_name]['std'] for month in range(1, 13)]
                
                # Original parameters for comparison
                original_values = [getattr(self.parameters.overall_parameters[month], param_name) 
                                 for month in range(1, 13)]
                
                months = range(1, 13)
                
                # Plot ensemble mean with error bars
                ax.errorbar(months, means, yerr=stds, 
                           label=f'Ensemble Mean ± SD', 
                           capsize=5, capthick=2, linewidth=2)
                
                # Plot original parameters
                ax.plot(months, original_values, 'ro-', 
                       label='Original Parameters', linewidth=2, markersize=6)
                
                ax.set_xlabel('Month')
                ax.set_ylabel(param_name)
                ax.set_title(f'{param_name} Parameter Recovery ({method.title()})')
                ax.legend()
                ax.grid(True, alpha=0.3)
            
            plt.tight_layout()
            plot_path = Path(output_dir) / 'parameter_recovery_analysis.png'
            plt.savefig(plot_path, dpi=300, bbox_inches='tight')
            plt.close()
            plot_paths['parameter_recovery'] = str(plot_path)
        
        return plot_paths