<?php
declare(strict_types=1);

/*
 * Subscriber behavior monitoring.
 *
 * Identity conflicts are handled by subscriber_mikrotik_watch_lib.php. This
 * module watches a known subscriber over time and only raises an alert after
 * repeated, corroborated observations. It intentionally avoids expensive
 * firewall connection-table scans and never treats traffic volume alone as a
 * fault.
 */

if (!function_exists('mtwatch_behavior_ensure_schema')) {
  function mtwatch_behavior_ensure_schema(mysqli $conn): void {
    static $done = false;
    if ($done) return;
    $done = true;
    @mysqli_report(MYSQLI_REPORT_OFF);
    @ $conn->query("CREATE TABLE IF NOT EXISTS mtwatch_subscriber_behavior_state (
      behavior_key VARCHAR(190) NOT NULL,
      behavior_type VARCHAR(50) NOT NULL,
      subscriber_id INT NULL,
      username VARCHAR(191) NULL,
      mac_address VARCHAR(32) NULL,
      ip_address VARCHAR(45) NULL,
      last_value VARCHAR(500) NULL,
      violation_streak INT NOT NULL DEFAULT 0,
      clean_streak INT NOT NULL DEFAULT 0,
      event_count INT NOT NULL DEFAULT 0,
      window_started_at DATETIME NULL,
      first_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      meta_json LONGTEXT NULL,
      PRIMARY KEY (behavior_key),
      KEY idx_behavior_type_seen (behavior_type,last_seen_at),
      KEY idx_behavior_subscriber (subscriber_id,last_seen_at),
      KEY idx_behavior_mac (mac_address)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    @ $conn->query("CREATE TABLE IF NOT EXISTS mtwatch_subscriber_behavior_events (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      event_hash CHAR(64) NOT NULL,
      subscriber_id INT NOT NULL,
      username VARCHAR(191) NULL,
      mac_address VARCHAR(32) NULL,
      ip_address VARCHAR(45) NULL,
      event_type VARCHAR(50) NOT NULL,
      event_at DATETIME NOT NULL,
      meta_json LONGTEXT NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (id),
      UNIQUE KEY uniq_behavior_event (event_hash),
      KEY idx_behavior_event_sub (subscriber_id,event_type,event_at),
      KEY idx_behavior_event_mac (mac_address,event_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    @ $conn->query("DELETE FROM mtwatch_subscriber_behavior_events WHERE event_at < DATE_SUB(NOW(), INTERVAL 14 DAY)");
  }
}

if (!function_exists('mtwatch_behavior_json')) {
  function mtwatch_behavior_json(array $value): string {
    $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    return is_string($json) ? $json : '{}';
  }
}

if (!function_exists('mtwatch_behavior_insert_event')) {
  function mtwatch_behavior_insert_event(mysqli $conn, int $subscriberId, string $username, string $mac, string $ip, string $type, string $eventAt, string $hash, array $meta = []): bool {
    if ($subscriberId <= 0 || $hash === '') return false;
    mtwatch_behavior_ensure_schema($conn);
    $mac = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac($mac) : strtoupper(trim($mac));
    $username = function_exists('mtwatch_norm_user') ? mtwatch_norm_user($username) : trim($username);
    $eventAt = function_exists('mtwatch_event_time') ? mtwatch_event_time($eventAt) : date('Y-m-d H:i:s', strtotime($eventAt) ?: time());
    $json = mtwatch_behavior_json($meta);
    $stmt = @$conn->prepare("INSERT IGNORE INTO mtwatch_subscriber_behavior_events
      (event_hash,subscriber_id,username,mac_address,ip_address,event_type,event_at,meta_json,created_at)
      VALUES (?,?,?,?,?,?,?,?,NOW())");
    if (!$stmt) return false;
    $stmt->bind_param('sissssss', $hash, $subscriberId, $username, $mac, $ip, $type, $eventAt, $json);
    $ok = @$stmt->execute() && $stmt->affected_rows > 0;
    $stmt->close();
    return $ok;
  }
}

if (!function_exists('mtwatch_behavior_close')) {
  function mtwatch_behavior_close(mysqli $conn, string $alertKey, string $note): void {
    if (function_exists('mtwatch_close_alert_key')) {
      mtwatch_close_alert_key($conn, $alertKey, 'resolved', $note);
      return;
    }
    $key = $conn->real_escape_string(substr($alertKey, 0, 190));
    $note = $conn->real_escape_string($note);
    @ $conn->query("UPDATE subscriber_network_alerts SET is_open=0,stage='resolved',decision_status='resolved',decision_note='{$note}',updated_at=NOW() WHERE alert_key='{$key}' AND is_open=1");
  }
}

if (!function_exists('mtwatch_behavior_record_session_event')) {
  function mtwatch_behavior_record_session_event(mysqli $conn, int $subscriberId, string $username, string $mac, string $ip, string $kind, string $eventAt, string $sourceHash = ''): array {
    if ($subscriberId <= 0 || !in_array($kind, ['session_login','session_logout','session_restart'], true)) return ['recorded'=>false];
    $hash = $sourceHash !== '' ? hash('sha256', 'behavior|' . $sourceHash) : hash('sha256', implode('|', [$subscriberId,$kind,$eventAt,$mac,$ip]));
    if (!mtwatch_behavior_insert_event($conn, $subscriberId, $username, $mac, $ip, $kind, $eventAt, $hash)) return ['recorded'=>false,'duplicate'=>true];

    $sid = (int)$subscriberId;
    $rs = @$conn->query("SELECT
      SUM(event_type IN ('session_login','session_restart')) AS starts,
      SUM(event_type='session_logout') AS stops,
      COUNT(*) AS total,
      MIN(event_at) AS first_event,
      MAX(event_at) AS last_event
      FROM mtwatch_subscriber_behavior_events
      WHERE subscriber_id={$sid}
        AND event_type IN ('session_login','session_logout','session_restart')
        AND event_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
    $row = $rs ? $rs->fetch_assoc() : [];
    $starts = (int)($row['starts'] ?? 0);
    $stops = (int)($row['stops'] ?? 0);
    $total = (int)($row['total'] ?? 0);

    // A single reconnect, short outage, or incomplete log pair is normal. A
    // confirmed alert needs multiple starts and stops, or an unusually high
    // number of independently observed session restarts.
    $confirmed = ($starts >= 3 && $stops >= 3 && $total >= 6) || $starts >= 7 || $stops >= 7;
    if (!$confirmed) return ['recorded'=>true,'confirmed'=>false,'starts'=>$starts,'stops'=>$stops];

    $severity = $total >= 12 ? 'danger' : 'warn';
    $display = $username !== '' ? $username : ('#' . $subscriberId);
    $message = "تم رصد تذبذب متكرر لجلسة PPPoE للمشترك {$display}: {$starts} بدء/إعادة جلسة و{$stops} انقطاع خلال آخر 15 دقيقة.";
    if ($mac !== '') $message .= " MAC: {$mac}.";
    if ($ip !== '') $message .= " IP: {$ip}.";
    $message .= ' لم يصدر التنبيه من انقطاع واحد؛ تم تأكيده بعد تكرار الأحداث.';
    if (function_exists('mtwatch_alert')) {
      mtwatch_alert($conn, 'subscriber_session_flap:' . $subscriberId, $severity, 'تذبذب متكرر من حساب مشترك معروف', $message, $subscriberId, $username, $mac, $ip, 'subscriber_behavior', [
        'stage'=>'confirmed','behavior_type'=>'session_flap','window_minutes'=>15,
        'starts'=>$starts,'stops'=>$stops,'total'=>$total,
        'first_event'=>(string)($row['first_event'] ?? ''),'last_event'=>(string)($row['last_event'] ?? ''),
        'verification'=>'multiple_distinct_session_events'
      ]);
    }
    return ['recorded'=>true,'confirmed'=>true,'starts'=>$starts,'stops'=>$stops,'severity'=>$severity];
  }
}

if (!function_exists('mtwatch_behavior_process_active_rows')) {
  function mtwatch_behavior_process_active_rows(mysqli $conn, array $activeRows, ?string $sampleAt = null): array {
    mtwatch_behavior_ensure_schema($conn);
    $sampleAt = $sampleAt ?: date('Y-m-d H:i:s');
    $subs = function_exists('mtwatch_subscribers_by_user') ? mtwatch_subscribers_by_user($conn) : [];
    $processed = 0; $restarts = 0; $resolved = 0;
    foreach ($activeRows as $row) {
      $user = function_exists('mtwatch_norm_user') ? mtwatch_norm_user((string)($row['name'] ?? $row['user'] ?? '')) : trim((string)($row['name'] ?? ''));
      if ($user === '' || !isset($subs[$user])) continue;
      $sub = $subs[$user];
      $sid = (int)($sub['id'] ?? 0);
      if ($sid <= 0) continue;
      $mac = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac((string)($row['caller_id'] ?? $row['caller-id'] ?? '')) : '';
      $ip = trim((string)($row['address'] ?? ''));
      $uptime = trim((string)($row['uptime'] ?? ''));
      $startedAt = function_exists('mtwatch_session_started_at') ? mtwatch_session_started_at($sampleAt, $uptime) : $sampleAt;
      $key = 'session:' . $sid;
      $keyEsc = $conn->real_escape_string($key);
      $oldRs = @$conn->query("SELECT last_value,last_seen_at FROM mtwatch_subscriber_behavior_state WHERE behavior_key='{$keyEsc}' LIMIT 1");
      $old = $oldRs ? $oldRs->fetch_assoc() : null;
      $oldStart = trim((string)($old['last_value'] ?? ''));
      $oldSeenTs = strtotime((string)($old['last_seen_at'] ?? '')) ?: 0;
      $startTs = strtotime($startedAt) ?: 0;
      $oldStartTs = strtotime($oldStart) ?: 0;
      if ($oldStartTs > 0 && $startTs > 0 && abs($startTs - $oldStartTs) > 90 && $oldSeenTs >= time() - 900) {
        $eventHash = hash('sha256', "sample_restart|{$sid}|{$startedAt}|{$mac}|{$ip}");
        $result = mtwatch_behavior_record_session_event($conn, $sid, $user, $mac, $ip, 'session_restart', $sampleAt, $eventHash);
        if (!empty($result['recorded'])) $restarts++;
      }
      $meta = mtwatch_behavior_json(['uptime'=>$uptime,'session_started_at'=>$startedAt,'source'=>'active_sample']);
      $stmt = @$conn->prepare("INSERT INTO mtwatch_subscriber_behavior_state
        (behavior_key,behavior_type,subscriber_id,username,mac_address,ip_address,last_value,violation_streak,clean_streak,event_count,window_started_at,first_seen_at,last_seen_at,meta_json)
        VALUES (?,'session',?,?,?,?,?,0,0,0,NOW(),NOW(),NOW(),?)
        ON DUPLICATE KEY UPDATE username=VALUES(username),mac_address=VALUES(mac_address),ip_address=VALUES(ip_address),last_value=VALUES(last_value),last_seen_at=NOW(),meta_json=VALUES(meta_json)");
      if ($stmt) {
        $stmt->bind_param('sisssss', $key, $sid, $user, $mac, $ip, $startedAt, $meta);
        @$stmt->execute(); $stmt->close();
      }
      $uptimeSeconds = function_exists('mtwatch_ppp_uptime_seconds') ? mtwatch_ppp_uptime_seconds($uptime) : 0;
      if ($uptimeSeconds >= 1800) {
        $recentRs = @$conn->query("SELECT COUNT(*) AS c FROM mtwatch_subscriber_behavior_events WHERE subscriber_id={$sid} AND event_type IN ('session_login','session_logout','session_restart') AND event_at>=DATE_SUB(NOW(),INTERVAL 30 MINUTE)");
        $recent = $recentRs ? (int)(($recentRs->fetch_assoc()['c'] ?? 0)) : 0;
        if ($recent === 0) {
          mtwatch_behavior_close($conn, 'subscriber_session_flap:' . $sid, 'استقرت جلسة PPPoE لأكثر من 30 دقيقة ولم تظهر أحداث تذبذب جديدة.');
          $resolved++;
        }
      }
      $processed++;
    }
    return ['ok'=>true,'processed'=>$processed,'restarts'=>$restarts,'resolved_checks'=>$resolved];
  }
}

if (!function_exists('mtwatch_behavior_known_macs')) {
  function mtwatch_behavior_known_macs(mysqli $conn): array {
    $owners = function_exists('mtwatch_subscribers_by_mac') ? mtwatch_subscribers_by_mac($conn) : [];
    $out = [];
    foreach ($owners as $mac => $row) {
      $norm = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac((string)$mac) : strtoupper((string)$mac);
      if ($norm !== '') $out[$norm] = $row;
    }
    return $out;
  }
}

if (!function_exists('mtwatch_behavior_process_arp_conflicts')) {
  function mtwatch_behavior_process_arp_conflicts(mysqli $conn, array $devices, ?string $sampleAt = null): array {
    mtwatch_behavior_ensure_schema($conn);
    $sampleAt = $sampleAt ?: date('Y-m-d H:i:s');
    $owners = mtwatch_behavior_known_macs($conn);
    $bySegment = [];
    foreach ($devices as $d) {
      if (($d['source'] ?? '') !== 'arp' || empty($d['arp_complete'])) continue;
      $ip = trim((string)($d['ip'] ?? ''));
      $mac = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac((string)($d['mac'] ?? '')) : '';
      if ($ip === '' || $mac === '') continue;
      $iface = trim((string)($d['interface'] ?? ''));
      // The same address can legitimately exist in isolated VLANs/VRFs. Only
      // compare ARP claims that belong to the same observed L2 interface.
      $segmentKey = $ip . '|' . mb_strtolower($iface, 'UTF-8');
      if (!isset($bySegment[$segmentKey])) $bySegment[$segmentKey] = ['ip'=>$ip,'interface'=>$iface,'macs'=>[]];
      $bySegment[$segmentKey]['macs'][$mac] = true;
    }
    $activeKeys = []; $confirmed = 0;
    foreach ($bySegment as $segment) {
      $ip = (string)$segment['ip'];
      $iface = (string)$segment['interface'];
      $macs = array_keys((array)$segment['macs']);
      if (count($macs) < 2) continue;
      $known = array_values(array_filter($macs, static fn($m) => isset($owners[$m])));
      if (!$known) continue;
      sort($macs);
      $segmentId = substr(hash('sha256', mb_strtolower($iface, 'UTF-8')), 0, 12);
      $key = 'ip_conflict:' . $ip . ':' . $segmentId;
      $activeKeys[$key] = true;
      $keyEsc = $conn->real_escape_string($key);
      $rs = @$conn->query("SELECT violation_streak,first_seen_at FROM mtwatch_subscriber_behavior_state WHERE behavior_key='{$keyEsc}' LIMIT 1");
      $old = $rs ? $rs->fetch_assoc() : null;
      $streak = max(0, (int)($old['violation_streak'] ?? 0)) + 1;
      $first = (string)($old['first_seen_at'] ?? $sampleAt);
      $age = max(0, time() - (strtotime($first) ?: time()));
      $primary = $owners[$known[0]];
      $sid = (int)($primary['id'] ?? 0);
      $user = function_exists('mtwatch_norm_user') ? mtwatch_norm_user((string)($primary['ppp_username'] ?? $primary['username'] ?? '')) : '';
      $macList = implode(', ', $macs);
      $meta = mtwatch_behavior_json(['macs'=>$macs,'interface'=>$iface,'complete_arp_only'=>true,'sample_at'=>$sampleAt]);
      $stmt = @$conn->prepare("INSERT INTO mtwatch_subscriber_behavior_state
        (behavior_key,behavior_type,subscriber_id,username,mac_address,ip_address,last_value,violation_streak,clean_streak,event_count,window_started_at,first_seen_at,last_seen_at,meta_json)
        VALUES (?,'ip_conflict',?,?,?,?,?,1,0,1,NOW(),NOW(),NOW(),?)
        ON DUPLICATE KEY UPDATE subscriber_id=VALUES(subscriber_id),username=VALUES(username),mac_address=VALUES(mac_address),last_value=VALUES(last_value),violation_streak=violation_streak+1,clean_streak=0,event_count=event_count+1,last_seen_at=NOW(),meta_json=VALUES(meta_json)");
      if ($stmt) { $stmt->bind_param('sisssss', $key, $sid, $user, $known[0], $ip, $macList, $meta); @$stmt->execute(); $stmt->close(); }
      if ($streak >= 3 && $age >= 90 && function_exists('mtwatch_alert')) {
        $message = "العنوان {$ip} ظهر في ثلاث قراءات مكتملة أو أكثر مرتبطًا بعدة عناوين MAC ضمن نفس الواجهة" . ($iface !== '' ? " {$iface}" : '') . ": {$macList}. أحدها يعود للمشترك {$user}. تم تجاهل سجلات ARP غير المكتملة ومن شبكات L2 الأخرى لمنع الإنذار الكاذب.";
        mtwatch_alert($conn, 'subscriber_ip_conflict:' . $ip . ':' . $segmentId, 'danger', 'تعارض IP مستمر مرتبط بمشترك معروف', $message, $sid ?: null, $user, $known[0], $ip, 'subscriber_behavior', [
          'stage'=>'confirmed','behavior_type'=>'persistent_ip_conflict','macs'=>$macs,
          'interface'=>$iface,'consecutive_samples'=>$streak,'age_seconds'=>$age,'verification'=>'complete_arp_same_segment_multiple_samples'
        ]);
        $confirmed++;
      }
    }
    $rs = @$conn->query("SELECT behavior_key,ip_address,clean_streak FROM mtwatch_subscriber_behavior_state WHERE behavior_type='ip_conflict'");
    if ($rs) while ($row = $rs->fetch_assoc()) {
      $key = (string)($row['behavior_key'] ?? '');
      if ($key === '' || isset($activeKeys[$key])) continue;
      $clean = (int)($row['clean_streak'] ?? 0) + 1;
      $keyEsc = $conn->real_escape_string($key);
      @ $conn->query("UPDATE mtwatch_subscriber_behavior_state SET clean_streak={$clean},last_seen_at=NOW()" . ($clean >= 3 ? ",violation_streak=0" : '') . " WHERE behavior_key='{$keyEsc}' LIMIT 1");
      if ($clean >= 3) {
        $suffix = str_starts_with($key, 'ip_conflict:') ? substr($key, strlen('ip_conflict:')) : (string)($row['ip_address'] ?? '');
        mtwatch_behavior_close($conn, 'subscriber_ip_conflict:' . $suffix, 'اختفى تعارض IP في ثلاث قراءات مكتملة متتالية.');
      }
    }
    return ['ok'=>true,'candidate_ips'=>count($activeKeys),'confirmed'=>$confirmed];
  }
}

if (!function_exists('mtwatch_behavior_process_bridge_hosts')) {
  function mtwatch_behavior_process_bridge_hosts(mysqli $conn, array $rows, ?string $sampleAt = null): array {
    mtwatch_behavior_ensure_schema($conn);
    $sampleAt = $sampleAt ?: date('Y-m-d H:i:s');
    $owners = mtwatch_behavior_known_macs($conn);
    $portsByMac = [];
    foreach ($rows as $row) {
      $local = strtolower(trim((string)($row['local'] ?? '')));
      $disabled = strtolower(trim((string)($row['disabled'] ?? '')));
      if (in_array($local, ['true','yes','1'], true) || in_array($disabled, ['true','yes','1'], true)) continue;
      $mac = function_exists('mtwatch_norm_mac') ? mtwatch_norm_mac((string)($row['mac-address'] ?? '')) : '';
      if ($mac === '' || !isset($owners[$mac])) continue;
      $port = trim((string)($row['on-interface'] ?? $row['interface'] ?? ''));
      if ($port === '') continue;
      $portsByMac[$mac][$port] = true;
    }
    $moves = 0; $confirmed = 0;
    foreach ($portsByMac as $mac => $portSet) {
      $ports = array_keys($portSet); sort($ports);
      $value = implode(',', $ports);
      $owner = $owners[$mac];
      $sid = (int)($owner['id'] ?? 0);
      if ($sid <= 0) continue;
      $user = function_exists('mtwatch_norm_user') ? mtwatch_norm_user((string)($owner['ppp_username'] ?? $owner['username'] ?? '')) : '';
      $key = 'mac_move:' . $mac;
      $keyEsc = $conn->real_escape_string($key);
      $rs = @$conn->query("SELECT last_value,event_count,window_started_at,last_seen_at,clean_streak FROM mtwatch_subscriber_behavior_state WHERE behavior_key='{$keyEsc}' LIMIT 1");
      $old = $rs ? $rs->fetch_assoc() : null;
      $previous = trim((string)($old['last_value'] ?? ''));
      $windowTs = strtotime((string)($old['window_started_at'] ?? '')) ?: 0;
      $recent = (strtotime((string)($old['last_seen_at'] ?? '')) ?: 0) >= time() - 240;
      $changed = $previous !== '' && $previous !== $value && $recent;
      $multiPort = count($ports) > 1;
      $eventCount = ($windowTs > 0 && $windowTs >= time() - 600) ? (int)($old['event_count'] ?? 0) : 0;
      if ($changed) $eventCount++;
      if ($multiPort) $eventCount += 2;
      $clean = ($changed || $multiPort) ? 0 : ((int)($old['clean_streak'] ?? 0) + 1);
      $windowStart = ($windowTs > 0 && $windowTs >= time() - 600) ? date('Y-m-d H:i:s', $windowTs) : $sampleAt;
      $meta = mtwatch_behavior_json(['ports'=>$ports,'previous'=>$previous,'changed'=>$changed,'multi_port'=>$multiPort,'sample_at'=>$sampleAt]);
      $stmt = @$conn->prepare("INSERT INTO mtwatch_subscriber_behavior_state
        (behavior_key,behavior_type,subscriber_id,username,mac_address,ip_address,last_value,violation_streak,clean_streak,event_count,window_started_at,first_seen_at,last_seen_at,meta_json)
        VALUES (?,'mac_move',?,?,?,'',?,0,?,?,?,NOW(),NOW(),?)
        ON DUPLICATE KEY UPDATE subscriber_id=VALUES(subscriber_id),username=VALUES(username),last_value=VALUES(last_value),violation_streak=IF(?=1 OR ?=1,violation_streak+1,0),clean_streak=VALUES(clean_streak),event_count=VALUES(event_count),window_started_at=VALUES(window_started_at),last_seen_at=NOW(),meta_json=VALUES(meta_json)");
      if ($stmt) {
        $changedInt = $changed ? 1 : 0; $multiInt = $multiPort ? 1 : 0;
        $stmt->bind_param('sisssiissii', $key, $sid, $user, $mac, $value, $clean, $eventCount, $windowStart, $meta, $changedInt, $multiInt);
        @$stmt->execute(); $stmt->close();
      }
      if ($changed || $multiPort) $moves++;
      if ($eventCount >= 4 && function_exists('mtwatch_alert')) {
        $message = "عنوان MAC المعروف {$mac} للمشترك {$user} تنقل أو ظهر بين منافذ Bridge بصورة متكررة خلال 10 دقائق. المنافذ الحالية: {$value}";
        if ($previous !== '') $message .= ". القراءة السابقة: {$previous}";
        $message .= '. تم تأخير التنبيه حتى تكرار الحركة لمنع اعتبار انتقال عابر خللًا.';
        mtwatch_alert($conn, 'subscriber_mac_flap:' . $mac, $eventCount >= 8 ? 'danger' : 'warn', 'انتقال متكرر لـMAC مشترك معروف', $message, $sid, $user, $mac, '', 'subscriber_behavior', [
          'stage'=>'confirmed','behavior_type'=>'mac_flap','event_count'=>$eventCount,
          'window_minutes'=>10,'ports'=>$ports,'previous'=>$previous,'verification'=>'repeated_bridge_host_moves'
        ]);
        $confirmed++;
      } elseif ($clean >= 5) {
        mtwatch_behavior_close($conn, 'subscriber_mac_flap:' . $mac, 'استقر موقع MAC على منفذ واحد لخمس قراءات متتالية.');
      }
    }
    return ['ok'=>true,'known_macs_seen'=>count($portsByMac),'moves'=>$moves,'confirmed'=>$confirmed];
  }
}
