"""
Zernike Helper Functions for Atomizer Optimization
===================================================

Convenience wrappers and utilities for using Zernike analysis
in optimization studies. These helpers simplify integration with
the standard Atomizer optimization patterns.

Usage in run_optimization.py:
    from optimization_engine.extractors.zernike_helpers import (
        create_zernike_objective,
        ZernikeObjectiveBuilder
    )

    # Simple: create objective function
    zernike_obj = create_zernike_objective(
        op2_finder=lambda: sim_dir / "model-solution_1.op2",
        subcase="20",
        metric="filtered_rms_nm"
    )

    # Use in Optuna trial
    rms = zernike_obj()
"""

from pathlib import Path
from typing import Callable, Dict, Any, Optional, Union, List
import logging

from optimization_engine.extractors.extract_zernike import (
    ZernikeExtractor,
    extract_zernike_from_op2,
    extract_zernike_filtered_rms,
)

logger = logging.getLogger(__name__)


def create_zernike_objective(
    op2_finder: Callable[[], Path],
    bdf_finder: Optional[Callable[[], Path]] = None,
    subcase: Union[int, str] = "20",
    metric: str = "filtered_rms_nm",
    displacement_unit: str = "mm",
    **kwargs
) -> Callable[[], float]:
    """
    Create a Zernike objective function for optimization.

    This factory creates a callable that:
    1. Finds the OP2 file (using op2_finder)
    2. Extracts Zernike metrics
    3. Returns the specified metric value

    Args:
        op2_finder: Callable that returns path to current OP2 file
        bdf_finder: Callable that returns path to BDF file (auto-detect if None)
        subcase: Subcase to analyze (e.g., "20" for 20 deg elevation)
        metric: Metric to return (see available_metrics below)
        displacement_unit: Unit of displacement in OP2 file
        **kwargs: Additional arguments for ZernikeExtractor

    Returns:
        Callable that returns the metric value

    Available metrics:
        - global_rms_nm: Global RMS wavefront error
        - filtered_rms_nm: Filtered RMS (low orders removed)
        - defocus_nm: Defocus aberration
        - astigmatism_rms_nm: Combined astigmatism
        - coma_rms_nm: Combined coma
        - trefoil_rms_nm: Combined trefoil
        - spherical_nm: Primary spherical aberration

    Example:
        op2_finder = lambda: Path("sim_dir") / "model-solution_1.op2"
        objective = create_zernike_objective(op2_finder, subcase="20")

        # In optimization loop
        rms_value = objective()  # Returns filtered RMS in nm
    """
    def evaluate() -> float:
        op2_path = op2_finder()
        bdf_path = bdf_finder() if bdf_finder else None

        result = extract_zernike_from_op2(
            op2_path,
            bdf_path,
            subcase=subcase,
            displacement_unit=displacement_unit,
            **kwargs
        )

        if metric not in result:
            available = [k for k in result.keys() if isinstance(result[k], (int, float))]
            raise ValueError(f"Metric '{metric}' not found. Available: {available}")

        return result[metric]

    return evaluate


def create_relative_zernike_objective(
    op2_finder: Callable[[], Path],
    target_subcase: Union[int, str],
    reference_subcase: Union[int, str],
    bdf_finder: Optional[Callable[[], Path]] = None,
    metric: str = "relative_filtered_rms_nm",
    **kwargs
) -> Callable[[], float]:
    """
    Create objective for relative Zernike metrics between subcases.

    Useful for minimizing gravity-induced deformation relative to
    a reference orientation (e.g., polishing position at 90 deg).

    Args:
        op2_finder: Callable returning OP2 path
        target_subcase: Subcase to analyze
        reference_subcase: Reference subcase to subtract
        bdf_finder: Optional BDF path finder
        metric: Relative metric to return
        **kwargs: Additional ZernikeExtractor arguments

    Returns:
        Callable that returns relative metric value
    """
    def evaluate() -> float:
        op2_path = op2_finder()
        bdf_path = bdf_finder() if bdf_finder else None

        extractor = ZernikeExtractor(op2_path, bdf_path, **kwargs)
        result = extractor.extract_relative(
            str(target_subcase),
            str(reference_subcase)
        )

        if metric not in result:
            available = [k for k in result.keys() if isinstance(result[k], (int, float))]
            raise ValueError(f"Metric '{metric}' not found. Available: {available}")

        return result[metric]

    return evaluate


