"""
Production-Ready NN Optimization with Confidence Bounds

This script runs multi-objective optimization using the CV-validated neural network
with proper extrapolation warnings and confidence-bounded results.

Key Features:
1. Uses CV-validated model with known accuracy (1.8% mass, 1.1% freq MAPE)
2. Warns when extrapolating outside training data range
3. Reads optimization bounds from study's optimization_config.json
4. Constrains optimization to prescribed bounds for reliable predictions
5. Marks designs needing FEA validation
"""
import sys
from pathlib import Path
import time
import json
import argparse
import torch
import numpy as np
import optuna
from optuna.samplers import NSGAIISampler
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# Add project paths
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))


def load_config_bounds(study_path: Path) -> dict:
    """Load design variable bounds from optimization_config.json

    Returns dict: {param_name: (min, max, is_int)}
    """
    config_path = study_path / "1_setup" / "optimization_config.json"

    if not config_path.exists():
        raise FileNotFoundError(f"Config not found: {config_path}")

    with open(config_path) as f:
        config = json.load(f)

    bounds = {}
    for var in config.get('design_variables', []):
        # Support both 'parameter' and 'name' keys
        name = var.get('parameter') or var.get('name')

        # Support both "bounds": [min, max] and "min_value"/"max_value" formats
        if 'bounds' in var:
            min_val, max_val = var['bounds']
        else:
            min_val = var.get('min_value', var.get('min', 0))
            max_val = var.get('max_value', var.get('max', 1))

        # Detect integer type based on name or explicit type
        is_int = (var.get('type') == 'integer' or
                  'count' in name.lower() or
                  (isinstance(min_val, int) and isinstance(max_val, int) and max_val - min_val < 20))

        bounds[name] = (min_val, max_val, is_int)

    return bounds

from optimization_engine.processors.surrogates.active_learning_surrogate import EnsembleMLP


class ValidatedSurrogate:
    """Surrogate with CV-validated accuracy and extrapolation detection."""

    def __init__(self, model_path: str):
        state = torch.load(model_path, map_location='cpu')

        self.model = EnsembleMLP(
            input_dim=len(state['design_var_names']),
            output_dim=4,  # mass, freq, disp, stress
            hidden_dims=state['hidden_dims']
        )
        self.model.load_state_dict(state['model'])
        self.model.eval()

        self.input_mean = np.array(state['input_mean'])
        self.input_std = np.array(state['input_std'])
        self.output_mean = np.array(state['output_mean'])
        self.output_std = np.array(state['output_std'])
        self.design_var_names = state['design_var_names']

        # CV metrics
        self.cv_mass_mape = state['cv_mass_mape']
        self.cv_freq_mape = state['cv_freq_mape']
        self.cv_mass_std = state['cv_mass_std']
        self.cv_freq_std = state['cv_freq_std']
        self.n_training_samples = state['n_samples']

        # Training bounds (for extrapolation detection)
        self.bounds_min = self.input_mean - 2 * self.input_std
        self.bounds_max = self.input_mean + 2 * self.input_std

    def predict(self, params: dict) -> dict:
        """Predict with extrapolation check."""
        x = np.array([[params.get(name, 0.0) for name in self.design_var_names]], dtype=np.float32)

        # Check for extrapolation
        extrapolation_score = 0.0
        for i, name in enumerate(self.design_var_names):
            val = x[0, i]
            if val < self.bounds_min[i]:
                extrapolation_score += (self.bounds_min[i] - val) / (self.input_std[i] + 1e-8)
            elif val > self.bounds_max[i]:
                extrapolation_score += (val - self.bounds_max[i]) / (self.input_std[i] + 1e-8)

        # Normalize input
        x_norm = (x - self.input_mean) / (self.input_std + 1e-8)
        x_t = torch.FloatTensor(x_norm)

        # Predict
        with torch.no_grad():
            pred_norm = self.model(x_t).numpy()

        pred = pred_norm * (self.output_std + 1e-8) + self.output_mean

        # Calculate confidence-adjusted uncertainty
        base_uncertainty = self.cv_mass_mape / 100  # Base uncertainty from CV
        extrapolation_penalty = min(extrapolation_score * 0.1, 0.5)  # Max 50% extra uncertainty
        total_uncertainty = base_uncertainty + extrapolation_penalty

        return {
            'mass': float(pred[0, 0]),
            'frequency': float(pred[0, 1]),
            'max_displacement': float(pred[0, 2]),
            'max_stress': float(pred[0, 3]),
            'uncertainty': total_uncertainty,
            'extrapolating': extrapolation_score > 0.1,
            'extrapolation_score': extrapolation_score,
            'needs_fea_validation': extrapolation_score > 0.5
        }


