<?php
// admin/mikrotik_lib.php

require_once __DIR__ . '/mikrotik_connection_lib.php';


/* ===========================
   DB
=========================== */

function mt_db(){

    if(isset($GLOBALS['conn']) && $GLOBALS['conn'] instanceof mysqli){
        return $GLOBALS['conn'];
    }

    $boot = __DIR__.'/db_bootstrap.php';

    if(file_exists($boot)){
        require_once $boot;

        if(function_exists('ensure_db')){
            $c = @ensure_db(true);
            if($c instanceof mysqli){
                $GLOBALS['conn']=$c;
                return $c;
            }
        }
    }

    return null;
}




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

if (!function_exists('mt_runtime_cache_read')) {
  function mt_runtime_cache_read(string $key, int $ttl = 5): ?array {
    $path = mt_runtime_cache_dir() . '/' . preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $key) . '.json';
    if (!is_file($path)) return null;
    if ($ttl >= 0) {
      $age = time() - (int)@filemtime($path);
      if ($age > $ttl) return null;
    }
    $raw = @file_get_contents($path);
    $data = json_decode((string)$raw, true);
    return is_array($data) ? $data : null;
  }
}

if (!function_exists('mt_runtime_cache_write')) {
  function mt_runtime_cache_write(string $key, array $data): void {
    $path = mt_runtime_cache_dir() . '/' . preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $key) . '.json';
    @file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  }
}

if (!function_exists('mt_table_exists_fast')) {
  function mt_table_exists_fast(mysqli $conn, string $table): bool {
    static $memo = [];
    $k = $table;
    if (isset($memo[$k])) return $memo[$k];
    $tableEsc = $conn->real_escape_string($table);
    $r = @$conn->query("SHOW TABLES LIKE '{$tableEsc}'");
    return $memo[$k] = (bool)($r && $r->num_rows > 0);
  }
}

if (!function_exists('mt_col_exists_fast')) {
  function mt_col_exists_fast(mysqli $conn, string $table, string $col): bool {
    static $memo = [];
    $k = $table . '.' . $col;
    if (isset($memo[$k])) return $memo[$k];
    $tableEsc = $conn->real_escape_string($table);
    $colEsc = $conn->real_escape_string($col);
    $r = @$conn->query("SHOW COLUMNS FROM `{$tableEsc}` LIKE '{$colEsc}'");
    return $memo[$k] = (bool)($r && $r->num_rows > 0);
  }
}

if (!function_exists('mt_footer_counts_peek')) {
  function mt_footer_counts_peek(): ?array {
    return mt_runtime_cache_read('footer_counts', 3600);
  }
}


if (!function_exists('mt_footer_counts_quick')) {
  function mt_footer_counts_quick($conn = null): array {
    $cached = mt_footer_counts_peek();
    if (is_array($cached) && !empty($cached)) {
      if (($cached['source_unsafe'] ?? '') !== 'mikrotik_limits') $cached['unsafe_speed'] = 0;
      return $cached;
    }

    $db = ($conn instanceof mysqli) ? $conn : mt_db();
    $out = [
      'ok' => true,
      'ts' => time(),
      'total' => 0,
      'online' => 0,
      'offline' => 0,
      'disabled' => 0,
      'unsafe_speed' => 0,
      'open_all_mode' => 0,
      'source_online' => 'db_quick',
      'source_disabled' => 'db_quick',
      'reason' => 'quick_bootstrap',
    ];

    if ($db instanceof mysqli && mt_table_exists_fast($db, 'subscribers')) {
      $baseWhere = mt_col_exists_fast($db, 'subscribers', 'is_deleted') ? 'WHERE COALESCE(is_deleted,0)=0' : '';
      $q = @$db->query("SELECT COUNT(*) c FROM subscribers {$baseWhere}");
      if ($q && ($row = $q->fetch_assoc())) {
        $out['total'] = (int)($row['c'] ?? 0);
      }

      if (mt_col_exists_fast($db, 'subscribers', 'is_online')) {
        $onlineWhere = $baseWhere ? ($baseWhere . ' AND COALESCE(is_online,0)=1') : 'WHERE COALESCE(is_online,0)=1';
        $q = @$db->query("SELECT COUNT(*) c FROM subscribers {$onlineWhere}");
        if ($q && ($row = $q->fetch_assoc())) {
          $out['online'] = (int)($row['c'] ?? 0);
        }
      }

      $cutExprs = [];
      if (mt_col_exists_fast($db, 'subscribers', 'ppp_is_disabled')) $cutExprs[] = 'COALESCE(ppp_is_disabled,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'mt_disabled')) $cutExprs[] = 'COALESCE(mt_disabled,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'is_cut')) $cutExprs[] = 'COALESCE(is_cut,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'speed_state')) $cutExprs[] = "UPPER(COALESCE(speed_state,''))='CUT'";
      if (mt_col_exists_fast($db, 'subscribers', 'status')) $cutExprs[] = "UPPER(COALESCE(status,'')) IN ('CUT','DISABLED','STOPPED')";
      if (!empty($cutExprs)) {
        $whereParts = [];
        if ($baseWhere !== '') $whereParts[] = preg_replace('/^WHERE\s+/i', '', $baseWhere);
        $whereParts[] = '(' . implode(' OR ', $cutExprs) . ')';
        $q = @$db->query("SELECT COUNT(*) c FROM subscribers WHERE " . implode(' AND ', $whereParts));
        if ($q && ($row = $q->fetch_assoc())) {
          $out['disabled'] = (int)($row['c'] ?? 0);
        }
      }

      if (function_exists('setting_get')) {
        try {
          $out['open_all_mode'] = ((string)setting_get($db, 'ppp_open_all_mode', '0') === '1') ? 1 : 0;
        } catch (Throwable $e) {}
      }
    }

    $out['offline'] = max(0, (int)$out['total'] - (int)$out['online']);
    mt_runtime_cache_write('footer_counts', $out);
    return $out;
  }
}

if (!function_exists('mt_footer_counts')) {
  function mt_footer_counts($conn = null, int $ttl = 5): array {
    static $mem = null;
    static $memTs = 0;
    $now = time();
    if (is_array($mem) && ($now - $memTs) <= $ttl) return $mem;

    $cached = mt_runtime_cache_read('footer_counts', $ttl);
    $staleCached = mt_runtime_cache_read('footer_counts', 3600);
    if (is_array($cached)) {
      if (($cached['source_unsafe'] ?? '') !== 'mikrotik_limits') $cached['unsafe_speed'] = 0;
      $mem = $cached;
      $memTs = $now;
      return $cached;
    }

    $db = ($conn instanceof mysqli) ? $conn : mt_db();
    $out = [
      'ok' => true,
      'ts' => $now,
      'total' => 0,
      'online' => 0,
      'offline' => 0,
      'disabled' => 0,
      'unsafe_speed' => 0,
      'open_all_mode' => 0,
      'source_online' => 'none',
      'source_disabled' => 'none',
      'source_unsafe' => 'none',
      'reason' => '',
    ];

    if ($db instanceof mysqli && mt_table_exists_fast($db, 'subscribers')) {
      $whereBase = mt_col_exists_fast($db, 'subscribers', 'is_deleted') ? 'WHERE is_deleted=0' : '';
      $q = @$db->query("SELECT COUNT(*) c FROM subscribers {$whereBase}");
      if ($q && ($row = $q->fetch_assoc())) $out['total'] = (int)($row['c'] ?? 0);

      if (function_exists('setting_get')) {
        try {
          $out['open_all_mode'] = ((string)setting_get($db, 'ppp_open_all_mode', '0') === '1') ? 1 : 0;
        } catch (Throwable $e) {}
      }

      if (function_exists('mt_ppp_users_without_limits')) {
        $localUsers = [];
        $rs = @$db->query("SELECT username, ppp_username FROM subscribers {$whereBase}");
        if ($rs) {
          while ($r = $rs->fetch_assoc()) {
            $u = trim((string)($r['ppp_username'] ?? ''));
            if ($u === '') $u = trim((string)($r['username'] ?? ''));
            if ($u !== '') $localUsers[] = $u;
          }
        }
        try {
          $out['unsafe_speed'] = count(mt_ppp_users_without_limits($db, $localUsers));
          $out['source_unsafe'] = 'mikrotik_limits';
        } catch (Throwable $e) {
          $out['unsafe_speed'] = 0;
          $out['source_unsafe'] = 'mikrotik_error';
        }
      }
    }

    $api = mt_api_connect($db);
    if ($api) {
      try {
        $rows = $api->command(['/ppp/active/print']);
        $active = 0;
        foreach ((array)$rows as $r) {
          if (($r['.type'] ?? '') === '!re') $active++;
        }
        $out['online'] = $active;
        $out['source_online'] = 'mikrotik';
      } catch (Throwable $e) {
        $out['reason'] = $e->getMessage();
      }

      try {
        $rows = $api->command(['/ppp/secret/print', '?disabled=yes']);
        $disabled = 0;
        foreach ((array)$rows as $r) {
          if (($r['.type'] ?? '') !== '!re') continue;
          $flag = strtolower((string)($r['disabled'] ?? ''));
          if ($flag === 'yes' || $flag === 'true') $disabled++;
        }
        $out['disabled'] = $disabled;
        $out['source_disabled'] = 'mikrotik';
      } catch (Throwable $e) {
        if ($out['reason'] === '') $out['reason'] = $e->getMessage();
      }

      try { $api->disconnect(); } catch (Throwable $e) {}
    } elseif ($db instanceof mysqli && mt_table_exists_fast($db, 'subscribers')) {
      $where = [];
      if (mt_col_exists_fast($db, 'subscribers', 'is_deleted')) $where[] = 'COALESCE(is_deleted,0)=0';
      $cutExprs = [];
      if (mt_col_exists_fast($db, 'subscribers', 'ppp_is_disabled')) $cutExprs[] = 'COALESCE(ppp_is_disabled,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'mt_disabled')) $cutExprs[] = 'COALESCE(mt_disabled,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'is_cut')) $cutExprs[] = 'COALESCE(is_cut,0)=1';
      if (mt_col_exists_fast($db, 'subscribers', 'speed_state')) $cutExprs[] = "speed_state='CUT'";
      if (mt_col_exists_fast($db, 'subscribers', 'status')) $cutExprs[] = "status IN ('CUT','cut','DISABLED','disabled','stopped','STOPPED')";
      if (!empty($cutExprs)) {
        $where[] = '(' . implode(' OR ', $cutExprs) . ')';
        $sqlWhere = 'WHERE ' . implode(' AND ', $where);
        $r = @$db->query("SELECT COUNT(*) c FROM subscribers {$sqlWhere}");
        if ($r && ($row = $r->fetch_assoc())) {
          $out['disabled'] = (int)($row['c'] ?? 0);
          $out['source_disabled'] = 'db_fallback';
        }
      }
      if (mt_col_exists_fast($db, 'subscribers', 'is_online')) {
        $where2 = mt_col_exists_fast($db, 'subscribers', 'is_deleted') ? 'WHERE is_deleted=0 AND is_online=1' : 'WHERE is_online=1';
        $r = @$db->query("SELECT COUNT(*) c FROM subscribers {$where2}");
        if ($r && ($row = $r->fetch_assoc())) {
          $out['online'] = (int)($row['c'] ?? 0);
          $out['source_online'] = 'db_fallback';
        }
      }
    }

    if (($out['source_online'] === 'none' || $out['source_disabled'] === 'none' || !$out['total']) && is_array($staleCached)) {
      if ($out['source_online'] === 'none' && isset($staleCached['online'])) {
        $out['online'] = (int)$staleCached['online'];
        $out['source_online'] = (string)($staleCached['source_online'] ?? 'cache');
      }
      if ($out['source_disabled'] === 'none' && isset($staleCached['disabled'])) {
        $out['disabled'] = (int)$staleCached['disabled'];
        $out['source_disabled'] = (string)($staleCached['source_disabled'] ?? 'cache');
      }
      if (!$out['total'] && isset($staleCached['total'])) {
        $out['total'] = (int)$staleCached['total'];
      }
      // unsafe_speed intentionally is not restored from stale cache; it must come from MikroTik only.
      if (!isset($out['open_all_mode']) && isset($staleCached['open_all_mode'])) {
        $out['open_all_mode'] = (int)$staleCached['open_all_mode'];
      }
    }

    if ($out['online'] > $out['total'] && $out['total'] > 0) {
      $out['online'] = $out['total'];
    }
    $out['offline'] = max(0, (int)$out['total'] - (int)$out['online']);

    mt_runtime_cache_write('footer_counts', $out);
    $mem = $out;
    $memTs = $now;
    return $out;
  }
}