class ZernikeObjectiveBuilder:
    """
    Builder for complex Zernike objectives with multiple subcases.

    This is useful for multi-subcase optimization where you want
    to combine metrics from different gravity orientations.

    Example:
        builder = ZernikeObjectiveBuilder(
            op2_finder=lambda: sim_dir / "model.op2"
        )

        # Add objectives for different subcases
        builder.add_subcase_objective("20", "filtered_rms_nm", weight=1.0)
        builder.add_subcase_objective("40", "filtered_rms_nm", weight=0.5)
        builder.add_subcase_objective("60", "filtered_rms_nm", weight=0.5)

        # Create combined objective
        objective = builder.build_weighted_sum()
        combined_rms = objective()  # Returns weighted sum
    """

    def __init__(
        self,
        op2_finder: Callable[[], Path],
        bdf_finder: Optional[Callable[[], Path]] = None,
        displacement_unit: str = "mm",
        **kwargs
    ):
        self.op2_finder = op2_finder
        self.bdf_finder = bdf_finder
        self.displacement_unit = displacement_unit
        self.kwargs = kwargs
        self.objectives: List[Dict[str, Any]] = []
        self._extractor = None

    def add_subcase_objective(
        self,
        subcase: Union[int, str],
        metric: str = "filtered_rms_nm",
        weight: float = 1.0,
        name: Optional[str] = None
    ) -> "ZernikeObjectiveBuilder":
        """Add a subcase objective to the builder."""
        self.objectives.append({
            "subcase": str(subcase),
            "metric": metric,
            "weight": weight,
            "name": name or f"{metric}_{subcase}"
        })
        return self

    def add_relative_objective(
        self,
        target_subcase: Union[int, str],
        reference_subcase: Union[int, str],
        metric: str = "relative_filtered_rms_nm",
        weight: float = 1.0,
        name: Optional[str] = None
    ) -> "ZernikeObjectiveBuilder":
        """Add a relative objective between subcases."""
        self.objectives.append({
            "target_subcase": str(target_subcase),
            "reference_subcase": str(reference_subcase),
            "metric": metric,
            "weight": weight,
            "name": name or f"rel_{target_subcase}_vs_{reference_subcase}",
            "is_relative": True
        })
        return self

    def _get_extractor(self) -> ZernikeExtractor:
        """Lazy-create extractor (reused for all objectives)."""
        if self._extractor is None:
            op2_path = self.op2_finder()
            bdf_path = self.bdf_finder() if self.bdf_finder else None
            self._extractor = ZernikeExtractor(
                op2_path, bdf_path,
                displacement_unit=self.displacement_unit,
                **self.kwargs
            )
        return self._extractor

    def _reset_extractor(self):
        """Reset extractor (call after OP2 changes)."""
        self._extractor = None

    def evaluate_all(self) -> Dict[str, float]:
        """
        Evaluate all objectives and return dict of values.

        Returns:
            Dict mapping objective name to value
        """
        self._reset_extractor()
        extractor = self._get_extractor()
        results = {}

        for obj in self.objectives:
            try:
                if obj.get("is_relative"):
                    rel_result = extractor.extract_relative(
                        obj["target_subcase"],
                        obj["reference_subcase"]
                    )
                    results[obj["name"]] = rel_result.get(obj["metric"], 0.0)
                else:
                    sub_result = extractor.extract_subcase(obj["subcase"])
                    results[obj["name"]] = sub_result.get(obj["metric"], 0.0)
            except Exception as e:
                logger.warning(f"Failed to evaluate {obj['name']}: {e}")
                results[obj["name"]] = float("inf")

        return results

    def build_weighted_sum(self) -> Callable[[], float]:
        """
        Build a callable that returns weighted sum of all objectives.

        Returns:
            Callable returning combined objective value
        """
        def evaluate() -> float:
            values = self.evaluate_all()
            total = 0.0
            for obj in self.objectives:
                val = values.get(obj["name"], 0.0)
                total += obj["weight"] * val
            return total

        return evaluate

    def build_max(self) -> Callable[[], float]:
        """
        Build a callable that returns maximum of all objectives.

        Useful for worst-case optimization across subcases.
        """
        def evaluate() -> float:
            values = self.evaluate_all()
            weighted = [
                obj["weight"] * values.get(obj["name"], 0.0)
                for obj in self.objectives
            ]
            return max(weighted) if weighted else 0.0

        return evaluate

    def build_individual(self) -> Callable[[], Dict[str, float]]:
        """
        Build a callable that returns dict of individual objective values.

        Useful for multi-objective optimization (NSGA-II).
        """
        return self.evaluate_all