def run_optimization(surrogate: ValidatedSurrogate, bounds: dict, n_trials: int = 1000):
    """Run multi-objective optimization.

    Args:
        surrogate: ValidatedSurrogate model
        bounds: Dict from load_config_bounds {name: (min, max, is_int)}
        n_trials: Number of optimization trials
    """
    print(f"\nOptimization bounds (from config):")
    for name, (low, high, is_int) in bounds.items():
        type_str = "int" if is_int else "float"
        print(f"  {name}: [{low}, {high}] ({type_str})")

    # Create study
    study = optuna.create_study(
        directions=["minimize", "minimize"],  # mass, -frequency
        sampler=NSGAIISampler()
    )

    # Track stats
    start_time = time.time()
    trial_times = []
    extrapolation_count = 0

    def objective(trial: optuna.Trial):
        nonlocal extrapolation_count

        params = {}
        for name, (low, high, is_int) in bounds.items():
            if is_int:
                params[name] = trial.suggest_int(name, int(low), int(high))
            else:
                params[name] = trial.suggest_float(name, float(low), float(high))

        trial_start = time.time()
        result = surrogate.predict(params)
        trial_time = (time.time() - trial_start) * 1000
        trial_times.append(trial_time)

        # Track extrapolation
        if result['extrapolating']:
            extrapolation_count += 1

        # Store metadata
        trial.set_user_attr('uncertainty', result['uncertainty'])
        trial.set_user_attr('extrapolating', result['extrapolating'])
        trial.set_user_attr('needs_fea', result['needs_fea_validation'])
        trial.set_user_attr('params', params)

        if trial.number % 200 == 0:
            print(f"  Trial {trial.number}: mass={result['mass']:.1f}g, freq={result['frequency']:.2f}Hz, extrap={result['extrapolating']}")

        return result['mass'], -result['frequency']

    print(f"\nRunning {n_trials} trials...")
    study.optimize(objective, n_trials=n_trials, show_progress_bar=True)

    total_time = time.time() - start_time

    return study, {
        'total_time': total_time,
        'avg_trial_time_ms': np.mean(trial_times),
        'trials_per_second': n_trials / total_time,
        'extrapolation_count': extrapolation_count,
        'extrapolation_pct': extrapolation_count / n_trials * 100
    }


def analyze_pareto_front(study, surrogate):
    """Analyze Pareto front with confidence information."""
    pareto_trials = study.best_trials

    results = {
        'total_pareto_designs': len(pareto_trials),
        'confident_designs': 0,
        'needs_fea_designs': 0,
        'designs': []
    }

    for trial in pareto_trials:
        mass = trial.values[0]
        freq = -trial.values[1]
        uncertainty = trial.user_attrs.get('uncertainty', 0)
        needs_fea = trial.user_attrs.get('needs_fea', False)
        params = trial.user_attrs.get('params', trial.params)

        if needs_fea:
            results['needs_fea_designs'] += 1
        else:
            results['confident_designs'] += 1

        results['designs'].append({
            'mass': mass,
            'frequency': freq,
            'uncertainty': uncertainty,
            'needs_fea': needs_fea,
            'params': params
        })

    # Sort by mass
    results['designs'].sort(key=lambda x: x['mass'])

    return results


