"""
Simple Beam Optimization Study
===============================

Multi-objective optimization:
- Minimize displacement (constraint: < 10mm)
- Minimize stress
- Minimize mass

Design Variables:
- beam_half_core_thickness: 10-40 mm
- beam_face_thickness: 10-40 mm
- holes_diameter: 150-450 mm
- hole_count: 5-15
"""

import sys
import json
import optuna
from pathlib import Path
from datetime import datetime
from typing import Dict

# Add parent directories to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from optimization_engine.nx_updater import NXParameterUpdater
from optimization_engine.nx_solver import NXSolver
from optimization_engine.result_extractors.generated.extract_displacement import extract_displacement
from optimization_engine.result_extractors.generated.extract_solid_stress import extract_solid_stress
from optimization_engine.result_extractors.generated.extract_expression import extract_expression


def print_section(title: str):
    """Print a section header."""
    print()
    print("=" * 80)
    print(f"  {title}")
    print("=" * 80)
    print()


def load_config(config_file: Path) -> dict:
    """Load JSON configuration."""
    with open(config_file, 'r') as f:
        return json.load(f)


def main():
    print_section("SIMPLE BEAM OPTIMIZATION STUDY")

    # File paths
    study_dir = Path(__file__).parent
    config_file = study_dir / "beam_optimization_config.json"
    prt_file = study_dir / "model" / "Beam.prt"
    sim_file = study_dir / "model" / "Beam_sim1.sim"

    if not config_file.exists():
        print(f"ERROR: Config file not found: {config_file}")
        sys.exit(1)

    if not prt_file.exists():
        print(f"ERROR: Part file not found: {prt_file}")
        sys.exit(1)

    if not sim_file.exists():
        print(f"ERROR: Simulation file not found: {sim_file}")
        sys.exit(1)

    # Load configuration
    config = load_config(config_file)

    print("Study Configuration:")
    print(f"  - Study: {config['study_name']}")
    print(f"  - Substudy: {config['substudy_name']}")
    print(f"  - Description: {config['description']}")
    print()
    print("Objectives:")
    for obj in config['objectives']:
        print(f"  - {obj['name']}: weight={obj['weight']}")
    print()
    print("Constraints:")
    for con in config['constraints']:
        print(f"  - {con['name']}: {con['type']} {con['value']} {con['units']}")
    print()
    print("Design Variables:")
    for var_name, var_info in config['design_variables'].items():
        print(f"  - {var_name}: {var_info['min']}-{var_info['max']} {var_info['units']}")
    print()
    print(f"Optimization Settings:")
    print(f"  - Algorithm: {config['optimization_settings']['algorithm']}")
    print(f"  - Trials: {config['optimization_settings']['n_trials']}")
    print(f"  - Sampler: {config['optimization_settings']['sampler']}")
    print()

    # Setup output directory
    output_dir = study_dir / "substudies" / config['substudy_name']
    output_dir.mkdir(parents=True, exist_ok=True)

    print(f"Part file: {prt_file}")
    print(f"Simulation file: {sim_file}")
    print(f"Output directory: {output_dir}")
    print()

    # =========================================================================
    # DEFINE OBJECTIVE FUNCTION
    # =========================================================================

    def objective(trial: optuna.Trial) -> float:
        """
        Optuna objective function.

        Evaluates one design point:
        1. Updates geometry parameters
        2. Runs FEM simulation
        3. Extracts results
        4. Computes weighted multi-objective with penalties
        """
        trial_num = trial.number

        print(f"\n[Trial {trial_num}] Starting...")

        # Sample design variables
        design_vars = {}
        for var_name, var_info in config['design_variables'].items():
            if var_info['type'] == 'continuous':
                design_vars[var_name] = trial.suggest_float(
                    var_name,
                    var_info['min'],
                    var_info['max']
                )
            elif var_info['type'] == 'integer':
                design_vars[var_name] = trial.suggest_int(
                    var_name,
                    int(var_info['min']),
                    int(var_info['max'])
                )

        print(f"[Trial {trial_num}] Design variables:")
        for var_name, var_value in design_vars.items():
            print(f"  - {var_name}: {var_value:.3f}")

        # Create trial directory
        trial_dir = output_dir / f"trial_{trial_num:03d}"
        trial_dir.mkdir(exist_ok=True)

        # Copy all 4 files to trial directory (.prt, _i.prt, .fem, .sim)
        import shutil
        trial_prt = trial_dir / prt_file.name
        trial_sim = trial_dir / sim_file.name

        shutil.copy2(prt_file, trial_prt)
        shutil.copy2(sim_file, trial_sim)

        # Copy FEM file
        fem_file = prt_file.parent / f"{prt_file.stem}_fem1.fem"
        if fem_file.exists():
            trial_fem = trial_dir / fem_file.name
            shutil.copy2(fem_file, trial_fem)

        # Copy idealized geometry (_i.prt) - contains midsurface thickness data
        # Pattern: Beam_fem1_i.prt (derived from FEM file name)
        if fem_file.exists():
            prt_i_file = prt_file.parent / f"{fem_file.stem}_i.prt"
            if prt_i_file.exists():
                trial_prt_i = trial_dir / prt_i_file.name
                shutil.copy2(prt_i_file, trial_prt_i)

        try:
            # Update geometry
            print(f"[Trial {trial_num}] Updating geometry...")
            updater = NXParameterUpdater(trial_prt)
            updater.update_expressions(design_vars)

            # Run simulation
            print(f"[Trial {trial_num}] Running FEM simulation...")
            solver = NXSolver()
            result = solver.run_simulation(trial_sim)

            if not result['success']:
                raise RuntimeError(f"Simulation failed: {result}")

            op2_file = result['op2_file']

            print(f"[Trial {trial_num}] Extracting results...")

            # Extract displacement
            disp_result = extract_displacement(op2_file)
            max_disp = disp_result['max_displacement']

            # Extract stress
            stress_result = extract_solid_stress(op2_file)
            max_stress = stress_result['max_von_mises']

            # Extract mass
            mass_result = extract_expression(trial_prt, 'p173')
            mass = mass_result['p173']

            print(f"[Trial {trial_num}] Results:")
            print(f"  - Displacement: {max_disp:.3f} mm")
            print(f"  - Stress: {max_stress:.3f} MPa")
            print(f"  - Mass: {mass:.3f} kg")

            # Compute weighted multi-objective
            objective_value = 0.0

            for obj in config['objectives']:
                if obj['extractor'] == 'max_displacement':
                    value = max_disp
                elif obj['extractor'] == 'max_stress':
                    value = max_stress
                elif obj['extractor'] == 'mass':
                    value = mass
                else:
                    continue

                weight = obj['weight']
                objective_value += weight * value

            # Apply constraint penalties
            penalty = 0.0
            for constraint in config['constraints']:
                if constraint['extractor'] == 'max_displacement':
                    current_value = max_disp
                elif constraint['extractor'] == 'max_stress':
                    current_value = max_stress
                else:
                    continue

                if constraint['type'] == 'less_than':
                    if current_value > constraint['value']:
                        violation = (current_value - constraint['value']) / constraint['value']
                        penalty += 1000.0 * violation
                        print(f"[Trial {trial_num}] CONSTRAINT VIOLATED: {constraint['name']}")
                        print(f"  Current: {current_value:.3f}, Limit: {constraint['value']}")

            total_objective = objective_value + penalty

            print(f"[Trial {trial_num}] Objective: {objective_value:.3f}, Penalty: {penalty:.3f}, Total: {total_objective:.3f}")

            # Save trial results
            trial_results = {
                'trial_number': trial_num,
                'design_variables': design_vars,
                'results': {
                    'max_displacement': max_disp,
                    'max_stress': max_stress,
                    'mass': mass
                },
                'objective': objective_value,
                'penalty': penalty,
                'total_objective': total_objective,
                'timestamp': datetime.now().isoformat()
            }

            with open(trial_dir / "results.json", 'w') as f:
                json.dump(trial_results, f, indent=2)

            return total_objective

        except Exception as e:
            print(f"[Trial {trial_num}] FAILED: {e}")
            import traceback
            traceback.print_exc()
            return 1e10  # Return large penalty for failed trials

    # =========================================================================
    # RUN OPTIMIZATION
    # =========================================================================

    print_section("RUNNING OPTIMIZATION")

    # Create Optuna study
    study = optuna.create_study(
        direction='minimize',
        sampler=optuna.samplers.TPESampler() if config['optimization_settings']['sampler'] == 'TPE' else None
    )

    # Run optimization
    print(f"Starting {config['optimization_settings']['n_trials']} optimization trials...")
    print()

    study.optimize(
        objective,
        n_trials=config['optimization_settings']['n_trials'],
        show_progress_bar=True
    )

    # =========================================================================
    # SAVE RESULTS
    # =========================================================================

    print_section("SAVING RESULTS")

    # Save full study
    study_file = output_dir / "optuna_study.pkl"
    import pickle
    with open(study_file, 'wb') as f:
        pickle.dump(study, f)

    print(f"Study saved to: {study_file}")

    # Save best trial
    best_trial = study.best_trial
    best_results = {
        'best_trial_number': best_trial.number,
        'best_params': best_trial.params,
        'best_value': best_trial.value,
        'timestamp': datetime.now().isoformat()
    }

    best_file = output_dir / "best_trial.json"
    with open(best_file, 'w') as f:
        json.dump(best_results, f, indent=2)

    print(f"Best trial saved to: {best_file}")
    print()

    # =========================================================================
    # PRINT SUMMARY
    # =========================================================================

    print_section("OPTIMIZATION COMPLETE")

    print(f"Total trials: {len(study.trials)}")
    print(f"Best trial: {best_trial.number}")
    print(f"Best objective value: {best_trial.value:.6f}")
    print()
    print("Best design variables:")
    for var_name, var_value in best_trial.params.items():
        print(f"  - {var_name}: {var_value:.3f}")
    print()

    # Load best trial results to show performance
    best_trial_dir = output_dir / f"trial_{best_trial.number:03d}"
    best_results_file = best_trial_dir / "results.json"

    if best_results_file.exists():
        with open(best_results_file, 'r') as f:
            best_results = json.load(f)

        print("Best performance:")
        print(f"  - Displacement: {best_results['results']['max_displacement']:.3f} mm")
        print(f"  - Stress: {best_results['results']['max_stress']:.3f} MPa")
        print(f"  - Mass: {best_results['results']['mass']:.3f} kg")
        print()


if __name__ == "__main__":
    main()
