"""
V12 — Executor: taker orders para momentum.

Live mode:
  - GTC aggressive limit (al ask + AGGRESSIVE_BUMP) — V12: reducido de +0.02 a +0.01
  - Verificación de fill vía CLOB get_trades API (V12: reemplaza positions API rota)
  - Balance check antes de operar
  - Cancel de órdenes pendientes si no se llenan

V12 fix crítico: verify_fill usa client.get_trades(asset_id) en vez de
data-api.polymarket.com/positions. La positions API tiene latencia de minutos
y nunca muestra fills recientes, causando que TODOS los trades caigan al path
FILL TRUST (que creaba trades fantasma). get_trades devuelve fills CONFIRMED
con transaction_hash — es la fuente de verdad on-chain.
"""
import os, time
from .config import Config, HOST, CHAIN_ID, MIN_SHARES, PAPER_SLIPPAGE, snap_price

from py_clob_client_v2 import ClobClient
from py_clob_client_v2.clob_types import (
    ApiCreds, OrderArgsV2, TradeParams,
    BalanceAllowanceParams, AssetType, OrderPayload,
)


class Executor:
    def __init__(self, cfg: Config):
        self.cfg    = cfg
        self.client = None
        self._balance_cache = 0.0
        self._balance_ts    = 0.0

        if cfg.live:
            pk       = os.getenv("PK", "").strip().replace("0x", "")
            proxy    = os.getenv("POLY_PROXY", "").strip()
            sig_type = int(os.getenv("POLY_SIG_TYPE", "2"))
            creds = ApiCreds(
                api_key=os.getenv("POLY_API_KEY"),
                api_secret=os.getenv("POLY_API_SECRET"),
                api_passphrase=os.getenv("POLY_API_PASSPHRASE"),
            )
            self.client = ClobClient(
                host=HOST, chain_id=CHAIN_ID, key=pk,
                creds=creds, signature_type=sig_type, funder=proxy,
            )
            bal = self.get_balance()
            print(f"[*] Executor TAKER LIVE [{cfg.asset.upper()}] | "
                  f"proxy={proxy} | balance=${bal:.4f}")
        else:
            print(f"[*] Executor TAKER PAPER [{cfg.asset.upper()}]")

    # -- Balance --------------------------------------------------------------

    def get_balance(self, use_cache: bool = False) -> float:
        if not self.cfg.live or not self.client:
            return 9999.0
        if use_cache and (time.time() - self._balance_ts) < 30:
            return self._balance_cache
        try:
            r = self.client.get_balance_allowance(
                BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
            )
            bal = int(r.get("balance", 0)) / 1e6
        except Exception:
            bal = self._balance_cache
        self._balance_cache = bal
        self._balance_ts = time.time()
        return bal

    # -- Taker order ----------------------------------------------------------

    def place_taker_order(self, token_id: str, price: float, size: float,
                          label: str) -> tuple[bool, str, float]:
        """
        Place a taker BUY order at the ask price.
        Returns (success, order_id, fill_price).
        Paper: simulates fill at ask + slippage.
        Live: GTC aggressive limit, then verify fill via positions.
        """
        price = snap_price(price)
        size  = max(round(size, 2), MIN_SHARES)

        if not self.cfg.live or not self.client:
            fill_price = snap_price(price + PAPER_SLIPPAGE)
            oid = f"paper_{int(time.time())}_{label}"
            print(f"  [PAPER TAKE] {label} @ {fill_price:.2f} (ask={price:.2f}) "
                  f"x {size:.1f} sh = ${fill_price * size:.2f}")
            return True, oid, fill_price, "matched"

        # Live: check balance first
        cost = price * size
        bal = self.get_balance()
        if bal < cost * 1.05:  # 5% margen
            print(f"  [TAKE SKIP] {label}: balance ${bal:.2f} < "
                  f"cost ${cost:.2f} + margen")
            return False, "", 0.0

        # V12: bump configurable (era 0.02 hardcoded en V11)
        aggressive_price = snap_price(price + self.cfg.aggressive_bump)

        try:
            args = OrderArgsV2(
                token_id=token_id,
                price=aggressive_price,
                size=size,
                side="BUY",
            )
            resp = self.client.create_and_post_order(
                order_args=args,
                order_type="GTC",
            )

            # API sometimes returns a raw string instead of dict
            if isinstance(resp, str):
                resp = {"success": True, "orderID": resp}

            success = resp.get("success", False)
            oid     = resp.get("orderID") or resp.get("order_id") or ""
            err_msg = resp.get("errorMsg") or resp.get("error_msg") or ""
            status  = (resp.get("status") or "").lower()

            if success or status in ("matched", "filled"):
                # Matched = llenada inmediatamente por el CLOB
                taking = float(resp.get("takingAmount") or 0)
                actual_size = taking if taking > 0 else size
                print(f"  [TAKE FILLED] {label} @ {aggressive_price:.2f} "
                      f"x {actual_size:.1f} sh | oid={str(oid)[:14]}...")
                return True, str(oid), aggressive_price, "matched"
            elif status in ("live", "open"):
                # Orden aceptada pero NO llenada aún — necesita verify_fill
                print(f"  [TAKE PENDING] {label} @ {aggressive_price:.2f} "
                      f"x {size:.1f} sh | status={status} | "
                      f"oid={str(oid)[:14]}...")
                return True, str(oid), aggressive_price, "pending"
            else:
                print(f"  [TAKE FAIL] {label}: {err_msg or status or resp}")
                return False, "", 0.0, ""

        except Exception as e:
            print(f"  [TAKE ERR] {label}: {str(e)[:120]}")
            return False, "", 0.0, ""

    # -- Fill verification ----------------------------------------------------

    def verify_fill(self, session, token_id: str,
                    order_ts: int = 0) -> dict | None:
        """Verify fill via CLOB get_trades API.

        Busca trades CONFIRMED donde nuestra address aparece como maker
        en maker_orders para el asset_id dado, desde order_ts en adelante.

        Returns {"size": float, "avg_price": float, "tx_hashes": list} or None.

        V12: Reemplaza la positions API (data-api.polymarket.com/positions)
        que tiene latencia de minutos y nunca mostraba fills recientes,
        causando trades fantasma via FILL TRUST.
        """
        if not self.cfg.live or not self.client:
            return None
        proxy = os.getenv("POLY_PROXY", "").strip().lower()
        if not proxy:
            return None

        # Buscar desde 60s antes de la orden para margen
        after_ts = (order_ts - 60) if order_ts > 0 else (int(time.time()) - 120)

        # NOTA: NO filtrar por asset_id en la API. Cuando alguien compra NO
        # y nosotros somos maker con YES, el top-level asset_id es el NO token.
        # Filtrar localmente por nuestro address + asset_id en maker_orders.
        try:
            trades = self.client.get_trades(
                TradeParams(after=after_ts),
                only_first_page=True,
            )
        except Exception as e:
            print(f"  [verify] get_trades error: {e}")
            return None

        # Buscar fills donde somos maker o taker para este token
        total_size = 0.0
        weighted_price = 0.0
        tx_hashes = []

        for t in trades:
            status = (t.get("status") or "").upper()
            if status != "CONFIRMED":
                continue

            # Caso 1: somos taker y el asset_id coincide
            if (t.get("maker_address", "").lower() == proxy
                    and t.get("trader_side") == "TAKER"
                    and str(t.get("asset_id", "")) == token_id):
                sz = float(t.get("size", 0))
                px = float(t.get("price", 0))
                if sz > 0 and px > 0:
                    total_size += sz
                    weighted_price += sz * px
                    tx_h = t.get("transaction_hash", "")
                    if tx_h:
                        tx_hashes.append(tx_h)

            # Caso 2: somos maker en maker_orders con nuestro asset_id
            for mo in t.get("maker_orders", []):
                if mo.get("maker_address", "").lower() != proxy:
                    continue
                mo_asset = str(mo.get("asset_id", ""))
                if mo_asset != token_id:
                    continue
                sz = float(mo.get("matched_amount", 0))
                px = float(mo.get("price", 0))
                if sz > 0 and px > 0:
                    total_size += sz
                    weighted_price += sz * px
                    tx_h = t.get("transaction_hash", "")
                    if tx_h and tx_h not in tx_hashes:
                        tx_hashes.append(tx_h)

        if total_size <= 0:
            return None

        avg_price = round(weighted_price / total_size, 6) if total_size > 0 else 0
        return {
            "size": round(total_size, 4),
            "avg_price": avg_price,
            "tx_hashes": tx_hashes,
        }

    # -- Cancel ---------------------------------------------------------------

    def cancel_order(self, order_id: str) -> bool:
        if not self.cfg.live or not self.client:
            return True
        try:
            resp = self.client.cancel_order(OrderPayload(orderID=order_id))
            if isinstance(resp, dict):
                cancelled = resp.get("canceled", [])
                not_cancelled = resp.get("not_canceled", {})
                if order_id in (cancelled or []):
                    return True
                if not_cancelled:
                    return "not found" in str(not_cancelled).lower()
            return True
        except Exception as e:
            err = str(e).lower()
            if "not found" in err or "already" in err:
                return True
            print(f"  [CANCEL ERR] {order_id[:14]}: {str(e)[:60]}")
            return False

    def cancel_all(self) -> int:
        if not self.cfg.live or not self.client:
            return 0
        try:
            resp = self.client.cancel_all()
            cancelled = resp.get("canceled", []) if isinstance(resp, dict) else []
            n = len(cancelled) if isinstance(cancelled, list) else 0
            if n:
                print(f"  [CANCEL ALL] {n} ordenes canceladas")
            return n
        except Exception as e:
            print(f"  [CANCEL ALL ERR] {str(e)[:60]}")
            return 0
