<?php
// Central refresh scheduler for receiver/subscriber matching.
// - Does not run from normal dashboard reads.
// - 6-hour gate for full receiver probes.
// - Can rebuild the official matching snapshot from existing caches after subscriber imports/sync.

require_once __DIR__ . '/db_bootstrap.php';
require_once __DIR__ . '/airmax_monitor_lib.php';
require_once __DIR__ . '/receiver_subscriber_match_lib.php';

if (!function_exists('ua_rx_refresh_dir')) {
function ua_rx_refresh_dir(): string {
    $dir = __DIR__ . '/cache/runtime/receiver_matches';
    if (!is_dir($dir)) @mkdir($dir, 0777, true);
    return $dir;
}
}

if (!function_exists('ua_rx_refresh_state_file')) {
function ua_rx_refresh_state_file(): string { return ua_rx_refresh_dir() . '/refresh_state.json'; }
}

if (!function_exists('ua_rx_refresh_lock_file')) {
function ua_rx_refresh_lock_file(): string { return ua_rx_refresh_dir() . '/refresh.lock'; }
}

if (!function_exists('ua_rx_refresh_read_state')) {
function ua_rx_refresh_read_state(): array {
    $path = ua_rx_refresh_state_file();
    if (!is_file($path)) return [];
    $raw = @file_get_contents($path);
    $data = json_decode((string)$raw, true);
    return is_array($data) ? $data : [];
}
}

if (!function_exists('ua_rx_refresh_write_state')) {
function ua_rx_refresh_write_state(array $state): void {
    $path = ua_rx_refresh_state_file();
    $state['updated_at'] = time();
    $tmp = $path . '.tmp';
    if (@file_put_contents($tmp, json_encode($state, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT), LOCK_EX) !== false) {
        @rename($tmp, $path);
    }
}
}

if (!function_exists('ua_rx_refresh_lock_acquire')) {
function ua_rx_refresh_lock_acquire(int $ttl = 300) {
    $path = ua_rx_refresh_lock_file();
    if (is_file($path) && (time() - (int)@filemtime($path)) > max(60, $ttl)) @unlink($path);
    $fh = @fopen($path, 'c+');
    if (!$fh) return false;
    if (!@flock($fh, LOCK_EX | LOCK_NB)) { @fclose($fh); return false; }
    @ftruncate($fh, 0);
    @fwrite($fh, (string)getmypid());
    @touch($path);
    return $fh;
}
}

if (!function_exists('ua_rx_refresh_lock_release')) {
function ua_rx_refresh_lock_release($fh): void {
    if ($fh) { @flock($fh, LOCK_UN); @fclose($fh); }
}
}

if (!function_exists('ua_rx_refresh_station_scan_entry')) {
function ua_rx_refresh_station_scan_entry(array $device, int $scanStartedAt, string $scanId): array {
    $id = (int)($device['id'] ?? 0);
    if ($id <= 0) return ['ok'=>false,'message'=>'invalid device id'];
    $cache = (function_exists('airmax_json_read') && function_exists('airmax_result_cache_path')) ? airmax_json_read(airmax_result_cache_path($device), null) : null;
    if (!is_array($cache)) {
        return ['ok'=>false,'message'=>'probe result cache was not written'];
    }
    $cacheTs = (int)($cache['ts'] ?? 0);
    if ($cacheTs <= 0 || $cacheTs < ($scanStartedAt - 10)) {
        return ['ok'=>false,'message'=>'probe result cache is stale and was not accepted into the current scan','cache_ts'=>$cacheTs,'scan_started_at'=>$scanStartedAt];
    }
    $m = function_exists('ua_rx_bridge_macs_from_cache') ? ua_rx_bridge_macs_from_cache($device) : ['macs'=>[],'raw_macs'=>[],'source'=>'','raw_count'=>0,'client_count'=>0];
    $probe = is_array($cache['out']['probe'] ?? null) ? $cache['out']['probe'] : [];
    $bridge = is_array($probe['bridge'] ?? null) ? $probe['bridge'] : (is_array($cache['out']['bridge'] ?? null) ? $cache['out']['bridge'] : []);
    return [
        'ok'=>true,
        'entry'=>[
            'device_id'=>$id,
            'name'=>(string)($device['name'] ?? ''),
            'host'=>(string)($device['host'] ?? ''),
            'scan_id'=>$scanId,
            'scan_started_at'=>$scanStartedAt,
            'cache_ts'=>$cacheTs,
            'macs'=>array_values((array)($m['macs'] ?? [])),
            'raw_macs'=>array_values((array)($m['raw_macs'] ?? [])),
            'source'=>(string)($m['source'] ?? ''),
            'raw_count'=>(int)($m['raw_count'] ?? 0),
            'client_count'=>(int)($m['client_count'] ?? count((array)($m['macs'] ?? []))),
            'client_count_method'=>(string)($bridge['client_count_method'] ?? ($m['client_count_method'] ?? '')),
            'ownership_policy'=>(string)($bridge['ownership_policy'] ?? ($m['ownership_policy'] ?? '')),
        ],
    ];
}
}

