"""
Intelligent Multi-Strategy Optimizer - Protocol 10 Implementation.

This is the main orchestrator for Protocol 10: Intelligent Multi-Strategy
Optimization (IMSO). It coordinates landscape analysis, strategy selection,
and dynamic strategy switching to create a self-tuning optimization system.

Architecture:
    1. Landscape Analyzer: Characterizes the optimization problem
    2. Strategy Selector: Recommends best algorithm based on characteristics
    3. Strategy Portfolio Manager: Handles dynamic switching between strategies
    4. Adaptive Callbacks: Integrates with Optuna for runtime adaptation

This module enables Atomizer to automatically adapt to different FEA problem
types without requiring manual algorithm configuration.

Usage:
    from optimization_engine.core.intelligent_optimizer import IntelligentOptimizer

    optimizer = IntelligentOptimizer(
        study_name="my_study",
        study_dir=Path("results"),
        config=config_dict
    )

    best_params = optimizer.optimize(
        objective_function=my_objective,
        n_trials=100
    )
"""

import optuna
from pathlib import Path
from typing import Dict, Callable, Optional, Any
import json
from datetime import datetime

from optimization_engine.reporting.landscape_analyzer import LandscapeAnalyzer, print_landscape_report
from optimization_engine.core.strategy_selector import (
    IntelligentStrategySelector,
    create_sampler_from_config
)
from optimization_engine.core.strategy_portfolio import (
    StrategyTransitionManager,
    AdaptiveStrategyCallback
)
from optimization_engine.processors.surrogates.adaptive_surrogate import AdaptiveExploitationCallback
from optimization_engine.processors.adaptive_characterization import CharacterizationStoppingCriterion
from optimization_engine.utils.realtime_tracking import create_realtime_callback


