<?php
declare(strict_types=1);

// Central CLI entry point for every recurring server-side job. Windows Task
// Scheduler invokes this file with one job name; application libraries retain
// ownership of their own business intervals and database locks.
if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

@ini_set('display_errors', '1');
@ini_set('log_errors', '0');
@ini_set('max_execution_time', '0');
@set_time_limit(0);

$job = strtolower(trim((string)($argv[1] ?? '')));
$allowedJobs = ['monitor', 'ppp-cache', 'subscriber-queue', 'automation', 'receiver-match', 'backup'];
if (!in_array($job, $allowedJobs, true)) {
    fwrite(STDERR, "Unknown background job.\n");
    exit(64);
}

$runtimeDir = dirname(__DIR__) . '/cache/runtime/background_workers';
if (!is_dir($runtimeDir) && !@mkdir($runtimeDir, 0775, true) && !is_dir($runtimeDir)) {
    fwrite(STDERR, "Cannot create background worker runtime directory.\n");
    exit(73);
}

$lockPath = $runtimeDir . '/' . $job . '.lock';
$lock = @fopen($lockPath, 'c+');
if (!$lock || !@flock($lock, LOCK_EX | LOCK_NB)) {
    echo json_encode(['ok'=>true, 'job'=>$job, 'skipped'=>true, 'reason'=>'already_running'], JSON_UNESCAPED_SLASHES) . PHP_EOL;
    if ($lock) @fclose($lock);
    exit(0);
}
@ftruncate($lock, 0);
@fwrite($lock, (string)getmypid());
@fflush($lock);

$startedAt = microtime(true);
$statePath = $runtimeDir . '/' . $job . '.json';
$result = null;
$exitCode = 0;

function background_compact_result(string $job, $result): array {
    if (!is_array($result)) return ['value'=>$result];
    $pick = static function(array $source, array $keys): array {
        $out = [];
        foreach ($keys as $key) if (array_key_exists($key, $source)) $out[$key] = $source[$key];
        return $out;
    };
    if ($job === 'monitor') {
        $out = $pick($result, ['ok']);
        foreach (['cleanup','fast_active','deep','ip_scan','daily_event_log'] as $part) {
            if (isset($result[$part]) && is_array($result[$part])) {
                $out[$part] = $pick($result[$part], ['ok','skipped','message','reason','updated','processed','watch','network','admin','day']);
            }
        }
        return $out;
    }
    if ($job === 'ppp-cache') {
        return $pick($result, ['ok','source','router_online','connection_state','live_data_reliable','data_available','online','age','data_age','message']);
    }
    if ($job === 'subscriber-queue') {
        return $pick($result, ['ok','processed','done','failed','pending','message','error']);
    }
    if ($job === 'automation') {
        $out = [];
        foreach ($result as $task => $taskResult) {
            $taskResult = is_array($taskResult) ? $taskResult : ['value'=>$taskResult];
            $out[$task] = $pick($taskResult, ['ok','skipped','reason','error']);
        }
        return $out;
    }
    if ($job === 'receiver-match') {
        return $pick($result, ['ok','skipped','partial','reason','message','next_in','failed_labels']);
    }
    return $pick($result, ['ok','due','next','status','message','error','file_name','bytes']);
}

try {
    require_once dirname(__DIR__) . '/db_bootstrap.php';
    $conn = ensure_db(true);
    if (!($conn instanceof mysqli)) {
        throw new RuntimeException('Database connection unavailable.');
    }

    switch ($job) {
        case 'monitor':
            require_once dirname(__DIR__) . '/subscriber_mikrotik_watch_lib.php';
            require_once dirname(__DIR__) . '/security_alerts_lib.php';
            require_once dirname(__DIR__) . '/security_monitor_lib.php';
            $result = ['ok'=>true];
            $result['cleanup'] = function_exists('sm_cleanup_stale_security_alerts')
                ? sm_cleanup_stale_security_alerts($conn, 3, 30)
                : ['ok'=>false, 'message'=>'cleanup helper missing'];
            $result['fast_active'] = function_exists('mtwatch_run_fast_active_sync')
                ? mtwatch_run_fast_active_sync($conn, false, 300)
                : ['ok'=>false, 'message'=>'fast active sync missing'];
            $result['deep'] = function_exists('mtwatch_run_hourly_deep_sync')
                ? mtwatch_run_hourly_deep_sync($conn, false)
                : ['ok'=>false, 'message'=>'deep sync missing'];
            $result['ip_scan'] = function_exists('mtwatch_run_network_ip_scan')
                ? mtwatch_run_network_ip_scan($conn, false, 60)
                : ['ok'=>false, 'message'=>'IP scan missing'];
            foreach (['fast_active', 'deep', 'ip_scan'] as $part) {
                if (empty($result[$part]['ok'])) {
                    $result['ok'] = false;
                    if (function_exists('sm_record_monitor_issue')) {
                        sm_record_monitor_issue($conn, $part, (string)($result[$part]['message'] ?? 'Background monitor failed.'), 'warn');
                    }
                }
            }
            $result['daily_event_log'] = function_exists('sm_sync_daily_event_log')
                ? sm_sync_daily_event_log($conn)
                : ['ok'=>false, 'message'=>'daily event log helper missing'];
            break;

        case 'ppp-cache':
            require_once dirname(__DIR__) . '/mikrotik_lib.php';
            require_once dirname(__DIR__) . '/ppp_active_cache_lib.php';
            $result = ua_ppp_cache_rebuild($conn, 10, true);
            break;

        case 'subscriber-queue':
            require_once dirname(__DIR__) . '/subscribers_queue_lib.php';
            $result = sq_run_worker($conn, 3, 50);
            break;

        case 'automation':
            require_once dirname(__DIR__) . '/automation_engine.php';
            $result = automation_run($conn, false);
            break;

        case 'receiver-match':
            require_once dirname(__DIR__) . '/receiver_match_refresh_lib.php';
            $result = ua_receiver_match_run_due($conn, 21600, false);
            break;

        case 'backup':
            require_once dirname(__DIR__) . '/backup_lib.php';
            ua_backup_ensure_schema($conn);
            $settings = ua_backup_settings($conn);
            $result = ua_backup_is_due($settings)
                ? ua_backup_run($conn, 'scheduled')
                : ['ok'=>true, 'due'=>false, 'next'=>($next = ua_backup_next_run($settings)) ? $next->format('Y-m-d H:i:s') : null];
            break;
    }

    if (is_array($result) && array_key_exists('ok', $result) && empty($result['ok'])) {
        $exitCode = 1;
    }
} catch (Throwable $e) {
    $exitCode = 1;
    $result = [
        'ok'=>false,
        'error'=>$e->getMessage(),
        'type'=>get_class($e),
    ];
}

$compactResult = background_compact_result($job, $result);
$state = [
    'ok'=>$exitCode === 0,
    'job'=>$job,
    'pid'=>getmypid(),
    'started_at'=>date('Y-m-d H:i:s', (int)$startedAt),
    'finished_at'=>date('Y-m-d H:i:s'),
    'duration_ms'=>(int)round((microtime(true) - $startedAt) * 1000),
    'result'=>$compactResult,
];
$json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$tmpPath = $statePath . '.tmp.' . getmypid();
if (@file_put_contents($tmpPath, $json, LOCK_EX) !== false) {
    @unlink($statePath);
    @rename($tmpPath, $statePath);
}

echo $json . PHP_EOL;
@flock($lock, LOCK_UN);
@fclose($lock);
exit($exitCode);
