#!/usr/bin/env python3
"""
CalculationLoader - Dynamic calculation loading system for BlackAnt SDK.

This module provides thread-safe, singleton-based dynamic loading of calculation
implementations. Supports environment variable configuration and automatic fallback.

Designed for production use with Gunicorn multi-worker environments (8 workers).
"""

import os
import threading
import importlib
from typing import Type, Dict, Optional

from calculation.base import CalculationBase
from blackant.utils.logging import get_logger


class CalculationLoader:
    """
    Thread-safe singleton loader for dynamic calculation instantiation.

    This loader supports:
    - Environment variable configuration (CALCULATION_MODULE, CALCULATION_CLASS)
    - Thread-safe caching for multi-worker environments
    - Automatic fallback to MyCalculation
    - Type validation (must inherit from CalculationBase)

    Thread Safety:
        Designed for Gunicorn with --workers=8 configuration.
        Uses threading.Lock for cache synchronization across threads.

    Usage:
        >>> loader = CalculationLoader()
        >>> CalculationClass = loader.get_calculation()
        >>> calc = CalculationClass(logger, **kwargs)
    """

    # Class-level singleton instance
    _instance: Optional['CalculationLoader'] = None
    _instance_lock = threading.Lock()

    def __new__(cls) -> 'CalculationLoader':
        """
        Create or return singleton instance (thread-safe).

        Returns:
            CalculationLoader: Singleton instance
        """
        if not cls._instance:
            with cls._instance_lock:
                # Double-check locking pattern for thread safety
                if not cls._instance:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance

    def _initialize(self) -> None:
        """Initialize loader state (called once by __new__)."""
        self._cache: Dict[str, Type[CalculationBase]] = {}
        self._cache_lock = threading.Lock()
        self._logger = get_logger("CalculationLoader")

        self._logger.info("CalculationLoader singleton initialized")

    def get_calculation(self) -> Type[CalculationBase]:
        """
        Get calculation class based on environment configuration.

        Environment Variables:
            CALCULATION_MODULE: Python module path (default: calculation.impl.my_calculation)
            CALCULATION_CLASS: Class name (default: MyCalculation)

        Returns:
            Type[CalculationBase]: Calculation class ready for instantiation

        Raises:
            TypeError: If loaded class does not inherit from CalculationBase

        Example:
            >>> os.environ['CALCULATION_MODULE'] = 'calculation.impl.simple_calc'
            >>> os.environ['CALCULATION_CLASS'] = 'SimpleCalculation'
            >>> loader = CalculationLoader()
            >>> CalcClass = loader.get_calculation()
            >>> calc = CalcClass(logger)
        """
        # Get configuration from environment
        module_path = os.getenv('CALCULATION_MODULE', 'calculation.impl.my_calculation')
        class_name = os.getenv('CALCULATION_CLASS', 'MyCalculation')

        cache_key = f"{module_path}.{class_name}"

        # Check cache first (thread-safe read)
        with self._cache_lock:
            if cache_key in self._cache:
                self._logger.debug(f"Cache hit: {cache_key}")
                return self._cache[cache_key]

        # Cache miss - load calculation
        self._logger.info(f"Loading calculation: {module_path}.{class_name}")

        try:
            calc_class = self._load_calculation(module_path, class_name)

            # Cache the result (thread-safe write)
            with self._cache_lock:
                self._cache[cache_key] = calc_class

            self._logger.info(f"Successfully loaded and cached: {cache_key}")
            return calc_class

        except Exception as exc:
            self._logger.error(f"Failed to load {cache_key}: {exc}")
            self._logger.warning("Falling back to default MyCalculation")

            # Fallback to default MyCalculation
            return self._load_fallback()

    def _load_calculation(self, module_path: str, class_name: str) -> Type[CalculationBase]:
        """
        Load calculation class from module path.

        Args:
            module_path: Python module path (e.g., 'calculation.impl.my_calculation')
            class_name: Class name (e.g., 'MyCalculation')

        Returns:
            Type[CalculationBase]: Loaded calculation class

        Raises:
            ImportError: If module cannot be imported
            AttributeError: If class does not exist in module
            TypeError: If class is not a CalculationBase subclass
        """
        # Import module
        try:
            module = importlib.import_module(module_path)
        except ImportError as exc:
            self._logger.error(f"Cannot import module '{module_path}': {exc}")
            raise

        # Get class from module
        try:
            calc_class = getattr(module, class_name)
        except AttributeError as exc:
            self._logger.error(f"Class '{class_name}' not found in '{module_path}': {exc}")
            raise

        # Validate it's a CalculationBase subclass
        if not isinstance(calc_class, type):
            raise TypeError(f"{class_name} is not a class (got {type(calc_class).__name__})")

        if not issubclass(calc_class, CalculationBase):
            raise TypeError(
                f"{class_name} must inherit from CalculationBase "
                f"(current bases: {[b.__name__ for b in calc_class.__bases__]})"
            )

        self._logger.debug(f"Validated: {class_name} is a valid CalculationBase subclass")
        return calc_class

    def _load_fallback(self) -> Type[CalculationBase]:
        """
        Load fallback calculation (MyCalculation).

        Returns:
            Type[CalculationBase]: MyCalculation class

        Raises:
            ImportError: If even fallback cannot be loaded (critical error)
        """
        fallback_module = 'calculation.impl.my_calculation'
        fallback_class = 'MyCalculation'

        cache_key = f"{fallback_module}.{fallback_class}"

        # Check if fallback is already cached
        with self._cache_lock:
            if cache_key in self._cache:
                return self._cache[cache_key]

        try:
            from calculation.impl.my_calculation import MyCalculation

            # Cache fallback
            with self._cache_lock:
                self._cache[cache_key] = MyCalculation

            self._logger.info("Fallback MyCalculation loaded successfully")
            return MyCalculation

        except ImportError as exc:
            self._logger.critical(f"CRITICAL: Cannot load fallback MyCalculation: {exc}")
            raise ImportError(
                "CalculationLoader failed: cannot load configured calculation "
                "and fallback MyCalculation is also unavailable. "
                "Ensure calculation/impl/my_calculation.py exists."
            ) from exc

    def clear_cache(self) -> None:
        """
        Clear calculation cache (useful for testing or hot-reload).

        Thread-safe operation.
        """
        with self._cache_lock:
            cache_size = len(self._cache)
            self._cache.clear()
            self._logger.info(f"Cache cleared ({cache_size} entries removed)")

    def get_cache_info(self) -> Dict[str, int]:
        """
        Get cache statistics.

        Returns:
            Dict[str, int]: Cache information
                {
                    'size': Number of cached calculations,
                    'entries': List of cached keys
                }
        """
        with self._cache_lock:
            return {
                'size': len(self._cache),
                'entries': list(self._cache.keys())
            }
