"""
Модуль отбора признаков (Feature Selection).

Функции:
    select_features_by_mutual_info — отбор топ-K признаков по взаимной информации
    get_common_features — единый список признаков для cross-ticker consistency
    plot_feature_importance — визуализация важности признаков

Использует sklearn.feature_selection.mutual_info_classif для оценки
взаимной информации между каждым признаком и целевой переменной.
"""

import logging
from typing import List, Optional, Tuple

import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)

# Полный список из 61 признака (единый для всех тикеров)
ALL_FEATURES = [
    'Open', 'High', 'Low', 'Close', 'Volume',
    'ret_1', 'ret_5', 'ret_10', 'ret_21',
    'log_ret_1', 'high_low_ratio', 'close_open_ratio',
    'upper_shadow', 'lower_shadow', 'spread_pct', 'close_position',
    'ema_9', 'ema_21', 'ema_50',
    'sma_20', 'sma_50', 'sma_200',
    'price_to_ema9', 'price_to_sma50', 'price_to_sma200',
    'macd', 'macd_signal', 'macd_hist', 'macd_hist_pct',
    'adx', 'plus_di', 'minus_di',
    'rsi_14', 'rsi_7', 'stoch_k', 'stoch_d', 'cci_20',
    'mfi_14', 'mfi_7',
    'bb_lower', 'bb_middle', 'bb_upper', 'bb_width', 'bb_position',
    'atr_14', 'atr_pct',
    'obv', 'volume_sma_20', 'volume_ratio', 'volume_change', 'volume_change_5',
    'day_of_week', 'month', 'quarter', 'day_of_month',
    'is_month_end', 'is_quarter_end',
    'day_sin', 'day_cos', 'month_sin', 'month_cos',
]


def select_features_by_mutual_info(
    X: np.ndarray,
    y: np.ndarray,
    feature_names: List[str],
    top_k: int = 30,
    random_state: int = 42,
) -> Tuple[List[str], np.ndarray, np.ndarray]:
    """
    Отобрать топ-K признаков по взаимной информации с целевой переменной.

    Args:
        X: матрица признаков (n_samples, n_features).
        y: целевая переменная (n_samples,) с классами 0/1/2.
        feature_names: список названий признаков длины n_features.
        top_k: количество отбираемых признаков.
        random_state: seed для воспроизводимости.

    Returns:
        Кортеж (selected_features, mi_scores, X_selected), где:
            selected_features: список названий топ-K признаков.
            mi_scores: массив MI-оценок для всех признаков.
            X_selected: матрица X только с отобранными признаками.
    """
    from sklearn.feature_selection import mutual_info_classif

    # Проверка NaN/Inf
    X_clean = np.nan_to_num(X, nan=0.0, posinf=10.0, neginf=-10.0)

    # Расчёт mutual information
    mi_scores = mutual_info_classif(
        X_clean, y,
        random_state=random_state,
    )

    # Сортировка по убыванию
    sorted_indices = np.argsort(mi_scores)[::-1]

    # Отбор топ-K
    top_indices = sorted_indices[:top_k]
    selected_features = [feature_names[i] for i in top_indices]
    X_selected = X_clean[:, top_indices]

    logger.info(f"Feature selection: {len(feature_names)} → {top_k} признаков")
    logger.info(f"  Топ-5: {selected_features[:5]}")
    logger.info(f"  MI scores (топ-5): {mi_scores[top_indices[:5]].round(4)}")

    return selected_features, mi_scores, X_selected


def get_feature_ranking(
    df: pd.DataFrame,
    target_col: str = 'target',
    feature_cols: Optional[List[str]] = None,
) -> pd.DataFrame:
    """
    Получить ранжирование всех признаков по mutual information.

    Args:
        df: DataFrame с признаками и целевой колонкой.
        target_col: название колонки с целевой переменной.
        feature_cols: список колонок-признаков (если None — все кроме target).

    Returns:
        DataFrame с колонками: feature, mi_score, rank.
    """
    from sklearn.feature_selection import mutual_info_classif

    if feature_cols is None:
        feature_cols = [c for c in df.columns if c != target_col]

    X = df[feature_cols].values.astype(np.float64)
    y = df[target_col].values

    X = np.nan_to_num(X, nan=0.0, posinf=10.0, neginf=-10.0)

    mi_scores = mutual_info_classif(X, y, random_state=42)

    ranking = pd.DataFrame({
        'feature': feature_cols,
        'mi_score': mi_scores,
    }).sort_values('mi_score', ascending=False)
    ranking['rank'] = range(1, len(ranking) + 1)

    return ranking


def apply_feature_selection(
    df: pd.DataFrame,
    top_k: int = 30,
    target_col: str = 'target',
) -> Tuple[pd.DataFrame, List[str]]:
    """
    Применить feature selection к DataFrame.

    Args:
        df: DataFrame с признаками и целевой колонкой.
        top_k: количество отбираемых признаков.
        target_col: название колонки с целевой переменной.

    Returns:
        Кортеж (df_filtered, selected_features).
    """
    exclude_cols = {'timestamp', 'Date', 'Time', target_col}
    feature_cols = [c for c in df.columns if c not in exclude_cols]

    ranking = get_feature_ranking(df, target_col, feature_cols)
    selected = ranking.head(top_k)['feature'].tolist()

    # Логируем результат
    logger.info(f"Feature selection: {len(feature_cols)} → {top_k}")
    logger.info(f"  Топ-5: {selected[:5]}")
    for _, row in ranking.head(10).iterrows():
        logger.info(f"    #{row['rank']:2d} {row['feature']:20s} MI={row['mi_score']:.4f}")

    keep_cols = list(exclude_cols & set(df.columns)) + selected
    df_filtered = df[keep_cols].copy()
    return df_filtered, selected
