import numpy as np
import pandas as pd
from pandas import DataFrame
from typing import Any
import re

from ruhken_utils.styling import RED, GRE, RES
from ruhken_utils.string_helper_functions import camel_to_snake

def expand_dic_column(df, column_name, prefix=None):
    """
    Expands a column containing dictionaries into separate columns without modifying the original DataFrame.

    For example, a 'vessel' column with dicts will be expanded into
    'vessel_id', 'vessel_name', 'vessel_type', etc.

    Args:
        df (pd.DataFrame): The original DataFrame.
        column_name (str): The name of the column containing dictionaries.
        prefix (str, optional): Prefix for the new columns. If None, the original
                                column name is used as prefix.

    Returns:
        pd.DataFrame: A **copy** of the DataFrame with the expanded columns appended.
                      The original column is kept.

    Raises:
        ValueError: If `column_name` is not present in the DataFrame.
    """
    if column_name not in df.columns:
        raise ValueError(f"Column '{column_name}' does not exist in the DataFrame.")
    if prefix is None:
        prefix = column_name

    df_copy = df.copy()

    expanded = pd.json_normalize(df_copy[column_name].dropna())
    expanded = expanded.add_prefix(f"{prefix}_")
    expanded.index = df_copy.loc[df_copy[column_name].notna()].index

    return df_copy.join(expanded)

def get_unique_number_patterns(series):
    """
    Extract all unique number-length patterns from a string Series.

    Parameters
    ----------
    series : pd.Series
        Series containing string values

    Returns
    -------
    list of str
        Unique patterns in the order of occurrence
    """
    # transform each string to number-length pattern
    transformed = series.astype(str).apply(
        lambda x: re.sub(r"\d+", lambda m: str(len(m.group())), x)
    )

    # unique patterns, in order of appearance
    seen = set()
    unique_patterns = []
    for pat in transformed:
        if pat not in seen:
            seen.add(pat)
            unique_patterns.append(pat)

    return unique_patterns

def clean_feature_names(columns):
    """
    Cleans column names (camel to snake, replace space with _,

    Parameters
    ----------
    columns : array of column names
    """
    cleaned = []
    for col in columns:
        col = col.strip()
        col = col.replace(" ", "_")
        col = camel_to_snake(col)
        cleaned.append(col)
    return cleaned

def _aggregate_df(df: DataFrame, feature_names: list[str] | None, ignore_feature_names : list[str] | None,
                  target_feature: Any | None, target_feature_name: str | None) -> tuple[Any, Any, str]:
    has_target_feature = target_feature_name is not None or target_feature is not None
    df = df.copy()

    if ignore_feature_names is not None:
        if feature_names is None:
            feature_names = [col for col in df.columns if col not in ignore_feature_names]
        else:
            feature_names = [col for col in feature_names if col not in ignore_feature_names]

    if feature_names is not None:
        if target_feature_name is not None:
            feature_names.append(target_feature_name)
        df = df.loc[:, df.columns.intersection(feature_names)]

    if has_target_feature:
        if target_feature_name is None:
            target_feature_name = "Target"
        if target_feature is not None:
            df[target_feature_name] = target_feature
    return df, has_target_feature, target_feature_name

def validate_pipeline(pipeline, X_train, X_test, pipeline_name, verbose = True):
    X_train_transformed = pipeline.fit_transform(X_train)
    X_test_transformed = pipeline.transform(X_test)

    print(f"{pipeline_name}: {X_train_transformed.shape}")
    train_nan_sum = np.isnan(X_train_transformed).sum()
    test_nan_sum = np.isnan(X_test_transformed).sum()

    if verbose:
        if train_nan_sum > 0:
            print(f"NaNs in X_train: {RED}{train_nan_sum}{RES}")

        if test_nan_sum > 0:
            print(f"NaNs in X_test: {RED}{test_nan_sum}{RES}")

        if train_nan_sum == 0 and test_nan_sum == 0:
            print(f"{GRE}no NaNs detected{RES}")

        print()



    return pd.DataFrame(X_train_transformed, columns=X_train.columns, index=X_train.index), pd.DataFrame(X_test_transformed, columns=X_test.columns, index=X_test.index)
