"""
starter_6_market_making.py — Market-making bot with spread bands.

Quotes both sides of the book at configurable distances (in cents) from the
mid. Re-quotes when:
  - the mid moves outside a tolerance band
  - inventory drifts past the per-token cap
  - too much time has passed since last refresh

Targets the Polymarket maker-rewards program. Reward eligibility tightened
in 2025/2026 — orders must be near best bid/ask, in size, and persistent.
Read https://learn.polymarket.com/docs/guides/maker-rewards before going live.

⚠️  February 2026 nonce-cancellation attack: market makers running multiple
strategies on one EOA had quotes stuffed by a tiny, attacker-controlled
nonce. Mitigations:
   - Use one EOA per strategy (separate wallets)
   - Watch your own pending tx pool; if a low-value cancel from your address
     hits the chain that you didn't sign, kill switch immediately.
   - Cap per-market exposure aggressively at first.

Customize: TARGET_MARKETS, SPREAD_BPS, INVENTORY_CAP, ORDER_SIZE.

Run:
    python main.py

Defaults to DRY_RUN=true.
"""

from __future__ import annotations

import asyncio
import os
import signal
import time
from dataclasses import dataclass

from dotenv import load_dotenv

from common import (
    JsonlLogger,
    OrderIntent,
    RiskLayer,
    cancel_all,
    close_http,
    fetch_market_by_slug,
    get_market_book,
    healthcheck,
    make_client,
    parse_token_ids,
    place_gtc,
)


# ---------------------------------------------------------------------------
# Config — list of slugs to make markets on
# ---------------------------------------------------------------------------

TARGET_SLUGS = [s.strip() for s in os.getenv("TARGET_SLUGS", "").split(",") if s.strip()]

# Spread to quote each side, in cents. 0.01 = 1 cent. Tighter = higher fill
# rate but worse adverse selection.
SPREAD_CENTS = float(os.getenv("SPREAD_CENTS", "0.01"))
# Reference: when mid moves by more than this, re-quote.
REQUOTE_THRESHOLD_CENTS = float(os.getenv("REQUOTE_THRESHOLD_CENTS", "0.005"))
# Order size in shares per side.
ORDER_SIZE = float(os.getenv("ORDER_SIZE", "20"))
# Max net position (shares) per market. Stop quoting the heavier side at cap.
INVENTORY_CAP = float(os.getenv("INVENTORY_CAP", "500"))
# Refresh quotes every N seconds even if mid stable.
REFRESH_INTERVAL_SEC = float(os.getenv("REFRESH_INTERVAL_SEC", "30"))
# Skip markets where the spread is already wider than this (probably stale or illiquid)
MAX_NATURAL_SPREAD = float(os.getenv("MAX_NATURAL_SPREAD", "0.05"))

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


@dataclass
class MarketState:
    slug: str
    yes_token: str
    no_token: str
    last_mid_yes: float = 0.0
    last_quote_ts: float = 0.0
    inventory_yes: float = 0.0  # shares of YES token held
    open_orders: dict = None    # {"buy": order_id, "sell": order_id}

    def __post_init__(self):
        if self.open_orders is None:
            self.open_orders = {"buy": None, "sell": None}


def round_price(p: float) -> float:
    """Polymarket tick size is 1 cent. Snap to grid."""
    return round(min(max(p, 0.01), 0.99), 2)


# ---------------------------------------------------------------------------
# Per-market quote loop
# ---------------------------------------------------------------------------

