#!/usr/bin/env python3
"""
Bot Watchdog - Surveillance et redémarrage automatique du bot
============================================================

Fonctionnalités :
- Vérifie toutes les 60 secondes si le bot est actif
- Redémarre automatiquement le bot s'il crash
- Envoie des alertes en cas de problèmes répétés
- Logs détaillés des incidents
"""

import os
import sys
import io
import time
import json
import subprocess
import psutil
from datetime import datetime
from pathlib import Path

# ── Forcer stdout/stderr en UTF-8 sur Windows pour éviter le crash charmap ──
if sys.platform == 'win32':
    try:
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
    except AttributeError:
        pass  # déjà wrappé ou contexte sans buffer

# Configuration
SCRIPT_DIR = Path(__file__).parent.absolute()
BOT_PID_FILE = SCRIPT_DIR / "bot.pid"
WATCHDOG_PID_FILE = SCRIPT_DIR / "watchdog.pid"  # PID du watchdog lui-même
WATCHDOG_LOG = SCRIPT_DIR / "watchdog.log"
WATCHDOG_STATUS = SCRIPT_DIR / "watchdog_status.json"
CHECK_INTERVAL = 60  # Vérifier toutes les 60 secondes
MAX_RESTARTS_PER_HOUR = 5  # Maximum de redémarrages par heure

class Colors:
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    BLUE = '\033[94m'
    RESET = '\033[0m'

