<?php
declare(strict_types=1);

// Only allow CLI execution
if (php_sapi_name() !== 'cli') {
    http_response_code(403);
    exit('CLI only');
}

define('BASE_PATH',   __DIR__);
define('DATA_PATH',   __DIR__ . '/data');
define('UPLOAD_PATH', __DIR__ . '/data/uploads');
define('DB_PATH',     __DIR__ . '/data/database.sqlite');
define('APP_BASE',    '');

// Concurrency lock — prevents overlapping cron runs from sending duplicate
// reminders, racing on settings, or triggering two parallel auto-updates.
$_lockFile = is_dir(DATA_PATH) ? DATA_PATH . '/cron.lock' : sys_get_temp_dir() . '/plenvo_cron.lock';
$_lockFh   = fopen($_lockFile, 'c');
if ($_lockFh === false || !flock($_lockFh, LOCK_EX | LOCK_NB)) {
    echo 'Anderer Cron-Lauf aktiv – beende.' . PHP_EOL;
    exit(0);
}
register_shutdown_function(function () use ($_lockFh) {
    @flock($_lockFh, LOCK_UN);
    @fclose($_lockFh);
});

// APP_VERSION wird für Updater benötigt — robust per Regex aus index.php
$_indexSrc = @file_get_contents(__DIR__ . '/index.php') ?: '';
preg_match("/define\('APP_VERSION',\s*'([^']+)'\)/", $_indexSrc, $_m);
define('APP_VERSION', $_m[1] ?? 'unknown');
unset($_indexSrc, $_m);

require_once BASE_PATH . '/vendor/autoload.php';

foreach (['Database', 'Migrator', 'Auth', 'Lang', 'Helper', 'Mailer', 'TOTP', 'Updater', 'SecurityLog', 'HealthCheck', 'PluginManager', 'Crypto', 'InactiveUserCleanup', 'BundledPluginInstaller', 'Backup'] as $class) {
    require_once BASE_PATH . '/src/' . $class . '.php';
}

// Drain the bundled-plugins staging dir before loading plugins. Same rationale
// as in index.php: legacy updaters skip plugins/, so official plugins ride
// piggyback under _bundled-plugins/.
BundledPluginInstaller::run();
require_once BASE_PATH . '/controllers/SettingsController.php';

if (!file_exists(DB_PATH)) {
    echo "Datenbank nicht gefunden – Installation ausstehend.\n";
    exit(1);
}

Lang::load('de');

$db = Database::getInstance();
$db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('cron_last_run',?)", [(string)time()]);

// Plugins must be loaded after Database so they can read settings, but before
// any work that may depend on filters/hooks they register.
PluginManager::load();
// Plugin tick is wrapped: a misbehaving third-party plugin must not abort
// the whole cron run and starve mailer reminders / cleanup tasks.
try {
    PluginManager::fire('cron_tick', ['at' => time()]);
} catch (\Throwable $e) {
    error_log('[Plenvo cron] plugin tick threw: ' . $e->getMessage());
}

// Inactive-account cleanup. Same defensive wrap so a fluky run cannot
// starve the rest of the cron pipeline.
try {
    InactiveUserCleanup::runIfDue();
} catch (\Throwable $e) {
    error_log('[Plenvo cron] inactive-cleanup threw: ' . $e->getMessage());
}

// Run any pending versioned migrations (failsafe if web boot did not catch them)
$migLog = Migrator::run();
foreach ($migLog as $m) {
    echo '[' . date('Y-m-d H:i:s') . '] Migration ' . $m['id'] . ': ' . ($m['ok'] ? 'OK' : 'FAILED – ' . ($m['error'] ?? '')) . PHP_EOL;
}

// Drain the outbound mail queue first. Web requests park notifications
// in the `mail_queue` table to keep request latency low; cron is the
// authoritative dispatcher. Wrapped in try/catch so a transient SMTP
// failure cannot abort the rest of the cron run.
echo '[' . date('Y-m-d H:i:s') . '] Verarbeite Mail-Queue...' . PHP_EOL;
try {
    $mq = Mailer::processQueue();
    if ($mq['sent'] || $mq['failed'] || $mq['gave_up']) {
        echo '[' . date('Y-m-d H:i:s') . '] Mail-Queue: ' . $mq['sent'] . ' verschickt, ' . $mq['failed'] . ' fehlgeschlagen (Retry), ' . $mq['gave_up'] . ' aufgegeben' . PHP_EOL;
    }
} catch (\Throwable $e) {
    error_log('[Plenvo cron] mail-queue dispatch threw: ' . $e->getMessage());
}

echo '[' . date('Y-m-d H:i:s') . '] Sende 3-Tage-Erinnerungen...' . PHP_EOL;
Mailer::sendReminders();

