<?php
require_once __DIR__ . '/subscriber_history_lib.php';
require_once __DIR__ . '/mikrotik_lib.php';
require_once __DIR__ . '/quota_lib.php';
// Unified PPP active sessions cache for subscribers page.
// Reads are file-based and fast; refresh is guarded by a lock and max-age.

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

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

if (!function_exists('ua_ppp_cache_file')) {
  function ua_ppp_cache_file(): string { return ua_cache_dir() . '/ppp_active.json'; }
}

if (!function_exists('ua_ppp_cache_lock')) {
  function ua_ppp_cache_lock(): string { return ua_cache_dir() . '/ppp_active.lock'; }
}

if (!function_exists('ua_ppp_cache_log')) {
  function ua_ppp_cache_log(string $msg): void {
    $stateFile = ua_cache_dir() . '/ppp_cache_log_state.json';
    $state = [];
    if (is_file($stateFile)) {
      $decoded = json_decode((string)@file_get_contents($stateFile), true);
      if (is_array($decoded)) $state = $decoded;
    }
    $now = time();
    $hash = hash('sha256', $msg);
    // Polling runs every few seconds. Record the transition once, then only
    // one daily heartbeat while the exact same outage continues.
    if (($state['state'] ?? '') === 'failed'
        && hash_equals((string)($state['hash'] ?? ''), $hash)
        && ($now - (int)($state['ts'] ?? 0)) < 86400) {
      return;
    }
    @file_put_contents(ua_log_dir() . '/ppp_cache.log', '['.date('Y-m-d H:i:s').'] '.$msg.PHP_EOL, FILE_APPEND);
    @file_put_contents($stateFile, json_encode(['state'=>'failed','hash'=>$hash,'ts'=>$now], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
  }
}

if (!function_exists('ua_ppp_cache_log_recovered')) {
  function ua_ppp_cache_log_recovered(): void {
    $stateFile = ua_cache_dir() . '/ppp_cache_log_state.json';
    $state = [];
    if (is_file($stateFile)) {
      $decoded = json_decode((string)@file_get_contents($stateFile), true);
      if (is_array($decoded)) $state = $decoded;
    }
    if (($state['state'] ?? '') === 'failed') {
      @file_put_contents(ua_log_dir() . '/ppp_cache.log', '['.date('Y-m-d H:i:s').'] connection recovered'.PHP_EOL, FILE_APPEND);
    }
    @file_put_contents($stateFile, json_encode(['state'=>'ok','hash'=>'','ts'=>time()], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
  }
}

if (!function_exists('ua_norm_ppp_user')) {
  function ua_norm_ppp_user(string $u): string {
    $u = trim($u);
    $u = preg_replace('/@.*/', '', $u);
    return trim((string)$u);
  }
}

if (!function_exists('ua_col_exists')) {
  function ua_col_exists(mysqli $conn, string $table, string $col): bool {
    static $cache = [];
    $k = $table.'|'.$col;
    if (array_key_exists($k, $cache)) return $cache[$k];
    $t = $conn->real_escape_string($table);
    $c = $conn->real_escape_string($col);
    $rs = @$conn->query("SHOW COLUMNS FROM `{$t}` LIKE '{$c}'");
    return $cache[$k] = (bool)($rs && $rs->num_rows > 0);
  }
}

if (!function_exists('ua_ppp_cache_default')) {
  function ua_ppp_cache_default(): array {
    return [
      'ok' => false,
      'router_online' => null,
      'live_data_reliable' => false,
      'ts' => 0,
      'age' => 999999,
      'source' => 'empty',
      'online' => 0,
      'users' => [],
      'rows' => [],
      'message' => 'لا يوجد كاش بعد',
    ];
  }
}

if (!function_exists('ua_ppp_clear_live_state_caches')) {
  function ua_ppp_clear_live_state_caches(): void {
    $runtime = ua_cache_dir() . '/runtime';
    $snapshot = $runtime . '/app_stats_snapshot.json';
    if (is_file($snapshot)) @unlink($snapshot);
    $liveDir = $runtime . '/subscribers-live';
    if (is_dir($liveDir)) {
      foreach ((array)glob($liveDir . '/*.json') as $path) {
        if (is_file($path)) @unlink($path);
      }
    }
  }
}

if (!function_exists('ua_ppp_router_offline_snapshot')) {
  /**
   * A router outage is not a subscriber logout. Preserve the last verified
   * MikroTik observation and mark its freshness separately. Never manufacture
   * an empty active set and never mutate subscriber state from a transport error.
   */
  function ua_ppp_router_offline_snapshot(mysqli $conn, string $message, array $lastVerified = []): array {
    $hasVerifiedData = !empty($lastVerified['data_available'])
      || (!empty($lastVerified['ok']) && !empty($lastVerified['observed_ts']) && isset($lastVerified['users']));
    $observedTs = (int)($lastVerified['observed_ts'] ?? $lastVerified['last_success_ts'] ?? $lastVerified['ts'] ?? 0);
    $data = [
      'ok' => true,
      'ts' => time(),
      'observed_ts' => $observedTs,
      'last_success_ts' => $observedTs,
      'source' => $hasVerifiedData ? 'last_verified_mikrotik' : 'router_unreachable_no_snapshot',
      'router_online' => null,
      'connection_state' => 'unreachable',
      'live_data_reliable' => false,
      'data_available' => $hasVerifiedData,
      'online' => $hasVerifiedData ? (int)($lastVerified['online'] ?? count((array)($lastVerified['users'] ?? []))) : 0,
      'users' => $hasVerifiedData ? array_values((array)($lastVerified['users'] ?? [])) : [],
      'rows' => $hasVerifiedData ? array_values((array)($lastVerified['rows'] ?? [])) : [],
      'message' => $message !== '' ? $message : 'router offline',
    ];
    ua_ppp_cache_write($data);
    ua_ppp_clear_live_state_caches();
    return ua_ppp_cache_read(86400);
  }
}

if (!function_exists('ua_ppp_cache_read')) {
  function ua_ppp_cache_read(int $maxAge = 3600): array {
    $path = ua_ppp_cache_file();
    if (!is_file($path)) return ua_ppp_cache_default();
    $raw = @file_get_contents($path);
    $data = json_decode((string)$raw, true);
    if (!is_array($data)) return ua_ppp_cache_default();
    $ts = (int)($data['ts'] ?? 0);
    $data['age'] = $ts > 0 ? max(0, time() - $ts) : 999999;
    $observedTs = (int)($data['observed_ts'] ?? $data['last_success_ts'] ?? $ts);
    $data['observed_ts'] = $observedTs;
    $data['data_age'] = $observedTs > 0 ? max(0, time() - $observedTs) : 999999;
    if ($data['age'] > $maxAge) $data['stale'] = true;
    if (!isset($data['users']) || !is_array($data['users'])) $data['users'] = [];
    if (!isset($data['rows']) || !is_array($data['rows'])) $data['rows'] = [];
    $data['online'] = (int)($data['online'] ?? count($data['users']));
    return $data;
  }
}

if (!function_exists('ua_ppp_normalize_active_rows')) {
  function ua_ppp_normalize_active_rows(array $activeRows): array {
    $users = [];
    $rows = [];
    foreach ($activeRows as $r) {
      if (!is_array($r) || in_array((string)($r['.type'] ?? ''), ['!done','!trap','!fatal'], true)) continue;
      $u = '';
      foreach (['name','NAME','user','USER','username','USERNAME'] as $k) {
        if (isset($r[$k]) && trim((string)$r[$k]) !== '') { $u = (string)$r[$k]; break; }
      }
      $u = ua_norm_ppp_user($u);
      if ($u === '') continue;
      $users[$u] = true;
      $rows[$u] = [
        'name' => $u,
        'address' => (string)($r['address'] ?? $r['ip'] ?? ''),
        'caller_id' => (string)($r['caller_id'] ?? $r['caller-id'] ?? ''),
        'uptime' => (string)($r['uptime'] ?? ''),
        'service' => (string)($r['service'] ?? ''),
        'rx_bytes' => (int)($r['rx_bytes'] ?? $r['rx-bytes'] ?? 0),
        'tx_bytes' => (int)($r['tx_bytes'] ?? $r['tx-bytes'] ?? 0),
      ];
    }
    $userList = array_keys($users);
    sort($userList, SORT_NATURAL | SORT_FLAG_CASE);
    return ['users'=>$userList, 'rows'=>array_values($rows), 'online'=>count($userList)];
  }
}

if (!function_exists('ua_ppp_recent_router_command_snapshot')) {
  /** Recover a recent successful RouterOS read when the next TCP probe fails. */
  function ua_ppp_recent_router_command_snapshot(mysqli $conn, int $maxAge = 300): array {
    if (!function_exists('mt_get_settings')) return [];
    $cfg = mt_get_settings($conn);
    if (!is_array($cfg)) return [];
    $host = trim((string)($cfg['host'] ?? ''));
    $user = trim((string)($cfg['username'] ?? ''));
    $port = (int)($cfg['api_port'] ?? 8728);
    if ($host === '' || $user === '') return [];
    $prefix = substr(sha1($host . '|' . $port . '|' . $user), 0, 16) . '_';
    $dir = __DIR__ . '/cache/runtime/mt_command_cache';
    $best = [];
    foreach ((array)glob($dir . '/' . $prefix . '*.json') as $file) {
      $raw = @file_get_contents($file);
      $payload = json_decode((string)$raw, true);
      if (!is_array($payload) || (string)($payload['path'] ?? '') !== '/ppp/active/print') continue;
      $ts = (int)($payload['ts'] ?? @filemtime($file));
      if ($ts <= 0 || (time() - $ts) > max(30, $maxAge) || $ts <= (int)($best['observed_ts'] ?? 0)) continue;
      $normalized = ua_ppp_normalize_active_rows((array)($payload['rows'] ?? []));
      $best = $normalized + [
        'ok'=>true,
        'data_available'=>true,
        'observed_ts'=>$ts,
        'last_success_ts'=>$ts,
        'source'=>'router_command_cache',
      ];
    }
    return $best;
  }
}

if (!function_exists('ua_ppp_reconcile_verified_users')) {
  /** Mirror a verified RouterOS active set into the DB without inventing event timestamps. */
  function ua_ppp_reconcile_verified_users(mysqli $conn, array $userList): void {
    if (!ua_col_exists($conn, 'subscribers', 'is_online')) return;
    $users = [];
    foreach ($userList as $user) {
      $user = ua_norm_ppp_user((string)$user);
      if ($user !== '') $users[$user] = true;
    }
    $hasDel = ua_col_exists($conn, 'subscribers', 'is_deleted');
    $hasPpp = ua_col_exists($conn, 'subscribers', 'ppp_username');
    $userExpr = $hasPpp ? "SUBSTRING_INDEX(COALESCE(NULLIF(ppp_username,''), username),'@',1)" : "SUBSTRING_INDEX(username,'@',1)";
    $delCond = $hasDel ? " AND COALESCE(is_deleted,0)=0" : '';
    if (!$users) {
      @$conn->query("UPDATE subscribers SET is_online=0 WHERE COALESCE(is_online,0)=1{$delCond}");
      return;
    }
    $in = "'" . implode("','", array_map([$conn, 'real_escape_string'], array_keys($users))) . "'";
    @$conn->query("UPDATE subscribers SET is_online=0 WHERE COALESCE(is_online,0)=1{$delCond} AND {$userExpr} NOT IN ({$in})");
    @$conn->query("UPDATE subscribers SET is_online=1 WHERE 1=1{$delCond} AND {$userExpr} IN ({$in})");
  }
}

if (!function_exists('ua_ppp_cache_snapshot')) {
  function ua_ppp_cache_snapshot(int $maxAge = 86400): array {
    $cache = ua_ppp_cache_read($maxAge);
    if (!empty($cache['ok'])) {
      $cache['source'] = (string)($cache['source'] ?? 'ppp_active_cache');
      if (!empty($cache['stale'])) {
        $cache['source'] = 'stale_' . $cache['source'];
      }
    }
    return $cache;
  }
}

if (!function_exists('ua_ppp_cache_lock_acquire')) {
  function ua_ppp_cache_lock_acquire(int $ttl = 10) {
    $lock = ua_ppp_cache_lock();
    if (is_file($lock) && (time() - (int)@filemtime($lock)) < $ttl) return false;
    $fh = @fopen($lock, 'c+');
    if (!$fh) return false;
    if (!@flock($fh, LOCK_EX | LOCK_NB)) { @fclose($fh); return false; }
    @ftruncate($fh, 0); @fwrite($fh, (string)getmypid()); @touch($lock);
    return $fh;
  }
}

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

if (!function_exists('ua_ppp_cache_write')) {
  function ua_ppp_cache_write(array $data): bool {
    $data['ts'] = (int)($data['ts'] ?? time());
    $tmp = ua_ppp_cache_file() . '.tmp';
    $ok = @file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT), LOCK_EX);
    if ($ok === false) return false;
    return @rename($tmp, ua_ppp_cache_file());
  }
}

if (!function_exists('ua_ppp_cache_rebuild')) {
  function ua_ppp_cache_rebuild(mysqli $conn, int $minAge = 10, bool $force = false): array {
    $cached = ua_ppp_cache_read(86400);
    if (!$force && !empty($cached['ok']) && (int)($cached['age'] ?? 999999) < $minAge) return $cached;

    $lock = ua_ppp_cache_lock_acquire(max(5, $minAge));
    if (!$lock) return $cached;

    $routerConnected = false;
    try {
      if (function_exists('mt_require_router_online_or_throw')) {
        mt_require_router_online_or_throw($conn, 'تحديث كاش الجلسات النشطة');
      }
      $activeRows = function_exists('mt_ppp_active') ? mt_ppp_active($conn) : [];
      if (!is_array($activeRows)) $activeRows = [];
      if (function_exists('mt_ppp_active') && (($GLOBALS['mt_ppp_active_last_ok'] ?? false) !== true)) {
        throw new RuntimeException((string)($GLOBALS['mt_last_error'] ?? 'تعذر قراءة جلسات PPP من الراوتر'));
      }
      $routerConnected = true;

      $normalized = ua_ppp_normalize_active_rows($activeRows);
      $rows = $normalized['rows'];

      if (function_exists('q_sync_monthly_usage_from_active_rows')) {
        try {
          q_sync_monthly_usage_from_active_rows($conn, date('Y-m'), $rows);
        } catch (Throwable $e) {
          ua_ppp_cache_log('usage sync failed: '.$e->getMessage());
        }
      }
      $userList = $normalized['users'];
      $now = date('Y-m-d H:i:s');

      // مهم للأداء: لا نعالج MAC/تنبيهات من كاش صفحة المشتركين.
      // هذا الملف يُستدعى تلقائياً كل 10 ثواني تقريباً، لذلك نكتفي بتحديث حالة online/offline.
      // معالجة MAC والسجل والتنبيهات تعمل من cron/mikrotik_watch_worker.php.

      if (ua_col_exists($conn, 'subscribers', 'is_online')) {
        $hasDel = ua_col_exists($conn,'subscribers','is_deleted');
        $hasSeen = ua_col_exists($conn,'subscribers','last_seen_at');
        $hasDisc = ua_col_exists($conn,'subscribers','last_disconnect_at');
        $hasPpp = ua_col_exists($conn,'subscribers','ppp_username');
        // نفس التطبيع المستخدم في العرض: تجاهل الدومين بعد @ حتى لا تفشل المطابقة.
        $userExpr = $hasPpp ? "SUBSTRING_INDEX(COALESCE(NULLIF(ppp_username,''), username),'@',1)" : "SUBSTRING_INDEX(username,'@',1)";
        $delCond = $hasDel ? " AND COALESCE(is_deleted,0)=0" : "";
        $wentOnline = [];
        $wentOffline = [];
        if ($userList) {
          $in = "'" . implode("','", array_map([$conn, 'real_escape_string'], $userList)) . "'";
          $qOff = @$conn->query("SELECT id, {$userExpr} AS ppp_user FROM subscribers WHERE COALESCE(is_online,0)=1 {$delCond} AND {$userExpr} NOT IN ({$in})");
          if ($qOff) while ($r = $qOff->fetch_assoc()) $wentOffline[] = ['id'=>(int)$r['id'], 'user'=>(string)($r['ppp_user'] ?? '')];
          $qOn = @$conn->query("SELECT id, {$userExpr} AS ppp_user FROM subscribers WHERE COALESCE(is_online,0)=0 {$delCond} AND {$userExpr} IN ({$in})");
          if ($qOn) while ($r = $qOn->fetch_assoc()) $wentOnline[] = ['id'=>(int)$r['id'], 'user'=>(string)($r['ppp_user'] ?? '')];

          @$conn->query("UPDATE subscribers SET is_online=0".($hasDisc ? ", last_disconnect_at='{$now}'" : "")." WHERE COALESCE(is_online,0)=1 {$delCond} AND {$userExpr} NOT IN ({$in})");
          @$conn->query("UPDATE subscribers SET is_online=1".($hasSeen ? ", last_seen_at='{$now}'" : "")." WHERE 1=1 {$delCond} AND {$userExpr} IN ({$in})");
        } else {
          $qOff = @$conn->query("SELECT id, {$userExpr} AS ppp_user FROM subscribers WHERE COALESCE(is_online,0)=1 {$delCond}");
          if ($qOff) while ($r = $qOff->fetch_assoc()) $wentOffline[] = ['id'=>(int)$r['id'], 'user'=>(string)($r['ppp_user'] ?? '')];
          @$conn->query("UPDATE subscribers SET is_online=0".($hasDisc ? ", last_disconnect_at='{$now}'" : "")." WHERE COALESCE(is_online,0)=1 {$delCond}");
        }

        if (function_exists('sh_log_online_state')) {
          foreach ($wentOnline as $it) if ((int)$it['id'] > 0) @sh_log_online_state($conn, (int)$it['id'], true, $now, 'تم تسجيل اتصال المشترك');
          foreach ($wentOffline as $it) if ((int)$it['id'] > 0) @sh_log_online_state($conn, (int)$it['id'], false, $now, 'تم تسجيل فصل/انقطاع المشترك');
        }
      }

      $data = [
        'ok' => true,
        'ts' => time(),
        'observed_ts' => time(),
        'last_success_ts' => time(),
        'source' => 'mikrotik_cache_10s',
        'router_online' => true,
        'connection_state' => 'online',
        'live_data_reliable' => true,
        'data_available' => true,
        'online' => count($userList),
        'users' => $userList,
        'rows' => $rows,
        'message' => 'cache refreshed',
      ];
      ua_ppp_cache_log_recovered();
      ua_ppp_cache_write($data);

      // Keep the old footer quick cache in sync, so all old boxes read the same number.
      if (function_exists('mt_runtime_cache_write')) {
        $fc = function_exists('mt_footer_counts_quick') ? mt_footer_counts_quick($conn) : [];
        if (!is_array($fc)) $fc = [];
        $fc['ok'] = true;
        $fc['ts'] = time();
        $fc['online'] = count($userList);
        $fc['router_online'] = true;
        $fc['live_data_reliable'] = true;
        $fc['source_online'] = 'ppp_active_cache';
        if (isset($fc['total'])) $fc['offline'] = max(0, (int)$fc['total'] - (int)$fc['online']);
        mt_runtime_cache_write('footer_counts', $fc);
      }
      return ua_ppp_cache_read(86400);
    } catch (Throwable $e) {
      ua_ppp_cache_log('refresh failed: '.$e->getMessage());
      $lastVerified = ua_ppp_recent_router_command_snapshot($conn, 3600);
      if (empty($lastVerified)) {
        $cachedSource = (string)($cached['source'] ?? '');
        if (!empty($cached['data_available']) || (!empty($cached['ok']) && isset($cached['users']) && !in_array($cachedSource, ['router_unreachable_no_snapshot','router_offline'], true))) {
          $lastVerified = $cached;
        }
      }
      if (!empty($lastVerified['data_available']) && (time() - (int)($lastVerified['observed_ts'] ?? 0)) <= 3600) {
        ua_ppp_reconcile_verified_users($conn, (array)($lastVerified['users'] ?? []));
      }
      return ua_ppp_router_offline_snapshot($conn, $e->getMessage(), $lastVerified);
    } finally {
      ua_ppp_cache_lock_release($lock);
    }
  }
}

if (!function_exists('ua_ppp_cache_active_usernames')) {
  function ua_ppp_cache_active_usernames(mysqli $conn, int $maxAge = 10, bool $allowRefresh = false): array {
    $cache = $allowRefresh ? ua_ppp_cache_rebuild($conn, $maxAge, false) : ua_ppp_cache_snapshot(max(3600, $maxAge));
    $users = $cache['users'] ?? [];
    return is_array($users) ? array_values(array_filter(array_map('strval', $users))) : [];
  }
}

if (!function_exists('ua_ppp_cache_stats')) {
  function ua_ppp_cache_stats(mysqli $conn, int $maxAge = 10, bool $allowRefresh = false): array {
    $cache = $allowRefresh ? ua_ppp_cache_rebuild($conn, $maxAge, false) : ua_ppp_cache_snapshot(max(3600, $maxAge));
    return [
      'online' => (int)($cache['online'] ?? 0),
      'source' => (string)($cache['source'] ?? 'ppp_active_cache'),
      'age' => (int)($cache['age'] ?? 0),
      'ok' => !empty($cache['ok']),
    ];
  }
}
