from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from decimal import Decimal
from typing import Any


D0 = Decimal("0")


@dataclass(frozen=True)
class TrendRetestFeatures:
    coin: str
    current_price: Decimal
    candle_ts: int
    data_window_id: str
    open_15m: Decimal
    high_15m: Decimal
    low_15m: Decimal
    close_15m: Decimal
    move_15m_pct: Decimal
    move_1h_pct: Decimal
    sma_1h_fast: Decimal
    sma_1h_slow: Decimal
    sma_4h_fast: Decimal
    sma_4h_slow: Decimal
    atr_pct: Decimal
    rsi_15m: Decimal
    volume_24h: Decimal
    volume_ratio: Decimal
    recent_high: Decimal
    recent_low: Decimal
    pullback_from_high_pct: Decimal
    rebound_from_low_pct: Decimal
    spread_pct: Decimal
    bid_depth_notional_5: Decimal
    ask_depth_notional_5: Decimal
    buy_impact_1k_pct: Decimal
    sell_impact_1k_pct: Decimal
    funding_rate_hourly_pct: Decimal
    open_interest: Decimal
    premium_pct: Decimal
    strength_rank: int = 999
    weakness_rank: int = 999
    breadth_positive_pct: Decimal = D0
    data_quality_allowed: bool = True
    data_quality_reasons: tuple[str, ...] = ()
    continuation_closes_confirmed: bool = False
    vwap_15m: Decimal = D0
    bb_upper_15m: Decimal = D0
    bb_lower_15m: Decimal = D0
    bb_width_pct: Decimal = D0
    adx_15m: Decimal = D0
    sma_1h_spread_pct: Decimal = D0
    sma_4h_spread_pct: Decimal = D0
    best_bid: Decimal = D0
    best_ask: Decimal = D0


def _d(value: Any, default: str = "0") -> Decimal:
    try:
        return Decimal(str(value))
    except Exception:
        return Decimal(default)


def _row(raw: dict[str, Any]) -> dict[str, Decimal | int]:
    return {
        "t": int(raw.get("t") or raw.get("T") or 0),
        "T": int(raw.get("T") or raw.get("t") or 0),
        "o": _d(raw.get("o")),
        "h": _d(raw.get("h")),
        "l": _d(raw.get("l")),
        "c": _d(raw.get("c")),
        "v": _d(raw.get("v")),
    }


def normalize_closed_candles(rows: list[dict[str, Any]], *, now_ms: int) -> list[dict[str, Decimal | int]]:
    parsed = [_row(row) for row in rows if isinstance(row, dict)]
    valid = [row for row in parsed if row["o"] > 0 and row["h"] > 0 and row["l"] > 0 and row["c"] > 0 and int(row["T"]) <= now_ms]
    return sorted(valid, key=lambda row: int(row["t"]))


def _sma(values: list[Decimal], length: int) -> Decimal:
    if len(values) < length:
        return D0
    return sum(values[-length:], D0) / Decimal(length)


def _atr_pct(candles: list[dict[str, Decimal | int]], length: int = 14) -> Decimal:
    if len(candles) < length + 1:
        return D0
    ranges: list[Decimal] = []
    for idx in range(len(candles) - length, len(candles)):
        row = candles[idx]
        prev_close = _d(candles[idx - 1]["c"])
        high = _d(row["h"])
        low = _d(row["l"])
        ranges.append(max(high - low, abs(high - prev_close), abs(low - prev_close)))
    close = _d(candles[-1]["c"])
    return (sum(ranges, D0) / Decimal(length) / close * Decimal("100")) if close > 0 else D0


def _rsi(closes: list[Decimal], length: int = 14) -> Decimal:
    if len(closes) < length + 1:
        return Decimal("50")
    changes = [closes[idx] - closes[idx - 1] for idx in range(len(closes) - length, len(closes))]
    gains = sum((max(change, D0) for change in changes), D0) / Decimal(length)
    losses = sum((max(-change, D0) for change in changes), D0) / Decimal(length)
    if losses == 0:
        return Decimal("100") if gains > 0 else Decimal("50")
    rs = gains / losses
    return Decimal("100") - Decimal("100") / (Decimal("1") + rs)


def _bollinger(closes: list[Decimal], length: int = 20) -> tuple[Decimal, Decimal, Decimal]:
    if len(closes) < length:
        return D0, D0, D0
    values = closes[-length:]
    mean = sum(values, D0) / Decimal(length)
    variance = sum(((value - mean) ** 2 for value in values), D0) / Decimal(length)
    deviation = variance.sqrt()
    upper, lower = mean + Decimal("2") * deviation, mean - Decimal("2") * deviation
    width = (upper - lower) / mean * Decimal("100") if mean > 0 else D0
    return upper, lower, width


