#!/usr/bin/env python3
"""
Bybit account checker — read-only.
Usage:
  python3 bybit_check.py overview          # Portfolio summary (balances + positions + PnL)
  python3 bybit_check.py balance           # Wallet balance detail
  python3 bybit_check.py positions         # All open positions (linear + inverse + spot)
  python3 bybit_check.py orders            # Open/active orders across categories
  python3 bybit_check.py fees BTCUSDT      # Fee rate for a symbol
  python3 bybit_check.py transactions      # Recent transaction log (last 24h)
  python3 bybit_check.py earn              # Earn product balances (if any)
  python3 bybit_check.py p2p USDT ARS sell # P2P ad rates (sell USDT for ARS)
"""

import sys
import json
import os
import time
import hmac
import hashlib
from datetime import datetime, timezone

import requests as req

# ── Credentials ──────────────────────────────────────────────────────
API_KEY = os.environ.get("BYBIT_API_KEY", "4Ve53Row4nb7Wm7aJq")
API_SECRET = os.environ.get("BYBIT_API_SECRET", "kvAfwTBHuSWBOyY2fF9K0b4hINHyHLGRCpmI")
BASE_URL = "https://api.bybit.com"

from pybit.unified_trading import HTTP

session = HTTP(testnet=False, api_key=API_KEY, api_secret=API_SECRET, recv_window=10000)


def signed_get(endpoint, query=""):
    """Raw signed GET for endpoints pybit doesn't cover (like earn)."""
    ts = str(int(time.time() * 1000))
    recv = "10000"
    sig_payload = ts + API_KEY + recv + query
    signature = hmac.new(API_SECRET.encode(), sig_payload.encode(), hashlib.sha256).hexdigest()
    headers = {
        "X-BAPI-API-KEY": API_KEY,
        "X-BAPI-TIMESTAMP": ts,
        "X-BAPI-RECV-WINDOW": recv,
        "X-BAPI-SIGN": signature,
    }
    url = BASE_URL + endpoint
    if query:
        url += "?" + query
    return req.get(url, headers=headers, timeout=10).json()


def signed_post(endpoint, body):
    """Raw signed POST for endpoints pybit doesn't cover (like P2P)."""
    ts = str(int(time.time() * 1000))
    recv = "10000"
    body_str = json.dumps(body)
    sig_payload = ts + API_KEY + recv + body_str
    signature = hmac.new(API_SECRET.encode(), sig_payload.encode(), hashlib.sha256).hexdigest()
    headers = {
        "X-BAPI-API-KEY": API_KEY,
        "X-BAPI-TIMESTAMP": ts,
        "X-BAPI-RECV-WINDOW": recv,
        "X-BAPI-SIGN": signature,
        "Content-Type": "application/json",
    }
    return req.post(BASE_URL + endpoint, headers=headers, data=body_str, timeout=10).json()


def fmt_usd(val):
    """Format a string number as USD."""
    try:
        return f"${float(val):,.2f}"
    except (ValueError, TypeError):
        return val or "—"


def fmt_pct(val):
    """Format as percentage."""
    try:
        return f"{float(val)*100:.2f}%"
    except (ValueError, TypeError):
        return val or "—"


def check_response(resp):
    """Extract result from Bybit response, raise on error."""
    if isinstance(resp, dict):
        if resp.get("retCode", 0) != 0:
            print(f"ERROR: {resp.get('retMsg', 'Unknown error')} (code {resp['retCode']})")
            sys.exit(1)
        return resp.get("result", {})
    return resp


# ── Commands ─────────────────────────────────────────────────────────

