"""
Registry system for chunking strategies.

This module provides a decorator-based registry system that allows chunking
strategies to register themselves with metadata including capabilities,
dependencies, performance characteristics, and usage information.

The registry system enables:
- Automatic discovery of chunking strategies
- Metadata-driven strategy selection
- Dependency management and validation
- Performance-based recommendations
- Plugin architecture for extensibility

Key Components:
- ChunkerRegistry: Global registry for managing strategies
- ChunkerMetadata: Comprehensive metadata for strategies
- register_chunker: Decorator for strategy registration
- ComplexityLevel, SpeedLevel, MemoryUsage: Performance classifications

Architecture:
The registry follows a plugin pattern where:
1. Strategies register themselves using the @register_chunker decorator
2. Metadata includes performance characteristics and dependencies
3. The registry provides discovery and recommendation services
4. Strategies can be filtered by category, complexity, and capabilities

Example Usage:
    from chunking_strategy.core.registry import register_chunker, ComplexityLevel
    
    @register_chunker(
        name="my_chunker",
        category="text",
        complexity=ComplexityLevel.LOW,
        dependencies=["nltk"]
    )
    class MyChunker(BaseChunker):
        def chunk(self, content, **kwargs):
            # Implementation here
            pass
"""

import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Type, Callable, Union
from enum import Enum
import importlib

from chunking_strategy.exceptions import ChunkerNotFoundError, ChunkingConfigurationError, StrategyUnavailableError

# Handle pkg_resources deprecation gracefully
pkg_resources = None
importlib_metadata = None

# Prefer importlib.metadata over deprecated pkg_resources
try:
    from importlib import metadata as importlib_metadata
except ImportError:
    try:
        import importlib_metadata
    except ImportError:
        # Only fall back to pkg_resources if absolutely necessary
        try:
            import warnings
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", DeprecationWarning)
                warnings.simplefilter("ignore", UserWarning)
                import pkg_resources
        except ImportError:
            pkg_resources = None

from chunking_strategy.core.base import BaseChunker, ModalityType

logger = logging.getLogger(__name__)


class ComplexityLevel(Enum):
    """Complexity levels for chunking strategies."""

    LOW = "low"          # Simple, fast, minimal dependencies
    MEDIUM = "medium"    # Moderate complexity, some dependencies
    HIGH = "high"        # Complex algorithms, heavy dependencies
    VERY_HIGH = "very_high"  # ML/AI models, significant resources


class SpeedLevel(Enum):
    """Performance speed classifications."""

    VERY_FAST = "very_fast"    # < 1ms per chunk
    FAST = "fast"              # 1-10ms per chunk
    MEDIUM = "medium"          # 10-100ms per chunk
    SLOW = "slow"              # 100ms-1s per chunk
    VERY_SLOW = "very_slow"    # > 1s per chunk


class MemoryUsage(Enum):
    """Memory usage classifications."""

    VERY_LOW = "very_low"      # < 10MB
    LOW = "low"                # 10-100MB
    MEDIUM = "medium"          # 100MB-1GB
    HIGH = "high"              # 1-10GB
    VERY_HIGH = "very_high"    # > 10GB


