"""
Generic Optimization Runner - Phase 3.2 Integration
===================================================

Flexible optimization runner supporting both manual and LLM modes:

**LLM Mode** (Natural Language):
    python run_optimization.py --llm "maximize displacement, ensure safety factor > 4" \\
        --prt model/part.prt --sim model/sim.sim

**Manual Mode** (JSON Config):
    python run_optimization.py --config config.json \\
        --prt model/part.prt --sim model/sim.sim

Features:
- Phase 2.7: LLM workflow analysis from natural language
- Phase 3.1: Auto-generated extractors
- Phase 2.9: Auto-generated hooks
- Phase 1: Plugin system with lifecycle hooks
- Graceful fallback if LLM generation fails

Author: Antoine Letarte
Version: 1.0.0 (Phase 3.2)
Last Updated: 2025-11-17
"""

import argparse
import json
import logging
import sys
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from optimization_engine.future.llm_workflow_analyzer import LLMWorkflowAnalyzer
from optimization_engine.future.llm_optimization_runner import LLMOptimizationRunner
from optimization_engine.core.runner import OptimizationRunner
from optimization_engine.nx.updater import NXParameterUpdater
from optimization_engine.nx.solver import NXSolver

# Setup logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def print_banner(text: str):
    """Print a formatted banner."""
    print()
    print("=" * 80)
    print(f"  {text}")
    print("=" * 80)
    print()