/* ===========================
   Settings
=========================== */

if (!function_exists('mt_ensure_settings_table')) {
function mt_ensure_settings_table(mysqli $db): void {
    try {
        @$db->query("CREATE TABLE IF NOT EXISTS mikrotik_settings (
            id INT AUTO_INCREMENT PRIMARY KEY,
            host VARCHAR(120) NULL,
            username VARCHAR(120) NULL,
            password VARCHAR(255) NULL,
            api_port INT NOT NULL DEFAULT 8728,
            limit_speed VARCHAR(64) NULL DEFAULT '512k/512k',
            cut_speed VARCHAR(64) NULL DEFAULT '512k/512k',
            created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    } catch (Throwable $e) {}
}
}

function mt_get_settings($conn = null){

    $db = ($conn instanceof mysqli) ? $conn : mt_db();
    if(!$db) return false;

    mt_ensure_settings_table($db);

    try {
        $q = @$db->query("SELECT * FROM mikrotik_settings LIMIT 1");
    } catch (Throwable $e) {
        return false;
    }
    $legacy = ($q && $q->num_rows) ? $q->fetch_assoc() : [];

    // The operator-facing routers table is the connection source of truth.
    // Keep the legacy row only for speed/queue defaults and old installs.
    try {
        $hasRouters = @$db->query("SHOW TABLES LIKE 'routers'");
        if ($hasRouters && $hasRouters->num_rows > 0) {
            $rq = @$db->query("SELECT * FROM routers WHERE is_enabled=1 AND role='pppoe' ORDER BY id ASC LIMIT 1");
            if ($rq && ($router = $rq->fetch_assoc())) {
                foreach (['id','name','host','username','password','api_port','role','is_enabled'] as $field) {
                    if (array_key_exists($field, $router)) $legacy[$field] = $router[$field];
                }
                $legacy['connection_source'] = 'routers.pppoe';
            }
        }
    } catch (Throwable $e) {}

    return !empty($legacy) ? $legacy : false;
}


/* ===========================
   Connect
=========================== */

function mt_api_connect($conn = null){

    $cfg = mt_get_settings($conn);
    if(!$cfg) {
        $GLOBALS['mt_last_error'] = 'إعدادات MikroTik غير موجودة في جدول mikrotik_settings.';
        return false;
    }

    $err = '';
    $api = ua_mt_connect_config($cfg, $err, 0.8);
    if (!$api) {
        $GLOBALS['mt_last_error'] = $err ?: ($GLOBALS['mt_last_error'] ?? 'تعذر الاتصال بالراوتر');
        return false;
    }
    return $api;
}

// Alias for backward-compat (some helpers call mt_api)
if (!function_exists('mt_api')) {
  function mt_api($conn = null){
    return mt_api_connect($conn);
  }
}

if (!function_exists('mt_router_online_status')) {
  function mt_router_online_status($conn = null, int $ttl = 4): array {
    $cached = mt_runtime_cache_read('router_online_status', $ttl);
    if (is_array($cached) && array_key_exists('ok', $cached)) return $cached;
    $out = ['ok' => false, 'message' => 'الراوتر غير متصل'];
    $api = mt_api_connect($conn);
    if ($api) {
      $out = ['ok' => true, 'message' => 'ok'];
      try { $api->disconnect(); } catch (Throwable $e) {}
    } else {
      $out['message'] = (string)($GLOBALS['mt_last_error'] ?? 'تعذر الاتصال بالراوتر');
    }
    mt_runtime_cache_write('router_online_status', $out + ['ts' => time()]);
    return $out;
  }
}

if (!function_exists('mt_require_router_online_or_throw')) {
  function mt_require_router_online_or_throw($conn = null, string $actionLabel = 'عملية مزامنة'): void {
    $st = mt_router_online_status($conn, 4);
    if (!($st['ok'] ?? false)) {
      $msg = trim((string)($st['message'] ?? 'تعذر الاتصال بالراوتر'));
      throw new RuntimeException($actionLabel . ' متوقف: ' . $msg);
    }
  }
}



/* ===========================
   PPP helpers (used by quota_runner + automation)
=========================== */

/**
 * Get active PPP sessions with counters.
 * Returns array of rows including: name, rx-bytes, tx-bytes, .id
 */
function mt_ppp_dynamic_iface_counters($api, array $needUsers = []): array {
    $wanted = [];
    foreach ($needUsers as $u) {
        $u = trim((string)$u);
        if ($u !== '') $wanted[$u] = true;
    }

    $commands = [];
    // بعض نسخ RouterOS/API تُظهر العدادات فقط عند الطلب بصيغة stats=yes.
    $commands[] = ['kind' => 'command', 'path' => ['/interface/print', '?dynamic=yes', '=.proplist=name,rx-byte,tx-byte']];
    $commands[] = ['kind' => 'command', 'path' => ['/interface/print', '?dynamic=yes', '=stats=yes', '=.proplist=name,rx-byte,tx-byte']];
    $commands[] = ['kind' => 'comm',    'path' => '/interface/print', 'params' => ['?dynamic' => 'yes', '.proplist' => 'name,rx-byte,tx-byte']];
    $commands[] = ['kind' => 'comm',    'path' => '/interface/print', 'params' => ['?dynamic' => 'yes', 'stats' => 'yes', '.proplist' => 'name,rx-byte,tx-byte']];

    foreach ($commands as $cmd) {
        try {
            if ($cmd['kind'] === 'command' && method_exists($api, 'command')) {
                $rows = $api->command($cmd['path']);
            } elseif ($cmd['kind'] === 'comm' && method_exists($api, 'comm')) {
                $rows = $api->comm($cmd['path'], $cmd['params']);
            } else {
                continue;
            }
        } catch (Throwable $e) {
            continue;
        }

        if (!is_array($rows) || empty($rows)) continue;

        $map = [];
        foreach ($rows as $ir) {
            if (is_array($ir) && isset($ir['.type']) && $ir['.type'] !== '!re') continue;
            $n = trim((string)($ir['name'] ?? ''));
            if ($n === '' || !preg_match('/^<pppoe-(.+)>$/', $n, $m)) continue;
            $u = trim((string)$m[1]);
            if ($u === '') continue;
            if (!empty($wanted) && !isset($wanted[$u])) continue;
            $rx = (int)($ir['rx-byte'] ?? $ir['rx-bytes'] ?? 0);
            $tx = (int)($ir['tx-byte'] ?? $ir['tx-bytes'] ?? 0);
            if ($rx < 0) $rx = 0;
            if ($tx < 0) $tx = 0;
            $map[$u] = ['rx' => $rx, 'tx' => $tx];
        }

        if (!empty($map)) return $map;
    }

    return [];
}

function mt_ppp_dynamic_queue_counters($api, array $needUsers = []): array {
    $wanted = [];
    foreach ($needUsers as $u) {
        $u = trim((string)$u);
        if ($u !== '') $wanted[$u] = true;
    }

    $commands = [];
    $commands[] = ['kind' => 'command', 'path' => ['/queue/simple/print', '?dynamic=yes', '=stats=yes', '=.proplist=name,target,bytes,total-bytes']];
    $commands[] = ['kind' => 'command', 'path' => ['/queue/simple/print', '=stats=yes', '=.proplist=name,target,bytes,total-bytes']];
    $commands[] = ['kind' => 'comm',    'path' => '/queue/simple/print', 'params' => ['?dynamic' => 'yes', 'stats' => 'yes', '.proplist' => 'name,target,bytes,total-bytes']];
    $commands[] = ['kind' => 'comm',    'path' => '/queue/simple/print', 'params' => ['stats' => 'yes', '.proplist' => 'name,target,bytes,total-bytes']];

    foreach ($commands as $cmd) {
        try {
            if ($cmd['kind'] === 'command' && method_exists($api, 'command')) {
                $rows = $api->command($cmd['path']);
            } elseif ($cmd['kind'] === 'comm' && method_exists($api, 'comm')) {
                $rows = $api->comm($cmd['path'], $cmd['params']);
            } else {
                continue;
            }
        } catch (Throwable $e) {
            continue;
        }

        if (!is_array($rows) || empty($rows)) continue;

        $map = [];
        foreach ($rows as $qr) {
            if (is_array($qr) && isset($qr['.type']) && $qr['.type'] !== '!re') continue;
            $hay = trim((string)($qr['name'] ?? '')) . ' ' . trim((string)($qr['target'] ?? ''));
            if (!preg_match('/<pppoe-([^>]+)>/', $hay, $m)) continue;
            $u = trim((string)$m[1]);
            if ($u === '') continue;
            if (!empty($wanted) && !isset($wanted[$u])) continue;

            $bytes = trim((string)($qr['bytes'] ?? ''));
            if ($bytes === '') $bytes = trim((string)($qr['total-bytes'] ?? ''));
            $parts = explode('/', $bytes);
            if (count($parts) !== 2) continue;
            $rx = max(0, (int)$parts[0]);
            $tx = max(0, (int)$parts[1]);
            if ($rx <= 0 && $tx <= 0) continue;
            $map[$u] = ['rx' => $rx, 'tx' => $tx];
        }

        if (!empty($map)) return $map;
    }

    return [];
}

function mt_ppp_active($conn = null): array {
    $GLOBALS['mt_ppp_active_last_ok'] = false;
    $api = mt_api_connect($conn);
    if(!$api) return [];
    try {
        // بعض الإصدارات لا تُرجع rx/tx ضمن /ppp/active مباشرة.
        // لذلك نستخدم عدادات الواجهات الديناميكية كـ fallback وبأكثر من صيغة متوافقة.
        $rows = $api->command(['/ppp/active/print']);

        $out = [];
        $needIface = [];

        foreach($rows as $r){
            if(($r['.type'] ?? '') !== '!re') continue;
            $name = (string)($r['name'] ?? ($r['user'] ?? ''));
            $rx = (int)($r['rx-bytes'] ?? $r['rx_bytes'] ?? 0);
            $tx = (int)($r['tx-bytes'] ?? $r['tx_bytes'] ?? 0);

            if(($rx <= 0 && $tx <= 0) && trim($name) !== ''){
              $needIface[trim($name)] = true;
            }

            $out[] = [
                'id' => $r['.id'] ?? '',
                'name' => $name,
                'address' => (string)($r['address'] ?? ''),
                'caller_id' => (string)($r['caller-id'] ?? $r['caller_id'] ?? ''),
                'uptime' => (string)($r['uptime'] ?? ''),
                'service' => (string)($r['service'] ?? ''),
                'rx_bytes' => $rx,
                'tx_bytes' => $tx,
            ];
        }

        if(!empty($needIface)){
          $ifaceMap = mt_ppp_dynamic_iface_counters($api, array_keys($needIface));
          foreach($out as &$row){
            $u = trim((string)($row['name'] ?? ''));
            if($u === '') continue;
            if(((int)$row['rx_bytes'] <= 0 && (int)$row['tx_bytes'] <= 0) && isset($ifaceMap[$u])){
              $row['rx_bytes'] = (int)$ifaceMap[$u]['rx'];
              $row['tx_bytes'] = (int)$ifaceMap[$u]['tx'];
            }
          }
          unset($row);

          $needQueue = [];
          foreach($out as $row){
            $u = trim((string)($row['name'] ?? ''));
            if($u !== '' && (int)($row['rx_bytes'] ?? 0) <= 0 && (int)($row['tx_bytes'] ?? 0) <= 0) {
              $needQueue[$u] = true;
            }
          }
          if(!empty($needQueue)){
            $queueMap = mt_ppp_dynamic_queue_counters($api, array_keys($needQueue));
            foreach($out as &$row){
              $u = trim((string)($row['name'] ?? ''));
              if($u === '') continue;
              if(((int)$row['rx_bytes'] <= 0 && (int)$row['tx_bytes'] <= 0) && isset($queueMap[$u])){
                $row['rx_bytes'] = (int)$queueMap[$u]['rx'];
                $row['tx_bytes'] = (int)$queueMap[$u]['tx'];
              }
            }
            unset($row);
          }
        }

        $GLOBALS['mt_ppp_active_last_ok'] = true;
        return $out;
    } finally {
        $api->disconnect();
    }
}


if (!function_exists('mt_billing_protected_mode_enabled')) {
  function mt_billing_protected_mode_enabled($conn): bool {
    if (!($conn instanceof mysqli)) return false;
    try {
      $rs = @ $conn->query("SHOW TABLES LIKE 'app_settings'");
      if (!($rs && $rs->num_rows)) return false;
      $r = @ $conn->query("SELECT setting_value FROM app_settings WHERE setting_key='billing_protected_mode' LIMIT 1");
      if ($r && $r->num_rows) {
        $v = (string)($r->fetch_assoc()['setting_value'] ?? '1');
        return $v !== '0';
      }
      return true;
    } catch (Throwable $e) {
      return false;
    }
  }
}

if (!function_exists('mt_is_billing_approval_execution')) {
  function mt_is_billing_approval_execution(): bool {
    return defined('ALLOW_BILLING_APPROVAL_EXECUTION') && ALLOW_BILLING_APPROVAL_EXECUTION === true;
  }
}

if (!function_exists('mt_is_billing_enforcement_rate')) {
  function mt_is_billing_enforcement_rate($conn, string $rateLimit): bool {
    if (!($conn instanceof mysqli)) return false;
    $rateLimit = trim($rateLimit);
    if ($rateLimit === '') return false;
    try {
      $rs = @ $conn->query("SHOW TABLES LIKE 'mikrotik_settings'");
      if (!($rs && $rs->num_rows)) return false;
      $row = @ $conn->query("SELECT limit_speed, cut_speed FROM mikrotik_settings ORDER BY id DESC LIMIT 1");
      $row = ($row && $row->num_rows) ? $row->fetch_assoc() : [];
      $limit = trim((string)($row['limit_speed'] ?? '512k/512k'));
      $cut = trim((string)($row['cut_speed'] ?? '0/0'));
      return ($rateLimit !== '' && ($rateLimit === $limit || $rateLimit === $cut));
    } catch (Throwable $e) {
      return false;
    }
  }
}

/**
 * Set rate-limit for PPP secret by username.
 */
function mt_ppp_secret_set_rate($conn, string $username, string $rateLimit): bool {
    $username = trim($username);
    $rateLimit = trim($rateLimit);
    if($username==='') return false;
    if (mt_billing_protected_mode_enabled($conn) && !mt_is_billing_approval_execution() && mt_is_billing_enforcement_rate($conn, $rateLimit)) {
        $GLOBALS['mt_last_error'] = 'تم منع تطبيق سرعة التخفيض/القطع تلقائياً بسبب وضع الحماية القصوى';
        return false;
    }
    $api = mt_api_connect($conn);
    if(!$api) return false;
    try {
        // find secret
        $resp = $api->command(['/ppp/secret/print', '?name='.$username]);
        $sid = '';
        foreach($resp as $row){
            if(($row['.type'] ?? '') !== '!re') continue;
            $sid = (string)($row['.id'] ?? '');
            break;
        }
        if($sid===''){
            $GLOBALS['mt_last_error'] = 'PPPoE secret غير موجود للمشترك: '.$username;
            return false;
        }
        $api->command(['/ppp/secret/set', '=.id='.$sid, '=rate-limit='.$rateLimit]);
        return true;
    } finally {
        $api->disconnect();
    }
}


// Enable/Disable PPP secret (Cut/Restore).
if (!function_exists('mt_ppp_secret_set_disabled')) {
  function mt_ppp_secret_set_disabled($conn, string $username, bool $disabled): bool {
    try {
      if ($disabled && mt_billing_protected_mode_enabled($conn) && !mt_is_billing_approval_execution() && !defined('ALLOW_MANUAL_PPP_CUT_EXECUTION')) {
        $GLOBALS['mt_last_error'] = 'تم منع القطع التلقائي بسبب وضع الحماية القصوى';
        return false;
      }
      $api = mt_api($conn);
      if (!$api) return false;
      $u = trim($username);
      if ($u === '') return true;
      $rows = $api->command(['/ppp/secret/print', '?name='.$u]);
      if (empty($rows)) return true;
      $id = $rows[0]['.id'] ?? null;
      if (!$id) return true;
      $flag = $disabled ? 'yes' : 'no';
      $api->command(['/ppp/secret/set', '=.id='.$id, '=disabled='.$flag]);
      return true;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}

/**
 * Disconnect active session (if any) by username.
 */
function mt_ppp_active_disconnect($conn, string $username): bool {
    $username = trim($username);
    if($username==='') return false;
    $api = mt_api_connect($conn);
    if(!$api) return false;
    try {
        $act = $api->command(['/ppp/active/print', '?name='.$username]);
        foreach($act as $row){
            if(($row['.type'] ?? '') !== '!re') continue;
            $aid = (string)($row['.id'] ?? '');
            if($aid!==''){
                $api->command(['/ppp/active/remove', '=.id='.$aid]);
            }
            break;
        }
        return true;
    } finally {
        $api->disconnect();
    }
}

// ---------------- PPPoE helpers ----------------

if (!function_exists('mt_rows_has_error')) {
  function mt_rows_has_error(array $rows, string &$msg = ''): bool {
    foreach ($rows as $row) {
      if (!is_array($row)) continue;
      $t = (string)($row['.type'] ?? '');
      if ($t === '!trap' || $t === '!fatal') {
        $msg = (string)($row['message'] ?? $row['category'] ?? 'RouterOS trap');
        return true;
      }
    }
    return false;
  }
}

if (!function_exists('mt_is_unlimited_profile_name')) {
  function mt_is_unlimited_profile_name(string $profile): bool {
    $p = trim($profile);
    if ($p === '') return false;
    return (bool)preg_match('/(^|[-_])UNLIMITED($|[-_])|LASER-UNLIMITED/i', $p);
  }
}

if (!function_exists('mt_ppp_profiles_map')) {
  function mt_ppp_profiles_map($conn): array {
    $disconnect = false;
    $api = null;
    if (is_object($conn) && ($conn instanceof RouterOSAPI)) {
      $api = $conn;
    } else {
      $api = mt_api($conn);
      $disconnect = (bool)$api;
    }
    if (!$api) return [];
    try {
      $rows = $api->command(['/ppp/profile/print']);
      $map = [];
      foreach ($rows as $r) {
        if (($r['.type'] ?? '') !== '!re') continue;
        $name = trim((string)($r['name'] ?? ''));
        if ($name === '') continue;
        $map[$name] = [
          'id' => (string)($r['.id'] ?? ''),
          'rate_limit' => trim((string)($r['rate-limit'] ?? '')),
          'only_one' => (string)($r['only-one'] ?? ''),
        ];
      }
      return $map;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return [];
    } finally {
      if ($disconnect && $api) $api->disconnect();
    }
  }
}

if (!function_exists('mt_ppp_secrets_map')) {
  function mt_ppp_secrets_map($conn, bool $pppoeOnly = true): array {
    $GLOBALS['mt_ppp_secrets_last_ok'] = false;
    $disconnect = false;
    $api = null;
    if (is_object($conn) && ($conn instanceof RouterOSAPI)) {
      $api = $conn;
    } else {
      $api = mt_api($conn);
      $disconnect = (bool)$api;
    }
    if (!$api) return [];
    try {
      $rows = $api->command(['/ppp/secret/print']);
      $map = [];
      foreach ($rows as $r) {
        if (($r['.type'] ?? '') !== '!re') continue;
        $name = trim((string)($r['name'] ?? ''));
        if ($name === '') continue;
        $svc = trim((string)($r['service'] ?? ''));
        if ($pppoeOnly && $svc !== '' && strtolower($svc) !== 'pppoe') continue;
        $map[$name] = [
          'id' => (string)($r['.id'] ?? ''),
          'service' => $svc,
          'profile' => trim((string)($r['profile'] ?? '')),
          'rate_limit' => trim((string)($r['rate-limit'] ?? '')),
          'disabled' => ((string)($r['disabled'] ?? 'false') === 'true') || ((string)($r['disabled'] ?? '') === 'yes'),
          'comment' => (string)($r['comment'] ?? ''),
          'last_logged_out' => (string)($r['last-logged-out'] ?? ''),
          'last_caller_id'  => (string)($r['last-caller-id'] ?? ''),
          'last_disconnect_reason' => (string)($r['last-disconnect-reason'] ?? ''),
        ];
      }
      $GLOBALS['mt_ppp_secrets_last_ok'] = true;
      return $map;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return [];
    } finally {
      if ($disconnect && $api) $api->disconnect();
    }
  }
}
if (!function_exists('mt_ppp_secret_reality_map_cached')) {
  function mt_ppp_secret_reality_map_cached($conn, int $ttl = 12, bool $allowRefresh = true): array {
    $ttl = max(3, (int)$ttl);
    $dir = __DIR__ . '/cache/runtime';
    if (!is_dir($dir)) { @mkdir($dir, 0777, true); }
    $file = $dir . '/ppp_secret_reality_map.json';
    $now = time();

    $lastPayload = null;
    if (is_file($file)) {
      $raw = @file_get_contents($file);
      $json = json_decode((string)$raw, true);
      if (is_array($json) && isset($json['map']) && is_array($json['map'])) $lastPayload = $json;
    }
    if (is_array($lastPayload) && (($now - (int)@filemtime($file)) <= $ttl)) {
      return $lastPayload['map'];
    }

    if (!$allowRefresh) {
      if (is_array($lastPayload)) return $lastPayload['map'];
      return [];
    }

    $secrets = mt_ppp_secrets_map($conn, true);
    if (($GLOBALS['mt_ppp_secrets_last_ok'] ?? false) !== true) {
      // A transport error is not proof that every secret disappeared or was
      // restored. Keep the last verified RouterOS map intact.
      return is_array($lastPayload) ? $lastPayload['map'] : [];
    }
    $map = [];
    foreach ((array)$secrets as $user => $row) {
      $u = preg_replace('/@.*/', '', trim((string)$user));
      if ($u === '') continue;
      $flag = $row['disabled'] ?? false;
      $last = trim((string)($row['last_logged_out'] ?? $row['last-logged-out'] ?? ''));
      $map[$u] = [
        'disabled' => ($flag === true || $flag === 1 || $flag === '1' || $flag === 'true' || $flag === 'yes'),
        'last_logged_out' => $last,
        'last_caller_id' => trim((string)($row['last_caller_id'] ?? $row['last-caller-id'] ?? '')),
      ];
    }

    @file_put_contents($file, json_encode(['ts' => $now, 'map' => $map], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    return $map;
  }
}



if (!function_exists('mt_ppp_disabled_state_map_cached')) {
  function mt_ppp_disabled_state_map_cached($conn, int $ttl = 12): array {
    $map = [];
    foreach (mt_ppp_secret_reality_map_cached($conn, max(3, $ttl), true) as $user => $row) {
      $map[(string)$user] = !empty($row['disabled']);
    }
    return $map;
  }
}

if (!function_exists('mt_ppp_effective_rate_limit')) {
  function mt_ppp_effective_rate_limit(array $secretRow, array $profilesMap = []): string {
    $direct = trim((string)($secretRow['rate_limit'] ?? $secretRow['rate-limit'] ?? ''));
    if ($direct !== '') return $direct;
    $profile = trim((string)($secretRow['profile'] ?? ''));
    if ($profile !== '' && isset($profilesMap[$profile])) {
      return trim((string)($profilesMap[$profile]['rate_limit'] ?? ''));
    }
    return '';
  }
}

if (!function_exists('mt_ppp_users_without_limits')) {
  function mt_ppp_users_without_limits($conn, array $usernames = []): array {
    $disconnect = false;
    $api = null;
    if (is_object($conn) && ($conn instanceof RouterOSAPI)) {
      $api = $conn;
    } else {
      $api = mt_api($conn);
      $disconnect = (bool)$api;
    }
    if (!$api) {
      throw new RuntimeException((string)($GLOBALS['mt_last_error'] ?? 'تعذر الاتصال بـ MikroTik لقراءة الحسابات غير المقيدة.'));
    }
    try {
      $GLOBALS['mt_last_error'] = '';
      $profiles = mt_ppp_profiles_map($api);
      $secrets  = mt_ppp_secrets_map($api, true);
      if (empty($secrets)) {
        $readError = trim((string)($GLOBALS['mt_last_error'] ?? ''));
        if ($readError !== '') throw new RuntimeException($readError);
        return [];
      }

      $wanted = [];
      foreach ($usernames as $u) {
        $u = trim((string)$u);
        if ($u === '') continue;
        $wanted[strtolower($u)] = true;
        $wanted[strtolower((string)preg_replace('/@.*/', '', $u))] = true;
      }

      $queueLimits = [];
      foreach (mt_simple_queues($api, 5000) as $q) {
        $qName = trim((string)($q['name'] ?? ''));
        if ($qName === '' || !preg_match('/^<pppoe-(.+)>$/', $qName, $m)) continue;
        $qUser = trim((string)$m[1]);
        if ($qUser === '') continue;
        if (($q['disabled'] ?? 'no') === 'true') continue;
        $limit = trim((string)($q['max_limit'] ?? $q['max-limit'] ?? ''));
        $queueLimits[strtolower($qUser)] = $limit;
        $queueLimits[strtolower((string)preg_replace('/@.*/', '', $qUser))] = $limit;
      }

      $out = [];
      foreach ($secrets as $name => $row) {
        $nameKey = strtolower(trim((string)$name));
        $nameBase = strtolower((string)preg_replace('/@.*/', '', $nameKey));
        if ($wanted && !isset($wanted[$nameKey]) && !isset($wanted[$nameBase])) continue;
        if (!empty($row['disabled'])) continue;

        $profile = trim((string)($row['profile'] ?? ''));
        $effective = trim((string)mt_ppp_effective_rate_limit($row, $profiles));
        $queueLimit = trim((string)($queueLimits[$nameKey] ?? $queueLimits[$nameBase] ?? ''));

        $hasProfileLimit = ($effective !== '');
        $hasQueueLimit   = ($queueLimit !== '' && $queueLimit !== '0/0');

        if (!$hasProfileLimit && !$hasQueueLimit) {
          $row['effective_rate_limit'] = $effective;
          $row['queue_limit'] = $queueLimit;
          $row['is_unlimited_profile'] = mt_is_unlimited_profile_name($profile) ? 1 : 0;
          $out[$name] = $row;
        }
      }
      return $out;
    } finally {
      if ($disconnect && $api) $api->disconnect();
    }
  }
}

if (!function_exists('mt_ppp_profile_rate_limit')) {
  function mt_ppp_profile_rate_limit($conn, string $profile): string {
    $p = trim($profile);
    if ($p === '') return '';
    $profiles = mt_ppp_profiles_map($conn);
    return trim((string)($profiles[$p]['rate_limit'] ?? ''));
  }
}

if (!function_exists('mt_ppp_profile_is_safe')) {
  function mt_ppp_profile_is_safe($conn, string $profile, string $fallbackRateLimit = ''): bool {
    $p = trim($profile);
    if ($p === '') return false;
    if (mt_is_unlimited_profile_name($p)) return true;
    $rl = trim($fallbackRateLimit);
    if ($rl === '') $rl = mt_ppp_profile_rate_limit($conn, $p);
    return $rl !== '';
  }
}

if (!function_exists('mt_api_rows_timed_out')) {
  function mt_api_rows_timed_out($rows): bool {
    if (!is_array($rows)) return false;
    foreach ($rows as $r) {
      if (!is_array($r)) continue;
      $msg = (string)($r['message'] ?? '');
      if (($r['.type'] ?? '') === '!trap' && (stripos($msg, 'مهلة') !== false || stripos($msg, 'timeout') !== false)) {
        return true;
      }
    }
    return false;
  }
}

if (!function_exists('mt_api_find_first_re')) {
  function mt_api_find_first_re($rows): ?array {
    if (!is_array($rows)) return null;
    foreach ($rows as $r) {
      if (is_array($r) && (($r['.type'] ?? '') === '!re')) return $r;
    }
    return null;
  }
}

if (!function_exists('mt_ppp_find_profile_row')) {
  function mt_ppp_find_profile_row($api, string $profile): ?array {
    $p = trim($profile);
    if ($p === '' || !$api) return null;
    $msg = '';
    $rows = $api->command(['/ppp/profile/print', '?name='.$p]);
    if (mt_rows_has_error($rows, $msg)) {
      $GLOBALS['mt_last_error'] = $msg;
      return null;
    }
    return mt_api_find_first_re($rows);
  }
}

if (!function_exists('mt_ppp_find_secret_row')) {
  function mt_ppp_find_secret_row($api, string $username): ?array {
    $u = trim($username);
    if ($u === '' || !$api) return null;
    $msg = '';
    $rows = $api->command(['/ppp/secret/print', '?name='.$u]);
    if (mt_rows_has_error($rows, $msg)) {
      $GLOBALS['mt_last_error'] = $msg;
      return null;
    }
    return mt_api_find_first_re($rows);
  }
}

if (!function_exists('mt_ipv4_to_uint')) {
  function mt_ipv4_to_uint(string $ip): ?int {
    $ip = trim($ip);
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return null;
    $n = ip2long($ip);
    if ($n === false) return null;
    return (int)sprintf('%u', $n);
  }
}

if (!function_exists('mt_ipv4_in_range')) {
  function mt_ipv4_in_range(string $ip, string $range): bool {
    $n = mt_ipv4_to_uint($ip);
    if ($n === null) return false;
    $range = trim($range);
    if ($range === '') return false;
    if (strpos($range, '-') !== false) {
      [$a,$b] = array_map('trim', explode('-', $range, 2));
      $na = mt_ipv4_to_uint($a); $nb = mt_ipv4_to_uint($b);
      if ($na === null || $nb === null) return false;
      if ($na > $nb) { $t=$na; $na=$nb; $nb=$t; }
      return $n >= $na && $n <= $nb;
    }
    $one = mt_ipv4_to_uint($range);
    return $one !== null && $n === $one;
  }
}

if (!function_exists('mt_ppp_static_ip_preflight')) {
  /**
   * Check exact MikroTik duplicates for a PPPoE static IP.
   * pool_hits are returned as warnings because some networks deliberately reserve sub-ranges manually.
   */
  function mt_ppp_static_ip_preflight($conn, string $ip, string $currentUser=''): array {
    $ip = trim($ip);
    $currentUser = trim($currentUser);
    if ($ip === '') return ['ok'=>true, 'conflicts'=>[], 'pool_hits'=>[], 'message'=>''];
    try {
      $api = mt_api($conn);
      if (!$api) return ['ok'=>false, 'conflicts'=>[], 'pool_hits'=>[], 'message'=>$GLOBALS['mt_last_error'] ?? 'تعذر الاتصال بـ MikroTik'];

      $conflicts = [];
      $poolHits = [];
      $msg = '';

      $secretRows = $api->command(['/ppp/secret/print']);
      if (mt_rows_has_error($secretRows, $msg)) {
        return ['ok'=>false, 'conflicts'=>[], 'pool_hits'=>[], 'message'=>$msg ?: 'فشل فحص PPP secrets على MikroTik'];
      }
      foreach ($secretRows as $r) {
        if (!is_array($r) || (($r['.type'] ?? '') !== '!re')) continue;
        $u = trim((string)($r['name'] ?? ''));
        $ra = trim((string)($r['remote-address'] ?? ''));
        if ($ra === $ip && $u !== '' && $u !== $currentUser) {
          $conflicts[] = ['type'=>'secret', 'username'=>$u, 'detail'=>'PPP Secret'];
        }
      }

      $activeRows = $api->command(['/ppp/active/print']);
      if (mt_rows_has_error($activeRows, $msg)) {
        return ['ok'=>false, 'conflicts'=>[], 'pool_hits'=>[], 'message'=>$msg ?: 'فشل فحص الجلسات النشطة على MikroTik'];
      }
      foreach ($activeRows as $r) {
        if (!is_array($r) || (($r['.type'] ?? '') !== '!re')) continue;
        $u = trim((string)($r['name'] ?? ($r['user'] ?? '')));
        $addr = trim((string)($r['address'] ?? ''));
        if ($addr === $ip && $u !== '' && $u !== $currentUser) {
          $conflicts[] = ['type'=>'active', 'username'=>$u, 'detail'=>'PPP Active'];
        }
      }

      try {
        $poolRows = $api->command(['/ip/pool/print']);
        if (!mt_rows_has_error($poolRows, $msg)) {
          foreach ($poolRows as $r) {
            if (!is_array($r) || (($r['.type'] ?? '') !== '!re')) continue;
            $poolName = trim((string)($r['name'] ?? ''));
            $ranges = trim((string)($r['ranges'] ?? ''));
            if ($ranges === '') continue;
            foreach (array_map('trim', explode(',', $ranges)) as $range) {
              if ($range !== '' && mt_ipv4_in_range($ip, $range)) {
                $poolHits[] = ['name'=>$poolName, 'range'=>$range];
              }
            }
          }
        }
      } catch (Throwable $ignorePool) {}

      if (!empty($conflicts)) {
        $parts = [];
        foreach ($conflicts as $c) $parts[] = ($c['username'] ?? '-') . ' / ' . ($c['detail'] ?? 'MikroTik');
        return ['ok'=>false, 'conflicts'=>$conflicts, 'pool_hits'=>$poolHits, 'message'=>'هذا الـ IP مستخدم حالياً على MikroTik: '.implode('، ', $parts)];
      }
      return ['ok'=>true, 'conflicts'=>[], 'pool_hits'=>$poolHits, 'message'=>''];
    } catch (Throwable $e) {
      return ['ok'=>false, 'conflicts'=>[], 'pool_hits'=>[], 'message'=>$e->getMessage()];
    }
  }
}

if (!function_exists('mt_ppp_secret_set_remote_address')) {
  /** Set or unset PPP secret remote-address, then verify and disconnect active session to apply it. */
  function mt_ppp_secret_set_remote_address($conn, string $username, string $remoteAddress='', bool $unsetWhenEmpty=true): bool {
    try {
      $api = mt_api($conn);
      if (!$api) return false;
      $u = trim($username);
      $remoteAddress = trim($remoteAddress);
      if ($u === '') { $GLOBALS['mt_last_error'] = 'اسم حساب PPPoE فارغ'; return false; }
      $row = mt_ppp_find_secret_row($api, $u);
      if (!$row) { $GLOBALS['mt_last_error'] = 'PPPoE secret غير موجود للمشترك: '.$u; return false; }
      $sid = trim((string)($row['.id'] ?? ''));
      if ($sid === '') { $GLOBALS['mt_last_error'] = 'تعذر تحديد .id لحساب PPP الحالي'; return false; }

      $msg = '';
      if ($remoteAddress !== '') {
        $res = $api->command(['/ppp/secret/set', '=.id='.$sid, '=remote-address='.$remoteAddress]);
        if (mt_rows_has_error($res, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
      } elseif ($unsetWhenEmpty) {
        $res = $api->command(['/ppp/secret/unset', '=.id='.$sid, '=value-name=remote-address']);
        if (mt_rows_has_error($res, $msg)) {
          // Fallback for RouterOS/API variants that accept clearing via set.
          $res2 = $api->command(['/ppp/secret/set', '=.id='.$sid, '=remote-address=']);
          if (mt_rows_has_error($res2, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
        }
      }

      $verify = mt_ppp_find_secret_row($api, $u);
      if (!$verify) { $GLOBALS['mt_last_error'] = 'تعذر التحقق من حساب PPPoE بعد تعديل IP'; return false; }
      $final = trim((string)($verify['remote-address'] ?? ''));
      if ($remoteAddress !== '' && $final !== $remoteAddress) {
        $GLOBALS['mt_last_error'] = 'تم تنفيذ الأمر لكن remote-address النهائي لا يطابق الـ IP المطلوب';
        return false;
      }
      if ($remoteAddress === '' && $unsetWhenEmpty && $final !== '') {
        $GLOBALS['mt_last_error'] = 'تعذر إلغاء remote-address من حساب PPPoE';
        return false;
      }
      @mt_ppp_active_disconnect($conn, $u);
      return true;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}

if (!function_exists('mt_ppp_profile_exists')) {
  function mt_ppp_profile_exists($conn, string $profile): bool {
    try {
      $api = mt_api($conn);
      if (!$api) return false;
      return mt_ppp_find_profile_row($api, $profile) !== null;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}

if (!function_exists('mt_ppp_profile_ensure')) {
  /** Create or update a PPP profile safely. Refuse blank/non-limited profiles unless intentionally UNLIMITED. */
  function mt_ppp_profile_ensure($conn, string $profile, string $rateLimit=''): bool {
    try {
      $api = mt_api($conn);
      if (!$api) return false;
      $p = trim($profile);
      $rateLimit = trim($rateLimit);
      if ($p === '') { $GLOBALS['mt_last_error'] = 'اسم بروفايل PPP فارغ'; return false; }

      $row = mt_ppp_find_profile_row($api, $p);
      if (!$row) {
        if ($rateLimit === '' && !mt_is_unlimited_profile_name($p)) {
          $GLOBALS['mt_last_error'] = 'رفض إنشاء بروفايل بلا rate-limit';
          return false;
        }
        $args = ['/ppp/profile/add', '=name='.$p, '=only-one=yes'];
        if ($rateLimit !== '') $args[] = '=rate-limit='.$rateLimit;
        $res = $api->command($args);
        $msg = '';
        if (mt_rows_has_error($res, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
      } else {
        $args = ['/ppp/profile/set', '=.id='.(string)($row['.id'] ?? ''), '=only-one=yes'];
        if ($rateLimit !== '') $args[] = '=rate-limit='.$rateLimit;
        $res = $api->command($args);
        $msg = '';
        if (mt_rows_has_error($res, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
      }

      $verify = mt_ppp_find_profile_row($api, $p);
      if (!$verify) { $GLOBALS['mt_last_error'] = 'تعذر التأكد من وجود البروفايل على MikroTik'; return false; }
      $finalRate = trim((string)($verify['rate-limit'] ?? ''));
      if (!mt_is_unlimited_profile_name($p) && $finalRate === '') {
        $GLOBALS['mt_last_error'] = 'البروفايل موجود لكن بلا rate-limit، وتم رفضه لحماية التقييد';
        return false;
      }
      return true;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}

if (!function_exists('mt_ppp_secret_upsert')) {
  /** Create or update PPP secret (PPPoE) safely; refuse non-limited profiles. */
  function mt_ppp_secret_upsert($conn, string $username, string $password, string $profile='', string $remoteAddress='', string $comment='', string $expectedRateLimit=''): bool {
    try {
      $api = mt_api($conn);
      if (!$api) return false;
      $u = trim($username);
      $profile = trim($profile);
      $expectedRateLimit = trim($expectedRateLimit);
      if ($u === '') { $GLOBALS['mt_last_error']='PPPoE username فارغ'; return false; }
      if ($profile === '') { $GLOBALS['mt_last_error']='تم رفض إنشاء الحساب لأن PPP Profile فارغ'; return false; }

      $profileRow = mt_ppp_find_profile_row($api, $profile);
      if (!$profileRow) {
        $GLOBALS['mt_last_error'] = 'تعذر العثور على البروفايل على MikroTik قبل إنشاء الحساب';
        return false;
      }
      $effectiveProfileRate = trim((string)($profileRow['rate-limit'] ?? ''));
      if ($expectedRateLimit !== '' && $effectiveProfileRate === '') $effectiveProfileRate = $expectedRateLimit;
      if (!mt_is_unlimited_profile_name($profile) && $effectiveProfileRate === '') {
        $GLOBALS['mt_last_error'] = 'تم رفض إنشاء/تفعيل الحساب لأن البروفايل لا يملك rate-limit فعلي';
        return false;
      }

      $row = mt_ppp_find_secret_row($api, $u);
      if (!$row) {
        $args = ['/ppp/secret/add', '=name='.$u, '=password='.$password, '=service=pppoe', '=profile='.$profile];
        if (trim($remoteAddress) !== '') $args[] = '=remote-address='.trim($remoteAddress);
        if (trim($comment) !== '') $args[] = '=comment='.trim($comment);
        $res = $api->command($args);
        $msg = '';
        if (mt_rows_has_error($res, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
      } else {
        $sid = trim((string)($row['.id'] ?? ''));
        if ($sid === '') { $GLOBALS['mt_last_error'] = 'تعذر تحديد .id لحساب PPP الحالي'; return false; }
        $args = ['/ppp/secret/set', '=.id='.$sid, '=password='.$password, '=service=pppoe', '=profile='.$profile];
        if (trim($remoteAddress) !== '') $args[] = '=remote-address='.trim($remoteAddress);
        if (trim($comment) !== '') $args[] = '=comment='.trim($comment);
        $res = $api->command($args);
        $msg = '';
        if (mt_rows_has_error($res, $msg)) { $GLOBALS['mt_last_error'] = $msg; return false; }
      }

      $verify = mt_ppp_find_secret_row($api, $u);
      if (!$verify) { $GLOBALS['mt_last_error'] = 'لم يتم العثور على الحساب على MikroTik بعد الإضافة/التحديث'; return false; }
      $finalProfile = trim((string)($verify['profile'] ?? ''));
      if ($finalProfile !== $profile) {
        $GLOBALS['mt_last_error'] = 'تم إنشاء الحساب لكن البروفايل النهائي لا يطابق المطلوب';
        return false;
      }
      $requestedRemote = trim($remoteAddress);
      if ($requestedRemote !== '') {
        $finalRemote = trim((string)($verify['remote-address'] ?? ''));
        if ($finalRemote !== $requestedRemote) {
          $GLOBALS['mt_last_error'] = 'تم إنشاء/تحديث الحساب لكن الـ remote-address النهائي لا يطابق الـ IP المطلوب';
          return false;
        }
      }
      $directRate = trim((string)($verify['rate-limit'] ?? ''));
      $effective = $directRate !== '' ? $directRate : $effectiveProfileRate;
      if (!mt_is_unlimited_profile_name($profile) && trim($effective) === '') {
        $sid = (string)($verify['.id'] ?? '');
        if ($sid !== '') { @ $api->command(['/ppp/secret/set', '=.id='.$sid, '=disabled=yes']); }
        $GLOBALS['mt_last_error'] = 'الحساب أُنشئ لكن بدون rate-limit فعلي؛ تم رفض تفعيله لحماية التقييد';
        return false;
      }
      return true;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}

if (!function_exists('mt_ppp_secret_remove')) {
  function mt_ppp_secret_remove($conn, string $username): bool {
    try {
      $api = mt_api($conn);
      if (!$api) return false;
      $u = trim($username);
      if ($u === '') return true;
      $rows = $api->command(['/ppp/secret/print', '?name='.$u]);
      if (empty($rows)) return true;
      $id = $rows[0]['.id'] ?? null;
      if (!$id) return true;
      $api->command(['/ppp/secret/remove', '=.id='.$id]);
      // disconnect active session if exists
      @mt_ppp_active_disconnect($conn, $u);
      return true;
    } catch (Throwable $e) {
      $GLOBALS['mt_last_error'] = $e->getMessage();
      return false;
    }
  }
}


/* ===========================
   Test
=========================== */

function mt_api_test(){

    $api = mt_api_connect();

    if(!$api){
        return [
            'ok'=>false,
            'msg'=> (string)($GLOBALS['mt_last_error'] ?? 'فشل الاتصال بالراوتر')
        ];
    }

    try{
        $r = $api->command(['/system/identity/print']);
        $name = 'MikroTik';
        foreach($r as $row){
            if(($row['.type'] ?? '') === '!re'){
                $name = (string)($row['name'] ?? $name);
                break;
            }
        }

        $api->disconnect();

        return [
            'ok'=>true,
            'msg'=>'متصل: '.$name
        ];

    }catch(Throwable $e){

        return [
            'ok'=>false,
            'msg'=>'خطأ: '.$e->getMessage()
        ];
    }
}

/* ===========================
   Monitor + Queues helpers (used by dashboard/gauges)
=========================== */

/**
 * Monitor interface traffic (rx/tx bits per second).
 * @param mysqli|RouterOSAPI|null $connOrApi
 * @return array{rx_bps:int,tx_bps:int}
 */
function mt_monitor_traffic($connOrApi, string $iface): array {
    $iface = trim($iface);
    if($iface==='') return ['rx_bps'=>0,'tx_bps'=>0];

    $api = null;
    $owns = false;
    try{
        if (is_object($connOrApi) && ($connOrApi instanceof RouterOSAPI)) {
            $api = $connOrApi;
        } else {
            $api = mt_api_connect($connOrApi);
            $owns = true;
        }
        if(!$api) return ['rx_bps'=>0,'tx_bps'=>0];

        $rows = $api->command(['/interface/monitor-traffic', '=interface='.$iface, '=once=true']);
        $rx = 0; $tx = 0;
        foreach($rows as $r){
            if(($r['.type'] ?? '') !== '!re') continue;
            $rx = (int)($r['rx-bits-per-second'] ?? $r['rx-bits-per-sec'] ?? 0);
            $tx = (int)($r['tx-bits-per-second'] ?? $r['tx-bits-per-sec'] ?? 0);
            break;
        }
        return ['rx_bps'=>$rx,'tx_bps'=>$tx];
    } catch(Throwable $e){
        $GLOBALS['mt_last_error'] = $e->getMessage();
        return ['rx_bps'=>0,'tx_bps'=>0];
    } finally {
        if($owns && $api && method_exists($api,'disconnect')){
            try { $api->disconnect(); } catch(Throwable $e) {}
        }
    }
}

/**
 * Read simple queues (summary).
 */
function mt_simple_queues($connOrApi, int $limit = 100): array {
    $api = null;
    $owns = false;
    try{
        if (is_object($connOrApi) && ($connOrApi instanceof RouterOSAPI)) {
            $api = $connOrApi;
        } else {
            $api = mt_api_connect($connOrApi);
            $owns = true;
        }
        if(!$api) return [];

        $rows = $api->command(['/queue/simple/print']);
        $out = [];
        foreach($rows as $r){
            if(($r['.type'] ?? '') !== '!re') continue;
            $out[] = [
                'name' => (string)($r['name'] ?? ''),
                'target' => (string)($r['target'] ?? ''),
                'max_limit' => (string)($r['max-limit'] ?? ''),
                'bytes' => (string)($r['bytes'] ?? ''),
                'disabled' => (string)($r['disabled'] ?? 'no'),
            ];
            if(count($out) >= max(1,$limit)) break;
        }
        return $out;
    } catch(Throwable $e){
        $GLOBALS['mt_last_error'] = $e->getMessage();
        return [];
    } finally {
        if($owns && $api && method_exists($api,'disconnect')){
            try { $api->disconnect(); } catch(Throwable $e) {}
        }
    }
}

/**
 * List interfaces (name + type + running) for UI dropdowns.
 */
function mt_list_interfaces($connOrApi, int $limit = 500): array {
    $api = null;
    $owns = false;
    try{
        if (is_object($connOrApi) && ($connOrApi instanceof RouterOSAPI)) {
            $api = $connOrApi;
        } else {
            $api = mt_api_connect($connOrApi);
            $owns = true;
        }
        if(!$api) return [];

        $rows = $api->command(['/interface/print']);
        $out = [];
        foreach($rows as $r){
            if(($r['.type'] ?? '') !== '!re') continue;
            $out[] = [
                'name' => (string)($r['name'] ?? ''),
                'type' => (string)($r['type'] ?? ''),
                'running' => (string)($r['running'] ?? ''),
                'disabled' => (string)($r['disabled'] ?? ''),
            ];
            if(count($out) >= max(1,$limit)) break;
        }
        return $out;
    } catch(Throwable $e){
        $GLOBALS['mt_last_error'] = $e->getMessage();
        return [];
    } finally {
        if($owns && $api && method_exists($api,'disconnect')){
            try { $api->disconnect(); } catch(Throwable $e) {}
        }
    }
}

/**
 * Best-effort WAN interface auto-detect.
 * Accepts mysqli (will connect) OR RouterOSAPI instance.
 */
function mt_autodetect_wan_iface($connOrApi){
    $api = null;
    $owns = false;
    try{
        if (is_object($connOrApi) && ($connOrApi instanceof RouterOSAPI)) {
            $api = $connOrApi;
        } else {
            $api = mt_api_connect($connOrApi);
            $owns = true;
        }
        if(!$api) return '';

        // 1) Prefer default route gateway that is an interface name (pppoe-out1, etc.)
        $routes = $api->command(['/ip/route/print', '?dst-address=0.0.0.0/0']);
        foreach($routes as $r){
            if(($r['.type'] ?? '') !== '!re') continue;
            $gw = trim((string)($r['gateway'] ?? ''));
            if($gw!=='' && preg_match('/^[a-zA-Z0-9\-_.]+$/', $gw)){
                $ifs = $api->command(['/interface/print', '?name='.$gw]);
                foreach($ifs as $ir){
                    if(($ir['.type'] ?? '') === '!re') return $gw;
                }
            }
        }

        // 2) Prefer common names
        foreach(['pppoe-out1','ether1','sfp-sfpplus1','sfp-sfpplus2'] as $cand){
            $ifs = $api->command(['/interface/print', '?name='.$cand]);
            foreach($ifs as $ir){
                if(($ir['.type'] ?? '') === '!re') return $cand;
            }
        }

        // 3) First running interface
        $ifs = $api->command(['/interface/print']);
        foreach($ifs as $ir){
            if(($ir['.type'] ?? '') !== '!re') continue;
            $running = (string)($ir['running'] ?? 'no');
            if($running !== 'true' && $running !== 'yes') continue;
            $name = trim((string)($ir['name'] ?? ''));
            if($name==='') continue;
            return $name;
        }

        return '';
    } catch(Throwable $e){
        $GLOBALS['mt_last_error'] = $e->getMessage();
        return '';
    } finally {
        if($owns && $api && method_exists($api,'disconnect')){
            try { $api->disconnect(); } catch(Throwable $e) {}
        }
    }
}

/**
 * Ensure Hotspot user profile exists on MikroTik.
 * - If profile is missing: create it.
 * - If profile exists and $rate_limit provided: update rate-limit.
 * @return array{ok:bool,created:bool,updated:bool,error:?string}
 */
function mt_ensure_hotspot_profile(mysqli $conn, string $profile_name, string $rate_limit = ''): array {
  $profile_name = trim($profile_name);
  if ($profile_name === '') {
    return ['ok'=>false,'created'=>false,'updated'=>false,'error'=>'profile_name فارغ'];
  }
  try {
    $api = mt_api_connect($conn);
    $rows = $api->command(['/ip/hotspot/user/profile/print', '?name=' . $profile_name]);
    $found_id = null;
    foreach ($rows as $r) {
      if (($r['.type'] ?? '') === '!re') {
        $found_id = $r['.id'] ?? null;
        break;
      }
    }

    $created = false;
    $updated = false;
    if ($found_id) {
      if ($rate_limit !== '') {
        $api->command(['/ip/hotspot/user/profile/set', '=.id=' . $found_id, '=rate-limit=' . $rate_limit]);
        $updated = true;
      }
      return ['ok'=>true,'created'=>$created,'updated'=>$updated,'error'=>null];
    }

    $cmd = ['/ip/hotspot/user/profile/add', '=name=' . $profile_name];
    if ($rate_limit !== '') $cmd[] = '=rate-limit=' . $rate_limit;
    $api->command($cmd);
    $created = true;
    return ['ok'=>true,'created'=>$created,'updated'=>$updated,'error'=>null];
  } catch (Throwable $e) {
    return ['ok'=>false,'created'=>false,'updated'=>false,'error'=>$e->getMessage()];
  }
}



/* ===========================
   Provisioning (Billing -> PPP Secret)
=========================== */

if (!function_exists('mt_router_identity')) {
  function mt_router_identity($api): string {
    try {
      $rows = $api->command(['/system/identity/print']);
      foreach ($rows as $r) {
        if (($r['.type'] ?? '') === '!re') {
          return (string)($r['name'] ?? '');
        }
      }
    } catch (Throwable $e) {
      // ignore
    }
    return '';
  }
}

if (!function_exists('mt_provision_ppp_for_subscriber')) {
  /**
   * Create or update PPP secret for a subscriber using columns:
   * ppp_username, ppp_password, ppp_profile, ppp_rate_limit.
   * Returns: ['ok'=>bool,'msg'=>string,'host'=>string,'router'=>string]
   */
  function mt_provision_ppp_for_subscriber($conn, int $sid): array {
    $out = ['ok'=>false,'msg'=>'','host'=>'','router'=>''];

    $db = ($conn instanceof mysqli) ? $conn : mt_db();
    if(!$db){
      $out['msg'] = 'DB غير جاهز';
      return $out;
    }

    $sid = (int)$sid;
    if($sid<=0){
      $out['msg'] = 'subscriber_id غير صحيح';
      return $out;
    }

    // Read subscriber PPP fields
    $q = $db->prepare("SELECT id, is_deleted, ppp_username, ppp_password, ppp_profile, ppp_rate_limit FROM subscribers WHERE id=? LIMIT 1");
    if(!$q){
      $out['msg'] = 'DB prepare error: '.$db->error;
      return $out;
    }
    $q->bind_param('i', $sid);
    $q->execute();
    $res = $q->get_result();
    $sub = $res ? $res->fetch_assoc() : null;
    $q->close();

    if(!$sub){
      $out['msg'] = 'المشترك غير موجود';
      return $out;
    }

    $user = trim((string)($sub['ppp_username'] ?? ''));
    $pass = (string)($sub['ppp_password'] ?? '');
    $prof = trim((string)($sub['ppp_profile'] ?? ''));
    $rate = trim((string)($sub['ppp_rate_limit'] ?? ''));

    if($user==='' || $pass===''){
      $out['msg'] = 'بيانات PPP ناقصة (user/pass)';
      return $out;
    }

    // Connect
    $cfg = mt_get_settings($db);
    $host = trim($cfg['host'] ?? '');
    $port = (int)($cfg['api_port'] ?? 8728);
    $out['host'] = $host.':'.$port;

    $api = mt_api_connect($db);
    if(!$api){
      $out['msg'] = $GLOBALS['mt_last_error'] ?? 'فشل الاتصال بالراوتر';
      return $out;
    }

    try {
      $out['router'] = mt_router_identity($api);

      // ensure profile exists if provided
      if($prof !== ''){
        $profRows = $api->command(['/ppp/profile/print', '?name='.$prof]);
        $existsProf = false;
        foreach($profRows as $r){
          if(($r['.type'] ?? '')==='!re'){ $existsProf=true; break; }
        }
        if(!$existsProf){
          // fallback
          $prof = 'default';
        }
      } else {
        $prof = 'default';
      }

      // Find existing secret
      $rows = $api->command(['/ppp/secret/print', '?name='.$user]);
      $sid_ros = '';
      $trap = '';
      foreach($rows as $r){
        $t = $r['.type'] ?? '';
        if($t === '!trap') { $trap = $r['message'] ?? 'trap'; }
        if($t === '!re') { $sid_ros = (string)($r['.id'] ?? ''); break; }
      }
      if($trap){
        $out['msg'] = 'MikroTik trap (print): '.$trap;
        return $out;
      }

      $baseCmd = [];
      if($sid_ros !== ''){
        $baseCmd = ['/ppp/secret/set', '=.id='.$sid_ros, '=name='.$user, '=password='.$pass, '=service=pppoe', '=profile='.$prof, '=disabled=no', '=comment=ISP_PANEL:'.$sid];
        if($rate !== '') $baseCmd[] = '=rate-limit='.$rate;
      } else {
        $baseCmd = ['/ppp/secret/add', '=name='.$user, '=password='.$pass, '=service=pppoe', '=profile='.$prof, '=disabled=no', '=comment=ISP_PANEL:'.$sid];
        if($rate !== '') $baseCmd[] = '=rate-limit='.$rate;
      }

      $resp = $api->command($baseCmd);
      $trap2 = '';
      foreach($resp as $r){
        if(($r['.type'] ?? '')==='!trap'){ $trap2 = $r['message'] ?? 'trap'; break; }
      }
      if($trap2){
        $out['msg'] = 'MikroTik trap (write): '.$trap2;
        return $out;
      }

      // Verify existence
      $verify = $api->command(['/ppp/secret/print', '?name='.$user]);
      $found = false;
      foreach($verify as $r){
        if(($r['.type'] ?? '')==='!re'){ $found=true; break; }
        if(($r['.type'] ?? '')==='!trap'){ $out['msg'] = 'MikroTik trap (verify): '.($r['message'] ?? 'trap'); return $out; }
      }
      if(!$found){
        $out['msg'] = 'لم يتم العثور على secret بعد الكتابة (تحقق من الراوتر/الاتصال)';
        return $out;
      }

      // Update last sync
      if (col_exists($db,'subscribers','ppp_last_sync')) {
        $db->query("UPDATE subscribers SET ppp_last_sync=NOW() WHERE id=".$sid);
      }

      $out['ok'] = true;
      $noteDel = ((int)($sub['is_deleted'] ?? 0) === 1) ? ' (ملاحظة: المشترك مؤرشف is_deleted=1)' : '';
      $out['msg'] = 'تم إنشاء/تحديث PPP للمستخدم: '.$user.' على '.($out['router'] ?: 'router').$noteDel;
      return $out;

    } catch (Throwable $e) {
      $out['msg'] = $e->getMessage();
      return $out;
    } finally {
      try { $api->disconnect(); } catch(Throwable $e) {}
    }
  }
}


/* =========================================================
   Import helpers: list PPP secrets / Hotspot users
   (Used by mikrotik_import_* API)
========================================================= */

function mt_ppp_secrets($conn = null): array {
  $api = mt_api_connect($conn);
  if(!$api) return [];
  try {
    $rows = $api->command(['/ppp/secret/print']);
    $out = [];
    foreach($rows as $r){
      if(($r['.type'] ?? '') !== '!re') continue;
      $out[] = [
        'id' => (string)($r['.id'] ?? ''),
        'username' => (string)($r['name'] ?? ''),
        'password' => (string)($r['password'] ?? ''),
        'service'  => (string)($r['service'] ?? ''),
        'profile'  => (string)($r['profile'] ?? ''),
        'rate_limit' => (string)($r['rate-limit'] ?? ''),
        'remote_address' => (string)($r['remote-address'] ?? ''),
        'local_address'  => (string)($r['local-address'] ?? ''),
        'limit_bytes_in'  => (int)($r['limit-bytes-in'] ?? 0),
        'limit_bytes_out' => (int)($r['limit-bytes-out'] ?? 0),
        'disabled' => ((string)($r['disabled'] ?? '') === 'true') || ((string)($r['disabled'] ?? '') === 'yes'),
        'comment'  => (string)($r['comment'] ?? ''),
        'last_logged_out' => (string)($r['last-logged-out'] ?? ''),
        'last_caller_id'  => (string)($r['last-caller-id'] ?? ''),
        'last_disconnect_reason' => (string)($r['last-disconnect-reason'] ?? ''),
      ];
    }
    return $out;
  } finally {
    try { $api->disconnect(); } catch(Throwable $e) {}
  }
}

function mt_hotspot_users($conn = null): array {
  $api = mt_api_connect($conn);
  if(!$api) return [];
  try {
    $rows = $api->command(['/ip/hotspot/user/print']);
    $out = [];
    foreach($rows as $r){
      if(($r['.type'] ?? '') !== '!re') continue;
      $out[] = [
        'id' => (string)($r['.id'] ?? ''),
        'username' => (string)($r['name'] ?? ''),
        'password' => (string)($r['password'] ?? ''),
        'profile'  => (string)($r['profile'] ?? ''),
        'server'   => (string)($r['server'] ?? ''),
        'address'  => (string)($r['address'] ?? ''),
        'limit_bytes_in'   => (int)($r['limit-bytes-in'] ?? 0),
        'limit_bytes_out'  => (int)($r['limit-bytes-out'] ?? 0),
        'limit_bytes_total'=> (int)($r['limit-bytes-total'] ?? 0),
        'uptime_limit'     => (string)($r['limit-uptime'] ?? ''),
        'disabled' => ((string)($r['disabled'] ?? '') === 'true') || ((string)($r['disabled'] ?? '') === 'yes'),
        'comment'  => (string)($r['comment'] ?? ''),
      ];
    }
    return $out;
  } finally {
    try { $api->disconnect(); } catch(Throwable $e) {}
  }
}

function mt_bytes_human($bytes): string {
  $b = (float)$bytes;
  if ($b <= 0) return '0 B';
  $units = ['B','KB','MB','GB','TB','PB'];
  $i = 0;
  while ($b >= 1024 && $i < count($units)-1) { $b /= 1024; $i++; }
  return rtrim(rtrim(number_format($b, 2, '.', ''), '0'), '.') . ' ' . $units[$i];
}



if (!function_exists('mt_table_exists_simple')) {
  function mt_table_exists_simple(mysqli $conn, string $table): bool {
    $table = $conn->real_escape_string($table);
    $r = @$conn->query("SHOW TABLES LIKE '{$table}'");
    return $r && $r->num_rows > 0;
  }
}

if (!function_exists('mt_get_enabled_routers')) {
  function mt_get_enabled_routers(mysqli $conn): array {
    $rows = [];
    if (!mt_table_exists_simple($conn, 'routers')) return $rows;
    $r = @$conn->query("SELECT * FROM routers WHERE is_enabled=1 ORDER BY id ASC");
    if ($r) while ($row = $r->fetch_assoc()) $rows[] = $row;
    return $rows;
  }
}

if (!function_exists('mt_get_router_by_id')) {
  function mt_get_router_by_id(mysqli $conn, int $id): ?array {
    if ($id <= 0 || !mt_table_exists_simple($conn, 'routers')) return null;
    $id = (int)$id;
    $r = @$conn->query("SELECT * FROM routers WHERE id={$id} LIMIT 1");
    if ($r && $r->num_rows) return $r->fetch_assoc();
    return null;
  }
}

if (!function_exists('mt_pick_router_by_role')) {
  function mt_pick_router_by_role(mysqli $conn, string $role): ?array {
    if ($role === '' || !mt_table_exists_simple($conn, 'routers')) return null;
    $role = $conn->real_escape_string($role);
    $r = @$conn->query("SELECT * FROM routers WHERE role='{$role}' AND is_enabled=1 ORDER BY id ASC LIMIT 1");
    if ($r && $r->num_rows) return $r->fetch_assoc();
    return null;
  }
}

if (!function_exists('mt_default_router_config')) {
  function mt_default_router_config(mysqli $conn): ?array {
    $pppoe = mt_pick_router_by_role($conn, 'pppoe');
    if ($pppoe) return $pppoe;
    $list = mt_get_enabled_routers($conn);
    if (!empty($list)) return $list[0];
    $cfg = mt_get_settings($conn);
    if (!$cfg) return null;
    return [
      'id' => 0,
      'name' => trim((string)($cfg['host'] ?? 'MikroTik')),
      'host' => trim((string)($cfg['host'] ?? '')),
      'api_port' => (int)($cfg['api_port'] ?? 8728),
      'username' => trim((string)($cfg['username'] ?? '')),
      'password' => (string)($cfg['password'] ?? ''),
      'role' => 'other',
      'is_enabled' => 1,
    ];
  }
}

if (!function_exists('mt_get_source_router')) {
  function mt_get_source_router(mysqli $conn, string $settingKey, string $fallbackRole = '', bool $fallbackDefault = true): ?array {
    $id = '';
    if (function_exists('get_app_setting')) $id = trim((string)get_app_setting($conn, $settingKey, ''));
    if ($id !== '' && ctype_digit($id)) {
      $router = mt_get_router_by_id($conn, (int)$id);
      if ($router) return $router;
    }
    if ($fallbackRole !== '') {
      $router = mt_pick_router_by_role($conn, $fallbackRole);
      if ($router) return $router;
    }
    return $fallbackDefault ? mt_default_router_config($conn) : null;
  }
}

if (!function_exists('mt_api_connect_router')) {
  function mt_api_connect_router(array $router) {
    $err = '';
    $api = ua_mt_connect_config($router, $err, 0.8);
    if (!$api) {
      $GLOBALS['mt_last_error'] = $err ?: ($GLOBALS['mt_last_error'] ?? 'تعذر الاتصال بالراوتر');
      return false;
    }
    return $api;
  }
}
