"""
Inferencia y conversión automática de tipos para Series de pandas.

Proporciona herramientas para detectar y convertir tipos de datos automáticamente
desde strings a los tipos apropiados (int, float, datetime, bool, etc.).
"""
import datetime as dt
from typing import Optional, Literal

try:
    import pandas as pd
    import numpy as np
except ImportError:
    raise ImportError(
        "El módulo 'data' requiere pandas y numpy. "
        "Instala con: pip install tai-storage[data]"
    )


class TypeInferencer:
    """
    Infiere y convierte automáticamente el tipo de datos de una Serie de pandas.
    
    Detecta y convierte a:
    - Booleanos (bool)
    - Numéricos (Int64, Float64)
    - Fechas y horas (datetime64)
    - Timedelta (timedelta64)
    - Strings (str)
    
    Examples:
        >>> import pandas as pd
        >>> series = pd.Series(['1', '2', '3', None, '5'])
        >>> inferencer = TypeInferencer(series)
        >>> converted = inferencer.convert()
        >>> converted.dtype
        Int64Dtype()
        
        >>> dates = pd.Series(['2024-01-31', '2024-02-01', None])
        >>> inferencer = TypeInferencer(dates)
        >>> converted = inferencer.convert()
        >>> converted.dtype
        dtype('<M8[ns]')
    """
    
    def __init__(self, series: pd.Series):
        """
        Inicializa el inferenciador con una Serie.
        
        Args:
            series: Serie de pandas a analizar y convertir
        """
        self.original_series = series.copy()
        
        # Limpiar valores vacíos y errores comunes
        try:
            self.series = series.replace(r'^\s*$', None, regex=True)
            self.series = self.series.replace(r'^#N/A$', None, regex=True)
            self.series = self.series.replace(r'^N/A$', None, regex=True)
            self.series = self.series.infer_objects(copy=False)
        except Exception:
            self.series = series.copy()
        
        self.notna_series = self.series.dropna()
        
        self._typed_series: Optional[pd.Series] = None
        self._inferred_type: Optional[str] = None
        self._date_format: Optional[str] = None
    
    @property
    def is_empty(self) -> bool:
        """Verifica si la serie no tiene valores no-nulos."""
        return self.notna_series.empty
    
    @property
    def is_boolean(self) -> bool:
        """Verifica si la serie es de tipo booleano."""
        return pd.api.types.is_bool_dtype(self.notna_series.values)
    
    @property
    def is_numeric(self) -> bool:
        """Verifica si la serie ya es numérica."""
        return pd.api.types.is_any_real_numeric_dtype(self.notna_series.values)
    
    @property
    def is_numeric_candidate(self) -> bool:
        """
        Verifica si la serie podría convertirse a numérico.
        
        Detecta strings que representan números, incluso con separadores
        de miles/decimales.
        """
        # Si ya no es string/object, no es candidato
        if not pd.api.types.is_string_dtype(self.notna_series) and \
           not pd.api.types.is_object_dtype(self.notna_series):
            return False
        
        try:
            # Verificar que no haya múltiples puntos (excepto en notación de miles)
            dots = self.notna_series.str.count(r'\.')
            if dots[dots > 1].any():
                return False
            
            # Intentar convertir removiendo separadores comunes
            cleaned = self.notna_series.str.replace(r"[.,]", "", regex=True)
            return cleaned.str.match(r'^-?\d+$').all()
        except AttributeError:
            # Si .str accessor falla, no es una serie de strings
            return False
    
    @property
    def is_datetime(self) -> bool:
        """Verifica si la serie ya es datetime."""
        return pd.api.types.is_datetime64_any_dtype(self.notna_series.values)
    
    @property
    def is_timedelta(self) -> bool:
        """Verifica si la serie es timedelta."""
        return (
            pd.api.types.is_timedelta64_dtype(self.notna_series.values) or
            pd.api.types.is_timedelta64_ns_dtype(self.notna_series.values)
        )
    
    @property
    def date_format(self) -> Optional[str]:
        """
        Detecta el formato de fecha si la serie contiene strings de fechas.
        
        Returns:
            String con el formato detectado o None si no se detecta
        """
        if self._date_format is not None:
            return self._date_format
        
        if self.is_empty or not isinstance(self.notna_series.iloc[0], str):
            return None
        
        # Formatos comunes de fecha/hora
        date_formats = [
            "%Y-%m-%d",                  # 2023-01-31
            "%d-%m-%Y",                  # 31-01-2023
            "%m-%d-%Y",                  # 01-31-2023
            "%Y/%m/%d",                  # 2023/01/31
            "%d/%m/%Y",                  # 31/01/2023
            "%m/%d/%Y",                  # 01/31/2023
            "%Y.%m.%d",                  # 2023.01.31
            "%d.%m.%Y",                  # 31.01.2023
            "%m.%d.%Y",                  # 01.31.2023
            "%d-%b-%Y",                  # 31-Jan-2023
            "%d-%B-%Y",                  # 31-January-2023
            "%B %d, %Y",                 # January 31, 2023
            "%b %d, %Y",                 # Jan 31, 2023
            "%Y-%m-%d %H:%M:%S",         # 2023-01-31 15:30:45
            "%d-%m-%Y %H:%M:%S",         # 31-01-2023 15:30:45
            "%Y/%m/%d %H:%M",            # 2023/01/31 15:30
            "%Y-%m-%dT%H:%M:%S",         # 2023-01-31T15:30:45 (ISO)
            "%Y-%m-%d %I:%M:%S %p",      # 2023-01-31 03:30:45 PM
            "%d/%m/%Y %H:%M:%S",         # 31/01/2023 15:30:45
            "%m/%d/%Y %I:%M %p",         # 01/31/2023 03:30 PM
            "%Y-%m-%d %H:%M:%S.%f",      # 2023-01-31 15:30:45.123456
            "%H:%M:%S",                  # 15:30:45
            "%I:%M:%S %p",               # 03:30:45 PM
            "%H:%M",                     # 15:30
            "%I:%M %p",                  # 03:30 PM
            "%Y-%m-%d %H:%M:%S %z",      # 2023-01-31 15:30:45 +0100
            "%Y-%m-%d %H:%M:%S %Z",      # 2023-01-31 15:30:45 UTC
            "%Y-%m-%dT%H:%M:%S%z",       # 2023-01-31T15:30:45+0100
            "%Y-%m-%dT%H:%M:%S.%f",      # 2023-01-31T15:30:45.123456
        ]
        
        # Probar con el primer valor no nulo
        first_value = str(self.notna_series.iloc[0]).strip()
        
        for formato in date_formats:
            try:
                dt.datetime.strptime(first_value, formato)
                self._date_format = formato
                return formato
            except (ValueError, TypeError):
                continue
        
        return None
    
    @property
    def is_datetime_candidate(self) -> bool:
        """Verifica si la serie podría ser datetime."""
        return self.date_format is not None
    
    @property
    def is_string(self) -> bool:
        """Verifica si la serie es de tipo string."""
        return pd.api.types.is_string_dtype(self.notna_series.values)
    
    @property
    def is_object(self) -> bool:
        """Verifica si la serie es de tipo object."""
        return pd.api.types.is_object_dtype(self.notna_series.values)
    
    @property
    def inferred_type(self) -> Optional[str]:
        """
        Retorna el tipo inferido después de la conversión.
        
        Posibles valores: 'empty', 'bool', 'numeric', 'datetime', 'timedelta', 'string'
        """
        return self._inferred_type
    
    def convert(self) -> pd.Series:
        """
        Convierte la serie al tipo más apropiado detectado automáticamente.
        
        Orden de prioridad:
        1. Empty (si no hay valores)
        2. Boolean (si es bool)
        3. Numeric (si es o puede ser numérico)
        4. Datetime (si es o puede ser fecha/hora)
        5. Timedelta (si es timedelta)
        6. String (por defecto)
        
        Returns:
            Serie convertida al tipo apropiado
        """
        if self._typed_series is not None:
            return self._typed_series
        
        # Serie vacía
        if self.is_empty:
            self._inferred_type = 'empty'
            self._typed_series = self.series
            return self._typed_series
        
        # Booleano
        if self.is_boolean:
            self._inferred_type = 'bool'
            self._typed_series = self.series.astype(bool)
            return self._typed_series
        
        # Numérico (ya convertido)
        if self.is_numeric:
            self._inferred_type = 'numeric'
            try:
                self._typed_series = self.series.astype('Int64')
            except (ValueError, TypeError):
                self._typed_series = self.series.astype('Float64')
            return self._typed_series
        
        # Datetime (ya convertido)
        if self.is_datetime:
            self._inferred_type = 'datetime'
            self._typed_series = self.series
            return self._typed_series
        
        # Timedelta
        if self.is_timedelta:
            self._inferred_type = 'timedelta'
            self._typed_series = self.series
            return self._typed_series
        
        # Datetime candidato (string que puede ser fecha)
        if self.is_datetime_candidate:
            self._inferred_type = 'datetime'
            try:
                self._typed_series = pd.to_datetime(
                    self.series,
                    format=self.date_format,
                    errors='coerce'
                )
                return self._typed_series
            except Exception:
                # Si falla, continuar con otros tipos
                pass
        
        # Numérico candidato (string que puede ser número)
        if self.is_numeric_candidate:
            self._inferred_type = 'numeric'
            try:
                # Intentar convertir a entero primero
                self._typed_series = pd.to_numeric(self.series, errors='coerce').astype('Int64')
                return self._typed_series
            except (ValueError, TypeError):
                # Si falla, intentar float
                try:
                    self._typed_series = pd.to_numeric(self.series, errors='coerce').astype('Float64')
                    return self._typed_series
                except Exception:
                    pass
        
        # Por defecto: String
        if self.is_object or self.is_string:
            self._inferred_type = 'string'
            try:
                self._typed_series = self.series.astype(str)
            except Exception:
                self._typed_series = self.series
            return self._typed_series
        
        # Fallback: retornar serie original
        self._inferred_type = 'unknown'
        self._typed_series = self.series
        return self._typed_series


