"""
Study Creation Wizard - Comprehensive study setup for Atomizer
===============================================================

A powerful, LLM-friendly wizard that automates the complete study creation workflow:
1. Model Introspection - Discover expressions, solutions, and available results
2. Configuration Generation - Build optimization_config.json from user requirements
3. Script Generation - Generate run_optimization.py with proper extractors
4. Documentation Generation - Create README.md, STUDY_REPORT.md, MODEL_INTROSPECTION.md

This module is designed to work seamlessly with Claude Code skills.

Usage:
    from optimization_engine.study.wizard import StudyWizard

    wizard = StudyWizard(
        study_name="my_optimization",
        description="Optimize bracket for stiffness and mass"
    )

    # Step 1: Set model files
    wizard.set_model_files(
        prt_file="path/to/model.prt",
        sim_file="path/to/model_sim1.sim"
    )

    # Step 2: Introspect model
    introspection = wizard.introspect()

    # Step 3: Add design variables, objectives, constraints
    wizard.add_design_variable("thickness", bounds=[5, 20], units="mm")
    wizard.add_objective("mass", goal="minimize", extractor="extract_mass_from_bdf")
    wizard.add_constraint("max_stress", type="less_than", threshold=250, units="MPa")

    # Step 4: Generate study
    wizard.generate()

Author: Atomizer Development Team
Version: 1.0.0
Last Updated: 2025-12-06
"""

from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple, Union
from dataclasses import dataclass, field
from datetime import datetime
import json
import logging
import shutil

logger = logging.getLogger(__name__)


# =============================================================================
# Data Classes
# =============================================================================

@dataclass
class DesignVariable:
    """Design variable specification."""
    parameter: str
    bounds: Tuple[float, float]
    description: str = ""
    units: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "parameter": self.parameter,
            "bounds": list(self.bounds),
            "description": self.description or f"{self.parameter} design variable",
            "units": self.units
        }


@dataclass
class Objective:
    """Optimization objective specification."""
    name: str
    goal: str  # "minimize" or "maximize"
    extractor: str
    params: Dict[str, Any] = field(default_factory=dict)
    weight: float = 1.0
    description: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "goal": self.goal,
            "weight": self.weight,
            "description": self.description or f"{self.goal.capitalize()} {self.name}",
            "extraction": {
                "action": self.extractor,
                "domain": "result_extraction",
                "params": self.params
            }
        }


@dataclass
class Constraint:
    """Optimization constraint specification."""
    name: str
    type: str  # "less_than" or "greater_than"
    threshold: float
    extractor: str
    params: Dict[str, Any] = field(default_factory=dict)
    description: str = ""
    units: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "type": self.type,
            "threshold": self.threshold,
            "description": self.description or f"{self.name} constraint",
            "extraction": {
                "action": self.extractor,
                "domain": "result_extraction",
                "params": self.params
            }
        }


@dataclass
class IntrospectionResult:
    """Results from model introspection."""
    success: bool
    expressions: List[Dict[str, Any]] = field(default_factory=list)
    solutions: List[Dict[str, Any]] = field(default_factory=list)
    bodies: List[Dict[str, Any]] = field(default_factory=list)
    mass_properties: Dict[str, Any] = field(default_factory=dict)
    mesh_info: Dict[str, Any] = field(default_factory=dict)
    available_results: Dict[str, bool] = field(default_factory=dict)
    boundary_conditions: List[Dict[str, Any]] = field(default_factory=list)
    loads: List[Dict[str, Any]] = field(default_factory=list)
    materials: List[Dict[str, Any]] = field(default_factory=list)
    subcases: List[int] = field(default_factory=list)
    error: str = ""

    def get_expression_names(self) -> List[str]:
        """Get list of expression names."""
        return [e.get('name', '') for e in self.expressions]

    def get_solution_names(self) -> List[str]:
        """Get list of solution names."""
        return [s.get('name', '') for s in self.solutions]

    def suggest_design_variables(self) -> List[Dict[str, Any]]:
        """Suggest potential design variables from expressions."""
        suggestions = []
        for expr in self.expressions:
            name = expr.get('name', '')
            value = expr.get('value')

            # Skip system/reference expressions
            if name.startswith('p') and name[1:].isdigit():
                continue
            if 'mass' in name.lower() and 'input' not in name.lower():
                continue

            if value is not None and isinstance(value, (int, float)):
                # Suggest bounds based on value
                if value > 0:
                    bounds = (value * 0.5, value * 1.5)
                else:
                    bounds = (value * 1.5, value * 0.5)

                suggestions.append({
                    'name': name,
                    'current_value': value,
                    'suggested_bounds': bounds,
                    'units': expr.get('unit', ''),
                    'confidence': 'high' if any(kw in name.lower() for kw in ['thickness', 'angle', 'radius', 'length', 'width', 'height']) else 'medium'
                })

        return suggestions


# =============================================================================
# Extractor Catalog
# =============================================================================

