#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de Vente Automatique Complète - VERSION AMÉLIORÉE
========================================================

Fonctionnalités :
- Vend TOUTES les positions (depuis positions.json + vérification Binance)
- Met à jour automatiquement positions.json
- Crée un cooldown global de 5 minutes
- Gère les erreurs et nettoie les positions fantômes
- Évite les interventions manuelles

Usage:
    python sell_all.py              # Vend tout avec confirmation
    python sell_all.py --force      # Vend tout sans confirmation
"""

import os
import sys
import json
import time
from datetime import datetime
from binance.client import Client
from binance.exceptions import BinanceAPIException

# Fix encodage Windows
if sys.platform == 'win32':
    import codecs
    sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
    sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')

# Configuration
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(SCRIPT_DIR)

# Couleurs
class Colors:
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    RESET = '\033[0m'
    BOLD = '\033[1m'


def load_config():
    """Charge la configuration"""
    try:
        # Import dynamique pour éviter les dépendances
        sys.path.insert(0, SCRIPT_DIR)
        import config
        return config
    except Exception as e:
        print(f"{Colors.RED}❌ Impossible de charger config.py: {e}{Colors.RESET}")
        sys.exit(1)


def get_binance_client(config):
    """Crée le client Binance"""
    try:
        client = Client(config.BINANCE_API_KEY, config.BINANCE_API_SECRET, testnet=config.TESTNET_MODE)
        return client
    except Exception as e:
        print(f"{Colors.RED}❌ Erreur connexion Binance: {e}{Colors.RESET}")
        sys.exit(1)


def load_positions():
    """Charge les positions depuis positions.json"""
    positions_file = os.path.join(SCRIPT_DIR, 'positions.json')
    if os.path.exists(positions_file):
        try:
            with open(positions_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            return {}
    return {}


def save_positions(positions):
    """Sauvegarde les positions dans positions.json"""
    positions_file = os.path.join(SCRIPT_DIR, 'positions.json')
    try:
        with open(positions_file, 'w', encoding='utf-8') as f:
            json.dump(positions, f, indent=2, ensure_ascii=False)
        return True
    except Exception as e:
        print(f"{Colors.RED}❌ Erreur sauvegarde positions.json: {e}{Colors.RESET}")
        return False


def get_binance_positions(client):
    """Récupère toutes les positions depuis Binance
    
    🔴 FIX 27/02: Utilise get_all_tickers() en 1 seul appel API
    au lieu de N appels individuels. Le testnet a ~432 assets,
    ce qui causait un timeout de 120s avec l'ancienne méthode.
    """
    positions = []
    try:
        account = client.get_account()
        
        # 🔴 FIX 27/02: Récupérer TOUS les prix en 1 seul appel (0.2s vs 100s+)
        all_tickers = client.get_all_tickers()
        price_map = {t['symbol']: float(t['price']) for t in all_tickers}
        
        # Assets à ignorer (stablecoins, fiat, assets testnet spéciaux)
        IGNORE_ASSETS = {'USDT', 'BUSD', 'USDC', 'DAI', 'USD', 'EUR', 'BRL', 'TRY', 
                         'IDR', 'ARS', 'UAH', 'ZAR', 'PLN', 'RON', 'CZK', 'MXN', 
                         'COP', 'JPY', 'TUSD', 'FDUSD', 'USDP', 'AEUR', 'EURI',
                         'BFUSD', 'USD1', 'USDE', 'RLUSD', 'XUSD', 'FRAX', 'U',
                         '这是测试币', '456', 'BNB', 'LDBNB', 'ETHW', 'BETH', 'WBTC', 'WBETH', 'BNSOL', '币安人生'}
        
        for balance in account['balances']:
            asset = balance['asset']
            if asset in IGNORE_ASSETS:
                continue
            
            free = float(balance['free'])
            locked = float(balance['locked'])
            total = free + locked
            
            if total > 0:
                symbol = f"{asset}USDT"
                price = price_map.get(symbol, 0)
                
                if price > 0:
                    value_usdt = total * price
                    
                    # Seuil minimum de 5 USDT pour être considéré comme position réelle
                    if value_usdt >= 5.0:
                        positions.append({
                            'symbol': symbol,
                            'quantity': total,
                            'price': price,
                            'value_usdt': value_usdt
                        })
    
    except Exception as e:
        print(f"{Colors.RED}❌ Erreur récupération positions Binance: {e}{Colors.RESET}")
    
    return positions


def sell_position(client, symbol, quantity):
    """Vend une position"""
    try:
        # Récupérer les infos du symbole
        symbol_info = client.get_symbol_info(symbol)
        if not symbol_info:
            return False, "Symbole invalide"
        
        # Trouver le filtre LOT_SIZE pour arrondir correctement
        step_size = None
        min_qty = None
        for filter in symbol_info['filters']:
            if filter['filterType'] == 'LOT_SIZE':
                step_size = float(filter['stepSize'])
                min_qty = float(filter['minQty'])
                break
        
        if step_size is None:
            return False, "Impossible de trouver stepSize"
        
        # Arrondir la quantité au step_size
        precision = len(str(step_size).rstrip('0').split('.')[-1]) if '.' in str(step_size) else 0
        quantity = float(quantity)
        quantity = round(quantity - (quantity % step_size), precision)
        
        if quantity < min_qty:
            return False, f"Quantité trop faible ({quantity} < {min_qty})"
        
        # Ordre de vente au prix du marché
        order = client.order_market_sell(
            symbol=symbol,
            quantity=quantity
        )
        
        # Calculer la valeur vendue
        fills = order.get('fills', [])
        total_value = sum(float(fill['price']) * float(fill['qty']) for fill in fills)
        
        return True, f"${total_value:.2f}"
    
    except BinanceAPIException as e:
        return False, f"API Error {e.code}: {e.message}"
    except Exception as e:
        return False, str(e)


def create_trading_pause():
    """Crée une pause de trading de 5 minutes"""
    pause_file = os.path.join(SCRIPT_DIR, 'trading_pause.json')
    paused_until = time.time() + 300  # 5 minutes
    
    try:
        with open(pause_file, 'w', encoding='utf-8') as f:
            json.dump({
                'paused_until': paused_until,
                'reason': 'SELL_ALL_MANUAL',
                'timestamp': datetime.now().isoformat()
            }, f, indent=2)
        
        pause_until_str = datetime.fromtimestamp(paused_until).strftime('%H:%M:%S')
        print(f"{Colors.YELLOW}⏸️  TRADING EN PAUSE jusqu'à {pause_until_str} (5 minutes){Colors.RESET}")
        return True
    except Exception as e:
        print(f"{Colors.RED}❌ Erreur création pause: {e}{Colors.RESET}")
        return False


def main():
    """Fonction principale"""
    force = '--force' in sys.argv
    
    print(f"""
{Colors.BOLD}{Colors.CYAN}================================================================
    VENTE AUTOMATIQUE COMPLÈTE - Version Améliorée
