from __future__ import annotations

import argparse
import json
import math
import os
import time
import uuid
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any

from src.execution.order_intent import OrderIntent
from src.hyperliquid.market_data import HyperliquidMarketData, summarize_l2_book

RUNTIME_DIR = Path(os.getenv("CTB_SHADOW_RUNTIME_DIR", "runtime/experiments/market_regime_shadow"))
STATE_PATH = RUNTIME_DIR / "state.json"
SIGNALS = RUNTIME_DIR / "signal_journal.jsonl"
TRADES = RUNTIME_DIR / "trade_journal.jsonl"
HEALTH = RUNTIME_DIR / "runtime_health.jsonl"
PID_PATH = RUNTIME_DIR / "bot.pid"

PRIMARY = ["BTC", "ETH", "SOL", "LINK", "BCH", "ENA", "AAVE", "TON", "ZRO"]


def D(x: Any) -> Decimal:
    try:
        return Decimal(str(x))
    except Exception:
        return Decimal("0")


def _json_default(v: Any) -> Any:
    if isinstance(v, Decimal):
        return str(v)
    if isinstance(v, datetime):
        return v.isoformat()
    return str(v)


def append(path: Path, row: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    row.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
    path.open("a", encoding="utf-8").write(json.dumps(row, sort_keys=True, default=_json_default) + "\n")


def load_state() -> dict[str, Any]:
    if STATE_PATH.exists():
        return json.loads(STATE_PATH.read_text(encoding="utf-8"))
    return {"positions": {}, "realized_pnl": "0", "closed_trades": 0, "wins": 0, "losses": 0, "recent": []}


def save_state(s: dict[str, Any]) -> None:
    STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
    STATE_PATH.write_text(json.dumps(s, indent=2, sort_keys=True, default=_json_default), encoding="utf-8")


def ema(vals: list[Decimal], n: int) -> Decimal:
    if not vals:
        return Decimal("0")
    k = Decimal("2") / Decimal(n + 1)
    e = vals[0]
    for v in vals[1:]:
        e = v * k + e * (Decimal("1") - k)
    return e


def rsi(vals: list[Decimal], n: int = 14) -> Decimal:
    if len(vals) <= n:
        return Decimal("50")
    gains = []
    losses = []
    for a, b in zip(vals[-n - 1:-1], vals[-n:]):
        ch = b - a
        gains.append(max(ch, Decimal("0")))
        losses.append(abs(min(ch, Decimal("0"))))
    ag = sum(gains, Decimal("0")) / Decimal(n)
    al = sum(losses, Decimal("0")) / Decimal(n)
    if al == 0:
        return Decimal("100")
    rs = ag / al
    return Decimal("100") - (Decimal("100") / (Decimal("1") + rs))


def atr(candles: list[dict[str, Any]], n: int = 14) -> Decimal:
    if len(candles) < n + 1:
        return Decimal("0")
    trs = []
    for prev, cur in zip(candles[-n - 1:-1], candles[-n:]):
        h, l, pc = D(cur["h"]), D(cur["l"]), D(prev["c"])
        trs.append(max(h - l, abs(h - pc), abs(l - pc)))
    return sum(trs, Decimal("0")) / Decimal(n)


def candle_features(md: HyperliquidMarketData, coin: str) -> dict[str, Any]:
    now_ms = int(time.time() * 1000)
    c15 = md.get_candles(coin, "15m", now_ms - 72 * 60 * 60 * 1000, now_ms)
    c1h = md.get_candles(coin, "1h", now_ms - 14 * 24 * 60 * 60 * 1000, now_ms)
    c4h = md.get_candles(coin, "4h", now_ms - 45 * 24 * 60 * 60 * 1000, now_ms)
    closes15 = [D(c["c"]) for c in c15]
    closes1h = [D(c["c"]) for c in c1h]
    closes4h = [D(c["c"]) for c in c4h]
    close = closes15[-1]
    e20_1h, e50_1h = ema(closes1h[-80:], 20), ema(closes1h[-100:], 50)
    e20_4h, e50_4h = ema(closes4h[-80:], 20), ema(closes4h[-100:], 50)
    r = rsi(closes1h, 14)
    a = atr(c1h, 14)
    ret_4h = (closes1h[-1] / closes1h[-5] - Decimal("1")) * Decimal("100") if len(closes1h) >= 5 and closes1h[-5] else Decimal("0")
    ret_24h = (closes1h[-1] / closes1h[-25] - Decimal("1")) * Decimal("100") if len(closes1h) >= 25 and closes1h[-25] else Decimal("0")
    vol_last = D(c1h[-1].get("v", 0)) if c1h else Decimal("0")
    vols = [D(c.get("v", 0)) for c in c1h[-25:-1]]
    vol_avg = sum(vols, Decimal("0")) / Decimal(len(vols)) if vols else Decimal("0")
    return {"coin": coin, "close": close, "ema20_1h": e20_1h, "ema50_1h": e50_1h, "ema20_4h": e20_4h, "ema50_4h": e50_4h, "rsi_1h": r, "atr_1h": a, "atr_pct": (a / close * Decimal("100")) if close else Decimal("0"), "ret_4h_pct": ret_4h, "ret_24h_pct": ret_24h, "vol_ratio": (vol_last / vol_avg) if vol_avg else Decimal("1")}


def market_regime(md: HyperliquidMarketData) -> dict[str, Any]:
    btc = candle_features(md, "BTC")
    eth = candle_features(md, "ETH")
    bull_votes = 0
    bear_votes = 0
    for f in (btc, eth):
        if f["close"] > f["ema20_1h"] > f["ema50_1h"] and f["ema20_4h"] >= f["ema50_4h"] and f["ret_24h_pct"] > 0:
            bull_votes += 1
        if f["close"] < f["ema20_1h"] < f["ema50_1h"] and f["ema20_4h"] <= f["ema50_4h"] and f["ret_24h_pct"] < 0:
            bear_votes += 1
    regime = "bullish" if bull_votes >= 2 else "bearish" if bear_votes >= 2 else "choppy"
    return {"regime": regime, "btc": btc, "eth": eth, "bull_votes": bull_votes, "bear_votes": bear_votes}


def expected_round_trip_cost_pct(
    spread_pct: Decimal,
    taker_fee_rate: Decimal = Decimal("0.00035"),
    safety_margin_pct: Decimal = Decimal("0.05"),
) -> Decimal:
    """Conservative percent hurdle before a paper signal is worth trading.

    spread_pct is already quoted in percent. taker_fee_rate is decimal, per side.
    """
    return D(spread_pct) + taker_fee_rate * Decimal("2") * Decimal("100") + safety_margin_pct


def funding_carry_24h_pct(side: str, funding_rate: Decimal) -> Decimal:
    """Expected 24h funding carry in percent; positive helps the position."""
    direction = Decimal("1") if side == "short" else Decimal("-1")
    return D(funding_rate) * Decimal("24") * Decimal("100") * direction


def extract_funding_rates(meta_and_asset_ctxs: Any) -> dict[str, Decimal]:
    """Extract current hourly funding rates from Hyperliquid metaAndAssetCtxs."""
    universe = meta_and_asset_ctxs[0].get("universe", []) if isinstance(meta_and_asset_ctxs, list) else []
    ctxs = meta_and_asset_ctxs[1] if isinstance(meta_and_asset_ctxs, list) and len(meta_and_asset_ctxs) > 1 else []
    rates: dict[str, Decimal] = {}
    for row, ctx in zip(universe, ctxs):
        name = row.get("name")
        if name:
            rates[str(name).upper()] = D(ctx.get("funding", "0"))
    return rates


def score_coin(
    f: dict[str, Any],
    regime: str,
    spread_pct: Decimal,
    funding_rate: Decimal = Decimal("0"),
    taker_fee_rate: Decimal = Decimal("0.00035"),
    min_edge_multiple: Decimal = Decimal("1.25"),
) -> tuple[str | None, Decimal, list[str]]:
    reasons = []
    side = None
    raw_edge = Decimal("0")
    funding_carry = Decimal("0")
    if spread_pct > Decimal("0.08"):
        reasons.append("spread_too_wide")
    if f["atr_pct"] > Decimal("4"):
        reasons.append("volatility_too_wild")
    if f["vol_ratio"] < Decimal("0.65"):
        reasons.append("weak_volume")
    if regime == "bullish":
        side = "long"
        if not (f["close"] > f["ema20_1h"] > f["ema50_1h"]): reasons.append("no_1h_uptrend")
        if f["ret_4h_pct"] <= Decimal("0.15"): reasons.append("weak_4h_momentum")
        if not (Decimal("48") <= f["rsi_1h"] <= Decimal("72")): reasons.append("rsi_not_healthy_long")
        raw_edge = f["ret_4h_pct"]
        funding_carry = funding_carry_24h_pct(side, funding_rate)
        score = raw_edge + (f["rsi_1h"] - Decimal("50")) / Decimal("10") + funding_carry
        if funding_carry < 0:
            reasons.append("funding_against_long")
    elif regime == "bearish":
        side = "short"
        if not (f["close"] < f["ema20_1h"] < f["ema50_1h"]): reasons.append("no_1h_downtrend")
        if f["ret_4h_pct"] >= Decimal("-0.15"): reasons.append("weak_4h_down_momentum")
        if not (Decimal("28") <= f["rsi_1h"] <= Decimal("52")): reasons.append("rsi_not_healthy_short")
        raw_edge = abs(f["ret_4h_pct"])
        funding_carry = funding_carry_24h_pct(side, funding_rate)
        score = raw_edge + (Decimal("50") - f["rsi_1h"]) / Decimal("10") + funding_carry
        if funding_carry < 0:
            reasons.append("funding_against_short")
        if f["ret_24h_pct"] <= Decimal("-7") and f["rsi_1h"] <= Decimal("32"):
            reasons.append("short_chase_risk_overextended")
            score *= Decimal("0.4")
    else:
        side = None
        score = Decimal("0")
        reasons.append("market_choppy")

    if side:
        hurdle = expected_round_trip_cost_pct(spread_pct, taker_fee_rate=taker_fee_rate) * min_edge_multiple
        carry_adjusted_edge = raw_edge + funding_carry
        if carry_adjusted_edge < hurdle:
            reasons.append("edge_too_small_after_costs")
            score *= Decimal("0.75")
    return side, score, reasons


def manage_positions(state: dict[str, Any], mids: dict[str, Decimal]) -> list[dict[str, Any]]:
    actions = []
    positions = state.setdefault("positions", {})
    for coin in list(positions):
        p = positions[coin]
        mid = mids.get(coin)
        if mid is None:
            continue
        side = p["side"]
        entry = D(p["entry"])
        stop = D(p["stop"])
        size = D(p["size"])
        risk_dist = abs(entry - D(p.get("initial_stop", stop)))
        if risk_dist <= 0:
            continue
        pnl_dist = (mid - entry) if side == "long" else (entry - mid)
        pnl_r = pnl_dist / risk_dist
        # trailing/profit lock in paper
        if pnl_r >= Decimal("1.2"):
            if side == "long":
                new_stop = max(stop, entry + pnl_dist * Decimal("0.35"), mid - risk_dist * Decimal("0.75"))
            else:
                new_stop = min(stop, entry - pnl_dist * Decimal("0.35"), mid + risk_dist * Decimal("0.75"))
            p["stop"] = str(new_stop)
        stop_hit = (side == "long" and mid <= D(p["stop"])) or (side == "short" and mid >= D(p["stop"]))
        time_exit = (time.time() - float(p.get("opened_ts", time.time()))) > 4 * 3600 and pnl_r < Decimal("0.25")
        regime_exit = False
        if stop_hit or time_exit or regime_exit:
            gross = pnl_dist * size
            fees = D(p["notional"]) * Decimal("0.0009")
            net = gross - fees
            state["realized_pnl"] = str(D(state.get("realized_pnl")) + net)
            state["closed_trades"] = int(state.get("closed_trades", 0)) + 1
            if net > 0: state["wins"] = int(state.get("wins", 0)) + 1
            else: state["losses"] = int(state.get("losses", 0)) + 1
            row = {"event": "exit", "coin": coin, "side": side, "entry": entry, "exit": mid, "net_pnl_usd": net, "reason": "stop" if stop_hit else "dead_fish_time_exit", "paper_trading": True, "mainnet_signed_action": False, "pnl_r": pnl_r}
            append(TRADES, row)
            actions.append(row)
            positions.pop(coin, None)
    return actions


def run_once(coins: list[str]) -> dict[str, Any]:
    if os.getenv("CTB_LIVE_TRADING_ALLOWED", "").lower() == "true":
        raise PermissionError("shadow trader refuses CTB_LIVE_TRADING_ALLOWED=true")
    state = load_state()
    md = HyperliquidMarketData("mainnet")
    mids = md.get_all_mids()
    funding_rates = extract_funding_rates(md.get_meta_and_asset_ctxs())
    regime = market_regime(md)
    exits = manage_positions(state, mids)
    entries = []
    if len(state.get("positions", {})) < 3 and regime["regime"] != "choppy":
        candidates = []
        for coin in coins:
            if coin in state.get("positions", {}):
                continue
            try:
                f = candle_features(md, coin)
                book = md.get_l2_book(coin)
                spread = summarize_l2_book(book).spread_pct or Decimal("99")
                side, score, reasons = score_coin(f, regime["regime"], spread, funding_rate=funding_rates.get(coin, Decimal("0")))
                append(SIGNALS, {"coin": coin, "side": side, "score": score, "reasons": reasons, "regime": regime["regime"], "features": f, "spread_pct": spread, "funding_rate": funding_rates.get(coin, Decimal("0")), "funding_carry_24h_pct": funding_carry_24h_pct(side, funding_rates.get(coin, Decimal("0"))) if side else Decimal("0"), "paper_trading": True, "mainnet_signed_action": False})
                if side and not reasons:
                    candidates.append((score, coin, side, f))
            except Exception as exc:
                append(HEALTH, {"event": "coin_scan_error", "coin": coin, "error_type": type(exc).__name__, "message": str(exc)[:200]})
        candidates.sort(reverse=True, key=lambda x: x[0])
        for score, coin, side, f in candidates[: 3 - len(state.get("positions", {}))]:
            entry = D(f["close"])
            atrv = max(D(f["atr_1h"]), entry * Decimal("0.006"))
            stop = entry - atrv if side == "long" else entry + atrv
            notional = Decimal("15")
            size = notional / entry
            state.setdefault("positions", {})[coin] = {"side": side, "entry": str(entry), "stop": str(stop), "initial_stop": str(stop), "size": str(size), "notional": str(notional), "opened_at": datetime.now(timezone.utc).isoformat(), "opened_ts": time.time(), "score": str(score), "regime": regime["regime"]}
            row = {"event": "entry", "coin": coin, "side": side, "entry": entry, "stop": stop, "notional": notional, "score": score, "regime": regime["regime"], "paper_trading": True, "mainnet_signed_action": False}
            append(TRADES, row)
            entries.append(row)
    save_state(state)
    return {"status": "ok", "regime": regime["regime"], "positions": state.get("positions", {}), "entries": entries, "exits": exits, "realized_pnl": state.get("realized_pnl", "0"), "closed_trades": state.get("closed_trades", 0), "wins": state.get("wins", 0), "losses": state.get("losses", 0), "mainnet_signed_action": False, "paper_trading": True}


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Market-regime shadow trader: paper only, long in bullish regimes, short in bearish regimes.")
    parser.add_argument("--coins", default=",".join(PRIMARY))
    parser.add_argument("--iterations", type=int, default=1)
    parser.add_argument("--interval-seconds", type=int, default=60)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args(argv)
    RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
    PID_PATH.write_text(str(os.getpid()), encoding="utf-8")
    coins = [c.strip().upper() for c in args.coins.split(",") if c.strip()]
    results = []
    for i in range(args.iterations):
        try:
            results.append(run_once(coins))
        except Exception as exc:
            row = {
                "event": "api_degraded",
                "status": "degraded",
                "error_type": type(exc).__name__,
                "message": str(exc)[:300],
                "paper_trading": True,
                "mainnet_signed_action": False,
                "entries_blocked": True,
            }
            append(HEALTH, row)
            results.append(row)
        if i < args.iterations - 1:
            time.sleep(args.interval_seconds)
    payload = {"status": "degraded" if any(r.get("status") == "degraded" for r in results) else "ok", "runtime_dir": str(RUNTIME_DIR), "results": results}
    print(json.dumps(payload, indent=2, sort_keys=True, default=_json_default) if args.json else payload)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
