"""
starter_9_5min_crypto.py — 5-minute crypto Up/Down bot.

Polymarket runs continuous "Will BTC be Up or Down in the next 5 minutes?"
markets on BTC/ETH/SOL/XRP, settled by Chainlink price feeds. Each market
expires every 5 minutes, replaced by a fresh one.

This template uses a SIMPLE momentum signal (rolling N-minute return) as the
strategy stub. You should replace it with whatever edge you actually have:
order-flow imbalance, funding-rate shifts, news sentiment, etc.

⚠️  These markets are *very* efficient. Last-second price action by other
bots is the norm. If your signal isn't clearly +EV after fees on out-of-sample
data, paper-trade until you find one.

Customize: ASSETS, MOMENTUM_LOOKBACK_SEC, MIN_PROB_EDGE, MAX_TRADE_USD.

Run:
    python main.py

Defaults to DRY_RUN=true.
"""

from __future__ import annotations

import asyncio
import os
import signal
import time
from collections import deque
from typing import Optional

from dotenv import load_dotenv

from common import (
    JsonlLogger,
    OrderIntent,
    RiskLayer,
    cancel_all,
    close_http,
    fetch_active_markets,
    get_http,
    get_market_book,
    healthcheck,
    make_client,
    parse_token_ids,
    place_fok_with_retry,
)


# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

ASSETS = [a.strip().upper() for a in os.getenv("ASSETS", "BTC,ETH,SOL,XRP").split(",")]

# Use external price feed for our signal (Polymarket's own feeds are settle-only).
COINGECKO_IDS = {
    "BTC": "bitcoin",
    "ETH": "ethereum",
    "SOL": "solana",
    "XRP": "ripple",
}

# Lookback for momentum signal in seconds.
MOMENTUM_LOOKBACK_SEC = float(os.getenv("MOMENTUM_LOOKBACK_SEC", "60"))
# Required edge in implied probability units to trade. 0.05 = 5pp.
MIN_PROB_EDGE = float(os.getenv("MIN_PROB_EDGE", "0.05"))
# Per-trade USD cap.
MAX_TRADE_USD = float(os.getenv("MAX_TRADE_USD", "10"))
# Don't enter inside this many seconds before market expiry (avoid bad fills).
ENTRY_DEADLINE_SEC = float(os.getenv("ENTRY_DEADLINE_SEC", "60"))
# Polling interval for prices and books.
POLL_INTERVAL_SEC = float(os.getenv("POLL_INTERVAL_SEC", "5"))

DRY_RUN = os.getenv("DRY_RUN", "true").lower() != "false"


# ---------------------------------------------------------------------------
# Price feed
# ---------------------------------------------------------------------------

class PriceTracker:
    """Simple deque-backed price history per asset."""

    def __init__(self, max_seconds: float = 600):
        self.max_seconds = max_seconds
        self.history: dict[str, deque] = {a: deque() for a in ASSETS}

    async def update(self):
        http = await get_http()
        ids = ",".join(COINGECKO_IDS[a] for a in ASSETS)
        url = f"https://api.coingecko.com/api/v3/simple/price?ids={ids}&vs_currencies=usd"
        try:
            r = await http.get(url, timeout=5.0)
            r.raise_for_status()
            data = r.json()
            now = time.time()
            for asset in ASSETS:
                cg_id = COINGECKO_IDS[asset]
                price = data.get(cg_id, {}).get("usd")
                if price is None:
                    continue
                dq = self.history[asset]
                dq.append((now, float(price)))
                while dq and dq[0][0] < now - self.max_seconds:
                    dq.popleft()
        except Exception as e:
            print(f"  ⚠️  coingecko: {e}")

    def momentum(self, asset: str, lookback_sec: float) -> Optional[float]:
        dq = self.history.get(asset)
        if not dq or len(dq) < 2:
            return None
        now = dq[-1][0]
        latest = dq[-1][1]
        for ts, px in dq:
            if ts >= now - lookback_sec:
                if px <= 0:
                    return None
                return (latest - px) / px
        return None


# ---------------------------------------------------------------------------
# Find current 5-min markets
# ---------------------------------------------------------------------------

async def find_5min_markets() -> dict[str, dict]:
    """Returns dict of asset -> active 5-minute up/down market."""
    markets = await fetch_active_markets(closed=False, limit=200)
    out = {}
    for m in markets:
        slug = (m.get("slug") or "").lower()
        question = (m.get("question") or "").lower()
        text = slug + " " + question
        # Heuristic: 5-minute markets contain "up or down" plus the asset symbol
        if "up or down" not in text:
            continue
        for asset in ASSETS:
            if asset.lower() in text and asset not in out:
                out[asset] = m
                break
    return out


def market_expiry_seconds(m: dict) -> Optional[float]:
    """Seconds remaining until market closes."""
    end = m.get("endDate") or m.get("end_date")
    if not end:
        return None
    try:
        from datetime import datetime, timezone as tz
        if isinstance(end, str):
            dt = datetime.fromisoformat(end.replace("Z", "+00:00"))
        else:
            dt = datetime.fromtimestamp(float(end), tz=tz.utc)
        return (dt - datetime.now(tz.utc)).total_seconds()
    except Exception:
        return None