def parse_arguments():
    """Parse command-line arguments."""
    parser = argparse.ArgumentParser(
        description="Atomizer Optimization Runner - Phase 3.2 Integration",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:

  LLM Mode (Natural Language):
    python run_optimization.py \\
        --llm "maximize displacement, ensure safety factor > 4" \\
        --prt model/Bracket.prt \\
        --sim model/Bracket_sim1.sim \\
        --trials 20

  Manual Mode (JSON Config):
    python run_optimization.py \\
        --config config.json \\
        --prt model/Bracket.prt \\
        --sim model/Bracket_sim1.sim \\
        --trials 50

  With custom output directory:
    python run_optimization.py \\
        --llm "minimize stress" \\
        --prt model/part.prt \\
        --sim model/sim.sim \\
        --output results/my_study
        """
    )

    # Mode selection (mutually exclusive)
    mode_group = parser.add_mutually_exclusive_group(required=True)
    mode_group.add_argument(
        '--llm',
        type=str,
        help='Natural language optimization request (LLM mode)'
    )
    mode_group.add_argument(
        '--config',
        type=Path,
        help='Path to JSON configuration file (manual mode)'
    )

    # Required arguments
    parser.add_argument(
        '--prt',
        type=Path,
        required=True,
        help='Path to NX part file (.prt)'
    )
    parser.add_argument(
        '--sim',
        type=Path,
        required=True,
        help='Path to NX simulation file (.sim)'
    )

    # Optional arguments
    parser.add_argument(
        '--trials',
        type=int,
        default=20,
        help='Number of optimization trials (default: 20)'
    )
    parser.add_argument(
        '--output',
        type=Path,
        help='Output directory for results (default: ./optimization_results)'
    )
    parser.add_argument(
        '--study-name',
        type=str,
        help='Study name (default: auto-generated from timestamp)'
    )
    parser.add_argument(
        '--nastran-version',
        type=str,
        default='2412',
        help='Nastran version (default: 2412)'
    )
    parser.add_argument(
        '--api-key',
        type=str,
        help='Anthropic API key for LLM mode (uses Claude Code by default)'
    )

    return parser.parse_args()


def run_llm_mode(args) -> Dict[str, Any]:
    """
    Run optimization in LLM mode (natural language request).

    This uses the LLM workflow analyzer to parse the natural language request,
    then runs optimization with auto-generated extractors and hooks.

    Args:
        args: Parsed command-line arguments

    Returns:
        Optimization results dictionary
    """
    print_banner("LLM MODE - Natural Language Optimization")

    print(f"User Request: \"{args.llm}\"")
    print()

    # Step 1: Analyze natural language request using LLM
    print("Step 1: Analyzing request with LLM...")
    analyzer = LLMWorkflowAnalyzer(
        api_key=args.api_key,
        use_claude_code=(args.api_key is None)
    )

    try:
        llm_workflow = analyzer.analyze_request(args.llm)
        logger.info("LLM analysis complete!")
        logger.info(f"  Engineering features: {len(llm_workflow.get('engineering_features', []))}")
        logger.info(f"  Inline calculations: {len(llm_workflow.get('inline_calculations', []))}")
        logger.info(f"  Post-processing hooks: {len(llm_workflow.get('post_processing_hooks', []))}")
        print()

        # Validate LLM workflow structure
        required_fields = ['engineering_features', 'optimization']
        missing_fields = [f for f in required_fields if f not in llm_workflow]
        if missing_fields:
            raise ValueError(f"LLM workflow missing required fields: {missing_fields}")

        if 'design_variables' not in llm_workflow.get('optimization', {}):
            raise ValueError("LLM workflow optimization section missing 'design_variables'")

        logger.info("LLM workflow validation passed")

    except Exception as e:
        logger.error(f"LLM analysis failed: {e}")
        logger.error("Falling back to manual mode - please provide a config.json file")
        sys.exit(1)

    # Step 2: Create model updater and simulation runner
    print("Step 2: Setting up model updater and simulation runner...")

    updater = NXParameterUpdater(prt_file_path=args.prt)
    def model_updater(design_vars: dict):
        # Note: update_expressions() uses NX journal to update the file directly (use_nx_import=True by default)
        # so we don't need to call save() which would overwrite with stale binary content
        updater.update_expressions(design_vars)

    solver = NXSolver(nastran_version=args.nastran_version, use_journal=True)
    def simulation_runner() -> Path:
        # NOTE: We do NOT pass expression_updates because:
        # 1. The PRT file was already updated by model_updater (via NX import journal)
        # 2. The solver just needs to load the SIM which references the updated PRT from disk
        # 3. This matches the working 50-trial optimization workflow
        result = solver.run_simulation(args.sim)
        return result['op2_file']

    logger.info("  Model updater ready")
    logger.info("  Simulation runner ready")
    print()

    # Step 3: Initialize LLM optimization runner
    print("Step 3: Initializing LLM optimization runner...")

    # Determine output directory
    if args.output:
        output_dir = args.output
    else:
        output_dir = Path.cwd() / "optimization_results"

    # Determine study name
    if args.study_name:
        study_name = args.study_name
    else:
        study_name = f"llm_optimization_{datetime.now().strftime('%Y%m%d_%H%M%S')}"

    try:
        runner = LLMOptimizationRunner(
            llm_workflow=llm_workflow,
            model_updater=model_updater,
            simulation_runner=simulation_runner,
            study_name=study_name,
            output_dir=output_dir / study_name
        )

        logger.info(f"  Study name: {study_name}")
        logger.info(f"  Output directory: {runner.output_dir}")
        logger.info(f"  Extractors: {len(runner.extractors)}")
        logger.info(f"  Hooks: {runner.hook_manager.get_summary()['enabled_hooks']}")
        print()

    except Exception as e:
        logger.error(f"Failed to initialize LLM optimization runner: {e}")
        logger.error("This may be due to extractor generation or hook initialization failure")
        import traceback
        traceback.print_exc()
        sys.exit(1)

    # Step 4: Run optimization
    print_banner(f"RUNNING OPTIMIZATION - {args.trials} TRIALS")
    print(f"This will take several minutes...")
    print()

    start_time = datetime.now()
    results = runner.run_optimization(n_trials=args.trials)
    end_time = datetime.now()

    duration = (end_time - start_time).total_seconds()

    print()
    print_banner("OPTIMIZATION COMPLETE!")
    print(f"Duration: {duration:.1f} seconds ({duration/60:.1f} minutes)")
    print(f"Trials completed: {len(results['history'])}")
    print()
    print("Best Design Found:")
    for param, value in results['best_params'].items():
        print(f"  - {param}: {value:.4f}")
    print(f"  - Objective value: {results['best_value']:.6f}")
    print()
    print(f"Results saved to: {runner.output_dir}")
    print()

    return results


def run_manual_mode(args) -> Dict[str, Any]:
    """
    Run optimization in manual mode (JSON config file).

    NOTE: Manual mode integration is in progress (Task 1.2).
    For now, please use study-specific run_optimization.py scripts.

    Args:
        args: Parsed command-line arguments

    Returns:
        Optimization results dictionary
    """
    print_banner("MANUAL MODE - JSON Configuration")

    print(f"Configuration file: {args.config}")
    print()

    logger.warning("="*80)
    logger.warning("MANUAL MODE - Phase 3.2 Task 1.2 (In Progress)")
    logger.warning("="*80)
    logger.warning("")
    logger.warning("The unified runner's manual mode is currently under development.")
    logger.warning("")
    logger.warning("For manual JSON-based optimization, please use:")
    logger.warning("  - Study-specific run_optimization.py scripts")
    logger.warning("  - Example: studies/simple_beam_optimization/run_optimization.py")
    logger.warning("")
    logger.warning("Alternatively, use --llm mode for natural language optimization:")
    logger.warning("  python run_optimization.py --llm \"your request\" --prt ... --sim ...")
    logger.warning("")
    logger.warning("="*80)
    print()

    sys.exit(1)


def main():
    """Main entry point."""
    print_banner("ATOMIZER OPTIMIZATION RUNNER - Phase 3.2")

    # Parse arguments
    args = parse_arguments()

    # Validate file paths
    if not args.prt.exists():
        logger.error(f"Part file not found: {args.prt}")
        sys.exit(1)

    if not args.sim.exists():
        logger.error(f"Simulation file not found: {args.sim}")
        sys.exit(1)

    logger.info(f"Part file: {args.prt}")
    logger.info(f"Simulation file: {args.sim}")
    logger.info(f"Trials: {args.trials}")
    print()

    # Run appropriate mode
    try:
        if args.llm:
            results = run_llm_mode(args)
        else:
            results = run_manual_mode(args)

        print_banner("SUCCESS!")
        logger.info("Optimization completed successfully")

    except KeyboardInterrupt:
        print()
        logger.warning("Optimization interrupted by user")
        sys.exit(1)
    except Exception as e:
        print()
        logger.error(f"Optimization failed: {e}", exc_info=True)
        sys.exit(1)


if __name__ == '__main__':
    main()
