from dataclasses import asdict
from typing import Any, Literal

import numpy as np
from pydantic.v1 import dataclasses

from molflux.modelzoo.info import ModelInfo
from molflux.modelzoo.models.pyod import (
    PyODClassificationMixin,
    PyODModelBase,
    PyODModelConfig,
)

try:
    from pyod.models.cblof import CBLOF
except ImportError as e:
    from molflux.modelzoo.errors import ExtrasDependencyImportError

    raise ExtrasDependencyImportError("pyod", e) from None


_DESCRIPTION = """
The CBLOF operator calculates the outlier score based on cluster-based
local outlier factor.

CBLOF takes as an input the data set and the cluster model that was
generated by a clustering algorithm. It classifies the clusters into small
clusters and large clusters using the parameters alpha and beta.
The anomaly score is then calculated based on the size of the cluster the
point belongs to as well as the distance to the nearest large cluster.

Use weighting for outlier factor based on the sizes of the clusters as
proposed in the original publication. Since this might lead to unexpected
behavior (outliers close to small clusters are not found), it is disabled
by default.Outliers scores are solely computed based on their distance to
the closest large cluster center.

By default, kMeans is used for clustering algorithm instead of
Squeezer algorithm mentioned in the original paper for multiple reasons.

See :cite:`he2003discovering` for details.
"""

_CONFIG_DESCRIPTION = """
Parameters
----------
n_clusters : int, optional (default=8)
    The number of clusters to form as well as the number of
    centroids to generate.

contamination : float in (0., 0.5), optional (default=0.1)
    The amount of contamination of the data set,
    i.e. the proportion of outliers in the data set. Used when fitting to
    define the threshold on the decision function.

clustering_estimator : Estimator, optional (default=None)
    The base clustering algorithm for performing data clustering.
    A valid clustering algorithm should be passed in. The estimator should
    have standard sklearn APIs, fit() and predict(). The estimator should
    have attributes ``labels_`` and ``cluster_centers_``.
    If ``cluster_centers_`` is not in the attributes once the model is fit,
    it is calculated as the mean of the samples in a cluster.

    If not set, CBLOF uses KMeans for scalability. See
    https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html

alpha : float in (0.5, 1), optional (default=0.9)
    Coefficient for deciding small and large clusters. The ratio
    of the number of samples in large clusters to the number of samples in
    small clusters.

beta : int or float in (1,), optional (default=5).
    Coefficient for deciding small and large clusters. For a list
    sorted clusters by size `|C1|, |C2|, ..., |Cn|, beta = |Ck|/|Ck-1|`

use_weights : bool, optional (default=False)
    If set to True, the size of clusters are used as weights in
    outlier score calculation.

check_estimator : bool, optional (default=False)
    If set to True, check whether the base estimator is consistent with
    sklearn standard.

    .. warning::
        check_estimator may throw errors with scikit-learn 0.20 above.

random_state : int, np.random.Generator or None, optional (default=None)
    If int, random_state is the seed used by the random
    number generator; If RandomState instance, random_state is the random
    number generator; If None, the random number generator is the
    RandomState instance used by `np.random`.
"""

Method = Literal["default", "fast"]


class Config:
    arbitrary_types_allowed = True
    extra = "forbid"


@dataclasses.dataclass(config=Config)
class CBLOFDetectorConfig(PyODModelConfig):
    n_clusters: int = 8
    contamination: float = 0.1
    clustering_estimator: Any = None
    alpha: float = 0.9
    beta: float = 5
    use_weights: bool = False
    check_estimator: bool = False
    random_state: int | np.random.Generator | None = None


class CBLOFDetector(PyODClassificationMixin, PyODModelBase[CBLOFDetectorConfig]):
    @property
    def config(self) -> dict[str, Any]:
        return asdict(self.model_config)

    @property
    def _config_builder(self) -> type[CBLOFDetectorConfig]:
        return CBLOFDetectorConfig

    def _info(self) -> ModelInfo:
        return ModelInfo(
            model_description=_DESCRIPTION,
            config_description=_CONFIG_DESCRIPTION,
        )

    def _instantiate_model(self) -> CBLOF:
        config = self.model_config
        return CBLOF(
            n_clusters=config.n_clusters,
            contamination=config.contamination,
            clustering_estimator=config.clustering_estimator,
            alpha=config.alpha,
            beta=config.beta,
            use_weights=config.use_weights,
            check_estimator=config.check_estimator,
            random_state=config.random_state,
        )