echo '[' . date('Y-m-d H:i:s') . '] Sende Fälligkeitstag-Erinnerungen...' . PHP_EOL;
Mailer::sendDueDayReminders();

echo '[' . date('Y-m-d H:i:s') . '] Sende Überfälligkeits-Benachrichtigungen...' . PHP_EOL;
Mailer::sendOverdueNotifications();

echo '[' . date('Y-m-d H:i:s') . '] Sende Startdatum-Benachrichtigungen...' . PHP_EOL;
Mailer::sendStartDateNotifications();

echo '[' . date('Y-m-d H:i:s') . '] Bereinige alte Benachrichtigungen...' . PHP_EOL;
$db->execute(
    "DELETE FROM notifications WHERE is_read = 1 AND created_at < datetime('now', '-30 days')"
);

// ── Unverified user cleanup ──────────────────────────────────────────────────
$maxHours = (int)($db->fetch("SELECT value FROM settings WHERE key='unverified_user_max_hours'")['value'] ?? 48);
if ($maxHours > 0) {
    $cutoff   = date('Y-m-d H:i:s', time() - $maxHours * 3600);
    $stale    = $db->fetchAll(
        "SELECT id, email FROM users
         WHERE is_active = 0 AND email_verify_token IS NOT NULL AND created_at < ?",
        [$cutoff]
    );
    foreach ($stale as $u) {
        SettingsController::deleteUser($db, (int)$u['id']);
        SecurityLog::log('unverified_user_purged', SecurityLog::SEV_INFO, [
            'target_user_id' => (int)$u['id'],
            'email_hint'     => Helper::maskEmail($u['email'] ?? ''),
            'after_hours'    => $maxHours,
        ]);
    }
    if (!empty($stale)) {
        echo '[' . date('Y-m-d H:i:s') . '] Unverified-User-Cleanup: ' . count($stale) . ' Konto(en) entfernt (älter als ' . $maxHours . ' h).' . PHP_EOL;
    }
}

$retentionDays = (int)($db->fetch("SELECT value FROM settings WHERE key='security_log_retention_days'")['value'] ?? 365);
if ($retentionDays > 0) {
    $deleted = SecurityLog::pruneOlderThan($retentionDays);
    echo '[' . date('Y-m-d H:i:s') . '] Security-Log Pruning: ' . $deleted . ' Eintrag/Einträge entfernt (Retention ' . $retentionDays . ' Tage).' . PHP_EOL;
}

// ── Backup-Aufbewahrung ──────────────────────────────────────────────────────
$backupRetention = (int)($db->fetch("SELECT value FROM settings WHERE key='backup_retention_days'")['value'] ?? 0);
if ($backupRetention > 0) {
    $removedBackups = Backup::pruneOlderThan($backupRetention);
    if ($removedBackups > 0) {
        echo '[' . date('Y-m-d H:i:s') . '] Backup-Pruning: ' . $removedBackups . ' Backup(s) entfernt (älter als ' . $backupRetention . ' Tage).' . PHP_EOL;
    }
}

