#!/usr/bin/env python3
"""
🔄 Service de Mise à Jour Automatique
======================================
Ce service s'exécute en arrière-plan et maintient automatiquement :
1. Les données historiques à jour (toutes les 6 heures)
2. Le modèle IA entraîné avec les dernières données (toutes les 24 heures)
3. La watchlist synchronisée

Usage:
    python auto_updater_service.py              # Mode normal
    python auto_updater_service.py --once       # Exécuter une seule fois
    python auto_updater_service.py --daemon     # Mode démon (arrière-plan)
"""

import os
import sys
import json
import time
import signal
import logging
import argparse
import threading
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Optional

# ═══════════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════

SCRIPT_DIR = Path(__file__).parent
if sys.platform == 'win32':
    VENV_PYTHON = SCRIPT_DIR / '.venv' / 'Scripts' / 'python.exe'
else:
    VENV_PYTHON = SCRIPT_DIR / '.venv' / 'bin' / 'python3'

# Fichiers
STATUS_FILE = SCRIPT_DIR / 'auto_updater_status.json'
PID_FILE = SCRIPT_DIR / 'auto_updater.pid'
LOG_FILE = SCRIPT_DIR / 'auto_updater.log'

# Intervalles de mise à jour (en secondes)
UPDATE_INTERVALS = {
    'historical_data': 6 * 3600,      # Toutes les 6 heures
    'ai_training': 24 * 3600,          # Toutes les 24 heures
    'quick_sync': 1 * 3600,            # Sync rapide toutes les heures
    'adaptive_retrain': 4 * 3600,      # Vérification retraining adaptatif toutes les 4h
}

# Scripts à exécuter
SCRIPTS = {
    'fetch_historical': SCRIPT_DIR / 'fetch_historical_data.py',
    'train_ai': SCRIPT_DIR / 'train_ai_model.py',
    'historical_updater': SCRIPT_DIR / 'historical_data_updater.py',
}

# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING
# ═══════════════════════════════════════════════════════════════════════════════

# Fix encoding pour Windows
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