def cmd_overview():
    """Portfolio overview: equity, balances, open positions."""
    result = check_response(session.get_wallet_balance(accountType="UNIFIED"))
    accounts = result.get("list", [])

    if not accounts:
        print("No unified account data found.")
        return

    acct = accounts[0]
    print("=" * 60)
    print("  BYBIT PORTFOLIO OVERVIEW")
    print("=" * 60)
    print(f"  Total Equity:          {fmt_usd(acct.get('totalEquity'))}")
    print(f"  Wallet Balance:        {fmt_usd(acct.get('totalWalletBalance'))}")
    print(f"  Margin Balance:        {fmt_usd(acct.get('totalMarginBalance'))}")
    print(f"  Available Balance:     {fmt_usd(acct.get('totalAvailableBalance'))}")
    print(f"  Unrealized PnL:        {fmt_usd(acct.get('totalPerpUPL'))}")
    print(f"  Account IM Rate:       {fmt_pct(acct.get('accountIMRate'))}")
    print(f"  Account MM Rate:       {fmt_pct(acct.get('accountMMRate'))}")

    coins = acct.get("coin", [])
    if coins:
        print()
        print("  COIN BREAKDOWN:")
        print("  " + "-" * 56)
        print(f"  {'Coin':<8} {'Equity':>12} {'Wallet':>12} {'Locked':>10} {'USD Val':>12}")
        print("  " + "-" * 56)
        for c in coins:
            eq = c.get("equity", "0")
            wb = c.get("walletBalance", "0")
            lk = c.get("locked", "0")
            uv = c.get("usdValue", "0")
            if float(eq or 0) == 0 and float(wb or 0) == 0:
                continue
            print(f"  {c['coin']:<8} {float(eq):>12.6f} {float(wb):>12.6f} {float(lk):>10.6f} {fmt_usd(uv):>12}")

    # Positions
    has_positions = False
    for cat in ["linear", "inverse"]:
        try:
            pos_resp = session.get_positions(category=cat, settleCoin="USDT" if cat == "linear" else None)
            pos_result = check_response(pos_resp)
            positions = [p for p in pos_result.get("list", []) if float(p.get("size", 0)) > 0]
            if positions:
                has_positions = True
                print()
                print(f"  OPEN POSITIONS ({cat.upper()}):")
                print("  " + "-" * 56)
                for p in positions:
                    side = p.get("side", "?")
                    size = p.get("size", "0")
                    symbol = p.get("symbol", "?")
                    pnl = p.get("unrealisedPnl", "0")
                    entry = p.get("avgPrice", "?")
                    lev = p.get("leverage", "?")
                    liq = p.get("liqPrice", "?")
                    pnl_val = float(pnl)
                    pnl_str = f"+{fmt_usd(pnl)}" if pnl_val >= 0 else fmt_usd(pnl)
                    print(f"  {symbol}  {side} {size}  @{entry}  {lev}x  Liq:{liq}  PnL:{pnl_str}")
        except Exception:
            pass

    if not has_positions:
        print()
        print("  No open positions.")

    # Spot positions
    try:
        spot_resp = session.get_positions(category="spot")
        spot_result = check_response(spot_resp)
        spot_pos = [p for p in spot_result.get("list", []) if float(p.get("size", 0)) > 0]
        if spot_pos:
            print()
            print("  SPOT HOLDINGS:")
            print("  " + "-" * 56)
            for p in spot_pos:
                symbol = p.get("symbol", "?")
                size = p.get("size", "0")
                side = p.get("side", "?")
                avg = p.get("avgPrice", "?")
                pnl = p.get("unrealisedPnl", "0")
                print(f"  {symbol}  {side} {size}  avg@{avg}  PnL:{fmt_usd(pnl)}")
    except Exception:
        pass

    print("=" * 60)


