"""Data handling utilities: outlier treatment and empirical CDF.

This module provides the :class:`WDataHandler` class with lightweight
helpers for cleaning numeric data before feeding it into models or
indicators.

Example:
    >>> from wael_lib import WDataHandler
    >>> dh = WDataHandler()
    >>> df["Close"] = dh.handle_outliers(df, "Close")
"""

from __future__ import annotations

from typing import Tuple

import numpy as np
import pandas as pd


class WDataHandler:
    """Utility class for data cleaning and statistical helpers.

    Example:
        >>> dh = WDataHandler()
        >>> df["Volume"] = dh.handle_outliers(df, "Volume")
        >>> sorted_vals, ecdf = dh.calculate_ECDF(df["Close"].values)
    """

    def __init__(self) -> None:
        """Initialise the WDataHandler (no configuration required)."""
        pass

    def handle_outliers(self, df: pd.DataFrame, feature: str) -> pd.Series:
        """Clip outliers using IQR (Interquartile Range) fencing.

        Values above ``Q3 + 1.5 * IQR`` are clipped to the upper fence and
        values below ``Q1 - 1.5 * IQR`` are clipped to the lower fence.

        Args:
            df (pd.DataFrame): DataFrame containing the column to clean.
            feature (str): Name of the column whose outliers should be
                clipped.

        Returns:
            pd.Series: The clipped column (the column in *df* is also
            modified **in-place**).

        Note:
            This method **mutates** the column inside *df*.

        Example:
            >>> dh = WDataHandler()
            >>> df["Volume"] = dh.handle_outliers(df, "Volume")
        """
        Q1 = df[feature].quantile(0.25)
        Q3 = df[feature].quantile(0.75)
        IQR = Q3 - Q1
        upper_fense = Q3 + 1.5 * IQR
        lower_fense = Q1 - 1.5 * IQR
        df[feature] = np.where(
            df[feature] > upper_fense,
            upper_fense,
            np.where(df[feature] < lower_fense, lower_fense, df[feature]),
        )
        return df[feature]

    def calculate_ECDF(
        self,
        data: np.ndarray,
        percent: bool = False,
    ) -> Tuple[np.ndarray, np.ndarray]:
        """Calculate the Empirical Cumulative Distribution Function (ECDF).

        The ECDF is the *survival* variant (1 - CDF) so the first value
        starts near 1.0 (or 100 %) and decreases towards 0.

        Args:
            data (np.ndarray): 1-D array-like of numeric observations.
            percent (bool): If ``True``, multiply ECDF values by 100 so
                they represent percentages.  Defaults to ``False``.

        Returns:
            Tuple[np.ndarray, np.ndarray]: ``(sorted_data, ecdf)`` where:

            * **sorted_data** – observations sorted in ascending order.
            * **ecdf** – corresponding survival probabilities (or
              percentages if *percent* is ``True``).

        Example:
            >>> dh = WDataHandler()
            >>> sorted_vals, ecdf = dh.calculate_ECDF(df["Close"].values)
            >>> sorted_vals, ecdf_pct = dh.calculate_ECDF(df["Close"].values, percent=True)
        """
        sorted_data = np.sort(data)
        n = len(data)
        ecdf = (n - np.arange(1, n + 1)) / n
        if percent:
            ecdf *= 100
        return sorted_data, ecdf
