#!/usr/bin/env php
<?php
/**
 * Master-Cronjob: Läuft in Endlosschleife mit 1 Sekunde Sleep.
 * Liest aus der DB, welche Jobs aus wecker/cronjob/ in welchem Intervall laufen sollen,
 * und startet sie bei Bedarf.
 *
 * Start (im Hintergrund):
 *   php /pfad/zu/wecker/cronjob.php
 *
 * Crontab (läuft dann z.B. beim Boot):
 *   @reboot /usr/bin/php /var/www/wecker/cronjob.php
 */
$weckerRoot = __DIR__;
$config = $weckerRoot . '/config.php';
if (!is_file($config)) {
    fwrite(STDERR, "config.php nicht gefunden: " . $config . "\n");
    exit(1);
}

require $config;
require $weckerRoot . '/functions/functions_db.php';
require $weckerRoot . '/functions/functions_wecker.php';
require $weckerRoot . '/functions/functions_common.php';

$cronjobDir = $weckerRoot . '/cronjob';
if (!is_dir($cronjobDir)) {
    fwrite(STDERR, "Cronjob-Ordner nicht gefunden: " . $cronjobDir . "\n");
    exit(1);
}

$pdo = $GLOBALS['pdo'] ?? null;
if (!$pdo) {
    fwrite(STDERR, "Keine DB-Verbindung.\n");
    exit(1);
}

$storageRoot = defined('STORAGE_ROOT') ? rtrim(STORAGE_ROOT, DIRECTORY_SEPARATOR) : $weckerRoot . DIRECTORY_SEPARATOR . 'storage';
$cronPidFile  = $storageRoot . DIRECTORY_SEPARATOR . 'cronjob_master.pid';
$cronStateFile = $storageRoot . DIRECTORY_SEPARATOR . 'cronjob_master.json';
$masterPid = getmypid();

/** Prüfen ob ein Prozess (PID) noch läuft (Linux/Unix) */
function cron_process_laeuft(int $pid): bool {
    if ($pid <= 0) return false;
    if (function_exists('posix_kill')) {
        return @posix_kill($pid, 0);
    }
    if (PHP_OS_FAMILY === 'Windows') {
        $out = [];
        @exec('tasklist /FI "PID eq ' . $pid . '" 2>NUL', $out);
        return !empty($out) && strpos(implode('', $out), (string) $pid) !== false;
    }
    $proc = '/proc/' . $pid;
    return is_dir($proc);
}

/** Aktuellen Master in PID- und State-Datei schreiben (bei Start oder Übernahme) */
function cron_master_schreiben(string $pidFile, string $stateFile, int $pid): void {
    $dir = dirname($pidFile);
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    @file_put_contents($pidFile, (string) $pid);
    $state = [
        'pid'           => $pid,
        'started_at'    => date('c'),
        'last_activity' => date('c'),
        'takeover'      => true,
    ];
    @file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE));
}

/** Letzte Aktivität in State-Datei aktualisieren (wird jede Sekunde aus der Schleife aufgerufen) */
function cron_master_activity_tick(string $stateFile): void {
    if (!is_file($stateFile)) {
        return;
    }
    $content = @file_get_contents($stateFile);
    if ($content === false || $content === '') {
        return;
    }
    $state = @json_decode($content, true);
    if (!is_array($state)) {
        return;
    }
    $state['last_activity'] = date('c');
    @file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE));
}

/** Prüfen ob ein neuer Master uns abgelöst hat; dann beenden. Gibt true zurück wenn wir weiterlaufen sollen. */
function cron_master_pruefen(string $pidFile, string $stateFile, int $myPid): bool {
    if (!is_file($pidFile)) {
        cron_master_schreiben($pidFile, $stateFile, $myPid);
        return true;
    }
    $content = @file_get_contents($pidFile);
    $otherPid = trim($content) !== '' ? (int) trim($content) : 0;
    if ($otherPid === $myPid) {
        return true;
    }
    if ($otherPid > 0 && cron_process_laeuft($otherPid)) {
        return false;
    }
    cron_master_schreiben($pidFile, $stateFile, $myPid);
    return true;
}

cron_master_schreiben($cronPidFile, $cronStateFile, $masterPid);
$masterCheckCounter = 0;

/** Aktivierte Cronjobs aus DB holen (Name, Intervall, letzte Ausführung) */
function cronjobs_laden(PDO $pdo): array {
    $stmt = $pdo->query("
        SELECT id, name, interval_sekunden, letzte_ausfuehrung
        FROM cronjobs
        WHERE aktiv = 1 AND interval_sekunden > 0
        ORDER BY id
    ");
    return $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
}

/** Letzte Ausführung auf jetzt setzen */
function cronjob_ausfuehrung_setzen(PDO $pdo, int $id): void {
    $stmt = $pdo->prepare("UPDATE cronjobs SET letzte_ausfuehrung = NOW() WHERE id = ?");
    $stmt->execute([$id]);
}

/** Einzelnen Cronjob-Skript ausführen (inkl. Datei-Check) */
function cronjob_ausfuehren(string $cronjobDir, string $name): bool {
    $safeName = preg_replace('/[^a-z0-9_-]/', '', strtolower($name));
    if ($safeName === '') {
        return false;
    }
    $script = $cronjobDir . '/' . $safeName . '.php';
    if (!is_file($script)) {
        return false;
    }
    try {
        include $script;
        return true;
    } catch (Throwable $e) {
        error_log('[cronjob ' . $safeName . '] ' . $e->getMessage());
        return false;
    }
}

// Endlosschleife: jede Sekunde prüfen, welche Jobs fällig sind
while (true) {
    $masterCheckCounter++;
    if ($masterCheckCounter >= 30) {
        $masterCheckCounter = 0;
        if (!cron_master_pruefen($cronPidFile, $cronStateFile, $masterPid)) {
            exit(0);
        }
    }
    try {
        $jobs = cronjobs_laden($pdo);
        $now = time();
        foreach ($jobs as $job) {
            $last = $job['letzte_ausfuehrung'] ? strtotime($job['letzte_ausfuehrung']) : 0;
            $interval = max(1, (int) $job['interval_sekunden']);
            if ($now - $last >= $interval) {
                cronjob_ausfuehren($cronjobDir, $job['name']);
                cronjob_ausfuehrung_setzen($pdo, (int) $job['id']);
            }
        }
    } catch (Throwable $e) {
        error_log('[cronjob master] ' . $e->getMessage());
    }
    cron_master_activity_tick($cronStateFile);
    sleep(1);
}
