"""
starter_7_ai_llm.py — AI/LLM-driven discretionary bot.

Pulls a candidate market list, scrapes news/context, and asks Claude (via the
Anthropic API) to output a structured Yes/No/skip + confidence + reasoning.
Bets the Kelly fraction (capped) when confidence > threshold.

⚠️  This is the LOWEST EXPECTED-VALUE strategy in this skill. LLM predictions
on prediction markets have a long history of underperforming the market price,
because the market price already integrates the news. Use as a research tool
or paper-trade only until you've shown a consistent edge over the close-line.

Customize: ANTHROPIC_MODEL, MIN_CONFIDENCE, KELLY_CAP, candidate filters.

Run:
    python main.py

Defaults to DRY_RUN=true.

Requires: ANTHROPIC_API_KEY environment variable, and `pip install anthropic`.
"""

from __future__ import annotations

import asyncio
import json
import os
import signal
import time
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
# ---------------------------------------------------------------------------

ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-opus-4-7")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")

# Only act when LLM disagrees with market by this much (probability units).
MIN_DISAGREEMENT = float(os.getenv("MIN_DISAGREEMENT", "0.10"))
# Hard floor on LLM-stated confidence.
MIN_CONFIDENCE = float(os.getenv("MIN_CONFIDENCE", "0.65"))
# Kelly fraction cap. Even if Kelly suggests 30%, we never risk more than this.
KELLY_CAP = float(os.getenv("KELLY_CAP", "0.05"))  # 5% of bankroll
BANKROLL_USD = float(os.getenv("BANKROLL_USD", "1000"))
MAX_TRADE_USD = float(os.getenv("MAX_TRADE_USD", "50"))

# Filter for candidates worth burning LLM tokens on.
MIN_VOLUME_USD = float(os.getenv("MIN_VOLUME_USD", "10000"))
PRICE_BAND_MIN = float(os.getenv("PRICE_BAND_MIN", "0.10"))   # skip 0–10c (tiny upside)
PRICE_BAND_MAX = float(os.getenv("PRICE_BAND_MAX", "0.90"))   # skip 90–100c (tiny upside, big risk)

SCAN_INTERVAL_SEC = float(os.getenv("SCAN_INTERVAL_SEC", "300"))  # LLM calls cost — be patient
DRY_RUN = os.getenv("DRY_RUN", "true").lower() != "false"


# ---------------------------------------------------------------------------
# Anthropic client (lazy import so the script runs even without the lib)
# ---------------------------------------------------------------------------

def _get_anthropic():
    try:
        from anthropic import Anthropic
    except ImportError:
        raise RuntimeError("pip install anthropic")
    if not ANTHROPIC_API_KEY:
        raise RuntimeError("Set ANTHROPIC_API_KEY in .env")
    return Anthropic(api_key=ANTHROPIC_API_KEY)


SYSTEM_PROMPT = """You are a calibrated forecaster evaluating prediction-market questions.

For each question, output a JSON object with these fields:
  - decision: "YES", "NO", or "SKIP" (only act when you have a real edge)
  - probability: your point estimate in [0, 1] for YES resolving true
  - confidence: how much weight to put on your estimate, in [0, 1]
  - reasoning: 2-3 sentences citing concrete factors

Rules:
- Be CONSERVATIVE. The market price already integrates public information.
  Only deviate from the market when you have a specific reason.
- Output VALID JSON only. No code fences, no commentary outside the object.
- If you genuinely don't know, output decision="SKIP" with low confidence."""


def ask_llm(client_anthropic, question: str, current_market_price: float,
            context: str = "") -> Optional[dict]:
    user_msg = f"""Question: {question}

Current market YES price: ${current_market_price:.4f} (implied probability {current_market_price:.1%})

Additional context: {context or '(none)'}

Output JSON now."""
    try:
        msg = client_anthropic.messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=400,
            system=SYSTEM_PROMPT,
            messages=[{"role": "user", "content": user_msg}],
        )
        text = msg.content[0].text if msg.content else ""
        # Strip code fences if Claude added them despite instructions.
        text = text.strip()
        if text.startswith("```"):
            text = text.split("```")[1]
            if text.startswith("json"):
                text = text[4:]
        return json.loads(text)
    except Exception as e:
        print(f"  ⚠️  LLM error: {e}")
        return None


def kelly_fraction(p: float, price: float) -> float:
    """
    Kelly for binary bets at odds (1/price - 1):
        f* = (p*(b+1) - 1) / b   where b = 1/price - 1 = (1-price)/price
    Returns 0 if negative EV.
    """
    if price <= 0 or price >= 1:
        return 0.0
    b = (1 - price) / price
    f = (p * (b + 1) - 1) / b
    return max(0.0, f)


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