async def update_quotes(client, state: MarketState, risk: RiskLayer, log: JsonlLogger):
    book = await asyncio.to_thread(get_market_book, client, state.yes_token)
    bids = book.get("bids") or []
    asks = book.get("asks") or []
    if not bids or not asks:
        return

    best_bid = float(bids[0]["price"])
    best_ask = float(asks[0]["price"])
    natural_spread = best_ask - best_bid
    if natural_spread > MAX_NATURAL_SPREAD:
        log.log("mm_skip", slug=state.slug, reason="wide_spread", spread=natural_spread)
        return

    mid = (best_bid + best_ask) / 2
    if (
        abs(mid - state.last_mid_yes) < REQUOTE_THRESHOLD_CENTS
        and time.time() - state.last_quote_ts < REFRESH_INTERVAL_SEC
    ):
        return  # nothing to do

    bid_px = round_price(mid - SPREAD_CENTS)
    ask_px = round_price(mid + SPREAD_CENTS)
    if bid_px >= ask_px:
        log.log("mm_skip", slug=state.slug, reason="crossed", bid=bid_px, ask=ask_px)
        return

    # Inventory management: skew or pause one side at cap.
    quote_buy = state.inventory_yes < INVENTORY_CAP
    quote_sell = state.inventory_yes > -INVENTORY_CAP

    print(f"[{state.slug}] mid={mid:.4f} → quote bid={bid_px:.4f} ask={ask_px:.4f} inv={state.inventory_yes:+.0f}")
    log.log("mm_requote", slug=state.slug, mid=mid, bid=bid_px, ask=ask_px,
            inventory=state.inventory_yes)

    if DRY_RUN:
        state.last_mid_yes = mid
        state.last_quote_ts = time.time()
        return

    # Cancel old quotes for this token before placing new ones.
    try:
        cancel_all(client, [state.yes_token])
    except Exception as e:
        log.log("mm_cancel_error", slug=state.slug, error=str(e))

    if quote_buy:
        intent = OrderIntent(
            token_id=state.yes_token, side="BUY", size=ORDER_SIZE, price=bid_px,
            strategy_id="mm_v1", market_slug=state.slug,
        )
        if risk.allow(intent):
            try:
                r = place_gtc(client, state.yes_token, "BUY", ORDER_SIZE, bid_px)
                state.open_orders["buy"] = r.order_id
                log.log("mm_buy_placed", slug=state.slug, **r.__dict__)
            except Exception as e:
                log.log("mm_buy_error", slug=state.slug, error=str(e))

    if quote_sell:
        intent = OrderIntent(
            token_id=state.yes_token, side="SELL", size=ORDER_SIZE, price=ask_px,
            strategy_id="mm_v1", market_slug=state.slug,
        )
        if risk.allow(intent):
            try:
                r = place_gtc(client, state.yes_token, "SELL", ORDER_SIZE, ask_px)
                state.open_orders["sell"] = r.order_id
                log.log("mm_sell_placed", slug=state.slug, **r.__dict__)
            except Exception as e:
                log.log("mm_sell_error", slug=state.slug, error=str(e))

    state.last_mid_yes = mid
    state.last_quote_ts = time.time()


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

    print("=" * 60)
    print("Polymarket Market Maker")
    print("=" * 60)
    print(f"Mode:        {'DRY-RUN' if DRY_RUN else '🔴 LIVE'}")
    print(f"Markets:     {len(TARGET_SLUGS)}")
    print(f"Spread:      ±{SPREAD_CENTS:.3f}")
    print(f"Order size:  {ORDER_SIZE}")
    print(f"Inv cap:     ±{INVENTORY_CAP}")
    print("=" * 60)

    if not TARGET_SLUGS:
        print("⚠️  Set TARGET_SLUGS in .env (comma-separated market slugs)")
        return

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

    states: list[MarketState] = []
    for slug in TARGET_SLUGS:
        m = await fetch_market_by_slug(slug)
        if not m:
            print(f"  ⚠️  market not found: {slug}")
            continue
        try:
            yes_id, no_id = parse_token_ids(m)
        except Exception as e:
            print(f"  ⚠️  parse error for {slug}: {e}")
            continue
        states.append(MarketState(slug=slug, yes_token=yes_id, no_token=no_id))
        print(f"  ✓ {slug}")

    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())

    while not stop.is_set():
        for s in states:
            try:
                await update_quotes(client, s, risk, log)
            except Exception as e:
                log.log("mm_error", slug=s.slug, error=str(e))
                print(f"⚠️  {s.slug}: {e}")
        try:
            await asyncio.wait_for(stop.wait(), timeout=2.0)
        except asyncio.TimeoutError:
            pass

    print("Cancelling all open quotes...")
    try:
        cancel_all(client)
    except Exception as e:
        print(f"cancel_all error: {e}")
    await close_http()
    print("Done.")


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