#!/usr/bin/perl
# ============================================================
# XDEMD - Daemon de gestion de scripts distants
# Version : 1.1.2
# Auteur  : Généré pour projet XDEM
# Usage   : Lancé automatiquement par systemd en root
# ============================================================

use strict;
use warnings;
use POSIX qw(setsid strftime);
use File::Basename;
use File::Path qw(make_path);
use File::Copy;
use Digest::MD5 qw(md5_hex);
use LWP::UserAgent;
use HTTP::Request;
use JSON;
use Sys::Hostname;

# ============================================================
# CONSTANTES
# ============================================================
my $VERSION      = '1.1.2';
my $CONFIG_FILE  = '/etc/xdem/xdem.conf';
my $DAEMON_FILE  = '/usr/local/sbin/xdemd.pl';
my $HOSTNAME     = hostname();
my $RUSTDESK_ID  = '';

# ============================================================
# CHARGEMENT DE LA CONFIGURATION
# ============================================================
my %config = load_config($CONFIG_FILE);

# Répertoires essentiels
my $SCRIPTS_DIR = $config{scripts_dir} || '/etc/xdem/scripts';
my $LOGS_DIR    = $config{logs_dir}    || '/var/log/xdem';
my $LOG_FILE    = $config{log_file}    || '/var/log/xdem/xdemd.log';
my $LOCK_FILE   = $config{lock_file}   || '/var/run/xdemd.pid';

# Créer les répertoires si nécessaires
make_path($SCRIPTS_DIR, $LOGS_DIR) unless -d $SCRIPTS_DIR;

# ============================================================
# GESTION DU PID (éviter les doublons)
# ============================================================
if (-f $LOCK_FILE) {
    open(my $fh, '<', $LOCK_FILE) or die "Impossible de lire $LOCK_FILE";
    my $old_pid = <$fh>;
    close($fh);
    chomp $old_pid;
    if ($old_pid && kill(0, $old_pid)) {
        die "XDEMD est déjà en cours d'exécution (PID: $old_pid)\n";
    }
}

# Écrire le PID actuel
open(my $pid_fh, '>', $LOCK_FILE) or die "Impossible d'écrire $LOCK_FILE";
print $pid_fh $$;
close($pid_fh);

# Nettoyage du PID à la sortie
END {
    unlink $LOCK_FILE if -f $LOCK_FILE;
}

# Gestion des signaux
$SIG{TERM} = $SIG{INT} = sub {
    log_msg('INFO', "XDEMD arrêté proprement (signal reçu)");
    unlink $LOCK_FILE if -f $LOCK_FILE;
    exit 0;
};
$SIG{HUP} = sub {
    log_msg('INFO', "Signal HUP reçu — rechargement de la configuration");
    %config = load_config($CONFIG_FILE);
};

# ============================================================
# DÉMARRAGE
# ============================================================
log_msg('INFO', "=" x 60);
log_msg('INFO', "XDEMD v$VERSION démarré — Hostname: $HOSTNAME");
log_msg('INFO', "=" x 60);

# Heartbeat de démarrage
send_webhook('info', 'xdemd', 'started', "Daemon démarré — v$VERSION");

# Boucle principale
while (1) {
    log_msg('INFO', "--- Début du cycle de vérification ---");

    # 1. Vérifier mise à jour du daemon lui-même EN PREMIER
    update_daemon();

    # 2. Mettre à jour la configuration si nécessaire
    update_config();

    # 3. Recharger la config après éventuelle mise à jour
    %config = load_config($CONFIG_FILE);

    # 4. Récupérer l'ID RustDesk si disponible
    $RUSTDESK_ID = get_rustdesk_id();
    if ($RUSTDESK_ID) {
        log_msg('INFO', "RustDesk ID: $RUSTDESK_ID");
    } else {
        log_msg('DEBUG', "RustDesk non installé ou ID indisponible");
    }

    # 5. Heartbeat de cycle
    send_webhook('info', 'xdemd', 'heartbeat', "Cycle OK — v$VERSION");

    # 6. Lire le CSV et traiter les scripts
    process_csv();

    # 7. Attendre l'intervalle défini
    my $interval = ($config{check_interval} || 120) * 60;
    log_msg('INFO', "Prochain cycle dans $config{check_interval} minutes");
    sleep($interval);
}