EXTRACTOR_CATALOG = {
    # Mass extractors
    "extract_mass_from_bdf": {
        "module": "optimization_engine.extractors.bdf_mass_extractor",
        "function": "extract_mass_from_bdf",
        "input": ".dat/.bdf",
        "output_unit": "kg",
        "description": "Extract total mass from BDF/DAT file"
    },
    "extract_part_mass": {
        "module": "optimization_engine.extractors.extract_part_mass_material",
        "function": "extract_part_mass",
        "input": ".prt",
        "output_unit": "kg",
        "description": "Extract mass from NX part file via journal"
    },

    # Displacement extractors
    "extract_displacement": {
        "module": "optimization_engine.extractors.extract_displacement",
        "function": "extract_displacement",
        "input": ".op2",
        "output_unit": "mm",
        "description": "Extract displacement results from OP2"
    },

    # Stress extractors
    "extract_solid_stress": {
        "module": "optimization_engine.extractors.extract_von_mises_stress",
        "function": "extract_solid_stress",
        "input": ".op2",
        "output_unit": "MPa",
        "description": "Extract von Mises stress from OP2"
    },
    "extract_principal_stress": {
        "module": "optimization_engine.extractors.extract_principal_stress",
        "function": "extract_principal_stress",
        "input": ".op2",
        "output_unit": "MPa",
        "description": "Extract principal stresses (σ1, σ2, σ3)"
    },

    # Strain energy extractors
    "extract_strain_energy": {
        "module": "optimization_engine.extractors.extract_strain_energy",
        "function": "extract_strain_energy",
        "input": ".op2",
        "output_unit": "J",
        "description": "Extract strain energy from OP2"
    },
    "extract_total_strain_energy": {
        "module": "optimization_engine.extractors.extract_strain_energy",
        "function": "extract_total_strain_energy",
        "input": ".op2",
        "output_unit": "J",
        "description": "Extract total strain energy (convenience)"
    },

    # Reaction forces
    "extract_spc_forces": {
        "module": "optimization_engine.extractors.extract_spc_forces",
        "function": "extract_spc_forces",
        "input": ".op2",
        "output_unit": "N",
        "description": "Extract SPC/reaction forces from OP2"
    },

    # Frequency extractors
    "extract_frequency": {
        "module": "optimization_engine.extractors.extract_frequency",
        "function": "extract_frequency",
        "input": ".op2",
        "output_unit": "Hz",
        "description": "Extract natural frequencies from modal analysis"
    },
    "get_first_frequency": {
        "module": "optimization_engine.extractors.extract_modal_mass",
        "function": "get_first_frequency",
        "input": ".f06",
        "output_unit": "Hz",
        "description": "Get first natural frequency from F06"
    },

    # Temperature extractors (Phase 3)
    "extract_temperature": {
        "module": "optimization_engine.extractors.extract_temperature",
        "function": "extract_temperature",
        "input": ".op2",
        "output_unit": "K/°C",
        "description": "Extract temperatures from thermal analysis"
    },
    "get_max_temperature": {
        "module": "optimization_engine.extractors.extract_temperature",
        "function": "get_max_temperature",
        "input": ".op2",
        "output_unit": "K/°C",
        "description": "Get maximum temperature (convenience)"
    },

    # Modal mass (Phase 3)
    "extract_modal_mass": {
        "module": "optimization_engine.extractors.extract_modal_mass",
        "function": "extract_modal_mass",
        "input": ".f06",
        "output_unit": "kg",
        "description": "Extract modal effective mass from F06"
    },

    # Zernike (optical)
    "extract_zernike_from_op2": {
        "module": "optimization_engine.extractors.extract_zernike",
        "function": "extract_zernike_from_op2",
        "input": ".op2 + .bdf",
        "output_unit": "nm",
        "description": "Extract Zernike coefficients for optical surfaces"
    }
}


# =============================================================================
# Protocol Catalog
# =============================================================================

PROTOCOL_CATALOG = {
    "protocol_10_single": {
        "name": "Single-Objective IMSO",
        "sampler": "TPESampler",
        "description": "Adaptive single-objective optimization",
        "use_when": ["single objective", "maximize or minimize one thing"],
        "directions": 1
    },
    "protocol_11_multi": {
        "name": "Multi-Objective NSGA-II",
        "sampler": "NSGAIISampler",
        "description": "Pareto-optimal multi-objective optimization",
        "use_when": ["multiple objectives", "pareto front", "trade-offs"],
        "directions": "multiple"
    }
}


# =============================================================================
# Study Wizard
# =============================================================================

