"""
@Author: yp.zuo
@Date: 2023-10-15 14:30:00
@Desc: 异常检测模块，集成多种异常检测算法
"""

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union

import numpy as np
from scipy import stats
from sklearn.covariance import EllipticEnvelope
from sklearn.decomposition import PCA
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor, NearestNeighbors
from sklearn.preprocessing import StandardScaler

# region Method-specific Argument Types


class _ZScoreArgs(TypedDict, total=False):
    threshold: float
    use_mahalanobis: bool
    aggregation: Literal['max', 'l2', 'l1', 'mean']


class _IQRArgs(TypedDict, total=False):
    multiplier: float
    aggregation: Literal['max', 'l2', 'l1', 'mean']


class _IsolationForestArgs(TypedDict, total=False):
    n_estimators: int
    max_samples: Union[int, float, str]
    contamination: Union[float, str]
    max_features: Union[int, float]
    random_state: Optional[int]


class _LOFArgs(TypedDict, total=False):
    n_neighbors: int
    algorithm: Literal['auto', 'ball_tree', 'kd_tree', 'brute']
    leaf_size: int
    metric: str
    p: int
    contamination: Union[float, str]
    novelty: bool


class _EllipticEnvelopeArgs(TypedDict, total=False):
    support_fraction: float
    contamination: float
    random_state: Optional[int]


class _PCAArgs(TypedDict, total=False):
    n_components: Union[int, float, None]
    threshold_percentile: float


class _KNNArgs(TypedDict, total=False):
    n_neighbors: int
    threshold_percentile: float


_MethodArgs = Union[
    _ZScoreArgs,
    _IQRArgs,
    _IsolationForestArgs,
    _LOFArgs,
    _EllipticEnvelopeArgs,
    _PCAArgs,
    _KNNArgs,
]

_MethodName = Literal[
    'z_score', 'iqr', 'lof', 'isolation_forest', 'pca', 'elliptic_envelope', 'knn'
]

# endregion

# region Outlier Detection Strategies (Internal Implementation)


class _OutlierStrategy(ABC):
    """Abstract base class for outlier detection strategies."""

    def __init__(self, **kwargs):
        self.params = kwargs
        self.model_ = None
        self.statistics_ = None
        self.threshold_ = None

    @abstractmethod
    def fit(self, X: np.ndarray) -> '_OutlierStrategy':
        """Fit the model or compute statistics."""
        pass

    @abstractmethod
    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """Predict outliers and scores."""
        pass

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        self.fit(X)
        return self.predict(X)


class _ZScoreStrategy(_OutlierStrategy):
    """Z-Score (including Mahalanobis distance) strategy."""

    def fit(self, X: np.ndarray) -> '_ZScoreStrategy':
        use_mahalanobis = self.params.get('use_mahalanobis', False) and X.shape[1] > 1
        if use_mahalanobis:
            self.statistics_ = {
                'mean': np.mean(X, axis=0),
                'cov_inv': np.linalg.pinv(np.cov(X.T)),
                'use_mahalanobis': True,
                'n_features': X.shape[1],
            }
        else:
            self.statistics_ = {
                'mean': np.mean(X, axis=0),
                'std': np.std(X, axis=0),
                'use_mahalanobis': False,
            }
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        threshold = self.params.get('threshold')
        aggregation = self.params.get('aggregation', 'l2')

        if self.statistics_.get('use_mahalanobis'):
            mean, cov_inv = self.statistics_['mean'], self.statistics_['cov_inv']
            n_features = self.statistics_['n_features']

            # 维度自适应阈值：如果用户未指定阈值，使用卡方分布分位数
            if threshold is None:
                # q=0.997 对应约 3σ 的两侧尾概率
                threshold = stats.chi2.ppf(0.997, df=n_features)

            diff = X - mean
            scores = np.sqrt(np.sum(diff @ cov_inv * diff, axis=1))
        else:
            mean, std = self.statistics_['mean'], self.statistics_['std']
            if threshold is None:
                threshold = 3.0  # 标准 Z-score 阈值

            with np.errstate(divide='ignore', invalid='ignore'):
                z_scores = np.abs((X - mean) / np.where(std == 0, 1e-9, std))

            # 多维聚合策略
            if X.ndim == 1 or X.shape[1] == 1:
                scores = np.nan_to_num(z_scores).flatten()  # 确保单变量返回 1D
            else:
                z_scores = np.nan_to_num(z_scores)
                if aggregation == 'max':
                    scores = np.max(z_scores, axis=1)
                elif aggregation == 'l2':
                    scores = np.sqrt(np.mean(z_scores**2, axis=1))
                elif aggregation == 'l1':
                    scores = np.mean(z_scores, axis=1)
                elif aggregation == 'mean':
                    scores = np.mean(z_scores, axis=1)
                else:
                    raise ValueError(f'Unsupported aggregation method: {aggregation}')

        outliers = scores > threshold
        return outliers, scores


