#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════════════════════
  convert_dust_to_usdc.py — Convertit en masse tous les petits assets PROD vers USDC
═══════════════════════════════════════════════════════════════════════════════

Utilise l'API Binance Convert (/sapi/v1/convert) qui :
  - accepte n'importe quelle taille (pas de minNotional carnet)
  - convertit DIRECT asset → USDC sans passer par BNB
  - applique un quote fixe (zéro slippage)
  - aucune commission visible (déjà incluse dans le quote)

Usage :
    python3 convert_dust_to_usdc.py            # mode preview (DRY-RUN)
    python3 convert_dust_to_usdc.py --execute  # exécute vraiment
    python3 convert_dust_to_usdc.py --execute --min-usdc 0.5
                                               # seuil personnalisé (défaut 1$)
"""
import sys
import time
import hmac
import json
import hashlib
import argparse
import requests
from urllib.parse import urlencode

# ── Charger les clés PROD (jamais testnet pour ce script) ──
sys.path.insert(0, '/home/ubuntu/crypto_trading_prod')
try:
    from config import BINANCE_API_KEY, BINANCE_API_SECRET  # type: ignore
except ImportError:
    print("❌ Impossible de charger /home/ubuntu/crypto_trading_prod/config.py")
    sys.exit(1)

API_URL = "https://api.binance.com"
HEADERS = {"X-MBX-APIKEY": BINANCE_API_KEY}
SESSION = requests.Session()
SESSION.headers.update(HEADERS)

# Couleurs terminal
def C(t, color):
    codes = {'g': 32, 'r': 31, 'y': 33, 'b': 34, 'c': 36, 'gray': 90, 'bold': 1}
    return f"\033[{codes.get(color, 0)}m{t}\033[0m"


def _sign(params: dict) -> str:
    qs = urlencode(params)
    return hmac.new(BINANCE_API_SECRET.encode(), qs.encode(), hashlib.sha256).hexdigest()


def signed(method: str, path: str, params: dict | None = None) -> dict | None:
    if params is None:
        params = {}
    params['timestamp'] = int(time.time() * 1000)
    params['recvWindow'] = 10000
    params['signature'] = _sign(params)
    url = f"{API_URL}{path}"
    try:
        if method == "POST":
            r = SESSION.post(url, params=params, timeout=15)
        else:
            r = SESSION.get(url, params=params, timeout=15)
        return r.json()
    except Exception as e:
        return {'error': str(e)}


def get_prices() -> dict[str, float]:
    """Récupère le last price de toutes les paires en une requête."""
    try:
        r = SESSION.get(f"{API_URL}/api/v3/ticker/price", timeout=10)
        return {p['symbol']: float(p['price']) for p in r.json()}
    except Exception as e:
        print(C(f"❌ Erreur récupération prix : {e}", 'r'))
        sys.exit(1)


def list_balances() -> list[dict]:
    """Liste tous les balances non nuls + valeur USDC estimée."""
    acc = signed("GET", "/api/v3/account")
    if not acc or 'balances' not in acc:
        print(C(f"❌ Erreur récupération wallet : {acc}", 'r'))
        sys.exit(1)
    prices = get_prices()
    result = []
    for b in acc['balances']:
        free = float(b['free'])
        locked = float(b['locked'])
        total = free + locked
        if total <= 0:
            continue
        asset = b['asset']
        if asset in ('USDC',):
            usdc_value = total
        elif asset == 'USDT':
            usdc_value = total * prices.get('USDCUSDT', 1.0)  # ~1
        else:
            # Essai paire directe USDC, sinon USDT, sinon BTC
            p = prices.get(f"{asset}USDC") or prices.get(f"{asset}USDT")
            if p:
                usdc_value = total * p
            else:
                btc = prices.get(f"{asset}BTC")
                btc_usdc = prices.get('BTCUSDC')
                usdc_value = (total * btc * btc_usdc) if (btc and btc_usdc) else 0
        result.append({
            'asset': asset, 'free': free, 'locked': locked,
            'total': total, 'usdc_value': round(usdc_value, 4),
        })
    return sorted(result, key=lambda x: -x['usdc_value'])


def get_quote(asset: str, amount: float) -> dict | None:
    """Demande un quote Convert asset→USDC."""
    return signed("POST", "/sapi/v1/convert/getQuote", {
        'fromAsset': asset,
        'toAsset': 'USDC',
        'fromAmount': f"{amount:.8f}".rstrip('0').rstrip('.'),
        'walletType': 'SPOT',
    })


def accept_quote(quote_id: str) -> dict | None:
    return signed("POST", "/sapi/v1/convert/acceptQuote", {'quoteId': quote_id})


def get_dust_eligible() -> list[str]:
    """Liste des assets éligibles à la conversion dust→BNB (sub-$5 généralement)."""
    r = signed("POST", "/sapi/v1/asset/dust-btc", {})
    if not r or 'details' not in r:
        return []
    return [d['asset'] for d in r.get('details', [])]


def convert_dust_to_bnb(assets: list[str]) -> dict | None:
    """Convertit en lot des assets dust en BNB."""
    return signed("POST", "/sapi/v1/asset/dust", {'asset': ','.join(assets)})


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--execute', action='store_true', help='Exécuter (sinon DRY-RUN)')
    ap.add_argument('--min-usdc', type=float, default=1.0,
                    help='Valeur USDC minimale pour tenter (défaut 1.0)')
    ap.add_argument('--max-usdc', type=float, default=None,
                    help='Valeur USDC max par asset (défaut: pas de limite)')
    ap.add_argument('--exclude', nargs='+', default=['USDC', 'USDT', 'BNB'],
                    help='Assets à NE PAS convertir (défaut: USDC USDT BNB)')
    ap.add_argument('--include-bnb', action='store_true',
                    help='Forcer la conversion de BNB (sinon préservé pour fees)')
    ap.add_argument('--dust-to-bnb', action='store_true',
                    help='Convertir TOUS les sub-$1 invendables en BNB (1 seul call API)')
    args = ap.parse_args()

    if args.include_bnb and 'BNB' in args.exclude:
        args.exclude.remove('BNB')

    print(C("═" * 78, 'b'))
    print(C(f"  💱 BINANCE Convert → USDC  {'[DRY-RUN]' if not args.execute else '[EXECUTION RÉELLE]'}", 'bold'))
    print(C("═" * 78, 'b'))
    print(f"  Compte         : PROD ({BINANCE_API_KEY[:8]}...)")
    print(f"  Seuil min USDC : {args.min_usdc}$")
    print(f"  Exclus         : {', '.join(args.exclude)}")
    print()

    print(C("⏳ Récupération du wallet…", 'gray'))
    balances = list_balances()
    total_wallet = sum(b['usdc_value'] for b in balances)
    candidates = [
        b for b in balances
        if b['asset'] not in args.exclude
        and b['usdc_value'] >= args.min_usdc
        and (args.max_usdc is None or b['usdc_value'] <= args.max_usdc)
    ]

    print(C(f"\n📊 Wallet actuel ({len(balances)} assets, total ≈ {total_wallet:.2f} USDC) :", 'bold'))
    print(f"  {'Asset':<10} {'Solde':>16} {'≈ USDC':>10}  Action")
    print(C("  " + "─" * 76, 'gray'))
    for b in balances[:30]:
        action = ''
        if b['asset'] in args.exclude:
            action = C('— exclu', 'gray')
        elif b['usdc_value'] < args.min_usdc:
            action = C(f"— ignoré (< {args.min_usdc}$)", 'gray')
        elif args.max_usdc and b['usdc_value'] > args.max_usdc:
            action = C(f"— ignoré (> {args.max_usdc}$)", 'gray')
        else:
            action = C('→ CONVERT', 'g')
        print(f"  {b['asset']:<10} {b['total']:>16.6f} {b['usdc_value']:>10.2f}  {action}")
    if len(balances) > 30:
        print(f"  ... et {len(balances)-30} autres (visibles avec --min-usdc 0)")

    if not candidates:
        print(C("\n✓ Aucun asset à convertir via Convert API.", 'g'))
        # ── Mode Dust→BNB (pour micro-soldes < 1$) ─────────────────
        if not args.dust_to_bnb:
            print(C("\n💡 Astuce : pour les micro-soldes < 1$ invendables, relance avec :", 'c'))
            print(C("     python3 convert_dust_to_usdc.py --dust-to-bnb --execute", 'c'))
            print(C("   Cela les regroupe en BNB en 1 seul call (puis tu reconverts BNB→USDC).", 'gray'))
            return

        # Lister les éligibles via l'API Binance
        print(C("\n⏳ Récupération des dust éligibles…", 'gray'))
        eligible = get_dust_eligible()
        if not eligible:
            print(C("✓ Aucun dust éligible côté Binance (déjà nettoyé ou montants trop bas).", 'g'))
            return
        print(C(f"\n🧹 {len(eligible)} asset(s) dust éligibles → BNB :", 'c'))
        for a in eligible:
            bal = next((b for b in balances if b['asset'] == a), None)
            if bal:
                print(f"  • {a:<10} {bal['total']:>16.6f} ≈ {bal['usdc_value']:>6.3f} USDC")

        if not args.execute:
            print(C("\n⚠️  Mode DRY-RUN — relance avec --execute pour exécuter", 'y'))
            return

        print(C("\n⚠️  CONFIRMATION dust→BNB : tape 'OUI' pour exécuter.", 'y'))
        if input("  > ").strip() != 'OUI':
            print(C("Annulé.", 'gray'))
            return
        res = convert_dust_to_bnb(eligible)
        if res and 'totalTransfered' in res:
            print(C(f"\n✓ Converti : {res.get('totalTransfered', 0)} BNB "
                    f"(commission : {res.get('totalServiceCharge', 0)} BNB)", 'g'))
            print(C("→ Tu peux maintenant reconvertir BNB → USDC :", 'c'))
            print(C("    python3 convert_dust_to_usdc.py --execute --include-bnb", 'c'))
        else:
            print(C(f"❌ Erreur : {res}", 'r'))
        return

    total_to_convert = sum(b['usdc_value'] for b in candidates)
    print(C(f"\n🎯 {len(candidates)} asset(s) à convertir, total ≈ {total_to_convert:.2f} USDC", 'c'))

    if not args.execute:
        print(C("\n⚠️  Mode DRY-RUN — relance avec --execute pour exécuter", 'y'))
        return

    print(C("\n⚠️  CONFIRMATION : tape 'OUI' pour exécuter, autre chose pour annuler.", 'y'))
    if input("  > ").strip() != 'OUI':
        print(C("Annulé.", 'gray'))
        return

    # ── Exécution ─────────────────────────────────────────────────
    print(C("\n🚀 Conversion en cours…\n", 'bold'))
    ok, ko = 0, 0
    total_received = 0.0
    for b in candidates:
        asset = b['asset']
        amt = b['free']  # on convertit uniquement la part free
        if amt <= 0:
            print(f"  ⚠️ {asset}: 0 free (locked uniquement) — skip")
            continue

        # Quote
        q = get_quote(asset, amt)
        if not q or 'quoteId' not in q:
            err = (q or {}).get('msg', 'erreur inconnue')
            print(f"  {C('❌', 'r')} {asset:<8} {amt:>14.6f} → quote KO : {err}")
            ko += 1
            time.sleep(0.3)
            continue

        quote_id = q['quoteId']
        to_amt = float(q.get('toAmount', 0))
        ratio = float(q.get('ratio', 0))
        valid_ms = int(q.get('validTimestamp', 0)) - int(time.time()*1000)

        # Accept
        a = accept_quote(quote_id)
        if not a or a.get('orderStatus') not in ('SUCCESS', 'PROCESS'):
            err = (a or {}).get('msg', 'erreur acceptQuote')
            print(f"  {C('❌', 'r')} {asset:<8} {amt:>14.6f} → accept KO : {err}")
            ko += 1
            time.sleep(0.3)
            continue

        print(f"  {C('✓', 'g')} {asset:<8} {amt:>14.6f}  →  {to_amt:>10.4f} USDC  "
              f"(taux 1 {asset} = {ratio:.6f} USDC)")
        ok += 1
        total_received += to_amt
        time.sleep(0.4)  # courtoisie API

    print(C("\n" + "═" * 78, 'b'))
    print(C(f"  Résultat : {ok} OK · {ko} KO · {total_received:+.2f} USDC reçus", 'bold'))
    print(C("═" * 78, 'b'))


if __name__ == "__main__":
    main()