class StudyWizard:
    """
    Comprehensive study creation wizard for Atomizer.

    This wizard guides the complete study setup process:
    1. Model introspection (discover expressions, solutions, results)
    2. Configuration generation (optimization_config.json)
    3. Script generation (run_optimization.py, reset_study.py)
    4. Documentation generation (README.md, STUDY_REPORT.md, MODEL_INTROSPECTION.md)
    """

    def __init__(
        self,
        study_name: str,
        description: str = "",
        studies_dir: Optional[Path] = None
    ):
        """
        Initialize study wizard.

        Args:
            study_name: Name of the study (used for directory name)
            description: Human-readable description
            studies_dir: Base directory for studies (default: project/studies/)
        """
        self.study_name = study_name
        self.description = description

        # Set studies directory
        if studies_dir is None:
            # Find project root by looking for CLAUDE.md
            current = Path(__file__).parent
            while current != current.parent:
                if (current / "CLAUDE.md").exists():
                    studies_dir = current / "studies"
                    break
                current = current.parent
            else:
                studies_dir = Path.cwd() / "studies"

        self.studies_dir = Path(studies_dir)
        self.study_dir = self.studies_dir / study_name

        # Model files
        self.prt_file: Optional[Path] = None
        self.sim_file: Optional[Path] = None
        self.fem_file: Optional[Path] = None
        self.op2_file: Optional[Path] = None

        # Configuration
        self.design_variables: List[DesignVariable] = []
        self.objectives: List[Objective] = []
        self.constraints: List[Constraint] = []

        # Introspection results
        self.introspection: Optional[IntrospectionResult] = None

        # Settings
        self.protocol = "protocol_11_multi"  # Default to multi-objective
        self.n_trials = 100
        self.timeout_per_trial = 400
        self.neural_enabled = False

        logger.info(f"StudyWizard initialized for '{study_name}'")
        logger.info(f"  Study directory: {self.study_dir}")

    # =========================================================================
    # Model File Management
    # =========================================================================

    def set_model_files(
        self,
        prt_file: Union[str, Path],
        sim_file: Optional[Union[str, Path]] = None,
        fem_file: Optional[Union[str, Path]] = None,
        op2_file: Optional[Union[str, Path]] = None
    ) -> "StudyWizard":
        """
        Set model files for the study.

        Args:
            prt_file: Path to NX part file (.prt)
            sim_file: Path to simulation file (.sim) - auto-detected if None
            fem_file: Path to FEM file (.fem) - auto-detected if None
            op2_file: Path to OP2 results file (.op2) - optional

        Returns:
            Self for method chaining
        """
        self.prt_file = Path(prt_file)

        if not self.prt_file.exists():
            raise FileNotFoundError(f"Part file not found: {self.prt_file}")

        # Auto-detect sim file
        if sim_file:
            self.sim_file = Path(sim_file)
        else:
            # Look for *_sim1.sim or *.sim in same directory
            prt_dir = self.prt_file.parent
            sim_candidates = list(prt_dir.glob("*_sim1.sim")) + list(prt_dir.glob("*.sim"))
            if sim_candidates:
                self.sim_file = sim_candidates[0]
                logger.info(f"  Auto-detected sim file: {self.sim_file.name}")

        # Auto-detect fem file
        if fem_file:
            self.fem_file = Path(fem_file)
        elif self.sim_file:
            fem_candidates = list(self.sim_file.parent.glob("*_fem1.fem")) + list(self.sim_file.parent.glob("*.fem"))
            if fem_candidates:
                self.fem_file = fem_candidates[0]
                logger.info(f"  Auto-detected fem file: {self.fem_file.name}")

        # Set op2 file if provided
        if op2_file:
            self.op2_file = Path(op2_file)

        logger.info(f"Model files set:")
        logger.info(f"  PRT: {self.prt_file}")
        logger.info(f"  SIM: {self.sim_file}")
        logger.info(f"  FEM: {self.fem_file}")

        return self

    # =========================================================================
    # Model Introspection
    # =========================================================================

    def introspect(self, run_baseline: bool = False) -> IntrospectionResult:
        """
        Introspect model to discover expressions, solutions, and available results.

        Uses the model_introspection module for comprehensive analysis.

        Args:
            run_baseline: If True, run a baseline solve to generate OP2 for introspection

        Returns:
            IntrospectionResult with all discovered information
        """
        logger.info("=" * 60)
        logger.info("MODEL INTROSPECTION")
        logger.info("=" * 60)

        try:
            from optimization_engine.hooks.nx_cad.model_introspection import (
                introspect_part,
                introspect_simulation,
                introspect_op2
            )
        except ImportError:
            logger.warning("Model introspection module not available")
            self.introspection = IntrospectionResult(
                success=False,
                error="Model introspection module not available"
            )
            return self.introspection

        result = IntrospectionResult(success=True)

        # Introspect part file
        if self.prt_file and self.prt_file.exists():
            logger.info(f"\nIntrospecting part: {self.prt_file.name}")
            part_result = introspect_part(str(self.prt_file))

            if part_result.get('success'):
                data = part_result.get('data', {})
                result.expressions = data.get('expressions', [])
                result.bodies = data.get('bodies', [])
                result.mass_properties = data.get('mass_properties', {})

                logger.info(f"  Found {len(result.expressions)} expressions")
                logger.info(f"  Found {len(result.bodies)} bodies")
                if result.mass_properties:
                    logger.info(f"  Mass: {result.mass_properties.get('mass', 'N/A')} kg")
            else:
                logger.warning(f"  Part introspection failed: {part_result.get('error')}")

        # Introspect simulation file
        if self.sim_file and self.sim_file.exists():
            logger.info(f"\nIntrospecting simulation: {self.sim_file.name}")
            sim_result = introspect_simulation(str(self.sim_file))

            if sim_result.get('success'):
                data = sim_result.get('data', {})
                result.solutions = data.get('solutions', [])
                result.boundary_conditions = data.get('boundary_conditions', [])
                result.loads = data.get('loads', [])
                result.materials = data.get('materials', [])
                result.mesh_info = data.get('mesh_info', {})

                logger.info(f"  Found {len(result.solutions)} solutions")
                logger.info(f"  Found {len(result.boundary_conditions)} boundary conditions")
                logger.info(f"  Found {len(result.loads)} loads")
            else:
                logger.warning(f"  Simulation introspection failed: {sim_result.get('error')}")

        # Introspect OP2 file if available
        if self.op2_file and self.op2_file.exists():
            logger.info(f"\nIntrospecting OP2: {self.op2_file.name}")
            op2_result = introspect_op2(str(self.op2_file))

            if op2_result.get('success'):
                data = op2_result.get('data', {})
                result.available_results = data.get('available_results', {})
                result.subcases = data.get('subcases', [])

                logger.info(f"  Available results: {result.available_results}")
                logger.info(f"  Subcases: {result.subcases}")
            else:
                logger.warning(f"  OP2 introspection failed: {op2_result.get('error')}")

        self.introspection = result
        return result

    # =========================================================================
    # Design Variable, Objective, Constraint Management
    # =========================================================================

    def add_design_variable(
        self,
        parameter: str,
        bounds: Tuple[float, float],
        description: str = "",
        units: str = ""
    ) -> "StudyWizard":
        """
        Add a design variable to the study.

        Args:
            parameter: Name of the NX expression to vary
            bounds: (min, max) bounds for the variable
            description: Human-readable description
            units: Units (e.g., "mm", "degrees")

        Returns:
            Self for method chaining
        """
        dv = DesignVariable(
            parameter=parameter,
            bounds=bounds,
            description=description,
            units=units
        )
        self.design_variables.append(dv)
        logger.info(f"Added design variable: {parameter} [{bounds[0]}, {bounds[1]}] {units}")
        return self

    def add_objective(
        self,
        name: str,
        goal: str,
        extractor: str,
        params: Optional[Dict[str, Any]] = None,
        weight: float = 1.0,
        description: str = ""
    ) -> "StudyWizard":
        """
        Add an optimization objective.

        Args:
            name: Objective name (e.g., "mass", "stiffness")
            goal: "minimize" or "maximize"
            extractor: Extractor function name from catalog
            params: Additional parameters for extractor
            weight: Weight for multi-objective optimization
            description: Human-readable description

        Returns:
            Self for method chaining
        """
        if extractor not in EXTRACTOR_CATALOG:
            logger.warning(f"Extractor '{extractor}' not in catalog, proceeding anyway")

        obj = Objective(
            name=name,
            goal=goal,
            extractor=extractor,
            params=params or {},
            weight=weight,
            description=description
        )
        self.objectives.append(obj)
        logger.info(f"Added objective: {goal} {name} (extractor: {extractor})")
        return self

    def add_constraint(
        self,
        name: str,
        constraint_type: str,
        threshold: float,
        extractor: str,
        params: Optional[Dict[str, Any]] = None,
        description: str = "",
        units: str = ""
    ) -> "StudyWizard":
        """
        Add an optimization constraint.

        Args:
            name: Constraint name (e.g., "max_stress")
            constraint_type: "less_than" or "greater_than"
            threshold: Constraint threshold value
            extractor: Extractor function name from catalog
            params: Additional parameters for extractor
            description: Human-readable description
            units: Units for display

        Returns:
            Self for method chaining
        """
        const = Constraint(
            name=name,
            type=constraint_type,
            threshold=threshold,
            extractor=extractor,
            params=params or {},
            description=description,
            units=units
        )
        self.constraints.append(const)
        logger.info(f"Added constraint: {name} {constraint_type} {threshold} {units}")
        return self

    # =========================================================================
    # Settings
    # =========================================================================

    def set_protocol(self, protocol: str) -> "StudyWizard":
        """Set optimization protocol."""
        if protocol not in PROTOCOL_CATALOG:
            raise ValueError(f"Unknown protocol: {protocol}. Available: {list(PROTOCOL_CATALOG.keys())}")
        self.protocol = protocol
        return self

    def set_trials(self, n_trials: int) -> "StudyWizard":
        """Set number of optimization trials."""
        self.n_trials = n_trials
        return self

    def enable_neural(self, enabled: bool = True) -> "StudyWizard":
        """Enable/disable neural acceleration."""
        self.neural_enabled = enabled
        return self

    # =========================================================================
    # Generation
    # =========================================================================

    def generate(self, copy_model_files: bool = True) -> Dict[str, Path]:
        """
        Generate complete study structure.

        Creates:
        - Study directory structure
        - optimization_config.json
        - workflow_config.json
        - run_optimization.py
        - reset_study.py
        - README.md
        - STUDY_REPORT.md
        - MODEL_INTROSPECTION.md

        Args:
            copy_model_files: If True, copy model files to study directory

        Returns:
            Dict of generated file paths
        """
        logger.info("=" * 60)
        logger.info("GENERATING STUDY")
        logger.info("=" * 60)

        # Validate
        if not self.design_variables:
            raise ValueError("At least one design variable is required")
        if not self.objectives:
            raise ValueError("At least one objective is required")

        # Create directory structure
        setup_dir = self.study_dir / "1_setup"
        model_dir = setup_dir / "model"
        results_dir = self.study_dir / "2_results"

        setup_dir.mkdir(parents=True, exist_ok=True)
        model_dir.mkdir(exist_ok=True)
        results_dir.mkdir(exist_ok=True)

        logger.info(f"Created study directory: {self.study_dir}")

        generated_files = {}

        # Copy model files
        if copy_model_files and self.prt_file:
            self._copy_model_files(model_dir)

        # Generate optimization_config.json
        config_path = setup_dir / "optimization_config.json"
        self._generate_config(config_path)
        generated_files['optimization_config'] = config_path

        # Generate workflow_config.json
        workflow_path = setup_dir / "workflow_config.json"
        self._generate_workflow_config(workflow_path)
        generated_files['workflow_config'] = workflow_path

        # Generate run_optimization.py
        run_script_path = self.study_dir / "run_optimization.py"
        self._generate_run_script(run_script_path)
        generated_files['run_optimization'] = run_script_path

        # Generate reset_study.py
        reset_script_path = self.study_dir / "reset_study.py"
        self._generate_reset_script(reset_script_path)
        generated_files['reset_study'] = reset_script_path

        # Generate documentation
        readme_path = self.study_dir / "README.md"
        self._generate_readme(readme_path)
        generated_files['readme'] = readme_path

        report_path = self.study_dir / "STUDY_REPORT.md"
        self._generate_study_report(report_path)
        generated_files['study_report'] = report_path

        introspection_path = self.study_dir / "MODEL_INTROSPECTION.md"
        self._generate_introspection_report(introspection_path)
        generated_files['model_introspection'] = introspection_path

        logger.info("=" * 60)
        logger.info("STUDY GENERATION COMPLETE")
        logger.info("=" * 60)
        logger.info(f"\nGenerated files:")
        for name, path in generated_files.items():
            logger.info(f"  {name}: {path}")

        logger.info(f"\nNext steps:")
        logger.info(f"  1. Review generated files")
        logger.info(f"  2. cd {self.study_dir}")
        logger.info(f"  3. python run_optimization.py --discover")
        logger.info(f"  4. python run_optimization.py --validate")
        logger.info(f"  5. python run_optimization.py --run --trials {self.n_trials}")

        return generated_files

    def _copy_model_files(self, model_dir: Path):
        """Copy model files to study directory."""
        logger.info("Copying model files...")

        files_to_copy = [self.prt_file, self.sim_file, self.fem_file]
        for src in files_to_copy:
            if src and src.exists():
                dst = model_dir / src.name
                if not dst.exists():
                    shutil.copy2(src, dst)
                    logger.info(f"  Copied: {src.name}")
                else:
                    logger.info(f"  Already exists: {src.name}")

    def _generate_config(self, path: Path):
        """Generate optimization_config.json."""
        logger.info(f"Generating: {path.name}")

        # Determine simulation files
        model_name = self.prt_file.stem if self.prt_file else "model"
        sim_name = self.sim_file.stem if self.sim_file else f"{model_name}_sim1"

        # Infer dat and op2 file names
        dat_file = f"{sim_name.lower()}-solution_1.dat"
        op2_file = f"{sim_name.lower()}-solution_1.op2"

        # Determine sampler from protocol
        sampler = PROTOCOL_CATALOG.get(self.protocol, {}).get('sampler', 'NSGAIISampler')

        config = {
            "study_name": self.study_name,
            "description": self.description,
            "engineering_context": f"Generated by StudyWizard on {datetime.now().strftime('%Y-%m-%d %H:%M')}",

            "template_info": {
                "category": "structural",
                "analysis_type": "static",
                "typical_applications": [],
                "neural_enabled": self.neural_enabled
            },

            "optimization_settings": {
                "protocol": self.protocol,
                "n_trials": self.n_trials,
                "sampler": sampler,
                "pruner": None,
                "timeout_per_trial": self.timeout_per_trial
            },

            "design_variables": [dv.to_dict() for dv in self.design_variables],
            "objectives": [obj.to_dict() for obj in self.objectives],
            "constraints": [const.to_dict() for const in self.constraints],

            "simulation": {
                "model_file": self.prt_file.name if self.prt_file else "",
                "sim_file": self.sim_file.name if self.sim_file else "",
                "fem_file": self.fem_file.name if self.fem_file else "",
                "solver": "nastran",
                "analysis_types": ["static"],
                "solution_name": "Solution 1",
                "dat_file": dat_file,
                "op2_file": op2_file
            },

            "result_extraction": self._build_extraction_config(),

            "reporting": {
                "generate_plots": True,
                "save_incremental": True,
                "llm_summary": True,
                "generate_pareto_front": len(self.objectives) > 1
            },

            "neural_acceleration": {
                "enabled": self.neural_enabled,
                "min_training_points": 50,
                "auto_train": True,
                "epochs": 100,
                "validation_split": 0.2
            }
        }

        with open(path, 'w') as f:
            json.dump(config, f, indent=2)

    def _build_extraction_config(self) -> Dict[str, Any]:
        """Build result_extraction section of config."""
        extraction = {}

        # Add extractors for objectives
        for obj in self.objectives:
            extractor_info = EXTRACTOR_CATALOG.get(obj.extractor, {})
            extraction[obj.name] = {
                "method": obj.extractor,
                "extractor_module": extractor_info.get('module', ''),
                "function": extractor_info.get('function', obj.extractor),
                "output_unit": extractor_info.get('output_unit', '')
            }

        # Add extractors for constraints
        for const in self.constraints:
            if const.name not in extraction:
                extractor_info = EXTRACTOR_CATALOG.get(const.extractor, {})
                extraction[const.name] = {
                    "method": const.extractor,
                    "extractor_module": extractor_info.get('module', ''),
                    "function": extractor_info.get('function', const.extractor),
                    "output_unit": extractor_info.get('output_unit', const.units)
                }

        return extraction

    def _generate_workflow_config(self, path: Path):
        """Generate workflow_config.json."""
        logger.info(f"Generating: {path.name}")

        config = {
            "workflow_id": f"{self.study_name}_workflow",
            "description": f"Workflow for {self.study_name}",
            "steps": []
        }

        with open(path, 'w') as f:
            json.dump(config, f, indent=2)

    def _generate_run_script(self, path: Path):
        """Generate run_optimization.py script."""
        logger.info(f"Generating: {path.name}")

        # Build import statements for extractors
        extractor_imports = set()
        for obj in self.objectives:
            info = EXTRACTOR_CATALOG.get(obj.extractor, {})
            if info.get('module'):
                extractor_imports.add(f"from {info['module']} import {info.get('function', obj.extractor)}")

        for const in self.constraints:
            info = EXTRACTOR_CATALOG.get(const.extractor, {})
            if info.get('module'):
                extractor_imports.add(f"from {info['module']} import {info.get('function', const.extractor)}")

        # Determine if multi-objective
        is_multi = len(self.objectives) > 1
        sampler = PROTOCOL_CATALOG.get(self.protocol, {}).get('sampler', 'NSGAIISampler')

        # Build objective function extraction code
        extraction_code = self._build_extraction_code()

        # Build return statement
        if is_multi:
            returns = ", ".join([f"obj_{obj.name}" for obj in self.objectives])
            return_stmt = f"return ({returns})"
        else:
            obj = self.objectives[0]
            return_stmt = f"return obj_{obj.name}"

        script = f'''"""
{self.study_name} - Optimization Script
{"=" * 60}

{self.description}

Protocol: {PROTOCOL_CATALOG.get(self.protocol, {}).get('name', self.protocol)}

Staged Workflow:
----------------
1. DISCOVER: python run_optimization.py --discover
2. VALIDATE: python run_optimization.py --validate
3. TEST: python run_optimization.py --test
4. RUN: python run_optimization.py --run --trials {self.n_trials}

Generated by StudyWizard on {datetime.now().strftime('%Y-%m-%d %H:%M')}
"""

from pathlib import Path
import sys
import json
import argparse
from datetime import datetime
from typing import Optional, Tuple, List

# Add parent directory to path
project_root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(project_root))

import optuna
from optuna.samplers import {sampler}

# Core imports
from optimization_engine.nx.solver import NXSolver
from optimization_engine.utils.logger import get_logger

# Extractor imports
{chr(10).join(sorted(extractor_imports))}


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


def clean_nastran_files(model_dir: Path, logger) -> List[Path]:
    """Remove old Nastran solver output files."""
    patterns = ['*.op2', '*.f06', '*.log', '*.f04', '*.pch', '*.DBALL', '*.MASTER', '_temp*.txt']
    deleted = []

    for pattern in patterns:
        for f in model_dir.glob(pattern):
            try:
                f.unlink()
                deleted.append(f)
                logger.info(f"  Deleted: {{f.name}}")
            except Exception as e:
                logger.warning(f"  Failed to delete {{f.name}}: {{e}}")

    return deleted


def objective(trial: optuna.Trial, config: dict, nx_solver: NXSolver,
              model_dir: Path, logger) -> {"Tuple[" + ", ".join(["float"] * len(self.objectives)) + "]" if is_multi else "float"}:
    """
    Objective function for optimization.

    {"Returns tuple of objectives for multi-objective optimization." if is_multi else "Returns single objective value."}
    """
    # Sample design variables
    design_vars = {{}}
    for var in config['design_variables']:
        param_name = var['parameter']
        bounds = var['bounds']
        design_vars[param_name] = trial.suggest_float(param_name, bounds[0], bounds[1])

    logger.trial_start(trial.number, design_vars)

    try:
        # Get file paths
        sim_file = model_dir / config['simulation']['sim_file']

        # Run FEA simulation
        result = nx_solver.run_simulation(
            sim_file=sim_file,
            working_dir=model_dir,
            expression_updates=design_vars,
            solution_name=config['simulation'].get('solution_name'),
            cleanup=True
        )

        if not result['success']:
            logger.trial_failed(trial.number, f"Simulation failed: {{result.get('error', 'Unknown')}}")
            return {"(" + ", ".join(["float('inf')"] * len(self.objectives)) + ")" if is_multi else "float('inf')"}

        op2_file = result['op2_file']
        dat_file = model_dir / config['simulation']['dat_file']

{extraction_code}

        # Check constraints
        feasible = True
        constraint_results = {{}}
{self._build_constraint_check_code()}

        # Set user attributes
{self._build_user_attrs_code()}
        trial.set_user_attr('feasible', feasible)

        objectives = {{{", ".join([f"'{obj.name}': obj_{obj.name}" for obj in self.objectives])}}}
        logger.trial_complete(trial.number, objectives, constraint_results, feasible)

        {return_stmt}

    except Exception as e:
        logger.trial_failed(trial.number, str(e))
        return {"(" + ", ".join(["float('inf')"] * len(self.objectives)) + ")" if is_multi else "float('inf')"}


def main():
    """Main optimization workflow."""
    parser = argparse.ArgumentParser(description='{self.study_name}')

    stage_group = parser.add_mutually_exclusive_group()
    stage_group.add_argument('--discover', action='store_true', help='Discover model outputs')
    stage_group.add_argument('--validate', action='store_true', help='Run single validation trial')
    stage_group.add_argument('--test', action='store_true', help='Run 3-trial test')
    stage_group.add_argument('--run', action='store_true', help='Run optimization')

    parser.add_argument('--trials', type=int, default={self.n_trials}, help='Number of trials')
    parser.add_argument('--resume', action='store_true', help='Resume existing study')
    parser.add_argument('--clean', action='store_true', help='Clean old files first')

    args = parser.parse_args()

    if not any([args.discover, args.validate, args.test, args.run]):
        print("No stage specified. Use --discover, --validate, --test, or --run")
        return 1

    # Setup paths
    study_dir = Path(__file__).parent
    config_path = study_dir / "1_setup" / "optimization_config.json"
    model_dir = study_dir / "1_setup" / "model"
    results_dir = study_dir / "2_results"
    results_dir.mkdir(exist_ok=True)

    study_name = "{self.study_name}"

    # Initialize
    logger = get_logger(study_name, study_dir=results_dir)
    config = load_config(config_path)
    nx_solver = NXSolver()

    if args.clean:
        clean_nastran_files(model_dir, logger)

    # Run appropriate stage
    if args.discover or args.validate or args.test:
        # Run limited trials for these stages
        n = 1 if args.discover or args.validate else 3
        storage = f"sqlite:///{{results_dir / 'study_test.db'}}"

        study = optuna.create_study(
            study_name=f"{{study_name}}_test",
            storage=storage,
            sampler={sampler}({"population_size=5, seed=42" if is_multi else "seed=42"}),
            {"directions=['minimize'] * " + str(len(self.objectives)) if is_multi else "direction='minimize'"},
            load_if_exists=False
        )

        study.optimize(
            lambda trial: objective(trial, config, nx_solver, model_dir, logger),
            n_trials=n,
            show_progress_bar=True
        )

        logger.info(f"Completed {{len(study.trials)}} trial(s)")
        return 0

    # Full optimization run
    storage = f"sqlite:///{{results_dir / 'study.db'}}"

    if args.resume:
        study = optuna.load_study(
            study_name=study_name,
            storage=storage,
            sampler={sampler}({"population_size=20, seed=42" if is_multi else "seed=42"})
        )
    else:
        study = optuna.create_study(
            study_name=study_name,
            storage=storage,
            sampler={sampler}({"population_size=20, seed=42" if is_multi else "seed=42"}),
            {"directions=['minimize'] * " + str(len(self.objectives)) if is_multi else "direction='minimize'"},
            load_if_exists=True
        )

    logger.study_start(study_name, args.trials, "{sampler}")

    study.optimize(
        lambda trial: objective(trial, config, nx_solver, model_dir, logger),
        n_trials=args.trials,
        show_progress_bar=True
    )

    n_complete = len([t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE])
    logger.study_complete(study_name, len(study.trials), n_complete)

    # Report results
    {"pareto_trials = study.best_trials" if is_multi else "best_trial = study.best_trial"}
    logger.info(f"\\nOptimization Complete!")
    logger.info(f"Total trials: {{len(study.trials)}}")
    logger.info(f"Successful: {{n_complete}}")

    return 0


if __name__ == "__main__":
    exit(main())
'''

        with open(path, 'w') as f:
            f.write(script)

    def _build_extraction_code(self) -> str:
        """Build extraction code for objective function."""
        lines = []
        lines.append("        # Extract results")

        for obj in self.objectives:
            info = EXTRACTOR_CATALOG.get(obj.extractor, {})
            func = info.get('function', obj.extractor)

            # Determine source file
            if '.dat' in info.get('input', '') or '.bdf' in info.get('input', ''):
                source = "dat_file"
            else:
                source = "op2_file"

            # Build extraction call
            if 'displacement' in obj.extractor.lower():
                lines.append(f"        disp_result = {func}({source}, subcase=1)")
                lines.append(f"        max_displacement = disp_result['max_displacement']")
                if obj.goal == 'maximize':
                    lines.append(f"        # For stiffness maximization, use inverse of displacement")
                    lines.append(f"        applied_force = 1000.0  # N - adjust based on your model")
                    lines.append(f"        obj_{obj.name} = -applied_force / max(abs(max_displacement), 1e-6)")
                else:
                    lines.append(f"        obj_{obj.name} = max_displacement")
            elif 'mass' in obj.extractor.lower():
                lines.append(f"        obj_{obj.name} = {func}(str({source}))")
                if obj.goal == 'maximize':
                    lines.append(f"        obj_{obj.name} = -obj_{obj.name}  # Negate for maximization")
            elif 'stress' in obj.extractor.lower():
                lines.append(f"        stress_result = {func}({source}, subcase=1)")
                lines.append(f"        obj_{obj.name} = stress_result.get('max_von_mises', float('inf'))")
                if obj.goal == 'maximize':
                    lines.append(f"        obj_{obj.name} = -obj_{obj.name}  # Negate for maximization")
            else:
                # Generic extraction
                lines.append(f"        obj_{obj.name} = {func}({source})")
                if obj.goal == 'maximize':
                    lines.append(f"        obj_{obj.name} = -obj_{obj.name}  # Negate for maximization")

            lines.append(f"        logger.info(f'  {obj.name}: {{obj_{obj.name}}}')")
            lines.append("")

        return "\n".join(lines)

    def _build_constraint_check_code(self) -> str:
        """Build constraint checking code."""
        if not self.constraints:
            return "        pass  # No constraints defined"

        lines = []
        for const in self.constraints:
            lines.append(f"        # Check {const.name}")

            # Get the value to check (may need extraction)
            if any(obj.name == const.name for obj in self.objectives):
                # Already extracted as objective
                value_var = f"obj_{const.name}"
            else:
                # Need to extract
                info = EXTRACTOR_CATALOG.get(const.extractor, {})
                func = info.get('function', const.extractor)
                source = "dat_file" if '.dat' in info.get('input', '') else "op2_file"
                lines.append(f"        const_{const.name} = {func}({source})")
                value_var = f"const_{const.name}"

            lines.append(f"        constraint_results['{const.name}'] = {value_var}")

            if const.type == "less_than":
                lines.append(f"        if {value_var} > {const.threshold}:")
            else:
                lines.append(f"        if {value_var} < {const.threshold}:")

            lines.append(f"            feasible = False")
            lines.append(f"            logger.warning(f'  Constraint violation: {const.name} = {{{value_var}}} vs {const.threshold}')")
            lines.append("")

        return "\n".join(lines)

    def _build_user_attrs_code(self) -> str:
        """Build user attributes setting code."""
        lines = []
        for obj in self.objectives:
            lines.append(f"        trial.set_user_attr('{obj.name}', obj_{obj.name})")
        return "\n".join(lines)

    def _generate_reset_script(self, path: Path):
        """Generate reset_study.py script."""
        logger.info(f"Generating: {path.name}")

        script = f'''"""
Reset study - Delete results database and logs.

Usage:
    python reset_study.py
    python reset_study.py --confirm  # Skip confirmation
"""

from pathlib import Path
import shutil


def main():
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--confirm', action='store_true', help='Skip confirmation')
    args = parser.parse_args()

    study_dir = Path(__file__).parent
    results_dir = study_dir / "2_results"

    if not args.confirm:
        print(f"This will delete all results in: {{results_dir}}")
        response = input("Are you sure? (y/N): ")
        if response.lower() != 'y':
            print("Cancelled.")
            return

    # Delete database files
    for f in results_dir.glob("*.db"):
        f.unlink()
        print(f"Deleted: {{f.name}}")

    # Delete log files
    for f in results_dir.glob("*.log"):
        f.unlink()
        print(f"Deleted: {{f.name}}")

    # Delete JSON results
    for f in results_dir.glob("*.json"):
        f.unlink()
        print(f"Deleted: {{f.name}}")

    print("Study reset complete.")


if __name__ == "__main__":
    main()
'''

        with open(path, 'w') as f:
            f.write(script)

    def _generate_readme(self, path: Path):
        """Generate README.md documentation."""
        logger.info(f"Generating: {path.name}")

        # Build design variables table
        dv_table = "| Parameter | Bounds | Units | Description |\n|-----------|--------|-------|-------------|\n"
        for dv in self.design_variables:
            dv_table += f"| `{dv.parameter}` | [{dv.bounds[0]}, {dv.bounds[1]}] | {dv.units} | {dv.description} |\n"

        # Build objectives table
        obj_table = "| Objective | Goal | Extractor | Weight |\n|-----------|------|-----------|--------|\n"
        for obj in self.objectives:
            obj_table += f"| {obj.name} | {obj.goal} | `{obj.extractor}` | {obj.weight} |\n"

        # Build constraints table
        const_table = "| Constraint | Type | Threshold | Units |\n|------------|------|-----------|-------|\n"
        for const in self.constraints:
            const_table += f"| {const.name} | {const.type} | {const.threshold} | {const.units} |\n"

        protocol_info = PROTOCOL_CATALOG.get(self.protocol, {})

        readme = f'''# {self.study_name}

{self.description}

**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**Protocol**: {protocol_info.get('name', self.protocol)}
**Trials**: {self.n_trials}

---

## 1. Engineering Problem

{self.description}

---

## 2. Mathematical Formulation

### Design Variables

{dv_table}

### Objectives

{obj_table}

### Constraints

{const_table if self.constraints else "No constraints defined."}

---

## 3. Optimization Algorithm

- **Protocol**: {self.protocol}
- **Sampler**: {protocol_info.get('sampler', 'TPESampler')}
- **Trials**: {self.n_trials}
- **Neural Acceleration**: {"Enabled" if self.neural_enabled else "Disabled"}

---

## 4. Simulation Pipeline

```
Design Variables → NX Expression Update → Nastran Solve → Result Extraction → Objective Evaluation
```

---

## 5. Result Extraction Methods

| Result | Extractor | Source |
|--------|-----------|--------|
{chr(10).join([f"| {obj.name} | `{obj.extractor}` | OP2/DAT |" for obj in self.objectives])}

---

## 6. Study File Structure

```
{self.study_name}/
├── 1_setup/
│   ├── model/
│   │   ├── {self.prt_file.name if self.prt_file else "Model.prt"}
│   │   ├── {self.sim_file.name if self.sim_file else "Model_sim1.sim"}
│   │   └── {self.fem_file.name if self.fem_file else "Model_fem1.fem"}
│   ├── optimization_config.json
│   └── workflow_config.json
├── 2_results/
│   ├── study.db
│   └── optimization.log
├── run_optimization.py
├── reset_study.py
├── README.md
├── STUDY_REPORT.md
└── MODEL_INTROSPECTION.md
```

---

## 7. Quick Start

```bash
# 1. Discover model outputs
python run_optimization.py --discover

# 2. Validate setup with single trial
python run_optimization.py --validate

# 3. Run integration test (3 trials)
python run_optimization.py --test

# 4. Run full optimization
python run_optimization.py --run --trials {self.n_trials}

# 5. Resume if interrupted
python run_optimization.py --run --trials 50 --resume
```

---

## 8. Results Location

| File | Description |
|------|-------------|
| `2_results/study.db` | Optuna SQLite database |
| `2_results/optimization.log` | Structured log file |
| `2_results/pareto_front.json` | Pareto-optimal solutions |

---

## 9. References

- [Atomizer Documentation](../../docs/)
- [Protocol {self.protocol}](../../docs/protocols/system/)
- [Extractor Library](../../docs/protocols/system/SYS_12_EXTRACTOR_LIBRARY.md)
'''

        with open(path, 'w') as f:
            f.write(readme)

    def _generate_study_report(self, path: Path):
        """Generate STUDY_REPORT.md template."""
        logger.info(f"Generating: {path.name}")

        report = f'''# Study Report: {self.study_name}

**Status**: Not Started
**Created**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**Last Updated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}

---

## 1. Optimization Progress

| Metric | Value |
|--------|-------|
| Total Trials | 0 |
| Successful Trials | 0 |
| Best Objective | - |
| Duration | - |

---

## 2. Best Solutions

*No optimization runs completed yet.*

---

## 3. Pareto Front (if multi-objective)

*No Pareto front generated yet.*

---

## 4. Design Variable Sensitivity

*Analysis pending optimization runs.*

---

## 5. Constraint Satisfaction

*Analysis pending optimization runs.*

---

## 6. Recommendations

*Recommendations will be added after optimization runs.*

---

## 7. Next Steps

1. [ ] Run `python run_optimization.py --discover`
2. [ ] Run `python run_optimization.py --validate`
3. [ ] Run `python run_optimization.py --test`
4. [ ] Run `python run_optimization.py --run --trials {self.n_trials}`
5. [ ] Analyze results and update this report

---

*Generated by StudyWizard*
'''

        with open(path, 'w') as f:
            f.write(report)

    def _generate_introspection_report(self, path: Path):
        """Generate MODEL_INTROSPECTION.md report."""
        logger.info(f"Generating: {path.name}")

        # Build expressions table
        if self.introspection and self.introspection.expressions:
            expr_table = "| Name | Value | Unit | Optimization Candidate |\n|------|-------|------|------------------------|\n"
            for expr in self.introspection.expressions[:20]:  # Limit to 20
                name = expr.get('name', '')
                value = expr.get('value', 'N/A')
                unit = expr.get('unit', '')
                candidate = "✓ High" if any(kw in name.lower() for kw in ['thickness', 'angle', 'radius', 'length']) else "Medium"
                expr_table += f"| {name} | {value} | {unit} | {candidate} |\n"
        else:
            expr_table = "*Run introspection to discover expressions.*"

        # Build solutions table
        if self.introspection and self.introspection.solutions:
            sol_table = "| Solution | Type | Status |\n|----------|------|--------|\n"
            for sol in self.introspection.solutions:
                sol_table += f"| {sol.get('name', 'Unknown')} | {sol.get('type', 'Static')} | ✓ Active |\n"
        else:
            sol_table = "*Run introspection to discover solutions.*"

        report = f'''# Model Introspection Report

**Study**: {self.study_name}
**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**Introspection Version**: 1.0

---

## 1. Files Discovered

| Type | File | Status |
|------|------|--------|
| Part (.prt) | {self.prt_file.name if self.prt_file else "Not set"} | {"✓ Found" if self.prt_file and self.prt_file.exists() else "❌ Missing"} |
| Simulation (.sim) | {self.sim_file.name if self.sim_file else "Not set"} | {"✓ Found" if self.sim_file and self.sim_file.exists() else "❌ Missing"} |
| FEM (.fem) | {self.fem_file.name if self.fem_file else "Not set"} | {"✓ Found" if self.fem_file and self.fem_file.exists() else "⚠ Will be created"} |

---

## 2. Expressions (Potential Design Variables)

{expr_table}

---

## 3. Solutions

{sol_table}

---

## 4. Available Results

| Result Type | Available | Subcases |
|-------------|-----------|----------|
| Displacement | {"✓" if self.introspection and self.introspection.available_results.get('displacement') else "?"} | - |
| Stress | {"✓" if self.introspection and self.introspection.available_results.get('stress') else "?"} | - |
| SPC Forces | {"✓" if self.introspection and self.introspection.available_results.get('spc_forces') else "?"} | - |

---

## 5. Optimization Configuration

### Selected Design Variables

{chr(10).join([f"- `{dv.parameter}`: [{dv.bounds[0]}, {dv.bounds[1]}] {dv.units}" for dv in self.design_variables]) if self.design_variables else "*No design variables configured yet.*"}

### Selected Objectives

{chr(10).join([f"- {obj.goal.capitalize()} `{obj.name}` using `{obj.extractor}`" for obj in self.objectives]) if self.objectives else "*No objectives configured yet.*"}

### Selected Constraints

{chr(10).join([f"- `{c.name}` {c.type} {c.threshold} {c.units}" for c in self.constraints]) if self.constraints else "*No constraints configured.*"}

---

*Ready to create optimization study? Run `python run_optimization.py --discover` to proceed.*
'''

        with open(path, 'w') as f:
            f.write(report)