def log_message(msg, level="INFO"):
    """Log avec timestamp"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    color = {
        "INFO": Colors.BLUE,
        "SUCCESS": Colors.GREEN,
        "WARNING": Colors.YELLOW,
        "ERROR": Colors.RED
    }.get(level, Colors.RESET)
    
    log_line = f"[{timestamp}] [{level}] {msg}"
    try:
        print(f"{color}{log_line}{Colors.RESET}", flush=True)
    except (UnicodeEncodeError, UnicodeDecodeError):
        safe = log_line.encode('ascii', 'replace').decode('ascii')
        try:
            print(f"{color}{safe}{Colors.RESET}", flush=True)
        except Exception:
            pass
    
    # Écrire dans le fichier log - FORCER UTF-8 pour éviter crash Unicode
    try:
        with open(WATCHDOG_LOG, 'a', encoding='utf-8') as f:
            f.write(log_line + '\n')
    except Exception:
        try:
            msg_clean = msg.encode('ascii', 'ignore').decode('ascii')
            log_line_clean = f"[{timestamp}] [{level}] {msg_clean}"
            with open(WATCHDOG_LOG, 'a', encoding='utf-8') as f:
                f.write(log_line_clean + '\n')
        except Exception:
            pass

def load_watchdog_status():
    """Charger le statut du watchdog"""
    if WATCHDOG_STATUS.exists():
        try:
            with open(WATCHDOG_STATUS, 'r') as f:
                return json.load(f)
        except:
            pass
    return {
        'restarts': [],
        'total_restarts': 0,
        'last_check': None,
        'last_restart': None
    }

def save_watchdog_status(status):
    """Sauvegarder le statut du watchdog"""
    with open(WATCHDOG_STATUS, 'w') as f:
        json.dump(status, f, indent=2)

def is_bot_running():
    """Vérifier si le bot est actif"""
    if not BOT_PID_FILE.exists():
        return False
    
    try:
        with open(BOT_PID_FILE, 'r') as f:
            pid = int(f.read().strip())
        
        # Vérifier si le processus existe (et n'est pas zombie)
        if psutil.pid_exists(pid):
            proc = psutil.Process(pid)
            # Vérifier que c'est bien un processus Python non-zombie
            if 'python' in proc.name().lower() and proc.status() != psutil.STATUS_ZOMBIE:
                return True
        
        return False
    except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied):
        return False

def check_bot_activity():
    """Vérifier si le bot génère des signaux récemment"""
    signals_log = SCRIPT_DIR / "trade_logs" / "signals_log.jsonl"
    
    if not signals_log.exists():
        return False
    
    try:
        # Lire les 5 dernières lignes
        with open(signals_log, 'r', encoding='utf-8') as f:
            lines = f.readlines()
            if not lines:
                return False
            
            # Vérifier le timestamp du dernier signal
            last_line = lines[-1].strip()
            if last_line:
                data = json.loads(last_line)
                timestamp_str = data.get('timestamp', '')
                
                # Parser le timestamp
                try:
                    last_signal_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f")
                except:
                    last_signal_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
                
                # Si le dernier signal date de plus de 10 minutes, c'est suspect
                time_diff = (datetime.now() - last_signal_time).total_seconds()
                if time_diff > 600:  # 10 minutes
                    log_message(f"⚠️ Aucun signal depuis {int(time_diff/60)} minutes", "WARNING")
                    return False
                
                return True
    except Exception as e:
        log_message(f"Erreur lors de la vérification d'activité: {e}", "ERROR")
        return False

def start_bot():
    """Démarrer le bot"""
    log_message("🚀 Démarrage du bot de trading...", "INFO")
    
    try:
        # Utiliser l'environnement virtuel si disponible
        if sys.platform == 'win32':
            venv_python = SCRIPT_DIR / '.venv' / 'Scripts' / 'python.exe'
        else:
            venv_python = SCRIPT_DIR / '.venv' / 'bin' / 'python3'
        if venv_python.exists():
            python_exe = str(venv_python)
        else:
            python_exe = sys.executable
        
        bot_script = SCRIPT_DIR / "trading_bot.py"
        
        if not bot_script.exists():
            log_message("❌ trading_bot.py non trouvé!", "ERROR")
            return False
        
        # Lancer en arrière-plan
        CREATE_NO_WINDOW = 0x08000000
        DETACHED_PROCESS = 0x00000008
        
        if os.name == 'nt':  # Windows
            process = subprocess.Popen(
                [python_exe, str(bot_script)],
                creationflags=CREATE_NO_WINDOW | DETACHED_PROCESS,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
                cwd=str(SCRIPT_DIR),
                start_new_session=True
            )
        else:  # Linux/Mac
            process = subprocess.Popen(
                [python_exe, str(bot_script)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
                cwd=str(SCRIPT_DIR),
                start_new_session=True
            )
        
        log_message(f"Bot lancé (processus: {process.pid})", "INFO")
        
        # Attendre 15 secondes pour que le bot démarre et crée son PID
        time.sleep(15)
        
        if is_bot_running():
            log_message("✅ Bot démarré avec succès", "SUCCESS")
            return True
        else:
            log_message("❌ Échec du démarrage du bot (PID non créé ou crash immédiat)", "ERROR")
            return False
            
    except Exception as e:
        log_message(f"❌ Erreur lors du démarrage: {e}", "ERROR")
        return False

def restart_bot(status):
    """Redémarrer le bot avec vérifications"""
    now = datetime.now()
    
    # Vérifier le nombre de redémarrages dans la dernière heure
    recent_restarts = [
        r for r in status['restarts']
        if (now - datetime.fromisoformat(r)).total_seconds() < 3600
    ]
    
    if len(recent_restarts) >= MAX_RESTARTS_PER_HOUR:
        log_message(
            f"🛑 ALERTE: Trop de redémarrages ({len(recent_restarts)}) dans la dernière heure",
            "ERROR"
        )
        log_message("Le bot semble avoir un problème récurrent. Intervention manuelle requise.", "ERROR")
        return False
    
    log_message("🔄 Redémarrage du bot...", "WARNING")
    
    # Tuer le processus si nécessaire
    if BOT_PID_FILE.exists():
        try:
            with open(BOT_PID_FILE, 'r') as f:
                pid = int(f.read().strip())
            
            if psutil.pid_exists(pid):
                proc = psutil.Process(pid)
                proc.terminate()
                time.sleep(2)
                if proc.is_running():
                    proc.kill()
                    
            os.remove(BOT_PID_FILE)
        except Exception as e:
            log_message(f"Erreur lors de l'arrêt du processus: {e}", "WARNING")
    
    # Démarrer le bot
    if start_bot():
        status['restarts'].append(now.isoformat())
        status['total_restarts'] += 1
        status['last_restart'] = now.isoformat()
        save_watchdog_status(status)
        return True
    else:
        return False

def main():
    """Boucle principale du watchdog"""
    # Écrire le PID du watchdog
    try:
        with open(WATCHDOG_PID_FILE, 'w') as f:
            f.write(str(os.getpid()))
        log_message(f"Watchdog PID: {os.getpid()}", "INFO")
    except Exception as e:
        log_message(f"⚠️ Impossible d'écrire watchdog.pid: {e}", "WARNING")
    
    log_message("=" * 60, "INFO")
    log_message("Bot Watchdog demarre", "SUCCESS")
    log_message("=" * 60, "INFO")
    
    status = load_watchdog_status()
    consecutive_failures = 0
    error_count = 0  # Compteur d'erreurs dans la boucle
    
    try:
        while True:
            try:
                status['last_check'] = datetime.now().isoformat()

                # 🔧 Respect du flag de désactivation manuelle (bot.disabled)
                disabled_flag = SCRIPT_DIR / 'bot.disabled'
                if disabled_flag.exists():
                    log_message("⏸️ Bot désactivé manuellement (bot.disabled) — aucun redémarrage", "WARNING")
                    consecutive_failures = 0
                    save_watchdog_status(status)
                    time.sleep(CHECK_INTERVAL)
                    continue

                # Vérifier si le bot est actif
                if is_bot_running():
                    log_message("Bot actif", "SUCCESS")
                    consecutive_failures = 0
                    error_count = 0  # Reset sur succès
                else:
                    consecutive_failures += 1
                    log_message(f"Bot inactif (tentative {consecutive_failures})", "ERROR")
                    
                    # Redémarrer après 2 échecs consécutifs
                    if consecutive_failures >= 2:
                        if restart_bot(status):
                            consecutive_failures = 0
                        else:
                            log_message("⛔ Impossible de redémarrer le bot", "ERROR")
                
                # Sauvegarder le statut
                save_watchdog_status(status)
                
                # Attendre avant la prochaine vérification
                time.sleep(CHECK_INTERVAL)
                
            except Exception as e:
                error_count += 1
                log_message(f"Erreur dans check (#{error_count}): {e}", "ERROR")
                
                # Si trop d'erreurs consécutives, c'est critique
                if error_count >= 5:
                    log_message(f"CRITIQUE: {error_count} erreurs consecutives - Watchdog instable!", "ERROR")
                    log_message("Tentative de recuperation...", "WARNING")
                    error_count = 0  # Reset et continuer
                
                time.sleep(CHECK_INTERVAL)
                
    except KeyboardInterrupt:
        log_message("Watchdog arrete par l'utilisateur", "WARNING")
    except Exception as e:
        log_message(f"ERREUR FATALE WATCHDOG: {e}", "ERROR")
        import traceback
        log_message(traceback.format_exc(), "ERROR")
    finally:
        # Nettoyer le PID
        try:
            if WATCHDOG_PID_FILE.exists():
                os.remove(WATCHDOG_PID_FILE)
            log_message("Watchdog arrêté proprement", "INFO")
        except:
            pass

if __name__ == "__main__":
    main()
