"""
Модуль проверки качества данных для ML-датасетов.

Функции:
    run_data_quality_checks — полный отчёт о качестве данных
    check_missing_values — проверка пропущенных значений
    check_outliers — проверка выбросов
    check_class_balance — проверка баланса классов
    check_temporal_order — проверка хронологического порядка
    check_constant_features — проверка константных признаков
    validate_dataset — проверка готового датасета перед сохранением
"""

from typing import Any, Optional

import numpy as np
import pandas as pd


def check_missing_values(
    df: pd.DataFrame,
    threshold: float = 0.1,
) -> dict[str, Any]:
    """
    Проверить пропущенные значения в признаках.

    Args:
        df: DataFrame для проверки.
        threshold: максимально допустимая доля пропусков (по умолчанию 10%).

    Returns:
        Словарь с результатами проверки:
            - 'has_nulls': есть ли пропуски
            - 'null_pct': доля пропусков по каждому признаку
            - 'high_null_features': признаки с пропусками > threshold
            - 'total_null': общее количество пропусков
    """
    null_pct = df.isnull().mean()
    has_nulls = null_pct.sum() > 0
    high_null = null_pct[null_pct > threshold]

    return {
        'has_nulls': bool(has_nulls),
        'null_pct': null_pct.to_dict() if has_nulls else {},
        'high_null_features': high_null.to_dict() if len(high_null) > 0 else {},
        'total_null': int(df.isnull().sum().sum()),
    }


def check_outliers(
    df: pd.DataFrame,
    zscore_threshold: float = 5.0,
    numeric_only: bool = True,
) -> dict[str, Any]:
    """
    Проверить наличие выбросов (Z-score метод).

    Args:
        df: DataFrame для проверки.
        zscore_threshold: порог Z-score для выброса (по умолчанию 5.0).
        numeric_only: проверять только числовые колонки.

    Returns:
        Словарь с результатами:
            - 'has_outliers': есть ли выбросы
            - 'outlier_counts': количество выбросов по каждому признаку
            - 'outlier_pct': доля выбросов по каждому признаку
            - 'extreme_outliers': выбросы с Z-score > 10
    """
    if numeric_only:
        numeric_df = df.select_dtypes(include=[np.number])
    else:
        numeric_df = df

    if numeric_df.empty:
        return {'has_outliers': False, 'outlier_counts': {}, 'outlier_pct': {}, 'extreme_outliers': {}}

    z_scores = np.abs((numeric_df - numeric_df.mean()) / numeric_df.std().replace(0, 1))
    outlier_counts = (z_scores > zscore_threshold).sum()
    has_outliers = outlier_counts.sum() > 0
    extreme = (z_scores > 10).sum()

    return {
        'has_outliers': bool(has_outliers),
        'outlier_counts': outlier_counts[outlier_counts > 0].to_dict(),
        'outlier_pct': (outlier_counts / len(df))[outlier_counts > 0].to_dict(),
        'extreme_outliers': extreme[extreme > 0].to_dict() if extreme.sum() > 0 else {},
    }


def check_class_balance(
    df: pd.DataFrame,
    target_col: str = 'target',
    min_class_pct: float = 0.1,
) -> dict[str, Any]:
    """
    Проверить баланс классов целевой переменной.

    Args:
        df: DataFrame с целевой колонкой.
        target_col: название колонки с целевой переменной.
        min_class_pct: минимально допустимая доля класса.

    Returns:
        Словарь с результатами:
            - 'class_counts': абсолютные количества
            - 'class_pct': доли классов
            - 'is_imbalanced': флаг дисбаланса
            - 'n_classes': количество классов
    """
    if target_col not in df.columns:
        return {
            'class_counts': {},
            'class_pct': {},
            'is_imbalanced': False,
            'n_classes': 0,
            'error': f'Колонка {target_col} не найдена',
        }

    # Удаляем NaN перед подсчётом
    target = df[target_col].dropna()

    if len(target) == 0:
        return {
            'class_counts': {},
            'class_pct': {},
            'is_imbalanced': True,
            'n_classes': 0,
        }

    class_counts = target.value_counts().sort_index()
    class_pct = target.value_counts(normalize=True).sort_index()
    is_imbalanced = class_pct.min() < min_class_pct

    return {
        'class_counts': class_counts.to_dict(),
        'class_pct': class_pct.to_dict(),
        'is_imbalanced': bool(is_imbalanced),
        'n_classes': int(class_counts.count()),
    }


def check_temporal_order(
    df: pd.DataFrame,
    timestamp_col: str = 'timestamp',
) -> dict[str, Any]:
    """
    Проверить хронологический порядок данных.

    Args:
        df: DataFrame для проверки.
        timestamp_col: колонка с timestamp.

    Returns:
        Словарь с результатами:
            - 'is_sorted': отсортирован ли по возрастанию
            - 'has_duplicates': есть ли дубликаты timestamp
            - 'has_gaps': есть ли пропуски в последовательности
            - 'gap_info': информация о пропусках
    """
    result: dict[str, Any] = {}

    if timestamp_col not in df.columns:
        return {'error': f'Колонка {timestamp_col} не найдена'}

    ts = df[timestamp_col].values

    # Проверка сортировки
    is_sorted = bool(np.all(np.diff(ts) >= 0))
    result['is_sorted'] = is_sorted

    # Проверка дубликатов
    has_duplicates = bool(len(ts) != len(np.unique(ts)))
    result['has_duplicates'] = has_duplicates

    # Проверка пропусков (для D1 — пропуск > 1 дня)
    if len(ts) > 1:
        diffs = np.diff(ts)
        # Средний шаг (в секундах)
        median_step = np.median(diffs) if len(diffs) > 0 else 0
        # Пропуски — отклонение от медианы более чем на 50%
        gaps = diffs[diffs > median_step * 1.5]
        result['median_step_seconds'] = int(median_step)
        result['n_gaps'] = int(len(gaps))
        if len(gaps) > 0:
            result['max_gap_seconds'] = int(gaps.max())
            result['gap_positions'] = np.where(diffs > median_step * 1.5)[0].tolist()[:10]

    return result