class IntelligentOptimizer:
    """
    Self-tuning multi-strategy optimizer for FEA problems.

    This class implements Protocol 10: Intelligent Multi-Strategy Optimization.
    It automatically:
    1. Analyzes problem characteristics
    2. Selects appropriate optimization algorithms
    3. Switches strategies dynamically based on performance
    4. Logs all decisions for transparency and learning
    """

    def __init__(
        self,
        study_name: str,
        study_dir: Path,
        config: Dict,
        verbose: bool = True
    ):
        """
        Initialize intelligent optimizer.

        Args:
            study_name: Name for the optimization study
            study_dir: Directory to save optimization results
            config: Configuration dictionary with Protocol 10 settings
            verbose: Print detailed progress information
        """
        self.study_name = study_name
        self.study_dir = Path(study_dir)
        self.config = config
        self.verbose = verbose

        # Extract Protocol 10 configuration
        self.protocol_config = config.get('intelligent_optimization', {})
        self.enabled = self.protocol_config.get('enabled', True)

        # Setup tracking directory
        self.tracking_dir = self.study_dir / "intelligent_optimizer"
        self.tracking_dir.mkdir(parents=True, exist_ok=True)

        # Initialize components
        self.landscape_analyzer = LandscapeAnalyzer(
            min_trials_for_analysis=self.protocol_config.get('min_analysis_trials', 10)
        )

        self.strategy_selector = IntelligentStrategySelector(verbose=verbose)

        self.transition_manager = StrategyTransitionManager(
            stagnation_window=self.protocol_config.get('stagnation_window', 10),
            min_improvement_threshold=self.protocol_config.get('min_improvement_threshold', 0.001),
            verbose=verbose,
            tracking_dir=self.tracking_dir
        )

        # State tracking
        self.current_phase = "initialization"
        self.current_strategy = None
        self.landscape_cache = None
        self.recommendation_cache = None

        # Optuna study (will be created in optimize())
        self.study: Optional[optuna.Study] = None
        self.directions: Optional[list] = None  # Store study directions

        # Protocol 13: Create realtime tracking callback
        self.realtime_callback = create_realtime_callback(
            tracking_dir=self.tracking_dir,
            optimizer_ref=self,
            verbose=self.verbose
        )

        # Protocol 11: Print multi-objective support notice
        if self.verbose:
            print(f"\n[Protocol 11] Multi-objective optimization: ENABLED")
            print(f"[Protocol 11] Supports single-objective and multi-objective studies")
            print(f"[Protocol 13] Real-time tracking: ENABLED (per-trial JSON writes)")

    def optimize(
        self,
        objective_function: Callable,
        design_variables: Dict[str, tuple],
        n_trials: int = 100,
        target_value: Optional[float] = None,
        tolerance: float = 0.1,
        directions: Optional[list] = None
    ) -> Dict[str, Any]:
        """
        Run intelligent multi-strategy optimization.

        This is the main entry point that orchestrates the entire Protocol 10 process.

        Args:
            objective_function: Function to minimize, signature: f(trial) -> float or tuple
            design_variables: Dict of {var_name: (low, high)} bounds
            n_trials: Total trial budget
            target_value: Target objective value (optional, for single-objective)
            tolerance: Acceptable error from target
            directions: List of 'minimize' or 'maximize' for multi-objective (e.g., ['minimize', 'minimize'])
                       If None, defaults to single-objective minimization

        Returns:
            Dictionary with:
            - best_params: Best parameter configuration found
            - best_value: Best objective value achieved (or tuple for multi-objective)
            - strategy_used: Final strategy used
            - landscape_analysis: Problem characterization
            - performance_summary: Strategy performance breakdown
        """
        # Store directions for study creation
        self.directions = directions
        if not self.enabled:
            return self._run_fallback_optimization(
                objective_function, design_variables, n_trials
            )

        # Stage 1: Adaptive Characterization
        self.current_phase = "characterization"
        if self.verbose:
            self._print_phase_header("STAGE 1: ADAPTIVE CHARACTERIZATION")

        # Get characterization config
        char_config = self.protocol_config.get('characterization', {})
        min_trials = char_config.get('min_trials', 10)
        max_trials = char_config.get('max_trials', 30)
        confidence_threshold = char_config.get('confidence_threshold', 0.85)
        check_interval = char_config.get('check_interval', 5)

        # Create stopping criterion
        stopping_criterion = CharacterizationStoppingCriterion(
            min_trials=min_trials,
            max_trials=max_trials,
            confidence_threshold=confidence_threshold,
            check_interval=check_interval,
            verbose=self.verbose,
            tracking_dir=self.tracking_dir
        )

        # Create characterization study with random sampler (unbiased exploration)
        self.study = self._create_study(
            sampler=optuna.samplers.RandomSampler(),
            design_variables=design_variables
        )

        # Run adaptive characterization
        while not stopping_criterion.should_stop(self.study):
            # Run batch of trials
            self.study.optimize(
                objective_function,
                n_trials=check_interval,
                callbacks=[self.realtime_callback]
            )

            # Analyze landscape
            self.landscape_cache = self.landscape_analyzer.analyze(self.study)

            # Update stopping criterion
            if self.landscape_cache.get('ready', False):
                completed_trials = [t for t in self.study.trials if t.state == optuna.trial.TrialState.COMPLETE]
                stopping_criterion.update(self.landscape_cache, len(completed_trials))

        # Print characterization summary
        if self.verbose:
            print(stopping_criterion.get_summary_report())
            print_landscape_report(self.landscape_cache)

        # Stage 2: Intelligent Strategy Selection
        self.current_phase = "strategy_selection"
        if self.verbose:
            self._print_phase_header("STAGE 2: STRATEGY SELECTION")

        strategy, recommendation = self.strategy_selector.recommend_strategy(
            landscape=self.landscape_cache,
            trials_completed=len(self.study.trials),
            trials_budget=n_trials
        )

        self.current_strategy = strategy
        self.recommendation_cache = recommendation

        # Create new study with recommended strategy
        sampler = create_sampler_from_config(recommendation['sampler_config'])
        self.study = self._create_study(
            sampler=sampler,
            design_variables=design_variables,
            load_from_previous=True  # Preserve initial trials
        )

        # Setup adaptive callbacks
        callbacks = self._create_callbacks(target_value, tolerance)

        # Stage 3: Adaptive Optimization with Monitoring
        self.current_phase = "adaptive_optimization"
        if self.verbose:
            self._print_phase_header("STAGE 3: ADAPTIVE OPTIMIZATION")

        remaining_trials = n_trials - len(self.study.trials)

        if remaining_trials > 0:
            # Add realtime tracking to callbacks
            all_callbacks = callbacks + [self.realtime_callback]
            self.study.optimize(
                objective_function,
                n_trials=remaining_trials,
                callbacks=all_callbacks
            )

        # Generate final report
        results = self._compile_results()

        if self.verbose:
            self._print_final_summary(results)

        return results

    def _create_study(
        self,
        sampler: optuna.samplers.BaseSampler,
        design_variables: Dict[str, tuple],
        load_from_previous: bool = False
    ) -> optuna.Study:
        """
        Create Optuna study with specified sampler.

        Args:
            sampler: Optuna sampler to use
            design_variables: Parameter bounds
            load_from_previous: Load trials from previous study

        Returns:
            Configured Optuna study
        """
        # Create study storage
        storage_path = self.study_dir / "study.db"
        storage = f"sqlite:///{storage_path}"

        if load_from_previous and storage_path.exists():
            # Load existing study and change sampler
            study = optuna.load_study(
                study_name=self.study_name,
                storage=storage,
                sampler=sampler
            )
        else:
            # Create new study (single or multi-objective)
            if self.directions is not None:
                # Multi-objective optimization
                study = optuna.create_study(
                    study_name=self.study_name,
                    storage=storage,
                    directions=self.directions,
                    sampler=sampler,
                    load_if_exists=True
                )
            else:
                # Single-objective optimization (backward compatibility)
                study = optuna.create_study(
                    study_name=self.study_name,
                    storage=storage,
                    direction='minimize',
                    sampler=sampler,
                    load_if_exists=True
                )

        return study

    def _create_callbacks(
        self,
        target_value: Optional[float],
        tolerance: float
    ) -> list:
        """Create list of Optuna callbacks for adaptive optimization."""
        callbacks = []

        # Adaptive exploitation callback (from Protocol 8)
        adaptive_callback = AdaptiveExploitationCallback(
            target_value=target_value,
            tolerance=tolerance,
            min_confidence_for_exploitation=0.65,
            min_trials=15,
            verbose=self.verbose,
            tracking_dir=self.tracking_dir
        )
        callbacks.append(adaptive_callback)

        # Strategy switching callback (Protocol 10)
        strategy_callback = AdaptiveStrategyCallback(
            transition_manager=self.transition_manager,
            landscape_analyzer=self.landscape_analyzer,
            strategy_selector=self.strategy_selector,
            reanalysis_interval=self.protocol_config.get('reanalysis_interval', 15)
        )
        callbacks.append(strategy_callback)

        return callbacks

    def _compile_results(self) -> Dict[str, Any]:
        """Compile comprehensive optimization results (supports single and multi-objective)."""
        is_multi_objective = len(self.study.directions) > 1

        if is_multi_objective:
            # Multi-objective: Return Pareto front info
            best_trials = self.study.best_trials
            if best_trials:
                # Select the first Pareto-optimal solution as representative
                representative_trial = best_trials[0]
                best_params = representative_trial.params
                best_value = representative_trial.values  # Tuple of objectives
                best_trial_num = representative_trial.number
            else:
                best_params = {}
                best_value = None
                best_trial_num = None
        else:
            # Single-objective: Use standard Optuna API
            best_params = self.study.best_params
            best_value = self.study.best_value
            best_trial_num = self.study.best_trial.number

        return {
            'best_params': best_params,
            'best_value': best_value,
            'best_trial': best_trial_num,
            'is_multi_objective': is_multi_objective,
            'pareto_front_size': len(self.study.best_trials) if is_multi_objective else 1,
            'total_trials': len(self.study.trials),
            'final_strategy': self.current_strategy,
            'landscape_analysis': self.landscape_cache,
            'strategy_recommendation': self.recommendation_cache,
            'transition_history': self.transition_manager.transition_history,
            'strategy_performance': {
                name: {
                    'trials_used': perf.trials_used,
                    'best_value': perf.best_value_achieved,
                    'improvement_rate': perf.improvement_rate
                }
                for name, perf in self.transition_manager.strategy_history.items()
            },
            'protocol_used': 'Protocol 10: Intelligent Multi-Strategy Optimization'
        }

    def _run_fallback_optimization(
        self,
        objective_function: Callable,
        design_variables: Dict[str, tuple],
        n_trials: int
    ) -> Dict[str, Any]:
        """Fallback to standard TPE optimization if Protocol 10 is disabled (supports multi-objective)."""
        if self.verbose:
            print("\n  Protocol 10 disabled - using standard TPE optimization\n")

        sampler = optuna.samplers.TPESampler(multivariate=True, n_startup_trials=10)
        self.study = self._create_study(sampler, design_variables)

        self.study.optimize(
            objective_function,
            n_trials=n_trials,
            callbacks=[self.realtime_callback]
        )

        # Handle both single and multi-objective
        is_multi_objective = len(self.study.directions) > 1

        if is_multi_objective:
            best_trials = self.study.best_trials
            if best_trials:
                representative_trial = best_trials[0]
                best_params = representative_trial.params
                best_value = representative_trial.values
                best_trial_num = representative_trial.number
            else:
                best_params = {}
                best_value = None
                best_trial_num = None
        else:
            best_params = self.study.best_params
            best_value = self.study.best_value
            best_trial_num = self.study.best_trial.number

        return {
            'best_params': best_params,
            'best_value': best_value,
            'best_trial': best_trial_num,
            'is_multi_objective': is_multi_objective,
            'total_trials': len(self.study.trials),
            'protocol_used': 'Standard TPE (Protocol 10 disabled)'
        }

    def _print_phase_header(self, phase_name: str):
        """Print formatted phase transition header."""
        print(f"\n{'='*70}")
        print(f"  {phase_name}")
        print(f"{'='*70}\n")

    def _print_final_summary(self, results: Dict):
        """Print comprehensive final optimization summary."""
        print(f"\n{'='*70}")
        print(f"  OPTIMIZATION COMPLETE")
        print(f"{'='*70}")
        print(f"  Protocol: {results['protocol_used']}")
        print(f"  Total Trials: {results['total_trials']}")

        # Handle both single and multi-objective best values
        best_value = results['best_value']
        if results.get('is_multi_objective', False):
            # Multi-objective: best_value is a tuple
            formatted_value = str(best_value)  # Show as tuple
            print(f"  Best Values (Pareto): {formatted_value} (Trial #{results['best_trial']})")
        else:
            # Single-objective: best_value is a scalar
            print(f"  Best Value: {best_value:.6f} (Trial #{results['best_trial']})")

        print(f"  Final Strategy: {results.get('final_strategy', 'N/A').upper()}")

        if results.get('transition_history'):
            print(f"\n  Strategy Transitions: {len(results['transition_history'])}")
            for event in results['transition_history']:
                print(f"    Trial #{event['trial_number']}: "
                      f"{event['from_strategy']} → {event['to_strategy']}")

        print(f"\n  Best Parameters:")
        for param, value in results['best_params'].items():
            print(f"    {param}: {value:.6f}")

        print(f"{'='*70}\n")

        # Print strategy performance report
        if self.transition_manager.strategy_history:
            print(self.transition_manager.get_performance_report())

    def save_intelligence_report(self, filepath: Optional[Path] = None):
        """
        Save comprehensive intelligence report to JSON.

        This report contains all decision-making data for transparency,
        debugging, and transfer learning to future optimizations.
        """
        if filepath is None:
            filepath = self.tracking_dir / "intelligence_report.json"

        report = {
            'study_name': self.study_name,
            'timestamp': datetime.now().isoformat(),
            'configuration': self.protocol_config,
            'landscape_analysis': self.landscape_cache,
            'initial_recommendation': self.recommendation_cache,
            'final_strategy': self.current_strategy,
            'transition_history': self.transition_manager.transition_history,
            'strategy_performance': {
                name: {
                    'trials_used': perf.trials_used,
                    'best_value_achieved': perf.best_value_achieved,
                    'improvement_rate': perf.improvement_rate,
                    'last_used_trial': perf.last_used_trial
                }
                for name, perf in self.transition_manager.strategy_history.items()
            },
            'recommendation_history': self.strategy_selector.recommendation_history
        }

        try:
            with open(filepath, 'w') as f:
                json.dump(report, f, indent=2)

            if self.verbose:
                print(f"\n  Intelligence report saved: {filepath}\n")
        except Exception as e:
            if self.verbose:
                print(f"\n  Warning: Failed to save intelligence report: {e}\n")


# Convenience function for quick usage
def create_intelligent_optimizer(
    study_name: str,
    study_dir: Path,
    config: Optional[Dict] = None,
    verbose: bool = True
) -> IntelligentOptimizer:
    """
    Factory function to create IntelligentOptimizer with sensible defaults.

    Args:
        study_name: Name for the optimization study
        study_dir: Directory for results
        config: Optional configuration (uses defaults if None)
        verbose: Print progress

    Returns:
        Configured IntelligentOptimizer instance
    """
    if config is None:
        # Default Protocol 10 configuration
        config = {
            'intelligent_optimization': {
                'enabled': True,
                'characterization_trials': 15,
                'stagnation_window': 10,
                'min_improvement_threshold': 0.001,
                'min_analysis_trials': 10,
                'reanalysis_interval': 15
            }
        }

    return IntelligentOptimizer(
        study_name=study_name,
        study_dir=study_dir,
        config=config,
        verbose=verbose
    )
