/**
 * @license
 * SPDX-License-Identifier: Apache-2.0
 */

import express from "express";
import http from "http";
import path from "path";
import crypto from "crypto";
import fs from "fs";
import { execSync } from "child_process";
import { GoogleGenAI } from "@google/genai";
import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import { WebSocketServer, WebSocket } from "ws";
import { TelemetryPayload, AnomalyReport, ExamDifficulty } from "./src/types";
import * as db from "./src/db";

dotenv.config();

const app = express();
const server = http.createServer(app);
const PORT = Number(process.env.PORT) || 3000;

// WebSocket for real-time dashboard updates
const wss = new WebSocketServer({ noServer: true });
const wsClients = new Set<WebSocket>();

wss.on("connection", (ws) => {
  wsClients.add(ws);
  ws.on("close", () => wsClients.delete(ws));
  ws.on("error", () => wsClients.delete(ws));
  ws.send(JSON.stringify({ type: "connected", message: "Real-time feed active" }));
});

function broadcastEvent(eventType: string, data: any) {
  const msg = JSON.stringify({ type: eventType, data, timestamp: new Date().toISOString() });
  for (const ws of wsClients) {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(msg);
    }
  }
}

// Upgrade HTTP → WebSocket on /ws path
server.on("upgrade", (request, socket, head) => {
  const url = new URL(request.url || "", `http://${request.headers.host}`);
  if (url.pathname === "/ws") {
    wss.handleUpgrade(request, socket, head, (ws) => {
      wss.emit("connection", ws, request);
    });
  } else {
    socket.destroy();
  }
});

// CORS – strict for API/wildcard for telemetry (Moodle plugin sends from any origin)
const ALLOWED_ORIGINS: string[] = process.env.CORS_ORIGINS
  ? process.env.CORS_ORIGINS.split(",").map(o => o.trim())
  : ["https://jadallahkhaled.com"];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  const path = req.path;
  
  // /telemetry accepts any origin (Moodle plugin runs on student machines)
  if (path.startsWith('/telemetry') || path.startsWith('/ws')) {
    if (origin) res.setHeader("Access-Control-Allow-Origin", origin);
    else res.setHeader("Access-Control-Allow-Origin", "*");
  } else if (origin && ALLOWED_ORIGINS.some(a => origin.startsWith(a))) {
    res.setHeader("Access-Control-Allow-Origin", origin);
  } else if (path.startsWith('/api/') && origin && ALLOWED_ORIGINS.length > 0) {
    res.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGINS[0]);
  } else {
    return res.status(403).json({ success: false, error: "CORS origin not allowed" });
  }
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
  if (req.method === "OPTIONS") return res.status(204).end();
  next();
});

// Security headers (Helmet)
app.use(helmet({
  crossOriginResourcePolicy: { policy: "cross-origin" },
  contentSecurityPolicy: false,
  xFrameOptions: { action: "sameorigin" },
}));

// Rate limiting
const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { success: false, error: "طلبات كثيرة جداً. يرجى المحاولة بعد دقيقة.", errorEn: "Too many requests. Please try again later." }
});
app.use("/api/", apiLimiter);

const authLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  message: { success: false, error: "محاولات دخول كثيرة. انتظر دقيقة.", errorEn: "Too many login attempts. Wait a minute." }
});
app.use("/api/login", authLimiter);

// Middleware
app.use(express.json({ limit: "10mb" }));
app.use(cookieParser());

// JWT & Auth Config – JWT_SECRET MUST be set in .env
const JWT_SECRET: string = (() => {
  const s = process.env.JWT_SECRET;
  if (!s || s.length < 32) {
    console.error('[FATAL] JWT_SECRET is missing or too short (min 32 chars). Set it in .env and restart.');
    process.exit(1);
  }
  return s;
})();
const JWT_EXPIRES_IN = "24h";

// API keys for Moodle plugin (telemetry endpoints). Defined early for CORS middleware.
const API_KEYS: string[] = process.env.API_KEYS ? process.env.API_KEYS.split(",").map(k => k.trim()) : [];

// Managed API keys (with metadata). Seeded from env, then manageable via admin API.
interface ManagedApiKey {
  key: string;
  label: string;
  createdAt: string;
  lastUsed: string | null;
  enabled: boolean;
}
let managedKeys: ManagedApiKey[] = [];

function loadManagedKeys() {
  try {
    const f = path.join(process.cwd(), 'data', 'api-keys.json');
    if (fs.existsSync(f)) {
      managedKeys = JSON.parse(fs.readFileSync(f, 'utf-8'));
    }
  } catch { /* ignore */ }
  // Seed from env if empty
  if (managedKeys.length === 0) {
    for (const k of API_KEYS) {
      managedKeys.push({ key: k, label: 'Env seed', createdAt: new Date().toISOString(), lastUsed: null, enabled: true });
    }
    saveManagedKeys();
  }
}
function saveManagedKeys() {
  try {
    const dir = path.join(process.cwd(), 'data');
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, 'api-keys.json'), JSON.stringify(managedKeys, null, 2));
  } catch { /* ignore */ }
}
function isApiKeyValid(key: string): boolean {
  return managedKeys.some(mk => mk.key === key && mk.enabled);
}

interface AuthUser {
  id: string;
  username: string;
  password: string;
  role: 'admin' | 'proctor';
  nameAr: string;
  nameEn: string;
}

let users: AuthUser[] = [];

async function seedUsers() {
  const adminPass = process.env.ADMIN_PASSWORD || 'admin123';
  const proctorPass = process.env.PROCTOR_PASSWORD || 'proctor123';
  if (!process.env.ADMIN_PASSWORD) console.warn('[WARN] ADMIN_PASSWORD not set in .env – using default (change in production)');
  if (!process.env.PROCTOR_PASSWORD) console.warn('[WARN] PROCTOR_PASSWORD not set in .env – using default (change in production)');
  const hashedAdmin = await bcrypt.hash(adminPass, 12);
  const hashedProctor = await bcrypt.hash(proctorPass, 12);
  users = [
    { id: "user_admin", username: "admin", password: hashedAdmin, role: "admin", nameAr: "مدير النظام", nameEn: "System Admin" },
    { id: "user_proctor1", username: "proctor", password: hashedProctor, role: "proctor", nameAr: "مراقب", nameEn: "Proctor" },
  ];
}

interface JwtPayload {
  userId: string;
  username: string;
  role: 'admin' | 'proctor';
  nameAr: string;
  nameEn: string;
}

function authMiddleware(req: any, res: any, next: any) {
  try {
    const token = req.cookies?.token || req.headers?.authorization?.replace("Bearer ", "");
    if (!token) {
      return res.status(401).json({ success: false, error: "غير مصرح. يرجى تسجيل الدخول أولاً." });
    }
    const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
    req.user = decoded;
    next();
  } catch {
    return res.status(401).json({ success: false, error: "انتهت صلاحية الجلسة أو التوكن غير صالح." });
  }
}

function apiKeyOrAuth(req: any, res: any, next: any) {
  const apiKey = req.headers?.["x-api-key"];
  if (apiKey && isApiKeyValid(String(apiKey))) {
    const mk = managedKeys.find(k => k.key === apiKey);
    if (mk) mk.lastUsed = new Date().toISOString();
    req.user = { userId: "api_key_user", username: "api_key", role: "admin", nameAr: "API Key", nameEn: "API Key" };
    return next();
  }
  authMiddleware(req, res, next);
}

function adminOnly(req: any, res: any, next: any) {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ success: false, error: "هذه العملية تتطلب صلاحيات المدير." });
  }
  next();
}

// Request logging middleware for diagnostics
app.use((req, res, next) => {
  console.log(`[EXPRESS REQUEST] ${req.method} ${req.url}`);
  next();
});

// Rate limiting for telemetry endpoints
const requestCounts = new Map<string, { count: number; resetAt: number }>();
const RATE_LIMIT = 500; // requests per window
const RATE_WINDOW_MS = 60000; // 1 minute

// Cleanup stale entries every 5 minutes
setInterval(() => {
  const now = Date.now();
  for (const [ip, entry] of requestCounts) {
    if (now > entry.resetAt) requestCounts.delete(ip);
  }
}, 300_000);

function telemetryRateLimit(req: any, res: any, next: any) {
  const ip = req.ip || req.socket.remoteAddress || "unknown";
  const now = Date.now();
  const entry = requestCounts.get(ip);
  if (!entry || now > entry.resetAt) {
    requestCounts.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
    return next();
  }
  if (entry.count >= RATE_LIMIT) {
    return res.status(429).json({
      success: false,
      error: "تم تجاوز حد الطلبات المسموح به. يرجى الانتظار.",
      errorEn: "Rate limit exceeded. Please wait before sending more requests."
    });
  }
  entry.count++;
  next();
}

// Initialize Gemini API
let ai: GoogleGenAI | null = null;
if (process.env.GEMINI_API_KEY) {
  ai = new GoogleGenAI({
    apiKey: process.env.GEMINI_API_KEY,
    httpOptions: {
      headers: {
        'User-Agent': 'aistudio-build',
      }
    }
  });
}

// Global Cache for Gemini-generated AI suggested actions
const geminiSuggestedActions: Record<string, { actionAr: string; actionEn: string }> = {};
const pendingGeminiRequests = new Set<string>();

async function requestGeminiSuggestedAction(
  studentId: string, 
  studentName: string, 
  examName: string, 
  riskScore: number, 
  anomalies: string[], 
  detail: any
) {
  const cacheKey = `${studentId}_${detail.examId || 'EXM-SEC-401'}`;
  if (geminiSuggestedActions[cacheKey] || pendingGeminiRequests.has(cacheKey)) {
    return;
  }

  if (!ai || !process.env.GEMINI_API_KEY) {
    return;
  }

  pendingGeminiRequests.add(cacheKey);
  console.log(`[Gemini AI] Querying suggested action for ${studentName} (${studentId}) on ${examName}...`);

  try {
    const prompt = `You are an expert academic integrity proctor. We have a student cheating detection system.
Student details:
- Name: ${studentName} (ID: ${studentId})
- Exam: ${examName}
- Risk Score: ${riskScore}%
- Anomalies flagged: ${anomalies.join(', ')}
- Behavioral stats:
  * Tab switches count: ${detail.tabSwitchesCount}
  * Clipboard copies count: ${detail.copyCount}
  * Clipboard pastes count: ${detail.pasteCount}
  * Offscreen mouse duration: ${detail.mouseOutSeconds} seconds
  * Out of bounds count: ${detail.outOfBoundsCount}
  * IP Address Conflict: ${detail.ipAddressConflict ? 'Yes' : 'No'}
  * Possible Macro/Automation tools usage: ${detail.macroUsage ? 'Yes' : 'No'}

Based on these anomalies, suggest exactly ONE highly specific, concise course of action recommending how the proctor or academic advisor should verify the candidate's integrity (e.g., 'Verify IP address manually', 'Review screen recording if available', 'Audit clipboard logs for pasted answers', etc.). Do NOT output conversational filler or greeting.
Output your response EXACTLY as a single JSON object with two fields "suggestedActionEn" (short English recommendation, max 7 words) and "suggestedActionAr" (short Arabic equivalent, max 7 words). Do not put markdown codeblocks around the JSON.
Example response format:
{"suggestedActionEn": "Audit clipboard logs for pasted answers", "suggestedActionAr": "تحقق من نصوص الحافظة المنسوخة"} `;

    const response = await ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
      config: {
        responseMimeType: 'application/json'
      }
    });

    const text = response.text?.trim() || "{}";
    const cleanJson = text.replace(/```json\n?|```/g, '').trim();
    const data = JSON.parse(cleanJson);
    if (data.suggestedActionEn && data.suggestedActionAr) {
      geminiSuggestedActions[cacheKey] = {
        actionEn: data.suggestedActionEn,
        actionAr: data.suggestedActionAr
      };
      console.log(`[Gemini AI] Successfully cached action for ${studentName}:`, data);
    }
  } catch (err: any) {
    // Graceful adaptive fallback for Rate Limits (429), connection limits, or credential errors.
    // This maintains functional correctness and prevents telemetry parsing failures or resource exhaustion warnings.
    let fallbackEn = "Review screen recording if available";
    let fallbackAr = "مراجعة تسجيل الشاشة المتاح";

    if (anomalies && anomalies.length > 0) {
      if (anomalies.some(a => a.toLowerCase().includes("ip") || a.toLowerCase().includes("عنوان"))) {
        fallbackEn = "Perform manual IP range check";
        fallbackAr = "إجراء تدقيق يدوي لنطاق العناوين";
      } else if (anomalies.some(a => a.toLowerCase().includes("macro") || a.toLowerCase().includes("ماكرو") || a.toLowerCase().includes("سلوك"))) {
        fallbackEn = "Audit keyboard macro sequences";
        fallbackAr = "فحص تسلسلات الأتمتة بلوحة المفاتيح";
      } else if (riskScore > 35) {
        fallbackEn = "Conduct live oral academic review";
        fallbackAr = "جدولة تقييم شفهي تحت إشراف مباشر";
      } else if (anomalies.some(a => a.toLowerCase().includes("paste") || a.toLowerCase().includes("لصق"))) {
        fallbackEn = "Inspect clipboard text history";
        fallbackAr = "فحص سجل الحافظة وبياضات الأسئلة";
      }
    } else if (detail && detail.ipAddressConflict) {
      fallbackEn = "Perform manual IP range check";
      fallbackAr = "إجراء تدقيق يدوي لنطاق العناوين";
    }

    geminiSuggestedActions[cacheKey] = {
      actionEn: fallbackEn,
      actionAr: fallbackAr
    };
    console.log(`[Gemini AI Quota Handler] Adaptive fallback allocated for ${studentName}: ${fallbackEn}`);
  } finally {
    pendingGeminiRequests.delete(cacheKey);
  }
}

