<?php
// Shared source for Security Monitor page/API helpers.
// Keeps allow-list and summary query helpers in one authoritative file.

if (!function_exists('sm_str')) {
  function sm_str($v, int $max = 191): string {
    return mb_substr(trim((string)$v), 0, $max, 'UTF-8');
  }
}

if (!function_exists('sm_norm_kind')) {
  function sm_norm_kind($kind): string {
    $kind = strtolower(trim((string)$kind));
    return in_array($kind, ['mac','ip','username','mac_ip','interface','ip_cidr','ip_prefix','source'], true) ? $kind : '';
  }
}

if (!function_exists('sm_scalar')) {
  function sm_scalar(mysqli $conn, string $sql): int {
    $rs = @$conn->query($sql);
    if (!$rs) return 0;
    $r = $rs->fetch_row();
    return (int)($r[0] ?? 0);
  }
}

if (!function_exists('sm_rows')) {
  function sm_rows(mysqli $conn, string $sql): array {
    $rows = [];
    $rs = @$conn->query($sql);
    if ($rs) while ($r = $rs->fetch_assoc()) $rows[] = $r;
    return $rows;
  }
}

if (!function_exists('sm_ensure_event_log_schema')) {
  function sm_ensure_event_log_schema(mysqli $conn): void {
    @ $conn->query("CREATE TABLE IF NOT EXISTS security_monitor_event_log (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      event_date DATE NOT NULL,
      source_type VARCHAR(30) NOT NULL,
      source_id BIGINT UNSIGNED NOT NULL,
      category VARCHAR(40) NOT NULL,
      event_type VARCHAR(80) NOT NULL,
      severity VARCHAR(20) NOT NULL DEFAULT 'info',
      title VARCHAR(255) NOT NULL,
      message TEXT NULL,
      ip_address VARCHAR(64) NULL,
      mac_address VARCHAR(32) NULL,
      username VARCHAR(191) NULL,
      first_seen_at DATETIME NULL,
      last_seen_at DATETIME NULL,
      hit_count INT UNSIGNED NOT NULL DEFAULT 1,
      review_status VARCHAR(30) NOT NULL DEFAULT 'pending',
      review_action VARCHAR(30) NULL,
      review_note TEXT NULL,
      reviewed_by VARCHAR(191) NULL,
      reviewed_at DATETIME NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (id),
      UNIQUE KEY uq_security_event_day (source_type, source_id, event_date),
      KEY idx_security_event_date_status (event_date, review_status),
      KEY idx_security_event_category (category, event_date),
      KEY idx_security_event_last_seen (last_seen_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");

    @ $conn->query("CREATE TABLE IF NOT EXISTS security_monitor_event_actions (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      event_id BIGINT UNSIGNED NOT NULL,
      action_name VARCHAR(30) NOT NULL,
      action_note TEXT NULL,
      actor VARCHAR(191) NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (id),
      KEY idx_security_event_action_event (event_id, created_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
  }
}

if (!function_exists('sm_sync_daily_event_log')) {
  function sm_sync_daily_event_log(mysqli $conn, ?string $day = null): array {
    sm_ensure_event_log_schema($conn);
    $day = trim((string)$day);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $day)) {
      $dayResult = @$conn->query('SELECT CURDATE()');
      $dayRow = $dayResult ? $dayResult->fetch_row() : null;
      $day = (string)($dayRow[0] ?? date('Y-m-d'));
    }
    $dayEsc = $conn->real_escape_string($day);
    $from = $dayEsc . ' 00:00:00';
    $to = date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00';
    $toEsc = $conn->real_escape_string($to);
    $out = ['watch'=>0, 'network'=>0, 'admin'=>0, 'day'=>$day];

    $has = static function (mysqli $db, string $table): bool {
      $safe = $db->real_escape_string($table);
      $rs = @$db->query("SHOW TABLES LIKE '{$safe}'");
      return $rs && $rs->num_rows > 0;
    };

    if ($has($conn, 'network_ip_watch')) {
      @ $conn->query("INSERT INTO security_monitor_event_log
        (event_date,source_type,source_id,category,event_type,severity,title,message,ip_address,mac_address,username,first_seen_at,last_seen_at,hit_count)
        SELECT '{$dayEsc}','watch',id,
          CASE WHEN stage='confirmed' THEN 'network' WHEN stage='suspect' THEN 'suspect' ELSE 'identifying' END,
          COALESCE(NULLIF(stage,''),'observed'),
          CASE WHEN stage='confirmed' THEN 'danger' WHEN stage='suspect' THEN 'warn' ELSE 'info' END,
          CASE WHEN stage='confirmed' THEN 'ملاحظة شبكة مؤكدة' WHEN stage='suspect' THEN 'اشتباه شبكة' ELSE 'جهاز قيد التعرف' END,
          COALESCE(decision_note,''),ip_address,mac_address,username,first_seen_at,last_seen_at,GREATEST(1,seen_count)
        FROM network_ip_watch
        WHERE is_pppoe_active=0 AND is_allowlisted=0
          AND stage IN ('observed','identifying','suspect','confirmed')
          AND last_seen_at >= '{$from}' AND last_seen_at < '{$toEsc}'
        ON DUPLICATE KEY UPDATE
          category=VALUES(category),event_type=VALUES(event_type),severity=VALUES(severity),
          title=VALUES(title),message=VALUES(message),ip_address=VALUES(ip_address),
          mac_address=VALUES(mac_address),username=VALUES(username),
          first_seen_at=VALUES(first_seen_at),last_seen_at=VALUES(last_seen_at),
          hit_count=VALUES(hit_count),updated_at=NOW()");
      $out['watch'] = max(0, (int)$conn->affected_rows);
    }

    if ($has($conn, 'subscriber_network_alerts')) {
      @ $conn->query("INSERT INTO security_monitor_event_log
        (event_date,source_type,source_id,category,event_type,severity,title,message,ip_address,mac_address,username,first_seen_at,last_seen_at,hit_count)
        SELECT '{$dayEsc}','network',id,
          CASE
            WHEN source IN ('pppoe_match','pppoe_secret','pppoe_duplicate_login') THEN 'pppoe'
            WHEN source='mac_ownership' THEN 'mac_conflict'
            WHEN source='active' AND (
              alert_key LIKE 'mac_owner_moved_to_account:%'
              OR alert_key LIKE 'mac_on_unknown_account:%'
              OR meta_json LIKE '%\"conflict_type\":\"known_mac_on_other_known_account\"%'
            ) THEN 'account_change'
            ELSE 'network'
          END,
          COALESCE(NULLIF(source,''),'network'),COALESCE(NULLIF(severity,''),'warn'),
          COALESCE(NULLIF(title,''),'تنبيه شبكة'),COALESCE(message,''),
          ip_address,mac_address,username,first_seen_at,last_seen_at,GREATEST(1,hit_count)
        FROM subscriber_network_alerts
        WHERE is_open=1
          AND last_seen_at >= '{$from}' AND last_seen_at < '{$toEsc}'
        ON DUPLICATE KEY UPDATE
          category=VALUES(category),event_type=VALUES(event_type),severity=VALUES(severity),
          title=VALUES(title),message=VALUES(message),ip_address=VALUES(ip_address),
          mac_address=VALUES(mac_address),username=VALUES(username),
          first_seen_at=VALUES(first_seen_at),last_seen_at=VALUES(last_seen_at),
          hit_count=VALUES(hit_count),updated_at=NOW()");
      $out['network'] = max(0, (int)$conn->affected_rows);
    }

    if ($has($conn, 'admin_security_alerts')) {
      @ $conn->query("UPDATE admin_security_alerts
        SET title=CASE
          WHEN meta_json LIKE '%\"source\":\"fast_active\"%' THEN 'تعذر تحديث الجلسات النشطة'
          WHEN meta_json LIKE '%\"source\":\"deep\"%' THEN 'تعذر الفحص العميق لمراقب الشبكة'
          WHEN meta_json LIKE '%\"source\":\"ip_scan\"%' THEN 'تعذر فحص عناوين الشبكة'
          ELSE 'تعذر تشغيل مراقب الشبكة'
        END
        WHERE alert_type='security_monitor_error' AND title='Security monitor issue'");
      @ $conn->query("INSERT INTO security_monitor_event_log
        (event_date,source_type,source_id,category,event_type,severity,title,message,ip_address,mac_address,username,first_seen_at,last_seen_at,hit_count)
        SELECT '{$dayEsc}','admin',id,
          CASE
            WHEN alert_type IN (
              'router_account_change','router_config_change','router_user_added',
              'router_user_changed','router_user_removed','app_router_credentials_changed'
            ) THEN 'router_change'
            WHEN alert_type IN (
              'login_failed','login_rate_limited','router_login_failed','router_security_warning'
            ) THEN 'dangerous_login'
            ELSE 'admin'
          END,
          alert_type,COALESCE(NULLIF(severity,''),'warn'),
          COALESCE(NULLIF(title,''),'حدث أمان'),COALESCE(message,''),
          ip_address,'',username,first_seen_at,last_seen_at,GREATEST(1,hit_count)
        FROM admin_security_alerts
        WHERE is_open=1
          AND last_seen_at >= '{$from}' AND last_seen_at < '{$toEsc}'
          AND alert_type<>'admin_unauthorized_access'
        ON DUPLICATE KEY UPDATE
          category=VALUES(category),event_type=VALUES(event_type),severity=VALUES(severity),
          title=VALUES(title),message=VALUES(message),ip_address=VALUES(ip_address),
          username=VALUES(username),first_seen_at=VALUES(first_seen_at),
          last_seen_at=VALUES(last_seen_at),hit_count=VALUES(hit_count),updated_at=NOW()");
      $out['admin'] = max(0, (int)$conn->affected_rows);
    }

    return $out;
  }
}

if (!function_exists('sm_event_log_review')) {
  function sm_event_log_review(mysqli $conn, int $eventId, string $action, string $note = '', string $actor = ''): array {
    sm_ensure_event_log_schema($conn);
    if ($eventId <= 0) return ['ok'=>false, 'message'=>'رقم الحدث غير صالح.'];
    $allowedActions = ['ignore'=>'ignored', 'allow'=>'allowed', 'resolve'=>'resolved', 'delete'=>'deleted'];
    if (!isset($allowedActions[$action])) return ['ok'=>false, 'message'=>'إجراء المعالجة غير صالح.'];

    $status = $allowedActions[$action];
    $note = sm_str($note, 2000);
    if ($note === '') {
      $note = [
        'ignore'=>'تم تجاهل الحدث بعد المراجعة.',
        'allow'=>'تمت إضافة قيمة الحدث إلى قائمة السماح.',
        'resolve'=>'تمت معالجة الحدث وإغلاقه.',
        'delete'=>'تم حذف الحدث من قائمة العمل.',
      ][$action];
    }
    $actor = sm_str($actor, 191);
    $eventId = (int)$eventId;
    $started = @$conn->begin_transaction();
    if (!$started) return ['ok'=>false, 'message'=>'تعذر بدء معاملة معالجة الحدث.'];

    try {
      $rs = @$conn->query("SELECT * FROM security_monitor_event_log WHERE id={$eventId} LIMIT 1 FOR UPDATE");
      $event = $rs ? $rs->fetch_assoc() : null;
      if (!$event) throw new RuntimeException('الحدث غير موجود في السجل.');

      $sourceType = (string)($event['source_type'] ?? '');
      $sourceId = (int)($event['source_id'] ?? 0);
      $targetIds = [$eventId];

      if ($action === 'allow') {
        $username = trim((string)($event['username'] ?? ''));
        $ip = trim((string)($event['ip_address'] ?? ''));
        $allowOk = $username !== ''
          ? sm_insert_allow($conn, 'username', $username, '', 'سماح من سجل المراقبة', $note, false)
          : ($ip !== '' ? sm_insert_allow($conn, 'ip', $ip, '', 'سماح من سجل المراقبة', $note, false) : false);
        if (!$allowOk) throw new RuntimeException('لا توجد قيمة Username أو IP صالحة لإضافتها إلى السماح.');

        $identityWhere = $username !== ''
          ? "username='" . $conn->real_escape_string($username) . "'"
          : "ip_address='" . $conn->real_escape_string($ip) . "'";
        $related = @$conn->query("SELECT id FROM security_monitor_event_log
          WHERE event_date=CURDATE() AND review_status='pending' AND {$identityWhere}
          FOR UPDATE");
        if ($related) while ($row = $related->fetch_assoc()) $targetIds[] = (int)$row['id'];
        $targetIds = array_values(array_unique(array_filter($targetIds)));
      } elseif ($sourceType === 'network' && $sourceId > 0) {
        $closed = sm_close_network_alert($conn, $sourceId, $action === 'resolve' ? 'resolved' : 'closed', $note, false);
        if (empty($closed['ok'])) throw new RuntimeException('تعذر إغلاق تنبيه الشبكة المرتبط.');
      } elseif ($sourceType === 'admin' && $sourceId > 0) {
        $closed = sm_close_admin_alert($conn, $sourceId, 'closed', $note, false, false);
        if (empty($closed['ok'])) throw new RuntimeException('تعذر إغلاق تنبيه الأمان المرتبط.');
      } elseif ($sourceType === 'watch' && $sourceId > 0) {
        $noteEsc = $conn->real_escape_string($note);
        if (!@$conn->query("UPDATE network_ip_watch SET stage='resolved',decision_note='{$noteEsc}' WHERE id={$sourceId} LIMIT 1")) {
          throw new RuntimeException('تعذر إغلاق سجل المراقبة المرتبط.');
        }
      }

      $statusEsc = $conn->real_escape_string($status);
      $actionEsc = $conn->real_escape_string($action);
      $noteEsc = $conn->real_escape_string($note);
      $actorEsc = $conn->real_escape_string($actor);
      $idList = implode(',', array_map('intval', $targetIds));
      if (!@$conn->query("UPDATE security_monitor_event_log
        SET review_status='{$statusEsc}',review_action='{$actionEsc}',review_note='{$noteEsc}',
            reviewed_by='{$actorEsc}',reviewed_at=NOW(),updated_at=NOW()
        WHERE id IN ({$idList})")) {
        throw new RuntimeException('تعذر حفظ قرار معالجة الحدث.');
      }
      if (!@$conn->query("INSERT INTO security_monitor_event_actions(event_id,action_name,action_note,actor)
        SELECT id,'{$actionEsc}','{$noteEsc}','{$actorEsc}'
        FROM security_monitor_event_log WHERE id IN ({$idList})")) {
        throw new RuntimeException('تعذر حفظ سجل تدقيق قرار المعالجة.');
      }
      if (!@$conn->commit()) throw new RuntimeException('تعذر تثبيت قرار معالجة الحدث.');
      return ['ok'=>true, 'status'=>$status, 'id'=>$eventId, 'affected'=>count($targetIds)];
    } catch (Throwable $e) {
      @$conn->rollback();
      return ['ok'=>false, 'message'=>$e->getMessage()];
    }
  }
}

if (!function_exists('sm_event_log_rows')) {
  function sm_event_log_rows(mysqli $conn, string $status = 'pending', int $days = 30, int $limit = 200): array {
    sm_ensure_event_log_schema($conn);
    $allowed = ['pending','ignored','allowed','resolved','deleted','all'];
    $status = in_array($status, $allowed, true) ? $status : 'pending';
    $days = max(1, min(365, $days));
    $limit = max(1, min(1000, $limit));
    $daysBack = $days - 1;
    $where = ["event_date >= DATE_SUB(CURDATE(), INTERVAL {$daysBack} DAY)"];
    if ($status !== 'all') $where[] = "review_status='" . $conn->real_escape_string($status) . "'";
    return sm_rows($conn, "SELECT * FROM security_monitor_event_log
      WHERE " . implode(' AND ', $where) . "
      ORDER BY event_date DESC,last_seen_at DESC,id DESC LIMIT {$limit}");
  }
}

if (!function_exists('sm_insert_allow')) {
  function sm_insert_allow(mysqli $conn, string $kind, string $value1, string $value2 = '', string $label = '', string $note = '', bool $syncEventLog = true): bool {
    $kind = sm_norm_kind($kind);
    if ($kind === '') return false;
    // سماح MAC لم يعد مساراً صالحاً لحل تعارض الملكية؛ يجب استخدام اعتماد مالك MAC من مركز المراقبة.
    if ($kind === 'mac' || $kind === 'mac_ip') return false;
    if (($kind === 'mac' || $kind === 'mac_ip') && function_exists('mtwatch_norm_mac')) $value1 = mtwatch_norm_mac($value1);
    $value1 = sm_str($value1, 191);
    $value2 = sm_str($value2, 191);
    $label = sm_str($label, 191);
    $note = sm_str($note, 2000);
    if ($value1 === '') return false;
    if ($kind === 'mac_ip' && $value2 === '') return false;

    $stmt = @$conn->prepare("INSERT INTO mtwatch_allowlist (kind,value1,value2,label,note,enabled,created_at) VALUES (?,?,?,?,?,1,NOW()) ON DUPLICATE KEY UPDATE label=VALUES(label), note=VALUES(note), enabled=1, updated_at=NOW()");
    if (!$stmt) return false;
    $stmt->bind_param('sssss', $kind, $value1, $value2, $label, $note);
    $ok = @$stmt->execute();
    $stmt->close();

    if ($ok) {
      $v1 = $conn->real_escape_string($value1);
      $v2 = $conn->real_escape_string($value2);
      $noteEsc = $conn->real_escape_string('تمت الإضافة لقائمة السماح من مراقب الشبكة.');
      if ($kind === 'mac') {
        @ $conn->query("UPDATE subscriber_network_alerts SET is_open=0, stage='allowlisted', decision_status='allowed', decision_note='{$noteEsc}', updated_at=NOW() WHERE mac_address='{$v1}' AND is_open=1");
        @ $conn->query("UPDATE network_ip_watch SET is_allowlisted=1, stage='allowlisted', decision_note='{$noteEsc}' WHERE mac_address='{$v1}'");
      } elseif ($kind === 'ip') {
        @ $conn->query("UPDATE subscriber_network_alerts SET is_open=0, stage='allowlisted', decision_status='allowed', decision_note='{$noteEsc}', updated_at=NOW() WHERE ip_address='{$v1}' AND is_open=1");
        @ $conn->query("UPDATE network_ip_watch SET is_allowlisted=1, stage='allowlisted', decision_note='{$noteEsc}' WHERE ip_address='{$v1}'");
      } elseif ($kind === 'username') {
        $u = function_exists('mtwatch_norm_user') ? $conn->real_escape_string(mtwatch_norm_user($value1)) : $v1;
        @ $conn->query("UPDATE subscriber_network_alerts SET is_open=0, stage='allowlisted', decision_status='allowed', decision_note='{$noteEsc}', updated_at=NOW() WHERE username='{$u}' AND is_open=1");
        @ $conn->query("UPDATE network_ip_watch SET is_allowlisted=1, stage='allowlisted', decision_note='{$noteEsc}' WHERE username='{$u}'");
      } elseif ($kind === 'mac_ip') {
        @ $conn->query("UPDATE subscriber_network_alerts SET is_open=0, stage='allowlisted', decision_status='allowed', decision_note='{$noteEsc}', updated_at=NOW() WHERE mac_address='{$v1}' AND ip_address='{$v2}' AND is_open=1");
        @ $conn->query("UPDATE network_ip_watch SET is_allowlisted=1, stage='allowlisted', decision_note='{$noteEsc}' WHERE mac_address='{$v1}' AND ip_address='{$v2}'");
      }
      $logWhere = '';
      if ($kind === 'ip') $logWhere = "ip_address='{$v1}'";
      elseif ($kind === 'username') $logWhere = "username='{$u}'";
      if ($syncEventLog && $logWhere !== '') {
        @ $conn->query("INSERT INTO security_monitor_event_actions(event_id,action_name,action_note,actor)
          SELECT id,'allow','{$noteEsc}','security_monitor'
          FROM security_monitor_event_log
          WHERE event_date=CURDATE() AND review_status='pending' AND {$logWhere}");
        @ $conn->query("UPDATE security_monitor_event_log
          SET review_status='allowed',review_action='allow',review_note='{$noteEsc}',
              reviewed_by='security_monitor',reviewed_at=NOW(),updated_at=NOW()
          WHERE event_date=CURDATE() AND review_status='pending' AND {$logWhere}");
      }
    }
    return (bool)$ok;
  }
}

if (!function_exists('sm_close_network_alert')) {
  function sm_close_network_alert(mysqli $conn, int $id, string $status = 'closed', string $note = '', bool $syncEventLog = true): array {
    if ($id <= 0) return ['ok'=>false, 'message'=>'bad_id', 'affected'=>0];
    $status = in_array($status, ['closed','resolved','expired','allowed'], true) ? $status : 'closed';
    $note = sm_str($note !== '' ? $note : 'Closed from security monitor.', 2000);
    $id = (int)$id;
    $noteEsc = $conn->real_escape_string($note);
    $statusEsc = $conn->real_escape_string($status);

    $alert = null;
    $rs = @$conn->query("SELECT id, alert_key, mac_address, ip_address, source FROM subscriber_network_alerts WHERE id={$id} LIMIT 1");
    if ($rs) $alert = $rs->fetch_assoc();

    $ok = @ $conn->query("UPDATE subscriber_network_alerts
      SET is_open=0,
          stage='{$statusEsc}',
          decision_status='{$statusEsc}',
          decision_note='{$noteEsc}',
          updated_at=NOW()
      WHERE id={$id} LIMIT 1");
    $affected = max(0, (int)$conn->affected_rows);

    if ($ok && $alert) {
      $where = [];
      $mac = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac((string)($alert['mac_address'] ?? '')) : trim((string)($alert['mac_address'] ?? ''));
      $ip = trim((string)($alert['ip_address'] ?? ''));
      $src = trim((string)($alert['source'] ?? ''));
      if ($mac !== '') $where[] = "mac_address='" . $conn->real_escape_string($mac) . "'";
      if ($ip !== '') $where[] = "ip_address='" . $conn->real_escape_string($ip) . "'";
      if ($src !== '') $where[] = "source='" . $conn->real_escape_string($src) . "'";
      if ($where) {
        @ $conn->query("UPDATE network_ip_watch
          SET stage='resolved',
              decision_note='{$noteEsc}',
              last_seen_at=last_seen_at
          WHERE " . implode(' AND ', $where));
      }
      if ($syncEventLog) {
        $logAction = $status === 'allowed' ? 'allow' : 'resolve';
        $logStatus = $status === 'allowed' ? 'allowed' : 'resolved';
        @ $conn->query("INSERT INTO security_monitor_event_actions(event_id,action_name,action_note,actor)
          SELECT id,'{$logAction}','{$noteEsc}','security_monitor'
          FROM security_monitor_event_log
          WHERE source_type='network' AND source_id={$id} AND event_date=CURDATE() AND review_status='pending'");
        @ $conn->query("UPDATE security_monitor_event_log
          SET review_status='{$logStatus}',
              review_action='{$logAction}',
              review_note='{$noteEsc}',reviewed_by='security_monitor',reviewed_at=NOW(),updated_at=NOW()
          WHERE source_type='network' AND source_id={$id} AND event_date=CURDATE() AND review_status='pending'");
      }
    }

    return ['ok'=>(bool)$ok, 'affected'=>$affected, 'id'=>$id];
  }
}

if (!function_exists('sm_close_admin_alert')) {
  function sm_close_admin_alert(mysqli $conn, int $id, string $status = 'closed', string $note = '', bool $syncEventLog = true, bool $ensureSchema = true): array {
    if ($id <= 0) return ['ok'=>false, 'message'=>'bad_id', 'affected'=>0];
    if ($ensureSchema && function_exists('ua_security_alerts_ensure_schema')) ua_security_alerts_ensure_schema($conn);
    $note = sm_str($note !== '' ? $note : 'Closed from security monitor.', 2000);
    $id = (int)$id;
    $ok = @ $conn->query("UPDATE admin_security_alerts
      SET is_open=0,
          updated_at=NOW()
      WHERE id={$id} LIMIT 1");
    if ($ok && $syncEventLog) {
      $noteEsc = $conn->real_escape_string($note);
      @ $conn->query("INSERT INTO security_monitor_event_actions(event_id,action_name,action_note,actor)
        SELECT id,'resolve','{$noteEsc}','security_monitor'
        FROM security_monitor_event_log
        WHERE source_type='admin' AND source_id={$id} AND event_date=CURDATE() AND review_status='pending'");
      @ $conn->query("UPDATE security_monitor_event_log
        SET review_status='resolved',review_action='resolve',review_note='{$noteEsc}',
            reviewed_by='security_monitor',reviewed_at=NOW(),updated_at=NOW()
        WHERE source_type='admin' AND source_id={$id} AND event_date=CURDATE() AND review_status='pending'");
    }
    return ['ok'=>(bool)$ok, 'affected'=>max(0, (int)$conn->affected_rows), 'id'=>$id];
  }
}

if (!function_exists('sm_cleanup_stale_security_alerts')) {
  function sm_cleanup_stale_security_alerts(mysqli $conn, int $alertDays = 3, int $watchDays = 30): array {
    $alertDays = max(1, min(30, $alertDays));
    $watchDays = max(7, min(365, $watchDays));
    $note = $conn->real_escape_string('Auto-closed because the alert was older than the security review window.');
    $out = ['network_expired'=>0, 'admin_expired'=>0, 'watch_resolved'=>0];

    $rsNet = @$conn->query("SHOW TABLES LIKE 'subscriber_network_alerts'");
    if ($rsNet && $rsNet->num_rows > 0) {
      @ $conn->query("UPDATE subscriber_network_alerts
        SET is_open=0,
            stage='expired',
            decision_status='expired',
            decision_note='{$note}',
            updated_at=NOW()
        WHERE is_open=1
          AND last_seen_at < DATE_SUB(NOW(), INTERVAL {$alertDays} DAY)");
      $out['network_expired'] = max(0, (int)$conn->affected_rows);
    }

    $rs = @$conn->query("SHOW TABLES LIKE 'admin_security_alerts'");
    if ($rs && $rs->num_rows > 0) {
      // These rows were produced by expired background/API sessions. They are
      // navigation telemetry, not authentication failures.
      @ $conn->query("UPDATE admin_security_alerts
        SET is_open=0,
            updated_at=NOW()
        WHERE is_open=1
          AND alert_type='admin_unauthorized_access'");
      $out['admin_navigation_noise_closed'] = max(0, (int)$conn->affected_rows);
      @ $conn->query("UPDATE admin_security_alerts
        SET is_open=0,
            updated_at=NOW()
        WHERE is_open=1
          AND last_seen_at < DATE_SUB(NOW(), INTERVAL {$alertDays} DAY)");
      $out['admin_expired'] = max(0, (int)$conn->affected_rows);
    }

    $rsWatch = @$conn->query("SHOW TABLES LIKE 'network_ip_watch'");
    if ($rsWatch && $rsWatch->num_rows > 0) {
      @ $conn->query("UPDATE network_ip_watch
        SET stage='resolved',
            decision_note='{$note}'
        WHERE stage IN ('confirmed','suspect','identifying','observed')
          AND last_seen_at < DATE_SUB(NOW(), INTERVAL {$watchDays} DAY)");
      $out['watch_resolved'] = max(0, (int)$conn->affected_rows);
    }

    return $out;
  }
}

if (!function_exists('sm_security_summary')) {
  /**
   * One authoritative, non-overlapping incident summary.
   * Counts today's open incident records, never historical rows or hit_count.
   */
  function sm_security_summary(mysqli $conn): array {
    $identifying = sm_scalar($conn, "SELECT COUNT(*) FROM network_ip_watch
      WHERE is_pppoe_active=0 AND is_allowlisted=0 AND stage='identifying'
        AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");
    $suspect = sm_scalar($conn, "SELECT COUNT(*) FROM network_ip_watch
      WHERE is_pppoe_active=0 AND is_allowlisted=0 AND stage='suspect'
        AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");
    $pppoe = sm_scalar($conn, "SELECT COUNT(*) FROM subscriber_network_alerts
      WHERE is_open=1 AND source IN ('pppoe_match','pppoe_secret','pppoe_duplicate_login')
        AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");
    $accountChanges = sm_scalar($conn, "SELECT COUNT(*) FROM subscriber_network_alerts
      WHERE is_open=1 AND source='active'
        AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)
        AND (alert_key LIKE 'mac_owner_moved_to_account:%'
          OR alert_key LIKE 'mac_on_unknown_account:%'
          OR meta_json LIKE '%\"conflict_type\":\"known_mac_on_other_known_account\"%')");
    $macConflicts = sm_scalar($conn, "SELECT COUNT(*) FROM subscriber_network_alerts
      WHERE is_open=1 AND source='mac_ownership'
        AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");
    $routerChanges = sm_scalar($conn, "SELECT COUNT(*) FROM admin_security_alerts
      WHERE is_open=1 AND alert_type IN (
        'router_account_change','router_config_change','router_user_added',
        'router_user_changed','router_user_removed','app_router_credentials_changed'
      ) AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");
    $dangerousLogins = sm_scalar($conn, "SELECT COUNT(*) FROM admin_security_alerts
      WHERE is_open=1 AND alert_type IN (
        'login_failed','login_rate_limited','router_login_failed','router_security_warning'
      ) AND last_seen_at >= CURDATE() AND last_seen_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)");

    $total = $identifying + $suspect + $pppoe + $accountChanges
      + $macConflicts + $routerChanges + $dangerousLogins;
    $confirmed = $pppoe + $accountChanges + $macConflicts + $routerChanges + $dangerousLogins;
    $status = $confirmed > 0 ? 'confirmed' : ($suspect > 0 ? 'suspect' : ($identifying > 0 ? 'identifying' : 'clear'));

    return [
      'identifying'=>$identifying,
      'suspect'=>$suspect,
      'pppoe_conflicts'=>$pppoe,
      'account_changes'=>$accountChanges,
      'router_changes'=>$routerChanges,
      'mac_conflicts'=>$macConflicts,
      'dangerous_logins'=>$dangerousLogins,
      'confirmed'=>$confirmed,
      'admin_open'=>$dangerousLogins,
      'ownership_conflicts'=>$macConflicts,
      'total_attention'=>$total,
      'status'=>$status,
    ];
  }
}

if (!function_exists('sm_record_monitor_issue')) {
  function sm_record_monitor_issue(mysqli $conn, string $source, string $message, string $severity = 'warn'): void {
    $severity = in_array($severity, ['info','warn','danger'], true) ? $severity : 'warn';
    $source = sm_str($source, 60);
    $message = sm_str($message, 2000);
    if ($source === '' || $message === '') return;
    if (!function_exists('ua_security_alerts_ensure_schema')) return;
    try {
      ua_security_alerts_ensure_schema($conn);
      $key = hash('sha256', 'security-monitor-issue|' . $source . '|' . mb_strtolower($message, 'UTF-8'));
      $titles = [
        'fast_active' => 'تعذر تحديث الجلسات النشطة',
        'deep' => 'تعذر الفحص العميق لمراقب الشبكة',
        'ip_scan' => 'تعذر فحص عناوين الشبكة',
      ];
      $title = $titles[$source] ?? 'تعذر تشغيل مراقب الشبكة';
      $type = 'security_monitor_error';
      $meta = json_encode(['source'=>$source], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
      if (!is_string($meta)) $meta = '{}';
      $stmt = @$conn->prepare("INSERT INTO admin_security_alerts
        (alert_key, alert_type, severity, title, message, is_open, first_seen_at, last_seen_at, hit_count, meta_json, created_at)
        VALUES (?,?,?,?,?,1,NOW(),NOW(),1,?,NOW())
        ON DUPLICATE KEY UPDATE severity=VALUES(severity), title=VALUES(title), message=VALUES(message), is_open=1, last_seen_at=NOW(), hit_count=hit_count+1, meta_json=VALUES(meta_json), updated_at=NOW()");
      if (!$stmt) return;
      $stmt->bind_param('ssssss', $key, $type, $severity, $title, $message, $meta);
      @$stmt->execute();
      $stmt->close();
    } catch (Throwable $e) {
      return;
    }
  }
}

// Backward-compatible aliases used by the HTML page.
if (!function_exists('smp_s')) { function smp_s($v, int $max = 191): string { return sm_str($v, $max); } }
if (!function_exists('smp_kind')) { function smp_kind($kind): string { return sm_norm_kind($kind); } }
if (!function_exists('smp_scalar')) { function smp_scalar(mysqli $conn, string $sql): int { return sm_scalar($conn, $sql); } }
if (!function_exists('smp_rows')) { function smp_rows(mysqli $conn, string $sql): array { return sm_rows($conn, $sql); } }
if (!function_exists('smp_allow')) { function smp_allow(mysqli $conn, string $kind, string $value1, string $value2 = '', string $label = '', string $note = ''): bool { return sm_insert_allow($conn, $kind, $value1, $value2, $label, $note); } }

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

if (!function_exists('sm_security_warmup_read')) {
  function sm_security_warmup_read(): array {
    $file = sm_security_warmup_state_file();
    if (!is_file($file)) return [];
    $data = json_decode((string)@file_get_contents($file), true);
    return is_array($data) ? $data : [];
  }
}

if (!function_exists('sm_background_worker_health')) {
  function sm_background_worker_health(string $job = 'monitor', int $ttlSeconds = 180): array {
    $safeJob = preg_replace('/[^a-z0-9\-]+/i', '', $job) ?: 'monitor';
    $file = __DIR__ . '/cache/runtime/background_workers/' . $safeJob . '.json';
    if (!is_file($file)) {
      return ['configured'=>false, 'ok'=>false, 'stale'=>true, 'age'=>null, 'last_run'=>'', 'message'=>'لم تعمل مهمة المراقبة الخلفية بعد.'];
    }
    $state = json_decode((string)@file_get_contents($file), true);
    if (!is_array($state)) {
      return ['configured'=>true, 'ok'=>false, 'stale'=>true, 'age'=>null, 'last_run'=>'', 'message'=>'ملف حالة المراقبة الخلفية غير صالح.'];
    }
    $mtime = (int)@filemtime($file);
    $age = $mtime > 0 ? max(0, time() - $mtime) : null;
    $stale = $age === null || $age > max(60, $ttlSeconds);
    $message = (string)($state['result']['error'] ?? '');
    if ($message === '') {
      foreach (['fast_active','deep','ip_scan'] as $part) {
        $partResult = (array)($state['result'][$part] ?? []);
        if (array_key_exists('ok', $partResult) && empty($partResult['ok'])) {
          $message = (string)($partResult['message'] ?? 'تعذر تنفيذ فحص الشبكة الخلفي.');
          break;
        }
      }
    }
    if ($stale && $message === '') $message = 'توقفت تحديثات مهمة المراقبة الخلفية.';
    return [
      'configured'=>true,
      'ok'=>!empty($state['ok']) && !$stale,
      'stale'=>$stale,
      'age'=>$age,
      'last_run'=>(string)($state['finished_at'] ?? ''),
      'duration_ms'=>(int)($state['duration_ms'] ?? 0),
      'message'=>$message,
    ];
  }
}

if (!function_exists('sm_security_warmup_write')) {
  function sm_security_warmup_write(array $data): void {
    $file = sm_security_warmup_state_file();
    $tmp = $file . '.tmp';
    @file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
    @rename($tmp, $file);
  }
}

if (!function_exists('sm_security_warmup_maybe')) {
  function sm_security_warmup_maybe(mysqli $conn, int $delaySeconds = 300, int $ttlSeconds = 600): array {
    $now = time();
    $delaySeconds = max(60, $delaySeconds);
    $ttlSeconds = max(300, $ttlSeconds);
    $state = sm_security_warmup_read();
    if (empty($state['started_at'])) {
      $state = [
        'started_at'=>$now,
        'started_iso'=>date('Y-m-d H:i:s', $now),
        'last_run_at'=>0,
        'last_status'=>'waiting_first_delay',
      ];
      sm_security_warmup_write($state);
      return ['ok'=>true,'status'=>'waiting','seconds_until_run'=>$delaySeconds,'started_at'=>$state['started_iso']];
    }
    $started = (int)($state['started_at'] ?? $now);
    if (($now - $started) < $delaySeconds) {
      return ['ok'=>true,'status'=>'waiting','seconds_until_run'=>max(0, $delaySeconds - ($now - $started)),'started_at'=>(string)($state['started_iso'] ?? '')];
    }
    $lastRun = (int)($state['last_run_at'] ?? 0);
    if ($lastRun > 0 && ($now - $lastRun) < $ttlSeconds) {
      return ['ok'=>true,'status'=>'gated','last_run_at'=>(string)($state['last_run_iso'] ?? ''),'next_after'=>max(0, $ttlSeconds - ($now - $lastRun)),'last_result'=>(array)($state['last_result'] ?? [])];
    }
    $lockFile = sm_security_warmup_state_file() . '.lock';
    $lock = @fopen($lockFile, 'c');
    if (!$lock || !@flock($lock, LOCK_EX|LOCK_NB)) {
      return ['ok'=>true,'status'=>'busy','message'=>'security warmup already running'];
    }
    try {
      @set_time_limit(120);
      $result = ['started_at'=>date('Y-m-d H:i:s')];
      if (function_exists('mtwatch_run_hourly_deep_sync')) {
        $result['deep'] = mtwatch_run_hourly_deep_sync($conn, false);
      } else {
        $result['deep'] = ['ok'=>false,'message'=>'mtwatch_run_hourly_deep_sync missing'];
      }
      if (function_exists('mtwatch_run_network_ip_scan')) {
        $result['ip_scan'] = mtwatch_run_network_ip_scan($conn, false, 300);
      } else {
        $result['ip_scan'] = ['ok'=>false,'message'=>'mtwatch_run_network_ip_scan missing'];
      }
      // Resolve MAC ownership again using cached/DB sources even if RouterOS is gated/offline.
      if (function_exists('uamac_resolve_mac_ownership')) {
        $result['ownership_cache_pass'] = uamac_resolve_mac_ownership($conn, [], ['auto_cleanup'=>false]);
      }
      foreach (['deep','ip_scan','ownership_cache_pass'] as $part) {
        if (isset($result[$part]) && is_array($result[$part]) && empty($result[$part]['ok']) && function_exists('sm_record_monitor_issue')) {
          sm_record_monitor_issue($conn, 'warmup_' . $part, (string)($result[$part]['message'] ?? 'Security monitor warmup failed.'), 'warn');
        }
      }
      $result['finished_at'] = date('Y-m-d H:i:s');
      $state['last_run_at'] = time();
      $state['last_run_iso'] = date('Y-m-d H:i:s', (int)$state['last_run_at']);
      $state['last_status'] = 'ran';
      $state['last_result'] = $result;
      sm_security_warmup_write($state);
      return ['ok'=>true,'status'=>'ran','result'=>$result,'last_run_at'=>$state['last_run_iso']];
    } catch (Throwable $e) {
      if (function_exists('sm_record_monitor_issue')) sm_record_monitor_issue($conn, 'warmup_exception', $e->getMessage(), 'danger');
      $state['last_run_at'] = time();
      $state['last_run_iso'] = date('Y-m-d H:i:s', (int)$state['last_run_at']);
      $state['last_status'] = 'error';
      $state['last_result'] = ['ok'=>false,'message'=>$e->getMessage()];
      sm_security_warmup_write($state);
      return ['ok'=>false,'status'=>'error','message'=>$e->getMessage()];
    } finally {
      if ($lock) { @flock($lock, LOCK_UN); @fclose($lock); }
    }
  }
}