@dataclass
class ChunkerMetadata:
    """
    Comprehensive metadata for chunking strategies.

    This metadata helps users select appropriate strategies and understand
    their characteristics, dependencies, and performance implications.

    Example:
        ```python
        @register_chunker(
            name="semantic_chunking",
            category="text",
            complexity=ComplexityLevel.HIGH,
            supported_formats=["txt", "pdf", "docx"],
            dependencies=["sentence-transformers", "torch"],
            speed=SpeedLevel.SLOW,
            memory=MemoryUsage.HIGH,
            quality=0.9,
            parameters_schema={
                "model_name": {"type": "string", "default": "all-MiniLM-L6-v2"},
                "similarity_threshold": {"type": "number", "default": 0.7}
            },
            use_cases=["RAG", "semantic search", "content analysis"]
        )
        class SemanticChunker(BaseChunker):
            pass
        ```
    """

    # Basic information
    name: str
    category: str  # text, multimedia, document, general, etc.
    description: str = ""
    version: str = "1.0.0"

    # Capabilities
    supported_modalities: List[ModalityType] = field(default_factory=lambda: [ModalityType.TEXT])
    supported_formats: List[str] = field(default_factory=list)  # File extensions

    # Complexity and dependencies
    complexity: ComplexityLevel = ComplexityLevel.LOW
    dependencies: List[str] = field(default_factory=list)  # Required packages
    optional_dependencies: List[str] = field(default_factory=list)

    # Performance characteristics
    speed: SpeedLevel = SpeedLevel.MEDIUM
    memory: MemoryUsage = MemoryUsage.LOW
    quality: float = 0.5  # Quality score 0.0-1.0

    # Configuration
    parameters_schema: Dict[str, Any] = field(default_factory=dict)
    default_parameters: Dict[str, Any] = field(default_factory=dict)

    # Usage information
    use_cases: List[str] = field(default_factory=list)
    best_for: List[str] = field(default_factory=list)  # Specific scenarios
    limitations: List[str] = field(default_factory=list)

    # Technical details
    streaming_support: bool = False
    adaptive_support: bool = False
    hierarchical_support: bool = False

    # Maintenance info
    author: str = ""
    maintainer: str = ""
    license: str = "MIT"
    docs_url: str = ""

    def to_dict(self) -> Dict[str, Any]:
        """Convert metadata to dictionary format."""
        result = {}
        for key, value in self.__dict__.items():
            if isinstance(value, Enum):
                result[key] = value.value
            elif isinstance(value, list) and value and isinstance(value[0], Enum):
                result[key] = [item.value for item in value]
            else:
                result[key] = value
        return result


