"""
Accessor .profile para pandas DataFrame.

Registra un accessor que permite perfilar DataFrames directamente:

    >>> import tai_storage.data  # activa el accessor al importar
    >>> df.profile.get('columna')
    >>> df.profile['columna']     # alias de get()
    >>> df.profile.show()
    >>> df.profile.show_correlation_matrix()

Los perfiles se cachean por id de DataFrame. Cuando se accede a
`df.profile` por primera vez se crea un `Profiles`; las llamadas
siguientes reutilizan el mismo objeto sin recalcular.
"""
from __future__ import annotations

import weakref
from typing import Union

import pandas as pd

from .profile import ColumnProfile, Profiles

# Cache: id(df) → (weakref a df, Profiles)
# La weakref permite detectar entradas obsoletas si se reutiliza un id.
_profiles_cache: dict[int, tuple[weakref.ref, Profiles]] = {}


def _get_or_create_profiles(df: pd.DataFrame) -> Profiles:
    df_id = id(df)
    if df_id in _profiles_cache:
        ref, profiles = _profiles_cache[df_id]
        if ref() is df:
            return profiles
        # id reutilizado por otro DataFrame: reemplazar entrada
        del _profiles_cache[df_id]

    profiles = Profiles(df)
    try:
        _profiles_cache[df_id] = (weakref.ref(df), profiles)
    except TypeError:
        # DataFrame no soporta weakref: cachear solo por id
        _profiles_cache[df_id] = (lambda: df, profiles)  # type: ignore[assignment]
    return profiles


@pd.api.extensions.register_dataframe_accessor("profile")
class ProfileAccessor:
    """
    Accessor .profile para pandas DataFrame.

    Expone la funcionalidad de `Profiles` directamente sobre cualquier DataFrame.
    Los perfiles calculados se cachean entre llamadas a `df.profile`.

    Examples:
        >>> import pandas as pd
        >>> import tai_storage.data  # activa el accessor
        >>>
        >>> df = pd.DataFrame({'a': [1, 2, 3], 'b': ['x', 'y', 'x']})
        >>>
        >>> df.profile.data_name = 'ventas'
        >>> df.profile.get('a')
        ColumnProfile(name='a', data='ventas')
        >>> df.profile['b']
        ColumnProfile(name='b', data='ventas')
        >>> df.profile.generate().show()
    """

    def __init__(self, pandas_obj: pd.DataFrame):
        self._obj = pandas_obj

    @property
    def _profiles(self) -> Profiles:
        """Profiles cacheado para este DataFrame."""
        return _get_or_create_profiles(self._obj)

    # ------------------------------------------------------------------
    # Configuración
    # ------------------------------------------------------------------

    @property
    def data_name(self) -> str:
        """Nombre del dataset (usado en reportes y en integridad referencial)."""
        return self._profiles.data_name

    @data_name.setter
    def data_name(self, value: str) -> None:
        self._profiles.data_name = value

    # ------------------------------------------------------------------
    # Acceso a perfiles individuales
    # ------------------------------------------------------------------

    def get(self, column_name: str) -> ColumnProfile:
        """
        Retorna el perfil de una columna (generándolo si no existe aún).

        Args:
            column_name: Nombre de la columna.

        Returns:
            ColumnProfile de la columna.

        Raises:
            KeyError: Si la columna no existe en el DataFrame.
        """
        return self._profiles.get(column_name)

    def __getitem__(self, column_name: str) -> ColumnProfile:
        """Alias de `get()`: permite `df.profile['columna']`."""
        return self.get(column_name)

    # ------------------------------------------------------------------
    # Generación y visualización
    # ------------------------------------------------------------------

    def generate(self) -> ProfileAccessor:
        """
        Genera perfiles para todas las columnas (solo las no calculadas aún).

        Returns:
            self (para encadenamiento).
        """
        self._profiles.generate()
        return self

    def show(self) -> None:
        """
        Muestra el perfil de todas las columnas.

        Genera automáticamente los perfiles pendientes antes de mostrar.
        """
        self._profiles.generate()
        self._profiles.show()

    def show_correlation_matrix(self, include_na: bool = True) -> None:
        """
        Muestra la matriz de coincidencias entre columnas como heatmap.

        Args:
            include_na: Si True, cuenta como coincidencia cuando ambas columnas son nulas.
        """
        self._profiles.show_correlation_matrix(include_na=include_na)

    def show_duplicates_by_key(self, main_column: Union[str, list]) -> None:
        """
        Muestra las columnas con valor constante dentro de cada grupo de `main_column`.

        Args:
            main_column: Columna o lista de columnas a usar como clave de agrupación.
        """
        self._profiles.show_duplicates_by_key(main_column)

    # ------------------------------------------------------------------
    # Representación
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        return repr(self._profiles)