# ============================================================
# SOUS-ROUTINES
# ============================================================

# ------------------------------------------------------------
# Chargement du fichier de configuration
# ------------------------------------------------------------
sub load_config {
    my ($file) = @_;
    my %cfg;
    open(my $fh, '<', $file) or do {
        warn "Impossible de lire $file: $!\n";
        return %cfg;
    };
    while (<$fh>) {
        chomp;
        next if /^\s*#/;    # ignorer commentaires
        next if /^\s*$/;    # ignorer lignes vides
        if (/^\s*(\w+)\s*=\s*(.+?)\s*$/) {
            $cfg{$1} = $2;
        }
    }
    close($fh);
    return %cfg;
}

# ------------------------------------------------------------
# Mise à jour automatique du daemon xdemd.pl lui-même
# ------------------------------------------------------------
sub update_daemon {
    my $remote_url = build_url($config{server_url}, '/xdem/xdemd.pl');
    my $temp_file  = "$DAEMON_FILE.download";

    log_msg('DEBUG', "Vérification mise à jour daemon: $remote_url");

    my $content = http_get($remote_url);
    unless (defined $content) {
        log_msg('WARN', "Impossible de récupérer xdemd.pl distant — mise à jour ignorée");
        return;
    }

    # Extraire la version distante
    my ($remote_ver) = $content =~ /^my\s+\$VERSION\s*=\s*'(.+?)'\s*;/m;
    $remote_ver //= '?';

    # Comparer les MD5 — logs explicites pour débugger
    my $md5_remote = md5_hex($content);
    my $md5_local  = file_md5($DAEMON_FILE);
    log_msg('DEBUG', "Daemon MD5 local=$md5_local distant=$md5_remote");
    log_msg('DEBUG', "Daemon version locale=$VERSION distante=$remote_ver");

    if ($md5_local eq $md5_remote) {
        log_msg('DEBUG', "xdemd.pl v$VERSION — aucun changement (MD5 identique)");
        return;
    }

    log_msg('INFO', "Nouvelle version du daemon: v$remote_ver (actuelle: v$VERSION)");

    # Écrire le fichier temporaire
    open(my $fh, '>', $temp_file) or do {
        log_msg('ERROR', "Impossible d'écrire $temp_file: $!");
        return;
    };
    print $fh $content;
    close($fh);
    chmod 0700, $temp_file;

    # Vérification syntaxique Perl avant de remplacer
    my $check = system("/usr/bin/perl -c $temp_file 2>/dev/null");
    if ($check != 0) {
        log_msg('ERROR', "Syntaxe invalide dans v$remote_ver — mise à jour annulée");
        send_webhook('error', 'xdemd.pl', 'daemon_update_failed',
            "Syntaxe invalide dans v$remote_ver");
        unlink $temp_file;
        return;
    }

    # Remplacer le daemon
    copy($temp_file, $DAEMON_FILE) or do {
        log_msg('ERROR', "Impossible de copier vers $DAEMON_FILE: $!");
        unlink $temp_file;
        return;
    };
    chmod 0700, $DAEMON_FILE;
    unlink $temp_file;

    log_msg('INFO', "Daemon mis a jour v$VERSION -> v$remote_ver — redemarrage dans 2s");
    send_webhook('info', 'xdemd.pl', 'daemon_updated',
        "Daemon mis a jour v$VERSION -> v$remote_ver");

    sleep(2);
    exec('/bin/systemctl', 'restart', 'xdemd')
        or log_msg('ERROR', "Impossible de redemarrer: $!");
}