if (!function_exists('ua_rx_refresh_commit_current_scan')) {
function ua_rx_refresh_commit_current_scan(array $stations, string $reason, int $scanStartedAt, string $scanId): array {
    $expected = [];
    foreach ($stations as $d) {
        $id = (int)($d['id'] ?? 0);
        if ($id > 0) {
            $expected[(string)$id] = [
                'id'=>$id,
                'name'=>(string)($d['name'] ?? ''),
                'host'=>(string)($d['host'] ?? ''),
            ];
        }
    }
    $scan = [
        'version'=>1,
        'scan_id'=>$scanId,
        'reason'=>$reason,
        'scan_started_at'=>$scanStartedAt,
        'scan_completed_at'=>time(),
        'ts'=>time(),
        'source'=>'atomic_full_receiver_refresh',
        'devices'=>[],
        'expected_devices'=>$expected,
        'failed_devices'=>[],
        'summary'=>['stations_expected'=>count($expected),'stations_ok'=>0,'stations_failed'=>0,'current_macs'=>0,'raw_macs'=>0,'partial'=>false],
        'ownership_policy'=>[
            'ubiquiti'=>'LAN0/eth0 client rows only; raw bridge MACs are diagnostics.',
            'mimosa'=>'Diagnostics FDB Ethernet/port 4 rows are ownership rows.',
        ],
    ];
    $errors = [];
    foreach ($stations as $d) {
        $id = (int)($d['id'] ?? 0);
        if ($id <= 0) continue;
        $entry = ua_rx_refresh_station_scan_entry($d, $scanStartedAt, $scanId);
        if (empty($entry['ok'])) {
            $fail = ['id'=>$id,'name'=>(string)($d['name'] ?? ''),'host'=>(string)($d['host'] ?? ''),'message'=>(string)($entry['message'] ?? 'current scan entry failed'),'cache_ts'=>(int)($entry['cache_ts'] ?? 0)];
            $errors[] = $fail;
            $scan['failed_devices'][(string)$id] = $fail;
            $scan['summary']['stations_failed']++;
            continue;
        }
        $e = (array)$entry['entry'];
        $scan['devices'][(string)$id] = $e;
        $scan['summary']['stations_ok']++;
        $scan['summary']['current_macs'] += count((array)($e['macs'] ?? []));
        $scan['summary']['raw_macs'] += count((array)($e['raw_macs'] ?? []));
    }
    $scan['summary']['stations'] = (int)$scan['summary']['stations_ok'];
    $scan['summary']['partial'] = ((int)$scan['summary']['stations_failed'] > 0);
    if ((int)$scan['summary']['stations_ok'] <= 0) {
        return ['ok'=>false,'message'=>'current scan was not committed because no receiver returned a fresh usable result','errors'=>$errors,'scan'=>$scan];
    }
    if (function_exists('ua_rx_json_write') && function_exists('ua_rx_current_scan_stage_file') && function_exists('ua_rx_current_scan_file')) {
        ua_rx_json_write(ua_rx_current_scan_stage_file(), $scan);
        @rename(ua_rx_current_scan_stage_file(), ua_rx_current_scan_file());
    }
    return ['ok'=>true,'partial'=>!empty($scan['summary']['partial']),'errors'=>$errors,'scan'=>$scan];
}
}

