"""
Adaptive surrogate modeling with confidence-based exploration-exploitation transitions.

This module implements state-of-the-art Bayesian optimization strategies that
dynamically adjust exploration vs exploitation based on surrogate model confidence.
"""

import numpy as np
from typing import Optional, Dict, List
import optuna
from scipy.stats import variation
import json
from pathlib import Path


class SurrogateConfidenceMetrics:
    """Calculate confidence metrics for surrogate model quality.

    STUDY-AWARE: Uses study.trials directly instead of session-based history
    to properly track confidence across multiple optimization runs.
    """

    def __init__(self, min_trials_for_confidence: int = 15):
        self.min_trials = min_trials_for_confidence

    def update(self, study: optuna.Study, trial: optuna.trial.FrozenTrial):
        """Update metrics after each trial (no-op for study-aware implementation)."""
        pass  # Study-aware: we read directly from study.trials

    def calculate_confidence(self, study: optuna.Study) -> Dict[str, float]:
        """
        Calculate comprehensive surrogate confidence metrics.

        STUDY-AWARE: Uses ALL completed trials from the study database,
        not just trials from the current session.

        PROTOCOL 11: Multi-objective studies are NOT supported by adaptive
        characterization. Return immediately with max confidence to skip
        characterization phase.

        Returns:
            Dict with confidence scores:
            - 'overall_confidence': 0-1 score, where 1 = high confidence
            - 'convergence_score': How stable recent improvements are
            - 'exploration_coverage': How well parameter space is covered
            - 'prediction_stability': How consistent the model predictions are
        """
        # [Protocol 11] Multi-objective NOT supported by adaptive characterization
        is_multi_objective = len(study.directions) > 1
        if is_multi_objective:
            return {
                'overall_confidence': 1.0,  # Skip characterization
                'convergence_score': 1.0,
                'exploration_coverage': 1.0,
                'prediction_stability': 1.0,
                'ready_for_exploitation': True,  # Go straight to NSGA-II
                'total_trials': len(study.trials),
                'message': '[Protocol 11] Multi-objective: skipping adaptive characterization, using NSGA-II directly'
            }

        # Get ALL completed trials from study (study-aware)
        all_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]

        if len(all_trials) < self.min_trials:
            return {
                'overall_confidence': 0.0,
                'convergence_score': 0.0,
                'exploration_coverage': 0.0,
                'prediction_stability': 0.0,
                'ready_for_exploitation': False,
                'total_trials': len(all_trials),
                'message': f'Need {self.min_trials - len(all_trials)} more trials for confidence assessment (currently {len(all_trials)} trials)'
            }

        # 1. Convergence Score - are we finding consistent improvements?
        recent_window = 10
        recent_trials = all_trials[-recent_window:]
        recent_values = [t.value for t in recent_trials]  # Safe: single-objective only

        # Calculate improvement rate
        improvements = []
        for i in range(1, len(recent_values)):
            if recent_values[i] < recent_values[i-1]:
                improvement = (recent_values[i-1] - recent_values[i]) / abs(recent_values[i-1])
                improvements.append(improvement)

        # If we're consistently finding improvements, convergence is good
        if improvements:
            avg_improvement = np.mean(improvements)
            improvement_consistency = 1.0 - variation(improvements) if len(improvements) > 1 else 0.5
            convergence_score = min(1.0, avg_improvement * improvement_consistency * 10)
        else:
            convergence_score = 0.0

        # 2. Exploration Coverage - how well have we covered parameter space?
        # Use coefficient of variation for each parameter
        param_names = list(all_trials[0].params.keys()) if all_trials else []

        coverage_scores = []
        for param in param_names:
            values = [t.params[param] for t in all_trials]

            # Get parameter bounds
            distribution = all_trials[0].distributions[param]
            param_range = distribution.high - distribution.low

            # Calculate spread relative to bounds
            spread = max(values) - min(values)
            coverage = spread / param_range
            coverage_scores.append(coverage)

        exploration_coverage = np.mean(coverage_scores)

        # 3. Prediction Stability - are recent trials clustered in good regions?
        recent_best_values = []
        current_best = float('inf')
        for trial in recent_trials:
            current_best = min(current_best, trial.value)
            recent_best_values.append(current_best)

        # If best hasn't improved much recently, model is stable
        if len(recent_best_values) > 1:
            best_stability = 1.0 - (recent_best_values[0] - recent_best_values[-1]) / (recent_best_values[0] + 1e-10)
            prediction_stability = max(0.0, min(1.0, best_stability))
        else:
            prediction_stability = 0.0

        # 4. Overall Confidence - weighted combination
        overall_confidence = (
            0.4 * convergence_score +
            0.3 * exploration_coverage +
            0.3 * prediction_stability
        )

        # Decision: Ready for intensive exploitation?
        ready_for_exploitation = (
            overall_confidence >= 0.65 and
            exploration_coverage >= 0.5 and
            len(all_trials) >= self.min_trials
        )

        message = self._get_confidence_message(overall_confidence, ready_for_exploitation)

        return {
            'overall_confidence': overall_confidence,
            'convergence_score': convergence_score,
            'exploration_coverage': exploration_coverage,
            'prediction_stability': prediction_stability,
            'ready_for_exploitation': ready_for_exploitation,
            'total_trials': len(all_trials),
            'message': message
        }

    def _get_confidence_message(self, confidence: float, ready: bool) -> str:
        """Generate human-readable confidence assessment."""
        if ready:
            return f"HIGH CONFIDENCE ({confidence:.1%}) - Transitioning to exploitation phase"
        elif confidence >= 0.5:
            return f"MEDIUM CONFIDENCE ({confidence:.1%}) - Continue exploration with some exploitation"
        elif confidence >= 0.3:
            return f"LOW CONFIDENCE ({confidence:.1%}) - Focus on exploration"
        else:
            return f"VERY LOW CONFIDENCE ({confidence:.1%}) - Need more diverse exploration"