class ChunkerRegistry:
    """
    Global registry for chunking strategies.

    Manages registration, discovery, and instantiation of chunking strategies
    with comprehensive metadata support.
    """

    def __init__(self):
        """Initialize empty registry."""
        self._strategies: Dict[str, Type[BaseChunker]] = {}
        self._metadata: Dict[str, ChunkerMetadata] = {}
        self._categories: Dict[str, List[str]] = {}

    def register(
        self,
        chunker_class: Type[BaseChunker],
        metadata: ChunkerMetadata,
        allow_override: bool = False
    ) -> None:
        """
        Register a chunker class with metadata.

        Args:
            chunker_class: The chunker class to register
            metadata: Associated metadata
            allow_override: Allow overriding existing chunkers

        Raises:
            ValueError: If chunker name already exists and override not allowed
        """
        name = metadata.name

        if name in self._strategies:
            if allow_override:
                logger.warning(f"Overriding existing chunker: {name}")
            else:
                logger.warning(f"Chunker already registered: {name}, use allow_override=True to override")

        self._strategies[name] = chunker_class
        self._metadata[name] = metadata

        # Update category index
        category = metadata.category
        if category not in self._categories:
            self._categories[category] = []
        if name not in self._categories[category]:
            self._categories[category].append(name)

        logger.debug(f"Registered chunker: {name} (category: {category})")

    def unregister(self, name: str) -> bool:
        """
        Unregister a chunker.

        Args:
            name: Name of the chunker to unregister

        Returns:
            True if successfully unregistered, False if not found
        """
        if name not in self._strategies:
            logger.warning(f"Chunker not found for unregistration: {name}")
            return False

        # Get metadata for category cleanup
        metadata = self._metadata.get(name)

        # Remove from strategies and metadata
        del self._strategies[name]
        del self._metadata[name]

        # Clean up category index
        if metadata:
            category = metadata.category
            if category in self._categories and name in self._categories[category]:
                self._categories[category].remove(name)
                # Remove empty category
                if not self._categories[category]:
                    del self._categories[category]

        logger.debug(f"Unregistered chunker: {name}")
        return True

    def is_registered(self, name: str) -> bool:
        """
        Check if a chunker is registered.

        Args:
            name: Name of the chunker to check

        Returns:
            True if registered, False otherwise
        """
        return name in self._strategies

    def get_registered_names(self) -> List[str]:
        """Get list of all registered chunker names."""
        return list(self._strategies.keys())

    def clear_custom_algorithms(self, category_filter: str = "custom") -> int:
        """
        Clear all custom algorithms (useful for testing and cleanup).

        Args:
            category_filter: Category to filter by (default: "custom")

        Returns:
            Number of algorithms removed
        """
        to_remove = []

        for name, metadata in self._metadata.items():
            if metadata.category == category_filter:
                to_remove.append(name)

        removed_count = 0
        for name in to_remove:
            if self.unregister(name):
                removed_count += 1

        logger.info(f"Cleared {removed_count} custom algorithms")
        return removed_count

    def get(self, name: str) -> Optional[Type[BaseChunker]]:
        """
        Get a chunker class by name.

        Args:
            name: Name of the chunker

        Returns:
            Chunker class or None if not found
        """
        return self._strategies.get(name)

    def get_metadata(self, name: str) -> Optional[ChunkerMetadata]:
        """
        Get metadata for a chunker.

        Args:
            name: Name of the chunker

        Returns:
            Chunker metadata or None if not found
        """
        return self._metadata.get(name)

    def list_chunkers(
        self,
        category: Optional[str] = None,
        modality: Optional[ModalityType] = None,
        complexity: Optional[ComplexityLevel] = None,
        dependencies_available: bool = True
    ) -> List[str]:
        """
        List available chunkers with optional filtering.

        Args:
            category: Filter by category
            modality: Filter by supported modality
            complexity: Filter by complexity level
            dependencies_available: Only include chunkers with available dependencies

        Returns:
            List of chunker names matching criteria
        """
        candidates = list(self._strategies.keys())

        if category:
            candidates = [name for name in candidates
                         if self._metadata[name].category == category]

        if modality:
            candidates = [name for name in candidates
                         if modality in self._metadata[name].supported_modalities]

        if complexity:
            candidates = [name for name in candidates
                         if self._metadata[name].complexity == complexity]

        if dependencies_available:
            candidates = [name for name in candidates
                         if self._check_dependencies(name)]

        return sorted(candidates)

    def list_categories(self) -> List[str]:
        """List all available categories."""
        return sorted(self._categories.keys())

    def get_category_chunkers(self, category: str) -> List[str]:
        """Get all chunkers in a specific category."""
        return self._categories.get(category, [])

    def create_chunker(
        self,
        name: str,
        **kwargs
    ) -> 'BaseChunker':
        """
        Create an instance of a chunker.

        Args:
            name: Name of the chunker
            **kwargs: Parameters to pass to chunker constructor

        Returns:
            Chunker instance

        Raises:
            ChunkerNotFoundError: If the chunker strategy is not found
            ChunkingConfigurationError: If the chunker configuration is invalid
            StrategyUnavailableError: If the chunker dependencies are not available
        """
        # NOTE: Removed automatic strategy loading to support true lazy loading
        # Strategies are now loaded on-demand by the main package when needed

        chunker_class = self.get(name)
        if not chunker_class:
            error_msg = f"Chunker not found: {name}. Available strategies: {list(self._strategies.keys())}"
            logger.error(error_msg)
            raise ChunkerNotFoundError(error_msg)

        try:
            metadata = self._metadata[name]

            # Merge default parameters with provided ones
            params = metadata.default_parameters.copy()
            params.update(kwargs)

            # Only add name parameter if not already explicitly handled by chunker class
            # Built-in chunkers handle name in their __init__, custom ones may need it injected
            try:
                # First try without injecting name (for built-in chunkers)
                return chunker_class(**params)
            except TypeError as e:
                if "missing 1 required positional argument: 'name'" in str(e):
                    # Custom chunker needs name parameter injected
                    params['name'] = name
                    return chunker_class(**params)
                else:
                    # Some other TypeError - likely invalid configuration
                    error_msg = f"Invalid configuration for chunker {name}: {e}"
                    logger.error(error_msg)
                    raise ChunkingConfigurationError(error_msg) from e

        except ValueError as e:
            # Re-raise validation errors (like invalid parameter values)
            error_msg = f"Invalid parameters for chunker {name}: {e}"
            logger.error(error_msg)
            raise ChunkingConfigurationError(error_msg) from e
        except ImportError as e:
            # Missing dependencies
            error_msg = f"Strategy {name} is not available due to missing dependencies: {e}"
            logger.error(error_msg)
            raise StrategyUnavailableError(error_msg) from e
        except Exception as e:
            # Other errors - convert to StrategyUnavailableError for consistency
            error_msg = f"Failed to create chunker {name}: {e}"
            logger.error(error_msg)
            raise StrategyUnavailableError(error_msg) from e

    def check_dependencies(self, name: str) -> Dict[str, bool]:
        """
        Check availability of dependencies for a chunker.

        Args:
            name: Name of the chunker

        Returns:
            Dictionary mapping dependency names to availability status
        """
        metadata = self._metadata.get(name)
        if not metadata:
            return {}

        results = {}

        for dep in metadata.dependencies:
            results[dep] = self._is_package_available(dep)

        for dep in metadata.optional_dependencies:
            results[f"{dep} (optional)"] = self._is_package_available(dep)

        return results

    def _check_dependencies(self, name: str) -> bool:
        """Check if all required dependencies are available."""
        metadata = self._metadata.get(name)
        if not metadata:
            return False

        for dep in metadata.dependencies:
            if not self._is_package_available(dep):
                return False

        return True

    def _is_package_available(self, package_name: str) -> bool:
        """Check if a package is available for import."""
        try:
            importlib.import_module(package_name)
            return True
        except ImportError:
            try:
                # Prefer importlib.metadata over deprecated pkg_resources
                if importlib_metadata is not None:
                    importlib_metadata.distribution(package_name)
                    return True
                elif pkg_resources is not None:
                    pkg_resources.get_distribution(package_name)
                    return True
            except Exception:
                return False

        return False

    def get_recommendations(
        self,
        modality: ModalityType,
        use_case: str,
        performance_priority: str = "balanced"  # speed, memory, quality, balanced
    ) -> List[str]:
        """
        Get recommended chunkers for specific requirements.

        Args:
            modality: Required modality support
            use_case: Target use case
            performance_priority: Performance optimization preference

        Returns:
            List of recommended chunker names in priority order
        """
        candidates = self.list_chunkers(modality=modality)

        # Filter by use case
        relevant = []
        for name in candidates:
            metadata = self._metadata[name]
            if (use_case.lower() in [uc.lower() for uc in metadata.use_cases] or
                use_case.lower() in [bf.lower() for bf in metadata.best_for]):
                relevant.append(name)

        if not relevant:
            relevant = candidates  # Fallback to all compatible

        # Sort by performance priority
        def score_chunker(name: str) -> float:
            metadata = self._metadata[name]

            if performance_priority == "speed":
                speed_scores = {
                    SpeedLevel.VERY_FAST: 1.0,
                    SpeedLevel.FAST: 0.8,
                    SpeedLevel.MEDIUM: 0.6,
                    SpeedLevel.SLOW: 0.4,
                    SpeedLevel.VERY_SLOW: 0.2
                }
                return speed_scores.get(metadata.speed, 0.5)

            elif performance_priority == "memory":
                memory_scores = {
                    MemoryUsage.VERY_LOW: 1.0,
                    MemoryUsage.LOW: 0.8,
                    MemoryUsage.MEDIUM: 0.6,
                    MemoryUsage.HIGH: 0.4,
                    MemoryUsage.VERY_HIGH: 0.2
                }
                return memory_scores.get(metadata.memory, 0.5)

            elif performance_priority == "quality":
                return metadata.quality

            else:  # balanced
                speed_score = score_chunker(name) if performance_priority != "speed" else 0.8
                memory_score = score_chunker(name) if performance_priority != "memory" else 0.8
                quality_score = metadata.quality
                return (speed_score + memory_score + quality_score) / 3.0

        return sorted(relevant, key=score_chunker, reverse=True)

    def export_registry(self) -> Dict[str, Any]:
        """Export complete registry data for serialization."""
        return {
            "strategies": list(self._strategies.keys()),
            "metadata": {name: meta.to_dict() for name, meta in self._metadata.items()},
            "categories": self._categories
        }