class _IQRStrategy(_OutlierStrategy):
    """IQR strategy (vectorized and optimized)."""

    def fit(self, X: np.ndarray) -> '_IQRStrategy':
        self.statistics_ = {
            'q1': np.percentile(X, 25, axis=0),
            'q3': np.percentile(X, 75, axis=0),
        }
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        multiplier = self.params.get('multiplier', 1.5)
        aggregation = self.params.get('aggregation', 'max')  # IQR 传统上使用 max

        q1, q3 = self.statistics_['q1'], self.statistics_['q3']
        iqr = q3 - q1
        lower_bound = q1 - multiplier * iqr
        upper_bound = q3 + multiplier * iqr

        outliers_matrix = (X < lower_bound) | (X > upper_bound)

        with np.errstate(divide='ignore', invalid='ignore'):
            scores_matrix = np.maximum(lower_bound - X, X - upper_bound) / np.where(
                iqr == 0, 1e-9, iqr
            )
        scores_matrix = np.maximum(scores_matrix, 0)
        scores_matrix = np.nan_to_num(scores_matrix)

        # 处理单变量和多变量情况
        if X.ndim == 1 or X.shape[1] == 1:
            outliers = outliers_matrix.flatten()  # 确保单变量返回 1D
            scores = scores_matrix.flatten()
        else:
            # 多维聚合策略
            if aggregation == 'max':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.max(scores_matrix, axis=1)
            elif aggregation == 'l2':
                outliers = np.any(outliers_matrix, axis=1)  # 任一维异常即为异常
                scores = np.sqrt(np.mean(scores_matrix**2, axis=1))
            elif aggregation == 'l1':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.mean(scores_matrix, axis=1)
            elif aggregation == 'mean':
                outliers = np.any(outliers_matrix, axis=1)
                scores = np.mean(scores_matrix, axis=1)
            else:
                raise ValueError(f'Unsupported aggregation method: {aggregation}')

        return outliers, scores


class _SklearnStrategy(_OutlierStrategy):
    """Generic strategy for scikit-learn outlier detection models."""

    def __init__(self, type_name: str, model_class, **kwargs):
        super().__init__(**kwargs)
        self.type_name = type_name
        self.model_class = model_class
        self.model_ = None

    def fit(self, X: np.ndarray) -> '_SklearnStrategy':
        self.model_ = self.model_class(**self.params)

        # LOF(novelty=False) does not support fit, it only has fit_predict.
        # So we do nothing here for that specific case.
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            pass  # Defer fitting to fit_predict
        else:
            self.model_.fit(X)

        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        # 特殊处理 LOF：当 novelty=False 时，使用 fit_predict
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            raise ValueError(
                'LOF with novelty=False does not support predict(). '
                'Use fit_predict() instead for this configuration.'
            )
        else:
            # 其他情况使用 predict
            predictions = self.model_.predict(X)

        outliers = predictions == -1

        if hasattr(self.model_, 'score_samples'):
            scores = -self.model_.score_samples(X)
        elif hasattr(self.model_, 'decision_function'):
            scores = -self.model_.decision_function(X)
        elif hasattr(self.model_, 'mahalanobis'):
            scores = self.model_.mahalanobis(X)
        else:
            scores = (predictions * -1 + 1) / 2  # Fallback: 1 for outlier, 0 for inlier
        return outliers, scores

    def fit_predict(self, X):
        if self.model_ is None:
            self.model_ = self.model_class(**self.params)
        if self.type_name == 'lof' and not self.params.get('novelty', True):
            predictions = self.model_.fit_predict(X)
            outliers = predictions == -1
            # LOF(novelty=False) has negative_outlier_factor_ for scores
            scores = -self.model_.negative_outlier_factor_
            return outliers, scores
        else:
            # 对于所有其他 sklearn 模型，使用基类的默认行为
            return super().fit_predict(X)