def _vwap(candles: list[dict[str, Decimal | int]], length: int = 96) -> Decimal:
    rows = candles[-length:]
    volume = sum((_d(row["v"]) for row in rows), D0)
    if volume <= 0:
        return D0
    quote = sum((((_d(row["h"]) + _d(row["l"]) + _d(row["c"])) / Decimal("3")) * _d(row["v"]) for row in rows), D0)
    return quote / volume


def _adx(candles: list[dict[str, Decimal | int]], length: int = 14) -> Decimal:
    if len(candles) < (length * 2) + 1:
        return D0
    true_ranges: list[Decimal] = []
    plus_moves: list[Decimal] = []
    minus_moves: list[Decimal] = []
    for idx in range(1, len(candles)):
        row, previous = candles[idx], candles[idx - 1]
        up = _d(row["h"]) - _d(previous["h"])
        down = _d(previous["l"]) - _d(row["l"])
        plus_moves.append(up if up > down and up > 0 else D0)
        minus_moves.append(down if down > up and down > 0 else D0)
        previous_close = _d(previous["c"])
        true_ranges.append(max(_d(row["h"]) - _d(row["l"]), abs(_d(row["h"]) - previous_close), abs(_d(row["l"]) - previous_close)))
    smoothed_tr = sum(true_ranges[:length], D0)
    smoothed_plus = sum(plus_moves[:length], D0)
    smoothed_minus = sum(minus_moves[:length], D0)
    dx_values: list[Decimal] = []
    for idx in range(length - 1, len(true_ranges)):
        if idx >= length:
            smoothed_tr = smoothed_tr - smoothed_tr / Decimal(length) + true_ranges[idx]
            smoothed_plus = smoothed_plus - smoothed_plus / Decimal(length) + plus_moves[idx]
            smoothed_minus = smoothed_minus - smoothed_minus / Decimal(length) + minus_moves[idx]
        if smoothed_tr <= 0:
            dx_values.append(D0)
            continue
        plus_di = smoothed_plus / smoothed_tr * Decimal("100")
        minus_di = smoothed_minus / smoothed_tr * Decimal("100")
        denominator = plus_di + minus_di
        dx_values.append(abs(plus_di - minus_di) / denominator * Decimal("100") if denominator > 0 else D0)
    if not dx_values:
        return D0
    seed = dx_values[:length]
    adx = sum(seed, D0) / Decimal(len(seed))
    for dx in dx_values[length:]:
        adx = (adx * Decimal(length - 1) + dx) / Decimal(length)
    return adx