# Global registry instance
_global_registry = ChunkerRegistry()


def register_chunker(
    name: str,
    category: str = "general",
    description: str = "",
    supported_modalities: Optional[List[ModalityType]] = None,
    supported_formats: Optional[List[str]] = None,
    complexity: ComplexityLevel = ComplexityLevel.LOW,
    dependencies: Optional[List[str]] = None,
    optional_dependencies: Optional[List[str]] = None,
    speed: SpeedLevel = SpeedLevel.MEDIUM,
    memory: MemoryUsage = MemoryUsage.LOW,
    quality: float = 0.5,
    parameters_schema: Optional[Dict[str, Any]] = None,
    default_parameters: Optional[Dict[str, Any]] = None,
    use_cases: Optional[List[str]] = None,
    best_for: Optional[List[str]] = None,
    limitations: Optional[List[str]] = None,
    streaming_support: bool = False,
    adaptive_support: bool = False,
    hierarchical_support: bool = False,
    **kwargs
) -> Callable[[Type[BaseChunker]], Type[BaseChunker]]:
    """
    Decorator to register a chunker with metadata.

    Example:
        ```python
        @register_chunker(
            name="semantic_chunking",
            category="text",
            complexity=ComplexityLevel.HIGH,
            dependencies=["sentence-transformers"],
            quality=0.9,
            use_cases=["RAG", "semantic search"]
        )
        class SemanticChunker(BaseChunker):
            def chunk(self, content, **kwargs):
                # Implementation here
                pass
        ```

    Args:
        name: Unique name for the chunker
        category: Category classification
        description: Human-readable description
        supported_modalities: List of supported modality types
        supported_formats: List of supported file formats
        complexity: Algorithmic complexity level
        dependencies: Required package dependencies
        optional_dependencies: Optional package dependencies
        speed: Performance speed classification
        memory: Memory usage classification
        quality: Quality score (0.0 to 1.0)
        parameters_schema: JSON schema for parameters
        default_parameters: Default parameter values
        use_cases: List of primary use cases
        best_for: List of specific scenarios this chunker excels at
        limitations: List of known limitations
        streaming_support: Whether chunker supports streaming
        adaptive_support: Whether chunker supports adaptation
        hierarchical_support: Whether chunker supports hierarchical chunking
        **kwargs: Additional metadata fields

    Returns:
        Decorator function
    """
    def decorator(chunker_class: Type[BaseChunker]) -> Type[BaseChunker]:
        metadata = ChunkerMetadata(
            name=name,
            category=category,
            description=description,
            supported_modalities=supported_modalities or [ModalityType.TEXT],
            supported_formats=supported_formats or [],
            complexity=complexity,
            dependencies=dependencies or [],
            optional_dependencies=optional_dependencies or [],
            speed=speed,
            memory=memory,
            quality=quality,
            parameters_schema=parameters_schema or {},
            default_parameters=default_parameters or {},
            use_cases=use_cases or [],
            best_for=best_for or [],
            limitations=limitations or [],
            streaming_support=streaming_support,
            adaptive_support=adaptive_support,
            hierarchical_support=hierarchical_support,
            **kwargs
        )

        _global_registry.register(chunker_class, metadata)
        return chunker_class

    return decorator