class _PCAReconstructionStrategy(_OutlierStrategy):
    """PCA reconstruction error strategy."""

    def fit(self, X: np.ndarray) -> '_PCAReconstructionStrategy':
        n_components = self.params.get('n_components', 0.95)
        self.model_ = PCA(n_components=n_components)
        X_transformed = self.model_.fit_transform(X)
        X_reconstructed = self.model_.inverse_transform(X_transformed)
        reconstruction_errors = np.sum((X - X_reconstructed) ** 2, axis=1)
        threshold_percentile = self.params.get('threshold_percentile', 95.0)
        self.threshold_ = np.percentile(reconstruction_errors, threshold_percentile)
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        X_transformed = self.model_.transform(X)
        X_reconstructed = self.model_.inverse_transform(X_transformed)
        reconstruction_errors = np.sum((X - X_reconstructed) ** 2, axis=1)
        outliers = reconstruction_errors > self.threshold_
        return outliers, reconstruction_errors


class _KNNDistanceStrategy(_OutlierStrategy):
    """KNN distance strategy."""

    def fit(self, X: np.ndarray) -> '_KNNDistanceStrategy':
        n_neighbors = self.params.get('n_neighbors', 5)
        self.n_neighbors_ = max(1, n_neighbors)

        self.model_ = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
        self.n_neighbors_ = n_neighbors

        # 计算训练阈值时排除自身
        distances, _ = self.model_.kneighbors(X, n_neighbors=self.n_neighbors_ + 1)
        knn_distances_excl_self = distances[:, -1]

        threshold_percentile = float(self.params.get('threshold_percentile', 95.0))
        self.threshold_ = np.percentile(knn_distances_excl_self, threshold_percentile)

        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        k = self.n_neighbors_
        n_query = k + 1 if k > 0 else 1
        distances, _ = self.model_.kneighbors(X, n_neighbors=n_query)

        # 识别测试点中的训练点 (自匹配)
        mask_self = np.isclose(distances[:, 0], 0.0)
        # 初始化分数数组
        knn_distances = np.zeros(X.shape[0])
        # 对于非自匹配点，其k-th邻居在倒数第二列 (index -2)
        # distances has shape (n_samples, k+1)
        if np.any(~mask_self):
            knn_distances[~mask_self] = distances[~mask_self, -2]

        # 对于自匹配点，其k-th“其他”邻居在最后一列 (index -1)
        if np.any(mask_self):
            knn_distances[mask_self] = distances[mask_self, -1]

        outliers = knn_distances > self.threshold_
        return outliers, knn_distances


# 默认参数配置
DEFAULT_Z_SCORE_ARGS = {'threshold': None, 'aggregation': 'l2'}  # 使用自适应阈值和L2聚合
DEFAULT_IQR_ARGS = {'multiplier': 1.5, 'aggregation': 'max'}
DEFAULT_PCA_ARGS = {'n_components': 0.95, 'threshold_percentile': 99.0}
DEFAULT_KNN_ARGS = {'n_neighbors': 5, 'threshold_percentile': 95.0}  # 更保守的默认值
DEFAULT_LOF_ARGS: _LOFArgs = {
    'n_neighbors': 5,  # 对小样本更友好
    'algorithm': 'auto',
    'novelty': False,
    # 'leaf_size': 30,
    # 'metric': 'minkowski',
    # 'p': 2,
    'contamination': 'auto',
}
DEFAULT_ISOLATION_FOREST_ARGS = {
    'n_estimators': 100,
    'contamination': 'auto',
    'random_state': None,
}
DEFAULT_ELLIPTIC_ENVELOPE_ARGS = {'contamination': 0.1, 'random_state': None}