# =============================================================================
# Convenience Functions
# =============================================================================

def create_study(
    study_name: str,
    description: str,
    prt_file: Union[str, Path],
    design_variables: List[Dict[str, Any]],
    objectives: List[Dict[str, Any]],
    constraints: Optional[List[Dict[str, Any]]] = None,
    n_trials: int = 100,
    protocol: str = "protocol_11_multi"
) -> Dict[str, Path]:
    """
    Convenience function to create a complete study in one call.

    Args:
        study_name: Name of the study
        description: Human-readable description
        prt_file: Path to NX part file
        design_variables: List of design variable dicts with keys:
            - parameter: str
            - bounds: [min, max]
            - units: str (optional)
            - description: str (optional)
        objectives: List of objective dicts with keys:
            - name: str
            - goal: "minimize" or "maximize"
            - extractor: str
            - params: dict (optional)
        constraints: List of constraint dicts with keys:
            - name: str
            - type: "less_than" or "greater_than"
            - threshold: float
            - extractor: str
            - units: str (optional)
        n_trials: Number of optimization trials
        protocol: Optimization protocol

    Returns:
        Dict of generated file paths

    Example:
        create_study(
            study_name="bracket_opt",
            description="Optimize bracket for stiffness",
            prt_file="Bracket.prt",
            design_variables=[
                {"parameter": "thickness", "bounds": [5, 20], "units": "mm"}
            ],
            objectives=[
                {"name": "stiffness", "goal": "maximize", "extractor": "extract_displacement"}
            ],
            constraints=[
                {"name": "mass", "type": "less_than", "threshold": 0.5, "extractor": "extract_mass_from_bdf", "units": "kg"}
            ]
        )
    """
    wizard = StudyWizard(study_name, description)
    wizard.set_model_files(prt_file)
    wizard.set_protocol(protocol)
    wizard.set_trials(n_trials)

    # Add design variables
    for dv in design_variables:
        wizard.add_design_variable(
            parameter=dv['parameter'],
            bounds=tuple(dv['bounds']),
            units=dv.get('units', ''),
            description=dv.get('description', '')
        )

    # Add objectives
    for obj in objectives:
        wizard.add_objective(
            name=obj['name'],
            goal=obj['goal'],
            extractor=obj['extractor'],
            params=obj.get('params', {}),
            weight=obj.get('weight', 1.0),
            description=obj.get('description', '')
        )

    # Add constraints
    for const in (constraints or []):
        wizard.add_constraint(
            name=const['name'],
            constraint_type=const['type'],
            threshold=const['threshold'],
            extractor=const['extractor'],
            params=const.get('params', {}),
            units=const.get('units', ''),
            description=const.get('description', '')
        )

    # Run introspection if model files exist
    try:
        wizard.introspect()
    except Exception as e:
        logger.warning(f"Introspection failed: {e}")

    return wizard.generate()


