"""Streaming outlier detectors using robust statistics."""

from math import isnan
from collections import deque
from statistics import quantiles

import numpy as np


class OutlierDetector:
    """IQR-based outlier detector using a sliding window.

    Computes Tukey's fences from the previous buffer state,
    then appends the new sample — avoiding self-contamination bias.
    """

    def __init__(self, buffer_size: int = 11, k: float = 1.5):
        if buffer_size < 2:
            raise ValueError("Buffer size must be at least 2")
        if not 0.5 <= k <= 5:
            raise ValueError("Spread factor k seems poorly chosen")
        self.buffer = deque(maxlen=buffer_size)
        self.k = k

    def process(self, x: float) -> bool | None:
        if x is None or isnan(x):
            return None

        is_outlier = False
        if len(self.buffer) == self.buffer.maxlen:
            q = quantiles(self.buffer, n=4)
            iqr = q[2] - q[0]
            if iqr > 0:
                low_fence = q[0] - self.k * iqr
                high_fence = q[2] + self.k * iqr
                is_outlier = x > high_fence or x < low_fence

        self.buffer.append(x)
        return is_outlier


class OutlierDetectorMAD:
    """MAD-based outlier detector using a sliding window.

    Uses median absolute deviation for more robust spread estimation.
    Computes fences before appending the new sample.
    """

    def __init__(self, buffer_size: int = 11, k: float = 1.5):
        if buffer_size < 2:
            raise ValueError("Buffer size must be at least 2")
        if not 0.5 <= k <= 5:
            raise ValueError("Spread factor k seems poorly chosen")
        self.buffer = deque(maxlen=buffer_size)
        self.k = k

    def process(self, x: float) -> bool | None:
        if x is None or isnan(x):
            return None

        is_outlier = False
        if len(self.buffer) == self.buffer.maxlen:
            buf = np.array(self.buffer)
            med = np.median(buf)
            mad = np.median(np.abs(buf - med))
            if mad > 0:
                low_fence = med - self.k * mad
                high_fence = med + self.k * mad
                is_outlier = x > high_fence or x < low_fence

        self.buffer.append(x)
        return is_outlier


class OutlierDetectorFrugalMAD:
    """Memory-efficient outlier detector using frugal median and MAD estimation.

    Adapts the Frugal-1U-Median algorithm (Ma, Muthukrishnan & Sandler, 2014)
    to estimate both median and MAD recursively with O(1) memory.
    """

    def __init__(self, k: float = 4.0):
        if not 0.5 <= k <= 5.0:
            raise ValueError("k must be between 0.5 and 5.0")
        self.k = k
        self.med = None
        self.mad = None

    def process(self, x: float) -> bool | None:
        if x is None or isnan(x):
            return None

        alpha = 0.1 if self.mad is None else self.mad / 100

        if self.med is None:
            self.med = x
        elif x > self.med:
            self.med += alpha
        else:
            self.med -= alpha

        dev = abs(x - self.med)
        if self.mad is None:
            self.mad = dev
        elif dev > self.mad:
            self.mad += alpha
        else:
            self.mad = max(0.0, self.mad - alpha)

        if self.mad > 0:
            low_fence = self.med - self.k * self.mad
            high_fence = self.med + self.k * self.mad
            return x > high_fence or x < low_fence

        return False
