"""Analysis controller for LSCSIM."""

from __future__ import annotations

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

from pytola_sim.lscsim.utils.logger import get_logger

logger = get_logger(__name__)


class AnalysisType(Enum):
    """Supported analysis types."""

    STATIC = "static"
    DYNAMIC = "dynamic"
    THERMAL = "thermal"
    FREQUENCY = "frequency"
    BUCKLING = "buckling"


class SolverType(Enum):
    """Available solver types."""

    IMPLICIT = "implicit"
    EXPLICIT = "explicit"
    EIGEN = "eigen"


@dataclass
class AnalysisParameters:
    """Parameters for analysis setup."""

    analysis_type: AnalysisType
    solver_type: SolverType
    time_step: float = 0.001
    max_time: float = 1.0
    convergence_tolerance: float = 1e-6
    max_iterations: int = 100
    output_frequency: int = 10
    damping_ratio: float = 0.02
    additional_params: dict[str, Any] = field(default_factory=dict)


@dataclass
class AnalysisResult:
    """Structure for analysis results."""

    analysis_id: str
    analysis_type: AnalysisType
    solver_type: SolverType
    status: str  # pending, running, completed, failed
    start_time: datetime
    end_time: datetime | None = None
    results_data: dict[str, Any] = field(default_factory=dict)
    convergence_history: list[dict[str, Any]] = field(default_factory=list)
    error_messages: list[str] = field(default_factory=list)


@dataclass
class AnalysisOperationResult:
    """Result of analysis operations."""

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