strategy_map = {
    'z_score': (_ZScoreStrategy, DEFAULT_Z_SCORE_ARGS),
    'iqr': (_IQRStrategy, DEFAULT_IQR_ARGS),
    'pca': (_PCAReconstructionStrategy, DEFAULT_PCA_ARGS),
    'knn': (_KNNDistanceStrategy, DEFAULT_KNN_ARGS),
    'lof': (_SklearnStrategy, DEFAULT_LOF_ARGS),
    'isolation_forest': (_SklearnStrategy, DEFAULT_ISOLATION_FOREST_ARGS),
    'elliptic_envelope': (_SklearnStrategy, DEFAULT_ELLIPTIC_ENVELOPE_ARGS),
}


def _get_strategy(method: str, method_args: Dict[str, Any]) -> _OutlierStrategy:
    """Strategy factory function."""
    method_lower = method.lower()

    # 1. 定义策略类和默认参数
    if method_lower not in strategy_map:
        raise ValueError(f'Unsupported method: {method}')

    strategy_class, default_params = strategy_map[method_lower]

    # 2. 合并参数：用户传入的参数会覆盖默认值
    final_params = default_params.copy()
    final_params.update(method_args)

    # 3. 为 scikit-learn 策略准备参数
    if strategy_class is _SklearnStrategy:
        model_map = {
            'lof': LocalOutlierFactor,
            'isolation_forest': IsolationForest,
            'elliptic_envelope': EllipticEnvelope,
        }
        # 将模型类和合并后的参数一起传入
        return _SklearnStrategy(
            type_name=method_lower, model_class=model_map[method_lower], **final_params
        )
    else:
        # 对于非 sklearn 策略，直接传入合并后的参数
        return strategy_class(**final_params)


# endregion


class OutlierDetector:
    """多算法异常检测器

    一个封装了多种常见异常检测算法的统一接口，包括基于统计、
    距离和模型的方法。遵循 sklearn 风格的 fit-predict 模式。

    支持的算法:
    - `z_score`: 基于标准差的统计方法（支持马氏距离和多种聚合策略）
    - `iqr`: 四分位距方法（支持多种聚合策略）
    - `lof`: 局部异常因子
    - `isolation_forest`: 孤立森林
    - `pca`: 主成分分析重构误差
    - `elliptic_envelope`: 椭圆包络法
    - `knn`: K近邻距离法

    Z-Score 方法特性:
    - 维度自适应阈值：马氏距离时自动使用卡方分布分位数
    - 多维聚合策略：支持 'max', 'l2', 'l1', 'mean' 等聚合方式
    - 形状一致性：单变量时确保返回 1D 数组

    Examples
    --------
    >>> detector = OutlierDetector('isolation_forest', method_args={'contamination': 0.1})
    >>> detector.fit(X_train)
    >>> outliers, scores = detector.predict(X_test)
    """

    def __init__(
        self,
        method: _MethodName = 'lof',
        method_args: Optional[_MethodArgs] = None,
        normalize: bool | Literal['auto'] = 'auto',
    ):
        """初始化异常检测器

        Parameters
        ----------
        - `method` : `_MethodName`, 检测方法.
        - `method_args` : `_MethodArgs`, optional
            - 传递给所选检测算法的参数字典, by default None.
        - `normalize` : `bool | Literal['auto']`, optional
            - 是否对数据进行标准化, 默认为 'auto'。
            - 'auto' 模式下，会自动为需要标准化的算法启用。
        """
        self.method = method.lower()
        self.method_args = method_args or {}

        self.is_fitted = False

        DEFAULT_NEED_NORMALIZE = {
            'lof',
            'knn',
            'isolation_forest',
            'pca',
            'elliptic_envelope',
        }
        if normalize == 'auto':
            self.normalize = self.method in DEFAULT_NEED_NORMALIZE
        else:
            self.normalize = normalize

        self.scaler = StandardScaler() if self.normalize else None
        self.strategy_ = _get_strategy(self.method, self.method_args)

    def fit(self, X: np.ndarray) -> 'OutlierDetector':
        """训练异常检测器

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 训练数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `OutlierDetector`
            - 返回自身以支持链式调用。
        """
        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        # 对小样本进行参数自适应调整
        n_samples = X.shape[0]
        adjusted_args = self._adapt_parameters_for_sample_size(n_samples)

        # 重新创建策略以使用调整后的参数
        self.strategy_ = _get_strategy(self.method, adjusted_args)

        X_processed = self.scaler.fit_transform(X) if self.scaler else X
        self.strategy_.fit(X_processed)
        self.is_fitted = True
        return self

    def _adapt_parameters_for_sample_size(self, n_samples: int) -> dict:
        """根据样本量自适应调整参数"""
        # 1) 先合并默认 + 用户参数
        _, default_params = strategy_map[self.method]
        merged = default_params.copy()
        merged.update(self.method_args)

        # 2) 再做样本感知裁剪
        if self.method in ['lof', 'knn']:
            k = merged.get('n_neighbors', 5)
            merged['n_neighbors'] = max(1, min(k, n_samples - 1))
        return merged

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """使用已训练模型检测异常值

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 待检测数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `Tuple[np.ndarray, np.ndarray]`
            - `outlier_mask`: 布尔数组，True表示异常值。
            - `outlier_scores`: 每个样本的异常分数。

        Raises
        ------
        - `ValueError`
            - 当检测器尚未训练时抛出。
        """
        if not self.is_fitted:
            raise ValueError('检测器尚未训练，请先调用fit()方法')

        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        X_processed = self.scaler.transform(X) if self.scaler else X
        return self.strategy_.predict(X_processed)

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """
        在同一数据集上拟合模型并进行异常检测。

        Parameters
        ----------
        - `X` : `np.ndarray`
            - 输入数据，形状为 (n_samples, n_features).

        Returns
        -------
        - `Tuple[np.ndarray, np.ndarray]`
            - `outlier_mask`: 布尔数组，True表示异常值。
            - `outlier_scores`: 每个样本的异常分数。
        """
        X = np.asarray(X)
        if X.ndim == 1:
            X = X.reshape(-1, 1)

        # 同样需要参数自适应和数据标准化
        n_samples = X.shape[0]
        adjusted_args = self._adapt_parameters_for_sample_size(n_samples)
        self.strategy_ = _get_strategy(self.method, adjusted_args)

        # 注意：对于 fit_predict，标准化需要用 fit_transform
        X_processed = self.scaler.fit_transform(X) if self.scaler else X

        # is_fitted 状态应该在之后设置
        result = self.strategy_.fit_predict(X_processed)
        self.is_fitted = True
        return result