async def evaluate_market(poly_client, anthropic, m: dict, log: JsonlLogger,
                          risk: RiskLayer):
    try:
        yes_id, no_id = parse_token_ids(m)
    except Exception:
        return
    book = await asyncio.to_thread(get_market_book, poly_client, yes_id)
    asks = book.get("asks") or []
    bids = book.get("bids") or []
    if not asks or not bids:
        return
    yes_ask = float(asks[0]["price"])
    yes_bid = float(bids[0]["price"])
    mid = (yes_ask + yes_bid) / 2
    if not (PRICE_BAND_MIN <= mid <= PRICE_BAND_MAX):
        return

    question = m.get("question") or m.get("title") or m.get("slug", "")
    desc = (m.get("description") or "")[:1000]
    decision = await asyncio.to_thread(ask_llm, anthropic, question, mid, desc)
    if not decision:
        return

    log.log("llm_decision", market=m.get("slug"), market_price=mid, **decision)
    p = decision.get("probability", mid)
    conf = decision.get("confidence", 0.0)
    side = decision.get("decision", "SKIP").upper()
    reasoning = decision.get("reasoning", "")[:200]

    print(f"\n🤖 {question[:60]}")
    print(f"   market={mid:.3f} | LLM p={p:.3f} conf={conf:.2f} → {side}")
    print(f"   {reasoning}")

    if side == "SKIP" or conf < MIN_CONFIDENCE:
        return

    if side == "YES":
        edge = p - yes_ask
        token_id = yes_id
        price = yes_ask
    else:  # NO
        no_ask = (1.0 - yes_bid)  # rough — actually should fetch NO book
        edge = (1 - p) - no_ask
        token_id = no_id
        price = no_ask

    if edge < MIN_DISAGREEMENT:
        return

    f_kelly = kelly_fraction(p if side == "YES" else 1 - p, price)
    f = min(f_kelly, KELLY_CAP)
    size_usd = min(f * BANKROLL_USD, MAX_TRADE_USD)
    if size_usd < 1:
        return
    size = size_usd / price

    print(f"   ✓ EDGE {edge:.3f}, kelly={f_kelly:.3f}, betting ${size_usd:.2f} @ {price:.3f}")
    log.log("llm_bet_intended", market=m.get("slug"), side=side,
            price=price, size_usd=size_usd, edge=edge, kelly=f_kelly)

    if DRY_RUN:
        return

    intent = OrderIntent(
        token_id=token_id, side="BUY", size=size, price=price,
        strategy_id="ai_llm_v1", market_slug=m.get("slug"),
    )
    if not risk.allow(intent):
        log.log("llm_skipped", reason="risk_layer", market=m.get("slug"))
        return
    try:
        r = await asyncio.to_thread(
            place_fok_with_retry, poly_client, token_id, "BUY", size, price
        )
        risk.record_fill(intent, r.filled_size)
        log.log("llm_executed", market=m.get("slug"), **r.__dict__)
    except Exception as e:
        log.log("llm_error", market=m.get("slug"), error=str(e))


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

    print("=" * 60)
    print("AI/LLM-Driven Discretionary Bot")
    print("=" * 60)
    print(f"Mode:           {'DRY-RUN' if DRY_RUN else '🔴 LIVE'}")
    print(f"LLM:            {ANTHROPIC_MODEL}")
    print(f"Min confidence: {MIN_CONFIDENCE}")
    print(f"Kelly cap:      {KELLY_CAP:.0%}")
    print(f"Bankroll:       ${BANKROLL_USD}")
    print("=" * 60)

    if not ANTHROPIC_API_KEY:
        print("⚠️  Set ANTHROPIC_API_KEY in .env")
        return

    poly = make_client()
    healthcheck(poly)
    anthropic = _get_anthropic()
    risk = RiskLayer.from_env(strategy_id="ai_llm_v1")

    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():
        markets = await fetch_active_markets(closed=False, limit=50)
        # Filter by volume to keep LLM cost bounded
        candidates = [m for m in markets if float(m.get("volume24hr", 0) or 0) >= MIN_VOLUME_USD]
        print(f"\n[{time.strftime('%H:%M:%S')}] {len(candidates)} candidates (≥${MIN_VOLUME_USD:.0f} 24h vol)")
        for m in candidates[:20]:  # cap LLM calls per cycle
            try:
                await evaluate_market(poly, anthropic, m, log, risk)
            except Exception as e:
                log.log("eval_error", market=m.get("slug"), error=str(e))
        try:
            await asyncio.wait_for(stop.wait(), timeout=SCAN_INTERVAL_SEC)
        except asyncio.TimeoutError:
            pass

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


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