def cmd_balance():
    """Detailed wallet balance."""
    result = check_response(session.get_wallet_balance(accountType="UNIFIED"))
    accounts = result.get("list", [])

    for acct in accounts:
        print(f"Account: {acct['accountType']}")
        print(f"  Total Equity:      {fmt_usd(acct.get('totalEquity'))}")
        print(f"  Wallet Balance:    {fmt_usd(acct.get('totalWalletBalance'))}")
        print(f"  Margin Balance:    {fmt_usd(acct.get('totalMarginBalance'))}")
        print(f"  Available Balance: {fmt_usd(acct.get('totalAvailableBalance'))}")
        print(f"  Unrealized PnL:    {fmt_usd(acct.get('totalPerpUPL'))}")
        print()

        for c in acct.get("coin", []):
            eq = float(c.get("equity", 0))
            wb = float(c.get("walletBalance", 0))
            if eq == 0 and wb == 0:
                continue
            print(f"  {c['coin']}:")
            print(f"    Equity:            {c.get('equity', '0')}")
            print(f"    Wallet Balance:    {c.get('walletBalance', '0')}")
            print(f"    Available:         {float(c.get('equity',0)) - float(c.get('locked',0)):.6f}")
            print(f"    Locked:            {c.get('locked', '0')}")
            print(f"    Borrowed:          {c.get('borrowAmount', '0')}")
            print(f"    Unrealized PnL:    {c.get('unrealisedPnl', '0')}")
            print(f"    Cum. Realized PnL: {c.get('cumRealisedPnl', '0')}")
            print(f"    USD Value:         {fmt_usd(c.get('usdValue'))}")
            print(f"    Collateral:        {'Yes' if c.get('marginCollateral') else 'No'}")
            print()


def cmd_positions():
    """All open positions across categories."""
    found = False
    for cat in ["linear", "inverse", "option"]:
        try:
            if cat == "linear":
                resp = session.get_positions(category=cat, settleCoin="USDT")
            else:
                resp = session.get_positions(category=cat)
            result = check_response(resp)
            positions = [p for p in result.get("list", []) if float(p.get("size", 0)) > 0]
            if positions:
                found = True
                print(f"\n{'='*60}")
                print(f"  {cat.upper()} POSITIONS")
                print(f"{'='*60}")
                for p in positions:
                    side = p.get("side", "?")
                    size = p.get("size", "0")
                    symbol = p.get("symbol", "?")
                    pnl = float(p.get("unrealisedPnl", 0))
                    entry = p.get("avgPrice", "—")
                    lev = p.get("leverage", "—")
                    liq = p.get("liqPrice", "—")
                    pnl_str = f"+{fmt_usd(pnl)}" if pnl >= 0 else fmt_usd(pnl)
                    im = p.get("positionIM", "—")
                    mm = p.get("positionMM", "—")
                    be = p.get("breakEvenPrice", "—")
                    print(f"  {symbol}")
                    print(f"    Side:         {side}")
                    print(f"    Size:         {size}")
                    print(f"    Entry Price:  {entry}")
                    print(f"    Break Even:   {be}")
                    print(f"    Leverage:     {lev}x")
                    print(f"    Liq Price:    {liq}")
                    print(f"    Unrealized:   {pnl_str}")
                    print(f"    IM / MM:      {im} / {mm}")
                    print()
        except Exception as e:
            pass

    # Spot
    try:
        resp = session.get_positions(category="spot")
        result = check_response(resp)
        positions = [p for p in result.get("list", []) if float(p.get("size", 0)) > 0]
        if positions:
            found = True
            print(f"\n{'='*60}")
            print(f"  SPOT POSITIONS")
            print(f"{'='*60}")
            for p in positions:
                print(f"  {p.get('symbol','?')}  {p.get('side','')} {p.get('size','0')}  avg@{p.get('avgPrice','—')}  PnL:{fmt_usd(p.get('unrealisedPnl','0'))}")
    except Exception:
        pass

    if not found:
        print("No open positions in any category.")


def cmd_orders():
    """Open/active orders."""
    found = False
    for cat in ["linear", "spot", "inverse", "option"]:
        try:
            resp = session.get_open_orders(category=cat)
            result = check_response(resp)
            orders = result.get("list", [])
            if orders:
                found = True
                print(f"\n{'='*60}")
                print(f"  {cat.upper()} OPEN ORDERS ({len(orders)})")
                print(f"{'='*60}")
                for o in orders:
                    oid = o.get("orderId", "?")[:12] + "..."
                    sym = o.get("symbol", "?")
                    side = o.get("side", "?")
                    otype = o.get("orderType", "?")
                    qty = o.get("qty", "?")
                    price = o.get("price", "—")
                    status = o.get("orderStatus", "?")
                    tp = o.get("takeProfit", "")
                    sl = o.get("stopLoss", "")
                    extra = ""
                    if tp:
                        extra += f" TP:{tp}"
                    if sl:
                        extra += f" SL:{sl}"
                    print(f"  {sym}  {side} {otype} {qty} @{price}  [{status}]{extra}")
                    print(f"    ID: {oid}")
        except Exception:
            pass

    if not found:
        print("No open orders.")