class EnsembleOutlierDetector:
    """集成异常检测器

    通过结合多种不同算法的结果来提高异常检测的鲁棒性和准确性。

    支持的投票策略:
    - `majority`: 多数投票，超过一半的检测器认为异常则判定为异常。
    - `unanimous`: 一致投票，所有检测器都认为异常才判定为异常。
    - `score_threshold`: 基于集成分数的阈值判断。
    - `weighted`: 加权投票，允许为不同检测器设置权重。
    """

    def __init__(
        self,
        methods: Dict[_MethodName, Optional[_MethodArgs]],
        voting_strategy: Literal[
            'majority', 'unanimous', 'score_threshold', 'weighted'
        ] = 'majority',
        detector_weights: Optional[Dict[str, float]] = None,
        score_threshold_percentile: float = 90.0,
    ):
        """初始化集成检测器

        Parameters
        ----------
        - `methods` : `Dict[_MethodName, Optional[_MethodArgs]]`
            - 检测方法及其参数的字典。
        - `voting_strategy` : `str`, optional
            - 投票策略, by default 'majority'.
        - `detector_weights` : `Dict[str, float]`, optional
            - 各检测器的权重，仅在 'weighted' 策略下有效。
        - `score_threshold_percentile` : `float`, optional
            - 分数阈值百分位数，仅在 'score_threshold' 和 'weighted' 策略下有效。
        """
        self.methods = methods
        self.voting_strategy = voting_strategy
        self.detector_weights = detector_weights or {}
        self.score_threshold_percentile = score_threshold_percentile
        self.detectors: List[OutlierDetector] = []
        self.is_fitted = False

        self._validate_and_prepare_weights()

    def _validate_and_prepare_weights(self):
        valid_strategies = {'majority', 'unanimous', 'score_threshold', 'weighted'}
        if self.voting_strategy not in valid_strategies:
            raise ValueError(f'不支持的投票策略: {self.voting_strategy}')

        if self.voting_strategy == 'weighted':
            if not self.detector_weights:
                self.detector_weights = {method: 1.0 for method in self.methods.keys()}

            missing_weights = set(self.methods.keys()) - set(self.detector_weights.keys())
            if missing_weights:
                raise ValueError(f'缺少以下方法的权重: {missing_weights}')

            total_weight = sum(self.detector_weights.values())
            if total_weight > 0:
                self.detector_weights = {
                    k: v / total_weight for k, v in self.detector_weights.items()
                }

    def fit(self, X: np.ndarray) -> 'EnsembleOutlierDetector':
        """训练所有检测器"""
        self.detectors = [
            OutlierDetector(method=method, method_args=params).fit(X)
            for method, params in self.methods.items()
        ]
        self.is_fitted = True
        return self

    def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """集成检测"""
        if not self.is_fitted:
            raise ValueError('集成检测器尚未训练')

        predictions = [detector.predict(X) for detector in self.detectors]
        all_outliers = np.array([p[0] for p in predictions]).T
        all_scores = np.array([p[1] for p in predictions]).T

        return self._apply_voting_strategy(all_outliers, all_scores)

    def fit_predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
        """在同一数据集上拟合并预测"""
        self.detectors = [
            OutlierDetector(method=method, method_args=params)
            for method, params in self.methods.items()
        ]
        self.is_fitted = True
        predictions = [detector.fit_predict(X) for detector in self.detectors]
        all_outliers = np.array([p[0] for p in predictions]).T
        all_scores = np.array([p[1] for p in predictions]).T

        return self._apply_voting_strategy(all_outliers, all_scores)

    def _apply_voting_strategy(self, all_outliers, all_scores):
        # 标准化分数以进行公平比较
        with np.errstate(divide='ignore', invalid='ignore'):
            normalized_scores = stats.zscore(all_scores, axis=0, nan_policy='omit')
            normalized_scores = np.nan_to_num(normalized_scores)

        if self.voting_strategy == 'majority':
            ensemble_outliers = np.sum(all_outliers, axis=1) > len(self.methods) // 2
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
        elif self.voting_strategy == 'unanimous':
            ensemble_outliers = np.all(all_outliers, axis=1)
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
        elif self.voting_strategy == 'score_threshold':
            ensemble_scores = np.nanmean(normalized_scores, axis=1)
            threshold = np.nanpercentile(ensemble_scores, self.score_threshold_percentile)
            ensemble_outliers = ensemble_scores > threshold
        elif self.voting_strategy == 'weighted':
            weights = np.array([self.detector_weights[m] for m in self.methods.keys()])
            ensemble_scores = np.sum(normalized_scores * weights, axis=1)
            threshold = np.nanpercentile(ensemble_scores, self.score_threshold_percentile)
            ensemble_outliers = ensemble_scores > threshold
        else:
            raise ValueError(f'不支持的投票策略: {self.voting_strategy}')

        return ensemble_outliers, ensemble_scores


def detect_outliers(
    X: np.ndarray, method: _MethodName = 'lof', method_args: Optional[_MethodArgs] = None
) -> Tuple[np.ndarray, np.ndarray]:
    """简化的异常检测接口

    一个便利函数，用于快速执行“fit”和“predict”两个步骤。
    注意：此函数在同一数据上进行训练和预测，主要用于探索性分析。

    Parameters
    ----------
    - `X` : `np.ndarray`
        - 输入数据，形状为 (n_samples, n_features).
    - `method` : `_MethodName`, optional
        - 检测方法, by default 'lof'.
    - `method_args` : `_MethodArgs`, optional
        - 传递给所选方法的参数字典, by default None.

    Returns
    -------
    - `Tuple[np.ndarray, np.ndarray]`
        - `outliers`: 异常值掩码，True表示异常值。
        - `scores`: 异常分数，值越大越可能是异常值。
    """
    detector = OutlierDetector(method=method, method_args=method_args)
    return detector.fit_predict(X)