def plot_results(study, pareto_analysis, surrogate, save_path):
    """Generate visualization of optimization results."""
    fig, axes = plt.subplots(2, 2, figsize=(14, 12))

    # 1. Pareto Front with Confidence
    ax = axes[0, 0]
    pareto = pareto_analysis['designs']

    confident = [d for d in pareto if not d['needs_fea']]
    needs_fea = [d for d in pareto if d['needs_fea']]

    if confident:
        ax.scatter([d['mass'] for d in confident],
                   [d['frequency'] for d in confident],
                   c='green', s=50, alpha=0.7, label=f'Confident ({len(confident)})')

    if needs_fea:
        ax.scatter([d['mass'] for d in needs_fea],
                   [d['frequency'] for d in needs_fea],
                   c='orange', s=50, alpha=0.7, marker='^', label=f'Needs FEA ({len(needs_fea)})')

    ax.set_xlabel('Mass (g)')
    ax.set_ylabel('Frequency (Hz)')
    ax.set_title('Pareto Front with Confidence Assessment')
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 2. Uncertainty vs Performance
    ax = axes[0, 1]
    all_trials = study.trials
    masses = [t.values[0] for t in all_trials if t.values]
    freqs = [-t.values[1] for t in all_trials if t.values]
    uncertainties = [t.user_attrs.get('uncertainty', 0) for t in all_trials if t.values]

    sc = ax.scatter(masses, freqs, c=uncertainties, cmap='RdYlGn_r', s=10, alpha=0.5)
    plt.colorbar(sc, ax=ax, label='Uncertainty')
    ax.set_xlabel('Mass (g)')
    ax.set_ylabel('Frequency (Hz)')
    ax.set_title('All Trials Colored by Uncertainty')
    ax.grid(True, alpha=0.3)

    # 3. Top 10 Pareto Designs
    ax = axes[1, 0]
    top_10 = pareto_analysis['designs'][:10]

    y_pos = np.arange(len(top_10))
    bars = ax.barh(y_pos, [d['mass'] for d in top_10],
                   color=['green' if not d['needs_fea'] else 'orange' for d in top_10])

    ax.set_yticks(y_pos)
    ax.set_yticklabels([f"#{i+1}: {d['frequency']:.1f}Hz" for i, d in enumerate(top_10)])
    ax.set_xlabel('Mass (g)')
    ax.set_title('Top 10 Lowest Mass Designs')
    ax.grid(True, alpha=0.3, axis='x')

    # Add confidence text
    for i, (bar, d) in enumerate(zip(bars, top_10)):
        status = "FEA!" if d['needs_fea'] else "OK"
        ax.text(bar.get_width() + 10, bar.get_y() + bar.get_height()/2,
                status, va='center', fontsize=9)

    # 4. Summary Text
    ax = axes[1, 1]
    ax.axis('off')

    summary_text = f"""
OPTIMIZATION SUMMARY
====================

CV-Validated Model Accuracy:
  Mass MAPE: {surrogate.cv_mass_mape:.1f}% +/- {surrogate.cv_mass_std:.1f}%
  Freq MAPE: {surrogate.cv_freq_mape:.1f}% +/- {surrogate.cv_freq_std:.1f}%
  Training samples: {surrogate.n_training_samples}

Pareto Front:
  Total designs: {pareto_analysis['total_pareto_designs']}
  High confidence: {pareto_analysis['confident_designs']}
  Needs FEA validation: {pareto_analysis['needs_fea_designs']}

Best Confident Design:
"""
    # Find best confident design
    best_confident = [d for d in pareto_analysis['designs'] if not d['needs_fea']]
    if best_confident:
        best = best_confident[0]
        summary_text += f"""
  Mass: {best['mass']:.1f}g
  Frequency: {best['frequency']:.2f}Hz
  Parameters:
"""
        for k, v in best['params'].items():
            if isinstance(v, float):
                summary_text += f"    {k}: {v:.3f}\n"
            else:
                summary_text += f"    {k}: {v}\n"
    else:
        summary_text += "  No confident designs found - run more FEA!"

    ax.text(0.05, 0.95, summary_text, transform=ax.transAxes,
            fontfamily='monospace', fontsize=10, verticalalignment='top')

    plt.suptitle('NN-Based Multi-Objective Optimization Results', fontsize=14, fontweight='bold')
    plt.tight_layout()
    plt.savefig(save_path, dpi=150)
    plt.close()
    print(f"Saved plot: {save_path}")