def get_chunker(name: str) -> Optional[Type[BaseChunker]]:
    """Get a chunker class by name."""
    return _global_registry.get(name)


def list_chunkers(**kwargs) -> List[str]:
    """List available chunkers with optional filtering."""
    # Trigger lazy loading of strategies if not already loaded
    try:
        from chunking_strategy import _ensure_strategies_loaded
        _ensure_strategies_loaded()
    except ImportError:
        # If we can't import from main package, directly load heavy strategies
        try:
            import chunking_strategy.strategies.text.semantic_chunker  # noqa: F401
            import chunking_strategy.strategies.text.embedding_based_chunker  # noqa: F401
        except ImportError:
            pass  # Heavy strategies have optional dependencies

    return _global_registry.list_chunkers(**kwargs)


def get_chunker_metadata(name: str) -> Optional[ChunkerMetadata]:
    """Get metadata for a chunker."""
    return _global_registry.get_metadata(name)


def create_chunker(name: str, **kwargs) -> Optional[BaseChunker]:
    """Create an instance of a chunker."""
    return _global_registry.create_chunker(name, **kwargs)


def unregister_chunker(name: str) -> bool:
    """Unregister a chunker from the global registry."""
    return _global_registry.unregister(name)


def is_chunker_registered(name: str) -> bool:
    """Check if a chunker is registered in the global registry."""
    return _global_registry.is_registered(name)


def clear_custom_algorithms(category_filter: str = "custom") -> int:
    """Clear custom algorithms from the global registry."""
    return _global_registry.clear_custom_algorithms(category_filter)


def get_registry() -> ChunkerRegistry:
    """Get the global registry instance."""
    return _global_registry