================================================================{Colors.RESET}
""")
    
    # 1. Charger la configuration
    print(f"{Colors.BLUE}[1/6] Chargement configuration...{Colors.RESET}")
    config = load_config()
    mode = "TESTNET" if config.TESTNET_MODE else "PRODUCTION"
    print(f"      Mode: {Colors.YELLOW}{mode}{Colors.RESET}")
    
    # 2. Connexion Binance
    print(f"\n{Colors.BLUE}[2/6] Connexion à Binance...{Colors.RESET}")
    client = get_binance_client(config)
    print(f"      {Colors.GREEN}✓ Connecté{Colors.RESET}")
    
    # 3. Récupération des positions
    print(f"\n{Colors.BLUE}[3/6] Récupération des positions...{Colors.RESET}")
    
    # Positions depuis positions.json
    local_positions = load_positions()
    print(f"      positions.json: {len(local_positions)} positions")
    
    # Positions depuis Binance
    binance_positions = get_binance_positions(client)
    print(f"      Binance: {len(binance_positions)} positions (≥5 USDT)")
    
    # Fusionner les deux listes (union des symboles)
    all_symbols = set(local_positions.keys())
    all_symbols.update(pos['symbol'] for pos in binance_positions)
    
    if not all_symbols:
        print(f"\n{Colors.YELLOW}ℹ️  Aucune position à vendre{Colors.RESET}")
        return 0
    
    print(f"\n{Colors.BOLD}Total positions à traiter: {len(all_symbols)}{Colors.RESET}")
    
    # Afficher les positions avec leur valeur
    total_value = 0
    for pos in binance_positions:
        print(f"      • {pos['symbol']}: {pos['quantity']:.4f} × ${pos['price']:.4f} = ${pos['value_usdt']:.2f}")
        total_value += pos['value_usdt']
    
    if total_value > 0:
        print(f"\n{Colors.BOLD}Valeur totale estimée: ${total_value:.2f}{Colors.RESET}")
    
    # 4. Confirmation
    if not force:
        print(f"\n{Colors.YELLOW}⚠️  Confirmer la vente de TOUTES les positions ?{Colors.RESET}")
        response = input(f"{Colors.YELLOW}   Taper 'OUI' pour continuer: {Colors.RESET}").strip().upper()
        if response != 'OUI':
            print(f"{Colors.RED}❌ Annulé{Colors.RESET}")
            return 1
    
    # 5. Vente des positions
    print(f"\n{Colors.BLUE}[4/6] Vente des positions...{Colors.RESET}")
    print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}")
    
    sold_count = 0
    failed_count = 0
    total_recovered = 0.0
    
    for symbol in sorted(all_symbols):
        # Récupérer la quantité depuis Binance (source fiable)
        quantity = 0
        for pos in binance_positions:
            if pos['symbol'] == symbol:
                quantity = pos['quantity']
                break
        
        if quantity == 0:
            # Pas de quantité sur Binance = position fantôme
            print(f"{Colors.YELLOW}⚠️  {symbol}: Position fantôme (0 sur Binance), nettoyage...{Colors.RESET}")
            if symbol in local_positions:
                del local_positions[symbol]
            failed_count += 1
            continue
        
        # Tenter la vente
        success, result = sell_position(client, symbol, quantity)
        
        if success:
            print(f"{Colors.GREEN}✓ {symbol}: Vendu pour {result}{Colors.RESET}")
            sold_count += 1
            # Extraire la valeur
            try:
                value = float(result.replace('$', '').replace(',', ''))
                total_recovered += value
            except:
                pass
            
            # Retirer de positions.json
            if symbol in local_positions:
                del local_positions[symbol]
        else:
            print(f"{Colors.RED}✗ {symbol}: {result}{Colors.RESET}")
            failed_count += 1
            
            # Si erreur de quantité insuffisante = nettoyer quand même
            if 'insufficient' in result.lower() or 'quantité trop faible' in result.lower():
                print(f"{Colors.YELLOW}  → Nettoyage position fantôme{Colors.RESET}")
                if symbol in local_positions:
                    del local_positions[symbol]
    
    print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}")
    
    # 6. Sauvegarde et finalisation
    print(f"\n{Colors.BLUE}[5/6] Mise à jour positions.json...{Colors.RESET}")
    # 🔴 FIX 22/01: VIDER COMPLÈTEMENT positions.json après vente totale
    # Pour éviter que le bot ne recharge des positions fantômes au redémarrage
    if save_positions({}):  # Toujours vider à {} après vente complète
        print(f"      {Colors.GREEN}✓ positions.json vidé complètement (0 positions){Colors.RESET}")
    
    print(f"\n{Colors.BLUE}[6/6] Création pause trading...{Colors.RESET}")
    create_trading_pause()
    
    # 🔴 FIX COHERENCE: Créer sell_all_signal.json pour que le bot running
    # efface aussi ses positions EN MÉMOIRE (pas seulement sur disque)
    # Bug: sans ce signal, _save_positions() réécrivait les positions fantômes
    # dans positions.json avant que le disk sync ne puisse les détecter
    try:
        signal_file = os.path.join(SCRIPT_DIR, 'sell_all_signal.json')
        with open(signal_file, 'w', encoding='utf-8') as f:
            json.dump({
                'action': 'SELL_ALL',
                'symbols': list(all_symbols),
                'timestamp': datetime.now().isoformat(),
                'source': 'sell_all.py'
            }, f, indent=2)
        print(f"{Colors.GREEN}✓ Signal sell_all envoyé au bot (sell_all_signal.json){Colors.RESET}")
    except Exception as e:
        print(f"{Colors.YELLOW}⚠️ Impossible de créer sell_all_signal.json: {e}{Colors.RESET}")
    
    # Résumé final
    print(f"\n{Colors.CYAN}{'═' * 60}{Colors.RESET}")
    print(f"{Colors.BOLD}{Colors.CYAN}RÉSUMÉ{Colors.RESET}")
    print(f"{Colors.CYAN}{'═' * 60}{Colors.RESET}")
    print(f"{Colors.GREEN}✓ Positions vendues: {sold_count}/{len(all_symbols)}{Colors.RESET}")
    if failed_count > 0:
        print(f"{Colors.YELLOW}⚠️ Échecs/Nettoyages: {failed_count}/{len(all_symbols)}{Colors.RESET}")
    print(f"{Colors.BOLD}💰 Valeur récupérée: ${total_recovered:.2f}{Colors.RESET}")
    
    # Vérifier le solde final
    try:
        account = client.get_account()
        for balance in account['balances']:
            if balance['asset'] == 'USDT':
                usdt_total = float(balance['free']) + float(balance['locked'])
                print(f"{Colors.BOLD}💵 Solde USDT final: ${usdt_total:.2f}{Colors.RESET}")
                break
    except:
        pass
    
    print(f"{Colors.CYAN}{'═' * 60}{Colors.RESET}\n")
    
    if sold_count == len(all_symbols):
        print(f"{Colors.GREEN}✅ Toutes les positions ont été vendues avec succès !{Colors.RESET}")
        return 0
    else:
        print(f"{Colors.YELLOW}⚠️  Certaines positions n'ont pas pu être vendues (voir détails ci-dessus){Colors.RESET}")
        return 0  # Retourner 0 quand même car les fantômes ont été nettoyés


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print(f"\n{Colors.YELLOW}❌ Annulé par l'utilisateur{Colors.RESET}")
        sys.exit(1)
    except Exception as e:
        print(f"\n{Colors.RED}❌ Erreur fatale: {e}{Colors.RESET}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