class AnalysisController:
    """Controller for analysis operations."""

    # Expose enums for external access
    AnalysisType = AnalysisType
    SolverType = SolverType

    def __init__(self) -> None:
        self._active_analyses: dict[str, AnalysisResult] = {}
        self._completed_analyses: list[AnalysisResult] = []
        self._max_completed_storage = 50
        self._default_solver_settings: dict[AnalysisType, dict[str, Any]] = {
            AnalysisType.STATIC: {
                "solver_type": SolverType.IMPLICIT,
                "convergence_tolerance": 1e-6,
                "max_iterations": 50,
            },
            AnalysisType.DYNAMIC: {
                "solver_type": SolverType.EXPLICIT,
                "time_step": 0.001,
                "max_time": 1.0,
                "damping_ratio": 0.02,
            },
            AnalysisType.THERMAL: {
                "solver_type": SolverType.IMPLICIT,
                "time_step": 0.1,
                "max_time": 100.0,
            },
            AnalysisType.FREQUENCY: {
                "solver_type": SolverType.EIGEN,
                "max_iterations": 20,
            },
            AnalysisType.BUCKLING: {
                "solver_type": SolverType.EIGEN,
                "max_iterations": 30,
            },
        }
        logger.info("Analysis controller initialized")

    def setup_analysis(
        self,
        analysis_type: AnalysisType,
        model_data: dict[str, Any],
        custom_params: AnalysisParameters | None = None,
    ) -> AnalysisOperationResult:
        """Set up analysis with given parameters."""
        try:
            # Generate analysis ID
            analysis_id = self._generate_analysis_id()

            # Use custom parameters or defaults
            if custom_params:
                params = custom_params
            else:
                default_settings = self._default_solver_settings.get(analysis_type, {})
                params = AnalysisParameters(
                    analysis_type=analysis_type,
                    solver_type=default_settings.get(
                        "solver_type",
                        SolverType.IMPLICIT,
                    ),
                    time_step=default_settings.get("time_step", 0.001),
                    max_time=default_settings.get("max_time", 1.0),
                    convergence_tolerance=default_settings.get(
                        "convergence_tolerance",
                        1e-6,
                    ),
                    max_iterations=default_settings.get("max_iterations", 100),
                    damping_ratio=default_settings.get("damping_ratio", 0.02),
                )

            # Validate model data
            validation_result = self._validate_model_for_analysis(
                model_data,
                analysis_type,
            )
            if not validation_result.success:
                return validation_result

            # Create analysis result object
            analysis_result = AnalysisResult(
                analysis_id=analysis_id,
                analysis_type=analysis_type,
                solver_type=params.solver_type,
                status="pending",
                start_time=datetime.now(timezone.utc),
                results_data={
                    "model_data": model_data,
                    "parameters": {
                        "time_step": params.time_step,
                        "max_time": params.max_time,
                        "convergence_tolerance": params.convergence_tolerance,
                        "max_iterations": params.max_iterations,
                        "damping_ratio": params.damping_ratio,
                        "additional_params": params.additional_params,
                    },
                },
            )

            # Store analysis
            self._active_analyses[analysis_id] = analysis_result

            logger.info(f"Analysis {analysis_id} setup for {analysis_type.value}")
            return AnalysisOperationResult(
                success=True,
                message=f"{analysis_type.value.capitalize()} analysis setup successfully",
                analysis_id=analysis_id,
                result_data=analysis_result,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to setup analysis")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to setup analysis: {e!s}",
                error_code="SETUP_ERROR",
            )

    def run_analysis(self, analysis_id: str) -> AnalysisOperationResult:
        """Run the specified analysis."""
        try:
            # Check if analysis exists
            if analysis_id not in self._active_analyses:
                return AnalysisOperationResult(
                    success=False,
                    message=f"Analysis {analysis_id} not found",
                    error_code="ANALYSIS_NOT_FOUND",
                )

            analysis = self._active_analyses[analysis_id]

            # Check if already running
            if analysis.status == "running":
                return AnalysisOperationResult(
                    success=False,
                    message="Analysis is already running",
                    error_code="ALREADY_RUNNING",
                )

            # Update status
            analysis.status = "running"
            analysis.start_time = datetime.now(timezone.utc)

            # In a real implementation, this would:
            # 1. Prepare input files for the solver
            # 2. Launch the solver process
            # 3. Monitor progress
            # 4. Collect results

            # For demonstration, simulate analysis completion
            self._simulate_analysis_completion(analysis_id)

            logger.info(f"Started analysis {analysis_id}")
            return AnalysisOperationResult(
                success=True,
                message="Analysis started successfully",
                analysis_id=analysis_id,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception(f"Failed to run analysis {analysis_id}")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to run analysis: {e!s}",
                error_code="RUN_ERROR",
            )

    def get_analysis_status(self, analysis_id: str) -> AnalysisOperationResult:
        """Get status of specified analysis."""
        try:
            # Check active analyses first
            if analysis_id in self._active_analyses:
                analysis = self._active_analyses[analysis_id]
                return AnalysisOperationResult(
                    success=True,
                    message=f"Analysis status: {analysis.status}",
                    analysis_id=analysis_id,
                    result_data={
                        "status": analysis.status,
                        "analysis_type": analysis.analysis_type.value,
                        "solver_type": analysis.solver_type.value,
                        "start_time": analysis.start_time.isoformat(),
                        "end_time": analysis.end_time.isoformat() if analysis.end_time else None,
                        "progress": self._estimate_progress(analysis),
                    },
                )

            # Check completed analyses
            completed_analysis = next(
                (a for a in self._completed_analyses if a.analysis_id == analysis_id),
                None,
            )

            if completed_analysis:
                return AnalysisOperationResult(
                    success=True,
                    message="Analysis completed",
                    analysis_id=analysis_id,
                    result_data={
                        "status": completed_analysis.status,
                        "analysis_type": completed_analysis.analysis_type.value,
                        "results_summary": self._summarize_results(
                            completed_analysis.results_data,
                        ),
                    },
                )

            return AnalysisOperationResult(
                success=False,
                message=f"Analysis {analysis_id} not found",
                error_code="NOT_FOUND",
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to get analysis status")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to get analysis status: {e!s}",
                error_code="STATUS_ERROR",
            )

    def get_analysis_results(self, analysis_id: str) -> AnalysisOperationResult:
        """Get results of completed analysis."""
        try:
            # Check completed analyses
            analysis = next(
                (a for a in self._completed_analyses if a.analysis_id == analysis_id),
                None,
            )

            if not analysis:
                return AnalysisOperationResult(
                    success=False,
                    message=f"Analysis {analysis_id} not found or not completed",
                    error_code="RESULTS_NOT_AVAILABLE",
                )

            if analysis.status != "completed":
                return AnalysisOperationResult(
                    success=False,
                    message=f"Analysis {analysis_id} is not completed yet",
                    error_code="NOT_COMPLETED",
                )

            logger.info(f"Retrieved results for analysis {analysis_id}")
            return AnalysisOperationResult(
                success=True,
                message="Analysis results retrieved successfully",
                analysis_id=analysis_id,
                result_data=analysis.results_data,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to get analysis results")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to get analysis results: {e!s}",
                error_code="RESULTS_ERROR",
            )

    def cancel_analysis(self, analysis_id: str) -> AnalysisOperationResult:
        """Cancel running analysis."""
        try:
            if analysis_id not in self._active_analyses:
                return AnalysisOperationResult(
                    success=False,
                    message=f"Analysis {analysis_id} not found",
                    error_code="NOT_FOUND",
                )

            analysis = self._active_analyses[analysis_id]

            if analysis.status != "running":
                return AnalysisOperationResult(
                    success=False,
                    message=f"Analysis {analysis_id} is not running",
                    error_code="NOT_RUNNING",
                )

            # Update status
            analysis.status = "cancelled"
            analysis.end_time = datetime.now(timezone.utc)

            logger.info(f"Cancelled analysis {analysis_id}")
            return AnalysisOperationResult(
                success=True,
                message="Analysis cancelled successfully",
                analysis_id=analysis_id,
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to cancel analysis")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to cancel analysis: {e!s}",
                error_code="CANCEL_ERROR",
            )

    def get_active_analyses(self) -> list[dict[str, Any]]:
        """Get list of active analyses."""
        return [
            {
                "analysis_id": analysis.analysis_id,
                "analysis_type": analysis.analysis_type.value,
                "solver_type": analysis.solver_type.value,
                "status": analysis.status,
                "start_time": analysis.start_time.isoformat(),
            }
            for analysis in self._active_analyses.values()
        ]

    def get_completed_analyses(self, limit: int = 10) -> list[dict[str, Any]]:
        """Get list of completed analyses."""
        completed = self._completed_analyses[-limit:]  # Get most recent
        return [
            {
                "analysis_id": analysis.analysis_id,
                "analysis_type": analysis.analysis_type.value,
                "solver_type": analysis.solver_type.value,
                "status": analysis.status,
                "start_time": analysis.start_time.isoformat(),
                "end_time": analysis.end_time.isoformat() if analysis.end_time else None,
                "results_summary": self._summarize_results(analysis.results_data),
            }
            for analysis in completed
        ]

    def clear_completed_analyses(
        self,
        older_than_days: int = 30,
    ) -> AnalysisOperationResult:
        """Clear old completed analyses."""
        try:
            from datetime import timedelta

            cutoff_date = datetime.now(timezone.utc) - timedelta(days=older_than_days)

            initial_count = len(self._completed_analyses)
            self._completed_analyses = [
                analysis
                for analysis in self._completed_analyses
                if analysis.end_time and analysis.end_time > cutoff_date
            ]

            removed_count = initial_count - len(self._completed_analyses)
            logger.info(f"Cleared {removed_count} old analyses")

            return AnalysisOperationResult(
                success=True,
                message=f"Cleared {removed_count} old analyses",
            )

        except (OSError, RuntimeError, ValueError) as e:
            logger.exception("Failed to clear completed analyses")
            return AnalysisOperationResult(
                success=False,
                message=f"Failed to clear analyses: {e!s}",
                error_code="CLEAR_ERROR",
            )

    def _generate_analysis_id(self) -> str:
        """Generate unique analysis ID."""
        import uuid

        return f"ANA_{uuid.uuid4().hex[:8]}"

    def _validate_model_for_analysis(
        self,
        model_data: dict[str, Any],
        analysis_type: AnalysisType,
    ) -> AnalysisOperationResult:
        """Validate that model is suitable for specified analysis type."""
        required_elements = ["geometry", "materials", "mesh"]

        # Check basic model completeness
        missing_elements = [
            element for element in required_elements if element not in model_data or not model_data[element]
        ]

        if missing_elements:
            return AnalysisOperationResult(
                success=False,
                message=f"Incomplete model: missing {', '.join(missing_elements)}",
                error_code="INCOMPLETE_MODEL",
            )

        # Specific validations for analysis types
        if analysis_type == AnalysisType.STATIC:
            if "boundary_conditions" not in model_data or not model_data["boundary_conditions"]:
                return AnalysisOperationResult(
                    success=False,
                    message="Static analysis requires boundary conditions",
                    error_code="MISSING_BOUNDARY_CONDITIONS",
                )

        elif analysis_type == AnalysisType.DYNAMIC and ("loads" not in model_data or not model_data["loads"]):
            return AnalysisOperationResult(
                success=False,
                message="Dynamic analysis requires loads",
                error_code="MISSING_LOADS",
            )

        return AnalysisOperationResult(success=True, message="Model validation passed")

    def _simulate_analysis_completion(self, analysis_id: str) -> None:
        """Simulate analysis completion (for demonstration)."""
        import threading
        import time

        def complete_analysis() -> None:
            time.sleep(2)  # Simulate computation time

            if analysis_id in self._active_analyses:
                analysis = self._active_analyses[analysis_id]
                analysis.status = "completed"
                analysis.end_time = datetime.now(timezone.utc)

                # Generate sample results
                analysis.results_data.update(
                    {
                        "displacements": [0.001, 0.002, 0.0015],
                        "stresses": [150e6, 120e6, 180e6],
                        "strains": [0.00075, 0.0006, 0.0009],
                        "reaction_forces": [5000, 3000, 2000],
                    },
                )

                # Move to completed analyses
                self._completed_analyses.append(analysis)
                del self._active_analyses[analysis_id]

                # Maintain storage limit
                if len(self._completed_analyses) > self._max_completed_storage:
                    self._completed_analyses.pop(0)

        # Run in separate thread to simulate asynchronous execution
        thread = threading.Thread(target=complete_analysis)
        thread.daemon = True
        thread.start()

    def _estimate_progress(self, analysis: AnalysisResult) -> float:
        """Estimate analysis progress (0.0 to 1.0)."""
        if analysis.status == "completed":
            return 1.0
        if analysis.status == "running":
            # Simple time-based estimation
            elapsed = (datetime.now(timezone.utc) - analysis.start_time).total_seconds()
            estimated_duration = 5.0  # Assume 5 seconds for demo
            return min(elapsed / estimated_duration, 0.95)
        return 0.0

    def _summarize_results(self, results_data: dict[str, Any]) -> dict[str, Any]:
        """Create summary of analysis results."""
        summary = {}

        if "displacements" in results_data:
            disp = results_data["displacements"]
            summary["max_displacement"] = max(disp) if disp else 0
            summary["min_displacement"] = min(disp) if disp else 0

        if "stresses" in results_data:
            stresses = results_data["stresses"]
            summary["max_stress"] = max(stresses) if stresses else 0
            summary["stress_units"] = "Pa"

        if "reaction_forces" in results_data:
            forces = results_data["reaction_forces"]
            summary["total_reaction_force"] = sum(forces) if forces else 0
            summary["force_units"] = "N"

        return summary