// ── Automatische Backups ─────────────────────────────────────────────────────
// Run within a 240-minute window after the configured slot, at most once per
// scheduled period. Frequency = daily | weekly (only on configured weekday) |
// monthly (only on configured day-of-month, 1..28). The `auto_backup_last_run`
// setting holds the YYYY-MM-DD of the last successful run; equality with
// today's date suppresses re-runs within the same period.
$bkAutoEnabled = ($db->fetch("SELECT value FROM settings WHERE key='auto_backup_enabled'")['value'] ?? '0') === '1';
if ($bkAutoEnabled) {
    $bkFreq    =     ($db->fetch("SELECT value FROM settings WHERE key='auto_backup_frequency'")['value']     ?? 'daily');
    $bkTime    =     ($db->fetch("SELECT value FROM settings WHERE key='auto_backup_time'")['value']          ?? '03:00');
    $bkWday    = (int)($db->fetch("SELECT value FROM settings WHERE key='auto_backup_weekday'")['value']      ?? 0);
    $bkDom     = (int)($db->fetch("SELECT value FROM settings WHERE key='auto_backup_day_of_month'")['value'] ?? 1);
    $bkLastRun =     ($db->fetch("SELECT value FROM settings WHERE key='auto_backup_last_run'")['value']      ?? '');
    [$slotH, $slotM] = array_map('intval', array_pad(explode(':', $bkTime), 2, 0));
    $slotMinutes = $slotH * 60 + $slotM;
    $nowMinutes  = (int)date('G') * 60 + (int)date('i');
    $windowMin   = 240;
    $inWindow    = ($nowMinutes >= $slotMinutes && $nowMinutes <= $slotMinutes + $windowMin);
    $today       = date('Y-m-d');
    $shouldRun   = false;
    if ($inWindow && $bkLastRun !== $today) {
        if ($bkFreq === 'daily') {
            $shouldRun = true;
        } elseif ($bkFreq === 'weekly' && (int)date('w') === $bkWday) {
            $shouldRun = true;
        } elseif ($bkFreq === 'monthly' && (int)date('j') === $bkDom) {
            $shouldRun = true;
        }
    }
    if ($shouldRun) {
        echo '[' . date('Y-m-d H:i:s') . '] Auto-Backup wird erstellt (Frequenz: ' . $bkFreq . ')...' . PHP_EOL;
        try {
            $passphrase = Backup::defaultPassphrase();
            $bkResult   = Backup::create($passphrase);
            if ($bkResult['ok']) {
                $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_backup_last_run',?)",  [$today]);
                $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_backup_last_error',?)", ['']);
                $sizeMb = round(($bkResult['size'] ?? 0) / 1048576, 1);
                echo '[' . date('Y-m-d H:i:s') . '] ✓ Auto-Backup erstellt: ' . $bkResult['filename']
                   . ' (' . $sizeMb . ' MB' . ($bkResult['encrypted'] ? ', verschlüsselt' : '') . ').' . PHP_EOL;
                SecurityLog::log('auto_backup_created', SecurityLog::SEV_WARN, [
                    'filename'  => $bkResult['filename'],
                    'size'      => (int)($bkResult['size'] ?? 0),
                    'encrypted' => $bkResult['encrypted'] ? 1 : 0,
                    'frequency' => $bkFreq,
                ]);
            } else {
                $err = (string)($bkResult['error'] ?? 'unknown');
                $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_backup_last_run',?)",  [$today]);
                $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_backup_last_error',?)", [$err]);
                echo '[' . date('Y-m-d H:i:s') . '] ✗ Auto-Backup fehlgeschlagen: ' . $err . PHP_EOL;
                SecurityLog::log('auto_backup_failed', SecurityLog::SEV_CRITICAL, ['reason' => $err, 'frequency' => $bkFreq]);
            }
        } catch (\Throwable $e) {
            $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_backup_last_error',?)", [$e->getMessage()]);
            error_log('[Plenvo cron] auto-backup threw: ' . $e->getMessage());
        }
    }
}

// ── Tägliche Update-Prüfung ──────────────────────────────────────────────────
echo '[' . date('Y-m-d H:i:s') . '] Prüfe auf neue Plenvo-Version...' . PHP_EOL;
$lastUpdateCheck = (int)($db->fetch("SELECT value FROM settings WHERE key='update_cron_last_check'")['value'] ?? 0);
if ((time() - $lastUpdateCheck) >= 86400) {
    $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('update_cron_last_check',?)", [(string)time()]);
    $updateInfo = Updater::getRemoteInfo(true);
    Updater::notifyAdminsIfNewVersion($updateInfo);
    if ($updateInfo && version_compare($updateInfo['version'], APP_VERSION, '>')) {
        echo '[' . date('Y-m-d H:i:s') . '] Update ' . $updateInfo['version'] . ' verfügbar.' . PHP_EOL;
    } else {
        echo '[' . date('Y-m-d H:i:s') . '] Kein Update verfügbar.' . PHP_EOL;
    }
}

// ── Geplantes automatisches Update ───────────────────────────────────────────
$autoEnabled = ($db->fetch("SELECT value FROM settings WHERE key='auto_update_enabled'")['value'] ?? '0') === '1';
$autoAllowed = ($db->fetch("SELECT value FROM settings WHERE key='updates_enabled'")['value'] ?? '1') === '1';
if ($autoEnabled && $autoAllowed) {
    $autoTime    = $db->fetch("SELECT value FROM settings WHERE key='auto_update_time'")['value'] ?? '03:00';
    $lastAutoRun = $db->fetch("SELECT value FROM settings WHERE key='auto_update_last_run'")['value'] ?? '';
    $today       = date('Y-m-d');
    // Only run if we are within a window after the configured slot, so a missed
    // 03:00 schedule does not surprise the operator with an unexpected install
    // at 14:30 once cron comes back online.
    $windowMin   = max(15, (int)($db->fetch("SELECT value FROM settings WHERE key='auto_update_window_minutes'")['value'] ?? 240));
    [$slotH, $slotM] = array_map('intval', array_pad(explode(':', $autoTime), 2, 0));
    $slotMinutes = $slotH * 60 + $slotM;
    $nowMinutes  = (int)date('G') * 60 + (int)date('i');
    $inWindow    = ($nowMinutes >= $slotMinutes && $nowMinutes <= $slotMinutes + $windowMin);
    $pastWindow  = ($nowMinutes > $slotMinutes + $windowMin);

    if ($lastAutoRun !== $today && $inWindow) {
        echo '[' . date('Y-m-d H:i:s') . '] Geplantes Auto-Update wird geprüft (Slot ' . $autoTime . ', Fenster ' . $windowMin . ' min)...' . PHP_EOL;
        $info = Updater::getRemoteInfo(true);
        if ($info && version_compare($info['version'], APP_VERSION, '>')) {
            echo '[' . date('Y-m-d H:i:s') . '] Installiere Update ' . $info['version'] . '...' . PHP_EOL;
            $result = Updater::perform($info);
            foreach ($result['log'] ?? [] as $line) {
                echo '[' . date('Y-m-d H:i:s') . ']   ' . $line . PHP_EOL;
            }
        } else {
            echo '[' . date('Y-m-d H:i:s') . '] Kein Update zum Installieren verfügbar.' . PHP_EOL;
        }
        $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_update_last_run',?)", [$today]);
    } elseif ($lastAutoRun !== $today && $pastWindow) {
        // Window already missed for today — mark as done so we do not bounce later
        // tonight, and pick up again at the configured slot tomorrow.
        echo '[' . date('Y-m-d H:i:s') . '] Auto-Update-Fenster verpasst (Slot ' . $autoTime . ' + ' . $windowMin . ' min), warte bis morgen.' . PHP_EOL;
        $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('auto_update_last_run',?)", [$today]);
    }
}

