"""Modeling controller for LSCSIM."""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from pytola_sim.lscsim.models.project_model import Material
from pytola_sim.lscsim.utils.logger import get_logger

logger = get_logger(__name__)


class ModelingMode(Enum):
    """Different modeling modes."""

    FREE_FORM = "free_form"
    TEMPLATE_BASED = "template_based"
    PARAMETRIC = "parametric"


class GeometryType(Enum):
    """Supported geometry types."""

    PLATE = "plate"
    SPHERE = "sphere"
    CYLINDER = "cylinder"
    CUSTOM = "custom"


@dataclass
class ModelingParameters:
    """Parameters for modeling operations."""

    mode: ModelingMode = ModelingMode.FREE_FORM
    geometry_type: GeometryType = GeometryType.PLATE
    dimensions: dict[str, float] = field(default_factory=dict)
    material_name: str = ""
    mesh_size: float = 1.0
    symmetry: bool = False
    description: str = ""


@dataclass
class ModelingOperationResult:
    """Result of modeling operations."""

    success: bool
    message: str
    model_data: Any = None
    warnings: list[str] = field(default_factory=list)
    error_code: str | None = None


class ModelingController:
    """Controller for modeling operations."""

    # Expose enums for external access
    GeometryType = GeometryType
    ModelingMode = ModelingMode

    def __init__(self) -> None:
        self._available_materials: list[Material] = []
        self._current_model: dict[str, Any] | None = None
        self._model_history: list[dict[str, Any]] = []
        self._max_history_size = 10
        logger.info("Modeling controller initialized")

    def initialize_modeling_session(self) -> ModelingOperationResult:
        """Initialize a new modeling session."""
        try:
            # Load default materials
            self._load_default_materials()

            # Reset current model
            self._current_model = {
                "version": "1.0.0",
                "geometry": {},
                "materials": [],
                "mesh": {},
                "boundary_conditions": {},
                "loads": {},
            }

            logger.info("Modeling session initialized")
            return ModelingOperationResult(
                success=True,
                message="Modeling session initialized successfully",
                model_data=self._current_model,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to initialize modeling session")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to initialize modeling session: {e!s}",
                error_code="INITIALIZATION_ERROR",
            )

    def create_geometry(
        self,
        geometry_type: GeometryType,
        parameters: dict[str, float],
    ) -> ModelingOperationResult:
        """Create geometry based on type and parameters."""
        try:
            # Validate parameters based on geometry type
            validation_result = self._validate_geometry_parameters(
                geometry_type,
                parameters,
            )
            if not validation_result.success:
                return validation_result

            # Create geometry data
            geometry_data = {
                "type": geometry_type.value,
                "parameters": parameters,
                "created_at": self._get_timestamp(),
            }

            # Update current model
            if self._current_model:
                self._current_model["geometry"] = geometry_data
                self._add_to_history()

            logger.info(f"Created {geometry_type.value} geometry")
            return ModelingOperationResult(
                success=True,
                message=f"{geometry_type.value.capitalize()} geometry created successfully",
                model_data=geometry_data,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to create geometry")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to create geometry: {e!s}",
                error_code="GEOMETRY_CREATION_ERROR",
            )

    def assign_material(
        self,
        material_name: str,
        geometry_elements: list[str] | None = None,
    ) -> ModelingOperationResult:
        """Assign material to geometry elements."""
        try:
            # Check if material exists
            material = self._find_material(material_name)
            if not material:
                return ModelingOperationResult(
                    success=False,
                    message=f"Material '{material_name}' not found",
                    error_code="MATERIAL_NOT_FOUND",
                )

            # Create material assignment
            material_assignment = {
                "material_name": material_name,
                "material_properties": material.to_dict(),
                "elements": geometry_elements or ["all"],
                "assigned_at": self._get_timestamp(),
            }

            # Update current model
            if self._current_model:
                materials_list = self._current_model.get("materials", [])
                # Remove existing assignment for same elements if any
                materials_list = [
                    m for m in materials_list if not set(m["elements"]) & set(material_assignment["elements"])
                ]
                materials_list.append(material_assignment)
                self._current_model["materials"] = materials_list
                self._add_to_history()

            logger.info(f"Assigned material '{material_name}' to elements")
            return ModelingOperationResult(
                success=True,
                message=f"Material '{material_name}' assigned successfully",
                model_data=material_assignment,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to assign material")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to assign material: {e!s}",
                error_code="MATERIAL_ASSIGNMENT_ERROR",
            )

    def set_mesh_parameters(
        self,
        mesh_size: float,
        element_type: str = "solid",
    ) -> ModelingOperationResult:
        """Set mesh generation parameters."""
        try:
            if mesh_size <= 0:
                return ModelingOperationResult(
                    success=False,
                    message="Mesh size must be positive",
                    error_code="INVALID_MESH_SIZE",
                )

            mesh_data = {
                "element_type": element_type,
                "global_size": mesh_size,
                "local_sizes": {},  # Can be extended for local mesh refinement
                "algorithm": "automatic",
                "set_at": self._get_timestamp(),
            }

            # Update current model
            if self._current_model:
                self._current_model["mesh"] = mesh_data
                self._add_to_history()

            logger.info(f"Set mesh parameters: size={mesh_size}, type={element_type}")
            return ModelingOperationResult(
                success=True,
                message="Mesh parameters set successfully",
                model_data=mesh_data,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to set mesh parameters")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to set mesh parameters: {e!s}",
                error_code="MESH_PARAMETERS_ERROR",
            )

    def apply_boundary_conditions(
        self,
        condition_type: str,
        surfaces: list[str],
        values: dict[str, float],
    ) -> ModelingOperationResult:
        """Apply boundary conditions to model."""
        try:
            # Validate condition type
            supported_conditions = ["fixed", "pinned", "roller", "symmetry"]
            if condition_type not in supported_conditions:
                return ModelingOperationResult(
                    success=False,
                    message=f"Unsupported boundary condition: {condition_type}",
                    error_code="UNSUPPORTED_CONDITION",
                )

            bc_data = {
                "type": condition_type,
                "surfaces": surfaces,
                "values": values,
                "applied_at": self._get_timestamp(),
            }

            # Update current model
            if self._current_model:
                bcs = self._current_model.get("boundary_conditions", {})
                bc_key = f"{condition_type}_{'_'.join(surfaces)}"
                bcs[bc_key] = bc_data
                self._current_model["boundary_conditions"] = bcs
                self._add_to_history()

            logger.info(f"Applied {condition_type} boundary condition")
            return ModelingOperationResult(
                success=True,
                message=f"{condition_type.capitalize()} boundary condition applied successfully",
                model_data=bc_data,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to apply boundary conditions")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to apply boundary conditions: {e!s}",
                error_code="BOUNDARY_CONDITION_ERROR",
            )

    def apply_loads(
        self,
        load_type: str,
        surfaces: list[str],
        magnitude: float,
        direction: list[float],
    ) -> ModelingOperationResult:
        """Apply loads to model."""
        try:
            # Validate load type
            supported_loads = ["pressure", "force", "gravity", "centrifugal"]
            if load_type not in supported_loads:
                return ModelingOperationResult(
                    success=False,
                    message=f"Unsupported load type: {load_type}",
                    error_code="UNSUPPORTED_LOAD",
                )

            load_data = {
                "type": load_type,
                "surfaces": surfaces,
                "magnitude": magnitude,
                "direction": direction,
                "applied_at": self._get_timestamp(),
            }

            # Update current model
            if self._current_model:
                loads = self._current_model.get("loads", {})
                load_key = f"{load_type}_{'_'.join(surfaces)}"
                loads[load_key] = load_data
                self._current_model["loads"] = loads
                self._add_to_history()

            logger.info(f"Applied {load_type} load")
            return ModelingOperationResult(
                success=True,
                message=f"{load_type.capitalize()} load applied successfully",
                model_data=load_data,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to apply loads")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to apply loads: {e!s}",
                error_code="LOAD_APPLICATION_ERROR",
            )

    def get_available_materials(self) -> list[dict[str, Any]]:
        """Get list of available materials."""
        return [material.to_dict() for material in self._available_materials]

    def add_custom_material(self, material: Material) -> ModelingOperationResult:
        """Add a custom material to the library."""
        try:
            # Check if material already exists
            if self._find_material(material.name):
                return ModelingOperationResult(
                    success=False,
                    message=f"Material '{material.name}' already exists",
                    error_code="MATERIAL_EXISTS",
                )

            self._available_materials.append(material)
            logger.info(f"Added custom material: {material.name}")
            return ModelingOperationResult(
                success=True,
                message=f"Material '{material.name}' added successfully",
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to add custom material")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to add custom material: {e!s}",
                error_code="MATERIAL_ADDITION_ERROR",
            )

    def get_current_model(self) -> dict[str, Any] | None:
        """Get current model data."""
        return self._current_model.copy() if self._current_model else None

    def undo_last_operation(self) -> ModelingOperationResult:
        """Undo the last modeling operation."""
        try:
            if len(self._model_history) > 0:
                self._current_model = self._model_history.pop()
                logger.info("Undid last operation")
                return ModelingOperationResult(
                    success=True,
                    message="Last operation undone successfully",
                    model_data=self._current_model,
                )
            return ModelingOperationResult(
                success=False,
                message="No operations to undo",
                error_code="NO_HISTORY",
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to undo operation")
            return ModelingOperationResult(
                success=False,
                message=f"Failed to undo operation: {e!s}",
                error_code="UNDO_ERROR",
            )

    def _load_default_materials(self) -> None:
        """Load default materials into the library."""
        default_materials = [
            Material(
                name="Structural Steel",
                density=7850.0,
                young_modulus=200e9,
                poisson_ratio=0.3,
                yield_strength=250e6,
                description="Standard structural steel",
            ),
            Material(
                name="Aluminum 6061",
                density=2700.0,
                young_modulus=69e9,
                poisson_ratio=0.33,
                yield_strength=276e6,
                description="Common aluminum alloy",
            ),
            Material(
                name="Titanium Grade 5",
                density=4430.0,
                young_modulus=114e9,
                poisson_ratio=0.34,
                yield_strength=880e6,
                description="High-strength titanium alloy",
            ),
            Material(
                name="Concrete",
                density=2400.0,
                young_modulus=30e9,
                poisson_ratio=0.2,
                yield_strength=20e6,
                description="Standard concrete",
            ),
        ]

        self._available_materials.extend(default_materials)
        logger.info(f"Loaded {len(default_materials)} default materials")

    def _find_material(self, name: str) -> Material | None:
        """Find material by name."""
        for material in self._available_materials:
            if material.name.lower() == name.lower():
                return material
        return None

    def _validate_geometry_parameters(
        self,
        geometry_type: GeometryType,
        parameters: dict[str, float],
    ) -> ModelingOperationResult:
        """Validate geometry parameters."""
        required_params = {
            GeometryType.PLATE: ["length", "width", "thickness"],
            GeometryType.SPHERE: ["radius"],
            GeometryType.CYLINDER: ["radius", "height"],
            GeometryType.CUSTOM: [],
        }

        required = required_params.get(geometry_type, [])

        # Check required parameters
        missing_params = [param for param in required if param not in parameters]
        if missing_params:
            return ModelingOperationResult(
                success=False,
                message=f"Missing required parameters: {', '.join(missing_params)}",
                error_code="MISSING_PARAMETERS",
            )

        # Check parameter values
        for param, value in parameters.items():
            if value <= 0:
                return ModelingOperationResult(
                    success=False,
                    message=f"Parameter '{param}' must be positive",
                    error_code="INVALID_PARAMETER_VALUE",
                )

        return ModelingOperationResult(success=True, message="Parameters are valid")

    def _add_to_history(self) -> None:
        """Add current model state to history."""
        if self._current_model:
            self._model_history.append(self._current_model.copy())
            # Keep only recent history
            if len(self._model_history) > self._max_history_size:
                self._model_history = self._model_history[-self._max_history_size :]

    def _get_timestamp(self) -> str:
        """Get current timestamp."""
        from datetime import datetime, timezone

        return datetime.now(timezone.utc).isoformat()