# ------------------------------------------------------------
# Mise à jour automatique de xdem.conf depuis le serveur
# ------------------------------------------------------------
sub update_config {
    my $remote_url = build_url($config{server_url}, '/xdem/xdem.conf');
    my $local_file = $CONFIG_FILE;
    my $temp_file  = "$local_file.download";

    log_msg('DEBUG', "Vérification mise à jour config: $remote_url");

    my $content = http_get($remote_url);
    unless (defined $content) {
        log_msg('WARN', "Impossible de récupérer xdem.conf distant");
        return;
    }

    # Écrire le fichier temporaire
    write_file($temp_file, $content);

    # Comparer les MD5
    my $md5_remote = md5_hex($content);
    my $md5_local  = file_md5($local_file);

    if ($md5_local ne $md5_remote) {
        log_msg('INFO', "Nouvelle version de xdem.conf détectée — mise à jour");
        copy($temp_file, $local_file)
            or log_msg('ERROR', "Échec copie xdem.conf: $!");
        log_msg('INFO', "xdem.conf mis à jour avec succès");
        send_webhook('info', 'xdem.conf', 'updated', "Configuration mise à jour");
    } else {
        log_msg('DEBUG', "xdem.conf — aucun changement");
    }
    unlink $temp_file if -f $temp_file;
}

# ------------------------------------------------------------
# Lecture du CSV et traitement de chaque ligne
# ------------------------------------------------------------
sub process_csv {
    my $csv_url = build_url($config{server_url}, $config{csv_path});
    log_msg('INFO', "Récupération du CSV: $csv_url");

    my $content = http_get($csv_url);
    unless (defined $content) {
        log_msg('ERROR', "Impossible de récupérer le CSV");
        send_alert("Échec récupération CSV depuis $csv_url");
        return;
    }

    my @lines = split(/\r?\n/, $content);

    # Ignorer les commentaires (#) et la ligne d'entête (Langage;Type;...)
    # quelle que soit sa position dans le fichier
    @lines = grep {
        !/^\s*$/        &&   # pas de ligne vide
        !/^\s*#/        &&   # pas de commentaire
        !/^\s*Langage\s*;/i  # pas la ligne d'entête
    } @lines;

    log_msg('INFO', scalar(@lines) . " entrée(s) trouvée(s) dans le CSV");

    foreach my $line (@lines) {
        next if $line =~ /^\s*$/;
        next if $line =~ /^#/;

        # Parsing CSV avec séparateur ";"
        my ($langage, $type, $nom, $chemin, $frequence, $actif, $machines)
            = split(/;/, $line);

        # Nettoyage
        for ($langage, $type, $nom, $chemin, $frequence, $actif, $machines) {
            $_ //= '';
            s/^\s+|\s+$//g;
        }

        log_msg('DEBUG', "Traitement: $nom (actif=$actif, machines=$machines)");

        # Vérifier si actif
        if (lc($actif) ne 'oui') {
            log_msg('INFO', "Script $nom désactivé — ignoré");
            # Supprimer le cron si c'était un cron
            remove_cron($nom) if lc($type) eq 'cron';
            next;
        }

        # Vérifier si cette machine est ciblée
        unless (is_targeted($machines)) {
            log_msg('DEBUG', "Machine $HOSTNAME non ciblée pour $nom — ignoré");
            next;
        }

        # Traiter selon le type
        if (lc($type) eq 'script') {
            process_script($langage, $nom, $chemin, $frequence);
        } elsif (lc($type) eq 'cron') {
            process_cron($langage, $nom, $chemin, $frequence);
        } else {
            log_msg('WARN', "Type inconnu '$type' pour $nom");
        }
    }
}

# ------------------------------------------------------------
# Vérifier si cette machine est dans la liste cible
# ------------------------------------------------------------
sub is_targeted {
    my ($machines) = @_;
    return 1 if uc($machines) eq 'ALL';
    my @targets = split(/,/, $machines);
    for my $target (@targets) {
        $target =~ s/^\s+|\s+$//g;
        return 1 if lc($target) eq lc($HOSTNAME);
    }
    return 0;
}