def list_extractors() -> Dict[str, Dict[str, Any]]:
    """Return the extractor catalog."""
    return EXTRACTOR_CATALOG.copy()


def list_protocols() -> Dict[str, Dict[str, Any]]:
    """Return the protocol catalog."""
    return PROTOCOL_CATALOG.copy()


# =============================================================================
# CLI
# =============================================================================

def main():
    """CLI for study wizard."""
    import argparse

    parser = argparse.ArgumentParser(description="Atomizer Study Creation Wizard")
    parser.add_argument('--name', required=True, help='Study name')
    parser.add_argument('--description', default='', help='Study description')
    parser.add_argument('--prt', required=True, help='Path to PRT file')
    parser.add_argument('--list-extractors', action='store_true', help='List available extractors')

    args = parser.parse_args()

    if args.list_extractors:
        print("\nAvailable Extractors:")
        print("=" * 60)
        for name, info in EXTRACTOR_CATALOG.items():
            print(f"\n{name}:")
            print(f"  Input: {info.get('input', 'N/A')}")
            print(f"  Output: {info.get('output_unit', 'N/A')}")
            print(f"  {info.get('description', '')}")
        return 0

    # Interactive wizard would go here
    print(f"\nStudy Wizard initialized for: {args.name}")
    print("Use the Python API for full functionality.")

    return 0


if __name__ == "__main__":
    exit(main())
