"""
Lighter execution — real SDK calls, dry-run by default.

Verified against the installed SDK on 2026-11-11:
  SignerClient(url, account_index, {api_key_index: private_key}, chain_id=...)
  create_market_order(market_index, client_order_index, base_amount,
                      avg_execution_price, is_ask, reduce_only=...)
  create_order(..., price, is_ask, order_type, time_in_force,
               reduce_only=, trigger_price=)          <- stop-loss lives here
  cancel_order(market_index, order_index)
  TESTNET = https://testnet.zklighter.elliot.ai  chain_id 300
  MAINNET = https://mainnet.zklighter.elliot.ai  chain_id 304

WHAT WORKS WITHOUT KEYS
    python lighter_exec.py --probe        # public testnet, finds the ZEC market

WHAT NEEDS KEYS (yours, on your machine, never in chat)
    export LIGHTER_ACCOUNT_INDEX=...      # account index of your Lighter account
    export LIGHTER_API_KEY_INDEX=0
    export LIGHTER_API_PRIVATE_KEY=...    # created with ChangePubKey (trade only)
    python lighter_exec.py --probe-live   # authenticated read: balance + positions

SIZING WARNING (read before live)
    Lighter takes base_amount as an integer in the market's own size decimals.
    `--probe` prints that scale. This module refuses to send an order whose
    base_amount was not derived from that scale, which is why DRY_RUN defaults
    to True and every call prints the exact payload first.
"""
import argparse
import asyncio
import os
import sys
from decimal import Decimal as D

try:
    import lighter
    from lighter import SignerClient
except ImportError:
    sys.exit("pip install lighter-sdk")


def profile(testnet=True):
    return lighter.TESTNET if testnet else lighter.MAINNET


