from __future__ import annotations

import json
import math
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from statistics import mean, pstdev
from typing import Any, Iterable, Mapping


@dataclass(frozen=True)
class ChartIndicators:
    sma_fast: float
    sma_slow: float
    rsi_14: float
    atr_14: float
    bollinger_upper: float
    bollinger_lower: float
    vwap: float


@dataclass(frozen=True)
class MarketContextSnapshot:
    ts: datetime
    coin: str
    source: str
    timeframe: str
    indicators: ChartIndicators
    open_interest_usd: float | None
    funding_rate: float | None
    tags: tuple[str, ...]
    reliability_score: float

    def as_event(self) -> dict[str, Any]:
        event = asdict(self)
        event["event_type"] = "market_context"
        event["ts"] = self.ts.astimezone(timezone.utc).isoformat()
        event["coin"] = self.coin.upper()
        event["tags"] = list(self.tags)
        return event


@dataclass(frozen=True)
class SourceReliability:
    source: str
    sample_size: int
    hit_rate: float
    avg_return_pct: float


def _get(candle: Mapping[str, Any], key: str) -> float:
    return float(candle[key])


def _sma(values: list[float], n: int) -> float:
    if not values:
        return 0.0
    window = values[-min(n, len(values)):]
    return mean(window)


def _rsi(closes: list[float], n: int = 14) -> float:
    if len(closes) < 2:
        return 50.0
    deltas = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
    window = deltas[-min(n, len(deltas)):]
    gains = [max(x, 0.0) for x in window]
    losses = [abs(min(x, 0.0)) for x in window]
    avg_gain = mean(gains) if gains else 0.0
    avg_loss = mean(losses) if losses else 0.0
    if avg_loss == 0:
        return 100.0 if avg_gain > 0 else 50.0
    rs = avg_gain / avg_loss
    return 100.0 - (100.0 / (1.0 + rs))


def _atr(candles: list[dict[str, Any]], n: int = 14) -> float:
    if not candles:
        return 0.0
    true_ranges = []
    prev_close = None
    for c in candles:
        high = _get(c, "high")
        low = _get(c, "low")
        close = _get(c, "close")
        if prev_close is None:
            tr = high - low
        else:
            tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
        true_ranges.append(tr)
        prev_close = close
    return mean(true_ranges[-min(n, len(true_ranges)):])


def _vwap(candles: list[dict[str, Any]]) -> float:
    num = 0.0
    den = 0.0
    for c in candles:
        high = _get(c, "high")
        low = _get(c, "low")
        close = _get(c, "close")
        volume = float(c.get("volume", c.get("vol", 0.0)))
        typical = (high + low + close) / 3.0
        num += typical * volume
        den += volume
    return num / den if den else 0.0


def compute_chart_indicators(candles: Iterable[dict[str, Any]]) -> ChartIndicators:
    series = list(candles)
    closes = [_get(c, "close") for c in series]
    sma_fast = _sma(closes, 9)
    sma_slow = _sma(closes, 21)
    bb_window = closes[-min(20, len(closes)):] or [0.0]
    bb_mid = mean(bb_window)
    bb_std = pstdev(bb_window) if len(bb_window) > 1 else 0.0
    return ChartIndicators(
        sma_fast=round(sma_fast, 8),
        sma_slow=round(sma_slow, 8),
        rsi_14=round(_rsi(closes), 8),
        atr_14=round(_atr(series), 8),
        bollinger_upper=round(bb_mid + 2 * bb_std, 8),
        bollinger_lower=round(bb_mid - 2 * bb_std, 8),
        vwap=round(_vwap(series), 8),
    )


def _tags(indicators: ChartIndicators, funding_rate: float | None, open_interest_usd: float | None) -> tuple[str, ...]:
    tags: list[str] = []
    if indicators.sma_fast > indicators.sma_slow:
        tags.append("trend_up")
    elif indicators.sma_fast < indicators.sma_slow:
        tags.append("trend_down")
    if indicators.rsi_14 >= 70:
        tags.append("rsi_overbought")
    elif indicators.rsi_14 <= 30:
        tags.append("rsi_oversold")
    if funding_rate is not None:
        if funding_rate > 0.0005:
            tags.append("funding_crowded_long")
        elif funding_rate < -0.0005:
            tags.append("funding_crowded_short")
    if open_interest_usd and open_interest_usd > 0:
        tags.append("oi_available")
    return tuple(tags)


def build_market_context_snapshot(
    *,
    coin: str,
    candles: Iterable[dict[str, Any]],
    open_interest_usd: float | None = None,
    funding_rate: float | None = None,
    source: str = "hyperliquid",
    timeframe: str = "15m",
    ts: datetime | None = None,
) -> MarketContextSnapshot:
    indicators = compute_chart_indicators(candles)
    tags = _tags(indicators, funding_rate, open_interest_usd)
    reliability = 0.55
    if source == "hyperliquid":
        reliability += 0.20
    if open_interest_usd is not None:
        reliability += 0.10
    if funding_rate is not None:
        reliability += 0.05
    reliability = min(1.0, reliability)
    return MarketContextSnapshot(
        ts=ts or datetime.now(timezone.utc),
        coin=coin.upper(),
        source=source,
        timeframe=timeframe,
        indicators=indicators,
        open_interest_usd=open_interest_usd,
        funding_rate=funding_rate,
        tags=tags,
        reliability_score=round(reliability, 4),
    )


def score_signal_reliability(observations: Iterable[dict[str, Any]]) -> dict[str, SourceReliability]:
    grouped: dict[str, list[dict[str, Any]]] = {}
    for obs in observations:
        grouped.setdefault(str(obs.get("source", "unknown")), []).append(obs)
    scores: dict[str, SourceReliability] = {}
    for source, rows in grouped.items():
        hits = 0
        returns = []
        for row in rows:
            actual = float(row.get("actual_return_pct", 0.0))
            predicted = str(row.get("predicted_direction", "up"))
            hit = (predicted == "up" and actual > 0) or (predicted == "down" and actual < 0)
            hits += 1 if hit else 0
            returns.append(actual)
        scores[source] = SourceReliability(
            source=source,
            sample_size=len(rows),
            hit_rate=round(hits / len(rows), 4) if rows else 0.0,
            avg_return_pct=round(mean(returns), 8) if returns else 0.0,
        )
    return scores


def append_market_context_event(path: str | Path, snapshot: MarketContextSnapshot) -> dict[str, Any]:
    target = Path(path).expanduser()
    target.parent.mkdir(parents=True, exist_ok=True)
    event = snapshot.as_event()
    with target.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, sort_keys=True) + "\n")
    return event
