"""Deterministic multi-model anomaly detection for risk graph features."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from statistics import median
from typing import Any

ZERO = Decimal("0")


@dataclass(frozen=True, slots=True)
class AnomalyPoint:
    key: str
    amount: Decimal
    occurred_at: datetime | None = None
    segment: str = ""
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass(slots=True)
class AnomalyModelSignal:
    method: str
    score: int
    reason: str
    related_keys: list[str] = field(default_factory=list)

    def as_dict(self) -> dict[str, Any]:
        return {
            "method": self.method,
            "score": self.score,
            "reason": self.reason,
            "related_keys": list(self.related_keys),
        }


class MultiModelAnomalyDetector:
    def detect(
        self,
        points: list[AnomalyPoint],
        *,
        target_key: str,
    ) -> list[AnomalyModelSignal]:
        target = next((point for point in points if point.key == target_key), None)
        if target is None:
            return []
        peers = [
            point
            for point in points
            if point.key != target.key and (not target.segment or point.segment == target.segment)
        ]
        if len(peers) < 3:
            return []

        signals = [
            self._robust_statistical_signal(target, peers),
            self._isolation_proxy_signal(target, peers),
            self._local_outlier_signal(target, peers),
            self._temporal_jump_signal(target, peers),
            self._periodic_deviation_signal(target, peers),
        ]
        return [signal for signal in signals if signal is not None]

    def _robust_statistical_signal(
        self,
        target: AnomalyPoint,
        peers: list[AnomalyPoint],
    ) -> AnomalyModelSignal | None:
        values = [point.amount for point in peers if point.amount >= ZERO]
        if len(values) < 3:
            return None
        center = Decimal(str(median(values)))
        deviations = [abs(value - center) for value in values]
        mad = Decimal(str(median(deviations))) or Decimal("1")
        modified_z = abs(target.amount - center) / mad
        if modified_z < Decimal("3"):
            return None
        return AnomalyModelSignal(
            method="robust_statistics",
            score=min(100, int(modified_z * Decimal("18"))),
            reason="Target amount deviates from peer median by robust MAD.",
            related_keys=[point.key for point in peers],
        )

    def _isolation_proxy_signal(
        self,
        target: AnomalyPoint,
        peers: list[AnomalyPoint],
    ) -> AnomalyModelSignal | None:
        values = sorted(point.amount for point in peers)
        if target.amount <= values[-1] * Decimal("1.8"):
            return None
        return AnomalyModelSignal(
            method="isolation_forest_proxy",
            score=min(100, int((target.amount / max(values[-1], Decimal("1"))) * Decimal("45"))),
            reason="Target amount is isolated beyond the peer maximum envelope.",
            related_keys=[point.key for point in peers[-5:]],
        )

    def _local_outlier_signal(
        self,
        target: AnomalyPoint,
        peers: list[AnomalyPoint],
    ) -> AnomalyModelSignal | None:
        distances = sorted((abs(target.amount - point.amount), point.key) for point in peers)
        nearest = distances[: min(3, len(distances))]
        peer_distances = [
            abs(left.amount - right.amount)
            for index, left in enumerate(peers)
            for right in peers[index + 1 :]
        ]
        local_scale = Decimal(str(median(peer_distances))) if peer_distances else Decimal("1")
        local_scale = max(local_scale, Decimal("1"))
        target_distance = sum((distance for distance, _ in nearest), ZERO) / Decimal(len(nearest))
        ratio = target_distance / local_scale
        if ratio < Decimal("2.5"):
            return None
        return AnomalyModelSignal(
            method="local_outlier_factor_proxy",
            score=min(100, int(ratio * Decimal("24"))),
            reason="Target is far away from its nearest peer neighborhood.",
            related_keys=[key for _, key in nearest],
        )

    def _temporal_jump_signal(
        self,
        target: AnomalyPoint,
        peers: list[AnomalyPoint],
    ) -> AnomalyModelSignal | None:
        if target.occurred_at is None:
            return None
        previous = [
            point
            for point in peers
            if point.occurred_at is not None and point.occurred_at < target.occurred_at
        ]
        previous = sorted(previous, key=lambda item: item.occurred_at or datetime.min)[-3:]
        if len(previous) < 3:
            return None
        average = sum((point.amount for point in previous), ZERO) / Decimal(len(previous))
        if average <= ZERO or target.amount < average * Decimal("2.2"):
            return None
        return AnomalyModelSignal(
            method="temporal_jump",
            score=min(100, int((target.amount / average) * Decimal("32"))),
            reason="Target amount jumps above the recent moving average.",
            related_keys=[point.key for point in previous],
        )

    def _periodic_deviation_signal(
        self,
        target: AnomalyPoint,
        peers: list[AnomalyPoint],
    ) -> AnomalyModelSignal | None:
        if target.occurred_at is None:
            return None
        same_period = [
            point
            for point in peers
            if point.occurred_at is not None
            and point.occurred_at.weekday() == target.occurred_at.weekday()
        ]
        if len(same_period) < 2:
            return None
        average = sum((point.amount for point in same_period), ZERO) / Decimal(len(same_period))
        if average <= ZERO or target.amount < average * Decimal("2"):
            return None
        return AnomalyModelSignal(
            method="periodic_deviation",
            score=min(100, int((target.amount / average) * Decimal("30"))),
            reason="Target deviates from same-weekday periodic peer behavior.",
            related_keys=[point.key for point in same_period],
        )
