import numpy as np
import pandas as pd
import logging
from scipy.stats import skew, kurtosis
from .audit import AuditTrail

logger = logging.getLogger("ScoutML.Scout")


class Scout:
    def __init__(self, audit: AuditTrail, target_col, task_type=None, threshold=0.95):
        self.audit = audit
        self.target_col = target_col
        self.task_type = task_type
        self.threshold = threshold  # Threshold para correlação/leakage

        self.problem_type = None
        self.numeric_cols = []
        self.categorical_cols = []
        self.dropped_cols = []
        self.is_imbalanced = False
        self.cardinality_map = {}

    def analyze(self, df: pd.DataFrame):
        """Executa a varredura profunda no dataset."""
        logger.info(
            f"🕵️ [Scout v2] Iniciando Deep Scan Estatístico (Sensitivity: {self.threshold})..."
        )

        # Metadados Básicos
        self.audit.metadata["dataset_shape"] = df.shape
        self.audit.metadata["target"] = self.target_col

        # 1. Inferência de Tipos e Problema
        self._infer_problem_type(df)

        # 2. Perfilamento Estatístico Avançado (NOVO v2.0)
        # Calcula Skewness, Kurtosis e Outliers para guiar o Forge
        self._run_statistical_profiling(df)

        # 3. Verificações de Qualidade e Estrutura
        if self.problem_type != "regression":
            self._check_imbalance(df)

        self._check_data_quality(df)
        self._classify_columns(df)  # Define quem é numérico/categórico
        self._check_leakage(df)  # Caça vazamento de dados
        self._check_collinearity(df)  # Remove redundâncias

        return self

    def _run_statistical_profiling(self, df):
        """
        Calcula estatísticas avançadas (Skew, Kurtosis, Outliers)
        e registra no AuditTrail para o relatório final.
        """
        stats_data = []

        # Analisa apenas colunas numéricas
        numeric_df = df.select_dtypes(include=np.number)

        for col in numeric_df.columns:
            # Pula o target para não enviesar a análise de features
            if col == self.target_col:
                continue

            # Remove nulos para cálculo estatístico limpo
            series = numeric_df[col].dropna()
            if len(series) == 0:
                continue

            # --- 1. Testes de Normalidade e Forma ---
            # Skewness: Mede a assimetria da distribuição (>1 ou <-1 indica forte viés)
            skew_val = skew(series)
            # Kurtosis: Mede o "achatamento" ou caudas pesadas
            kurt_val = kurtosis(series)

            # --- 2. Detecção de Outliers (Método IQR) ---
            Q1 = series.quantile(0.25)
            Q3 = series.quantile(0.75)
            IQR = Q3 - Q1
            lower_bound = Q1 - 1.5 * IQR
            upper_bound = Q3 + 1.5 * IQR

            # Conta quantos valores caem fora dos limites
            outliers_count = ((series < lower_bound) | (series > upper_bound)).sum()
            outliers_pct = outliers_count / len(series)

            # --- 3. Recomendação Automática para o Forge ---
            # Essa lógica define qual Scaler será usado automaticamente
            recommendation = "StandardScaler"  # Default

            if outliers_pct > 0.05:
                # Se mais de 5% dos dados são outliers, StandardScaler é péssimo.
                recommendation = "RobustScaler"
            elif abs(skew_val) > 1.0:
                # Se os dados são muito tortos, tenta gaussianizar.
                recommendation = "PowerTransformer"

            stats_data.append(
                {
                    "column": col,
                    "missing_pct": round(df[col].isnull().mean(), 4),
                    "skewness": round(skew_val, 2),
                    "kurtosis": round(kurt_val, 2),
                    "outliers_pct": round(outliers_pct, 4),
                    "recommendation": recommendation,
                }
            )

        # Envia para o AuditTrail (Agora o Audit sabe lidar com DataFrame)
        if stats_data:
            df_stats = pd.DataFrame(stats_data)
            self.audit.register_feature_stats(df_stats)
            logger.info(
                f"📊 Estatísticas calculadas para {len(stats_data)} features numéricas."
            )

    def optimize_memory(self, df: pd.DataFrame) -> pd.DataFrame:
        """Reduz uso de memória e sanitiza infinitos."""
        start_mem = df.memory_usage().sum() / 1024**2

        # Sanitização de Infinitos (Vacina contra erros do Sklearn)
        if np.isinf(df.select_dtypes(include=np.number)).values.any():
            logger.warning("⚠️ [Scout] Infinitos detectados! Convertendo para NaN...")
            df = df.replace([np.inf, -np.inf], np.nan)

        # Downcasting Float
        for col in df.select_dtypes(include=["float64"]).columns:
            if df[col].isnull().all():
                continue
            c_min, c_max = df[col].min(), df[col].max()
            if c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                df[col] = df[col].astype(np.float32)

        # Downcasting Int
        for col in df.select_dtypes(include=["int64"]).columns:
            if df[col].isnull().all():
                continue
            c_min, c_max = df[col].min(), df[col].max()
            if c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                df[col] = df[col].astype(np.int32)

        end_mem = df.memory_usage().sum() / 1024**2
        self.audit.log_action(
            "optimization", f"Memória otimizada: {start_mem:.2f}MB -> {end_mem:.2f}MB"
        )
        return df

    def _infer_problem_type(self, df):
        y = df[self.target_col]
        if self.task_type:
            self.problem_type = self.task_type
        else:
            if pd.api.types.is_numeric_dtype(y) and y.nunique() > 20:
                self.problem_type = "regression"
            else:
                self.problem_type = "binary" if y.nunique() == 2 else "multiclass"

        self.audit.metadata["problem_type"] = self.problem_type
        self.audit.log_action(
            "inference", f"Tipo de Problema inferido: {self.problem_type}"
        )

    def _check_leakage(self, df):
        logger.info(
            f"🕵️ [Scout] Caçando Leakage via 'The Sentinel' (Threshold: {self.threshold})..."
        )
        potential_leaks = []

        # Prepara dados para o teste (remove target e já dropados)
        X = df.drop(columns=[self.target_col] + self.dropped_cols, errors="ignore")
        y = df[self.target_col]

        # Codifica target se necessário
        if self.problem_type != "regression" and not pd.api.types.is_numeric_dtype(y):
            y = y.astype("category").cat.codes

        # Seleciona features candidatas (Numéricas ou Categóricas convertidas)
        candidates = []
        X_encoded = pd.DataFrame()  # DataFrame temporário

        # 1. Numéricas
        num_cols = X.select_dtypes(include=np.number).columns
        for c in num_cols:
            X_encoded[c] = X[c].fillna(0)  # Fillna rápido para teste
            candidates.append(c)

        # 2. Categóricas (Ordinal Encoding rápido)
        cat_cols = X.select_dtypes(exclude=np.number).columns
        for c in cat_cols:
            X_encoded[c] = X[c].astype("category").cat.codes
            candidates.append(c)

        # Importações locais para evitar ciclo ou peso desnecessário no topo
        from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
        from sklearn.model_selection import cross_val_score

        # O Sentinel: Árvore de profundidade 1
        if self.problem_type == "regression":
            sentinel = DecisionTreeRegressor(max_depth=1)
            scoring = "r2"
        else:
            sentinel = DecisionTreeClassifier(max_depth=1)
            scoring = "roc_auc"

        for col in candidates:
            try:
                # Teste isolado
                feature_data = X_encoded[[col]]
                scores = cross_val_score(
                    sentinel, feature_data, y, cv=3, scoring=scoring
                )
                score_mean = scores.mean()

                if score_mean > self.threshold:
                    logger.warning(
                        f"🚨 Leakage Detectado em '{col}': Score isolado = {score_mean:.4f}"
                    )
                    potential_leaks.append(col)
            except Exception:
                pass

        if potential_leaks:
            self.dropped_cols.extend(potential_leaks)
            self.audit.log_action(
                "feature_action",
                f"🚨 LEAKAGE (Sentinel > {self.threshold}) REMOVIDO",
                str(potential_leaks),
            )

    def _check_collinearity(self, df):
        logger.info(f"👯 [Scout] Checando Multicolinearidade > {self.threshold}...")
        try:
            numeric_df = df.select_dtypes(include=[np.number]).drop(
                columns=[self.target_col], errors="ignore"
            )
            if numeric_df.empty:
                return

            corr_matrix = numeric_df.corr().abs()
            upper = corr_matrix.where(
                np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)
            )

            to_drop = [
                column
                for column in upper.columns
                if any(upper[column] > self.threshold)
            ]

            if to_drop:
                # Remove duplicatas com leakage
                to_drop = [c for c in to_drop if c not in self.dropped_cols]

                if to_drop:
                    logger.info(f"✂️ [Scout] Removendo Redundantes: {to_drop}")
                    self.dropped_cols.extend(to_drop)
                    self.audit.log_action(
                        "feature_action", "Multicollinearity Removal", str(to_drop)
                    )
        except Exception as e:
            logger.warning(f"⚠️ Erro no teste de colinearidade: {e}")

    def _check_data_quality(self, df):
        # Nulos > 95%
        null_counts = df.isnull().mean()
        drop_nulls = null_counts[null_counts > 0.95].index.tolist()

        # Colunas Constantes (1 único valor)
        const_cols = [c for c in df.columns if df[c].nunique() <= 1]

        to_drop = list(set(drop_nulls + const_cols))
        if self.target_col in to_drop:
            to_drop.remove(self.target_col)

        # Filtra se já foi dropado
        to_drop = [c for c in to_drop if c not in self.dropped_cols]

        if to_drop:
            self.dropped_cols.extend(to_drop)
            self.audit.log_action(
                "feature_action", "Colunas removidas (Baixa Qualidade)", str(to_drop)
            )

    def _classify_columns(self, df):
        # Identifica Numéricas vs Categóricas (e caça IDs)
        valid_cols = [
            c for c in df.columns if c != self.target_col and c not in self.dropped_cols
        ]
        X = df[valid_cols]

        for col in X.columns:
            # Lógica de ID: Alta cardinalidade (>98%) em string/object/int
            is_candidate_id = (
                pd.api.types.is_string_dtype(X[col])
                or pd.api.types.is_object_dtype(X[col])
                or pd.api.types.is_integer_dtype(X[col])
            )

            if is_candidate_id:
                n_unique = X[col].nunique()
                ratio = n_unique / len(X)
                if ratio > 0.98:
                    self.dropped_cols.append(col)
                    self.audit.log_action(
                        "feature_action",
                        f"Coluna de ID removida (Card: {ratio:.1%}): {col}",
                    )
                    continue

            if pd.api.types.is_numeric_dtype(X[col]):
                self.numeric_cols.append(col)
            else:
                self.categorical_cols.append(col)
                self.cardinality_map[col] = X[col].nunique()

    def _check_imbalance(self, df):
        counts = df[self.target_col].value_counts(normalize=True)
        minority = counts.min()
        if minority < 0.10:
            self.is_imbalanced = True
            self.audit.log_action(
                "inference",
                f"Dataset Desbalanceado detectado. Classe minoritária: {minority:.1%}",
            )
