from __future__ import annotations

import json
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable, Sequence

from config import BotConfig


@dataclass(frozen=True)
class NearMissEvent:
    ts: float
    strategy_id: str
    coin: str
    reason: str
    drop_pct: float
    leverage: float
    roe_drop_pct: float
    required_drop_pct: float
    required_roe_drop_pct: float
    distance_pct: float
    current_price: float
    high_price: float


def leverage_for_coin(cfg: BotConfig, coin: str) -> float:
    return float(cfg.leverage_by_coin.get(coin.upper(), cfg.default_leverage))


def roe_crash_required_drop_pct(cfg: BotConfig, coin: str) -> float:
    leverage = max(leverage_for_coin(cfg, coin), 0.01)
    return float(cfg.crash_roe_trigger_pct) / leverage


def effective_required_drop_pct(cfg: BotConfig, coin: str) -> float:
    """Unlevered drop threshold after respecting coin-specific leverage.

    The strategy's unlevered flash-crash trigger remains a hard floor, but a
    15% ROE-style crash is normalized per coin as 15 / leverage. The stricter
    of both thresholds avoids accidentally making high-leverage coins too easy
    to trigger unless the strategy explicitly lowers its flash-crash trigger.
    """
    return max(float(cfg.flash_crash_trigger_pct), roe_crash_required_drop_pct(cfg, coin))


def build_near_miss_event(
    *,
    strategy_id: str,
    coin: str,
    history: Iterable[tuple[float, float]],
    current_price: float,
    reason: str,
    cfg: BotConfig,
    now_ts: float | None = None,
) -> NearMissEvent | None:
    prices = [price for _, price in history]
    if not prices or current_price <= 0:
        return None
    high_price = max(prices)
    if high_price <= 0:
        return None
    drop_pct = ((current_price - high_price) / high_price) * 100.0
    leverage = leverage_for_coin(cfg, coin)
    roe_drop_pct = drop_pct * leverage
    required_drop_pct = effective_required_drop_pct(cfg, coin)
    required_roe = -required_drop_pct * leverage
    distance = max(0.0, required_drop_pct - abs(drop_pct))
    return NearMissEvent(
        ts=float(now_ts if now_ts is not None else time.time()),
        strategy_id=strategy_id,
        coin=coin.upper(),
        reason=reason,
        drop_pct=round(drop_pct, 6),
        leverage=round(leverage, 4),
        roe_drop_pct=round(roe_drop_pct, 6),
        required_drop_pct=round(required_drop_pct, 6),
        required_roe_drop_pct=round(required_roe, 6),
        distance_pct=round(distance, 6),
        current_price=float(current_price),
        high_price=float(high_price),
    )


def append_near_miss(path: Path, event: NearMissEvent) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as fh:
        fh.write(json.dumps(asdict(event), sort_keys=True) + "\n")


def top_near_misses(events: Sequence[NearMissEvent], *, limit: int = 10) -> list[NearMissEvent]:
    return sorted(events, key=lambda item: (item.distance_pct, -abs(item.drop_pct)))[:limit]