if (!function_exists('ua_receiver_match_mark_due')) {
function ua_receiver_match_mark_due(string $reason = 'manual'): void {
    $state = ua_rx_refresh_read_state();
    $state['due'] = true;
    $state['due_reason'] = $reason;
    $state['due_marked_at'] = time();
    ua_rx_refresh_write_state($state);
}
}

if (!function_exists('ua_receiver_match_rebuild_from_cache')) {
function ua_receiver_match_rebuild_from_cache(mysqli $conn, string $reason = 'cache_rebuild'): array {
    $snap = function_exists('ua_receiver_match_snapshot') ? ua_receiver_match_snapshot($conn, 0, true) : ['ok'=>false,'message'=>'receiver matcher unavailable'];
    $state = ua_rx_refresh_read_state();
    $state['last_snapshot_at'] = time();
    $state['last_snapshot_reason'] = $reason;
    $state['last_snapshot_ok'] = !empty($snap['ok']);
    $state['last_snapshot_matched'] = (int)($snap['summary']['matched_total'] ?? 0);
    if (!empty($state['due']) && in_array($reason, ['excel_import','mikrotik_sync','manual_cache_rebuild','manual_full_refresh','scheduled_full_refresh'], true)) {
        $state['due'] = false;
    }
    ua_rx_refresh_write_state($state);
    return $snap;
}
}

