#!/usr/bin/env php
<?php

// Background worker for processing backup jobs
// This script should be run as a daemon or via cron

// -----------------------------------------------------------------------------
// Self-attach to whp-backup.slice (cgroup v2 io.max throttle).
//
// The cron path wraps this script in /usr/local/sbin/whp-backup-runner which
// uses systemd-run --scope to place the worker inside whp-backup.slice. Manual
// triggers from the web UI bypass that path — they shell_exec/proc_open this
// script straight from php-fpm, so the worker inherits php-fpm.service's
// cgroup and the io.max bounds never apply.
//
// Fix: at startup, check our current cgroup. If we're not already under
// whp-backup.slice, re-exec self under systemd-run --scope so every child
// (mysqldump, pg_dump, tar, pigz) inherits the throttled cgroup. The cron
// path naturally short-circuits this check because the runner has already
// placed us in the slice.
//
// Falls back to running un-throttled (with a stderr warning) if systemd-run
// is missing or the re-exec fails — better to do an un-throttled backup than
// to fail outright.
// -----------------------------------------------------------------------------
(static function () {
    if (PHP_SAPI !== 'cli') {
        return;
    }
    if (getenv('WHP_BACKUP_SCOPED') === '1') {
        return; // Guard against infinite recursion.
    }
    $cgroup = @file_get_contents('/proc/self/cgroup');
    if (is_string($cgroup) && strpos($cgroup, 'whp-backup.slice') !== false) {
        return; // Already in the slice (cron path).
    }
    if (!is_file('/usr/bin/systemd-run') || !is_executable('/usr/bin/systemd-run')) {
        fwrite(STDERR, "[BackupWorker] systemd-run missing; running un-throttled\n");
        return;
    }

    $argv_passed = $_SERVER['argv'] ?? [];
    array_shift($argv_passed); // drop script name; systemd-run gets it explicitly

    $cmd = array_merge(
        [
            '/usr/bin/systemd-run',
            '--scope',
            '--quiet',
            '--collect',
            '--slice=whp-backup.slice',
            '--unit=whp-backup-' . time() . '-' . getmypid(),
            '--setenv=WHP_BACKUP_SCOPED=1',
            '--',
            PHP_BINARY,
            __FILE__,
        ],
        $argv_passed
    );

    $proc = @proc_open(
        $cmd,
        [0 => STDIN, 1 => STDOUT, 2 => STDERR],
        $pipes
    );
    if (!is_resource($proc)) {
        fwrite(STDERR, "[BackupWorker] proc_open(systemd-run) failed; running un-throttled\n");
        return;
    }
    exit(proc_close($proc));
})();

// Load configuration first
require_once(__DIR__ . '/../configs/config.php');
require_once('/docker/whp/web/libs/mysqlmgmt.php');
require_once(__DIR__ . '/../libs/BackupJob.php');
require_once(__DIR__ . '/../libs/BackupHistory.php');

use WHPBackup\BackupJob;
use WHPBackup\BackupHistory;

function log_message($message) {
    $timestamp = date('Y-m-d H:i:s');
    $message = str_replace(["\r", "\n"], ' ', $message);
    $formatted = "[$timestamp] [BackupWorker] $message";
    error_log($formatted);
    @file_put_contents('/var/log/whp-backup.log', $formatted . "\n", FILE_APPEND | LOCK_EX);
}

function cleanup_stuck_backups() {
    try {
        $mysql = new mysqlmgmt();
        $db = $mysql->getMySQLConnection();
        $db->exec("USE whp");

        // Find backups stuck in 'running' or 'claimed' state for more than 30 minutes
        $stmt = $db->prepare("
            UPDATE backup_history
            SET status = 'failed',
                error_message = 'Backup timed out or worker died',
                completed_at = NOW()
            WHERE status IN ('running', 'claimed')
            AND started_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)
        ");
        $stmt->execute();
        $affectedRows = $stmt->rowCount();

        if ($affectedRows > 0) {
            log_message("Cleaned up $affectedRows stuck backup(s)");
        }

        return $affectedRows;
    } catch (Exception $e) {
        log_message("Error cleaning up stuck backups: " . $e->getMessage());
        return 0;
    }
}

