#!/usr/bin/env python3
"""
IPWatch MQTT Agent
Agent à installer sur chaque machine pour recevoir les commandes shutdown/reboot via MQTT

Installation:
    pip install paho-mqtt psutil netifaces

Configuration:
    Créer /etc/ipwatch/mqtt-agent.conf avec:
    [mqtt]
    broker = localhost
    port = 1883
    username =
    password =

    [agent]
    hostname = auto
    check_interval = 30
"""

import paho.mqtt.client as mqtt
import platform
import os
import sys
import subprocess
import json
import time
import socket
import configparser
import logging
from datetime import datetime
from pathlib import Path

# Configuration du logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/var/log/ipwatch-mqtt-agent.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger('ipwatch-mqtt-agent')


class IPWatchMQTTAgent:
    """Agent MQTT pour recevoir les commandes de contrôle système"""

    def __init__(self, config_file='/etc/ipwatch/mqtt-agent.conf'):
        self.config = self.load_config(config_file)
        self.hostname = self.get_hostname()
        self.ip_address = self.get_ip_address()

        # Topics MQTT
        self.base_topic = f"ipwatch/device/{self.ip_address}"
        self.command_topic = f"{self.base_topic}/command"
        self.status_topic = f"{self.base_topic}/status"
        self.availability_topic = f"{self.base_topic}/availability"

        # Client MQTT
        self.client = mqtt.Client(client_id=f"ipwatch-agent-{self.hostname}")
        self.client.on_connect = self.on_connect
        self.client.on_message = self.on_message
        self.client.on_disconnect = self.on_disconnect

        # Will message (si l'agent se déconnecte brutalement)
        self.client.will_set(
            self.availability_topic,
            payload="offline",
            qos=1,
            retain=True
        )

    def load_config(self, config_file):
        """Charge la configuration depuis le fichier"""
        config = configparser.ConfigParser()

        if not Path(config_file).exists():
            logger.warning(f"Fichier de configuration {config_file} introuvable, utilisation des valeurs par défaut")
            return {
                'broker': 'localhost',
                'port': 1883,
                'username': None,
                'password': None,
                'check_interval': 30
            }

        config.read(config_file)

        return {
            'broker': config.get('mqtt', 'broker', fallback='localhost'),
            'port': config.getint('mqtt', 'port', fallback=1883),
            'username': config.get('mqtt', 'username', fallback=None),
            'password': config.get('mqtt', 'password', fallback=None),
            'check_interval': config.getint('agent', 'check_interval', fallback=30)
        }

    def get_hostname(self):
        """Récupère le hostname de la machine"""
        return platform.node()

    def get_ip_address(self):
        """Récupère l'adresse IP principale de la machine"""
        try:
            # Créer une socket pour déterminer l'IP locale
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except Exception as e:
            logger.error(f"Erreur récupération IP: {e}")
            return "127.0.0.1"

    def on_connect(self, client, userdata, flags, rc):
        """Callback lors de la connexion au broker MQTT"""
        if rc == 0:
            logger.info(f"✓ Connecté au broker MQTT {self.config['broker']}:{self.config['port']}")

            # S'abonner au topic des commandes
            client.subscribe(self.command_topic)
            client.subscribe(f"ipwatch/device/+/command")  # Pour broadcast
            logger.info(f"✓ Abonné à {self.command_topic}")

            # Publier la disponibilité
            client.publish(self.availability_topic, "online", qos=1, retain=True)

            # Publier le statut initial
            self.publish_status()
        else:
            logger.error(f"✗ Échec connexion MQTT, code: {rc}")

    def on_disconnect(self, client, userdata, rc):
        """Callback lors de la déconnexion"""
        if rc != 0:
            logger.warning(f"⚠ Déconnexion inattendue du broker MQTT (code {rc})")

    def on_message(self, client, userdata, msg):
        """Callback lors de la réception d'un message"""
        try:
            payload = msg.payload.decode('utf-8')
            logger.info(f"→ Message reçu sur {msg.topic}: {payload}")

            # Parser le message JSON
            try:
                command_data = json.loads(payload)
                command = command_data.get('command', payload)  # Support format simple ou JSON
            except json.JSONDecodeError:
                command = payload  # Format texte simple

            # Exécuter la commande
            self.execute_command(command)

        except Exception as e:
            logger.error(f"✗ Erreur traitement message: {e}")

    def execute_command(self, command):
        """Exécute une commande système"""
        logger.info(f"⚙ Exécution commande: {command}")

        try:
            if command == "shutdown":
                self.shutdown()
            elif command == "reboot":
                self.reboot()
            elif command == "status":
                self.publish_status()
            else:
                logger.warning(f"⚠ Commande inconnue: {command}")
                self.publish_response(f"Commande inconnue: {command}", success=False)

        except Exception as e:
            logger.error(f"✗ Erreur exécution commande: {e}")
            self.publish_response(str(e), success=False)

    def shutdown(self):
        """Éteint la machine"""
        logger.warning("🔴 Shutdown demandé, arrêt dans 5 secondes...")

        self.publish_response("Shutdown en cours...", success=True)
        time.sleep(1)

        # Publier offline avant l'arrêt
        self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
        time.sleep(1)

        # Commande d'arrêt selon l'OS
        if platform.system() == "Windows":
            subprocess.run(["shutdown", "/s", "/t", "5"])
        else:
            subprocess.run(["sudo", "shutdown", "-h", "+0"])

    def reboot(self):
        """Redémarre la machine"""
        logger.warning("🔄 Reboot demandé, redémarrage dans 5 secondes...")

        self.publish_response("Reboot en cours...", success=True)
        time.sleep(1)

        # Publier offline avant le redémarrage
        self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
        time.sleep(1)

        # Commande de redémarrage selon l'OS
        if platform.system() == "Windows":
            subprocess.run(["shutdown", "/r", "/t", "5"])
        else:
            subprocess.run(["sudo", "reboot"])

    def publish_status(self):
        """Publie le statut de la machine"""
        try:
            import psutil

            status = {
                "hostname": self.hostname,
                "ip": self.ip_address,
                "platform": platform.system(),
                "platform_version": platform.version(),
                "uptime": time.time() - psutil.boot_time(),
                "cpu_percent": psutil.cpu_percent(interval=1),
                "memory_percent": psutil.virtual_memory().percent,
                "disk_percent": psutil.disk_usage('/').percent,
                "timestamp": datetime.now().isoformat()
            }

            self.client.publish(
                self.status_topic,
                json.dumps(status),
                qos=1,
                retain=False
            )
            logger.info(f"✓ Statut publié")

        except ImportError:
            logger.warning("psutil non installé, statut limité")
            status = {
                "hostname": self.hostname,
                "ip": self.ip_address,
                "timestamp": datetime.now().isoformat()
            }
            self.client.publish(self.status_topic, json.dumps(status), qos=1)

    def publish_response(self, message, success=True):
        """Publie une réponse sur le topic de statut"""
        response = {
            "success": success,
            "message": message,
            "timestamp": datetime.now().isoformat()
        }
        self.client.publish(
            f"{self.base_topic}/response",
            json.dumps(response),
            qos=1
        )

    def run(self):
        """Démarre l'agent"""
        try:
            # Authentification si configurée
            if self.config['username'] and self.config['password']:
                self.client.username_pw_set(
                    self.config['username'],
                    self.config['password']
                )

            # Connexion au broker
            logger.info(f"→ Connexion au broker MQTT {self.config['broker']}:{self.config['port']}...")
            self.client.connect(
                self.config['broker'],
                self.config['port'],
                keepalive=60
            )

            # Démarrer la boucle MQTT
            self.client.loop_start()

            # Publier le statut périodiquement
            logger.info(f"✓ Agent IPWatch MQTT démarré sur {self.ip_address}")
            logger.info(f"  Topics: {self.command_topic} | {self.status_topic}")

            while True:
                time.sleep(self.config['check_interval'])
                self.publish_status()

        except KeyboardInterrupt:
            logger.info("\n→ Arrêt demandé par l'utilisateur")
            self.stop()
        except Exception as e:
            logger.error(f"✗ Erreur fatale: {e}")
            self.stop()
            sys.exit(1)

    def stop(self):
        """Arrête l'agent proprement"""
        logger.info("→ Arrêt de l'agent...")
        self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
        self.client.loop_stop()
        self.client.disconnect()
        logger.info("✓ Agent arrêté")


def main():
    """Point d'entrée principal"""
    import argparse

    parser = argparse.ArgumentParser(description='IPWatch MQTT Agent')
    parser.add_argument(
        '-c', '--config',
        default='/etc/ipwatch/mqtt-agent.conf',
        help='Fichier de configuration (défaut: /etc/ipwatch/mqtt-agent.conf)'
    )
    parser.add_argument(
        '--test',
        action='store_true',
        help='Mode test (affiche la config et quitte)'
    )

    args = parser.parse_args()

    agent = IPWatchMQTTAgent(config_file=args.config)

    if args.test:
        print(f"Configuration:")
        print(f"  Broker: {agent.config['broker']}:{agent.config['port']}")
        print(f"  Hostname: {agent.hostname}")
        print(f"  IP: {agent.ip_address}")
        print(f"  Command topic: {agent.command_topic}")
        print(f"  Status topic: {agent.status_topic}")
        return

    agent.run()


if __name__ == "__main__":
    main()