class AdaptiveExploitationCallback:
    """
    Dynamically adjust sampler behavior based on surrogate confidence.

    This callback monitors surrogate model confidence and adapts the optimization
    strategy from exploration-heavy to exploitation-heavy as confidence increases.

    STUDY-AWARE: Tracks phase transitions across multiple optimization runs
    and persists confidence history to JSON files.
    """

    def __init__(
        self,
        target_value: Optional[float] = None,
        tolerance: float = 0.1,
        min_confidence_for_exploitation: float = 0.65,
        min_trials: int = 15,
        verbose: bool = True,
        tracking_dir: Optional[Path] = None
    ):
        """
        Args:
            target_value: Target objective value (if known)
            tolerance: Acceptable error from target
            min_confidence_for_exploitation: Confidence threshold to enable intensive exploitation
            min_trials: Minimum trials before confidence assessment
            verbose: Print confidence updates
            tracking_dir: Directory to save phase transition tracking files
        """
        self.target_value = target_value
        self.tolerance = tolerance
        self.min_confidence = min_confidence_for_exploitation
        self.verbose = verbose
        self.tracking_dir = tracking_dir

        self.metrics = SurrogateConfidenceMetrics(min_trials_for_confidence=min_trials)
        self.consecutive_successes = 0

        # Initialize phase transition tracking
        self.phase_transition_file = None
        self.confidence_history_file = None
        if tracking_dir:
            self.tracking_dir = Path(tracking_dir)
            self.tracking_dir.mkdir(parents=True, exist_ok=True)
            self.phase_transition_file = self.tracking_dir / "phase_transitions.json"
            self.confidence_history_file = self.tracking_dir / "confidence_history.json"

        # Load existing phase transition data if available
        self.phase_transitions = self._load_phase_transitions()
        self.confidence_history = self._load_confidence_history()

        # Determine current phase from history
        self.phase = self._get_current_phase()

    def _load_phase_transitions(self) -> List[Dict]:
        """Load existing phase transition history from JSON."""
        if self.phase_transition_file and self.phase_transition_file.exists():
            try:
                with open(self.phase_transition_file, 'r') as f:
                    return json.load(f)
            except Exception:
                return []
        return []

    def _load_confidence_history(self) -> List[Dict]:
        """Load existing confidence history from JSON."""
        if self.confidence_history_file and self.confidence_history_file.exists():
            try:
                with open(self.confidence_history_file, 'r') as f:
                    return json.load(f)
            except Exception:
                return []
        return []

    def _get_current_phase(self) -> str:
        """Determine current phase from transition history."""
        if not self.phase_transitions:
            return "exploration"
        # If any transition to exploitation exists, we're in exploitation
        for transition in self.phase_transitions:
            if transition.get('to_phase') == 'exploitation':
                return "exploitation"
        return "exploration"

    def _save_phase_transition(self, trial_number: int, confidence: Dict):
        """Save phase transition event to JSON."""
        if not self.phase_transition_file:
            return

        transition_event = {
            'trial_number': trial_number,
            'from_phase': 'exploration',
            'to_phase': 'exploitation',
            'confidence_metrics': {
                'overall_confidence': confidence['overall_confidence'],
                'convergence_score': confidence['convergence_score'],
                'exploration_coverage': confidence['exploration_coverage'],
                'prediction_stability': confidence['prediction_stability']
            },
            'total_trials': confidence.get('total_trials', trial_number + 1)
        }

        self.phase_transitions.append(transition_event)

        try:
            with open(self.phase_transition_file, 'w') as f:
                json.dump(self.phase_transitions, f, indent=2)
        except Exception as e:
            if self.verbose:
                print(f"  Warning: Failed to save phase transition: {e}")

    def _save_confidence_snapshot(self, trial_number: int, confidence: Dict):
        """Save confidence metrics snapshot to history."""
        if not self.confidence_history_file:
            return

        snapshot = {
            'trial_number': trial_number,
            'phase': self.phase,
            'confidence_metrics': {
                'overall_confidence': confidence['overall_confidence'],
                'convergence_score': confidence['convergence_score'],
                'exploration_coverage': confidence['exploration_coverage'],
                'prediction_stability': confidence['prediction_stability']
            },
            'total_trials': confidence.get('total_trials', trial_number + 1)
        }

        self.confidence_history.append(snapshot)

        try:
            with open(self.confidence_history_file, 'w') as f:
                json.dump(self.confidence_history, f, indent=2)
        except Exception as e:
            if self.verbose:
                print(f"  Warning: Failed to save confidence history: {e}")

    def __call__(self, study: optuna.Study, trial: optuna.trial.FrozenTrial):
        """Called after each trial completes."""
        # Skip failed trials
        if trial.state != optuna.trial.TrialState.COMPLETE:
            return

        # Update metrics (no-op for study-aware implementation)
        self.metrics.update(study, trial)

        # Calculate confidence
        confidence = self.metrics.calculate_confidence(study)

        # Save confidence snapshot every 5 trials
        if trial.number % 5 == 0:
            self._save_confidence_snapshot(trial.number, confidence)

        # Print confidence report
        if self.verbose and trial.number % 5 == 0:  # Every 5 trials
            self._print_confidence_report(trial.number, confidence)

        # Check for phase transition
        if confidence['ready_for_exploitation'] and self.phase == "exploration":
            self.phase = "exploitation"

            # Save transition event
            self._save_phase_transition(trial.number, confidence)

            if self.verbose:
                print(f"\n{'='*60}")
                print(f"  PHASE TRANSITION: EXPLORATION -> EXPLOITATION")
                print(f"  Trial #{trial.number}")
                print(f"  Surrogate confidence: {confidence['overall_confidence']:.1%}")
                print(f"  Now focusing on refining best regions")
                print(f"{'='*60}\n")

        # Check for target achievement
        if self.target_value is not None and trial.value <= self.tolerance:
            self.consecutive_successes += 1

            if self.verbose:
                print(f"  [TARGET] Trial #{trial.number}: {trial.value:.6f} ≤ {self.tolerance:.6f}")
                print(f"  [TARGET] Consecutive successes: {self.consecutive_successes}/3")

            # Stop after 3 consecutive successes in exploitation phase
            if self.consecutive_successes >= 3 and self.phase == "exploitation":
                if self.verbose:
                    print(f"\n{'='*60}")
                    print(f"  TARGET ACHIEVED WITH HIGH CONFIDENCE")
                    print(f"  Best value: {study.best_value:.6f}")
                    print(f"  Stopping optimization")
                    print(f"{'='*60}\n")
                study.stop()
        else:
            self.consecutive_successes = 0

    def _print_confidence_report(self, trial_number: int, confidence: Dict):
        """Print confidence metrics report."""
        print(f"\n  [CONFIDENCE REPORT - Trial #{trial_number}]")
        print(f"  Phase: {self.phase.upper()}")
        print(f"  Overall Confidence: {confidence['overall_confidence']:.1%}")
        print(f"    - Convergence: {confidence['convergence_score']:.1%}")
        print(f"    - Coverage: {confidence['exploration_coverage']:.1%}")
        print(f"    - Stability: {confidence['prediction_stability']:.1%}")
        print(f"  {confidence['message']}")
        print()


def create_adaptive_sampler(
    n_startup_trials: int = 10,
    multivariate: bool = True,
    confidence_threshold: float = 0.65
) -> optuna.samplers.TPESampler:
    """
    Create TPE sampler configured for adaptive exploration-exploitation.

    Args:
        n_startup_trials: Initial random exploration trials
        multivariate: Enable multivariate TPE for correlated parameters
        confidence_threshold: Confidence needed before intensive exploitation

    Returns:
        Configured TPESampler
    """
    # Higher n_ei_candidates = more exploitation
    # Will be used once confidence threshold is reached
    return optuna.samplers.TPESampler(
        n_startup_trials=n_startup_trials,
        n_ei_candidates=24,
        multivariate=multivariate,
        warn_independent_sampling=True
    )