function getCreationRetryCount($db, $backupId) {
    $stmt = $db->prepare("SELECT metadata FROM backup_history WHERE id = ?");
    $stmt->execute([$backupId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if ($row && !empty($row['metadata'])) {
        $metadata = json_decode($row['metadata'], true);
        return $metadata['creation_retry_count'] ?? 0;
    }
    return 0;
}

function scheduleCreationRetry($db, $backupId, $retryCount, $delaySeconds, $lastError) {
    // Store retry count in metadata JSON
    $stmt = $db->prepare("SELECT metadata FROM backup_history WHERE id = ?");
    $stmt->execute([$backupId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $metadata = [];
    if ($row && !empty($row['metadata'])) {
        $metadata = json_decode($row['metadata'], true) ?: [];
    }
    $metadata['creation_retry_count'] = $retryCount;
    $metadata['last_creation_error'] = $lastError;
    $metadata['next_creation_retry'] = date('Y-m-d H:i:s', time() + $delaySeconds);

    // Reset to pending so the worker picks it up again after the delay
    $stmt = $db->prepare("
        UPDATE backup_history
        SET status = 'pending',
            creation_status = 'pending',
            error_message = ?,
            completed_at = NULL,
            started_at = ?,
            metadata = ?
        WHERE id = ?
    ");
    $nextRetryAt = date('Y-m-d H:i:s', time() + $delaySeconds);
    $stmt->execute([
        "Retry {$retryCount}: {$lastError}",
        $nextRetryAt,
        json_encode($metadata),
        $backupId
    ]);
}

function process_pending_backups() {
    try {
        $mysql = new mysqlmgmt();
        $db = $mysql->getMySQLConnection();
        $db->exec("USE whp");

        // Get maximum concurrent backups from settings file
        $max_concurrent = 2; // Default value
        if (file_exists('/docker/whp/settings.json')) {
            $settings = json_decode(file_get_contents('/docker/whp/settings.json'), true);
            if ($settings && isset($settings['backup_max_concurrent'])) {
                $max_concurrent = intval($settings['backup_max_concurrent']);
                if ($max_concurrent < 1 || $max_concurrent > 10) {
                    $max_concurrent = 2; // Fallback to default if invalid
                }
            }
        }

        log_message("Using maximum concurrent backups: $max_concurrent");

        // Count currently running backups (BackupJob uses 'running' status, not 'processing')
        $stmt = $db->prepare("SELECT COUNT(*) FROM backup_history WHERE status = 'running'");
        $stmt->execute();
        $running_count = $stmt->fetchColumn();

        log_message("Currently running backups: $running_count");

        // Calculate how many new backups we can start
        $available_slots = $max_concurrent - $running_count;

        if ($available_slots <= 0) {
            log_message("Maximum concurrent backups reached ($max_concurrent). No new backups can be started.");
            return;
        }

        log_message("Available backup slots: $available_slots");

        // Get pending backups (limit to available slots) with atomic update to prevent race conditions
        $db->beginTransaction();
        try {
            $stmt = $db->prepare("SELECT id FROM backup_history WHERE status = 'pending' AND started_at <= NOW() ORDER BY started_at ASC LIMIT ? FOR UPDATE");
            $stmt->execute([$available_slots]);
            $backups = $stmt->fetchAll(PDO::FETCH_ASSOC);

            // Immediately mark selected backups as 'claimed' to prevent other workers from taking them
            if (!empty($backups)) {
                $backupIds = array_column($backups, 'id');
                $placeholders = str_repeat('?,', count($backupIds) - 1) . '?';
                $stmt = $db->prepare("UPDATE backup_history SET status = 'claimed' WHERE id IN ($placeholders)");
                $stmt->execute($backupIds);
            }

            $db->commit();
        } catch (Exception $e) {
            $db->rollback();
            throw $e;
        }
        
        if (empty($backups)) {
            log_message("No pending backups found");
            return;
        }
        
        log_message("Found " . count($backups) . " pending backups");
        
        $job = new BackupJob();
        
        $maxCreationRetries = 3;

        foreach ($backups as $backup) {
            try {
                log_message("Processing backup ID: " . $backup['id']);
                $job->processBackup($backup['id']);
                log_message("Backup ID " . $backup['id'] . " completed successfully");
            } catch (Exception $e) {
                log_message("Backup ID " . $backup['id'] . " failed: " . $e->getMessage());
                log_message("Stack trace: " . $e->getTraceAsString());

                // Check retry count and reschedule if retries remain
                try {
                    $retryCount = getCreationRetryCount($db, $backup['id']);
                    if ($retryCount < $maxCreationRetries) {
                        $retryCount++;
                        $retryDelay = min(300, 60 * pow(2, $retryCount - 1)); // 60s, 120s, 240s (max 300s)
                        scheduleCreationRetry($db, $backup['id'], $retryCount, $retryDelay, $e->getMessage());
                        log_message("Backup ID " . $backup['id'] . " scheduled for creation retry {$retryCount}/{$maxCreationRetries} in {$retryDelay}s");
                    } else {
                        log_message("Backup ID " . $backup['id'] . " permanently failed after {$maxCreationRetries} creation attempts");
                    }
                } catch (Exception $retryEx) {
                    log_message("Failed to schedule retry for backup ID " . $backup['id'] . ": " . $retryEx->getMessage());
                }
            }
        }
        
    } catch (Exception $e) {
        log_message("Error in backup worker: " . $e->getMessage());
    }
}

// Main execution
if (php_sapi_name() === 'cli') {
    $lockFile = '/tmp/backup-worker.lock';

    // Check if lock file exists
    if (file_exists($lockFile)) {
        $existingPid = trim(file_get_contents($lockFile));

        // Check if the process is still running
        if ($existingPid && file_exists("/proc/$existingPid")) {
            // Process is still running, exit silently
            exit(0);
        } else {
            // Process is not running or stale lock file
            $lockAge = time() - filemtime($lockFile);
            log_message("Found stale lock file (age: {$lockAge} seconds, PID: $existingPid), removing it");
            unlink($lockFile);
        }
    }

    // Create lock file with current PID
    $pid = getmypid();
    if (file_put_contents($lockFile, $pid, LOCK_EX) === false) {
        log_message("Failed to create lock file, exiting");
        exit(1);
    }

    // Register shutdown function to clean up lock file
    register_shutdown_function(function() use ($lockFile, $pid) {
        // Only remove lock if it still contains our PID
        if (file_exists($lockFile)) {
            $currentPid = trim(file_get_contents($lockFile));
            if ($currentPid == $pid) {
                unlink($lockFile);
                log_message("Lock file cleaned up by shutdown handler");
            }
        }
    });

    // Set up signal handlers for clean shutdown
    if (function_exists('pcntl_signal')) {
        pcntl_signal(SIGTERM, function() use ($lockFile, $pid) {
            log_message("Received SIGTERM, cleaning up...");
            if (file_exists($lockFile)) {
                $currentPid = trim(file_get_contents($lockFile));
                if ($currentPid == $pid) {
                    unlink($lockFile);
                }
            }
            exit(0);
        });
        pcntl_signal(SIGINT, function() use ($lockFile, $pid) {
            log_message("Received SIGINT, cleaning up...");
            if (file_exists($lockFile)) {
                $currentPid = trim(file_get_contents($lockFile));
                if ($currentPid == $pid) {
                    unlink($lockFile);
                }
            }
            exit(0);
        });
    }

    log_message("Backup worker started (PID: $pid)");

    try {
        // First, clean up any stuck backups
        cleanup_stuck_backups();

        // Process pending backups
        process_pending_backups();
    } catch (Exception $e) {
        log_message("Fatal error in backup worker: " . $e->getMessage());
    }

    log_message("Backup worker finished");

    // Clean up lock file
    if (file_exists($lockFile)) {
        $currentPid = trim(file_get_contents($lockFile));
        if ($currentPid == $pid) {
            unlink($lockFile);
        }
    }
} else {
    echo "This script can only be run from command line\n";
    exit(1);
}