# ------------------------------------------------------------
# Traitement d'un script : téléchargement + exécution
# ------------------------------------------------------------
sub process_script {
    my ($langage, $nom, $chemin, $frequence) = @_;

    my $script_path  = "$SCRIPTS_DIR/$nom";
    my $temp_path    = "$script_path.download";
    my $freq_file    = "$SCRIPTS_DIR/.freq_$nom";

    # Vérifier si l'heure d'exécution est venue
    unless (should_run($freq_file, $frequence)) {
        log_msg('DEBUG', "Script $nom : pas encore l'heure d'exécuter");
        return;
    }

    log_msg('INFO', "Traitement script: $nom");

    # Télécharger le script
    my $content = http_get($chemin);
    unless (defined $content) {
        log_msg('ERROR', "Impossible de télécharger $nom depuis $chemin");
        send_alert("Échec téléchargement: $nom");
        send_webhook('error', $nom, 'download_failed', "Impossible de télécharger $nom");
        return;
    }

    write_file($temp_path, $content);

    # Comparer MD5
    my $md5_remote = md5_hex($content);
    my $md5_local  = file_md5($script_path);
    my $updated    = 0;

    if ($md5_local ne $md5_remote) {
        log_msg('INFO', "Nouveau/modifié: $nom — mise à jour");
        copy($temp_path, $script_path)
            or do {
                log_msg('ERROR', "Échec copie $nom: $!");
                unlink $temp_path;
                return;
            };
        chmod 0700, $script_path;
        $updated = 1;
        send_webhook('info', $nom, 'updated', "Script mis à jour");
    } else {
        log_msg('DEBUG', "$nom — aucun changement (MD5 identique)");
    }

    unlink $temp_path if -f $temp_path;

    # Exécuter le script
    execute_script($langage, $nom, $script_path);

    # Mettre à jour le timestamp d'exécution
    write_file($freq_file, time());
}

# ------------------------------------------------------------
# Exécution d'un script
# ------------------------------------------------------------
sub execute_script {
    my ($langage, $nom, $path) = @_;

    unless (-f $path) {
        log_msg('ERROR', "Script introuvable: $path");
        return;
    }

    my $interpreter = get_interpreter($langage);
    my $cmd = "$interpreter $path >> $LOGS_DIR/${nom}.log 2>&1";

    log_msg('INFO', "Exécution: $cmd");
    my $ret = system($cmd);

    if ($ret == 0) {
        log_msg('INFO', "Script $nom exécuté avec succès (exit 0)");
        send_webhook('success', $nom, 'executed', "Exécution réussie");
    } else {
        my $exit_code = $ret >> 8;
        log_msg('ERROR', "Script $nom échoué (exit code: $exit_code)");
        send_alert("Échec exécution $nom (exit: $exit_code) sur $HOSTNAME");
        send_webhook('error', $nom, 'exec_failed', "Échec exécution exit=$exit_code");
    }
}

# ------------------------------------------------------------
# Traitement d'un cron : créer/mettre à jour /etc/cron.d/
# ------------------------------------------------------------
sub process_cron {
    my ($langage, $nom, $chemin, $frequence) = @_;

    log_msg('INFO', "Traitement cron: $nom");

    my $script_path = "$SCRIPTS_DIR/$nom";
    my $temp_path   = "$script_path.download";

    # Télécharger le script cron
    my $content = http_get($chemin);
    unless (defined $content) {
        log_msg('ERROR', "Impossible de télécharger cron $nom");
        send_webhook('error', $nom, 'download_failed', "Impossible de télécharger cron $nom");
        return;
    }

    write_file($temp_path, $content);

    my $md5_remote = md5_hex($content);
    my $md5_local  = file_md5($script_path);

    if ($md5_local ne $md5_remote) {
        log_msg('INFO', "Cron $nom mis à jour");
        copy($temp_path, $script_path);
        chmod 0700, $script_path;
        send_webhook('info', $nom, 'updated', "Script cron mis à jour");
    }
    unlink $temp_path if -f $temp_path;

    # Générer la règle crontab depuis la fréquence (en heures)
    # frequence = nombre d'heures entre chaque exécution
    my $cron_name = $nom;
    $cron_name =~ s/[^a-zA-Z0-9_-]/_/g;
    my $cron_file = "/etc/cron.d/xdem_$cron_name";
    my $interpreter = get_interpreter($langage);

    # Convertir fréquence heures en expression cron
    my $cron_expr = hours_to_cron($frequence);
    my $cron_rule = "$cron_expr root $interpreter $script_path >> $LOGS_DIR/${nom}.log 2>&1\n";

    # Vérifier si la règle a changé
    my $existing = '';
    if (-f $cron_file) {
        open(my $fh, '<', $cron_file);
        $existing = do { local $/; <$fh> };
        close($fh);
    }

    unless ($existing eq $cron_rule) {
        log_msg('INFO', "Mise à jour règle cron: $cron_file");
        write_file($cron_file, "# Géré par XDEMD — ne pas modifier manuellement\n$cron_rule");
        send_webhook('info', $nom, 'cron_updated', "Règle cron mise à jour: $cron_expr");
    }
}