def infer_and_convert_dataframe(
    df: pd.DataFrame,
    columns: Optional[list[str]] = None,
    replace_empty: bool = True
) -> pd.DataFrame:
    """
    Aplica inferencia de tipos a todas las columnas de un DataFrame.
    
    Args:
        df: DataFrame a convertir
        columns: Lista de columnas a convertir (None = todas)
        replace_empty: Si True, reemplaza strings vacíos con NA
        
    Returns:
        DataFrame con tipos inferidos
        
    Examples:
        >>> df = pd.DataFrame({
        ...     'id': ['1', '2', '3'],
        ...     'fecha': ['2024-01-31', '2024-02-01', '2024-02-02'],
        ...     'valor': ['100.5', '200.3', '150.0']
        ... })
        >>> df_typed = infer_and_convert_dataframe(df)
        >>> df_typed.dtypes
        id        Int64
        fecha    datetime64[ns]
        valor    Float64
        dtype: object
    """
    df_result = df.copy()
    
    if replace_empty:
        df_result = df_result.replace({np.nan: pd.NA, r'^\s*$': pd.NA}, regex=True)
    
    columns_to_convert = columns if columns is not None else df_result.columns
    
    for col in columns_to_convert:
        if col not in df_result.columns:
            continue
        
        inferencer = TypeInferencer(df_result[col])
        df_result[col] = inferencer.convert()
    
    return df_result
