# -*- coding: utf-8 -*-
"""
PCA (Principal Component Analysis) outlier detector.

PCA-based outlier detection uses reconstruction error from principal components
to identify anomalies. Samples with large reconstruction error are more likely
to be anomalous.

Reference:
    Shyu, M.L., Chen, S.C., Sarinnapakorn, K. and Chang, L., 2003.
    A novel anomaly detection scheme based on principal component classifier.
    ICDM.
"""

from __future__ import annotations

from typing import Iterable

import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.utils import check_array

from .baseml import BaseVisionDetector
from .core_feature_base import CoreFeatureDetector
from .registry import register_model


class CorePCA:
    """Minimal PCA reconstruction-error detector (sklearn backend)."""

    def __init__(
        self,
        *,
        contamination: float = 0.1,
        n_components=None,
        n_selected_components: int | None = None,
        whiten: bool = False,
        svd_solver: str = "auto",
        weighted: bool = True,  # API compat (currently unused)
        standardization: bool = True,
        random_state=None,
        **pca_kwargs,
    ) -> None:
        self.contamination = float(contamination)
        self.n_components = n_components
        self.n_selected_components = n_selected_components
        self.whiten = bool(whiten)
        self.svd_solver = str(svd_solver)
        self.weighted = bool(weighted)
        self.standardization = bool(standardization)
        self.random_state = random_state
        self._pca_kwargs = dict(pca_kwargs)

        self.scaler_: StandardScaler | None = None
        self.pca_: PCA | None = None
        self.decision_scores_: np.ndarray | None = None

    def fit(self, x, y=None):  # noqa: ANN001, ANN201
        del y
        x = check_array(x, ensure_2d=True, dtype=np.float64)

        x_proc = x
        if self.standardization:
            self.scaler_ = StandardScaler()
            x_proc = self.scaler_.fit_transform(x_proc)

        self.pca_ = PCA(
            n_components=self.n_components,
            whiten=self.whiten,
            svd_solver=self.svd_solver,
            random_state=self.random_state,
            **self._pca_kwargs,
        )
        self.pca_.fit(x_proc)

        self.decision_scores_ = self.decision_function(x)
        return self

    def decision_function(self, x):  # noqa: ANN001, ANN201
        if self.pca_ is None:
            raise RuntimeError("Detector must be fitted before calling decision_function")

        x = check_array(x, ensure_2d=True, dtype=np.float64)
        x_proc = x
        if self.standardization:
            if self.scaler_ is None:
                raise RuntimeError("Internal error: missing scaler")
            x_proc = self.scaler_.transform(x_proc)

        z = self.pca_.transform(x_proc)

        if self.n_selected_components is not None:
            k = int(self.n_selected_components)
            if k < 1:
                raise ValueError("n_selected_components must be >= 1")
            k = min(k, z.shape[1])
            z_masked = np.zeros_like(z)
            z_masked[:, :k] = z[:, :k]
            x_recon = self.pca_.inverse_transform(z_masked)
        else:
            x_recon = self.pca_.inverse_transform(z)

        # Reconstruction error in the (optionally) standardized space.
        err = np.sum((x_proc - x_recon) ** 2, axis=1)
        return err.astype(np.float64).ravel()


@register_model(
    "core_pca",
    tags=("classical", "core", "features", "linear", "pca"),
    metadata={
        "description": "Core PCA reconstruction-error detector on feature matrices (native wrapper)",
        "input": "features",
        "paper": "ICDM 2003",
        "year": 2003,
    },
)
class CorePCAModel(CoreFeatureDetector):
    """Core (feature-matrix) PCA detector with BaseDetector thresholding."""

    def __init__(
        self,
        *,
        contamination: float = 0.1,
        n_components=None,
        n_selected_components: int | None = None,
        whiten: bool = False,
        svd_solver: str = "auto",
        weighted: bool = True,
        standardization: bool = True,
        random_state=None,
        **kwargs,
    ) -> None:
        self._backend_kwargs = dict(
            contamination=float(contamination),
            n_components=n_components,
            n_selected_components=n_selected_components,
            whiten=bool(whiten),
            svd_solver=str(svd_solver),
            weighted=bool(weighted),
            standardization=bool(standardization),
            random_state=random_state,
            **dict(kwargs),
        )
        super().__init__(contamination=float(contamination))

    def _build_detector(self):
        return CorePCA(**self._backend_kwargs)


@register_model(
    "vision_pca",
    tags=("vision", "classical", "linear", "pca"),
    metadata={
        "description": "Vision wrapper for PCA-based outlier detector",
        "paper": "ICDM 2003",
        "year": 2003,
        "classic": True,
        "interpretable": True,
    },
)
class VisionPCA(BaseVisionDetector):
    """Vision-compatible PCA detector for anomaly detection."""

    def __init__(
        self,
        *,
        feature_extractor=None,
        contamination: float = 0.1,
        n_components=None,
        n_selected_components: int | None = None,
        whiten: bool = False,
        svd_solver: str = "auto",
        weighted: bool = True,
        standardization: bool = True,
        random_state=None,
        **kwargs,
    ):
        self.detector_kwargs = dict(
            contamination=contamination,
            n_components=n_components,
            n_selected_components=n_selected_components,
            whiten=whiten,
            svd_solver=svd_solver,
            weighted=weighted,
            standardization=standardization,
            random_state=random_state,
            **kwargs,
        )
        super().__init__(contamination=contamination, feature_extractor=feature_extractor)

    def _build_detector(self):
        return CorePCA(**self.detector_kwargs)

    def fit(self, x: Iterable[str], y=None):
        return super().fit(x, y=y)

    def decision_function(self, x):
        return super().decision_function(x)