class LighterBroker:
    """Thin, honest wrapper. Nothing is sent unless dry_run=False."""

    def __init__(self, testnet=True, symbol="ZEC", dry_run=True):
        p = profile(testnet)
        self.url, self.chain_id, self.testnet = p.api_url, p.chain_id, testnet
        self.symbol = symbol.upper()
        self.dry_run = dry_run
        self.market = None            # dict(index, symbol, price_tick, size_decimals, ...)
        self.signer = None
        self.account_index = None
        self.client_order_index = 1

    # ── public, no keys ────────────────────────────────────────────────────
    async def connect(self):
        cfg = lighter.Configuration(host=self.url)
        client = lighter.ApiClient(cfg)
        try:
            api = lighter.OrderApi(client)
            res = await api.order_book_details()
            books = getattr(res, "order_book_details", None) or getattr(res, "order_books", None) or []
            self.symbols = [str(getattr(b, "symbol", "")) for b in books]
            zec = next((b for b in books
                        if self.symbol in str(getattr(b, "symbol", "")).upper()), None)
            if zec is None:
                raise RuntimeError(
                    f"{self.symbol} not listed on {self.url} "
                    f"({len(books)} markets, e.g. {', '.join(self.symbols[:10])})")
            raw = zec.to_dict() if hasattr(zec, "to_dict") else vars(zec)
        finally:
            await client.close()
        self.market = {k: raw.get(k) for k in raw}
        self.market["index"] = raw.get("market_id", raw.get("market_index"))
        self.market["tick"] = raw.get("price_tick", raw.get("tick_size"))
        self.market["size_decimals"] = raw.get("size_decimals", raw.get("base_decimals"))
        self.market["min_base"] = raw.get("min_base_amount", raw.get("min_size"))
        return self.market

    # ── signed ─────────────────────────────────────────────────────────────
    def sign_in(self, account_index, api_key_index, private_key):
        self.account_index = account_index
        self.signer = SignerClient(self.url, account_index,
                                   {api_key_index: private_key}, chain_id=self.chain_id)
        err = self.signer.check_client()
        if err is not None:
            raise RuntimeError(f"signer refused: {err}")
        return True

    def _idx(self):
        n = self.client_order_index
        self.client_order_index += 1
        return n

    def _payload(self, side, base_amount, price, stop=None, reduce_only=False):
        return dict(market_index=self.market["index"], client_order_index=self._idx(),
                    base_amount=base_amount, avg_execution_price=price,
                    is_ask=(side == "SHORT"), reduce_only=reduce_only,
                    trigger_price=stop or 0)

    def market_order(self, side, base_amount, price, reduce_only=False):
        """side: LONG (buy) / SHORT (sell). base_amount must already be in size decimals."""
        p = self._payload(side, base_amount, price, reduce_only=reduce_only)
        print(f"[{'DRY' if self.dry_run else 'SEND'}] market {side} {p}")
        if self.dry_run:
            return {"dry_run": True, **p}
        tx, resp, err = self.signer.create_market_order(
            p["market_index"], p["client_order_index"], p["base_amount"],
            p["avg_execution_price"], p["is_ask"], reduce_only=reduce_only)
        if err:
            raise RuntimeError(f"market order failed: {err}")
        return resp

    def stop_order(self, side, base_amount, stop_price):
        """Exchange-side disaster stop. Lives on the venue, survives this process dying."""
        p = self._payload(side, base_amount, stop_price, stop=stop_price, reduce_only=True)
        print(f"[{'DRY' if self.dry_run else 'SEND'}] stop {side} {p}")
        if self.dry_run:
            return {"dry_run": True, **p}
        tx, resp, err = self.signer.create_order(
            p["market_index"], p["client_order_index"], p["base_amount"], p["avg_execution_price"],
            p["is_ask"], SignerClient.ORDER_TYPE_STOP_LOSS,
            SignerClient.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
            reduce_only=True, trigger_price=stop_price)
        if err:
            raise RuntimeError(f"stop order failed: {err}")
        return resp

    def cancel(self, order_index):
        print(f"[{'DRY' if self.dry_run else 'SEND'}] cancel {order_index}")
        if self.dry_run:
            return {"dry_run": True, "order_index": order_index}
        tx, resp, err = self.signer.cancel_order(self.market["index"], order_index)
        if err:
            raise RuntimeError(f"cancel failed: {err}")
        return resp

    async def positions(self):
        """Authenticated read: what the venue says you hold. Use it on every boot."""
        cfg = lighter.Configuration(host=self.url)
        client = lighter.ApiClient(cfg)
        api = lighter.AccountApi(client)
        acct = await api.account(by="index", value=str(self.account_index))
        return getattr(acct, "positions", []) or []

    def reconcile(self, expected_side, positions):
        live = [p for p in positions if float(getattr(p, "position", 0) or 0) != 0]
        have = None
        if live:
            pos = float(getattr(live[0], "position", 0))
            have = "LONG" if pos > 0 else "SHORT"
        if have == expected_side:
            return f"in sync ({have or 'flat'})"
        if have and not expected_side:
            return f"venue holds {have}, bot thought flat -> adopt and mark carried"
        if not have and expected_side:
            return f"bot wanted {expected_side}, venue flat -> treat as flat"
        return f"MISMATCH {have} vs {expected_side} -> stop before trading"


async def probe(live=False):
    b = None
    for testnet in (True, False):
        cand = LighterBroker(testnet=testnet, dry_run=True)
        try:
            m = await cand.connect()
        except RuntimeError as e:
            print(f"{'TESTNET' if testnet else 'MAINNET'}: {e}")
            continue
        b = cand
        print(f"connected: {b.url} (chain {b.chain_id}, {'testnet' if testnet else 'MAINNET'})")
        show = {k: m[k] for k in ("index", "symbol", "tick", "size_decimals", "min_base") if k in m}
        print("market:", show)
        print("fields with values:", sorted([k for k in m if m[k] is not None])[:26])
        break
    if b is None:
        print("could not find the market on either network")
        return
    if live:
        idx = os.getenv("LIGHTER_ACCOUNT_INDEX")
        key = os.getenv("LIGHTER_API_PRIVATE_KEY")
        ki = int(os.getenv("LIGHTER_API_KEY_INDEX", "0"))
        if not (idx and key):
            print("no LIGHTER_ACCOUNT_INDEX / LIGHTER_API_PRIVATE_KEY set — skipping signed read")
            return
        b.sign_in(int(idx), ki, key)
        pos = await b.positions()
        print("positions:", pos)
    print("dry-run order payload check:")
    b.market_order("LONG", 1000, 1000)
    b.stop_order("LONG", 1000, 990)


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--probe", action="store_true", help="public testnet check, no keys")
    ap.add_argument("--probe-live", action="store_true", help="public + signed read")
    a = ap.parse_args()
    if a.probe or a.probe_live:
        asyncio.run(probe(live=a.probe_live))
    else:
        print(__doc__)