// ── Health Check Monitor ─────────────────────────────────────────────────────
$hcEnabled = ($db->fetch("SELECT value FROM settings WHERE key='health_check_cron_enabled'")['value'] ?? '0') === '1';
if ($hcEnabled) {
    $intervalMin = max(5, (int)($db->fetch("SELECT value FROM settings WHERE key='health_check_interval_minutes'")['value'] ?? 60));
    $lastTs      = (int)($db->fetch("SELECT value FROM settings WHERE key='health_check_last_run'")['value'] ?? 0);
    if (time() - $lastTs >= $intervalMin * 60) {
        echo '[' . date('Y-m-d H:i:s') . '] Health Check läuft...' . PHP_EOL;
        $db->execute("INSERT OR REPLACE INTO settings (key,value) VALUES ('health_check_last_run',?)", [(string)time()]);

        $checks   = HealthCheck::runAll();
        $previous = HealthCheck::lastCronState();
        $prevPer  = $previous['checks'] ?? [];

        // Build the diff: every check whose status worsened or improved.
        $newProblems = [];
        $resolved    = [];
        foreach ($checks as $c) {
            $prev = $prevPer[$c['id']] ?? null;
            $now  = $c['status'];
            // Newly-bad: was OK or absent, now warn/error → alert.
            // Escalated: was warn, now error → alert.
            if (in_array($now, ['warn', 'error'], true) &&
                ($prev === null || $prev === 'ok' || ($prev === 'warn' && $now === 'error'))) {
                $newProblems[] = $c;
            }
            // Resolved: was warn/error, now ok → notify.
            if ($now === 'ok' && in_array($prev, ['warn', 'error'], true)) {
                $resolved[] = $c;
            }
        }

        // Persist new state (regardless of changes).
        HealthCheck::storeCronState($checks);

        // Send a single mail per cron tick that aggregates all NEW problems / resolutions.
        // Recipients: the explicit alert email setting, else all active admins' addresses.
        if (!empty($newProblems) || !empty($resolved)) {
            $recipients = [];
            $explicit   = trim((string)($db->fetch("SELECT value FROM settings WHERE key='health_check_alert_email'")['value'] ?? ''));
            if ($explicit !== '') {
                foreach (preg_split('/[\s,;]+/', $explicit) as $addr) {
                    $addr = trim($addr);
                    if (filter_var($addr, FILTER_VALIDATE_EMAIL)) $recipients[] = $addr;
                }
            } else {
                foreach ($db->fetchAll("SELECT email FROM users WHERE role='admin' AND is_active=1") as $r) {
                    if (!empty($r['email'])) $recipients[] = $r['email'];
                }
            }
            $recipients = array_unique($recipients);

            foreach ($recipients as $to) {
                if (!empty($newProblems)) Mailer::sendHealthAlert($to, $newProblems, false);
                if (!empty($resolved))    Mailer::sendHealthAlert($to, $resolved,    true);
            }
            echo '[' . date('Y-m-d H:i:s') . '] Health Check: ' . count($newProblems) . ' neu, ' . count($resolved) . ' wiederhergestellt → ' . count($recipients) . ' Empfänger.' . PHP_EOL;
        } else {
            echo '[' . date('Y-m-d H:i:s') . '] Health Check: keine Zustandsänderung.' . PHP_EOL;
        }
    }
}

echo '[' . date('Y-m-d H:i:s') . '] Fertig.' . PHP_EOL;