if (!function_exists('ua_receiver_match_scan_all')) {
function ua_receiver_match_scan_all(mysqli $conn, string $reason = 'manual_full_refresh', bool $force = false): array {
    @set_time_limit(300);
    $lock = ua_rx_refresh_lock_acquire(600);
    if (!$lock) {
        return ['ok'=>false,'message'=>'receiver refresh is already running','lock_busy'=>true];
    }
    try {
        $devices = function_exists('airmax_get_all') ? airmax_get_all($conn, true) : [];
        $stations = [];
        foreach ($devices as $d) {
            $isStation = function_exists('airmax_device_is_station') ? airmax_device_is_station($d) : in_array(strtolower((string)($d['device_type'] ?? '')), ['station','receiver','powerbeam'], true);
            if ($isStation) $stations[] = $d;
        }
        $scanStartedAt = time();
        $scanId = date('Ymd_His', $scanStartedAt) . '_' . substr(sha1($reason . '|' . $scanStartedAt . '|' . random_int(1, PHP_INT_MAX)), 0, 8);
        @unlink(function_exists('ua_rx_current_scan_stage_file') ? ua_rx_current_scan_stage_file() : (__DIR__ . '/cache/runtime/receiver_matches/current_scan.stage.json'));

        $probe = ['total'=>count($stations), 'ok'=>0, 'failed'=>0, 'items'=>[]];
        foreach ($stations as $d) {
            $id = (int)($d['id'] ?? 0);
            if ($id <= 0) continue;
            try {
                $out = function_exists('airmax_probe_and_persist') ? airmax_probe_and_persist($conn, $id) : ['ok'=>false,'message'=>'probe unavailable'];
                $ok = !empty($out['ok']) || !empty($out['up']) || (($out['status'] ?? '') === 'success');
                $probe[$ok ? 'ok' : 'failed']++;
                $probe['items'][] = [
                    'id'=>$id,
                    'name'=>(string)($d['name'] ?? ''),
                    'host'=>(string)($d['host'] ?? ''),
                    'ok'=>$ok,
                    'message'=>(string)($out['message'] ?? ''),
                ];
            } catch (Throwable $e) {
                $probe['failed']++;
                $probe['items'][] = ['id'=>$id,'name'=>(string)($d['name'] ?? ''),'host'=>(string)($d['host'] ?? ''),'ok'=>false,'message'=>$e->getMessage()];
            }
        }

        $scanCommit = function_exists('ua_rx_refresh_commit_current_scan')
            ? ua_rx_refresh_commit_current_scan($stations, $reason, $scanStartedAt, $scanId)
            : ['ok'=>false,'message'=>'current scan commit helper unavailable'];
        if (empty($scanCommit['ok'])) {
            $state = ua_rx_refresh_read_state();
            $state['last_full_scan_failed_at'] = time();
            $state['last_full_scan_failed_reason'] = $reason . '_current_scan_commit_failed';
            $state['last_full_scan_ok'] = false;
            $state['last_full_scan_station_count'] = count($stations);
            $state['last_full_scan_probe_ok'] = (int)$probe['ok'];
            $state['last_full_scan_probe_failed'] = 0;
            $state['last_full_scan_current_scan_errors'] = (array)($scanCommit['errors'] ?? []);
            $state['due'] = true;
            $state['due_reason'] = $reason . '_current_scan_commit_failed';
            ua_rx_refresh_write_state($state);
            if (function_exists('airmax_invalidate_noc_card_cache')) airmax_invalidate_noc_card_cache();
            return [
                'ok'=>false,
                'reason'=>$reason,
                'message'=>'فشل تثبيت كاش الجولة الحالية، لذلك بقيت النتيجة الرسمية كما هي ولم يتم دمج أي قراءة جزئية.',
                'probe'=>$probe,
                'current_scan'=>$scanCommit,
                'snapshot_unchanged'=>true,
            ];
        }

        $snap = ua_receiver_match_rebuild_from_cache($conn, $reason);
        $state = ua_rx_refresh_read_state();
        $state['last_full_scan_at'] = time();
        $state['last_full_scan_reason'] = $reason;
        $state['last_full_scan_ok'] = true;
        $state['last_full_scan_partial'] = !empty($scanCommit['partial']);
        $state['last_full_scan_station_count'] = count($stations);
        $state['last_full_scan_probe_ok'] = (int)$probe['ok'];
        $state['last_full_scan_probe_failed'] = (int)$probe['failed'];
        $state['last_full_scan_id'] = $scanId;
        $state['last_full_scan_current_macs'] = (int)($scanCommit['scan']['summary']['current_macs'] ?? 0);
        $failedItems = array_values(array_filter($probe['items'], static function($it){ return empty($it['ok']); }));
        $names = [];
        foreach ($failedItems as $it) {
            $label = trim((string)($it['name'] ?? ''));
            $host = trim((string)($it['host'] ?? ''));
            $names[] = $label !== '' ? ($host !== '' ? $label . ' (' . $host . ')' : $label) : ($host !== '' ? $host : ('#' . (int)($it['id'] ?? 0)));
        }
        if ($names) {
            $state['last_full_scan_failed_devices'] = $names;
            $state['last_full_scan_failed_reason'] = $reason . '_partial';
        } else {
            unset($state['last_full_scan_failed_devices'], $state['last_full_scan_failed_reason']);
        }
        unset($state['last_full_scan_current_scan_errors']);
        $state['due'] = false;
        ua_rx_refresh_write_state($state);
        if (function_exists('airmax_invalidate_noc_card_cache')) airmax_invalidate_noc_card_cache();
        return [
            'ok'=>true,
            'partial'=>!empty($scanCommit['partial']),
            'reason'=>$reason,
            'message'=>!empty($scanCommit['partial'])
                ? 'تم تحديث المطابقة من المستقبلات التي نجح فحصها فقط. المستقبلات الفاشلة لم تدخل في المقارنة حتى لا تختلط نتائج قديمة بجديدة.'
                : 'تم تحديث مطابقة المستقبلات بنجاح.',
            'probe'=>$probe,
            'failed_devices'=>$failedItems,
            'failed_labels'=>$names,
            'current_scan'=>['scan_id'=>$scanId,'summary'=>(array)($scanCommit['scan']['summary'] ?? [])],
            'snapshot'=>$snap
        ];
    } finally {
        ua_rx_refresh_lock_release($lock);
    }
}
}

if (!function_exists('ua_receiver_match_run_due')) {
function ua_receiver_match_run_due(mysqli $conn, int $intervalSeconds = 21600, bool $force = false): array {
    $state = ua_rx_refresh_read_state();
    $last = (int)($state['last_full_scan_at'] ?? 0);
    $dueFlag = !empty($state['due']);
    $dueByTime = ($last <= 0 || (time() - $last) >= max(3600, $intervalSeconds));
    if (!$force && !$dueFlag && !$dueByTime) {
        return ['ok'=>true,'skipped'=>true,'message'=>'not due','next_in'=>max(0, max(3600, $intervalSeconds) - (time() - $last)), 'state'=>$state];
    }
    $reason = $force ? 'manual_full_refresh' : ($dueFlag ? (string)($state['due_reason'] ?? 'due_flag') : 'scheduled_full_refresh');
    return ua_receiver_match_scan_all($conn, $reason, true);
}
}