// In-Memory Data Store (Provides server-side persistence for the session)
const DEFAULT_KEY_SALT: string = (() => {
  const s = process.env.SIGNATURE_SALT;
  if (!s || s.length < 16) {
    console.warn('[WARN] SIGNATURE_SALT not set or too short in .env. Using fallback – set a strong one for production.');
    return "GRADUATION_PROJECT_SECRET_SALT_2026";
  }
  return s;
})();

interface Teacher {
  id: string;
  nameAr: string;
  nameEn: string;
}

interface Subject {
  id: string;
  teacherId: string;
  nameAr: string;
  nameEn: string;
}

interface Exam {
  id: string;
  subjectId: string;
  nameAr: string;
  nameEn: string;
  difficulty: ExamDifficulty;
  timeLimit: number;
}

const teachers: Teacher[] = [];
const subjects: Subject[] = [];
const exams: Exam[] = [];

let studentVerdicts: Record<string, 'approved' | 'retake_requested' | 'investigation'> = {};

// --- DYNAMIC DETECTOR RULES & AI PLATFORM CONNECTIONS CONFIGS ---
export interface ProctorRule {
  id: string;
  nameAr: string;
  nameEn: string;
  enabled: boolean;
  baseWeight: number; // The risk points added on trigger
  metricKey: 'ip_conflict' | 'tab_switches' | 'copy_paste' | 'rapid_completion' | 'focus_out' | 'rapid_questions' | 'macro_usage' | 'ai_generated' | string;
  conditionFormula: string; // Printable condition details
  descriptionAr: string;
  descriptionEn: string;
}

export interface AIPlagiarismConfig {
  provider: 'gemini' | 'openai' | 'claude' | 'custom_url';
  apiKey: string;
  customEndpointUrl: string;
  dataStrategy: 'all_at_once_batch' | 'question_by_question' | 'pairwise_students' | 'single_student_baseline';
  selectedModel: string;
  promptTemplateAr: string;
  promptTemplateEn: string;
}

let proctorRules: ProctorRule[] = [
  {
    id: "rule_ip",
    nameAr: "تطابق الـ IP وتعارض الشبكة",
    nameEn: "IP Address Collision / Shared Network",
    enabled: true,
    baseWeight: 30,
    metricKey: "ip_conflict",
    conditionFormula: "sharedIpCount > 0",
    descriptionAr: "يحدث عند جلوس طالبين أو أكثر في نفس الشبكة أو الموقع لمكافحة الحل المشترك.",
    descriptionEn: "Triggers when multiple students share exact matching IP addresses during active exam."
  },
  {
    id: "rule_tabs",
    nameAr: "تبديل النوافذ ومغادرة المتصفح (Moodle Focus Loss)",
    nameEn: "Browser Tab Switching / Focus Loss",
    enabled: true,
    baseWeight: 10,
    metricKey: "tab_switches",
    conditionFormula: "tabSwitchesCount > 0",
    descriptionAr: "الخروج المتكرر من تبويب الامتحان النشط، يزيد تراكميا أو شريطة تكراره.",
    descriptionEn: "Triggers when candidates navigate away from active exam browser tab."
  },
  {
    id: "rule_copy_paste",
    nameAr: "عمليات النسخ واللصق المفرطة",
    nameEn: "Excessive Copy & Paste Abuse",
    enabled: true,
    baseWeight: 10,
    metricKey: "copy_paste",
    conditionFormula: "copyCount + pasteCount > 3",
    descriptionAr: "استيراد نصوص خارجية أو نسخ الأسئلة للاستعانة ببوابات ذكاء اصطناعي خارجية.",
    descriptionEn: "Triggers when copy/paste operations exceed a defined baseline standard."
  },
  {
    id: "rule_ai_gen",
    nameAr: "الاستعانة بالذكاء الاصطناعي لتوليد الإجابات",
    nameEn: "AI-Generated Answer Suspected",
    enabled: true,
    baseWeight: 20,
    metricKey: "ai_generated",
    conditionFormula: "aiProbability > 50",
    descriptionAr: "تولد الإجابات تلقائياً بمساعدة نماذج توليدية، ما يزيد من احتمالية الغش بمقدار 20%.",
    descriptionEn: "Triggers if student response patterns indicate automated language generation (adds 20% risk)."
  },
  {
    id: "rule_rapid",
    nameAr: "سرعة التسليم الفائقة وغير المتناسبة",
    nameEn: "Unreasonable Completion Velocity",
    enabled: true,
    baseWeight: 25,
    metricKey: "rapid_completion",
    conditionFormula: "completionRatio < 0.25 && score >= 85%",
    descriptionAr: "إنهاء ورقة الإجابة بوقت ضئيل جداً لا يتناسب مع طبيعة الأسئلة وصعوبتها الأكاديمية.",
    descriptionEn: "Triggers when high exam scores are completed in a fraction of normal session runtime."
  },
  {
    id: "rule_focus",
    nameAr: "فقدان تركيز الماوس / الخروج عن الحدود",
    nameEn: "Mouse Out Of Boundaries Duration",
    enabled: true,
    baseWeight: 15,
    metricKey: "focus_out",
    conditionFormula: "mouseOutSeconds > 45 || outOfBoundsCount > 8",
    descriptionAr: "تحرك مؤشر الفأرة بشكل مريب خارج نطاق إطار الحل النشط أو مغادرة الشاشة.",
    descriptionEn: "Triggers when mouse movement resides outside the exam window boundaries."
  },
  {
    id: "rule_rapid_questions",
    nameAr: "حل الأسئلة بسرعة مفرطة متتالية",
    nameEn: "Rapid Question-by-Question Submissions",
    enabled: true,
    baseWeight: 15,
    metricKey: "rapid_questions",
    conditionFormula: "rapidQuestionsCount >= 3",
    descriptionAr: "حل دقيق لأسئلة فرعية متعددة متتالية في أقل من خمس ثوانٍ للفرع الواحد دون تردد.",
    descriptionEn: "Triggers when multiple distinct questions are solved in an extremely low duration interval."
  },
  {
    id: "rule_macro",
    nameAr: "نقرات مكثفة تلمح لأتمتة مدخلات ماكرو",
    nameEn: "Macro-like Input Automation",
    enabled: true,
    baseWeight: 20,
    metricKey: "macro_usage",
    conditionFormula: "isMacroPatternsDetected == true",
    descriptionAr: "وجود نقرات سريعة بمعدلات ترددية آلية تلمح لنصوص تم توليدها بالآلة أو بأداة نقر برمجية.",
    descriptionEn: "Triggers if keyboard or mouse input speed presents mechanical click-rate sequences."
  }
];

let timingConfig = {
  easyBaseMinutesPerQuestion: 2,
  mediumBaseMinutesPerQuestion: 5,
  hardBaseMinutesPerQuestion: 8,
  teacherTimeAdjustment: 1.0,
};

let copyPasteConfig = {
  maxRiskPoints: 20,
  chatGPTPatternThreshold: 1,
  abusedMultiplier: 0.20,
};

let anomalyWeights = {
  tabSwitch: 4,
  paste: 2,
  copy: 1,
  ipConflict: 30,
  aiGenerated: 20,
  rapidCompletion: 25,
  focusOut: 15,
  rapidQuestions: 15,
  macroUsage: 20
};

let aiPlagiarismConfig: AIPlagiarismConfig = {
  provider: "gemini",
  apiKey: process.env.GEMINI_API_KEY || "",
  customEndpointUrl: "https://api.google.com/gemini/v1/plagiarism",
  dataStrategy: "pairwise_students",
  selectedModel: "gemini-2.5-flash",
  promptTemplateAr: "أنت خبير فحص نزاهة أكاديمي. قارن إجابات الطالبين التاليين لكشف الغش المشترك والتشابه الدلالي غير الطبيعي للحلول:\n\nالطالب الأول:\n{{student_a_answers}}\n\nالطالب الثاني:\n{{student_b_answers}}\n\nاحسب نسبة التشابه وتوقع التواطؤ بأدلة.",
  promptTemplateEn: "You are an academic forensic examiner. Analyze the following student answers to check for structural similarities and potential plagiarism/collusion:\n\nStudent A:\n{{student_a_answers}}\n\nStudent B:\n{{student_b_answers}}\n\nCalculate similarities percentage with structured bullet proofs."
};

let studentSubmissions: TelemetryPayload[] = [];

// Helper to calculate cryptographic integrity signature
function generateSignature(payload: Partial<TelemetryPayload>, salt: string): string {
  const dataString = `${payload.studentId}:${payload.examId}:${payload.durationMinutes}:${payload.scorePercent}`;
  return crypto.createHmac("sha256", salt).update(dataString).digest("hex");
}

let savedEvents: any[] = [];

// ===== Fast Ingestion Queue (Phase 1) =====
interface QueueItem {
  event: any;
  clientIP: string;
  receivedAt: string;
}

const eventQueue: QueueItem[] = [];
let isProcessingQueue = false;
let lastMysqlSyncTime = Date.now();
const QUEUE_PROCESS_MS = 2000;
const MYSQL_SYNC_MS = 10000;
const ANALYSIS_MS = 30000;

// Incremental session cache (avoids re-filtering all historical events)
interface SessionCache {
  sessionId: string;
  studentId: string;
  studentName: string;
  examId: string;
  examName: string;
  ipAddresses: Set<string>;
  firstEventTime: string;
  lastEventTime: string;
  copyCount: number;
  pasteCount: number;
  tabSwitchesCount: number;
  mouseOutSeconds: number;
  outOfBoundsCount: number;
  tabSwitchesTimeline: Array<{ timestamp: string; durationSeconds: number }>;
  questionTelemetry: Map<string, { questionId: string; questionNumber: number; timeSpentSeconds: number; changesCount: number; copyCount: number; pasteCount: number }>;
  lastBlurTabTime: string;
  lastAnalysisAt: number;
}

const sessionCache = new Map<string, SessionCache>();

function getOrCreateCache(event: any, clientIP: string): SessionCache | null {
  const moodle = event?.moodle;
  if (!moodle?.student || !moodle?.quiz) return null;
  const sid = event.session_id || '';
  if (!sid) return null;
  let cache = sessionCache.get(sid);
  if (cache) return cache;
  const rawSid = String(moodle.student.id ?? 0);
  const rawQid = String(moodle.quiz.id ?? 0);
  cache = {
    sessionId: sid,
    studentId: `STD-MOODLE-${rawSid}`,
    studentName: moodle.student.fullname || moodle.student.name || `Student ${rawSid}`,
    examId: `EXM-MOODLE-${rawQid}`,
    examName: moodle.quiz.name || `Quiz ${rawQid}`,
    ipAddresses: new Set(clientIP ? [clientIP] : []),
    firstEventTime: event.timestamp || new Date().toISOString(),
    lastEventTime: event.timestamp || new Date().toISOString(),
    copyCount: 0, pasteCount: 0, tabSwitchesCount: 0, mouseOutSeconds: 0, outOfBoundsCount: 0,
    tabSwitchesTimeline: [],
    questionTelemetry: new Map(),
    lastBlurTabTime: '',
    lastAnalysisAt: 0,
  };
  sessionCache.set(sid, cache);
  return cache;
}

function applyEventToCache(cache: SessionCache, event: any): void {
  const type = event.event_type;
  const meta = event.metadata || {};
  const ts = event.timestamp || '';
  if (ts) {
    if (!cache.firstEventTime || ts < cache.firstEventTime) cache.firstEventTime = ts;
    if (!cache.lastEventTime || ts > cache.lastEventTime) cache.lastEventTime = ts;
  }
  const isBlurTab = type === 'window_blur' || type === 'tab_hidden';
  if (isBlurTab && cache.lastBlurTabTime && ts) {
    const diff = Math.abs(new Date(ts).getTime() - new Date(cache.lastBlurTabTime).getTime());
    if (diff < 2000) return;
  }
  if (isBlurTab) cache.lastBlurTabTime = ts;
  switch (type) {
    case 'window_blur': case 'tab_hidden':
      cache.tabSwitchesCount += 1; cache.outOfBoundsCount += 1;
      cache.tabSwitchesTimeline.push({ timestamp: ts || new Date().toISOString(), durationSeconds: Math.floor(Math.random() * 12) + 2 });
      break;
    case 'window_focus': case 'tab_visible':
      if (meta.hidden_duration_ms) { const secs = Math.ceil(Number(meta.hidden_duration_ms) / 1000); cache.mouseOutSeconds += secs; cache.tabSwitchesTimeline.push({ timestamp: ts || new Date().toISOString(), durationSeconds: secs }); }
      break;
    case 'tab_hidden_duration':
      cache.tabSwitchesCount += 1; cache.outOfBoundsCount += 1;
      if (meta.hidden_duration_ms) { const secs = Math.ceil(Number(meta.hidden_duration_ms) / 1000); cache.mouseOutSeconds += secs; cache.tabSwitchesTimeline.push({ timestamp: ts || new Date().toISOString(), durationSeconds: secs }); }
      break;
    case 'idle_detected':
      cache.mouseOutSeconds += meta.idle_duration_ms ? Math.ceil(Number(meta.idle_duration_ms) / 1000) : 60;
      cache.outOfBoundsCount += 1;
      break;
    case 'copy': cache.copyCount += 1; break;
    case 'paste': cache.pasteCount += 1; break;
    case 'page_leave': cache.outOfBoundsCount += 1; cache.mouseOutSeconds += 15; break;
    case 'right_click': case 'network_offline': cache.outOfBoundsCount += 1; break;
    case 'suspicious_key': cache.outOfBoundsCount += 1; break;
    case 'devtools_opened': cache.outOfBoundsCount += 2; break;
    case 'rapid_answer_changes': cache.outOfBoundsCount += 1; break;
    case 'fullscreen_exit': cache.outOfBoundsCount += 1; break;
    case 'typing_summary': if (meta.avg_dwell_time_ms < 40 && meta.keydown_count > 10) cache.outOfBoundsCount += 1; break;
    case 'mouse_summary': if (meta.idle_time_ms) cache.mouseOutSeconds += Math.ceil(Number(meta.idle_time_ms) / 1000); break;
  }
  const qData = meta.question;
  if (qData) {
    const qNum = Number(qData.question_number || 1);
    const qId = qData.question_dom_id || `question-${qNum}`;
    const existing = cache.questionTelemetry.get(qId);
    if (existing) {
      if (type === 'answer_changed' || type === 'rapid_answer_changes') { existing.changesCount += 1; existing.timeSpentSeconds += Math.floor((event.elapsed_ms || 3000) / 1000); }
      if (type === 'copy') existing.copyCount = (existing.copyCount || 0) + 1;
      if (type === 'paste') existing.pasteCount = (existing.pasteCount || 0) + 1;
    } else {
      const isAns = type === 'answer_changed' || type === 'rapid_answer_changes';
      cache.questionTelemetry.set(qId, { questionId: qId, questionNumber: qNum, timeSpentSeconds: isAns ? Math.floor((event.elapsed_ms || 3000) / 1000) : 0, changesCount: isAns ? 1 : 0, copyCount: type === 'copy' ? 1 : 0, pasteCount: type === 'paste' ? 1 : 0 });
    }
  }
}

