"""Microprice features (gold layer) — a FAITHFUL Python port of the Rust ml-features `microprice.rs`
(Block N+V, 8 features). The Rust is the oracle: this matches it bar-for-bar (TDD against its unit tests).

NOTE (verified via literature): the formula M = (P_b·V_a + P_a·V_b)/(V_a+V_b) is the *weighted mid-price*,
commonly mis-called "Stoikov microprice". Stoikov's true micro-price (Quantitative Finance 2018) is a
recursive Markov-chain estimator and is empirically a better predictor of future prices. We keep this
faithful port for SSOT consistency with the Rust framework, and will ADD the true micro-price as a
separate feature (so the gauntlet can A/B them) rather than silently diverge from the oracle.
"""
from __future__ import annotations

import math

from fxhnt.domain.marketdata import Mbp10Snapshot, price_to_f64

FEATURE_NAMES = (
    "stoikov_microprice", "microprice_change", "microprice_acceleration",
    "microprice_residual", "microprice_residual_sign",
    "microprice_top3", "microprice_top5", "microprice_cartea",
)


def stoikov_microprice(snap: Mbp10Snapshot) -> float:
    """Weighted mid: M = (P_b·V_a + P_a·V_b)/(V_a+V_b). Falls back to mid on zero L1 depth, 0 if empty."""
    if not snap.levels:
        return 0.0
    l1 = snap.levels[0]
    p_b, p_a = price_to_f64(l1.bid_px), price_to_f64(l1.ask_px)
    v_b, v_a = float(l1.bid_sz), float(l1.ask_sz)
    total = v_a + v_b
    if total < 1.0:
        return snap.mid_price()
    return (p_b * v_a + p_a * v_b) / total


def multilevel_microprice(snap: Mbp10Snapshot, top_k: int) -> float:
    """Generalized weighted mid over the top-K levels (each price weighted by the opposite side's depth)."""
    n = min(len(snap.levels), top_k)
    if n == 0:
        return 0.0
    num = den = 0.0
    for i in range(n):
        lvl = snap.levels[i]
        p_b, p_a = price_to_f64(lvl.bid_px), price_to_f64(lvl.ask_px)
        v_b, v_a = float(lvl.bid_sz), float(lvl.ask_sz)
        num += p_b * v_a + p_a * v_b
        den += v_a + v_b
    if den < 1.0:
        return snap.mid_price()
    return num / den


def cartea_adjusted_microprice(snap: Mbp10Snapshot, stoikov: float) -> float:
    """Cartea-Jaimungal 2017 L2 directional-pressure correction: M + λ·imb_L2·(mid_L2 − mid_L1)."""
    if len(snap.levels) < 2:
        return stoikov
    l1, l2 = snap.levels[0], snap.levels[1]
    v_b1, v_a1 = float(l1.bid_sz), float(l1.ask_sz)
    v_b2, v_a2 = float(l2.bid_sz), float(l2.ask_sz)
    depth_l1, depth_l2 = v_b1 + v_a1, v_b2 + v_a2
    if depth_l1 < 1.0 or depth_l2 < 1.0:
        return stoikov
    imb_l2 = (v_b2 - v_a2) / depth_l2
    mid_l1 = (price_to_f64(l1.bid_px) + price_to_f64(l1.ask_px)) / 2.0
    mid_l2 = (price_to_f64(l2.bid_px) + price_to_f64(l2.ask_px)) / 2.0
    lam = depth_l2 / depth_l1
    return stoikov + lam * imb_l2 * (mid_l2 - mid_l1)


class MicropriceFeatures:
    """Stateful Block N+V aggregator — holds prev/prev-prev Stoikov for the Δ/Δ² features."""
    name = "microprice"
    feature_names = FEATURE_NAMES

    def __init__(self) -> None:
        self._prev: float | None = None
        self._prev_prev: float | None = None

    def update(self, snap: Mbp10Snapshot) -> dict[str, float]:
        stoikov = stoikov_microprice(snap)
        residual = stoikov - snap.mid_price()
        residual_sign = math.copysign(1.0, residual)   # matches Rust f64::signum (0.0 → +1.0)
        mp_top3 = multilevel_microprice(snap, 3)
        mp_top5 = multilevel_microprice(snap, 5)
        cartea = cartea_adjusted_microprice(snap, stoikov)

        dmp = stoikov - self._prev if self._prev is not None else 0.0
        ddmp = (stoikov - 2.0 * self._prev + self._prev_prev
                if self._prev is not None and self._prev_prev is not None else 0.0)

        self._prev_prev = self._prev
        self._prev = stoikov

        return dict(zip(FEATURE_NAMES,
                        (stoikov, dmp, ddmp, residual, residual_sign, mp_top3, mp_top5, cartea)))

    def reset(self) -> None:
        self._prev = self._prev_prev = None

    def is_ready(self) -> bool:
        return self._prev_prev is not None