def cmd_fees(symbol=None):
    """Fee rates."""
    cats = ["spot", "linear", "inverse"]
    for cat in cats:
        try:
            kwargs = {"category": cat}
            if symbol:
                kwargs["symbol"] = symbol.upper()
            resp = session.get_fee_rate(**kwargs)
            result = check_response(resp)
            fees = result.get("list", [])
            if fees:
                print(f"{cat.upper()} fees:")
                for f in fees:
                    sym = f.get("symbol", symbol or "ALL")
                    maker = f.get("makerFeeRate", "?")
                    taker = f.get("takerFeeRate", "?")
                    print(f"  {sym}: Maker {fmt_pct(maker)} / Taker {fmt_pct(taker)}")
        except Exception:
            pass


def cmd_transactions():
    """Recent transaction log."""
    resp = session.get_transaction_log(category="linear", limit=20)
    result = check_response(resp)
    txns = result.get("list", [])

    if not txns:
        print("No recent transactions.")
        return

    print(f"{'='*60}")
    print(f"  RECENT TRANSACTIONS (last {len(txns)})")
    print(f"{'='*60}")
    for t in txns:
        ts = t.get("transactionTime", "")
        if ts:
            try:
                dt = datetime.fromtimestamp(int(ts) / 1000, tz=timezone.utc)
                ts = dt.strftime("%Y-%m-%d %H:%M UTC")
            except Exception:
                pass
        typ = t.get("type", "?")
        sym = t.get("symbol", "")
        side = t.get("side", "")
        change = t.get("change", "0")
        fee = t.get("fee", "0")
        bal = t.get("cashBalance", "")
        change_val = float(change)
        sign = "+" if change_val >= 0 else ""
        print(f"  {ts}  {typ:<12} {sym:<12} {side:<5} {sign}{change}  fee:{fee}  bal:{fmt_usd(bal)}")


def cmd_earn():
    """Earn positions (FlexibleSaving, OnChain)."""
    print(f"{'='*60}")
    print("  BYBIT EARN POSITIONS")
    print(f"{'='*60}")

    found = False
    for cat in ["FlexibleSaving", "OnChain"]:
        try:
            resp = signed_get("/v5/earn/position", f"category={cat}")
            if resp.get("retCode", 0) != 0:
                continue
            positions = resp.get("result", {}).get("list", [])
            if not positions:
                continue
            found = True
            print(f"\n  {cat}:")
            print("  " + "-" * 56)
            for p in positions:
                coin = p.get("coin", "?")
                amount = p.get("amount", "0")
                effective = p.get("effectiveAmount", "0")
                pnl = p.get("totalPnl", "0")
                claimable = p.get("claimableYield", "0")
                yesterday = p.get("yesterdayYield", "0")
                yesterday_date = p.get("yesterdayYieldDate", "")
                print(f"  {coin}:")
                print(f"    Staked:          {amount}")
                print(f"    Effective:       {effective}")
                print(f"    Total PnL:       {pnl}")
                print(f"    Claimable Yield: {claimable}")
                print(f"    Yesterday Yield: {yesterday} ({yesterday_date})")
                print()
        except Exception:
            pass

    if not found:
        print("\n  No earn positions found.")

    # Also show available high-APR FlexibleSaving options
    try:
        resp = signed_get("/v5/earn/product", "category=FlexibleSaving")
        if resp.get("retCode", 0) == 0:
            products = resp.get("result", {}).get("list", [])
            # Sort by APR descending, show top 5
            available = [p for p in products if p.get("status") == "Available" and p.get("estimateApr")]
            available.sort(key=lambda x: float(x["estimateApr"].replace("%", "")), reverse=True)
            if available:
                print(f"\n  TOP AVAILABLE FLEXIBLE SAVINGS (by APR):")
                print("  " + "-" * 56)
                for p in available[:5]:
                    apr = p.get("estimateApr", "?")
                    coin = p.get("coin", "?")
                    min_stake = p.get("minStakeAmount", "?")
                    remaining = p.get("remainingPoolAmount", "")
                    print(f"  {coin:<8} {apr:>8}  min:{min_stake}  pool:{remaining[:15]}")
    except Exception:
        pass

    print(f"{'='*60}")