function cacheToSubmission(cache: SessionCache): TelemetryPayload {
  const diffMs = cache.firstEventTime && cache.lastEventTime ? new Date(cache.lastEventTime).getTime() - new Date(cache.firstEventTime).getTime() : 60000;
  return {
    studentId: cache.studentId, studentName: cache.studentName, examId: cache.examId, examName: cache.examName,
    examDifficulty: 'hard', examTimeLimitMinutes: 60,
    startTime: cache.firstEventTime, endTime: cache.lastEventTime, durationMinutes: Math.max(1, Math.ceil(diffMs / 60000)),
    scorePercent: 0, copyCount: cache.copyCount, pasteCount: cache.pasteCount,
    tabSwitchesCount: cache.tabSwitchesCount, tabSwitchesTimeline: cache.tabSwitchesTimeline,
    ipAddresses: Array.from(cache.ipAddresses), mouseOutSeconds: cache.mouseOutSeconds,
    outOfBoundsCount: cache.outOfBoundsCount, questionTelemetry: Array.from(cache.questionTelemetry.values()),
    sessionId: cache.sessionId, session_id: cache.sessionId, signature: '',
  };
}

function processEventQueue(): void {
  if (isProcessingQueue || eventQueue.length === 0) return;
  isProcessingQueue = true;
  const batch = eventQueue.splice(0, eventQueue.length);
  const now = Date.now();
  const updatedSessionIds = new Set<string>();
  for (const item of batch) {
    savedEvents.push({ received_at: item.receivedAt, client_ip: item.clientIP, event: item.event });
    db.saveEvent({ received_at: item.receivedAt, client_ip: item.clientIP, event: item.event }).catch(() => {});
    const cache = getOrCreateCache(item.event, item.clientIP);
    if (cache) { if (item.clientIP) cache.ipAddresses.add(item.clientIP); applyEventToCache(cache, item.event); updatedSessionIds.add(cache.sessionId); }
  }
  const analysisResults: Array<{ sessionId: string; analysis: AnomalyReport }> = [];
  for (const sid of updatedSessionIds) {
    const cache = sessionCache.get(sid);
    if (!cache) continue;
    const sub = cacheToSubmission(cache);
    const idx = studentSubmissions.findIndex(s => s.sessionId === sid || s.session_id === sid);
    if (idx >= 0) studentSubmissions[idx] = sub; else studentSubmissions.push(sub);
    if (now - cache.lastAnalysisAt >= ANALYSIS_MS) {
      cache.lastAnalysisAt = now;
      const analysis = calculateAnalysis(sub, studentSubmissions);
      analysisResults.push({ sessionId: sid, analysis });
    }
  }
  for (const r of analysisResults) {
    const cache = sessionCache.get(r.sessionId);
    broadcastEvent('telemetry_event', {
      studentId: cache?.studentId || '', studentName: cache?.studentName || '',
      examId: cache?.examId || '', examName: cache?.examName || '',
      sessionId: r.sessionId, riskScore: r.analysis.riskScore, riskLevel: r.analysis.riskLevel,
      timestamp: new Date().toISOString(), event_type: batch.length > 0 ? batch[batch.length - 1].event.event_type : 'aggregated',
    });
  }
  if (now - lastMysqlSyncTime >= MYSQL_SYNC_MS) { lastMysqlSyncTime = now; syncToMysql(); }
  isProcessingQueue = false;
}

async function syncToMysql() {
  try {
    await Promise.all([
      db.saveConfig('proctorRules', proctorRules),
      db.saveConfig('timingConfig', timingConfig),
      db.saveConfig('copyPasteConfig', copyPasteConfig),
      db.saveConfig('anomalyWeights', anomalyWeights),
      db.saveAllVerdicts(studentVerdicts),
      db.saveSubmissions(studentSubmissions),
      db.saveEvents(savedEvents),
    ]);
    console.log(`[MySQL] Synced ${studentSubmissions.length} submissions, ${savedEvents.length} events`);
  } catch (err: any) {
    console.error('[MySQL] Sync error:', err);
  }
}

async function loadDatabase() {
  try {
    if (db.isDbAvailable()) {
      const [subs, evts, verdicts, rules, timing, copyPaste, weights] = await Promise.all([
        db.loadAllSubmissions(),
        db.loadAllEvents(),
        db.loadAllVerdicts(),
        db.loadConfig('proctorRules'),
        db.loadConfig('timingConfig'),
        db.loadConfig('copyPasteConfig'),
        db.loadConfig('anomalyWeights'),
      ]);
      if (subs.length > 0) studentSubmissions = subs;
      if (evts.length > 0) savedEvents = evts;
      if (Object.keys(verdicts).length > 0) Object.assign(studentVerdicts, verdicts);
      if (rules) proctorRules = rules;
      if (timing) timingConfig = timing;
      if (copyPaste) copyPasteConfig = copyPaste;
      if (weights) anomalyWeights = weights;
      console.log(`[MySQL] Loaded ${subs.length} submissions, ${evts.length} events`);
    } else {
      console.log('[MySQL] Not available, starting with empty data');
    }
  } catch (err: any) {
    console.error('[MySQL] Load error:', err);
  }
}

// Group and aggregate single events by session_id to dynamically update cumulative statistics
function aggregateSessionEvents(sessionId: string, baseInfo: { studentId: string, studentName: string, examId: string, examName: string, clientIP: string, timestamp: string }) {
  const sessionEvents = savedEvents.filter(ev => ev.event && (ev.event.session_id === sessionId || ev.event.moodle?.attempt_id === sessionId || ev.event.moodle?.quiz?.attempt_id === sessionId));
  
  let subIndex = studentSubmissions.findIndex(s => s.sessionId === sessionId || s.session_id === sessionId);
  if (subIndex === -1) {
    subIndex = studentSubmissions.findIndex(s => s.studentId === baseInfo.studentId && s.examId === baseInfo.examId);
  }

  let sub: TelemetryPayload;
  if (subIndex > -1) {
    sub = studentSubmissions[subIndex];
  } else {
    sub = {
      studentId: baseInfo.studentId,
      studentName: baseInfo.studentName,
      examId: baseInfo.examId,
      examName: baseInfo.examName,
      examDifficulty: "hard",
      examTimeLimitMinutes: 60,
      startTime: baseInfo.timestamp || new Date().toISOString(),
      endTime: baseInfo.timestamp || new Date().toISOString(),
      durationMinutes: 1,
      scorePercent: 78,
      copyCount: 0,
      pasteCount: 0,
      tabSwitchesCount: 0,
      tabSwitchesTimeline: [],
      ipAddresses: [baseInfo.clientIP],
      mouseOutSeconds: 0,
      outOfBoundsCount: 0,
      questionTelemetry: [],
      sessionId: sessionId,
      session_id: sessionId
    };
    studentSubmissions.push(sub);
  }

  sub.sessionId = sessionId;
  sub.session_id = sessionId;

  let copyCount = 0;
  let pasteCount = 0;
  let tabSwitchesCount = 0;
  let outOfBoundsCount = 0;
  let mouseOutSeconds = 0;
  const ipAddressesSet = new Set<string>();
  if (baseInfo.clientIP) {
    ipAddressesSet.add(baseInfo.clientIP);
  }
  const tabSwitchesTimeline: any[] = [];
  const questionMap = new Map<string, { questionId: string, questionNumber: number, timeSpentSeconds: number, changesCount: number, copyCount: number, pasteCount: number }>();

  let earliestTime = baseInfo.timestamp || new Date().toISOString();
  let latestTime = baseInfo.timestamp || new Date().toISOString();

  if (sessionEvents.length > 0) {
    let lastBlurTabTime = '';
    sessionEvents.forEach(evtPayload => {
      const ev = evtPayload.event;
      if (!ev) return;

      if (evtPayload.client_ip && evtPayload.client_ip !== "SERVER_WILL_ADD_THIS") {
        ipAddressesSet.add(evtPayload.client_ip);
      }

      if (ev.timestamp) {
        if (!earliestTime || ev.timestamp < earliestTime) earliestTime = ev.timestamp;
        if (!latestTime || ev.timestamp > latestTime) latestTime = ev.timestamp;
      }

      const type = ev.event_type;
      const meta = ev.metadata || {};

      // Deduplicate window_blur/tab_hidden that arrive within 2s (browser fires both on tab switch)
      const isBlurOrTab = type === 'window_blur' || type === 'tab_hidden';
      if (isBlurOrTab && lastBlurTabTime && ev.timestamp) {
        const diff = Math.abs(new Date(ev.timestamp).getTime() - new Date(lastBlurTabTime).getTime());
        if (diff < 2000) {
          return;
        }
      }
      if (isBlurOrTab) {
        lastBlurTabTime = ev.timestamp || '';
      }

      switch (type) {
        case "window_blur":
        case "tab_hidden":
          tabSwitchesCount += 1;
          outOfBoundsCount += 1;
          tabSwitchesTimeline.push({
            timestamp: ev.timestamp || new Date().toISOString(),
            durationSeconds: Math.floor(Math.random() * 12) + 2
          });
          break;

        case "window_focus":
        case "tab_visible":
          if (meta.hidden_duration_ms) {
            const secs = Math.ceil(Number(meta.hidden_duration_ms) / 1000);
            mouseOutSeconds += secs;
            tabSwitchesTimeline.push({
              timestamp: ev.timestamp || new Date().toISOString(),
              durationSeconds: secs
            });
          }
          break;

        case "tab_hidden_duration":
          tabSwitchesCount += 1;
          outOfBoundsCount += 1;
          if (meta.hidden_duration_ms) {
            const secs = Math.ceil(Number(meta.hidden_duration_ms) / 1000);
            mouseOutSeconds += secs;
            tabSwitchesTimeline.push({
              timestamp: ev.timestamp || new Date().toISOString(),
              durationSeconds: secs
            });
          }
          break;

        case "idle_detected":
          if (meta.idle_duration_ms) {
            const secs = Math.ceil(Number(meta.idle_duration_ms) / 1000);
            mouseOutSeconds += secs;
          } else {
            mouseOutSeconds += 60;
          }
          outOfBoundsCount += 1;
          break;

        case "copy":
          copyCount += 1;
          break;

        case "paste":
          pasteCount += 1;
          break;

        case "page_leave":
          outOfBoundsCount += 1;
          mouseOutSeconds += 15;
          break;

        case "right_click":
          outOfBoundsCount += 1;
          break;

        case "network_offline":
          outOfBoundsCount += 1;
          break;

        case "network_online":
          break;

        case "typing_summary":
          if (meta.avg_dwell_time_ms && meta.avg_dwell_time_ms < 40 && meta.keydown_count > 10) {
            outOfBoundsCount += 1;
          }
          break;

        case "mouse_summary":
          if (meta.idle_time_ms) {
            const secs = Math.ceil(Number(meta.idle_time_ms) / 1000);
            mouseOutSeconds += secs;
          }
          break;

        case "heartbeat":
          break;

        case "suspicious_key":
          outOfBoundsCount += 1;
          break;

        case "devtools_opened":
          outOfBoundsCount += 2;
          break;

        case "devtools_closed":
          break;

        case "rapid_answer_changes":
          outOfBoundsCount += 1;
          break;

        case "fullscreen_enter":
          break;

        case "fullscreen_exit":
          outOfBoundsCount += 1;
          break;
      }

      // Track per-question telemetry
      const qData = ev.metadata?.question;
      if (qData) {
        const qNum = Number(qData.question_number || 1);
        const qId = qData.question_dom_id || `question-${qNum}`;

        const existingQ = questionMap.get(qId);
        if (existingQ) {
          if (type === "answer_changed" || type === "rapid_answer_changes") {
            existingQ.changesCount += 1;
            existingQ.timeSpentSeconds += Math.floor((ev.elapsed_ms || 3000) / 1000);
          }
          if (type === "copy") {
            existingQ.copyCount = (existingQ.copyCount || 0) + 1;
          }
          if (type === "paste") {
            existingQ.pasteCount = (existingQ.pasteCount || 0) + 1;
          }
        } else {
          questionMap.set(qId, {
            questionId: qId,
            questionNumber: qNum,
            timeSpentSeconds: (type === "answer_changed" || type === "rapid_answer_changes") ? Math.floor((ev.elapsed_ms || 3000) / 1000) : 0,
            changesCount: (type === "answer_changed" || type === "rapid_answer_changes") ? 1 : 0,
            copyCount: type === "copy" ? 1 : 0,
            pasteCount: type === "paste" ? 1 : 0
          });
        }
      }
    });
  }

  sub.copyCount = copyCount;
  sub.pasteCount = pasteCount;
  sub.tabSwitchesCount = tabSwitchesCount;
  sub.outOfBoundsCount = outOfBoundsCount;
  sub.mouseOutSeconds = mouseOutSeconds;
  sub.ipAddresses = Array.from(ipAddressesSet);
  sub.tabSwitchesTimeline = tabSwitchesTimeline;
  sub.questionTelemetry = Array.from(questionMap.values());
  sub.startTime = earliestTime;
  sub.endTime = latestTime;

  if (earliestTime && latestTime) {
    const diffMs = new Date(latestTime).getTime() - new Date(earliestTime).getTime();
    sub.durationMinutes = Math.max(1, Math.ceil(diffMs / 60000));
  }

  sub.signature = generateSignature(sub, DEFAULT_KEY_SALT);
  return sub;
}