def main():
    parser = argparse.ArgumentParser(description='NN Optimization with Confidence Bounds')
    parser.add_argument('--study', default='uav_arm_optimization',
                        help='Study name (e.g., uav_arm_optimization)')
    parser.add_argument('--model', default=None,
                        help='Path to CV-validated model (default: cv_validated_surrogate.pt)')
    parser.add_argument('--trials', type=int, default=2000,
                        help='Number of optimization trials')
    args = parser.parse_args()

    print("="*70)
    print("Production-Ready NN Optimization with Confidence Bounds")
    print("="*70)

    # Load bounds from study config
    study_path = project_root / "studies" / args.study
    if not study_path.exists():
        print(f"ERROR: Study not found: {study_path}")
        return

    print(f"\nLoading bounds from study: {args.study}")
    bounds = load_config_bounds(study_path)
    print(f"  Loaded {len(bounds)} design variables from config")

    # Load CV-validated model
    model_path = Path(args.model) if args.model else project_root / "cv_validated_surrogate.pt"
    if not model_path.exists():
        print(f"ERROR: Run validate_surrogate_real_data.py first to create {model_path}")
        return

    print(f"\nLoading CV-validated model: {model_path}")
    surrogate = ValidatedSurrogate(str(model_path))

    print(f"\nModel Statistics:")
    print(f"  Training samples: {surrogate.n_training_samples}")
    print(f"  CV Mass MAPE: {surrogate.cv_mass_mape:.1f}% +/- {surrogate.cv_mass_std:.1f}%")
    print(f"  CV Freq MAPE: {surrogate.cv_freq_mape:.1f}% +/- {surrogate.cv_freq_std:.1f}%")
    print(f"  Design variables: {surrogate.design_var_names}")

    # Verify model variables match config variables
    config_vars = set(bounds.keys())
    model_vars = set(surrogate.design_var_names)
    if config_vars != model_vars:
        print(f"\nWARNING: Variable mismatch!")
        print(f"  Config has: {config_vars}")
        print(f"  Model has: {model_vars}")
        print(f"  Missing from model: {config_vars - model_vars}")
        print(f"  Extra in model: {model_vars - config_vars}")

    # Run optimization
    print("\n" + "="*70)
    print("Running Multi-Objective Optimization")
    print("="*70)

    study, stats = run_optimization(surrogate, bounds, n_trials=args.trials)

    print(f"\nOptimization Statistics:")
    print(f"  Total time: {stats['total_time']:.1f}s")
    print(f"  Avg trial time: {stats['avg_trial_time_ms']:.2f}ms")
    print(f"  Trials per second: {stats['trials_per_second']:.1f}")
    print(f"  Extrapolating trials: {stats['extrapolation_count']} ({stats['extrapolation_pct']:.1f}%)")

    # Analyze Pareto front
    print("\n" + "="*70)
    print("Pareto Front Analysis")
    print("="*70)

    pareto_analysis = analyze_pareto_front(study, surrogate)

    print(f"\nPareto Front Summary:")
    print(f"  Total Pareto-optimal designs: {pareto_analysis['total_pareto_designs']}")
    print(f"  High confidence designs: {pareto_analysis['confident_designs']}")
    print(f"  Needs FEA validation: {pareto_analysis['needs_fea_designs']}")

    print(f"\nTop 5 Lowest Mass Designs:")
    for i, d in enumerate(pareto_analysis['designs'][:5]):
        status = "[NEEDS FEA]" if d['needs_fea'] else "[OK]"
        print(f"  {i+1}. Mass={d['mass']:.1f}g, Freq={d['frequency']:.2f}Hz {status}")
        print(f"      Params: {d['params']}")

    # Generate plots
    plot_path = project_root / "validated_nn_optimization_results.png"
    plot_results(study, pareto_analysis, surrogate, str(plot_path))

    # Save results
    results_path = project_root / "validated_nn_optimization_results.json"
    with open(results_path, 'w') as f:
        json.dump({
            'stats': stats,
            'pareto_summary': {
                'total': pareto_analysis['total_pareto_designs'],
                'confident': pareto_analysis['confident_designs'],
                'needs_fea': pareto_analysis['needs_fea_designs']
            },
            'top_designs': pareto_analysis['designs'][:20],
            'cv_metrics': {
                'mass_mape': surrogate.cv_mass_mape,
                'freq_mape': surrogate.cv_freq_mape
            }
        }, f, indent=2)
    print(f"\nSaved results: {results_path}")

    print("\n" + "="*70)
    print("NEXT STEPS")
    print("="*70)

    if pareto_analysis['confident_designs'] > 0:
        print("1. Review the confident Pareto-optimal designs above")
        print("2. Consider validating top 3-5 designs with actual FEA")
        print("3. If FEA matches predictions, use for manufacturing")
    else:
        print("1. All Pareto designs are extrapolating - need more FEA data!")
        print("2. Run FEA on designs marked [NEEDS FEA]")
        print("3. Retrain the model with new data")


if __name__ == '__main__':
    main()