# Configuration du logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(levelname)s | %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S',
    handlers=[
        logging.FileHandler(LOG_FILE, encoding='utf-8'),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger('AutoUpdater')

# ═══════════════════════════════════════════════════════════════════════════════
# CLASSES
# ═══════════════════════════════════════════════════════════════════════════════

class AutoUpdaterService:
    """Service de mise à jour automatique"""
    
    def __init__(self):
        self.running = False
        self.status = self._load_status()
        self.lock = threading.Lock()
        
        # Vérifier Python
        if not VENV_PYTHON.exists():
            logger.error(f"❌ Python venv non trouvé: {VENV_PYTHON}")
            sys.exit(1)
    
    def _load_status(self) -> Dict:
        """Charge le statut précédent avec valeurs par défaut garanties"""
        # 🔧 FIX 08/02: Toujours partir des valeurs par défaut complètes
        # puis fusionner avec le fichier existant pour éviter KeyError
        defaults = {
            'last_historical_update': None,
            'last_ai_training': None,
            'last_quick_sync': None,
            'total_updates': 0,
            'total_trainings': 0,
            'errors': [],
            'started_at': None,
        }
        
        if STATUS_FILE.exists():
            try:
                with open(STATUS_FILE, 'r') as f:
                    saved = json.load(f)
                # Fusionner: defaults + saved (saved écrase les defaults)
                defaults.update(saved)
            except:
                pass
        
        return defaults
    
    def _save_status(self):
        """Sauvegarde le statut"""
        with self.lock:
            with open(STATUS_FILE, 'w') as f:
                json.dump(self.status, f, indent=2, default=str)
    
    def _run_script(self, script_path: Path, description: str, args: list = None, timeout: int = 3600) -> bool:
        """Exécute un script Python"""
        if not script_path.exists():
            logger.warning(f"⚠️ Script non trouvé: {script_path}")
            return False
        
        cmd = [str(VENV_PYTHON), '-u', str(script_path)]  # -u pour unbuffered output
        if args:
            cmd.extend(args)
        
        logger.info(f"🔄 Exécution: {description}")
        logger.info(f"   Commande: {' '.join(cmd)}")
        
        try:
            start_time = time.time()
            
            # Configuration environnement pour Windows - optimisé pour mode sans console
            env = os.environ.copy()
            env['PYTHONIOENCODING'] = 'utf-8'
            env['PYTHONUTF8'] = '1'
            env['PYTHONUNBUFFERED'] = '1'
            # Éviter les erreurs de fermeture de fenêtre Windows
            env['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'
            
            # Créer le processus avec gestion Windows améliorée
            creation_flags = 0
            if sys.platform == 'win32':
                creation_flags = subprocess.CREATE_NO_WINDOW
            
            result = subprocess.run(
                cmd,
                cwd=str(SCRIPT_DIR),
                capture_output=True,
                text=True,
                timeout=timeout,
                encoding='utf-8',
                errors='replace',
                env=env,
                creationflags=creation_flags
            )
            
            elapsed = time.time() - start_time
            
            # Patterns de succès dans la sortie
            success_patterns = ['terminé', 'completed', 'success', 'sauvegardé', 'saved']
            stdout_lower = (result.stdout or '').lower()
            
            # Considérer comme succès si:
            # 1. Code de retour 0
            # 2. Ou si la sortie contient un pattern de succès
            is_success = result.returncode == 0 or any(p in stdout_lower for p in success_patterns)
            
            if is_success:
                logger.info(f"✅ {description} terminé en {elapsed:.1f}s")
                return True
            else:
                logger.warning(f"⚠️ {description} terminé avec code {result.returncode} en {elapsed:.1f}s")
                stderr = result.stderr or ''
                
                # Ignorer les warnings non critiques
                ignore_patterns = ['UserWarning', 'FutureWarning', 'DeprecationWarning']
                is_just_warning = any(p in stderr for p in ignore_patterns) and 'Error' not in stderr
                
                if is_just_warning:
                    logger.info("   (Warnings ignorés)")
                    return True
                
                if stderr and not is_just_warning:
                    logger.error(f"   Erreur: {stderr[:300]}")
                    
                self.status['errors'].append({
                    'time': datetime.now().isoformat(),
                    'script': str(script_path.name),
                    'error': stderr[:200] if stderr else 'Unknown'
                })
                # Garder seulement les 10 dernières erreurs
                self.status['errors'] = self.status['errors'][-10:]
                return False
                
        except subprocess.TimeoutExpired as e:
            logger.error(f"⏰ {description} timeout après {timeout//60}min")
            return False
        except Exception as e:
            logger.error(f"❌ Erreur {description}: {e}")
            return False
    
    def update_historical_data(self) -> bool:
        """Met à jour les données historiques complètes"""
        logger.info("=" * 60)
        logger.info("📊 MISE À JOUR DES DONNÉES HISTORIQUES")
        logger.info("=" * 60)
        
        success = self._run_script(
            SCRIPTS['fetch_historical'],
            "Téléchargement données historiques"
        )
        
        if success:
            self.status['last_historical_update'] = datetime.now().isoformat()
            self.status['total_updates'] += 1
            self._save_status()
        
        return success
    
    def quick_sync(self) -> bool:
        """Synchronisation rapide des données récentes"""
        logger.info("-" * 40)
        logger.info("⚡ Synchronisation rapide")
        
        # Utiliser l'updater incrémental en mode ONE-SHOT
        if SCRIPTS['historical_updater'].exists():
            success = self._run_script(
                SCRIPTS['historical_updater'],
                "Sync incrémentale",
                ['--once']  # Mode one-shot pour terminer rapidement
            )
        else:
            # Sinon, mettre à jour seulement les dernières 24h
            success = self._run_script(
                SCRIPTS['fetch_historical'],
                "Sync rapide",
                ['--days', '1']
            )
        
        if success:
            self.status['last_quick_sync'] = datetime.now().isoformat()
            self._save_status()
        
        return success
    
    def train_ai_model(self) -> bool:
        """Entraîne le modèle IA avec les nouvelles données"""
        logger.info("=" * 60)
        logger.info("🧠 ENTRAÎNEMENT DU MODÈLE IA")
        logger.info("=" * 60)
        
        success = self._run_script(
            SCRIPTS['train_ai'],
            "Entraînement IA",
            ['--epochs', '30', '--symbols', '15'],  # Optimisé CPU sans GPU
            timeout=3600  # 1 heure timeout (réduit de 2h)
        )
        
        if success:
            self.status['last_ai_training'] = datetime.now().isoformat()
            self.status['total_trainings'] += 1
            self._save_status()
        
        return success
    
    def check_adaptive_retrain(self) -> bool:
        """Vérifie si un retraining adaptatif est nécessaire (régime marché, win rate).
        Appelle ai_adaptive_retrainer.py pour évaluer et exécuter si besoin.
        """
        logger.info("-" * 40)
        logger.info("🧠 Vérification Retraining Adaptatif")
        
        try:
            from ai_adaptive_retrainer import get_adaptive_retrainer
            retrainer = get_adaptive_retrainer()
            
            should_train, reason = retrainer.should_retrain()
            
            if not should_train:
                logger.info(f"   ℹ️  Pas de retrain nécessaire: {reason}")
                self.status['last_adaptive_retrain'] = datetime.now().isoformat()
                self.status['last_adaptive_reason'] = reason
                self._save_status()
                return True
            
            logger.info(f"   🚨 Retrain déclenché: {reason}")
            result = retrainer.retrain_all()
            
            status_str = result.get('status', 'UNKNOWN')
            logger.info(f"   ✅ Retraining adaptatif terminé: {status_str}")
            
            self.status['last_adaptive_retrain'] = datetime.now().isoformat()
            self.status['last_adaptive_reason'] = reason
            self.status['last_adaptive_result'] = status_str
            self._save_status()
            
            # Si retraining d'urgence (win rate catastrophique), relancer aussi train_ai_model
            if 'EMERGENCY' in reason:
                logger.info("   🚨 Emergency retrain détecté → relance entraînement LSTM complet")
                self.train_ai_model()
            
            return status_str in ('SUCCESS', 'PARTIAL')
            
        except ImportError:
            logger.warning("⚠️ ai_adaptive_retrainer non disponible")
            return False
        except Exception as e:
            logger.error(f"❌ Erreur retraining adaptatif: {e}")
            return False
    
    def _should_update(self, task: str, interval: int) -> bool:
        """Vérifie si une mise à jour est nécessaire"""
        last_update_key = f'last_{task}'
        last_update = self.status.get(last_update_key)
        
        if not last_update:
            return True
        
        try:
            last_dt = datetime.fromisoformat(last_update)
            elapsed = (datetime.now() - last_dt).total_seconds()
            return elapsed >= interval
        except:
            return True
    
    def run_once(self):
        """Exécute toutes les tâches une seule fois"""
        logger.info("🚀 Exécution unique des mises à jour")
        
        # Mise à jour des données
        self.update_historical_data()
        
        # Entraînement IA
        self.train_ai_model()
        
        # Vérification retraining adaptatif
        self.check_adaptive_retrain()
        
        logger.info("✅ Mises à jour terminées")
    
    def run_daemon(self):
        """Exécute le service en mode démon"""
        self.running = True
        self.status['started_at'] = datetime.now().isoformat()
        self._save_status()
        
        # Sauvegarder le PID
        with open(PID_FILE, 'w') as f:
            f.write(str(os.getpid()))
        
        logger.info("=" * 60)
        logger.info("🔄 AUTO-UPDATER SERVICE DÉMARRÉ")
        logger.info("=" * 60)
        logger.info(f"   PID: {os.getpid()}")
        logger.info(f"   Mise à jour données: toutes les {UPDATE_INTERVALS['historical_data']//3600}h")
        logger.info(f"   Sync rapide: toutes les {UPDATE_INTERVALS['quick_sync']//3600}h")
        logger.info(f"   Entraînement IA: toutes les {UPDATE_INTERVALS['ai_training']//3600}h")
        logger.info(f"   Retrain adaptatif: toutes les {UPDATE_INTERVALS['adaptive_retrain']//3600}h")
        logger.info("=" * 60)
        
        # Gestionnaire de signal pour arrêt propre
        def signal_handler(signum, frame):
            logger.info("🛑 Signal d'arrêt reçu, fermeture...")
            self.running = False
        
        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)
        
        # Délai de démarrage : attendre 10 min pour laisser dashboard+bot charger leurs modèles TF
        # Évite la surcharge RAM/CPU au démarrage qui causait les gels PC
        STARTUP_DELAY = 600  # 10 minutes
        logger.info(f"⏳ Délai démarrage {STARTUP_DELAY//60}min (chargement modèles TF en cours)...")
        for _ in range(STARTUP_DELAY):
            if not self.running:
                break
            time.sleep(1)
        
        # Exécution immédiate au démarrage si nécessaire (après le délai)
        if self.running and self._should_update('historical_update', UPDATE_INTERVALS['historical_data']):
            self.update_historical_data()
        
        if self.running and self._should_update('ai_training', UPDATE_INTERVALS['ai_training']):
            self.train_ai_model()
        
        # Boucle principale
        check_interval = 300  # Vérifier toutes les 5 minutes
        
        while self.running:
            try:
                # Sync rapide (toutes les heures)
                if self._should_update('quick_sync', UPDATE_INTERVALS['quick_sync']):
                    self.quick_sync()
                
                # Mise à jour complète des données (toutes les 6h)
                if self._should_update('historical_update', UPDATE_INTERVALS['historical_data']):
                    self.update_historical_data()
                
                # Entraînement IA (toutes les 24h)
                if self._should_update('ai_training', UPDATE_INTERVALS['ai_training']):
                    self.train_ai_model()
                
                # Retraining adaptatif (toutes les 4h - vérifie régime + win rate)
                if self._should_update('adaptive_retrain', UPDATE_INTERVALS['adaptive_retrain']):
                    self.check_adaptive_retrain()
                
                # Attendre avant la prochaine vérification
                for _ in range(check_interval):
                    if not self.running:
                        break
                    time.sleep(1)
                    
            except Exception as e:
                logger.error(f"❌ Erreur dans la boucle principale: {e}")
                time.sleep(60)  # Attendre 1 minute en cas d'erreur
        
        # Nettoyage
        if PID_FILE.exists():
            PID_FILE.unlink()
        
        logger.info("👋 Auto-updater arrêté proprement")
    
    def get_status(self) -> Dict:
        """Retourne le statut actuel du service"""
        return {
            **self.status,
            'running': self.running,
            'next_historical_update': self._get_next_run('historical_update', UPDATE_INTERVALS['historical_data']),
            'next_ai_training': self._get_next_run('ai_training', UPDATE_INTERVALS['ai_training']),
            'next_quick_sync': self._get_next_run('quick_sync', UPDATE_INTERVALS['quick_sync']),
            'next_adaptive_retrain': self._get_next_run('adaptive_retrain', UPDATE_INTERVALS['adaptive_retrain']),
        }
    
    def _get_next_run(self, task: str, interval: int) -> Optional[str]:
        """Calcule la prochaine exécution"""
        last_update_key = f'last_{task}'
        last_update = self.status.get(last_update_key)
        
        if not last_update:
            return "Immédiat"
        
        try:
            last_dt = datetime.fromisoformat(last_update)
            next_dt = last_dt + timedelta(seconds=interval)
            
            if next_dt <= datetime.now():
                return "Immédiat"
            
            remaining = next_dt - datetime.now()
            hours = int(remaining.total_seconds() // 3600)
            minutes = int((remaining.total_seconds() % 3600) // 60)
            
            return f"Dans {hours}h {minutes}min"
        except:
            return "Inconnu"


def is_running() -> bool:
    """Vérifie si le service est déjà en cours d'exécution"""
    if not PID_FILE.exists():
        return False
    
    try:
        with open(PID_FILE, 'r') as f:
            pid = int(f.read().strip())
        
        # Vérifier si le processus existe
        import psutil
        return psutil.pid_exists(pid)
    except:
        return False


def show_status():
    """Affiche le statut du service"""
    if STATUS_FILE.exists():
        with open(STATUS_FILE, 'r') as f:
            status = json.load(f)
        
        print("\n📊 STATUT AUTO-UPDATER")
        print("=" * 50)
        print(f"   Démarré: {status.get('started_at', 'N/A')}")
        print(f"   Dernière sync données: {status.get('last_historical_update', 'Jamais')}")
        print(f"   Dernier entraînement IA: {status.get('last_ai_training', 'Jamais')}")
        print(f"   Dernière sync rapide: {status.get('last_quick_sync', 'Jamais')}")
        print(f"   Total mises à jour: {status.get('total_updates', 0)}")
        print(f"   Total entraînements: {status.get('total_trainings', 0)}")
        
        if status.get('errors'):
            print(f"\n⚠️ Dernières erreurs:")
            for err in status['errors'][-3:]:
                print(f"   - {err.get('time', 'N/A')}: {err.get('script', 'N/A')}")
    else:
        print("\n⚠️ Aucun statut disponible (service jamais exécuté)")


# ═══════════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(description='Service de mise à jour automatique')
    parser.add_argument('--once', action='store_true', help='Exécuter une seule fois')
    parser.add_argument('--daemon', action='store_true', help='Mode démon (arrière-plan)')
    parser.add_argument('--status', action='store_true', help='Afficher le statut')
    parser.add_argument('--stop', action='store_true', help='Arrêter le service')
    
    args = parser.parse_args()
    
    if args.status:
        show_status()
        return
    
    if args.stop:
        if PID_FILE.exists():
            with open(PID_FILE, 'r') as f:
                pid = int(f.read().strip())
            try:
                os.kill(pid, signal.SIGTERM)
                print(f"🛑 Signal d'arrêt envoyé au processus {pid}")
            except:
                print(f"⚠️ Impossible d'arrêter le processus {pid}")
            PID_FILE.unlink()
        else:
            print("⚠️ Service non actif")
        return
    
    # Vérifier si déjà en cours
    if is_running():
        print("⚠️ Le service auto-updater est déjà en cours d'exécution")
        show_status()
        return
    
    service = AutoUpdaterService()
    
    if args.once:
        service.run_once()
    else:
        # Mode démon par défaut
        service.run_daemon()


if __name__ == '__main__':
    main()