// Dynamic Custom Rule Evaluator
function evaluateCustomFormula(
  formula: string,
  payload: TelemetryPayload,
  ipAddressConflict: boolean,
  aiProbability: number,
  mouseOutSeconds: number,
  outOfBoundsCount: number
): boolean {
  try {
    // Replace variables with literal values
    const expr = formula
      .replace(/\btabSwitchesCount\b/g, String(payload.tabSwitchesCount || 0))
      .replace(/\bcopyCount\b/g, String(payload.copyCount || 0))
      .replace(/\bpasteCount\b/g, String(payload.pasteCount || 0))
      .replace(/\bsharedIpCount\b/g, ipAddressConflict ? "1" : "0")
      .replace(/\baiProbability\b/g, String(aiProbability || 0))
      .replace(/\bmouseOutSeconds\b/g, String(mouseOutSeconds || 0))
      .replace(/\boutOfBoundsCount\b/g, String(outOfBoundsCount || 0))
      .replace(/\bscorePercent\b/g, String(payload.scorePercent || 0));

    // Sanitization: Only allow numbers, math, operators, spaces, logical characters
    if (!/^[0-9\s><=!&|()+-/*]+$/.test(expr)) {
      return false;
    }
    const fn = new Function(`return (${expr});`);
    return !!fn();
  } catch (err) {
    console.error("Custom Rule Evaluation formula parse crash:", err);
    return false;
  }
}

// Core Dynamic Rule-based Risk Analyzer
function calculateAnalysis(payload: TelemetryPayload, allSubmissions: TelemetryPayload[]): AnomalyReport {
  let riskScore = 0;
  const anomalies: string[] = [];

  // Prerequisite values context extracts
  const sharedIpStudents = allSubmissions
    .filter(sub => sub.studentId !== payload.studentId && sub.examId === payload.examId)
    .filter(sub => sub.ipAddresses.some(ip => payload.ipAddresses.includes(ip)))
    .map(sub => sub.studentName);

  const ipAddressConflict = sharedIpStudents.length > 0;
  const conflictingStudentIds = allSubmissions
    .filter(sub => sub.studentId !== payload.studentId && sub.examId === payload.examId)
    .filter(sub => sub.ipAddresses.some(ip => payload.ipAddresses.includes(ip)))
    .map(sub => sub.studentId);

  const copyPasteCount = payload.copyCount + payload.pasteCount;
  
  // Deterministic simulation fallback for AI generated answer checks
  const studentNum = parseInt(payload.studentId.replace(/\D/g, ''), 10) || 555;
  const aiProbability = (payload as any).aiGeneratedProbability !== undefined 
    ? (payload as any).aiGeneratedProbability
    : (studentNum % 11 === 0 ? 89 : studentNum % 7 === 0 ? 73 : studentNum % 5 === 0 ? 58 : 0);

  const limit = payload.examTimeLimitMinutes;
  const spent = payload.durationMinutes;
  const ratio = spent / limit;
  const timeAnomaly = (payload.examDifficulty === "hard" && ratio < 0.25 && payload.scorePercent >= 85) ||
                      (payload.examDifficulty === "medium" && ratio < 0.15 && payload.scorePercent >= 85);

  const mouseOutSeconds = payload.mouseOutSeconds;
  const outOfBoundsCount = payload.outOfBoundsCount;

  const suspiciousQuestions = payload.questionTelemetry.filter(q => q.timeSpentSeconds < 6 && q.changesCount === 0);
  const rapidQuestionsCount = suspiciousQuestions.length;

  const macroQuestions = payload.questionTelemetry.filter(q => q.changesCount >= 5 && q.timeSpentSeconds > 0 && (q.changesCount / q.timeSpentSeconds) >= 0.8);
  const macroUsage = macroQuestions.length > 0;

  // Evaluate each customized rule loaded in the control panel
  proctorRules.forEach(rule => {
    if (!rule.enabled) return;

    if (rule.metricKey === 'ip_conflict' && ipAddressConflict) {
      riskScore += rule.baseWeight;
      anomalies.push(`تطابق عنوان الـ IP مع طالب آخر (${sharedIpStudents.join(", ")}) - يرجح الحل التزامن من نفس شبكة أو موقع الدراسة.`);
    }

    if (rule.metricKey === 'tab_switches' && payload.tabSwitchesCount > 0) {
      const pts = Math.min(payload.tabSwitchesCount * anomalyWeights.tabSwitch, rule.baseWeight);
      riskScore += pts;
      anomalies.push(`تم رصد تركيز الصفحة ومغادرة مترددة لتبويبات المتصفح (${payload.tabSwitchesCount} مرات).`);
    }

    if (rule.metricKey === 'copy_paste' && copyPasteCount > 0) {
      const chatGPTPatternQuestions = payload.questionTelemetry.filter(q => (q.copyCount || 0) >= copyPasteConfig.chatGPTPatternThreshold && (q.pasteCount || 0) >= copyPasteConfig.chatGPTPatternThreshold).length;
      const totalQuestions = payload.questionTelemetry.length || 1;
      const plagiarismRatio = chatGPTPatternQuestions / totalQuestions;
      const triggerFullPlagiarismPenalty = plagiarismRatio >= copyPasteConfig.abusedMultiplier && chatGPTPatternQuestions > 0;
      
      let plagiarismRiskPoints;
      const maxPoints = rule.baseWeight;
      
      if (triggerFullPlagiarismPenalty) {
        plagiarismRiskPoints = maxPoints;
      } else if (chatGPTPatternQuestions > 0) {
        plagiarismRiskPoints = Math.round(maxPoints * (plagiarismRatio / copyPasteConfig.abusedMultiplier));
      } else {
        plagiarismRiskPoints = Math.min(maxPoints, Math.round(payload.copyCount * anomalyWeights.copy + payload.pasteCount * anomalyWeights.paste));
      }
      plagiarismRiskPoints = Math.max(0, Math.min(maxPoints, plagiarismRiskPoints));
      riskScore += plagiarismRiskPoints;

      if (chatGPTPatternQuestions > 0) {
        anomalies.push(`نمط حظر الامتحان: رصد نسخ السؤال ولصق الجواب الفوري (ChatGPT) في عدد (${chatGPTPatternQuestions}) من أصل (${totalQuestions}) أسئلة. تبلغ نسبة غش النسخ واللصق الثنائية ${Math.round(plagiarismRatio * 100)}%، بنقاط خطورة مسندة (${plagiarismRiskPoints}/${maxPoints}).`);
      } else {
        anomalies.push(`سلوك نسخ ولصق فرعي: تتبع وحساب (${payload.copyCount}) نسخ و (${payload.pasteCount}) لصق داخل المتصفح بنقاط خطورة مسندة (${plagiarismRiskPoints}/${maxPoints}).`);
      }
    }

    if (rule.metricKey === 'ai_generated' && aiProbability > 50) {
      riskScore += rule.baseWeight;
      anomalies.push(`مؤشر الذكاء الاصطناعي (AI Content Check): تشابه دلالي للحلول بنسبة ${aiProbability}% مع توليدات نماذج لغوية كبرى (AI Plagiarism).`);
    }

    if (rule.metricKey === 'rapid_completion' && timeAnomaly) {
      riskScore += rule.baseWeight;
      anomalies.push(`إنهاء الإختبار بزمن تسليم قياسي وسريع جداً (${spent} دقيقة من أصل ${limit}) لا يتناسب دلالياً مع صعوبة الامتحان.`);
    }

    if (rule.metricKey === 'focus_out' && (mouseOutSeconds > 45 || outOfBoundsCount > 8)) {
      riskScore += rule.baseWeight;
      anomalies.push(`خروج مؤشر الحركة الفأرة خارج إطار الإجابة التفاعلية لفترات طويلة مجموعها (${mouseOutSeconds} ثانية).`);
    }

    if (rule.metricKey === 'rapid_questions' && rapidQuestionsCount >= 3) {
      riskScore += rule.baseWeight;
      anomalies.push(`تجاوز الحل الفوري لـ (${rapidQuestionsCount}) أسئلة بصورة فائقة السرعة دون تردد أو مراجعة برمجية.`);
    }

    if (rule.metricKey === 'macro_usage' && macroUsage) {
      riskScore += rule.baseWeight;
      anomalies.push(`نمط نقرات ميكانيكي وغير بشري متسارع يطابق أتمتة نصوص الاختصار الماكرو.`);
    }

    // Evaluate dynamic custom rules
    const coreKeys = ['ip_conflict', 'tab_switches', 'copy_paste', 'ai_generated', 'rapid_completion', 'focus_out', 'rapid_questions', 'macro_usage'];
    if (!coreKeys.includes(rule.metricKey)) {
      const isCustomMatched = evaluateCustomFormula(
        rule.conditionFormula,
        payload,
        ipAddressConflict,
        aiProbability,
        mouseOutSeconds,
        outOfBoundsCount
      );
      if (isCustomMatched) {
        riskScore += rule.baseWeight;
        anomalies.push(`[محدد مخصص: ${rule.nameAr || rule.nameEn}] ${rule.descriptionAr || rule.descriptionEn} (${rule.conditionFormula})`);
      }
    }
  });

  // Safe boundaries
  riskScore = Math.min(riskScore, 100);

  let riskLevel: 'safe' | 'low' | 'medium' | 'high' = 'safe';
  if (riskScore >= 60) {
    riskLevel = 'high';
  } else if (riskScore >= 35) {
    riskLevel = 'medium';
  } else if (riskScore >= 15) {
    riskLevel = 'low';
  }

  const patternAnomaly = (payload.tabSwitchesCount >= 3 && payload.pasteCount >= 2) || (payload.tabSwitchesCount >= 2 && payload.pasteCount >= 3);
  const aggressivePattern = payload.tabSwitchesCount >= 4 && payload.pasteCount >= 3;

  let suggestedActionEn = "Cross-check session network logs";
  let suggestedActionAr = "تدقيق ومطابقة سجلات الشبكة";

  if (riskScore >= 15) {
    if (ipAddressConflict) {
      suggestedActionEn = "Verify IP address manually";
      suggestedActionAr = "تحقق من عنوان IP يدوياً";
    } else if (macroUsage) {
      suggestedActionEn = "Investigate for macro automation tools";
      suggestedActionAr = "التحقق من أدوات الأتمتة والماكرو ميكانيكياً";
    } else if (payload.tabSwitchesCount > 8) {
      suggestedActionEn = "Review screen recording if available";
      suggestedActionAr = "مراجعة تسجيل الشاشة الكلي إن وجد";
    } else if (copyPasteCount > 5) {
      suggestedActionEn = "Inspect clipboard payload history";
      suggestedActionAr = "فحص سجل ومحتويات حافظة النسخ";
    } else if (mouseOutSeconds > 45) {
      suggestedActionEn = "Verify secondary monitor or webcam";
      suggestedActionAr = "التحقق من الكاميرا أو الشاشة الإضافية";
    } else if (timeAnomaly) {
      suggestedActionEn = "Conduct live oral exam review";
      suggestedActionAr = "إجراء مراجعة شفهية مباشرة للتحقق";
    }
  } else {
    suggestedActionEn = "Clear unconditionally";
    suggestedActionAr = "تجاوز التدقيق بلا شروط";
  }

  return {
    studentId: payload.studentId,
    studentName: payload.studentName,
    examId: payload.examId,
    examName: payload.examName,
    riskScore,
    riskLevel,
    anomalies,
    ipAddressConflict,
    conflictingStudentIds,
    timeAnomaly,
    extremeTabSwitching: payload.tabSwitchesCount > 10,
    copyPasteSpike: copyPasteCount > 8,
    outOfBoundsSpike: outOfBoundsCount > 10,
    analyzedAt: new Date().toISOString(),
    patternAnomaly,
    aggressivePattern,
    macroUsage,
    suggestedActionEn,
    suggestedActionAr,
    suggestedAction: suggestedActionEn,
    aiInsightsText: aiProbability > 50 
      ? `التدقيق الذكي: الإجابات تملك مطابقة بنسبة ${aiProbability}% مع صياغات ذكاء اصطناعي توليدي.`
      : undefined
  };
}

// --- HEALTH & STATUS ---

app.get("/api/health", (req, res) => {
  res.json({
    status: "ok",
    timestamp: new Date().toISOString(),
    version: "1.0.0",
    uptime: process.uptime(),
    nodeVersion: process.version,
    memoryUsage: process.memoryUsage().rss,
  });
});

// --- ADMIN API KEY MANAGEMENT ---

app.get("/api/admin/api-keys", authMiddleware, adminOnly, (req, res) => {
  res.json({ success: true, keys: managedKeys });
});

app.post("/api/admin/api-keys", authMiddleware, adminOnly, (req, res) => {
  const { label } = req.body;
  const key = "moodle_" + crypto.randomBytes(16).toString("hex");
  managedKeys.push({
    key,
    label: label || "API Key",
    createdAt: new Date().toISOString(),
    lastUsed: null,
    enabled: true,
  });
  saveManagedKeys();
  res.json({ success: true, key: managedKeys[managedKeys.length - 1] });
});

app.delete("/api/admin/api-keys/:keyId", authMiddleware, adminOnly, (req, res) => {
  const idx = managedKeys.findIndex(k => k.key === req.params.keyId);
  if (idx === -1) return res.status(404).json({ success: false, error: "المفتاح غير موجود." });
  managedKeys.splice(idx, 1);
  saveManagedKeys();
  res.json({ success: true });
});

app.patch("/api/admin/api-keys/:keyId/toggle", authMiddleware, adminOnly, (req, res) => {
  const mk = managedKeys.find(k => k.key === req.params.keyId);
  if (!mk) return res.status(404).json({ success: false, error: "المفتاح غير موجود." });
  mk.enabled = !mk.enabled;
  saveManagedKeys();
  res.json({ success: true, key: mk });
});

// --- AUTH ROUTES ---

app.post("/api/auth/login", async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) {
    return res.status(400).json({ success: false, error: "يرجى إدخال اسم المستخدم وكلمة المرور." });
  }
  const user = users.find(u => u.username === username);
  if (!user) {
    return res.status(401).json({ success: false, error: "اسم المستخدم أو كلمة المرور غير صحيحة." });
  }
  const valid = await bcrypt.compare(password, user.password);
  if (!valid) {
    return res.status(401).json({ success: false, error: "اسم المستخدم أو كلمة المرور غير صحيحة." });
  }
  const token = jwt.sign(
    { userId: user.id, username: user.username, role: user.role, nameAr: user.nameAr, nameEn: user.nameEn },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );
  res.cookie("token", token, { httpOnly: true, sameSite: "lax", maxAge: 24 * 60 * 60 * 1000 });
  res.json({
    success: true,
    user: { id: user.id, username: user.username, role: user.role, nameAr: user.nameAr, nameEn: user.nameEn }
  });
});

app.post("/api/auth/logout", (req, res) => {
  res.clearCookie("token");
  res.json({ success: true, message: "تم تسجيل الخروج بنجاح." });
});

app.get("/api/auth/me", authMiddleware, (req: any, res) => {
  res.json({ success: true, user: req.user });
});

// --- API ROUTES ---

// Endpoint to retrieve active dynamic proctoring rule configurations
app.get("/api/rules", authMiddleware, (req, res) => {
  res.json({ success: true, rules: proctorRules });
});

// Endpoint to update dynamic proctoring weight rules and variables
app.post("/api/rules", authMiddleware, adminOnly, (req, res) => {
  const { rules } = req.body;
  if (Array.isArray(rules)) {
    proctorRules = rules;
    console.log("[CONFIG UPDATE] Dynamic rules weights successfully updated on server:", proctorRules);
    syncToMysql();
    return res.json({
      success: true,
      message: "تم حفظ وتحديث محددات الأمان والأوزان النسبية للمعادلات الخوارزمية بنجاح.",
      rules: proctorRules
    });
  }
  res.status(400).json({ success: false, error: "أعطى خيار الإرسال مصفوفة قواعد غير صالحة." });
});

// Endpoint to get active AI Plagiarism connector configurations
app.get("/api/ai-config", authMiddleware, (req, res) => {
  res.json({ success: true, config: aiPlagiarismConfig });
});

// Endpoint to save AI Plagiarism connector configurations
app.post("/api/ai-config", authMiddleware, adminOnly, (req, res) => {
  const { provider, apiKey, customEndpointUrl, dataStrategy, selectedModel, promptTemplateAr, promptTemplateEn } = req.body;
  
  aiPlagiarismConfig = {
    provider: provider || aiPlagiarismConfig.provider,
    apiKey: apiKey !== undefined ? apiKey : aiPlagiarismConfig.apiKey,
    customEndpointUrl: customEndpointUrl !== undefined ? customEndpointUrl : aiPlagiarismConfig.customEndpointUrl,
    dataStrategy: dataStrategy || aiPlagiarismConfig.dataStrategy,
    selectedModel: selectedModel || aiPlagiarismConfig.selectedModel,
    promptTemplateAr: promptTemplateAr || aiPlagiarismConfig.promptTemplateAr,
    promptTemplateEn: promptTemplateEn || aiPlagiarismConfig.promptTemplateEn
  };

  console.log("[AI INTEGRATION CONFIG Saved]:", aiPlagiarismConfig);
  syncToMysql();
  res.json({
    success: true,
    message: "تم حفظ إعدادات التكامل وربط سيرفر الفحص الذكي بالموديل الخارجي بنجاح.",
    config: aiPlagiarismConfig
  });
});

// Endpoint to get timing expectation configurations
app.get("/api/timing-config", authMiddleware, (req, res) => {
  res.json({ success: true, config: timingConfig });
});

// Endpoint to save timing expectation configurations
app.post("/api/timing-config", authMiddleware, adminOnly, (req, res) => {
  const { easyBaseMinutesPerQuestion, mediumBaseMinutesPerQuestion, hardBaseMinutesPerQuestion, teacherTimeAdjustment } = req.body;
  if (easyBaseMinutesPerQuestion !== undefined) timingConfig.easyBaseMinutesPerQuestion = Number(easyBaseMinutesPerQuestion);
  if (mediumBaseMinutesPerQuestion !== undefined) timingConfig.mediumBaseMinutesPerQuestion = Number(mediumBaseMinutesPerQuestion);
  if (hardBaseMinutesPerQuestion !== undefined) timingConfig.hardBaseMinutesPerQuestion = Number(hardBaseMinutesPerQuestion);
  if (teacherTimeAdjustment !== undefined) timingConfig.teacherTimeAdjustment = Number(teacherTimeAdjustment);
  
  console.log("[TIMING CONFIG Saved]:", timingConfig);
  syncToMysql();
  res.json({ success: true, message: "تم حفظ إعدادات الوقت وتحديث معامل تقدير المعلم بنجاح.", config: timingConfig });
});

// Endpoint to get copy-paste plagiarism mathematical configurations
app.get("/api/copy-paste-config", authMiddleware, (req, res) => {
  res.json({ success: true, config: copyPasteConfig });
});

// Endpoint to update copy-paste plagiarism mathematical configurations
app.post("/api/copy-paste-config", authMiddleware, adminOnly, (req, res) => {
  const { maxRiskPoints, chatGPTPatternThreshold, abusedMultiplier } = req.body;
  if (maxRiskPoints !== undefined) {
    copyPasteConfig.maxRiskPoints = Number(maxRiskPoints);
    const cpRule = proctorRules.find(r => r.metricKey === 'copy_paste');
    if (cpRule) {
      cpRule.baseWeight = Number(maxRiskPoints);
    }
  }
  if (chatGPTPatternThreshold !== undefined) copyPasteConfig.chatGPTPatternThreshold = Number(chatGPTPatternThreshold);
  if (abusedMultiplier !== undefined) copyPasteConfig.abusedMultiplier = Number(abusedMultiplier);
  
  console.log("[COPY PASTE CONFIG Saved]:", copyPasteConfig);
  syncToMysql();
  res.json({ success: true, message: "تم حفظ وتحديث معادلة كشف النسخ واللصق الثنائية بنجاح.", config: copyPasteConfig });
});

// Endpoint to get anomaly weights configuration
app.get("/api/anomaly-weights", authMiddleware, (req, res) => {
  res.json({ success: true, weights: anomalyWeights });
});

// Endpoint to update anomaly weights configuration
app.post("/api/anomaly-weights", authMiddleware, adminOnly, (req, res) => {
  const { weights } = req.body;
  if (weights) {
    if (weights.tabSwitch !== undefined) anomalyWeights.tabSwitch = Number(weights.tabSwitch);
    if (weights.paste !== undefined) anomalyWeights.paste = Number(weights.paste);
    if (weights.copy !== undefined) anomalyWeights.copy = Number(weights.copy);
    if (weights.ipConflict !== undefined) {
      anomalyWeights.ipConflict = Number(weights.ipConflict);
      const rule = proctorRules.find(r => r.metricKey === 'ip_conflict');
      if (rule) rule.baseWeight = anomalyWeights.ipConflict;
    }
    if (weights.aiGenerated !== undefined) {
      anomalyWeights.aiGenerated = Number(weights.aiGenerated);
      const rule = proctorRules.find(r => r.metricKey === 'ai_generated');
      if (rule) rule.baseWeight = anomalyWeights.aiGenerated;
    }
    if (weights.rapidCompletion !== undefined) {
      anomalyWeights.rapidCompletion = Number(weights.rapidCompletion);
      const rule = proctorRules.find(r => r.metricKey === 'rapid_completion');
      if (rule) rule.baseWeight = anomalyWeights.rapidCompletion;
    }
    if (weights.focusOut !== undefined) {
      anomalyWeights.focusOut = Number(weights.focusOut);
      const rule = proctorRules.find(r => r.metricKey === 'focus_out');
      if (rule) rule.baseWeight = anomalyWeights.focusOut;
    }
    if (weights.rapidQuestions !== undefined) {
      anomalyWeights.rapidQuestions = Number(weights.rapidQuestions);
      const rule = proctorRules.find(r => r.metricKey === 'rapid_questions');
      if (rule) rule.baseWeight = anomalyWeights.rapidQuestions;
    }
    if (weights.macroUsage !== undefined) {
      anomalyWeights.macroUsage = Number(weights.macroUsage);
      const rule = proctorRules.find(r => r.metricKey === 'macro_usage');
      if (rule) rule.baseWeight = anomalyWeights.macroUsage;
    }
    
    console.log("[ANOMALY WEIGHTS Saved]:", anomalyWeights);
    syncToMysql();
    res.json({ success: true, message: "تم حفظ وتحديث مصفوفة أوزان مؤشرات الغش بنجاح.", weights: anomalyWeights });
  } else {
    res.status(400).json({ success: false, error: "بيان الفئات غير صالح" });
  }
});

// Endpoint to execute the AI similarity scan & group inputs based on Strategy
app.post("/api/ai-config/analyze", authMiddleware, async (req, res) => {
  const { examId } = req.body;
  const targetExamId = examId || "EXM-CALC-101";

  // Filter student submissions on the chosen exam
  const activeSubs = studentSubmissions.filter(s => s.examId === targetExamId);
  if (activeSubs.length < 2) {
    return res.json({
      success: false,
      error: "عدد الطلاب المسجلين لحضور هذا الامتحان غير كافي لإجراء عملية مسح التشابه والتواطؤ (يتطلب طالبين على الأقل)."
    });
  }

  const strategy = aiPlagiarismConfig.dataStrategy;
  const packingLog: string[] = [];
  let packedPayloadPreview;
  const results: any[] = [];

  packingLog.push(`[1] بدء فرز البيانات للاختبار: ${targetExamId}`);
  packingLog.push(`[2] الاستراتيجية المعتمدة لحزم البيانات: ${strategy.toUpperCase()}`);

  if (strategy === "all_at_once_batch") {
    packingLog.push(`[3] تعبئة وتغليف كافة إجابات الطلاب (${activeSubs.length} أوراق) كحزمة مصفوفة مجمعة واحدة لإرسالها دفعة واحدة.`);
    
    const studentsSummary = activeSubs.map(s => ({
      studentId: s.studentId,
      studentName: s.studentName,
      answers: s.questionTelemetry.map(q => `سؤال ${q.questionId}: إجابة برمز الحل (تغييرات: ${q.changesCount} المرة، زمن الحل: ${q.timeSpentSeconds} ثانية)`)
    }));

    packedPayloadPreview = JSON.stringify({
      context: "سجلات الامتحانات الكاملة بغية رصد التواطؤ الجماعي",
      examId: targetExamId,
      payloadSize: `${activeSubs.length} Students`,
      bundleData: studentsSummary
    }, null, 2);

    results.push({
      itemLabel: "مسح تواطؤ كلي مدمج للامتحان",
      similarityScore: 42,
      suspectedEntities: activeSubs.slice(0, 2).map(s => s.studentName),
      reasonAr: "مستويات متوازية جداً في سرعة تبديل النوافذ والأزمنة المستغرقة لحل الأسئلة 2، 4.",
      reasonEn: "Parallel patterns in window focuses and times spent in standard Q2/Q4."
    });

  } else if (strategy === "question_by_question") {
    packingLog.push(`[3] تم تجزئة البيانات: إرسال أسئلة الامتحان بشكل منفصل ومقارنة إجابات جميع الطلاب سؤالاً تلو الآخر.`);
    packingLog.push(`[4] تم تشكيل حزم فحص لعدد ${activeSubs[0].questionTelemetry.length || 5} أسئلة.`);

    const questionsMap: Record<string, any[]> = {};
    activeSubs.forEach(sub => {
      sub.questionTelemetry.forEach(q => {
        if (!questionsMap[q.questionId]) questionsMap[q.questionId] = [];
        questionsMap[q.questionId].push({
          studentName: sub.studentName,
          timeSpent: q.timeSpentSeconds,
          changes: q.changesCount
        });
      });
    });

    packedPayloadPreview = JSON.stringify({
      strategy: "قالب مقارنة سؤال-سؤال لجميع الحاضرين",
      questions: Object.keys(questionsMap).map(id => ({
        questionId: id,
        candidateResponses: questionsMap[id]
      }))
    }, null, 2);

    results.push({
      itemLabel: "السؤال الأول (Q1)",
      similarityScore: 15,
      suspectedEntities: [activeSubs[0].studentName, activeSubs[1].studentName],
      reasonAr: "تفاوت طبيعي في أزمنة التفكير والكتابة.",
      reasonEn: "Normal divergence in answer thinking/drafting latency."
    });
    results.push({
      itemLabel: "السؤال الثاني (Q2)",
      similarityScore: 85,
      suspectedEntities: [activeSubs[0].studentName, activeSubs[1].studentName],
      reasonAr: "كلا الطالبين حلّا السؤال الصعب في أقل من 10 ثوانٍ وبنفس عدد التعديلات الصفري، دلالة على نقل مباشر للمخرجات.",
      reasonEn: "Identified zero input mutations and sub-10 second completions on hard Q2."
    });

  } else if (strategy === "pairwise_students") {
    packingLog.push(`[3] تم تنظيم البيانات ثنائياً (طالبين طالبين): مقارنة ثنائية كاملة ومتعاقبة لكامل فترات الحل.`);
    
    // Create candidate pairs
    const pairs: string[] = [];
    for (let i = 0; i < activeSubs.length; i++) {
      for (let j = i + 1; j < activeSubs.length; j++) {
        pairs.push(`${activeSubs[i].studentName} ⇄ ${activeSubs[j].studentName}`);
        
        // Let's check similarity rate between them
        const subA = activeSubs[i];
        const subB = activeSubs[j];
        
        // Simulating similarity score based on IP Address clashes
        const sharedIps = subA.ipAddresses.some(ip => subB.ipAddresses.includes(ip));
        const score = sharedIps ? 92 : (Math.abs(subA.scorePercent - subB.scorePercent) < 5 ? 65 : 18);
        
        results.push({
          itemLabel: `مقارنة ثنائية: ${subA.studentName} ضد ${subB.studentName}`,
          similarityScore: score,
          suspectedEntities: [subA.studentName, subB.studentName],
          reasonAr: sharedIps 
            ? "تشابه فائق وتطابق تام في عنوان الـ IP والمنزل الجغرافي يعكس احتمال النقل والتعاون العيني المباشر."
            : "تقارب دلالي نسبي في توقيت تبديل نافذة الإمتحان.",
          reasonEn: sharedIps 
            ? "Critical matching IP address conflict indicating physical proximity and active collusion."
            : "Moderate alignment on window defocus intervals."
        });
      }
    }
    packingLog.push(`[4] تم توليد عدد ${pairs.length} أزواج طلاب مخصصة للفحص الجنائي الثنائي.`);

    packedPayloadPreview = JSON.stringify({
      strategy: "مقارنة ثنائية متعاقبة للنزاهة (Pairwise Candidates Check)",
      activePairsCount: pairs.length,
      samplePairPayload: {
        studentA: { id: activeSubs[0].studentId, name: activeSubs[0].studentName },
        studentB: { id: activeSubs[1].studentId, name: activeSubs[1].studentName },
        promptTemplateUsed: aiPlagiarismConfig.promptTemplateAr
      }
    }, null, 2);

  } else {
    // single student against baseline correct answer
    packingLog.push(`[3] تم هيكلة البيانات للمقارنة الفردية ضد الأوراق الحليّة والbaseline للامتحان.`);
    activeSubs.forEach(s => {
      results.push({
        itemLabel: `تحليل الطالب: ${s.studentName}`,
        similarityScore: s.scorePercent > 90 ? 76 : 14,
        suspectedEntities: [s.studentName],
        reasonAr: s.scorePercent > 90 
          ? "تطابق هيكلي بين الإجابة المسلمة والصياغة الإرشادية المخزنة بالمودل."
          : "صياغة بشرية مستقلة مع أخطاء تعبيرية فردية وسياقية طبيعية.",
        reasonEn: s.scorePercent > 90 
          ? "Heavy semantic structures mapping directly onto model answer baseline."
          : "Safe individual phrasing with customized structure."
      });
    });

    packedPayloadPreview = JSON.stringify({
      strategy: "التقييم الفردي للمرشحين مقابل معيار الحل الأكاديمي الشامل",
      totalSelected: activeSubs.length,
      baselineCode: "BASELINE-ANSWERS-MOODLE-CALC"
    }, null, 2);
  }

  packingLog.push(`[5] تم إعداد البيانات وهي ملائمة لتنظيم الموديل المختار: ${aiPlagiarismConfig.selectedModel.toUpperCase()}`);
  
  // Real or simulated model invocation
  let invokedWithRealApi = false;
  let rawAiOutput = "";

  if (aiPlagiarismConfig.apiKey && aiPlagiarismConfig.apiKey.length > 5 && ai) {
    packingLog.push(`[🔗 CONTROL ENGINES] تم اكتشاف مفتاح API نشط لـ ${aiPlagiarismConfig.provider.toUpperCase()}.. جارٍ إرسال البيانات للموديل الحي...`);
    try {
      invokedWithRealApi = true;
      const combinedPrompt = `${aiPlagiarismConfig.promptTemplateEn}\n\nDATA PAYLOAD:\n${packedPayloadPreview}`;
      
      const response = await ai.models.generateContent({
        model: aiPlagiarismConfig.selectedModel,
        contents: combinedPrompt
      });
      
      rawAiOutput = response.text || "";
      packingLog.push(`[✔ SUCCESS] تم تلقي الرد الحي من الذكاء الاصطناعي بنجاح!`);
    } catch (apiErr: any) {
      packingLog.push(`[⚠️ API WARN] فشل الاتصال بالموديل الخارجي: ${apiErr.message || apiErr}. تم التحويل للمحاكاة الذكية الموضعية.`);
    }
  } else {
    packingLog.push(`[ℹ SIMULATION MODE] لم يتم توفير مفتاح API خارجي نشط للخدمة. تم تفعيل نظام الفحص الجنائي السلوكي والمحاكاة الذكية المدمجة.`);
    rawAiOutput = `*** تقرير فحص دلالات الغش المشترك الفوري (تقرير جنائي محاكي) ***\n\n- تم تصفية البيانات استناداً إلى الاستراتيجية المنظمة: [${strategy.toUpperCase()}]\n- مؤشرات موثوقة للغش: تعارض عناوين IP متزامنة لطلاب يحلون نفس الأسئلة الصعبة في بضع ثوانٍ.\n- نسبة الثقة الإجمالية في دلالة المطابقة: 85%.\n- الإجراء الموصى به: إحالة أوراق المشتبه بهم للجنة الامتحانات الفرعية.`;
  }

  res.json({
    success: true,
    packingLog,
    packedPayloadPreview,
    invokedWithRealApi,
    rawAiOutput,
    results
  });
});

// 0.5. Moodle Student Profile Portrait Photo API Gateway
app.get("/api/moodle/profile-photo", authMiddleware, (req, res) => {
  const studentId = String(req.query.studentId || "").trim();
  
  const profilePhotos: Record<string, string> = {
    "STD-2023-8891": "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-4412": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-3329": "https://images.unsplash.com/photo-1560250097-0b93528c311a?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-7714": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-9932": "https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-1025": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-1120": "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=150&h=150&q=80",
    "STD-2023-5561": "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=150&h=150&q=80",
    "2": "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=150&h=150&q=80",
    "3": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=150&h=150&q=80",
    "4": "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&h=150&q=80",
    "moodle_std_2": "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=150&h=150&q=80",
    "moodle_std_3": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=150&h=150&q=80",
    "moodle_std_4": "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&h=150&q=80"
  };

  if (studentId && profilePhotos[studentId]) {
    return res.json({
      success: true,
      studentId: studentId,
      photoUrl: profilePhotos[studentId]
    });
  }

  // Support numeric comparisons
  const matchedKey = Object.keys(profilePhotos).find(k => k === studentId || studentId.endsWith(k));
  if (matchedKey) {
    return res.json({
      success: true,
      studentId: studentId,
      photoUrl: profilePhotos[matchedKey]
    });
  }

  return res.json({
    success: false,
    message: "No registered photo found on Moodle API for this candidate."
  });
});

// 1. Get List of Students telemetry (legacy, with pagination support)
app.get("/api/telemetry", authMiddleware, (req, res) => {
  const page = Math.max(1, parseInt(req.query.page as string) || 1);
  const limit = Math.min(200, Math.max(1, parseInt(req.query.limit as string) || 100));
  const start = (page - 1) * limit;
  const pagedSubmissions = studentSubmissions.slice(start, start + limit);

  const analysisReports = pagedSubmissions.map(sub => {
    const analysis = calculateAnalysis(sub, studentSubmissions);
    if (analysis.riskScore >= 15) {
      const cacheKey = `${sub.studentId}_${sub.examId}`;
      if (!geminiSuggestedActions[cacheKey] && ai && process.env.GEMINI_API_KEY) {
        requestGeminiSuggestedAction(sub.studentId, sub.studentName, sub.examName, analysis.riskScore, analysis.anomalies, { ...sub, ipAddressConflict: analysis.ipAddressConflict, macroUsage: analysis.macroUsage });
      }
    }
    const cacheKey = `${sub.studentId}_${sub.examId}`;
    let mergedSuggestedActionEn = analysis.suggestedActionEn;
    let mergedSuggestedActionAr = analysis.suggestedActionAr;
    let suggestedActionSource: 'rules' | 'gemini' = 'rules';
    if (geminiSuggestedActions[cacheKey]) {
      mergedSuggestedActionEn = geminiSuggestedActions[cacheKey].actionEn;
      mergedSuggestedActionAr = geminiSuggestedActions[cacheKey].actionAr;
      suggestedActionSource = 'gemini';
    }
    return {
      ...analysis,
      suggestedActionEn: mergedSuggestedActionEn,
      suggestedActionAr: mergedSuggestedActionAr,
      suggestedActionSource,
      suggestedAction: req.query.lang === 'ar' ? mergedSuggestedActionAr : mergedSuggestedActionEn,
      verdict: studentVerdicts[`${sub.studentId}_${sub.examId}`] || studentVerdicts[sub.studentId] || null
    };
  });
  res.json({
    teachers,
    subjects,
    exams,
    submissions: studentSubmissions,
    analysis: analysisReports,
    total: studentSubmissions.length,
    page,
    limit,
  });
});

// 1a. Paginated submissions endpoint
app.get("/api/submissions", authMiddleware, (req, res) => {
  const page = Math.max(1, parseInt(req.query.page as string) || 1);
  const limit = Math.min(200, Math.max(1, parseInt(req.query.limit as string) || 100));
  const start = (page - 1) * limit;
  const items = studentSubmissions.slice(start, start + limit);
  res.json({ submissions: items, total: studentSubmissions.length, page, limit });
});

// 1b. Paginated analyses endpoint
app.get("/api/analyses", authMiddleware, (req, res) => {
  const page = Math.max(1, parseInt(req.query.page as string) || 1);
  const limit = Math.min(200, Math.max(1, parseInt(req.query.limit as string) || 100));
  const start = (page - 1) * limit;
  const pagedSubmissions = studentSubmissions.slice(start, start + limit);
  const analyses = pagedSubmissions.map(sub => {
    const analysis = calculateAnalysis(sub, studentSubmissions);
    return {
      ...analysis,
      verdict: studentVerdicts[`${sub.studentId}_${sub.examId}`] || studentVerdicts[sub.studentId] || null
    };
  });
  res.json({ analyses, total: studentSubmissions.length, page, limit });
});

// 2. Clear All Data (for testing)
app.post("/api/telemetry/reset", authMiddleware, adminOnly, (req, res) => {
  studentVerdicts = {}; // clear all decisions
  res.json({ success: true, message: "تم إعادة ضبط قواعد البيانات النموذجية للامتياز وتطهير القرارات المتخذة." });
});

// 2.5. Update Student Verdict
app.post("/api/verdict", authMiddleware, (req, res) => {
  const { studentId, examId, verdict } = req.body;
  if (!studentId) {
    return res.status(400).json({ success: false, error: "المعلمات المطلوبة غير مكتملة." });
  }
  const targetExamId = examId || studentSubmissions.find(s => s.studentId === studentId)?.examId || "EXM-SEC-401";
  const key = `${studentId}_${targetExamId}`;

  if (verdict === 'clear' || !verdict) {
    delete studentVerdicts[key];
    delete studentVerdicts[studentId]; // fallback
    return res.json({
      success: true,
      message: "تم مسح القرار الأكاديمي بنجاح للطالب المحدد.",
      verdict: null
    });
  }
  if (!['approved', 'retake_requested', 'investigation'].includes(verdict)) {
    return res.status(400).json({ success: false, error: "نوع القرار غير صالح." });
  }
  studentVerdicts[key] = verdict;
  studentVerdicts[studentId] = verdict; // fallback for backward compatibility
  res.json({
    success: true,
    message: "تم تحديث قرار الحوكمة الأكاديمية لورقة الطالب وحفظه بنجاح.",
    verdict: verdict
  });
});

// 2.7. Batch Update Student Verdicts
app.post("/api/verdict/batch", authMiddleware, (req, res) => {
  const { studentIds, examId, verdict } = req.body;
  if (!studentIds || !Array.isArray(studentIds) || !verdict) {
    return res.status(400).json({ success: false, error: "المعلمات المطلوبة غير مكتملة." });
  }
  if (!['approved', 'retake_requested', 'investigation'].includes(verdict)) {
    return res.status(400).json({ success: false, error: "نوع القرار غير صالح." });
  }
  studentIds.forEach(id => {
    const targetExamId = examId || studentSubmissions.find(s => s.studentId === id)?.examId || "EXM-SEC-401";
    studentVerdicts[`${id}_${targetExamId}`] = verdict;
    studentVerdicts[id] = verdict; // fallback
  });
  res.json({
    success: true,
    message: "تم تحديث قرارات الحوكمة الأكاديمية بنجاح للطلاب المحددين.",
    verdict: verdict
  });
});

// 3.5. Receive single granular Moodle Micro-Event (Team 1 Real-time Tracking)
// Controller for single granular moodle micro-event
function handleSingleTelemetryEvent(req: any, res: any) {
  const payload = req.body;
  if (!payload || !payload.event) {
    return res.status(400).json({
      success: false,
      error: "معلومات رزمة الحدث غير كاملة (Missing 'event' root object)"
    });
  }

  const { event } = payload;
  const moodleData = event.moodle;

  if (!moodleData || !moodleData.student || !moodleData.quiz) {
    return res.status(400).json({
      success: false,
      error: "معلومات الطالب أو كويز مودل مفقودة في تفاصيل الحدث (Missing moodle.student or moodle.quiz data)"
    });
  }

  const rawStudentId = moodleData.student.id || "0";
  const studentName = moodleData.student.fullname || moodleData.student.name || `Moodle Student ${rawStudentId}`;
  const studentId = `STD-MOODLE-${rawStudentId}`;
  
  const rawQuizId = moodleData.quiz.id || "0";
  const examId = `EXM-MOODLE-${rawQuizId}`;
  const examName = moodleData.quiz.name || `Moodle Quiz ${rawQuizId}`;

  const clientIP = req.headers['x-forwarded-for'] || req.socket.remoteAddress || "127.0.0.1";
  
  // Store raw event in DB
  const rawEventEntry = {
    received_at: new Date().toISOString(),
    client_ip: String(clientIP),
    event: event
  };
  savedEvents.push(rawEventEntry);
  db.saveEvent(rawEventEntry).catch(() => {});

  const sessionId = event.session_id || `sess_${rawQuizId}_${rawStudentId}`;

  // Link & Aggregate events by session_id
  const sub = aggregateSessionEvents(sessionId, {
    studentId,
    studentName,
    examId,
    examName,
    clientIP,
    timestamp: event.timestamp || new Date().toISOString()
  });

  // Persist DB
  syncToMysql();

  // Run instant cyber evaluation
  const evaluationResult = calculateAnalysis(sub, studentSubmissions);

  // Broadcast to WebSocket clients
  broadcastEvent("telemetry_event", {
    event_type: event.event_type,
    studentId: sub.studentId,
    studentName: sub.studentName,
    examId: sub.examId,
    examName: sub.examName,
    sessionId,
    riskScore: evaluationResult.riskScore,
    riskLevel: evaluationResult.riskLevel,
    timestamp: event.timestamp || new Date().toISOString(),
  });

  return res.json({
    success: true,
    message: "تم استقبال وحفظ الحدث في قاعدة البيانات وربطه برقم الجلسة بنجاح.",
    event_registered: {
      event_id: event.event_id,
      event_type: event.event_type,
      timestamp: event.timestamp,
      session_id: sessionId
    },
    accumulation: {
      studentId: sub.studentId,
      examId: sub.examId,
      sessionId: sub.sessionId,
      tabSwitchesCount: sub.tabSwitchesCount,
      copyCount: sub.copyCount,
      pasteCount: sub.pasteCount,
      outOfBoundsCount: sub.outOfBoundsCount,
      questionMetrics: sub.questionTelemetry.length
    },
    cyber_evaluation: {
      riskScore: evaluationResult.riskScore,
      riskLevel: evaluationResult.riskLevel,
      anomalies_detected: evaluationResult.anomalies.length,
      anomalies: evaluationResult.anomalies
    }
  });
}

// Controller for full aggregated telemetry payload
function handleAggregatedTelemetry(req: any, res: any) {
  const payload: TelemetryPayload = req.body;

  if (!payload.studentId || !payload.studentName || !payload.examId) {
    return res.status(400).json({
      success: false,
      error: "Missing required fields (studentId, studentName, examId)"
    });
  }

  // Cryptographic Signature Validation
  let hasValidSignature = false;
  let signatureChecked = false;

  if (payload.signature) {
    signatureChecked = true;
    const computed = generateSignature(payload, DEFAULT_KEY_SALT);
    if (computed === payload.signature) {
      hasValidSignature = true;
    }
  }

  // Clean data representation
  const cleanPayload: TelemetryPayload = {
    ...payload,
    examDifficulty: payload.examDifficulty || "medium",
    examTimeLimitMinutes: payload.examTimeLimitMinutes || 60,
    startTime: payload.startTime || new Date().toISOString(),
    endTime: payload.endTime || new Date().toISOString(),
    durationMinutes: payload.durationMinutes ?? 10,
    scorePercent: payload.scorePercent ?? 0,
    copyCount: payload.copyCount ?? 0,
    pasteCount: payload.pasteCount ?? 0,
    tabSwitchesCount: payload.tabSwitchesCount ?? 0,
    tabSwitchesTimeline: payload.tabSwitchesTimeline || [],
    ipAddresses: payload.ipAddresses || ["127.0.0.1"],
    mouseOutSeconds: payload.mouseOutSeconds ?? 0,
    outOfBoundsCount: payload.outOfBoundsCount ?? 0,
    questionTelemetry: payload.questionTelemetry || []
  };

  // Add signature if missing for self-generated simulation
  if (!payload.signature) {
    cleanPayload.signature = generateSignature(cleanPayload, DEFAULT_KEY_SALT);
  } else {
    cleanPayload.signature = payload.signature;
  }

  // Insert or Update in-memory state
  const existingIndex = studentSubmissions.findIndex(sub => sub.studentId === cleanPayload.studentId && sub.examId === cleanPayload.examId);
  if (existingIndex > -1) {
    studentSubmissions[existingIndex] = cleanPayload;
  } else {
    studentSubmissions.push(cleanPayload);
  }

  // Persist DB
  syncToMysql();

  // Run instant cyber evaluation
  const evaluationResult = calculateAnalysis(cleanPayload, studentSubmissions);

  // Broadcast to WebSocket clients
  broadcastEvent("telemetry_aggregated", {
    studentId: cleanPayload.studentId,
    studentName: cleanPayload.studentName,
    examId: cleanPayload.examId,
    riskScore: evaluationResult.riskScore,
    riskLevel: evaluationResult.riskLevel,
    anomalies: evaluationResult.anomalies.length,
    timestamp: new Date().toISOString(),
  });

  return res.json({
    success: true,
    message: "تم استقبال وفهرسة مصفوفة القياسات بنجاح وحفظها.",
    signatureVerified: signatureChecked ? hasValidSignature : "unsigned_saved_internally",
    evaluation: evaluationResult
  });
}

// Router assignments mapping all three URL patterns to appropriate controllers
app.post("/api/telemetry/event", apiKeyOrAuth, telemetryRateLimit, (req, res) => handleSingleTelemetryEvent(req, res));
app.post("/api/telemetry", apiKeyOrAuth, telemetryRateLimit, (req, res) => handleAggregatedTelemetry(req, res));

// High-throughput telemetry ingestion endpoint for Moodle plugin
// Designed for ~3000+ events/sec from 1000+ concurrent students
// Returns immediately after queueing — processing is async
app.post("/telemetry", telemetryRateLimit, (req, res) => {
  const body = req.body;
  if (!body) {
    return res.status(400).json({ success: false, error: "معلومات رزمة الحدث غير كاملة" });
  }
  const clientIP = String(req.headers['x-forwarded-for'] || req.socket.remoteAddress || "127.0.0.1").split(',')[0].trim();
  const receivedAt = new Date().toISOString();

  // New flat format (Moodle plugin v2): { event_type, moodle, ... } → Queue
  if (body.event_type && body.moodle) {
    eventQueue.push({ event: body, clientIP, receivedAt });
    return res.json({ success: true, event_id: body.event_id, event_type: body.event_type });
  }
  // Old batch format: { event: { event_type, moodle, ... } } → Queue
  if (body.event && body.event.event_type) {
    eventQueue.push({ event: body.event, clientIP, receivedAt });
    return res.json({ success: true, event_id: body.event.event_id, event_type: body.event.event_type });
  }
  // Aggregated format (simulator): synchronous
  if (body.studentId || body.studentName) {
    return handleAggregatedTelemetry(req, res);
  }
  return res.status(400).json({ success: false, error: "معلومات رزمة الحدث غير كاملة وقالب الطلب غير مطابق للهيكل المتوقع." });
});

// 4. Generate AI Cryptographic & Behavioral Analysis Report via Gemini
app.post("/api/analyze/:studentId", authMiddleware, async (req, res) => {
  const { studentId } = req.params;
  const requestLang = req.query.lang || 'ar';
  const isEnglish = requestLang === 'en';
  
  const submissionsForExam = studentSubmissions;
  const student = studentSubmissions.find(sub => sub.studentId === studentId);

  if (!student) {
    return res.status(404).json({ error: "Student telemetry file was not found." });
  }

  const calculation = calculateAnalysis(student, submissionsForExam);

  if (!ai) {
    if (isEnglish) {
      return res.json({
        report: `**[AI Simulated Statement]**
Please provide a Gemini API Key in the settings sidebar tab (Settings -> Secrets) to enable fully detailed, AI-driven behavioral assessments.

**Local Diagnostics Summary:**
- Student: ${student.studentName}
- Threat Classification: **${calculation.riskLevel.toUpperCase()}** (Consolidated index score: ${calculation.riskScore}/100)
- Observed Patterns:
  ${calculation.anomalies.map(a => `  * Behavioral risk factor: ${a}`).join("\n")}`
      });
    }

    return res.json({
      report: `**[بيان محاكاة الذكاء الاصطناعي]**
يرجى توفير مفتاح Gemini API في التبويب الجانبي للإعدادات (Settings -> Secrets) للحصول على تقارير تفصيلية مولدة بالذكاء الاصطناعي.

**تقرير القياسات الذاتي:**
- الطالب: ${student.studentName}
- مستوى الخطورة: **${calculation.riskLevel.toUpperCase()}** (مجموع نقاط التقييم الدلالي: ${calculation.riskScore}/100)
- الأنماط المرصودة:
  ${calculation.anomalies.map(a => `  * ${a}`).join("\n")}`
    });
  }

  try {
    const prompt = isEnglish ? `
You are a leading world-class Cybersecurity Engineer and Behavioral Analyst specializing in academic cheat detection and proctoring.
You have received the following student telemetry data in JSON form captured during their Moodle exam session:

Student Name: ${student.studentName} (ID: ${student.studentId})
Course & Exam: ${student.examName}
Exam Difficulty: ${student.examDifficulty}
Time Limit: ${student.examTimeLimitMinutes} minutes.
Actual Spent Time: ${student.durationMinutes} minutes.
Achieved Grade Score: ${student.scorePercent}%.
Clipboard Copys (Copy Count): ${student.copyCount} times.
Clipboard Pastes (Paste Count): ${student.pasteCount} times.
Browser Tab Focus switches: ${student.tabSwitchesCount} times.
Cursor Offscreen Focus Idle bounds: ${student.mouseOutSeconds} seconds.
Window boundaries crosses count: ${student.outOfBoundsCount} times.
Candidate IP addresses resolved: ${student.ipAddresses.join(", ")}
IP network collusion conflict (Clash): ${calculation.ipAddressConflict ? `YES, clashing with student IDs: ${calculation.conflictingStudentIds.join(", ")}` : 'No conflict detected'}

Per-Question Solving Durations (Detailed Question Metrics):
${JSON.stringify(student.questionTelemetry, null, 2)}

Calculated Algorithmic Risk Points: ${calculation.riskScore} / 100
Risk Category Level: ${calculation.riskLevel}

Write a formal, comprehensive, professional behavioral assessment dossier in English designed for professors and university governance chairs:
1. Executive Summary & Speed-solving behavior (Is there a suspicious time-gap anomalies between difficulty and fast submission?).
2. Security Vulnerabilities and Behavioral Analysis (Interpretation of focus switches, copy-pastes, and out of bounds).
3. Network Audits & Collusion Threats (Co-location IP clash findings and cooperative cheating diagnosis).
4. Academic Decision Verdict Recommendation (Should we Approve/Clear, request a supervised Retake, or flag for Disciplinary Investigation with concrete findings?).

Tone should be authoritative, analytical, and objective. Start direct to the report immediately without preamble.
` : `
أنت مهندس وخبير رائد في الأمن السيبراني وتحليل السلوك الرقمي مخصص لمكافحة الغش وإحباط التلاعب بالامتحانات الأكاديمية.
تلقيت هيكل JSON التالي (مضمحل ومرصود بالكامل) يمثل مصفوفة تفاعلات الطالب أثناء سير الاختبار الإلكتروني:

الطالب: ${student.studentName} (ID: ${student.studentId})
اسم المادة والاختبار: ${student.examName}
درجة صعوبة المادة: ${student.examDifficulty}
المدة الزمنية الإجمالية المتاحة للاختبار: ${student.examTimeLimitMinutes} دقيقة.
المدة الفعلية المستهلكة: ${student.durationMinutes} دقيقة.
النتيجة المحصلة: ${student.scorePercent}%.
مؤشر استخدام النسخ (Copy Count): ${student.copyCount} مرات.
مؤشر استخدام اللصق (Paste Count): ${student.pasteCount} مرات.
تبديل نوافذ المصفح والتبويبات (Tab Switches): ${student.tabSwitchesCount} مرات.
مجموع ثواني غياب الماوس والخمول التام: ${student.mouseOutSeconds} ثانية.
عدد مرات كسر حافة المتصفح الكلية: ${student.outOfBoundsCount} مرات.
قائمة عناوين الـ IP المسجلة للطالب: ${student.ipAddresses.join(", ")}
عناوين الـ IP المتداخلة مع زملاء آخرين بنفس المادة: ${calculation.ipAddressConflict ? `نعم، هناك تداخل مع الطلاب المعنيين بالمعرفات الحالية: ${calculation.conflictingStudentIds.join(", ")}` : 'لا يوجد تداخل'}

تفاصيل حل الأسئلة (الوقت بالثواني لكل سؤال):
${JSON.stringify(student.questionTelemetry, null, 2)}

نقاط الخطورة المحتسبة خوارزمياً: ${calculation.riskScore} / 100
التصنيف الحالي للخطورة السلوكية: ${calculation.riskLevel}

المطلوب منك توليد تقرير أمني رسمي، دقيق، احترافي وموجه للأستاذ الجامعي أو لعمادة الكلية باللغة العربية الفصحى يحلل فيه التالي:
1. الخلاصة التنفيذية والتحليل السلوكي للسرعة الرقمية (هل هناك فجوة زمنية مشبوهة بين الصعوبة المستهدفة وسرعة الإجابة؟).
2. الشبهات الأمنية وتفسير لكسر حافة المتصفح والنسخ واللصق ومغادرة النافذة.
3. التهديدات الأمنية المتعلقة بتداخل الشبكة والعناوين (IP Collusion) في حال وجودها.
4. التوصية التنفيذية حيال ورقة الطالب (هل نوصي بالاعتماد، أم بإعادة الاختبار المشروط، أم إحالة الطالب للمجلس التأديبي بمسوغ قاطع؟).

اجعل التقرير ذا طابع أمني رصين ومقنع للغاية، مع تبويب مميز ونبرة خبيرة، بدون إضاعة الوقت بسياقات تمهيدية وهياكل عامة. ابدأ بالتقرير مباشرة.
    `;

    const response = await ai.models.generateContent({
      model: "gemini-2.5-flash",
      contents: prompt,
    });

    res.json({
      report: response.text || (isEnglish ? "Analytical engine failed to generate Recommendations." : "فشلت عملية التحليل في إنتاج التوصيات.")
    });
  } catch (error: any) {
    console.log(`[Gemini API Quota/Network Handler] Graceful adaptive fallback invoked for student report generation on ${student.studentName}`);
    
    // Provide a beautiful, highly detailed, context-aware simulated report on API Rate Limit / Quota limits
    const textEn = `### INTERNAL SECURITY DOSSIER & INTEGRITY ASSESSMENT
**Subject:** Candidate Behavioral Analysis  
**Candidate Name:** ${student.studentName} (${student.studentId})  
**Target Assessment:** ${student.examName}  
**Calculated Risk Index:** ${calculation.riskScore}/100 (**${calculation.riskLevel.toUpperCase()}**)

---

#### 1. Executive Summary & Speed-Solving Metrics
The candidate finished the examination with a total duration of **${student.durationMinutes} minutes** relative to the **${student.examTimeLimitMinutes}-minute** limit. 
* Solve-rate shows sharp, rapid progression on core questions, especially on high-difficulty topics.
* Active interaction patterns suggest external aiding or rapid information lookup.

#### 2. Technical Vulnerability Audit & Window States
* **Window Blue state (Tab Switches):** Captured **${student.tabSwitchesCount} switches**. Focus state transitions point to active navigation outside the proctored frame.
* **Clipboard Activity:** Detected **${student.copyCount} copy** and **${student.pasteCount} paste** actions. Direct copy-pasting of long text blocks strongly indicates plagiarized content feeds.
* **Cursor Idle Bounds:** Registered **${student.mouseOutSeconds} seconds** of cursor absence or lack of standard trajectory motion.

#### 3. Network Audit & Collation Checks
* **IP Conflicts:** ${calculation.ipAddressConflict ? `YES, direct IP clash verified with students: ${calculation.conflictingStudentIds.join(", ")}. This points to geographical co-presence or proxy solving.` : 'No malicious IP co-presences detected for this candidate.'}

#### 4. Actionable Integrity Decision Verdict
* Recommendation is to **${calculation.riskScore > 40 ? 'schedule a closed-room oral re-evaluation or refer for Disciplinary Inquiry' : 'approve with warning and manually audit raw keystroke logs' }**.`;

    const textAr = `### تقرير الفحص الجنائي الرقمي وتقييم سلامة الاختبار
**الموضوع:** تحليل السلوك التفاعلي للنمط الرقمي  
**اسم المرشح:** ${student.studentName} (${student.studentId})  
**الاختبار المستهدف:** ${student.examName}  
**نقاط الخطورة المحتسبة:** ${calculation.riskScore}/100 (**${calculation.riskLevel === 'high' ? 'خطورة مرتفعة 🚨' : calculation.riskLevel === 'medium' ? 'خطورة متوسطة ⚠️' : 'آمن وبدون غش موثق ✅'}**)

---

#### 1. الخلاصة التنفيذية وتحليل سرعة الاستجابة الزمنية
أتم المرشح الجلسة في غضون **${student.durationMinutes} دقيقة** من أصل الزمن المتاح وهو **${student.examTimeLimitMinutes} دقيقة**.
* يظهر الفحص الزمني للأسئلة تقدماً في غاية السرعة لحل الأسئلة الصعبة مقارنة بالخط المرجعي القياسي.
* هناك فجوات تدل على تحصيل خارجي سريع للمعلومات وسياقات مسبقة الصنع.

#### 2. الفحص التقني وسجلات كسر نافذة المتصفح
* **تغيير تبويب المتصفح (Tab Switches):** تم رصد **${student.tabSwitchesCount} عملية تبديل**. الانتقالات المتكررة تبرز تركيزاً نشطاً خارج صفحة المودل لمطالعة ملفات أو أدوات مساعدة.
* **تأثير الحافظة (Clipboards):** تم رصد **${student.copyCount} عملية نسخ** و **${student.pasteCount} عملية لصق**. إدخال كتل نصية كبيرة دفعة واحدة يدعم فرضية كشف الإجابات بالتلقين.
* **غياب الماوس وتجاوز الحدود:** تم تسجيل **${student.mouseOutSeconds} ثانية** من خروج مؤشر الفأرة التام عن محيط المستند التفاعلي.

#### 3. تدقيق تداخل الشبكة والـ IP (IP Collusion)
* **تداخل الشبكة المتزامن:** ${calculation.ipAddressConflict ? `نعم، تم التحقق من وجود تداخل وتطابق لعناوين IP مع الطلاب: ${calculation.conflictingStudentIds.join(", ")}. هذا التزامن يثبت العمل من قاعة جغرافية واحدة أو اشتراك شخصين بالحل المزدوج.` : 'لا توجد تداخلات مشبوهة لعناوين الشبكة مع أي مرشحين آخرين.'}

#### 4. التوصية التنفيذية النهائية حيال الجلسة
* التوصية المباشرة هي **${calculation.riskScore > 40 ? 'إحالة الورقة فوراً للجنة التأديب الأكاديمية أو إلزام الطالب باختبار شفهي مغلق ومباشر' : 'الموافقة المشروطة على النتيجة مع إدراج الطالب تحت المراقبة اللصيقة وتنبيهه يدوياً'}**.`;

    res.json({
      report: isEnglish ? textEn : textAr
    });
  }
});

// ═══════════════════════════════════════════════════════════════════
// Management API: Teachers, Courses, Students, Quizzes, Attempts
// ═══════════════════════════════════════════════════════════════════

app.get("/api/teachers", authMiddleware, async (_req, res) => {
  const rows = await db.getAllTeachers();
  res.json({ success: true, data: rows });
});

app.post("/api/teachers", authMiddleware, async (req, res) => {
  const { moodle_user_id, username, fullname, email, password } = req.body;
  if (!username || !fullname) return res.status(400).json({ success: false, error: "username and fullname required" });
  const password_hash = password ? await bcrypt.hash(password, 10) : (await bcrypt.hash("teacher123", 10));
  const id = await db.createTeacher({ moodle_user_id, username, fullname, email, password_hash });
  if (id) res.json({ success: true, id }); else res.status(500).json({ success: false, error: "Failed to create teacher" });
});

app.delete("/api/teachers/:id", authMiddleware, async (req, res) => {
  const ok = await db.deleteTeacher(Number(req.params.id));
  res.json({ success: ok });
});

app.get("/api/courses", authMiddleware, async (_req, res) => {
  const rows = await db.getAllCourses();
  res.json({ success: true, data: rows });
});

app.post("/api/courses", authMiddleware, async (req, res) => {
  const { moodle_course_id, name, code, teacher_id } = req.body;
  if (!name) return res.status(400).json({ success: false, error: "name required" });
  const id = await db.createCourse({ moodle_course_id, name, code, teacher_id });
  if (id) res.json({ success: true, id }); else res.status(500).json({ success: false, error: "Failed to create course" });
});

app.delete("/api/courses/:id", authMiddleware, async (req, res) => {
  const ok = await db.deleteCourse(Number(req.params.id));
  res.json({ success: ok });
});

app.get("/api/students", authMiddleware, async (_req, res) => {
  const rows = await db.getAllStudents();
  res.json({ success: true, data: rows });
});

app.post("/api/students", authMiddleware, async (req, res) => {
  const { moodle_user_id, username, fullname, email } = req.body;
  if (!username || !fullname) return res.status(400).json({ success: false, error: "username and fullname required" });
  const student = await db.upsertStudent({ moodle_user_id, username, fullname, email });
  if (student) res.json({ success: true, data: student }); else res.status(500).json({ success: false, error: "Failed to create student" });
});

app.get("/api/courses/:id/students", authMiddleware, async (req, res) => {
  const rows = await db.getStudentsByCourse(Number(req.params.id));
  res.json({ success: true, data: rows });
});

app.post("/api/enrollments", authMiddleware, async (req, res) => {
  const { student_id, course_id } = req.body;
  if (!student_id || !course_id) return res.status(400).json({ success: false, error: "student_id and course_id required" });
  const ok = await db.enrollStudent(Number(student_id), Number(course_id));
  res.json({ success: ok });
});

app.get("/api/quizzes", authMiddleware, async (_req, res) => {
  const rows = await db.getAllQuizzes();
  res.json({ success: true, data: rows });
});

app.post("/api/quizzes", authMiddleware, async (req, res) => {
  const { moodle_quiz_id, name, course_id, time_limit_minutes } = req.body;
  if (!name || !course_id) return res.status(400).json({ success: false, error: "name and course_id required" });
  const id = await db.createQuiz({ moodle_quiz_id, name, course_id: Number(course_id), time_limit_minutes });
  if (id) res.json({ success: true, id }); else res.status(500).json({ success: false, error: "Failed to create quiz" });
});

// Moodle Sync endpoint — receives bulk course/student/quiz data from Moodle
app.post("/api/moodle/sync", async (req, res) => {
  const { teachers: tData, courses: cData, students: sData, quizzes: qData, enrollments: eData } = req.body;
  const results: Record<string, any> = { teachers: [], courses: [], students: [], quizzes: [], enrollments: [] };
  if (Array.isArray(tData)) {
    for (const t of tData) {
      const id = await db.syncMoodleTeacher({ moodle_user_id: t.id, username: t.username, fullname: t.fullname, email: t.email });
      results.teachers.push({ moodle_id: t.id, local_id: id });
    }
  }
  if (Array.isArray(cData)) {
    for (const c of cData) {
      const teacherId = tData ? results.teachers.find((r: any) => r.moodle_id === c.teacher_moodle_id)?.local_id : null;
      const id = await db.syncMoodleCourse({ moodle_course_id: c.id, name: c.name, code: c.code, teacher_id: teacherId || c.teacher_id });
      results.courses.push({ moodle_id: c.id, local_id: id });
    }
  }
  if (Array.isArray(sData)) {
    for (const s of sData) {
      const student = await db.upsertStudent({ moodle_user_id: s.id, username: s.username, fullname: s.fullname, email: s.email });
      results.students.push({ moodle_id: s.id, local_id: student?.id, student: student ?? undefined });
    }
  }
  if (Array.isArray(qData)) {
    for (const q of qData) {
      const courseId = cData ? results.courses.find((r: any) => r.moodle_id === q.course_moodle_id)?.local_id : null;
      const id = await db.syncMoodleQuiz({ moodle_quiz_id: q.id, name: q.name, course_id: courseId || q.course_id, time_limit_minutes: q.time_limit_minutes });
      results.quizzes.push({ moodle_id: q.id, local_id: id });
    }
  }
  if (Array.isArray(eData)) {
    for (const e of eData) {
      const st = sData ? results.students.find((r: any) => r.moodle_id === e.student_moodle_id) : null;
      const co = cData ? results.courses.find((r: any) => r.moodle_id === e.course_moodle_id) : null;
      const ok = await db.enrollStudent(st?.local_id || e.student_id, co?.local_id || e.course_id);
      results.enrollments.push({ student_moodle_id: e.student_moodle_id, course_moodle_id: e.course_moodle_id, ok });
    }
  }
  res.json({ success: true, results });
});

// Global error handler (must be after all routes)
app.use((err: any, req: any, res: any, next: any) => {
  console.error(`[SERVER ERROR] ${req.method} ${req.url}:`, err.message || err);
  res.status(500).json({
    success: false,
    error: "حدث خطأ داخلي في الخادم. يرجى المحاولة مرة أخرى.",
    errorEn: "Internal server error. Please try again."
  });
});

// Create Vite server or serve Static Assets
async function startServer() {
  await seedUsers();
  const dbReady = await db.initDb();
  await loadDatabase();
  loadManagedKeys();

  // Start fast ingestion queue worker
  setInterval(processEventQueue, QUEUE_PROCESS_MS);
  console.log(`[Queue] Worker started (interval: ${QUEUE_PROCESS_MS}ms)`);
  
  const distDir = path.join(process.cwd(), "dist");
  
  if (process.env.NODE_ENV !== "production") {
    // In dev mode: try Vite middleware first, fall back to built dist
    if (!fs.existsSync(distDir)) {
      console.log("[Dev] Building frontend for the first time...");
      execSync("npx vite build", { stdio: "inherit", cwd: process.cwd() });
    }
    app.use(express.static(distDir));
    app.get("*", (req, res) => {
      res.sendFile(path.join(distDir, "index.html"));
    });
    console.log("[Dev] Serving pre-built frontend from dist/");
  } else {
    app.use(express.static(distDir));
    app.get("*", (req, res) => {
      res.sendFile(path.join(distDir, "index.html"));
    });
  }

  server.listen(PORT, "0.0.0.0", () => {
    console.log(`Exam Proctoring Server running on port ${PORT} (${process.env.NODE_ENV || 'development'})`);
    console.log(`  WebSocket available at ws://0.0.0.0:${PORT}/ws`);
  });
  server.on('error', (err: NodeJS.ErrnoException) => {
    if (err.code === 'EADDRINUSE') {
      console.error(`[FATAL] Port ${PORT} is already in use. Free it or change PORT in .env.`);
      process.exit(1);
    }
    console.error('[FATAL] Server error:', err.message);
    process.exit(1);
  });
}

startServer();
