"""
NumPy/sklearn backend for PCA computations.

This implementation wraps sklearn's PCA to provide a consistent interface
that matches the PCABackend protocol.
"""

import numpy as np
from sklearn.decomposition import PCA
from typing import Dict, Tuple
from numpy.typing import NDArray


class NumPyPCABackend:
    """NumPy/sklearn backend for PCA computations."""

    def __init__(self):
        self.backend_name = "numpy"

    def fit(
        self,
        X: NDArray,
        n_components: int,
    ) -> Dict[str, NDArray]:
        """
        Fit PCA model to data using sklearn.

        Args:
            X: Data matrix, shape (n_samples, n_features)
            n_components: Number of principal components to compute

        Returns:
            Dictionary containing:
                - 'components': Principal component vectors, shape (n_components, n_features)
                - 'mean': Mean of the data, shape (n_features,)
                - 'explained_variance': Variance explained by each component
                - 'explained_variance_ratio': Fraction of variance explained
                - 'singular_values': Singular values corresponding to each component
        """
        X = np.asarray(X)
        n_samples, n_features = X.shape

        # Limit n_components to valid range
        n_components = min(n_components, n_samples, n_features)

        # Fit sklearn PCA
        pca = PCA(n_components=n_components, svd_solver="full")
        pca.fit(X)

        return {
            "components": pca.components_,
            "mean": pca.mean_,
            "explained_variance": pca.explained_variance_,
            "explained_variance_ratio": pca.explained_variance_ratio_,
            "singular_values": pca.singular_values_,
        }

    def transform(
        self,
        X: NDArray,
        components: NDArray,
        mean: NDArray,
    ) -> NDArray:
        """
        Project data onto principal components.

        Args:
            X: Data matrix, shape (n_samples, n_features)
            components: Principal components, shape (n_components, n_features)
            mean: Data mean for centering, shape (n_features,)

        Returns:
            Projected data, shape (n_samples, n_components)
        """
        X = np.asarray(X)
        components = np.asarray(components)
        mean = np.asarray(mean)

        # Center and project: X_transformed = (X - mean) @ components.T
        X_centered = X - mean
        return X_centered @ components.T

    def fit_transform(
        self,
        X: NDArray,
        n_components: int,
    ) -> Tuple[NDArray, Dict[str, NDArray]]:
        """
        Fit PCA and transform data in one step.

        Args:
            X: Data matrix, shape (n_samples, n_features)
            n_components: Number of principal components to compute

        Returns:
            Tuple of:
                - transformed: Projected data, shape (n_samples, n_components)
                - fit_result: Dictionary from fit() method
        """
        X = np.asarray(X)
        n_samples, n_features = X.shape

        # Limit n_components to valid range
        n_components = min(n_components, n_samples, n_features)

        # Fit sklearn PCA
        pca = PCA(n_components=n_components, svd_solver="full")
        X_transformed = pca.fit_transform(X)

        fit_result = {
            "components": pca.components_,
            "mean": pca.mean_,
            "explained_variance": pca.explained_variance_,
            "explained_variance_ratio": pca.explained_variance_ratio_,
            "singular_values": pca.singular_values_,
        }

        return X_transformed, fit_result

    def inverse_transform(
        self,
        X_transformed: NDArray,
        components: NDArray,
        mean: NDArray,
    ) -> NDArray:
        """
        Reconstruct data from principal components.

        Args:
            X_transformed: Projected data, shape (n_samples, n_components)
            components: Principal components, shape (n_components, n_features)
            mean: Data mean for centering, shape (n_features,)

        Returns:
            Reconstructed data, shape (n_samples, n_features)
        """
        X_transformed = np.asarray(X_transformed)
        components = np.asarray(components)
        mean = np.asarray(mean)

        # Reconstruct: X_reconstructed = X_transformed @ components + mean
        return X_transformed @ components + mean


# Singleton instance
_numpy_pca_backend = None


def get_numpy_pca_backend() -> NumPyPCABackend:
    """Get NumPy PCA backend instance (lazy initialization)."""
    global _numpy_pca_backend
    if _numpy_pca_backend is None:
        _numpy_pca_backend = NumPyPCABackend()
    return _numpy_pca_backend