# ------------------------------------------------------------
# Supprimer un cron désactivé
# ------------------------------------------------------------
sub remove_cron {
    my ($nom) = @_;
    my $cron_name = $nom;
    $cron_name =~ s/[^a-zA-Z0-9_-]/_/g;
    my $cron_file = "/etc/cron.d/xdem_$cron_name";
    if (-f $cron_file) {
        unlink $cron_file;
        log_msg('INFO', "Cron supprimé: $cron_file");
        send_webhook('info', $nom, 'cron_removed', "Règle cron supprimée (désactivé)");
    }
}

# ------------------------------------------------------------
# Convertir une fréquence en heures vers expression cron
# ------------------------------------------------------------
sub hours_to_cron {
    my ($hours) = @_;
    $hours = int($hours) || 24;
    if ($hours == 1)  { return "0 * * * *" }
    if ($hours <= 23) { return "0 */$hours * * *" }
    if ($hours == 24) { return "0 2 * * *" }      # tous les jours à 2h
    if ($hours == 48) { return "0 2 */2 * *" }    # tous les 2 jours
    if ($hours == 168){ return "0 2 * * 1" }      # toutes les semaines lundi
    # Cas générique
    my $days = int($hours / 24);
    return "0 2 */$days * *";
}

# ------------------------------------------------------------
# Vérifier si un script doit s'exécuter (basé sur fréquence)
# ------------------------------------------------------------
sub should_run {
    my ($freq_file, $frequence) = @_;
    $frequence = int($frequence) || 24;

    unless (-f $freq_file) {
        return 1;  # Jamais exécuté → on exécute
    }

    open(my $fh, '<', $freq_file) or return 1;
    my $last_run = <$fh>;
    close($fh);
    chomp $last_run;

    my $elapsed_hours = (time() - $last_run) / 3600;
    return $elapsed_hours >= $frequence;
}

# ------------------------------------------------------------
# Construire une URL avec token
# ------------------------------------------------------------
sub build_url {
    my ($base, $path) = @_;
    $base =~ s|/$||;
    $path =~ s|^/||;
    my $token = $config{token} || '';
    return "$base/$path?token=$token";
}

# ------------------------------------------------------------
# Requête HTTP GET
# ------------------------------------------------------------
sub http_get {
    my ($url) = @_;
    my $ua = LWP::UserAgent->new(timeout => 30);
    $ua->agent("XDEMD/$VERSION");
    # Accepter les certificats auto-signés si nécessaire
    # $ua->ssl_opts(verify_hostname => 0);

    my $response = $ua->get($url);
    if ($response->is_success) {
        return $response->content;
    } else {
        log_msg('WARN', "HTTP GET échoué pour $url : " . $response->status_line);
        return undef;
    }
}

# ------------------------------------------------------------
# Envoi webhook JSON vers le serveur
# ------------------------------------------------------------
sub send_webhook {
    my ($level, $script, $status, $message) = @_;

    my $webhook_url = $config{webhook_url} || '';
    return unless $webhook_url;

    my $token = $config{token} || '';
    my $url   = "$webhook_url?token=$token";

    my $payload = {
        hostname     => $HOSTNAME,
        timestamp    => strftime('%Y-%m-%dT%H:%M:%S', localtime),
        level        => $level,
        script       => $script,
        status       => $status,
        message      => $message,
        version      => $VERSION,
        version_conf => ($config{config_version} || ''),
        rustdesk_id  => $RUSTDESK_ID,
    };

    my $ua      = LWP::UserAgent->new(timeout => 10);
    my $request = HTTP::Request->new('POST', $url);
    $request->header('Content-Type' => 'application/json');
    $request->content(encode_json($payload));

    my $response = $ua->request($request);
    unless ($response->is_success) {
        log_msg('WARN', "Webhook échoué: " . $response->status_line);
    }
}

