"""
Base implementation for dimensionality reduction feature groups.
"""

from __future__ import annotations

from abc import abstractmethod
from typing import Any, Optional

from mloda.provider import FeatureGroup
from mloda.user import Feature
from mloda.provider import CHAIN_SEPARATOR, FeatureChainParser
from mloda.provider import (
    FeatureChainParserMixin,
)
from mloda.provider import FeatureSet
from mloda.user import Options
from mloda.provider import DefaultOptionKeys


class DimensionalityReductionFeatureGroup(FeatureChainParserMixin, FeatureGroup):
    """
    Base class for all dimensionality reduction feature groups.

    Dimensionality reduction feature groups reduce the dimensionality of feature spaces
    using various techniques like PCA, t-SNE, UMAP, etc. They support both string-based
    feature creation and configuration-based creation with proper group/context parameter separation.

    ## Supported Dimensionality Reduction Algorithms

    - `pca`: Principal Component Analysis
    - `tsne`: t-Distributed Stochastic Neighbor Embedding
    - `umap`: Uniform Manifold Approximation and Projection
    - `ica`: Independent Component Analysis
    - `lda`: Linear Discriminant Analysis
    - `isomap`: Isometric Mapping

    ## Feature Creation Methods

    ### 1. String-Based Creation

    Features follow the naming pattern: `{in_features}__{algorithm}_{dimension}d`

    Examples:
    ```python
    features = [
        "customer_metrics__pca_2d",      # PCA reduction to 2 dimensions
        "product_features__tsne_3d",     # t-SNE reduction to 3 dimensions
        "sensor_readings__umap_10d"      # UMAP reduction to 10 dimensions
    ]
    ```

    ### 2. Configuration-Based Creation

    Uses Options with proper group/context parameter separation:

    ```python
    feature = Feature(
        name="placeholder",  # Placeholder name, will be replaced
        options=Options(
            context={
                DimensionalityReductionFeatureGroup.ALGORITHM: "pca",
                DimensionalityReductionFeatureGroup.DIMENSION: 2,
                DefaultOptionKeys.in_features: "customer_metrics",
            }
        )
    )
    ```

    ## Result Columns

    The dimensionality reduction results are stored using the multiple result columns pattern.
    For each dimension in the reduced space, a column is created with the naming convention:
    `{feature_name}~dim{i+1}`

    ## Parameter Classification

    ### Context Parameters (Default)
    These parameters don't affect Feature Group resolution/splitting:
    - `algorithm`: The dimensionality reduction algorithm to use
    - `dimension`: Target dimension for the reduction
    - `in_features`: Source features to reduce

    ### Group Parameters
    Currently none for DimensionalityReductionFeatureGroup. Parameters that affect Feature Group
    resolution/splitting would be placed here.

    ## Requirements
    - The input data must contain the source features to be used for dimensionality reduction
    - The dimension parameter must be a positive integer less than the number of source features
    """

    # Option keys for dimensionality reduction configuration
    ALGORITHM = "algorithm"
    DIMENSION = "dimension"

    # Algorithm-specific option keys
    TSNE_MAX_ITER = "tsne_max_iter"
    TSNE_N_ITER_WITHOUT_PROGRESS = "tsne_n_iter_without_progress"
    TSNE_METHOD = "tsne_method"
    PCA_SVD_SOLVER = "pca_svd_solver"
    ICA_MAX_ITER = "ica_max_iter"
    ISOMAP_N_NEIGHBORS = "isomap_n_neighbors"

    # Define supported dimensionality reduction algorithms
    REDUCTION_ALGORITHMS = {
        "pca": "Principal Component Analysis",
        "tsne": "t-Distributed Stochastic Neighbor Embedding",
        "umap": "Uniform Manifold Approximation and Projection",
        "ica": "Independent Component Analysis",
        "lda": "Linear Discriminant Analysis",
        "isomap": "Isometric Mapping",
    }

    # Define the prefix pattern for this feature group
    PREFIX_PATTERN = r".*__([\w]+)_(\d+)d$"

    # In-feature configuration for FeatureChainParserMixin
    IN_FEATURE_SEPARATOR = ","
    MIN_IN_FEATURES = 1
    MAX_IN_FEATURES = None

    PROPERTY_MAPPING = {
        ALGORITHM: {
            **REDUCTION_ALGORITHMS,
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: True,
        },
        DIMENSION: {
            "explanation": "Target dimension for the reduction (positive integer)",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: True,
            DefaultOptionKeys.validation_function: lambda value: (
                isinstance(value, (int, str)) and str(value).isdigit() and int(value) > 0
            ),
        },
        DefaultOptionKeys.in_features: {
            "explanation": "Source features to use for dimensionality reduction",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: False,
        },
        # t-SNE specific parameters
        TSNE_MAX_ITER: {
            "explanation": "Maximum number of iterations for t-SNE optimization",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: False,
            DefaultOptionKeys.default: 250,
            DefaultOptionKeys.validation_function: lambda value: (
                isinstance(value, (int, str)) and str(value).isdigit() and int(value) > 0
            ),
        },
        TSNE_N_ITER_WITHOUT_PROGRESS: {
            "explanation": "Maximum iterations without progress before early stopping (t-SNE)",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: False,
            DefaultOptionKeys.default: 50,
            DefaultOptionKeys.validation_function: lambda value: (
                isinstance(value, (int, str)) and str(value).isdigit() and int(value) > 0
            ),
        },
        TSNE_METHOD: {
            "barnes_hut": "Barnes-Hut approximation (faster, O(n log n))",
            "exact": "Exact method (slower, O(n^2))",
            "explanation": "t-SNE computation method",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: True,
            DefaultOptionKeys.default: "barnes_hut",
        },
        # PCA specific parameters
        PCA_SVD_SOLVER: {
            "auto": "Automatically choose solver based on data shape",
            "full": "Full SVD using LAPACK",
            "arpack": "Truncated SVD using ARPACK",
            "randomized": "Randomized SVD",
            "explanation": "SVD solver algorithm for PCA",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: True,
            DefaultOptionKeys.default: "auto",
        },
        # ICA specific parameters
        ICA_MAX_ITER: {
            "explanation": "Maximum number of iterations for ICA",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: False,
            DefaultOptionKeys.default: 200,
            DefaultOptionKeys.validation_function: lambda value: (
                isinstance(value, (int, str)) and str(value).isdigit() and int(value) > 0
            ),
        },
        # Isomap specific parameters
        ISOMAP_N_NEIGHBORS: {
            "explanation": "Number of neighbors for Isomap",
            DefaultOptionKeys.context: True,
            DefaultOptionKeys.strict_validation: False,
            DefaultOptionKeys.default: 5,
            DefaultOptionKeys.validation_function: lambda value: (
                isinstance(value, (int, str)) and str(value).isdigit() and int(value) > 0
            ),
        },
    }

    @classmethod
    def parse_reduction_suffix(cls, feature_name: str) -> tuple[str, int]:
        """
        Parse the dimensionality reduction suffix into its components.

        Args:
            feature_name: The feature name to parse

        Returns:
            A tuple containing (algorithm, dimension)

        Raises:
            ValueError: If the suffix doesn't match the expected pattern
        """
        # Extract the suffix part (everything after the last double underscore)
        suffix_start = feature_name.rfind(CHAIN_SEPARATOR)
        if suffix_start == -1:
            raise ValueError(
                f"Invalid dimensionality reduction feature name format: {feature_name}. Missing double underscore separator."
            )

        suffix = feature_name[suffix_start + len(CHAIN_SEPARATOR) :]

        # Parse the suffix components
        parts = suffix.split("_")
        if len(parts) != 2 or not parts[1].endswith("d"):
            raise ValueError(
                f"Invalid dimensionality reduction feature name format: {feature_name}. "
                f"Expected format: {{in_features}}__{{algorithm}}_{{dimension}}d"
            )

        algorithm = parts[0]
        dimension_str = parts[1][:-1]  # Remove the 'd' suffix

        # Validate algorithm
        if algorithm not in cls.REDUCTION_ALGORITHMS:
            raise ValueError(
                f"Unsupported dimensionality reduction algorithm: {algorithm}. "
                f"Supported algorithms: {', '.join(cls.REDUCTION_ALGORITHMS.keys())}"
            )

        # Validate dimension
        try:
            dimension = int(dimension_str)
            if dimension <= 0:
                raise ValueError(f"Invalid dimension: {dimension}. Must be a positive integer.")
            return algorithm, dimension
        except ValueError:
            raise ValueError(f"Invalid dimension: {dimension_str}. Must be a positive integer.")

    @classmethod
    def _validate_string_match(cls, feature_name: str, _operation_config: str, _source_feature: str) -> bool:
        """
        Validate that a string-based feature name has valid dimensionality reduction components.

        Validates algorithm and dimension using parse_reduction_suffix().

        Args:
            feature_name: The full feature name to validate
            _operation_config: The operation config extracted by the regex (unused)
            _source_feature: The source feature extracted by the regex (unused)

        Returns:
            True if valid, False otherwise
        """
        if FeatureChainParser.is_chained_feature(feature_name):
            try:
                # Use existing validation logic that validates algorithm and dimension
                cls.parse_reduction_suffix(feature_name)
            except ValueError:
                # If validation fails, this feature doesn't match
                return False
        return True

    @classmethod
    def _extract_algorithm_dimension_and_source_features(cls, feature: Feature) -> tuple[str, int, list[str], Options]:
        """
        Extract algorithm, dimension, source features, and algorithm-specific options from a feature.

        Tries string-based parsing first, falls back to configuration-based approach.

        Args:
            feature: The feature to extract parameters from

        Returns:
            Tuple of (algorithm, dimension, source_features_list, algorithm_options)

        Raises:
            ValueError: If parameters cannot be extracted
        """
        source_features = cls._extract_source_features(feature)
        algorithm, dimension, algo_options = cls._extract_dim_reduction_params(feature)
        if algorithm is None or dimension is None:
            raise ValueError(f"Could not extract algorithm and dimension from: {feature.name}")
        return algorithm, dimension, source_features, algo_options

    @classmethod
    def _extract_dim_reduction_params(cls, feature: Feature) -> tuple[Optional[str], Optional[int], Options]:
        """
        Extract dimensionality reduction algorithm, dimension, and options from a feature.

        Tries string-based parsing first, falls back to configuration-based approach.

        Args:
            feature: The feature to extract parameters from

        Returns:
            Tuple of (algorithm, dimension, algorithm_options)
        """
        feature_name_str = feature.name

        # Try string-based parsing first
        if FeatureChainParser.is_chained_feature(feature_name_str):
            algorithm, dimension = cls.parse_reduction_suffix(feature_name_str)
            return algorithm, dimension, feature.options

        # Fall back to configuration-based approach
        algorithm = feature.options.get(cls.ALGORITHM)
        dimension = feature.options.get(cls.DIMENSION)

        if algorithm is None or dimension is None:
            return None, None, feature.options

        # Validate algorithm
        if algorithm not in cls.REDUCTION_ALGORITHMS:
            raise ValueError(
                f"Unsupported dimensionality reduction algorithm: {algorithm}. "
                f"Supported algorithms: {', '.join(cls.REDUCTION_ALGORITHMS.keys())}"
            )

        # Validate and convert dimension
        dimension = int(dimension)
        if dimension <= 0:
            raise ValueError(f"Invalid dimension: {dimension}. Must be a positive integer.")

        return algorithm, dimension, feature.options

    @classmethod
    def calculate_feature(cls, data: Any, features: FeatureSet) -> Any:
        """
        Perform dimensionality reduction operations.

        Processes all requested features, determining the dimensionality reduction algorithm,
        dimension, and source features from either string parsing or configuration-based options.

        Adds the dimensionality reduction results directly to the input data structure.
        """

        # Process each requested feature
        for feature in features.features:
            algorithm, dimension, source_features, options = cls._extract_algorithm_dimension_and_source_features(
                feature
            )

            # Check if all source features exist
            cls._check_source_features_exist(data, source_features)

            # Perform dimensionality reduction
            result = cls._perform_reduction(data, algorithm, dimension, source_features, options)

            # Add the result to the data
            data = cls._add_result_to_data(data, feature.name, result)
        return data

    @classmethod
    @abstractmethod
    def _check_source_features_exist(cls, data: Any, feature_names: list[str]) -> None:
        """
        Check if the source features exist in the data.

        Args:
            data: The input data
            feature_names: List of feature names to check

        Raises:
            ValueError: If any of the features do not exist in the data.
        """
        ...

    @classmethod
    @abstractmethod
    def _add_result_to_data(cls, data: Any, feature_name: str, result: Any) -> Any:
        """
        Add the result to the data.

        Args:
            data: The input data
            feature_name: The name of the feature to add
            result: The result to add

        Returns:
            The updated data
        """
        ...

    @classmethod
    @abstractmethod
    def _perform_reduction(
        cls,
        data: Any,
        algorithm: str,
        dimension: int,
        source_features: list[str],
        options: Options,
    ) -> Any:
        """
        Method to perform the dimensionality reduction. Should be implemented by subclasses.

        Args:
            data: The input data
            algorithm: The dimensionality reduction algorithm to use
            dimension: The target dimension for the reduction
            source_features: The list of source features to use for dimensionality reduction
            options: Options containing algorithm-specific parameters

        Returns:
            The result of the dimensionality reduction (typically the reduced features)
        """
        ...