def build_trend_retest_features(
    *,
    coin: str,
    candles_15m: list[dict[str, Any]],
    candles_1h: list[dict[str, Any]],
    candles_4h: list[dict[str, Any]],
    now_ms: int,
    spread_pct: Decimal,
    bid_depth_notional_5: Decimal,
    ask_depth_notional_5: Decimal,
    buy_impact_1k_pct: Decimal,
    sell_impact_1k_pct: Decimal,
    funding_rate_hourly_pct: Decimal = D0,
    open_interest: Decimal = D0,
    premium_pct: Decimal = D0,
    best_bid: Decimal = D0,
    best_ask: Decimal = D0,
) -> TrendRetestFeatures:
    c15 = normalize_closed_candles(candles_15m, now_ms=now_ms)
    c1h = normalize_closed_candles(candles_1h, now_ms=now_ms)
    c4h = normalize_closed_candles(candles_4h, now_ms=now_ms)
    reasons: list[str] = []
    if len(c15) < 50:
        reasons.append("insufficient_15m_candles")
    if len(c1h) < 50:
        reasons.append("insufficient_1h_candles")
    if len(c4h) < 50:
        reasons.append("insufficient_4h_candles")
    if reasons:
        raise ValueError("+".join(reasons))

    latest = c15[-1]
    last_1h = c1h[-1]
    close = _d(latest["c"])
    closes_15m = [_d(row["c"]) for row in c15]
    closes_1h = [_d(row["c"]) for row in c1h]
    closes_4h = [_d(row["c"]) for row in c4h]
    previous_volumes = [_d(row["v"]) for row in c15[-21:-1]]
    avg_volume = sum(previous_volumes, D0) / Decimal(len(previous_volumes)) if previous_volumes else D0
    volume_ratio = _d(latest["v"]) / avg_volume if avg_volume > 0 else D0
    recent = c15[-13:-1] or c15[-12:]
    recent_high = max((_d(row["h"]) for row in recent), default=close)
    recent_low = min((_d(row["l"]) for row in recent), default=close)
    pullback = (recent_high - close) / recent_high * Decimal("100") if recent_high > 0 else D0
    rebound = (close - recent_low) / recent_low * Decimal("100") if recent_low > 0 else D0
    move_15m = (close / _d(latest["o"]) - Decimal("1")) * Decimal("100")
    move_1h = (_d(last_1h["c"]) / _d(last_1h["o"]) - Decimal("1")) * Decimal("100")
    volume_24h = sum((_d(row["v"]) * _d(row["c"]) for row in c15[-96:]), D0)
    previous = c15[-2]
    continuation_closes_confirmed = (
        _d(previous["c"]) > _d(previous["o"])
        and close > _d(latest["o"])
        and close > _d(previous["c"])
    )
    bb_upper, bb_lower, bb_width = _bollinger(closes_15m)
    sma_1h_fast, sma_1h_slow = _sma(closes_1h, 20), _sma(closes_1h, 50)
    sma_4h_fast, sma_4h_slow = _sma(closes_4h, 20), _sma(closes_4h, 50)

    window_payload = {
        "coin": coin.upper(),
        "15m": [{key: str(row[key]) for key in ("t", "T", "o", "h", "l", "c", "v")} for row in c15[-50:]],
        "1h": [{key: str(row[key]) for key in ("t", "T", "o", "h", "l", "c", "v")} for row in c1h[-50:]],
        "4h": [{key: str(row[key]) for key in ("t", "T", "o", "h", "l", "c", "v")} for row in c4h[-50:]],
    }
    window_text = json.dumps(window_payload, sort_keys=True, separators=(",", ":"))
    window_id = hashlib.sha256(window_text.encode("utf-8")).hexdigest()[:16]
    dq_reasons: list[str] = []
    for label, rows, interval_ms in (("15m", c15, 900_000), ("1h", c1h, 3_600_000), ("4h", c4h, 14_400_000)):
        starts = [int(row["t"]) for row in rows[-50:]]
        if len(set(starts)) != len(starts):
            dq_reasons.append(f"duplicate_{label}_candles")
        if any(current - previous != interval_ms for previous, current in zip(starts, starts[1:])):
            dq_reasons.append(f"gapped_{label}_candles")
    if spread_pct <= 0:
        dq_reasons.append("spread_unavailable")
    if bid_depth_notional_5 <= 0 or ask_depth_notional_5 <= 0:
        dq_reasons.append("l2_depth_unavailable")
    if buy_impact_1k_pct < 0 or sell_impact_1k_pct < 0:
        dq_reasons.append("l2_impact_invalid")
    if volume_24h <= 0:
        dq_reasons.append("volume_unavailable")

    return TrendRetestFeatures(
        coin=coin.upper(),
        current_price=close,
        candle_ts=int(latest["T"]),
        data_window_id=window_id,
        open_15m=_d(latest["o"]),
        high_15m=_d(latest["h"]),
        low_15m=_d(latest["l"]),
        close_15m=close,
        move_15m_pct=move_15m,
        move_1h_pct=move_1h,
        sma_1h_fast=sma_1h_fast,
        sma_1h_slow=sma_1h_slow,
        sma_4h_fast=sma_4h_fast,
        sma_4h_slow=sma_4h_slow,
        atr_pct=_atr_pct(c15),
        rsi_15m=_rsi(closes_15m),
        volume_24h=volume_24h,
        volume_ratio=volume_ratio,
        recent_high=recent_high,
        recent_low=recent_low,
        pullback_from_high_pct=pullback,
        rebound_from_low_pct=rebound,
        spread_pct=spread_pct,
        bid_depth_notional_5=bid_depth_notional_5,
        ask_depth_notional_5=ask_depth_notional_5,
        buy_impact_1k_pct=buy_impact_1k_pct,
        sell_impact_1k_pct=sell_impact_1k_pct,
        funding_rate_hourly_pct=funding_rate_hourly_pct,
        open_interest=open_interest,
        premium_pct=premium_pct,
        data_quality_allowed=not dq_reasons,
        data_quality_reasons=tuple(dq_reasons),
        continuation_closes_confirmed=continuation_closes_confirmed,
        vwap_15m=_vwap(c15),
        bb_upper_15m=bb_upper,
        bb_lower_15m=bb_lower,
        bb_width_pct=bb_width,
        adx_15m=_adx(c15),
        sma_1h_spread_pct=(sma_1h_fast - sma_1h_slow) / close * Decimal("100") if close > 0 else D0,
        sma_4h_spread_pct=(sma_4h_fast - sma_4h_slow) / close * Decimal("100") if close > 0 else D0,
        best_bid=best_bid,
        best_ask=best_ask,
    )