def extract_zernike_for_trial(
    op2_path: Path,
    bdf_path: Optional[Path] = None,
    subcases: Optional[List[str]] = None,
    reference_subcase: str = "20",
    metrics: Optional[List[str]] = None,
    **kwargs
) -> Dict[str, Any]:
    """
    Extract comprehensive Zernike data for a trial.

    This is a high-level function for logging/exporting trial data.
    It extracts all metrics for specified subcases and computes
    relative metrics vs the reference.

    Args:
        op2_path: Path to OP2 file
        bdf_path: Path to BDF file (auto-detect if None)
        subcases: List of subcases to extract (None = all available)
        reference_subcase: Reference for relative calculations
        metrics: Specific metrics to extract (None = all)
        **kwargs: Additional ZernikeExtractor arguments

    Returns:
        Dict with complete trial Zernike data:
        {
            'subcases': {
                '20': {'global_rms_nm': ..., 'filtered_rms_nm': ..., ...},
                '40': {...},
                ...
            },
            'relative': {
                '40_vs_20': {'relative_filtered_rms_nm': ..., ...},
                ...
            },
            'summary': {
                'best_filtered_rms': ...,
                'worst_filtered_rms': ...,
                ...
            }
        }
    """
    extractor = ZernikeExtractor(op2_path, bdf_path, **kwargs)

    # Get available subcases
    available = list(extractor.displacements.keys())
    if subcases:
        subcases = [s for s in subcases if str(s) in available]
    else:
        subcases = available

    # Extract per-subcase data
    subcase_data = {}
    for sc in subcases:
        try:
            subcase_data[sc] = extractor.extract_subcase(str(sc))
        except Exception as e:
            logger.warning(f"Failed to extract subcase {sc}: {e}")

    # Extract relative data
    relative_data = {}
    if reference_subcase in subcases:
        for sc in subcases:
            if sc != reference_subcase:
                try:
                    key = f"{sc}_vs_{reference_subcase}"
                    relative_data[key] = extractor.extract_relative(
                        str(sc), str(reference_subcase)
                    )
                except Exception as e:
                    logger.warning(f"Failed to extract relative {key}: {e}")

    # Summary statistics
    filtered_rms_values = [
        d.get('filtered_rms_nm', float('inf'))
        for d in subcase_data.values()
    ]

    summary = {
        'best_filtered_rms': min(filtered_rms_values) if filtered_rms_values else None,
        'worst_filtered_rms': max(filtered_rms_values) if filtered_rms_values else None,
        'mean_filtered_rms': sum(filtered_rms_values) / len(filtered_rms_values) if filtered_rms_values else None,
        'n_subcases': len(subcases),
        'reference_subcase': reference_subcase,
    }

    return {
        'subcases': subcase_data,
        'relative': relative_data,
        'summary': summary,
    }


# Export all helpers
__all__ = [
    'create_zernike_objective',
    'create_relative_zernike_objective',
    'ZernikeObjectiveBuilder',
    'extract_zernike_for_trial',
]