# ------------------------------------------------------------
# Récupérer l'ID RustDesk via rustdesk --get-id
# ------------------------------------------------------------
sub get_rustdesk_id {
    # Chercher rustdesk dans les chemins courants
    my $rustdesk_bin = '';
    for my $path (qw(/usr/bin/rustdesk /usr/local/bin/rustdesk /opt/rustdesk/rustdesk)) {
        if (-x $path) {
            $rustdesk_bin = $path;
            last;
        }
    }
    return '' unless $rustdesk_bin;

    # Exécuter rustdesk --get-id et capturer la sortie
    my $id = '';
    eval {
        local $SIG{ALRM} = sub { die "timeout\n" };
        alarm(10);  # timeout 10 secondes max
        $id = `$rustdesk_bin --get-id 2>/dev/null`;
        alarm(0);
    };
    alarm(0);

    # Nettoyer la sortie (enlever espaces, retours à la ligne)
    $id =~ s/^\s+|\s+$//g if $id;

    # Valider que c'est bien un ID numérique RustDesk (9-10 chiffres)
    return $id if $id && $id =~ /^\d{6,12}$/;
    return '';
}

# ------------------------------------------------------------
# Envoi d'une alerte email (via sendmail/mail)
# ------------------------------------------------------------
sub send_alert {
    my ($message) = @_;
    my $email = $config{alert_email} || '';
    return unless $email;

    my $subject = "[XDEMD] Alerte sur $HOSTNAME";
    my $body    = "Horodatage : " . strftime('%Y-%m-%d %H:%M:%S', localtime) . "\n"
                . "Machine    : $HOSTNAME\n"
                . "Message    : $message\n"
                . "\n-- XDEMD v$VERSION";

    # Utiliser sendmail si disponible
    if (-x '/usr/sbin/sendmail') {
        open(my $mail, '|-', "/usr/sbin/sendmail -t") or return;
        print $mail "To: $email\n";
        print $mail "Subject: $subject\n";
        print $mail "Content-Type: text/plain; charset=UTF-8\n\n";
        print $mail $body;
        close($mail);
    } elsif (-x '/usr/bin/mail') {
        open(my $mail, '|-', "/usr/bin/mail -s '$subject' $email") or return;
        print $mail $body;
        close($mail);
    } else {
        log_msg('WARN', "Aucun outil mail disponible — alerte non envoyée: $message");
    }
}

# ------------------------------------------------------------
# Déterminer l'interpréteur selon le langage
# ------------------------------------------------------------
sub get_interpreter {
    my ($langage) = @_;
    my %interpreters = (
        perl   => '/usr/bin/perl',
        bash   => '/bin/bash',
        sh     => '/bin/sh',
        python => '/usr/bin/python3',
        ruby   => '/usr/bin/ruby',
    );
    return $interpreters{lc($langage)} || '/bin/bash';
}

# ------------------------------------------------------------
# Calculer le MD5 d'un fichier local
# ------------------------------------------------------------
sub file_md5 {
    my ($file) = @_;
    return '' unless -f $file;
    open(my $fh, '<', $file) or return '';
    binmode($fh);
    my $md5 = Digest::MD5->new->addfile($fh)->hexdigest;
    close($fh);
    return $md5;
}

# ------------------------------------------------------------
# Écrire du contenu dans un fichier
# ------------------------------------------------------------
sub write_file {
    my ($path, $content) = @_;
    open(my $fh, '>', $path) or do {
        log_msg('ERROR', "Impossible d'écrire $path: $!");
        return;
    };
    print $fh $content;
    close($fh);
}

# ------------------------------------------------------------
# Logger un message horodaté
# ------------------------------------------------------------
sub log_msg {
    my ($level, $message) = @_;

    my $log_level = $config{log_level} || 'INFO';
    my %levels = (DEBUG => 0, INFO => 1, WARN => 2, ERROR => 3);

    return if ($levels{$level} // 1) < ($levels{$log_level} // 1);

    my $timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime);
    my $line      = "[$timestamp] [$level] $message\n";

    # Écriture dans le fichier de log
    open(my $fh, '>>', $LOG_FILE) or warn "Log impossible: $!";
    print $fh $line;
    close($fh) if $fh;

    # Aussi sur STDOUT (visible dans journalctl)
    print $line;
    print STDOUT $line;
}
