"""
starter_3_negrisk_arb.py — NegRisk multi-condition arbitrage bot.

NegRisk events are mutually-exclusive multi-outcome markets (e.g. "Who wins
the 2028 election?"). Polymarket guarantees at most one outcome resolves YES,
so the sum of all YES prices SHOULD equal $1.00. When it doesn't:

    sum(best_ask_YES across all conditions) < 1.00 - epsilon
       => buy every YES, guaranteed exactly $1 payout

This was historically the most profitable arb on Polymarket — IMDEA's 2025
study attributed $29M of $40M total arb profit to NegRisk rebalancing. Edges
have compressed but still appear during news shocks and on long-tail markets.

Customize: MIN_EDGE_USD, MAX_TRADE_USD, MIN_OUTCOMES, MAX_OUTCOMES, target events.

Run:
    python main.py

Defaults to DRY_RUN=true.
"""

from __future__ import annotations

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

from dotenv import load_dotenv

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

GAMMA = "https://gamma-api.polymarket.com"

# Floor of expected edge per $1 of synthetic basket, AFTER fees.
MIN_EDGE_USD = float(os.getenv("MIN_EDGE_USD", "0.01"))
MAX_TRADE_USD = float(os.getenv("MAX_TRADE_USD", "100"))
MIN_DEPTH_USD = float(os.getenv("MIN_DEPTH_USD", "20"))
# NegRisk events come in many sizes. Skip giant buckets where leg count blows
# past your bankroll, and skip 2-outcome events (those are normal binary arb).
MIN_OUTCOMES = int(os.getenv("MIN_OUTCOMES", "3"))
MAX_OUTCOMES = int(os.getenv("MAX_OUTCOMES", "20"))
SCAN_INTERVAL_SEC = float(os.getenv("SCAN_INTERVAL_SEC", "10"))

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


# ---------------------------------------------------------------------------
# NegRisk discovery via Gamma /events
# ---------------------------------------------------------------------------

async def fetch_negrisk_events(limit: int = 200) -> list[dict]:
    """
    Pull events that are NegRisk-flagged from Gamma.
    Field name varies — neg_risk / negRisk on different versions.
    """
    http = await get_http()
    params = {"closed": "false", "active": "true", "limit": limit}
    r = await http.get(f"{GAMMA}/events", params=params)
    r.raise_for_status()
    events = r.json()
    out = []
    for ev in events:
        if ev.get("negRisk") or ev.get("neg_risk") or ev.get("negRiskMarketID"):
            n = len(ev.get("markets") or [])
            if MIN_OUTCOMES <= n <= MAX_OUTCOMES:
                out.append(ev)
    return out


async def evaluate_event(client, event: dict) -> Optional[dict]:
    """
    For one NegRisk event, fetch all condition books and check if
    sum of best YES asks < 1.0. Return opportunity dict or None.
    """
    markets = event.get("markets") or []
    legs: list[dict] = []
    sum_ask = 0.0
    min_depth_usd = float("inf")

    for m in markets:
        try:
            yes_id, _ = parse_token_ids(m)
        except Exception:
            return None  # malformed — skip whole event
        book = await asyncio.to_thread(get_market_book, client, yes_id)
        asks = book.get("asks") or []
        if not asks:
            return None  # missing liquidity
        price = float(asks[0]["price"])
        depth = book_depth_at_price(book, "buy", price)
        depth_usd = depth * price
        sum_ask += price
        min_depth_usd = min(min_depth_usd, depth_usd)
        legs.append({"token_id": yes_id, "price": price, "depth_usd": depth_usd, "slug": m.get("slug")})

    raw_edge = 1.0 - sum_ask
    net_edge = raw_edge * 0.98  # rough fee adjustment
    if net_edge < MIN_EDGE_USD:
        return None
    size_usd = min(min_depth_usd, MAX_TRADE_USD)
    if size_usd < MIN_DEPTH_USD:
        return None

    return {
        "event_slug": event.get("slug"),
        "event_title": event.get("title"),
        "n_outcomes": len(legs),
        "sum_ask": sum_ask,
        "net_edge": net_edge,
        "size_usd": size_usd,
        "legs": legs,
    }


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

async def scan_once(client, risk: RiskLayer, log: JsonlLogger) -> int:
    events = await fetch_negrisk_events()
    found = 0
    for ev in events:
        try:
            opp = await evaluate_event(client, ev)
        except Exception as e:
            log.log("event_error", event=ev.get("slug"), error=str(e))
            continue
        if not opp:
            continue

        found += 1
        print(f"\n🎯 NEGRISK ARB: {opp['event_title']}")
        print(f"   {opp['n_outcomes']} legs, sum={opp['sum_ask']:.4f}")
        print(f"   Net edge: ${opp['net_edge']:.4f}/$1, size: ${opp['size_usd']:.2f}")
        log.log("negrisk_opportunity", **{k: v for k, v in opp.items() if k != "legs"})

        if DRY_RUN:
            print("   [DRY RUN]")
            continue

        # Submit ALL legs in parallel as FOK — partial fills break the arb!
        intents = []
        for leg in opp["legs"]:
            size = opp["size_usd"] / leg["price"]
            intent = OrderIntent(
                token_id=leg["token_id"], side="BUY", size=size, price=leg["price"],
                strategy_id="negrisk_v1", market_slug=leg["slug"],
            )
            if not risk.allow(intent):
                log.log("negrisk_skipped", reason="risk_layer", event=opp["event_slug"])
                intents = []
                break
            intents.append((intent, leg))

        if not intents:
            continue

        try:
            results = await asyncio.gather(*[
                asyncio.to_thread(
                    place_fok_with_retry, client, leg["token_id"], "BUY",
                    opp["size_usd"] / leg["price"], leg["price"],
                )
                for _, leg in intents
            ])
            for (intent, _), r in zip(intents, results):
                risk.record_fill(intent, r.filled_size)
            log.log("negrisk_executed", event=opp["event_slug"],
                    n_legs=len(results), legs=[r.__dict__ for r in results])
            print(f"   ✅ Executed {len(results)} legs")
        except Exception as e:
            log.log("negrisk_error", event=opp["event_slug"], error=str(e))
            print(f"   ❌ error: {e}")

    return found


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

    print("=" * 60)
    print("Polymarket NegRisk Multi-Condition Arbitrage")
    print("=" * 60)
    print(f"Mode:          {'DRY-RUN' if DRY_RUN else '🔴 LIVE'}")
    print(f"Min edge:      ${MIN_EDGE_USD}")
    print(f"Outcome range: [{MIN_OUTCOMES}, {MAX_OUTCOMES}]")
    print(f"Max trade:     ${MAX_TRADE_USD}")
    print("=" * 60)

    client = make_client()
    healthcheck(client)
    risk = RiskLayer.from_env(strategy_id="negrisk_v1")

    stop = asyncio.Event()

    def _shutdown(*_):
        print("\nShutdown...")
        stop.set()

    for sig in (signal.SIGINT, signal.SIGTERM):
        try:
            asyncio.get_running_loop().add_signal_handler(sig, _shutdown)
        except NotImplementedError:
            signal.signal(sig, _shutdown)

    cycles = 0
    total = 0
    while not stop.is_set():
        t0 = time.time()
        try:
            n = await scan_once(client, risk, log)
            total += n
            cycles += 1
            print(f"[cycle {cycles:>4}] {time.time() - t0:5.1f}s — opps: {n} (total {total})")
        except Exception as e:
            log.log("scan_error", error=str(e))
            print(f"⚠️ {e}")
        try:
            await asyncio.wait_for(stop.wait(), timeout=SCAN_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())
