import pandas as pd
import numpy as np
from .Preprocessing import classify

def detect_outliers_iqr(group, variable, date_column, eval_period):
    
    """# 📊 IQR MODEL (Interquartile Range)
    ---

    The `detect_outliers_iqr` function is a statistical anomaly detection tool that identifies outliers by calculating the **Interquartile Range (IQR)** through a dynamic, **expanding window approach**.

    ## 🔍 Functional Overview
    The function partitions data into a baseline **"training" set** and an **"evaluation" period**. It identifies **"normal" data** as values falling within:
    > $$[Q1 - 1.5 \\times IQR, Q3 + 1.5 \\times IQR]$$

    Any data point exceeding these calculated boundaries is flagged as a statistical anomaly.

    ## 🧠 Core Logic Stages

    ### 1. Data Preparation and Validation
    * **Minimum Threshold:** To ensure statistical significance, the function requires at least **10 data points**; if the threshold isn't met, it returns an empty DataFrame.
    * **Safe Copying:** It operates on a **copy** of the input group to protect the original dataset from unintended modifications.

    ### 2. Initial Training Block
    * **Static Baseline:** For the initial block (all data before the `eval_period`), the function calculates a single set of baseline quartiles ($Q1$ and $Q3$) and the resulting $IQR$.
    * **Fixed Boundaries:** The lower bound is set to $max(Q1 - 1.5 \\times IQR, 0)$ and the upper bound to $Q3 + 1.5 \\times IQR$.
    * **Batch Classification:** These fixed bounds are applied to all rows in the training set, assigning them a **"TRAIN"** label and a boolean `is_IQR_anomaly` flag.

    ### 3. Expanding Window Evaluation
    * **Incremental Recalculation:** For every point in the evaluation period (the last $n$ points), the function recalculates $Q1$, $Q3$, and $IQR$ using **all previously observed data**.
    * **Dynamic Adaptation:** As the loop iterates, the training window **"expands."** This allows the model to adjust its expectations of "normal" as more data becomes available.
    * **Live Simulation:** By testing point $i$ against thresholds derived from points $0$ to $i-1$, the function accurately simulates how the outlier detection would behave in a production environment.

    ## 📤 Key Output Columns
    The function appends several analytical columns to the returned DataFrame:
    * **`Q1` / `Q3` / `IQR`**: The specific quartiles and range used for that row's calculation.
    * **`IQR_low` / `IQR_high`**: The calculated "fences" (bounds). The lower bound is clipped at zero.
    * **`set`**: Categorizes the row as either **"TRAIN"** or **"TEST"**.
    * **`IQR_anomaly`**: A descriptive label (e.g., "High," "Low," or "Normal").
    * **`is_IQR_anomaly`**: A boolean flag identifying if the value is an outlier.

    ## 💡 Usage Context
    The IQR method is a **classic, non-parametric approach** to anomaly detection. It is particularly effective for datasets where you **cannot assume a normal (Gaussian) distribution**.

    By using the expanding window, this function is more robust than a simple static boxplot, as it accounts for a growing history of data while remaining less sensitive to extreme outliers than mean-based methods (like Z-Score)."""
    
    n = len(group)
    if n < 10:
        return pd.DataFrame(columns=group.columns)

    group = group.copy()
    # Explicitly ensure date_column is datetime right at the start
    group[date_column] = pd.to_datetime(group[date_column])
    train_size = n - eval_period

    # --- 1. HANDLE TRAINING DATA (Initial Block) ---
    # Calculate baseline IQR using all data available before eval_period
    initial_train = group[variable].iloc[:train_size]
    
    q1 = initial_train.quantile(0.25)
    q3 = initial_train.quantile(0.75)
    iqr = q3 - q1
    
    low = max(q1 - 1.5 * iqr, 0)
    high = q3 + 1.5 * iqr

    # Assign initial bounds to the training rows
    group.loc[group.index[:train_size], 'Q1'] = q1
    group.loc[group.index[:train_size], 'Q3'] = q3
    group.loc[group.index[:train_size], 'IQR'] = iqr
    group.loc[group.index[:train_size], 'IQR_low'] = low
    group.loc[group.index[:train_size], 'IQR_high'] = high
    group.loc[group.index[:train_size], 'set'] = "TRAIN"
    group.loc[group.index[:train_size], 'IQR_anomaly'] = group[variable].iloc[:train_size].apply(
        lambda x: classify(x, low, high)
    )
    group.loc[group.index[:train_size], 'is_IQR_anomaly'] = (
        (group[variable].iloc[:train_size] < low) | 
        (group[variable].iloc[:train_size] > high)
    )

    
    # --- 2. HANDLE EVALUATION DATA (Expanding Window) ---
    # Iterate through the eval period, increasing the training set one point at a time
    for i in range(train_size, n):
        # Data available up to this point (expanding)
        current_train = group[variable].iloc[:i]
        
        Q1 = current_train.quantile(0.25)
        Q3 = current_train.quantile(0.75)
        IQR = Q3 - Q1
        
        lower_q = max(Q1 - 1.5 * IQR, 0)
        upper_q = Q3 + 1.5 * IQR
        
        # Test the current point i
        current_val = group[variable].iloc[i]
        group.iloc[i, group.columns.get_loc('Q1')] = Q1
        group.iloc[i, group.columns.get_loc('Q3')] = Q3
        group.iloc[i, group.columns.get_loc('IQR')] = IQR
        group.iloc[i, group.columns.get_loc('IQR_low')] = lower_q
        group.iloc[i, group.columns.get_loc('IQR_high')] = upper_q
        group.iloc[i, group.columns.get_loc('set')] = "TEST"
        group.iloc[i, group.columns.get_loc('IQR_anomaly')] = classify(current_val, lower_q, upper_q)
        group.iloc[i, group.columns.get_loc('is_IQR_anomaly')] = (current_val < lower_q) or (current_val > upper_q)

    # Cast boolean column properly
    group['is_IQR_anomaly'] = group['is_IQR_anomaly'].astype(bool)
    # FINAL SAFETY CHECK
    group[date_column] = pd.to_datetime(group[date_column])
    
    return group