"""
Business Logic Services
Separation of concerns - isolate business logic from HTTP layer
"""

import os
import re
import asyncio
import logging
import subprocess
import sys
from typing import Dict, List, Optional, Tuple, Any
from datetime import datetime, timedelta
from pathlib import Path

from .utils import load_json_file, save_json_file
from .models import TradingConfig, Position, Trade

logger = logging.getLogger(__name__)


class ConfigService:
    """Service pour gérer la configuration de trading"""

    def __init__(self, script_dir: str):
        self.script_dir = script_dir
        self.config_path = os.path.join(script_dir, 'config.py')

    def read_config(self) -> Optional[Dict[str, Any]]:
        """Lire la configuration depuis config.py"""
        try:
            if not os.path.exists(self.config_path):
                logger.error(f"Config file not found: {self.config_path}")
                return None

            with open(self.config_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Parser les valeurs avec regex
            patterns = {
                'STOP_LOSS_PERCENT': r'STOP_LOSS_PERCENT\s*=\s*([\d.]+)',
                'TAKE_PROFIT_PERCENT': r'TAKE_PROFIT_PERCENT\s*=\s*([\d.]+)',
                'MAX_ORDER_SIZE': r'MAX_ORDER_SIZE\s*=\s*(\d+)',
                'RSI_PERIOD': r'RSI_PERIOD\s*=\s*(\d+)',
                'RSI_OVERSOLD': r'RSI_OVERSOLD\s*=\s*([\d.]+)',
                'RSI_OVERBOUGHT': r'RSI_OVERBOUGHT\s*=\s*([\d.]+)',
                'EMA_SHORT': r'EMA_SHORT\s*=\s*(\d+)',
                'EMA_LONG': r'EMA_LONG\s*=\s*(\d+)',
                'BB_PERIOD': r'BB_PERIOD\s*=\s*(\d+)',
                'BB_STD': r'BB_STD\s*=\s*([\d.]+)',
                'REQUIRED_SIGNALS': r'REQUIRED_SIGNALS\s*=\s*(\d+)',
            }

            config = {}
            for key, pattern in patterns.items():
                match = re.search(pattern, content)
                if match:
                    value = match.group(1)
                    config[key] = float(value) if '.' in value else int(value)

            # Valeurs par défaut
            config.setdefault('REQUIRED_SIGNALS', 2)

            return config

        except Exception as e:
            logger.error(f"Error reading config: {e}")
            return None

    def apply_config(self, config: TradingConfig) -> bool:
        """Appliquer une nouvelle configuration"""
        try:
            with open(self.config_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Mapping des paramètres
            updates = config.dict()

            # Remplacer les valeurs dans le fichier
            for key, value in updates.items():
                if value is not None:
                    # Formatter la valeur
                    if isinstance(value, float):
                        formatted = f"{value:.1f}" if value == int(value) else f"{value}"
                    else:
                        formatted = str(int(value))

                    # Pattern pour remplacer la ligne
                    pattern = rf'^({key}\s*=\s*)([\d.]+)'
                    replacement = rf'\g<1>{formatted}'
                    content = re.sub(pattern, replacement, content, flags=re.MULTILINE)

            # Écrire le fichier de manière atomique
            temp_path = f"{self.config_path}.tmp"
            with open(temp_path, 'w', encoding='utf-8') as f:
                f.write(content)

            os.replace(temp_path, self.config_path)
            logger.info("Configuration updated successfully")
            return True

        except Exception as e:
            logger.error(f"Error applying config: {e}")
            return False


class TradingService:
    """Service pour gérer les positions et trades"""

    def __init__(self, script_dir: str):
        self.script_dir = script_dir
        self.positions_file = os.path.join(script_dir, 'positions.json')
        self.history_file = os.path.join(script_dir, 'trade_history.json')

    def get_positions(self) -> Dict[str, Position]:
        """Récupérer toutes les positions ouvertes"""
        data = load_json_file(self.positions_file, {})
        positions = {}

        for symbol, pos_data in data.items():
            try:
                positions[symbol] = Position(**pos_data)
            except Exception as e:
                logger.error(f"Invalid position data for {symbol}: {e}")

        return positions

    def get_trade_history(self, limit: int = 100) -> List[Trade]:
        """Récupérer l'historique des trades"""
        data = load_json_file(self.history_file, [])
        trades = []

        for trade_data in data[:limit]:
            try:
                trades.append(Trade(**trade_data))
            except Exception as e:
                logger.error(f"Invalid trade data: {e}")

        return trades

    async def get_current_prices(self) -> Dict[str, float]:
        """Récupérer les prix actuels depuis Binance (async)"""
        import aiohttp

        try:
            url = 'https://api.binance.com/api/v3/ticker/price'

            async with aiohttp.ClientSession() as session:
                async with session.get(url, timeout=10) as response:
                    if response.status == 200:
                        data = await response.json()
                        return {item['symbol']: float(item['price']) for item in data}

            return {}

        except Exception as e:
            logger.error(f"Error fetching prices: {e}")
            return {}

    def close_position(self, symbol: str, exit_price: float, reason: str = "manual") -> bool:
        """Fermer une position"""
        try:
            positions = load_json_file(self.positions_file, {})

            if symbol not in positions:
                logger.warning(f"Position {symbol} not found")
                return False

            pos = positions[symbol]
            entry_price = pos.get('entry_price', 0)
            quantity = pos.get('quantity', 0)

            # Calculer P&L
            pnl = (exit_price - entry_price) * quantity
            pnl_pct = ((exit_price / entry_price) - 1) * 100 if entry_price > 0 else 0

            # Créer le trade
            trade = {
                'symbol': symbol,
                'side': 'BUY',
                'entry_price': entry_price,
                'exit_price': exit_price,
                'quantity': quantity,
                'pnl': round(pnl, 4),
                'pnl_pct': round(pnl_pct, 2),
                'reason': reason,
                'entry_time': pos.get('timestamp', datetime.now().isoformat()),
                'exit_time': datetime.now().isoformat()
            }

            # Ajouter à l'historique
            history = load_json_file(self.history_file, [])
            history.insert(0, trade)
            save_json_file(self.history_file, history)

            # Supprimer la position
            del positions[symbol]
            save_json_file(self.positions_file, positions)

            logger.info(f"Position {symbol} closed: PnL = {pnl:+.2f} ({pnl_pct:+.2f}%)")
            return True

        except Exception as e:
            logger.error(f"Error closing position {symbol}: {e}")
            return False

    async def close_all_positions(self) -> Tuple[int, float, List[str]]:
        """
        Fermer toutes les positions IMMÉDIATEMENT via l'API Binance.
        Returns: (count_sold, total_pnl, failed_list)
        """
        positions = load_json_file(self.positions_file, {})

        if not positions:
            return 0, 0.0, []

        # Importer les fonctions nécessaires pour l'API Binance
        try:
            import hmac
            import hashlib
            import time
            import math
            import aiohttp
            
            # Charger les clés API depuis config
            import sys
            sys.path.insert(0, self.script_dir)
            from config import BINANCE_API_KEY, BINANCE_API_SECRET, TESTNET_MODE
            
            # URL de base selon le mode
            base_url = 'https://testnet.binance.vision' if TESTNET_MODE else 'https://api.binance.com'
            
            # 🔧 FIX: Synchroniser l'horloge avec le serveur Binance
            time_offset = 0
            try:
                import aiohttp as _aio
                async with _aio.ClientSession() as _ts:
                    async with _ts.get(f'{base_url}/api/v3/time', timeout=_aio.ClientTimeout(total=5)) as _tr:
                        if _tr.status == 200:
                            _td = await _tr.json()
                            server_time = _td['serverTime']
                            local_time = int(time.time() * 1000)
                            time_offset = server_time - local_time
                            if abs(time_offset) > 500:
                                logger.info(f"⏰ Sell-all: horloge synchronisée (décalage: {time_offset}ms)")
            except Exception as e:
                logger.warning(f"⚠️ Synchro horloge sell-all échouée: {e}")
            
            def get_signature(params: str) -> str:
                return hmac.new(
                    BINANCE_API_SECRET.encode(),
                    params.encode(),
                    hashlib.sha256
                ).hexdigest()
            
            def format_qty(qty, step_size):
                """Formater la quantité selon le LOT_SIZE de Binance (arrondi vers le bas)"""
                if step_size <= 0:
                    step_size = 0.001
                precision = max(0, int(round(-math.log10(step_size))))
                factor = 10 ** precision
                formatted = int(qty * factor) / factor
                if precision == 0:
                    return int(formatted)
                return formatted
            
            sold_count = 0
            total_pnl = 0.0
            failed = []
            sold_symbols = []
            
            async with aiohttp.ClientSession() as session:
                # 🔧 FIX: Récupérer les LOT_SIZE depuis l'exchange info
                lot_sizes = {}
                try:
                    info_url = f'{base_url}/api/v3/exchangeInfo'
                    async with session.get(info_url, timeout=aiohttp.ClientTimeout(total=10)) as response:
                        if response.status == 200:
                            data = await response.json()
                            for sym_info in data.get('symbols', []):
                                sym = sym_info['symbol']
                                if sym in positions:
                                    for f in sym_info.get('filters', []):
                                        if f['filterType'] == 'LOT_SIZE':
                                            lot_sizes[sym] = float(f.get('stepSize', 0.001))
                                            break
                    logger.info(f"📊 LOT_SIZE récupérés pour {len(lot_sizes)} symboles")
                except Exception as e:
                    logger.warning(f"⚠️ Impossible de récupérer exchange info: {e}")
                
                # Récupérer les prix actuels pour le P&L
                prices = await self.get_current_prices()
                
                for symbol, pos in positions.items():
                    try:
                        raw_qty = pos.get('quantity', 0)
                        entry_price = pos.get('entry_price', 0)
                        
                        if raw_qty <= 0:
                            continue
                        
                        # 🔧 FIX: Formater la quantité selon LOT_SIZE (crucial!)
                        step_size = lot_sizes.get(symbol, 0.001)
                        qty = format_qty(raw_qty, step_size)
                        
                        if qty <= 0:
                            failed.append(f"{symbol}: quantité trop petite après formatage LOT_SIZE ({raw_qty} → 0)")
                            logger.warning(f"⚠️ {symbol}: quantité {raw_qty} trop petite pour step_size {step_size}")
                            continue
                        
                        logger.info(f"📤 Vente {symbol}: qty={raw_qty} → formatted={qty} (step={step_size})")
                        
                        # Créer l'ordre de vente MARKET
                        timestamp = int(time.time() * 1000) + time_offset
                        params = f'symbol={symbol}&side=SELL&type=MARKET&quantity={qty}&recvWindow=10000&timestamp={timestamp}'
                        signature = get_signature(params)
                        
                        headers = {'X-MBX-APIKEY': BINANCE_API_KEY}
                        url = f'{base_url}/api/v3/order?{params}&signature={signature}'
                        
                        async with session.post(url, headers=headers) as response:
                            result = await response.json()
                            
                            if 'orderId' in result:
                                # Vente réussie
                                fills = result.get('fills', [])
                                exit_price = float(fills[0].get('price', prices.get(symbol, entry_price))) if fills else prices.get(symbol, entry_price)
                                pnl = (exit_price - entry_price) * raw_qty
                                total_pnl += pnl
                                sold_count += 1
                                sold_symbols.append(symbol)
                                
                                # Enregistrer dans l'historique
                                trade = {
                                    'symbol': symbol,
                                    'side': 'BUY',
                                    'entry_price': entry_price,
                                    'exit_price': exit_price,
                                    'quantity': raw_qty,
                                    'pnl': round(pnl, 4),
                                    'pnl_pct': round(((exit_price / entry_price) - 1) * 100, 2) if entry_price > 0 else 0,
                                    'profit_usdt': round(pnl, 4),
                                    'profit_percent': round(((exit_price / entry_price) - 1) * 100, 2) if entry_price > 0 else 0,
                                    'reason': 'manual_sell_all',
                                    'exit_reason': 'manual_sell_all',
                                    'entry_time': pos.get('timestamp', datetime.now().isoformat()),
                                    'exit_time': datetime.now().isoformat(),
                                    'pattern': pos.get('pattern', 'MANUAL_CLOSE')
                                }
                                
                                history = load_json_file(self.history_file, [])
                                history.insert(0, trade)
                                save_json_file(self.history_file, history)
                                
                                logger.info(f"✅ SOLD {symbol}: {qty} @ {exit_price:.6f} | PnL: {pnl:+.4f}")
                            else:
                                error_msg = result.get('msg', str(result))
                                failed.append(f"{symbol}: {error_msg}")
                                logger.error(f"❌ Failed to sell {symbol}: {error_msg}")
                        
                        # Petit délai pour éviter le rate limiting
                        await asyncio.sleep(0.2)
                        
                    except Exception as e:
                        failed.append(f"{symbol}: {str(e)}")
                        logger.error(f"❌ Error selling {symbol}: {e}")
            
            # 🔧 FIX: Ne retirer que les positions VENDUES (pas tout vider aveuglément)
            remaining = {s: p for s, p in positions.items() if s not in sold_symbols}
            save_json_file(self.positions_file, remaining)
            
            if sold_symbols:
                logger.info(f"✅ {sold_count} positions vendues, PnL total: {total_pnl:+.2f}")
            if remaining:
                logger.warning(f"⚠️ {len(remaining)} positions NON vendues: {list(remaining.keys())}")
            else:
                logger.info("🗑️ Toutes les positions vendues, positions.json vidé")
            
            # 🔧 FIX 08/02: TOUJOURS créer sell_all_signal.json avec action SELL_ALL_COOLDOWN
            # C'est INDISPENSABLE pour que le bot:
            #   1) Efface ses positions EN MÉMOIRE (sinon il réécrit l'ancien positions.json)
            #   2) Active la pause interne de 5 minutes
            # L'action SELL_ALL_COOLDOWN est acceptée par check_triggers_loop
            # Les close_position() du bot retourneront "insufficient balance" pour les positions
            # déjà vendues → elles seront supprimées du dict en mémoire (comportement existant)
            signal_file = os.path.join(self.script_dir, 'sell_all_signal.json')
            save_json_file(signal_file, {
                'action': 'SELL_ALL_COOLDOWN',
                'timestamp': datetime.now().isoformat(),
                'symbols': sold_symbols + list(remaining.keys()),
                'sold_by_api': sold_symbols,
                'reason': 'manual_dashboard_sync'
            })
            logger.info(f"📤 Signal SELL_ALL_COOLDOWN créé pour synchroniser le bot ({len(sold_symbols)} vendues)")
            
            return sold_count, total_pnl, failed
            
        except ImportError as e:
            logger.error(f"Import error: {e}")
            # Fallback: créer le signal pour le bot
            return await self._create_sell_signal_fallback(positions)
        except Exception as e:
            logger.error(f"Error in close_all_positions: {e}")
            return await self._create_sell_signal_fallback(positions)
    
    async def _create_sell_signal_fallback(self, positions: Dict) -> Tuple[int, float, List[str]]:
        """Fallback: créer un signal pour que le bot exécute les ventes"""
        prices = await self.get_current_prices()
        
        estimated_pnl = 0.0
        position_symbols = list(positions.keys())
        
        for symbol, pos in positions.items():
            if symbol in prices:
                entry_price = pos.get('entry_price', 0)
                exit_price = prices[symbol]
                quantity = pos.get('quantity', 0)
                pnl = (exit_price - entry_price) * quantity
                estimated_pnl += pnl

        # Créer signal pour le bot
        signal_file = os.path.join(self.script_dir, 'sell_all_signal.json')
        save_json_file(signal_file, {
            'action': 'SELL_ALL',
            'timestamp': datetime.now().isoformat(),
            'symbols': position_symbols,
            'reason': 'manual_dashboard'
        })
        
        logger.info(f"Created sell_all signal for {len(position_symbols)} positions (fallback)")
        return len(position_symbols), estimated_pnl, []

    def update_positions_sltp(self, stop_loss_pct: float, take_profit_pct: float) -> Dict[str, Any]:
        """
        Recalculer et appliquer les nouveaux SL/TP sur toutes les positions ouvertes.
        Prend effet immédiatement sur les trades en cours.
        
        Args:
            stop_loss_pct: Nouveau pourcentage de stop loss (ex: 3.0 pour 3%)
            take_profit_pct: Nouveau pourcentage de take profit (ex: 6.0 pour 6%)
        
        Returns:
            Dict avec le résultat de l'opération
        """
        try:
            positions = load_json_file(self.positions_file, {})
            
            if not positions:
                return {
                    'success': True,
                    'message': 'Aucune position ouverte',
                    'updated': 0
                }
            
            updated_count = 0
            updates = []
            
            for symbol, pos in positions.items():
                entry_price = pos.get('entry_price', 0)
                
                if entry_price <= 0:
                    continue
                
                old_sl = pos.get('stop_loss', 0)
                old_tp = pos.get('take_profit', 0)
                
                # Calculer les nouveaux niveaux
                new_sl = entry_price * (1 - stop_loss_pct / 100)
                new_tp = entry_price * (1 + take_profit_pct / 100)
                
                # Mettre à jour uniquement si les valeurs changent significativement
                sl_changed = abs(new_sl - old_sl) > entry_price * 0.0001
                tp_changed = abs(new_tp - old_tp) > entry_price * 0.0001
                
                if sl_changed or tp_changed:
                    pos['stop_loss'] = new_sl
                    pos['take_profit'] = new_tp
                    updated_count += 1
                    
                    updates.append({
                        'symbol': symbol,
                        'entry_price': entry_price,
                        'old_sl': round(old_sl, 6),
                        'new_sl': round(new_sl, 6),
                        'old_tp': round(old_tp, 6),
                        'new_tp': round(new_tp, 6)
                    })
                    
                    logger.info(f"📊 {symbol}: SL {old_sl:.6f}→{new_sl:.6f} | TP {old_tp:.6f}→{new_tp:.6f}")
            
            # Sauvegarder les positions mises à jour
            if updated_count > 0:
                save_json_file(self.positions_file, positions)
                logger.info(f"✅ {updated_count} position(s) mises à jour avec SL={stop_loss_pct}% TP={take_profit_pct}%")
                
                # Créer un fichier signal pour que le bot recharge les SL/TP en mémoire
                signal_file = os.path.join(self.script_dir, 'sltp_update_signal.json')
                save_json_file(signal_file, {
                    'stop_loss_pct': stop_loss_pct,
                    'take_profit_pct': take_profit_pct,
                    'timestamp': datetime.now().isoformat(),
                    'positions_updated': updated_count
                })
                logger.info(f"📡 Signal envoyé au bot pour synchronisation")
            
            return {
                'success': True,
                'message': f'{updated_count} position(s) mises à jour',
                'updated': updated_count,
                'stop_loss_pct': stop_loss_pct,
                'take_profit_pct': take_profit_pct,
                'details': updates
            }
            
        except Exception as e:
            logger.error(f"Erreur update_positions_sltp: {e}")
            return {
                'success': False,
                'error': str(e),
                'updated': 0
            }


class BotService:
    """Service pour gérer le bot de trading"""

    def __init__(self, script_dir: str):
        self.script_dir = script_dir
        self.pid_file = os.path.join(script_dir, 'bot.pid')

    def restart_bot(self, bot_script: str = 'advanced_trading_bot.py') -> Optional[int]:
        """Redémarrer le bot"""
        try:
            # Tuer le processus existant
            if os.path.exists(self.pid_file):
                try:
                    with open(self.pid_file, 'r') as f:
                        pid = int(f.read().strip())

                    import signal
                    os.kill(pid, signal.SIGTERM)
                    logger.info(f"Killed bot process PID {pid}")

                    # Attendre que le processus se termine
                    import time
                    for _ in range(10):
                        try:
                            os.kill(pid, 0)
                            time.sleep(0.3)
                        except OSError:
                            break

                except Exception as e:
                    logger.warning(f"Could not kill existing bot: {e}")

            # Lancer le nouveau bot
            bot_path = os.path.join(self.script_dir, bot_script)

            if not os.path.exists(bot_path):
                logger.error(f"Bot script not found: {bot_path}")
                return None

            venv_python = os.path.join(self.script_dir, '.venv', 'Scripts', 'python.exe')
            python_exe = venv_python if os.path.exists(venv_python) else sys.executable

            process = subprocess.Popen(
                [python_exe, bot_path],
                cwd=self.script_dir,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True
            )

            # Sauvegarder le PID
            with open(self.pid_file, 'w') as f:
                f.write(str(process.pid))

            logger.info(f"Bot restarted with PID {process.pid}")
            return process.pid

        except Exception as e:
            logger.error(f"Error restarting bot: {e}")
            return None