def check_constant_features(
    df: pd.DataFrame,
) -> dict[str, Any]:
    """
    Проверить константные признаки (одинаковые значения на всех строках).

    Args:
        df: DataFrame для проверки.

    Returns:
        Словарь с результатами:
            - 'constant_features': список константных признаков
            - 'low_variance_features': признаки с очень низкой дисперсией
    """
    numeric_df = df.select_dtypes(include=[np.number])

    if numeric_df.empty:
        return {'constant_features': [], 'low_variance_features': []}

    # Константные (одно уникальное значение)
    nunique = numeric_df.nunique()
    constant = nunique[nunique <= 1]
    constant_features = list(constant.index) if len(constant) > 0 else []

    # Низкая дисперсия (std < 1e-10)
    stds = numeric_df.std()
    low_var = stds[stds < 1e-10]
    low_variance_features = list(
        set(low_var.index.tolist()) - set(constant_features)
    )

    return {
        'constant_features': constant_features,
        'low_variance_features': low_variance_features,
    }


def run_data_quality_checks(
    df: pd.DataFrame,
    target_col: str = 'target',
) -> dict[str, Any]:
    """
    Выполнить полную проверку качества данных.

    Args:
        df: DataFrame для проверки.
        target_col: название колонки с целевой переменной.

    Returns:
        Словарь со всеми результатами проверки.
    """
    report: dict[str, Any] = {
        'shape': {'rows': int(df.shape[0]), 'cols': int(df.shape[1])},
        'dtypes': {str(k): str(v) for k, v in df.dtypes.items()},
    }

    report['missing_values'] = check_missing_values(df)
    report['outliers'] = check_outliers(df)
    report['class_balance'] = check_class_balance(df, target_col)
    report['temporal_order'] = check_temporal_order(df)
    report['constant_features'] = check_constant_features(df)

    # Общий вердикт
    issues = []
    if report['missing_values']['has_nulls']:
        issues.append(f"пропуски: {report['missing_values']['total_null']} всего")
    if report['outliers']['has_outliers']:
        n_out = sum(report['outliers']['outlier_counts'].values())
        issues.append(f"выбросы: {n_out} значений")
    if report['class_balance'].get('is_imbalanced'):
        issues.append('дисбаланс классов')
    if not report['temporal_order'].get('is_sorted', True):
        issues.append('нарушен хронологический порядок')
    if report['temporal_order'].get('has_duplicates'):
        issues.append('дубликаты timestamp')
    if report['constant_features']['constant_features']:
        n_const = len(report['constant_features']['constant_features'])
        issues.append(f"константные признаки: {n_const}")

    report['issues'] = issues
    report['is_clean'] = len(issues) == 0

    return report


def validate_dataset(
    X_train: np.ndarray,
    y_train: np.ndarray,
    X_val: np.ndarray,
    y_val: np.ndarray,
    X_test: np.ndarray,
    y_test: np.ndarray,
    feature_names: Optional[list[str]] = None,
) -> dict[str, Any]:
    """
    Проверить готовый датасет перед сохранением.

    Args:
        X_train: признаки обучающей выборки.
        y_train: цели обучающей выборки.
        X_val: признаки валидационной выборки.
        y_val: цели валидационной выборки.
        X_test: признаки тестовой выборки.
        y_test: цели тестовой выборки.
        feature_names: названия признаков.

    Returns:
        Словарь с метаданными датасета.
    """
    metadata: dict[str, Any] = {
        'n_features': X_train.shape[1] if X_train.ndim > 1 else 1,
    }

    if X_train.ndim == 3:
        metadata['n_features'] = X_train.shape[2]
        metadata['seq_len'] = X_train.shape[1]

    metadata['train_samples'] = len(X_train)
    metadata['val_samples'] = len(X_val)
    metadata['test_samples'] = len(X_test)
    metadata['total_samples'] = len(X_train) + len(X_val) + len(X_test)

    # Баланс классов
    if np.issubdtype(y_train.dtype, np.integer):
        for split_name, y in [('train', y_train), ('val', y_val), ('test', y_test)]:
            unique, counts = np.unique(y, return_counts=True)
            class_dist = {int(k): int(v) for k, v in zip(unique, counts)}
            metadata[f'class_dist_{split_name}'] = class_dist
            metadata[f'class_pct_{split_name}'] = {
                int(k): round(float(v) / len(y) * 100, 1)
                for k, v in zip(unique, counts)
            }

    # Проверка NaN
    metadata['nan_count'] = {
        'train': int(np.isnan(X_train).sum()),
        'val': int(np.isnan(X_val).sum()),
        'test': int(np.isnan(X_test).sum()),
    }

    # Feature names
    if feature_names:
        metadata['feature_names'] = feature_names

    return metadata