def cmd_p2p(token="USDT", currency="ARS", side="sell"):
    """P2P ad rates. Usage: p2p USDT ARS sell|buy"""
    side_val = "1" if side.lower() == "sell" else "0"  # 1=sell crypto, 0=buy crypto
    body = {
        "tokenId": token.upper(),
        "currencyId": currency.upper(),
        "side": side_val,
        "page": "1",
        "size": "20",
    }
    resp = signed_post("/v5/p2p/item/online", body)

    if resp.get("ret_code", -1) != 0:
        print(f"ERROR: {resp.get('ret_msg', 'Unknown error')}")
        return

    items = resp.get("result", {}).get("items", [])
    total = resp.get("result", {}).get("count", 0)

    # Filter to online sellers with quantity
    active = [i for i in items if i.get("isOnline") and float(i.get("quantity", 0)) > 0]

    # Sort: sell side = highest price best (you get more fiat); buy side = lowest price best
    if side.lower() == "sell":
        active.sort(key=lambda x: float(x["price"]), reverse=True)
    else:
        active.sort(key=lambda x: float(x["price"]))

    # Fallback: if no online, show top by price anyway
    show = active if len(active) >= 3 else sorted(items, key=lambda x: float(x["price"]), reverse=(side.lower() == "sell"))

    direction = f"SELL {token} -> {currency}" if side.lower() == "sell" else f"BUY {token} with {currency}"
    print(f"{'='*60}")
    print(f"  P2P {direction} (Bybit)")
    print(f"  Total ads: {total}, Online: {len(active)}")
    print(f"{'='*60}")
    print()

    if not show:
        print("  No ads found.")
        print(f"{'='*60}")
        return

    for i, ad in enumerate(show[:5]):
        price = float(ad["price"])
        name = ad["nickName"]
        qty = ad["quantity"]
        min_amt = ad["minAmount"]
        max_amt = ad["maxAmount"]
        orders = ad.get("recentOrderNum", 0)
        rate = ad.get("recentExecuteRate", 0)
        online = "ONLINE" if ad.get("isOnline") else "offline"
        print(f"  #{i+1}: {price:,.2f} {currency}/{token}  [{online}]")
        print(f"      Seller: {name}")
        print(f"      Available: {qty} {token}")
        print(f"      Limits: {min_amt} - {max_amt} {currency}")
        print(f"      Completion: {rate}% ({orders} recent orders)")
        print()

    print(f"{'='*60}")


# ── Main ─────────────────────────────────────────────────────────────

COMMANDS = {
    "overview": cmd_overview,
    "balance": cmd_balance,
    "positions": cmd_positions,
    "orders": cmd_orders,
    "fees": cmd_fees,
    "transactions": cmd_transactions,
    "earn": cmd_earn,
    "p2p": cmd_p2p,
}

if __name__ == "__main__":
    if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
        print(__doc__)
        sys.exit(0)

    cmd = sys.argv[1]
    args = sys.argv[2:]

    if cmd == "fees":
        cmd_fees(args[0] if args else None)
    elif cmd == "p2p":
        cmd_p2p(
            token=args[0] if len(args) > 0 else "USDT",
            currency=args[1] if len(args) > 1 else "ARS",
            side=args[2] if len(args) > 2 else "sell",
        )
    else:
        COMMANDS[cmd]()