# ---------------------------------------------------------------------------
# Strategy
# ---------------------------------------------------------------------------

async def evaluate_asset(client, prices: PriceTracker, asset: str, market: dict,
                         risk: RiskLayer, log: JsonlLogger):
    expiry = market_expiry_seconds(market)
    if expiry is None or expiry < ENTRY_DEADLINE_SEC:
        return
    momentum = prices.momentum(asset, MOMENTUM_LOOKBACK_SEC)
    if momentum is None:
        return

    # Crude calibration: 0.5pp move over lookback → 60% prob of "Up"
    # YOU SHOULD REPLACE THIS with a properly fitted model on historical data.
    implied_prob_up = 0.5 + min(max(momentum * 100.0, -0.4), 0.4)

    try:
        yes_id, no_id = parse_token_ids(market)
    except Exception:
        return

    # In Polymarket's UpDown markets, "Up" is typically the YES outcome.
    yes_book = await asyncio.to_thread(get_market_book, client, yes_id)
    asks = yes_book.get("asks") or []
    bids = yes_book.get("bids") or []
    if not asks or not bids:
        return
    yes_ask = float(asks[0]["price"])
    yes_bid = float(bids[0]["price"])

    edge_up = implied_prob_up - yes_ask  # positive = YES is cheap
    edge_dn = (1 - implied_prob_up) - (1 - yes_bid)  # NO mispriced

    print(f"  {asset}: mom={momentum*100:+.3f}%  p_up={implied_prob_up:.3f}  "
          f"yes={yes_bid:.3f}/{yes_ask:.3f}  expiry in {expiry:.0f}s")

    log.log("crypto_signal", asset=asset, momentum=momentum,
            implied_prob_up=implied_prob_up, yes_ask=yes_ask, yes_bid=yes_bid,
            expiry_sec=expiry)

    if edge_up >= MIN_PROB_EDGE:
        token_id, price, side_label = yes_id, yes_ask, "YES (Up)"
    elif edge_dn >= MIN_PROB_EDGE:
        token_id, price, side_label = no_id, 1 - yes_bid, "NO (Down)"
    else:
        return

    size_usd = min(MAX_TRADE_USD, 10.0)
    size = size_usd / price

    print(f"    🎯 {asset} {side_label} @ {price:.3f}  size=${size_usd:.2f}")
    log.log("crypto_intent", asset=asset, side=side_label, price=price, size_usd=size_usd)

    if DRY_RUN:
        return

    intent = OrderIntent(
        token_id=token_id, side="BUY", size=size, price=price,
        strategy_id="crypto5_v1", market_slug=market.get("slug"),
    )
    if not risk.allow(intent):
        return
    try:
        r = await asyncio.to_thread(
            place_fok_with_retry, client, token_id, "BUY", size, price
        )
        risk.record_fill(intent, r.filled_size)
        log.log("crypto_executed", asset=asset, **r.__dict__)
    except Exception as e:
        log.log("crypto_error", asset=asset, error=str(e))


async def main():
    load_dotenv()
    log = JsonlLogger("logs/crypto5_v1")

    print("=" * 60)
    print("5-Minute Crypto Up/Down Bot")
    print("=" * 60)
    print(f"Mode:        {'DRY-RUN' if DRY_RUN else '🔴 LIVE'}")
    print(f"Assets:      {', '.join(ASSETS)}")
    print(f"Lookback:    {MOMENTUM_LOOKBACK_SEC}s")
    print(f"Min edge:    {MIN_PROB_EDGE:.0%}")
    print(f"Max trade:   ${MAX_TRADE_USD}")
    print("=" * 60)

    client = make_client()
    healthcheck(client)
    risk = RiskLayer.from_env(strategy_id="crypto5_v1")
    prices = PriceTracker()

    # Warm up price history
    print("Warming up price history (60s)...")
    for _ in range(12):
        await prices.update()
        await asyncio.sleep(5)

    stop = asyncio.Event()
    for sig in (signal.SIGINT, signal.SIGTERM):
        try:
            asyncio.get_running_loop().add_signal_handler(sig, stop.set)
        except NotImplementedError:
            signal.signal(sig, lambda *_: stop.set())

    last_market_refresh = 0.0
    market_cache: dict[str, dict] = {}

    while not stop.is_set():
        await prices.update()
        # Refresh active 5-min markets every 60s (each lasts 5 mins)
        if time.time() - last_market_refresh > 60:
            market_cache = await find_5min_markets()
            last_market_refresh = time.time()
            print(f"  active markets: {list(market_cache.keys())}")

        for asset, market in market_cache.items():
            try:
                await evaluate_asset(client, prices, asset, market, risk, log)
            except Exception as e:
                log.log("eval_error", asset=asset, error=str(e))

        try:
            await asyncio.wait_for(stop.wait(), timeout=POLL_INTERVAL_SEC)
        except asyncio.TimeoutError:
            pass

    if not DRY_RUN:
        try:
            cancel_all(client)
        except Exception:
            pass
    await close_http()
    print("Done.")


if __name__ == "__main__":
    asyncio.run(main())
