import type { Express, Request, Response, NextFunction } from "express";
import type { Server } from "http";
import { storage, backfillInviteSlugs } from "./storage";
import { api } from "@shared/routes";
import { z } from "zod";
import multer from "multer";
import path from "path";
import fs from "fs";
import express from "express";
import session from "express-session";
import connectPgSimple from "connect-pg-simple";
import { waManager, randomBetween, WhatsAppManager } from "./whatsapp";
import { verifyDatabaseConnection, poolInstance } from "./db";
import crypto from "crypto";
import { setupCleanupJobs, sendScheduledReminders } from "./cleanup";
import { MESSAGE_TEMPLATES, REMINDER_TEMPLATES, buildMessage, buildMessageFromTemplate, buildReminderMessage } from "./message-utils";
import { authenticator } from "otplib";
import QRCode from "qrcode";
import bcrypt from "bcryptjs";
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import type { WebAuthnCredential, User } from "@shared/schema";

// ── WebAuthn RP helpers ───────────────────────────────────────────────────
function getWebAuthnRpID(req: Request): string {
  if (process.env.WEBAUTHN_RP_ID) return process.env.WEBAUTHN_RP_ID;
  return req.hostname || "localhost";
}
function getWebAuthnOrigin(req: Request): string {
  if (process.env.WEBAUTHN_ORIGIN) return process.env.WEBAUTHN_ORIGIN;
  // req.protocol is correct when trust proxy is set on the app
  return `${req.protocol}://${req.get("host")}`;
}

// ── Brute-force / IP-ban helpers ──────────────────────────────────────────
const loginAttempts = new Map<string, { count: number; lastAt: number }>();
const MAX_ATTEMPTS = 5;
const BAN_WINDOW_MS = 15 * 60 * 1000; // 15 min auto-ban window

function getClientIp(req: Request): string {
  const fwd = req.headers["x-forwarded-for"];
  if (typeof fwd === "string") return fwd.split(",")[0].trim();
  return req.socket.remoteAddress || "unknown";
}

async function checkBruteForce(ip: string): Promise<{ blocked: boolean; remaining: number }> {
  const banned = await storage.isIpBanned(ip).catch(() => false);
  if (banned) return { blocked: true, remaining: 0 };
  const entry = loginAttempts.get(ip);
  if (!entry) return { blocked: false, remaining: MAX_ATTEMPTS };
  // Reset window if older than BAN_WINDOW_MS
  if (Date.now() - entry.lastAt > BAN_WINDOW_MS) {
    loginAttempts.delete(ip);
    return { blocked: false, remaining: MAX_ATTEMPTS };
  }
  const remaining = Math.max(0, MAX_ATTEMPTS - entry.count);
  return { blocked: entry.count >= MAX_ATTEMPTS, remaining };
}

async function recordFailedAttempt(ip: string): Promise<number> {
  const entry = loginAttempts.get(ip) || { count: 0, lastAt: Date.now() };
  if (Date.now() - entry.lastAt > BAN_WINDOW_MS) {
    entry.count = 0;
  }
  entry.count++;
  entry.lastAt = Date.now();
  loginAttempts.set(ip, entry);
  if (entry.count >= MAX_ATTEMPTS) {
    await storage.banIp(ip, `تجاوز عدد محاولات تسجيل الدخول (${entry.count} محاولات)`, false).catch(() => {});
    await storage.addAuditLog({ action: "ip_auto_banned", details: `IP ${ip} banned after ${entry.count} failed attempts`, ip }).catch(() => {});
    return 0;
  }
  return MAX_ATTEMPTS - entry.count;
}

function clearAttempts(ip: string) {
  loginAttempts.delete(ip);
}

declare module "express-session" {
  interface SessionData {
    userId?: number;
    pendingPasswordUserId?: number;
    pendingResetUserId?: number;
    webauthnChallenge?: string;
  }
}

declare module "express-serve-static-core" {
  interface Request {
    effectiveUserId?: number;
  }
}

function generateAccessCode(): string {
  return crypto.randomBytes(4).toString("hex").toUpperCase().slice(0, 8);
}

function normalizeGuestPhone(raw: string): string {
  let n = raw.replace(/[\s\-().]/g, '');
  if (n.startsWith('+')) return n;
  if (n.startsWith('00')) return '+' + n.slice(2);
  if (n.startsWith('5') && n.length === 9) return '+966' + n;
  if (n.length >= 7) return '+' + n;
  return n;
}

async function requireAuth(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) {
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  const user = await storage.getUser(req.session.userId);
  if (!user) {
    req.session.destroy(() => {});
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  if (user.isSuspended) {
    req.session.destroy(() => {});
    return res.status(403).json({ message: "تم تعليق هذا الحساب. تواصل مع الإدارة." });
  }
  req.effectiveUserId = user.primaryUserId ?? req.session.userId!;
  next();
}

async function requireAdmin(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) {
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  const user = await storage.getUser(req.session.userId);
  if (!user || user.isSuspended) {
    req.session.destroy(() => {});
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  if (!user.isAdmin) {
    return res.status(403).json({ message: "غير مصرح" });
  }
  next();
}

async function requireAdminOrCoAdmin(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) {
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  const user = await storage.getUser(req.session.userId);
  if (!user || user.isSuspended) {
    req.session.destroy(() => {});
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  if (!user.isAdmin && !user.isCoAdmin) {
    return res.status(403).json({ message: "غير مصرح" });
  }
  next();
}

// Check if MyFatoorah is configured AND active system-wide (used by requireHall billing guard)
async function isMyfatoorahConfigured(): Promise<boolean> {
  try {
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    if (!admin) return false;
    const token = await storage.getSetting(admin.id, "myfatoorah_token");
    const amount = await storage.getSetting(admin.id, "myfatoorah_amount");
    if (!token || !amount) return false;
    // If admin explicitly disabled the gateway, treat as not configured
    const activeStr = await storage.getSetting(admin.id, "myfatoorah_active");
    if (activeStr === "false") return false;
    return true;
  } catch {
    return false;
  }
}

// Returns true if hall has valid billing access (grace period respected)
function hallHasValidBilling(user: { billingStatus?: string | null; subscriptionExpiresAt?: Date | null; nextBillingAt?: Date | null }): boolean {
  const now = new Date();
  // Compute grace expiry = max(subscriptionExpiresAt, nextBillingAt)
  const a = user.subscriptionExpiresAt ? new Date(user.subscriptionExpiresAt) : null;
  const b = user.nextBillingAt ? new Date(user.nextBillingAt) : null;
  const graceExpiry = a && b ? (a > b ? a : b) : (a || b);
  const graceExpired = !graceExpiry || graceExpiry < now;

  const status = user.billingStatus;
  if (status === "expired") return false;                          // payment failed → blocked
  if (status === "cancelled" && graceExpired) return false;       // cancelled + grace over → blocked
  if (status === "active" && graceExpired) return false;          // active but job hasn't suspended yet → blocked
  if ((!status || status === "paid") && graceExpired) return false; // manual/paid — subscription date passed → blocked
  return true;                                                     // otherwise allow
}

async function requireHall(req: Request, res: Response, next: NextFunction) {
  if (!req.session.userId) {
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  const user = await storage.getUser(req.session.userId);
  if (!user || user.isSuspended) {
    req.session.destroy(() => {});
    return res.status(401).json({ message: "غير مسجل الدخول" });
  }
  if (user.role !== "hall") {
    return res.status(403).json({ message: "غير مصرح" });
  }
  next();
}

// Additional billing enforcement middleware — applied on top of requireHall for data-access routes.
// Returns 402 when MyFatoorah is configured AND the hall's billing is not valid.
// NOT applied to payment initiation / cancel / hall/me endpoints so those always work.
async function requireHallBilling(req: Request, res: Response, next: NextFunction) {
  const user = await storage.getUser(req.session.userId!);
  if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });
  const mfEnabled = await isMyfatoorahConfigured();
  if (mfEnabled && !hallHasValidBilling(user)) {
    return res.status(402).json({ message: "اشتراكك منتهٍ. يرجى تجديد الاشتراك.", billingRequired: true });
  }
  next();
}

// Personal event fields stored per-user (not shared with primary account)
const PERSONAL_EVENT_FIELDS_GLOBAL = ["event_sender", "event_occasion"] as const;

/**
 * Returns event details for a user, merging:
 * - Shared fields (date, time, venue) from the effective (primary) user
 * - Personal fields (sender, occasion) from the session user's own settings
 */
async function getMergedEventDetails(sessionUserId: number, effectiveUserId: number) {
  const shared = await storage.getEventDetails(effectiveUserId);
  if (sessionUserId === effectiveUserId) return shared;
  const personal = await storage.getEventDetails(sessionUserId);
  const merged: Record<string, string> = { ...(shared || {}) };
  for (const field of PERSONAL_EVENT_FIELDS_GLOBAL) {
    if (personal?.[field] !== undefined) {
      merged[field] = personal[field];
    } else {
      delete merged[field];
    }
  }
  return merged;
}

export async function registerRoutes(
  httpServer: Server,
  app: Express
): Promise<Server> {
  const dbConnected = await verifyDatabaseConnection();
  if (!dbConnected) {
    console.error("WARNING: Database connection failed. Some features may not work.");
  }

  const PgStore = connectPgSimple(session);
  app.use(
    session({
      store: new PgStore({
        pool: poolInstance(),
        tableName: "session",
        createTableIfMissing: true,
        pruneSessionInterval: 3600,
      }),
      secret: process.env.SESSION_SECRET || "invatna-secret-key-change-me",
      resave: false,
      saveUninitialized: false,
      cookie: {
        maxAge: 30 * 24 * 60 * 60 * 1000,
        httpOnly: true,
        secure: false,
        sameSite: "lax",
      },
    })
  );

  const uploadDir = path.join(process.cwd(), "uploads");
  if (!fs.existsSync(uploadDir)) {
    fs.mkdirSync(uploadDir);
  }

  const storageConfig = multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, uploadDir);
    },
    filename: function (req, file, cb) {
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      cb(null, uniqueSuffix + path.extname(file.originalname));
    }
  });

  const upload = multer({ storage: storageConfig, limits: { fileSize: 50 * 1024 * 1024 } });
  const mediaUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 15 * 1024 * 1024 } });
  app.use('/uploads', express.static(uploadDir));

  // ── Security headers ─────────────────────────────────────────────────────
  app.use((req, res, next) => {
    res.setHeader("X-Content-Type-Options", "nosniff");
    // Allow /auto-login to be embedded in iframes from external origins (e.g. Replit store)
    if (!req.path.startsWith("/auto-login")) {
      res.setHeader("X-Frame-Options", "SAMEORIGIN");
    }
    res.setHeader("X-XSS-Protection", "1; mode=block");
    res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
    res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
    if (process.env.NODE_ENV === "production") {
      res.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload");
    }
    next();
  });

  // ========== AUTH ROUTES ==========

  const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "AmerFamily";

  app.post(api.auth.login.path, async (req, res) => {
    const ip = getClientIp(req);
    try {
      const { code, adminPassword } = z.object({ code: z.string().min(1), adminPassword: z.string().optional() }).parse(req.body);

      // Check IP ban / brute force
      const bf = await checkBruteForce(ip);
      if (bf.blocked) {
        return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك مؤقتاً بسبب كثرة المحاولات الفاشلة. تواصل مع المدير لفك الحظر.", ipBanned: true });
      }

      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user) {
        const remaining = await recordFailedAttempt(ip);
        await storage.addAuditLog({ action: "login_failed", details: `كود غير صحيح — المحاولات المتبقية: ${remaining}`, ip }).catch(() => {});
        if (remaining === 0) {
          return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك بسبب كثرة المحاولات الفاشلة.", ipBanned: true, remaining: 0 });
        }
        return res.status(401).json({ message: "كود الدخول غير صحيح", remaining });
      }
      if (user.isSuspended) {
        return res.status(403).json({ message: "تم تعليق هذا الحساب. تواصل مع الإدارة." });
      }
      if (user.isAdmin || user.isCoAdmin) {
        if (!adminPassword) {
          return res.json({ requiresAdminPassword: true });
        }
        if (adminPassword !== ADMIN_PASSWORD) {
          const remaining = await recordFailedAttempt(ip);
          await storage.addAuditLog({ action: "admin_password_failed", details: `محاولة كلمة مرور فاشلة — المتبقية: ${remaining}`, ip, actorId: user.id }).catch(() => {});
          if (remaining === 0) {
            return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك بسبب كثرة المحاولات الفاشلة.", ipBanned: true, remaining: 0 });
          }
          return res.status(401).json({ message: "كلمة المرور غير صحيحة", remaining });
        }
        // Check if admin/co-admin has 2FA enabled (uses the primary admin's 2FA setting)
        const allUsers = await storage.getAllUsers();
        const primaryAdmin = allUsers.find(u => u.isAdmin);
        const totpCheckId = primaryAdmin ? primaryAdmin.id : user.id;
        const totpEnabled = await storage.getSetting(totpCheckId, "admin_totp_enabled").catch(() => undefined);
        if (totpEnabled === "true") {
          return res.json({ requiresTotp: true });
        }
      }
      if (user.role === "hall" && user.hallPassword) {
        if (!adminPassword) {
          return res.json({ requiresHallPassword: true });
        }
        if (adminPassword !== user.hallPassword) {
          const remaining = await recordFailedAttempt(ip);
          if (remaining === 0) {
            return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك بسبب كثرة المحاولات الفاشلة.", ipBanned: true, remaining: 0 });
          }
          return res.status(401).json({ message: "كلمة المرور غير صحيحة", remaining });
        }
      }

      // Password check — if user already has a password, skip OTP entirely
      // Only applies to role=user accounts (not hall, scanner, admin, co-admin)
      if (user.role === "user" && user.passwordHash) {
        // Check if user has WebAuthn credentials
        const hasWebauthn = !!(user.webauthnCredentials?.length);
        return res.json({ requiresPassword: true, hasWebauthn });
      }

      // OTP check — required for all eligible users on first login (no password yet)
      if (isOtpEligible(user) && !user.phoneVerified) {
        const otpEnabled = await storage.getSystemMessageEnabled("otp");
        if (!otpEnabled) {
          // OTP disabled — bypass verification; if no password set, still require it
          clearAttempts(ip);
          if (!user.passwordHash) {
            // Mark OTP as "verified" via session so set-password endpoint can proceed
            req.session.pendingPasswordUserId = user.id;
            await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
            return res.json({ needsPassword: true });
          }
          req.session.userId = user.id;
          if (user.isAdmin && user.messageQuota !== -1) {
            await storage.updateUser(user.id, { messageQuota: -1 });
          }
          await storage.addAuditLog({ action: "login_success", details: `دخول: ${user.name} (${user.role})`, ip, actorId: user.id }).catch(() => {});
          await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
          return res.json(await buildLoginResponse(await storage.getUser(user.id)));
        }
        const otp = String(Math.floor(100000 + Math.random() * 900000));
        const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
        await storage.setSetting(user.id, "otp_code", otp);
        await storage.setSetting(user.id, "otp_expires_at", expiresAt);
        const msg = `🔐 كود التحقق الخاص بك في إنفايتنا:\n\n*${otp}*\n\nصالح لمدة 5 دقائق فقط. لا تشاركه مع أحد.`;
        const phone = user.phoneNumber.replace(/[^0-9]/g, '');
        const sent = await sendViaAnySystem(phone, msg, undefined, undefined, true);
        if (!sent) return res.status(503).json({ message: "تعذّر إرسال كود التحقق عبر واتساب. تأكد من اتصال واتساب وأعد المحاولة." });
        return res.json({ requiresOtp: true });
      }

      // If regular user already verified (returning user, no password yet) → ask for password setup
      if (user.role === "user" && user.phoneVerified && !user.passwordHash) {
        const otpEnabled = await storage.getSystemMessageEnabled("otp");
        if (otpEnabled) {
          // Force OTP re-verify before password setup
          await storage.updateUser(user.id, { phoneVerified: false });
          const otp = String(Math.floor(100000 + Math.random() * 900000));
          const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
          await storage.setSetting(user.id, "otp_code", otp);
          await storage.setSetting(user.id, "otp_expires_at", expiresAt);
          const msg = `🔐 كود التحقق الخاص بك في إنفايتنا:\n\n*${otp}*\n\nصالح لمدة 5 دقائق فقط. لا تشاركه مع أحد.`;
          const phone = user.phoneNumber.replace(/[^0-9]/g, '');
          const sent = await sendViaAnySystem(phone, msg, undefined, undefined, true);
          if (!sent) return res.status(503).json({ message: "تعذّر إرسال كود التحقق عبر واتساب. تأكد من اتصال واتساب وأعد المحاولة." });
          return res.json({ requiresOtp: true });
        }
        req.session.pendingPasswordUserId = user.id;
        await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
        return res.json({ needsPassword: true });
      }

      clearAttempts(ip);
      await storage.addAuditLog({ action: "login_success", details: `دخول: ${user.name} (${user.role})`, ip, actorId: user.id }).catch(() => {});
      req.session.userId = user.id;
      if (user.isAdmin && user.messageQuota !== -1) {
        await storage.updateUser(user.id, { messageQuota: -1 });
      }
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(await buildLoginResponse(await storage.getUser(user.id)));
    } catch (err) {
      res.status(400).json({ message: "كود غير صالح" });
    }
  });

  // Helper: checks if a user is eligible for OTP flow
  // OTP eligible = everyone except admin and scanner accounts
  function isOtpEligible(user: { isAdmin: boolean; isCoAdmin: boolean; role: string; isSuspended: boolean | null }) {
    return !user.isAdmin && !user.isCoAdmin && user.role !== "scanner" && !user.isSuspended;
  }

  // Helper: build a complete login response for a user
  // Handles secondary users by using the primary account's quota
  async function buildLoginResponse(user: Awaited<ReturnType<typeof storage.getUser>>) {
    if (!user) throw new Error("user is null");
    const quotaSource = user.primaryUserId
      ? (await storage.getUser(user.primaryUserId)) ?? user
      : user;
    const remaining = quotaSource.messageQuota === -1 ? -1 : Math.max(0, quotaSource.messageQuota - quotaSource.messagesSent);
    return {
      id: user.id,
      name: user.name,
      phoneNumber: user.phoneNumber,
      accessCode: user.accessCode,
      messageQuota: quotaSource.messageQuota,
      messagesSent: quotaSource.messagesSent,
      messagesRemaining: remaining,
      isAdmin: user.isAdmin,
      isCoAdmin: user.isCoAdmin,
      role: user.role,
      parentHallId: user.parentHallId ?? null,
      isSuspended: user.isSuspended ?? null,
      subscriptionExpiresAt: user.subscriptionExpiresAt ? user.subscriptionExpiresAt.toISOString() : null,
      hallPassword: user.hallPassword ? "set" : null,
      primaryUserId: user.primaryUserId ?? null,
    };
  }

  // Verify OTP — marks phoneVerified=true, creates session
  app.post("/api/auth/verify-otp", async (req, res) => {
    try {
      const { code, otp } = z.object({ code: z.string().min(1), otp: z.string().min(6).max(6) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user) return res.status(401).json({ message: "كود الدخول غير صحيح" });
      if (!isOtpEligible(user)) return res.status(403).json({ message: "غير مسموح" });
      if (user.isSuspended) return res.status(403).json({ message: "الحساب موقوف" });
      const savedOtp = await storage.getSetting(user.id, "otp_code");
      const expiresAt = await storage.getSetting(user.id, "otp_expires_at");
      if (!savedOtp || savedOtp !== otp) return res.status(401).json({ message: "كود التحقق غير صحيح" });
      if (!expiresAt || new Date(expiresAt) < new Date()) return res.status(401).json({ message: "انتهت صلاحية كود التحقق" });
      await storage.updateUser(user.id, { phoneVerified: true });
      await storage.setSetting(user.id, "otp_code", "");
      await storage.setSetting(user.id, "otp_expires_at", "");

      // If this OTP was for a password reset → now safely clear the old credentials
      if (req.session.pendingResetUserId === user.id) {
        await storage.updateUser(user.id, { passwordHash: null, webauthnCredentials: null });
        delete req.session.pendingResetUserId;
        req.session.pendingPasswordUserId = user.id;
        delete req.session.userId;
        await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
        return res.json({ needsPassword: true });
      }

      // If regular user has no password yet → require password setup before creating session
      if (user.role === "user" && !user.passwordHash) {
        req.session.pendingPasswordUserId = user.id;
        delete req.session.userId;
        await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
        return res.json({ needsPassword: true });
      }

      req.session.userId = user.id;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(await buildLoginResponse(user));
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // Resend OTP
  app.post("/api/auth/resend-otp", async (req, res) => {
    try {
      const { code } = z.object({ code: z.string().min(1) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user) return res.status(401).json({ message: "كود الدخول غير صحيح" });
      if (!isOtpEligible(user)) return res.status(403).json({ message: "غير مسموح" });
      const otpEnabled = await storage.getSystemMessageEnabled("otp");
      if (!otpEnabled) return res.status(503).json({ message: "خاصية التحقق عبر الواتساب معطّلة حالياً. تواصل مع المدير." });
      // Rate limit: max 1 resend per 60 seconds
      const lastResendAt = await storage.getSetting(user.id, "otp_last_resend_at");
      if (lastResendAt) {
        const elapsed = Date.now() - new Date(lastResendAt).getTime();
        if (elapsed < 60_000) {
          const remaining = Math.ceil((60_000 - elapsed) / 1000);
          return res.status(429).json({ message: `يرجى الانتظار ${remaining} ثانية قبل إعادة الإرسال`, remainingSeconds: remaining });
        }
      }
      const otp = String(Math.floor(100000 + Math.random() * 900000));
      const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
      await storage.setSetting(user.id, "otp_code", otp);
      await storage.setSetting(user.id, "otp_expires_at", expiresAt);
      await storage.setSetting(user.id, "otp_last_resend_at", new Date().toISOString());
      const msg = `🔐 كود التحقق الجديد في إنفايتنا:\n\n*${otp}*\n\nصالح لمدة 5 دقائق فقط.`;
      const phone = user.phoneNumber.replace(/[^0-9]/g, '');
      const sent = await sendViaAnySystem(phone, msg, undefined, undefined, true);
      if (!sent) return res.status(503).json({ message: "فشل إرسال رسالة واتساب، تأكد من اتصال الواتساب وحاول مجدداً" });
      res.json({ success: true });
    } catch (err) {
      res.status(400).json({ message: "فشل إعادة الإرسال" });
    }
  });

  // ── Set password (first-time) — called after OTP verify when needsPassword=true ──
  app.post("/api/auth/set-password", async (req, res) => {
    try {
      const { password } = z.object({
        password: z.string().min(6).max(100),
      }).parse(req.body);

      // Must have a pending password session from OTP verification
      const pendingId = req.session.pendingPasswordUserId;
      if (!pendingId) {
        return res.status(403).json({ message: "يجب التحقق عبر واتساب أولاً" });
      }
      const user = await storage.getUser(pendingId);
      if (!user) return res.status(404).json({ message: "المستخدم غير موجود" });
      if (!isOtpEligible(user)) return res.status(403).json({ message: "غير مسموح" });

      const hash = await bcrypt.hash(password, 12);
      await storage.updateUser(user.id, { passwordHash: hash });

      // Create proper session
      delete req.session.pendingPasswordUserId;
      req.session.userId = user.id;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));

      // Check if WebAuthn is available (we'll report it so frontend can offer registration)
      const hasWebauthn = !!(user.webauthnCredentials?.length);
      res.json({ ...(await buildLoginResponse(user)), passwordJustSet: true, hasWebauthn });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // ── Verify password (returning users with existing password) ──
  app.post("/api/auth/verify-password", async (req, res) => {
    const ip = getClientIp(req);
    try {
      const { code, password } = z.object({
        code: z.string().min(1),
        password: z.string().min(1),
      }).parse(req.body);

      const bf = await checkBruteForce(ip);
      if (bf.blocked) {
        return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك مؤقتاً.", ipBanned: true });
      }

      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user || user.role !== "user" || !user.passwordHash) {
        const remaining = await recordFailedAttempt(ip);
        return res.status(401).json({ message: "بيانات غير صحيحة", remaining });
      }
      if (user.isSuspended) return res.status(403).json({ message: "الحساب موقوف" });

      const valid = await bcrypt.compare(password, user.passwordHash);
      if (!valid) {
        const remaining = await recordFailedAttempt(ip);
        await storage.addAuditLog({ action: "password_failed", details: `كلمة مرور خاطئة`, ip, actorId: user.id }).catch(() => {});
        if (remaining === 0) return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك.", ipBanned: true, remaining: 0 });
        return res.status(401).json({ message: "كلمة المرور غير صحيحة", remaining });
      }

      clearAttempts(ip);
      req.session.userId = user.id;
      delete req.session.pendingPasswordUserId;
      await storage.addAuditLog({ action: "login_success", details: `دخول بكلمة مرور: ${user.name}`, ip, actorId: user.id }).catch(() => {});
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(await buildLoginResponse(user));
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // ── Forgot password — sends OTP for verification; passwordHash is NOT cleared until OTP passes ──
  app.post("/api/auth/forgot-password", async (req, res) => {
    try {
      const { code } = z.object({ code: z.string().min(1) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user || user.role !== "user") return res.status(404).json({ message: "الكود غير صحيح" });
      if (user.isSuspended) return res.status(403).json({ message: "الحساب موقوف" });

      const otpEnabled = await storage.getSystemMessageEnabled("otp");
      if (!otpEnabled) return res.status(503).json({ message: "خاصية التحقق معطّلة. تواصل مع المدير." });

      // Send OTP without touching the existing passwordHash
      const otp = String(Math.floor(100000 + Math.random() * 900000));
      const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
      await storage.setSetting(user.id, "otp_code", otp);
      await storage.setSetting(user.id, "otp_expires_at", expiresAt);
      const msg = `🔐 طلب إعادة تعيين كلمة المرور في إنفايتنا:\n\n*${otp}*\n\nصالح لمدة 5 دقائق فقط. إذا لم تطلب ذلك، تجاهل هذه الرسالة.`;
      const phone = user.phoneNumber.replace(/[^0-9]/g, '');
      const sent = await sendViaAnySystem(phone, msg, undefined, undefined, true);
      if (!sent) return res.status(503).json({ message: "تعذّر إرسال كود التحقق عبر واتساب." });

      // Mark reset intent in session only — passwordHash cleared after successful OTP
      req.session.pendingResetUserId = user.id;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json({ success: true, requiresOtp: true });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // ── WebAuthn: Generate registration options ──
  app.post("/api/auth/webauthn/register-options", requireAuth, async (req, res) => {
    try {
      const user = await storage.getUser(req.session.userId!);
      if (!user) return res.status(404).json({ message: "المستخدم غير موجود" });
      if (!isOtpEligible(user)) return res.status(403).json({ message: "غير مسموح" });

      const existingCredentials: WebAuthnCredential[] = user.webauthnCredentials ?? [];
      const rpID = getWebAuthnRpID(req);

      const options = await generateRegistrationOptions({
        rpName: "إنفايتنا",
        rpID,
        userID: new TextEncoder().encode(String(user.id)),
        userName: user.name,
        userDisplayName: user.name,
        attestationType: "none",
        excludeCredentials: existingCredentials.map((c) => ({
          id: c.credentialID,
          type: "public-key" as const,
        })),
        authenticatorSelection: {
          residentKey: "preferred",
          userVerification: "preferred",
        },
      });

      req.session.webauthnChallenge = options.challenge;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(options);
    } catch (err) {
      res.status(500).json({ message: "خطأ في إنشاء خيارات التسجيل" });
    }
  });

  // ── WebAuthn: Verify and save registration ──
  app.post("/api/auth/webauthn/register", requireAuth, async (req, res) => {
    try {
      const user = await storage.getUser(req.session.userId!);
      if (!user || !isOtpEligible(user)) return res.status(403).json({ message: "غير مسموح" });

      const challenge = req.session.webauthnChallenge;
      if (!challenge) return res.status(400).json({ message: "لا توجد جلسة تسجيل نشطة" });

      const rpID = getWebAuthnRpID(req);
      const origin = getWebAuthnOrigin(req);

      const verification = await verifyRegistrationResponse({
        response: req.body,
        expectedChallenge: challenge,
        expectedOrigin: origin,
        expectedRPID: rpID,
      });

      if (!verification.verified || !verification.registrationInfo) {
        return res.status(400).json({ message: "فشل التحقق من البصمة" });
      }

      const { credential } = verification.registrationInfo;
      const existingCredentials: WebAuthnCredential[] = user.webauthnCredentials ?? [];
      const newCredential: WebAuthnCredential = {
        credentialID: credential.id,
        credentialPublicKey: Buffer.from(credential.publicKey).toString("base64"),
        counter: credential.counter,
        transports: req.body.response?.transports ?? [],
        registeredAt: new Date().toISOString(),
      };
      const updatedCredentials: WebAuthnCredential[] = [...existingCredentials, newCredential];

      await storage.updateUser(user.id, { webauthnCredentials: updatedCredentials });
      delete req.session.webauthnChallenge;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json({ success: true });
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "خطأ في التسجيل";
      res.status(400).json({ message: msg });
    }
  });

  // ── WebAuthn: Generate authentication options ──
  app.post("/api/auth/webauthn/auth-options", async (req, res) => {
    try {
      const { code } = z.object({ code: z.string().min(1) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user || user.role !== "user" || !user.passwordHash) {
        return res.status(404).json({ message: "الكود غير صحيح" });
      }
      const credentials: WebAuthnCredential[] = user.webauthnCredentials ?? [];
      if (!credentials.length) return res.status(400).json({ message: "لا توجد بصمة مسجّلة" });

      const rpID = getWebAuthnRpID(req);
      const options = await generateAuthenticationOptions({
        rpID,
        userVerification: "preferred",
        allowCredentials: credentials.map((c) => ({
          id: c.credentialID,
          type: "public-key" as const,
        })),
      });

      req.session.webauthnChallenge = options.challenge;
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(options);
    } catch (err) {
      res.status(500).json({ message: "خطأ في إنشاء خيارات التحقق" });
    }
  });

  // ── WebAuthn: Verify authentication ──
  app.post("/api/auth/webauthn/auth-verify", async (req, res) => {
    const ip = getClientIp(req);
    try {
      const { code } = z.object({ code: z.string().min(1) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user || user.role !== "user" || !user.passwordHash) {
        return res.status(404).json({ message: "الكود غير صحيح" });
      }
      const credentials: WebAuthnCredential[] = user.webauthnCredentials ?? [];
      const challenge = req.session.webauthnChallenge;
      if (!challenge) return res.status(400).json({ message: "انتهت صلاحية جلسة المصادقة" });

      const bodyCredID: string = req.body.id;
      const credential = credentials.find((c) => c.credentialID === bodyCredID);
      if (!credential) return res.status(400).json({ message: "البصمة غير موجودة" });

      const rpID = getWebAuthnRpID(req);
      const origin = getWebAuthnOrigin(req);

      const verification = await verifyAuthenticationResponse({
        response: req.body,
        expectedChallenge: challenge,
        expectedOrigin: origin,
        expectedRPID: rpID,
        credential: {
          id: credential.credentialID,
          publicKey: Buffer.from(credential.credentialPublicKey, "base64"),
          counter: credential.counter,
          transports: credential.transports,
        },
      });

      if (!verification.verified) return res.status(400).json({ message: "فشل التحقق من البصمة" });

      // Update counter
      credential.counter = verification.authenticationInfo.newCounter;
      await storage.updateUser(user.id, { webauthnCredentials: credentials });

      clearAttempts(ip);
      req.session.userId = user.id;
      delete req.session.webauthnChallenge;
      await storage.addAuditLog({ action: "login_webauthn", details: `دخول بالبصمة: ${user.name}`, ip, actorId: user.id }).catch(() => {});
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(await buildLoginResponse(user));
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "خطأ في التحقق";
      res.status(400).json({ message: msg });
    }
  });

  // ── Remove WebAuthn credential (user settings) ──
  app.delete("/api/auth/webauthn/credential", requireAuth, async (req, res) => {
    try {
      const { credentialID } = z.object({ credentialID: z.string() }).parse(req.body);
      const user = await storage.getUser(req.session.userId!);
      if (!user) return res.status(404).json({ message: "غير موجود" });
      const credentials: WebAuthnCredential[] = user.webauthnCredentials ?? [];
      const updated: WebAuthnCredential[] = credentials.filter((c) => c.credentialID !== credentialID);
      await storage.updateUser(user.id, { webauthnCredentials: updated });
      res.json({ success: true });
    } catch (err) {
      res.status(400).json({ message: "خطأ" });
    }
  });

  // Verify admin TOTP (2FA)
  app.post("/api/auth/verify-totp", async (req, res) => {
    const ip = getClientIp(req);
    try {
      const { code, totp } = z.object({ code: z.string().min(1), totp: z.string().min(6).max(6) }).parse(req.body);
      const user = await storage.getUserByCode(code.toUpperCase());
      if (!user || (!user.isAdmin && !user.isCoAdmin)) return res.status(401).json({ message: "غير مسموح" });
      const bf = await checkBruteForce(ip);
      if (bf.blocked) return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك.", ipBanned: true });
      // All admins/co-admins share the primary admin's TOTP secret
      const allUsers = await storage.getAllUsers();
      const primaryAdmin = allUsers.filter(u => u.isAdmin).sort((a, b) => a.id - b.id)[0];
      const totpOwnerId = primaryAdmin ? primaryAdmin.id : user.id;
      const secret = await storage.getSetting(totpOwnerId, "admin_totp_secret");
      if (!secret) return res.status(400).json({ message: "2FA غير مُفعَّل" });
      const valid = authenticator.verify({ token: totp, secret });
      if (!valid) {
        const remaining = await recordFailedAttempt(ip);
        await storage.addAuditLog({ action: "totp_failed", details: `محاولة 2FA فاشلة`, ip, actorId: user.id }).catch(() => {});
        if (remaining === 0) return res.status(429).json({ message: "تم حظر عنوان IP الخاص بك.", ipBanned: true });
        return res.status(401).json({ message: "كود التحقق غير صحيح", remaining });
      }
      clearAttempts(ip);
      await storage.addAuditLog({ action: "login_success_2fa", details: `دخول بـ 2FA: ${user.name} (${user.isCoAdmin ? "co-admin" : "admin"})`, ip, actorId: user.id }).catch(() => {});
      req.session.userId = user.id;
      if (user.messageQuota !== -1) await storage.updateUser(user.id, { messageQuota: -1 });
      await new Promise<void>((resolve, reject) => req.session.save(err => err ? reject(err) : resolve()));
      res.json(await buildLoginResponse(await storage.getUser(user.id)));
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.post(api.auth.logout.path, (req, res) => {
    const ip = getClientIp(req);
    const userId = req.session.userId;
    req.session.destroy(() => {});
    if (userId) storage.addAuditLog({ action: "logout", ip, actorId: userId }).catch(() => {});
    res.json({ success: true });
  });

  app.get("/api/account/privacy-status", requireAuth, async (req, res) => {
    const val = await storage.getSetting(req.session.userId!, "privacy_accepted_v2");
    res.json({ accepted: val === "true" });
  });

  app.post("/api/account/accept-privacy", requireAuth, async (req, res) => {
    await storage.setSetting(req.session.userId!, "privacy_accepted_v2", "true");
    res.json({ success: true });
  });

  // ========== ACCOUNT SHARING ROUTES ==========

  app.get("/api/account/share", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const user = await storage.getUser(sessionUserId);
    if (!user || user.primaryUserId) {
      return res.status(403).json({ message: "المشاركون لا يمكنهم إدارة الحساب المشترك" });
    }
    const linked = await storage.getLinkedUsers(sessionUserId);
    res.json(linked.map(u => ({ id: u.id, name: u.name, phoneNumber: u.phoneNumber, accessCode: u.accessCode })));
  });

  app.post("/api/account/share", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const user = await storage.getUser(sessionUserId);
    if (!user || user.primaryUserId) {
      return res.status(403).json({ message: "المشاركون لا يمكنهم إضافة مشاركين جدد" });
    }
    if (user.isAdmin || user.isCoAdmin || user.role !== "user") {
      return res.status(403).json({ message: "المشاركة غير متاحة لهذا النوع من الحسابات" });
    }
    const existing = await storage.getLinkedUsers(sessionUserId);
    if (existing.length >= 3) {
      return res.status(400).json({ message: "لا يمكن إضافة أكثر من 3 مشاركين" });
    }
    const { name, phoneNumber } = z.object({
      name: z.string().min(2).max(50),
      phoneNumber: z.string().min(5).max(20),
    }).parse(req.body);
    const linked = await storage.createLinkedUser(sessionUserId, name, phoneNumber);

    // Send welcome WhatsApp message with access code via Green API (non-blocking)
    let shareWhatsappSent = false;
    try {
      const appUrl = `${req.protocol}://${req.get('host')}`;
      const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
      const msg = `🎉 مرحباً *${name}*!\n\nتمت إضافتك كمشارك في حساب إنفايتنا.\n\n🔑 كود الدخول: *${linked.accessCode}*\n\n🔗 ادخل من هنا:\n${appUrl}/login?code=${linked.accessCode}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم`;
      const greenResult = await sendViaGreenAPI(cleanPhone, msg).catch(() => ({ ok: false, error: "exception" }));
      shareWhatsappSent = greenResult.ok;
      if (!shareWhatsappSent) {
        console.log(`Linked user welcome message could not be sent via Green API to ${cleanPhone}: ${greenResult.error}`);
      }
    } catch (e) {
      console.error("Error sending linked user welcome message:", e);
    }

    res.status(201).json({ id: linked.id, name: linked.name, phoneNumber: linked.phoneNumber, accessCode: linked.accessCode, whatsappSent: shareWhatsappSent });
  });

  app.delete("/api/account/share/:id", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const user = await storage.getUser(sessionUserId);
    if (!user || user.primaryUserId) {
      return res.status(403).json({ message: "غير مصرح" });
    }
    const linkedId = Number(req.params.id);
    await storage.revokeLinkedUser(linkedId, sessionUserId);
    res.json({ success: true });
  });

  app.delete("/api/account/self", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const user = await storage.getUser(userId);
    if (!user) return res.status(404).json({ message: "المستخدم غير موجود" });
    if (user.isAdmin) return res.status(403).json({ message: "لا يمكن حذف حساب المدير" });
    if (user.role === "hall") {
      // Block if hall has active (non-archived) clients — archived clients have no active sessions
      const activeClients = await storage.getHallClients(userId);
      if (activeClients.length > 0) {
        return res.status(403).json({ message: `لا يمكن الحذف — يوجد ${activeClients.length} زبون نشط مرتبط، أرشفهم أولاً` });
      }
      // Block if hall has an active subscription
      if (user.billingStatus === "active") {
        return res.status(403).json({ message: "لا يمكن الحذف أثناء وجود اشتراك نشط — تواصل مع الدعم" });
      }
    }
    // For hall clients: if a discount code was generated, block deletion until balance is exhausted
    if (user.role === "user" && user.parentHallId) {
      const discountCode = await storage.getSetting(userId, "discount_code");
      if (discountCode && user.messageQuota !== -1 && user.messagesSent < user.messageQuota) {
        const remaining = user.messageQuota - user.messagesSent;
        return res.status(403).json({ message: `لا يمكن الحذف — يجب استنفاد رصيد الدعوات بالكامل أولاً (المتبقي: ${remaining} دعوة)` });
      }
    }
    const accounts = await storage.getWhatsappAccounts(userId);
    for (const acc of accounts) {
      try { await waManager.removeSession(acc.id); } catch {}
    }
    await storage.deleteAllUserData(userId);
    req.session.destroy(() => {});
    res.json({ success: true });
  });

  app.get(api.auth.me.path, async (req, res) => {
    if (!req.session.userId) {
      return res.status(401).json({ message: "غير مسجل الدخول" });
    }
    const user = await storage.getUser(req.session.userId);
    if (!user) {
      req.session.destroy(() => {});
      return res.status(401).json({ message: "المستخدم غير موجود" });
    }
    if (user.isSuspended) {
      req.session.destroy(() => {});
      return res.status(403).json({ message: "تم تعليق هذا الحساب. تواصل مع الإدارة." });
    }
    if (user.parentHallId) {
      const existingVenue = await storage.getSetting(user.id, "event_venue_name");
      if (!existingVenue) {
        const venueName = await storage.getSetting(user.parentHallId, "venue_name");
        if (venueName) {
          await storage.setSetting(user.id, "event_venue_name", venueName);
        }
      }
      const hallMapLink = await storage.getSetting(user.parentHallId, "venue_map_link");
      if (hallMapLink) {
        await storage.setSetting(user.id, "event_map_link", hallMapLink);
      }
    }
    const freshUser = await storage.getUser(user.id);
    // For secondary (linked) users, show primary account's quota
    const quotaSource = freshUser!.primaryUserId
      ? await storage.getUser(freshUser!.primaryUserId) ?? freshUser!
      : freshUser!;
    res.json({
      id: freshUser!.id,
      name: freshUser!.name,
      phoneNumber: freshUser!.phoneNumber,
      accessCode: freshUser!.accessCode,
      messageQuota: quotaSource.messageQuota,
      messagesSent: quotaSource.messagesSent,
      messagesRemaining: quotaSource.messageQuota === -1 ? -1 : Math.max(0, quotaSource.messageQuota - quotaSource.messagesSent),
      isAdmin: freshUser!.isAdmin,
      isCoAdmin: freshUser!.isCoAdmin,
      role: freshUser!.role,
      parentHallId: freshUser!.parentHallId,
      isSuspended: freshUser!.isSuspended,
      subscriptionExpiresAt: freshUser!.subscriptionExpiresAt ? freshUser!.subscriptionExpiresAt.toISOString() : null,
      hallPassword: freshUser!.hallPassword ? "set" : null,
      primaryUserId: freshUser!.primaryUserId ?? null,
    });
  });

  // Arab country calling codes (for phone candidate generation)
  const ARAB_CODES = ['966','962','971','965','973','974','968','20','964','961','963','967','218','216','213','212','249','970'];

  // Generate all plausible phone digit variants from raw user input
  function phoneDigitCandidates(raw: string): string[] {
    let digits = raw.replace(/[^0-9]/g, '');
    const set = new Set<string>();
    set.add(digits);
    // 00xxx international prefix → strip 00
    if (digits.startsWith('00')) { const s = digits.substring(2); set.add(s); digits = s; }
    // Saudi-specific shortcuts
    if (/^05\d{8}$/.test(digits)) set.add('966' + digits.substring(1));
    if (/^5\d{8}$/.test(digits))  set.add('966' + digits);
    // Local format with leading 0 (any Arab country): strip 0, try all Arab codes
    if (digits.startsWith('0') && digits.length >= 9) {
      const local = digits.substring(1);
      set.add(local);
      for (const code of ARAB_CODES) set.add(code + local);
    }
    // Short number without leading 0 and without country code (7-10 digits): try all Arab codes
    if (!digits.startsWith('0') && digits.length >= 7 && digits.length <= 10) {
      for (const code of ARAB_CODES) set.add(code + digits);
    }
    return Array.from(set);
  }

  app.post(api.auth.recoverCode.path, async (req, res) => {
    try {
      const { phoneNumber } = z.object({ phoneNumber: z.string().min(1) }).parse(req.body);
      const candidates = phoneDigitCandidates(phoneNumber);
      // Collect all matching accounts across all candidate formats
      const allMatches: Awaited<ReturnType<typeof storage.getUserByPhone>>[] = [];
      for (const c of candidates) {
        const matches = await storage.getUsersByPhone(c);
        allMatches.push(...matches);
      }
      if (allMatches.length === 0) {
        return res.status(404).json({ message: "لم يتم العثور على حساب بهذا الرقم" });
      }
      // If any admin account shares this phone, always return the admin code
      const user = allMatches.find(u => u.isAdmin) ?? allMatches[0];
      const recoverEnabled = await storage.getSystemMessageEnabled("recover_code");
      if (!recoverEnabled) {
        return res.status(503).json({ message: "خاصية استعادة الكود معطّلة حالياً. تواصل مع المدير." });
      }
      const loginUrl = `${req.protocol}://${req.get('host')}/login?code=${user.accessCode}`;
      const msg = `🔑 كود الدخول إلى إنفايتنا نظام إدارة الدعوات:\n\n*${user.accessCode}*\n\nأو ادخل من الرابط:\n${loginUrl}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم`;
      let sent = false;
      try {
        sent = await sendViaAnySystem(user.phoneNumber, msg, undefined, undefined, true);
      } catch (e) {
        console.error("Failed to send recovery via WhatsApp:", e);
      }
      if (!sent) {
        return res.status(503).json({ message: "لا يوجد أي طريقة إرسال متاحة حالياً. حاول لاحقاً." });
      }
      res.json({ success: true, message: "تم إرسال كود الدخول على الواتساب" });
    } catch (err) {
      res.status(400).json({ message: "حدث خطأ. تأكد من الرقم وحاول مجدداً." });
    }
  });

  app.post("/api/demo-invite", async (req, res) => {
    try {
      const { phoneNumber } = z.object({ phoneNumber: z.string().min(5) }).parse(req.body);
      const normalizedDemo = phoneNumber.startsWith('+') ? phoneNumber : '+' + phoneNumber;
      const demoEnabled = await storage.getSystemMessageEnabled("demo_invite");
      if (!demoEnabled) {
        return res.status(503).json({ message: "الدعوة التجريبية معطّلة حالياً." });
      }
      if (!(await isSystemSendable())) {
        return res.status(503).json({ message: "لا يوجد أي طريقة إرسال متاحة. فعّل Green API أو اربط رقم المشروع." });
      }
      const appUrl = `${req.protocol}://${req.get('host')}`;
      const msg = `✨ *دعوة تجريبية من إنفايتنا* ✨\n\nمرحباً!\n\nهذه دعوة تجريبية لعرض طريقة عمل منصة إنفايتنا لإدارة الدعوات.\n\n📅 التاريخ: *٢٥ سبتمبر ٢٠٢٥*\n🕐 الوقت: *٨:٠٠ مساءً*\n📍 المكان: *قاعة الاحتفالات الكبرى*\n\nلمشاهدة الدعوة وتأكيد الحضور:\n🔗 ${appUrl}/i/demo\n\nشكراً لاهتمامكم بإنفايتنا!`;
      const sent = await sendViaAnySystem(normalizedDemo, msg, "/uploads/demo-invitation.mp4", appUrl);
      if (!sent) {
        return res.status(500).json({ message: "فشل إرسال الدعوة التجريبية" });
      }
      res.json({ success: true, message: "تم إرسال الدعوة التجريبية بنجاح" });
    } catch (err: any) {
      console.error("Failed to send demo invite:", err);
      res.status(500).json({ message: err?.message || "فشل إرسال الدعوة التجريبية" });
    }
  });

  // ========== GUEST ROUTES (user-scoped) ==========

  app.get(api.guests.list.path, requireAuth, async (req, res) => {
    const guests = await storage.getGuests(req.session.userId!);
    res.json(guests);
  });

  app.get("/api/guests/partner", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const sessionUser = await storage.getUser(sessionUserId);
    if (!sessionUser) return res.json({ guests: [], partnerName: null, partners: [] });

    if (sessionUser.primaryUserId) {
      // Secondary user: return primary user's guests as a single partner
      const primaryUser = await storage.getUser(sessionUser.primaryUserId);
      if (!primaryUser) return res.json({ guests: [], partnerName: null, partners: [] });
      const guests = await storage.getGuests(sessionUser.primaryUserId);
      return res.json({
        guests,
        partnerName: primaryUser.name,
        partners: [{ userId: primaryUser.id, name: primaryUser.name, guests }],
      });
    } else {
      // Primary user: return each linked secondary user's guests separately
      const linked = await storage.getLinkedUsers(sessionUserId);
      if (!linked.length) return res.json({ guests: [], partnerName: null, partners: [] });
      const allGuests: import("@shared/schema").Guest[] = [];
      const partners: { userId: number; name: string; guests: import("@shared/schema").Guest[] }[] = [];
      for (const lu of linked) {
        const luGuests = await storage.getGuests(lu.id);
        allGuests.push(...luGuests);
        partners.push({ userId: lu.id, name: lu.name, guests: luGuests });
      }
      return res.json({
        guests: allGuests,
        partnerName: linked.map(l => l.name).join("، "),
        partners,
      });
    }
  });

  app.get(api.guests.get.path, requireAuth, async (req, res) => {
    const guest = await storage.getGuest(Number(req.params.id));
    if (!guest || guest.userId !== req.session.userId!) {
      return res.status(404).json({ message: 'Guest not found' });
    }
    res.json(guest);
  });

  app.get("/api/invite/demo", (_req, res) => {
    res.json({
      guest: { id: 0, name: "محمد وسارة", status: "confirmed", qrToken: "DEMO-INVITNA-2025" },
      event: { event_date: "٢٥ سبتمبر ٢٠٢٥", event_time: "٨:٠٠ مساءً", event_venue_name: "قاعة الاحتفالات الكبرى" },
      expired: false,
      expiryDate: null,
      media: { url: "/uploads/demo-invitation.mp4", type: "video" },
      includeQr: true,
      isDemo: true,
    });
  });

  app.get("/api/invite/:id", async (req, res) => {
    const param = req.params.id;
    const numericId = Number(param);
    const guest = (!isNaN(numericId) && numericId > 0)
      ? await storage.getGuest(numericId)
      : await storage.getGuestBySlug(param);
    if (!guest) {
      return res.status(404).json({ message: "الدعوة غير موجودة" });
    }
    // Resolve to primary user's shared event details, but use secondary user's personal fields
    const guestOwner = await storage.getUser(guest.userId);
    const effectiveEventUserId = guestOwner?.primaryUserId ?? guest.userId;
    const event = await getMergedEventDetails(guest.userId, effectiveEventUserId);
    let expired = false;
    let expiryDate: string | null = null;
    if (guest.sentAt) {
      const expiry = new Date(guest.sentAt);
      expiry.setDate(expiry.getDate() + 30);
      expiryDate = expiry.toISOString();
      expired = new Date() > expiry;
    }
    // Try guest owner's media first, fall back to primary user's media
    let mediaUrl = await storage.getSetting(guest.userId, "global_media_url");
    let mediaType = await storage.getSetting(guest.userId, "global_media_type");
    if (!mediaUrl && guestOwner?.primaryUserId) {
      mediaUrl = await storage.getSetting(guestOwner.primaryUserId, "global_media_url");
      mediaType = await storage.getSetting(guestOwner.primaryUserId, "global_media_type");
    }
    const settingQr = event?.include_qr === "true";
    // Fallback: scanner account exists means QR is enabled even if setting is missing
    let includeQr = settingQr;
    if (!settingQr) {
      const guestScanners = await storage.getScannersForUser(effectiveEventUserId);
      includeQr = guestScanners.length > 0;
      if (includeQr) {
        await storage.setSetting(effectiveEventUserId, "include_qr", "true");
      }
    }
    res.json({
      guest: { ...guest, qrToken: includeQr ? guest.qrToken : null },
      event,
      expired,
      expiryDate,
      media: mediaUrl ? { url: mediaUrl, type: mediaType } : null,
      includeQr,
    });
  });

  // Public RSVP endpoint — no auth required, guest updates own status via invite link
  app.post("/api/invite/:id/rsvp", async (req, res) => {
    try {
      const param = req.params.id;
      const numericId = Number(param);
      const { status } = z.object({ status: z.enum(["confirmed", "declined", "pending"]) }).parse(req.body);
      const guest = (!isNaN(numericId) && numericId > 0)
        ? await storage.getGuest(numericId)
        : await storage.getGuestBySlug(param);
      if (!guest) return res.status(404).json({ message: "الدعوة غير موجودة" });
      const guestId = guest.id;
      // Verify expiry — same logic as GET
      if (guest.sentAt) {
        const expiry = new Date(guest.sentAt);
        expiry.setDate(expiry.getDate() + 30);
        if (new Date() > expiry) return res.status(403).json({ message: "انتهت مهلة الرد" });
      }
      const updated = await storage.updateGuest(guestId, { status });
      if (!updated) return res.status(404).json({ message: "الدعوة غير موجودة" });
      res.json({ success: true, status: updated.status });
    } catch (err) {
      if (err instanceof z.ZodError) return res.status(400).json({ message: "حالة غير صالحة" });
      throw err;
    }
  });

  app.post(api.guests.create.path, requireAuth, async (req, res) => {
    try {
      const input = api.guests.create.input.parse(req.body);
      if (input.phoneNumber) {
        input.phoneNumber = normalizeGuestPhone(input.phoneNumber);
        if (!input.phoneNumber.startsWith('+')) {
          return res.status(400).json({ message: "يجب إدخال رقم صحيح مع مفتاح الدولة" });
        }
      }
      const guest = await storage.createGuest({ ...input, userId: req.session.userId! });
      res.status(201).json(guest);
    } catch (err) {
      if (err instanceof z.ZodError) {
        return res.status(400).json({ message: err.errors[0].message, field: err.errors[0].path.join('.') });
      }
      throw err;
    }
  });

  app.put(api.guests.update.path, requireAuth, async (req, res) => {
    try {
      const input = api.guests.update.input.parse(req.body);
      const existing = await storage.getGuest(Number(req.params.id));
      if (!existing || existing.userId !== req.session.userId!) {
        return res.status(404).json({ message: "Guest not found" });
      }
      const guest = await storage.updateGuest(Number(req.params.id), input);
      if (!guest) return res.status(404).json({ message: "Guest not found" });
      res.json(guest);
    } catch (err) {
      if (err instanceof z.ZodError) {
        return res.status(400).json({ message: err.errors[0].message, field: err.errors[0].path.join('.') });
      }
      throw err;
    }
  });

  app.delete(api.guests.delete.path, requireAuth, async (req, res) => {
    const guest = await storage.getGuest(Number(req.params.id));
    if (!guest || guest.userId !== req.session.userId!) {
      return res.status(404).json({ message: "الضيف غير موجود" });
    }
    await storage.deleteGuest(Number(req.params.id));
    res.status(204).send();
  });

  app.post("/api/guests/bulk-delete", requireAuth, async (req, res) => {
    try {
      const { ids } = z.object({ ids: z.array(z.number()).min(1).max(500) }).parse(req.body);
      await storage.bulkDeleteGuests(ids, req.session.userId!);
      res.json({ deleted: ids.length });
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.patch("/api/guests/:id/name", requireAuth, async (req, res) => {
    try {
      const { name } = z.object({ name: z.string().min(1).max(100) }).parse(req.body);
      const updated = await storage.updateGuestName(Number(req.params.id), name, req.session.userId!);
      if (!updated) return res.status(404).json({ message: "الضيف غير موجود" });
      res.json(updated);
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.patch("/api/guests/:id/phone", requireAuth, async (req, res) => {
    try {
      const { phone } = z.object({ phone: z.string().min(1).max(25) }).parse(req.body);
      const normalized = normalizeGuestPhone(phone);
      if (!normalized.startsWith('+')) {
        return res.status(400).json({ message: "يجب إدخال رقم صحيح مع مفتاح الدولة" });
      }
      const updated = await storage.updateGuestPhone(Number(req.params.id), normalized, req.session.userId!);
      if (!updated) return res.status(404).json({ message: "الضيف غير موجود" });
      res.json(updated);
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.patch("/api/guests/:id/guest-count", requireAuth, async (req, res) => {
    try {
      const { guestCount, additionalNames } = z.object({
        guestCount: z.number().int().min(1).max(2),
        additionalNames: z.string().max(200).optional(),
      }).parse(req.body);
      const guestId = Number(req.params.id);
      const guest = await storage.getGuest(guestId);
      if (!guest || guest.userId !== req.session.userId!) return res.status(404).json({ message: "الضيف غير موجود" });
      const updated = await storage.updateGuest(guestId, { guestCount, additionalNames: additionalNames ?? null } as any);
      res.json(updated);
    } catch (e: any) {
      res.status(400).json({ message: e.message || "بيانات غير صالحة" });
    }
  });

  app.post(api.guests.sendInvite.path, requireAuth, async (req, res) => {
    const dataUserId = req.effectiveUserId!;
    const settingsUserId = req.session.userId!;
    const user = await storage.getUser(dataUserId);
    if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });

    // Check if single-send feature is enabled by admin
    const allAdminUsers = await storage.getAllUsers();
    const adminUser = allAdminUsers.find(u => u.isAdmin);
    if (adminUser) {
      const singleSendSetting = await storage.getSetting(adminUser.id, "single_send_enabled");
      if (singleSendSetting === "false") {
        return res.status(403).json({ message: "ميزة الإرسال الفردي غير متاحة حالياً." });
      }
    }

    const remaining = user.messageQuota === -1 ? Infinity : (user.messageQuota - user.messagesSent);
    if (remaining <= 0) {
      return res.status(403).json({ message: "رصيد الرسائل منتهي. يرجى شراء باقة جديدة." });
    }

    const guest = await storage.getGuest(Number(req.params.id));
    if (!guest || guest.userId !== req.session.userId!) {
      return res.status(404).json({ message: "الضيف غير موجود" });
    }

    const accountId = req.body?.accountId ? Number(req.body.accountId) : undefined;
    const useSystem = req.body?.useSystem === true;
    
    const inviteLink = `${req.protocol}://${req.get('host')}/i/${guest.inviteSlug || guest.id}`;
    const templateId = await storage.getSetting(settingsUserId, "message_template_id") || "formal";
    const message = await buildMessage(templateId, dataUserId, guest.name, inviteLink, false, settingsUserId, settingsUserId);
    
    let mediaUrl = await storage.getSetting(settingsUserId, "global_media_url");
    // Fall back to primary user's media if secondary user has none
    if (!mediaUrl && settingsUserId !== dataUserId) {
      mediaUrl = await storage.getSetting(dataUserId, "global_media_url");
    }
    
    const appBaseUrl = `${req.protocol}://${req.get('host')}`;
    try {
      if (useSystem) {
        const sent = await sendViaAnySystem(guest.phoneNumber, message, mediaUrl || undefined, mediaUrl ? appBaseUrl : undefined);
        if (!sent) return res.status(503).json({ message: "لا يوجد أي طريقة إرسال متاحة." });
      } else {
        if (accountId) {
          const acct = await storage.getWhatsappAccount(accountId);
          if (!acct || acct.userId !== settingsUserId) {
            return res.status(403).json({ message: "لا يحق لك استخدام هذا الحساب" });
          }
        }
        let waSession = accountId ? waManager.getSession(accountId) : waManager.getConnectedSessionForUser(settingsUserId);
        if (!waSession || waSession.status !== 'connected') {
          return res.status(400).json({ message: "لا يوجد حساب واتساب متصل. اربط رقمك أو استخدم رقم المشروع." });
        }
        await waSession.sendMessage(guest.phoneNumber, message, mediaUrl);
      }
      await storage.incrementMessagesSent(dataUserId);
      await storage.markGuestSent(guest.id);
      res.json({ success: true, message: "تم إرسال الدعوة بنجاح" });
    } catch (error) {
      console.error("Failed to send invite:", error);
      res.status(500).json({ message: "فشل إرسال الدعوة" });
    }
  });

  // ---- Manual message reveal: generate pre-built message for a guest ----
  // First open of an unsent guest = deducts one invite from quota + marks sent.
  // Subsequent opens (or for already-sent guests) = free.
  app.post("/api/guests/:id/reveal-message", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!; // quota user (primary for secondary users)
    const sessionUserId = req.session.userId!;
    const user = await storage.getUser(userId);
    if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });

    const guest = await storage.getGuest(Number(req.params.id));
    if (!guest || guest.userId !== sessionUserId) {
      return res.status(404).json({ message: "الضيف غير موجود" });
    }

    const alreadySent = !!guest.sentAt;

    if (!alreadySent) {
      // Validate mandatory event fields before charging quota
      const eventDetails = await getMergedEventDetails(sessionUserId, userId);
      const missingFields: string[] = [];
      if (!eventDetails?.event_date) missingFields.push("تاريخ المناسبة");
      if (!eventDetails?.event_time) missingFields.push("وقت المناسبة");
      if (!eventDetails?.event_venue_name) missingFields.push("اسم المكان");
      if (!eventDetails?.event_sender) missingFields.push("اسم المُرسِل/الداعي");
      if (!eventDetails?.event_occasion) missingFields.push("اسم المناسبة");
      if (missingFields.length > 0) {
        return res.status(422).json({ message: `يرجى إكمال تفاصيل المناسبة قبل الإرسال: ${missingFields.join("، ")}` });
      }

      // Check quota
      const remaining = user.messageQuota === -1 ? Infinity : (user.messageQuota - user.messagesSent);
      if (remaining <= 0) {
        return res.status(403).json({ message: "رصيد الرسائل منتهي. يرجى شراء باقة جديدة." });
      }
    }

    const inviteLink = `${req.protocol}://${req.get("host")}/i/${guest.inviteSlug || guest.id}`;
    const revealSettingsUserId = req.session.userId!;
    const templateId = await storage.getSetting(revealSettingsUserId, "message_template_id") || "formal";
    const message = await buildMessage(templateId, userId, guest.name, inviteLink, false, revealSettingsUserId, revealSettingsUserId);

    if (!alreadySent) {
      await storage.markGuestSent(guest.id);
      await storage.incrementMessagesSent(userId);
    }

    res.json({ message, alreadySent });
  });

  app.post("/api/guests/bulk", requireAuth, async (req, res) => {
    try {
      const { guests: bulkGuests } = z.object({
        guests: z.array(z.object({
          name: z.string(),
          phoneNumber: z.string()
        }))
      }).parse(req.body);

      // Normalize phone numbers before validation
      const normalizedGuests = bulkGuests.map(g => ({ ...g, phoneNumber: normalizeGuestPhone(g.phoneNumber) }));
      const invalidPhones = normalizedGuests.filter(g => !g.phoneNumber.startsWith('+'));
      if (invalidPhones.length > 0) {
        return res.status(400).json({ message: "يجب أن تبدأ جميع الأرقام بعلامة + ومفتاح الدولة" });
      }

      const userId = req.session.userId!;
      const created = [];
      for (const g of normalizedGuests) {
        created.push(await storage.createGuest({ ...g, status: "pending", userId }));
      }
      // Auto-resume if a send is active/paused — picks up newly added guests
      const autoResumeResult = await autoResumeQueue(userId).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));
      res.status(201).json({ guests: created, autoResumed: autoResumeResult.resumed, resumedCount: autoResumeResult.count, resumeSpreadHours: autoResumeResult.spreadHours });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.post("/api/guests/import-vcf", requireAuth, upload.single('vcf'), async (req, res) => {
    try {
      if (!req.file) {
        return res.status(400).json({ message: "لم يتم رفع ملف" });
      }

      let vcfContent: string;
      try {
        vcfContent = fs.readFileSync(req.file.path, 'utf-8');
      } finally {
        try { fs.unlinkSync(req.file.path); } catch {}
      }

      const unfoldedContent = vcfContent.replace(/\r\n[ \t]/g, '').replace(/\r?\n[ \t]/g, '');

      const contacts: { name: string; phoneNumber: string }[] = [];
      const vcards = unfoldedContent.split('END:VCARD');

      for (const vcard of vcards) {
        if (!vcard.includes('BEGIN:VCARD')) continue;

        let name = '';
        let phone = '';

        const fnMatch = vcard.match(/FN[;:]([^\r\n]+)/);
        if (fnMatch) {
          let fnVal = fnMatch[1].replace(/^.*:/, '').trim();
          fnVal = fnVal.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
          name = fnVal;
        }
        if (!name) {
          const nMatch = vcard.match(/\nN[;:]([^\r\n]+)/);
          if (nMatch) {
            let nVal = nMatch[1].replace(/^.*:/, '');
            nVal = nVal.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
            const parts = nVal.split(';').filter(Boolean);
            name = parts.reverse().join(' ').trim();
          }
        }

        const telMatches = vcard.match(/TEL[^:\r\n]*:([^\r\n]+)/g);
        if (telMatches) {
          const cellMatch = telMatches.find(t => /CELL/i.test(t) || /PREF/i.test(t));
          const telLine = cellMatch || telMatches[0];
          phone = telLine.replace(/TEL[^:]*:/, '').replace(/[\s\-\(\)]/g, '').trim();
        }

        if (name && phone) {
          if (!phone.startsWith('+') && !phone.startsWith('0')) {
            phone = '+' + phone;
          }
          contacts.push({ name, phoneNumber: phone });
        }
      }

      if (contacts.length === 0) {
        return res.status(400).json({ message: "لم يتم العثور على جهات اتصال صالحة في الملف" });
      }

      const vcfUserId = req.session.userId!;
      const created = [];
      for (const c of contacts) {
        created.push(await storage.createGuest({ ...c, status: "pending", userId: vcfUserId }));
      }
      // Auto-resume if a send is active/paused — picks up newly added guests
      const vcfAutoResume = await autoResumeQueue(vcfUserId).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));
      res.status(201).json({ count: created.length, guests: created, autoResumed: vcfAutoResume.resumed, resumedCount: vcfAutoResume.count, resumeSpreadHours: vcfAutoResume.spreadHours });
    } catch (err) {
      res.status(400).json({ message: "خطأ في معالجة الملف" });
    }
  });

  app.get("/api/guests/bulk-send/progress", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const stats = await storage.getSendQueueStats(userId);
    const spreadHoursVal = parseInt(await storage.getSetting(userId, "bulk_send_spread_hours") || "0");
    return res.json({
      active: stats.pending > 0,
      paused: stats.paused > 0,
      sent: stats.sent,
      total: stats.pending + stats.paused + stats.sent + stats.failed,
      failed: stats.failed,
      spreadHours: spreadHoursVal,
    });
  });

  app.post("/api/bulk-send/stop", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    // Pause (not cancel) — preserves paused entries for manual resume
    await storage.pauseUserSendQueue(userId);
    res.json({ ok: true });
  });

  // Helper: pause queue then rebuild it with fresh template + any new unsent guests
  async function autoResumeQueue(userId: number): Promise<{ resumed: boolean; count: number; intervalMs: number; spreadHours: number }> {
    const stats = await storage.getSendQueueStats(userId);
    if (stats.pending === 0 && stats.paused === 0) return { resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 };

    // Pause current pending entries
    await storage.pauseUserSendQueue(userId);

    // Collect paused guest IDs + config
    const pausedInfo = await storage.resumeUserSendQueue(userId);
    if (!pausedInfo.baseUrl) return { resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 };

    // Collect newly added unsent guests not already in the paused list
    const pausedSet = new Set(pausedInfo.guestIds);
    const allGuests = await storage.getGuests(userId);
    const newUnsent = allGuests
      .filter(g => !g.sentAt && !pausedSet.has(g.id))
      .map(g => g.id);

    const mergedIds = [...pausedInfo.guestIds, ...newUnsent];
    if (mergedIds.length === 0) return { resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 };

    // Use settingsUserId from paused entries so secondary user's personal template is preserved
    const settingsUid = pausedInfo.settingsUserId ?? userId;
    const freshTemplateId = await storage.getSetting(settingsUid, "message_template_id") || "formal";

    const now = Date.now();
    const entries: import("@shared/schema").InsertSendQueueEntry[] = mergedIds.map((guestId, idx) => ({
      userId,
      guestId,
      scheduledAt: new Date(now + idx * pausedInfo.intervalMs),
      status: 'pending',
      templateId: freshTemplateId,
      accountId: pausedInfo.accountId,
      useSystem: pausedInfo.useSystem,
      warmupActive: pausedInfo.warmupActive,
      attempts: 0,
      lastError: null,
      baseUrl: pausedInfo.baseUrl,
      intervalMs: pausedInfo.intervalMs,
      settingsUserId: pausedInfo.settingsUserId,
    }));

    await storage.createSendQueueBatch(entries);

    // Note: batch_start_at is NOT updated on resume — we preserve the original batch start
    // so getSendQueueStats keeps counting all sent/failed entries from this send run

    // Calculate effective spread hours from intervalMs for polling timeout
    const spreadHours = !pausedInfo.useSystem
      ? Math.ceil((mergedIds.length * pausedInfo.intervalMs) / (1000 * 3600))
      : 0;

    console.log(`[Queue] Auto-resumed for user ${userId}: ${entries.length} entries (${pausedInfo.guestIds.length} paused + ${newUnsent.length} new), interval=${Math.round(pausedInfo.intervalMs/1000)}s`);
    return { resumed: true, count: entries.length, intervalMs: pausedInfo.intervalMs, spreadHours };
  }

  app.post("/api/bulk-send/resume", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const result = await autoResumeQueue(userId);
    if (!result.resumed) return res.status(400).json({ message: "لا توجد بيانات لاستئناف الإرسال" });
    res.json({ ok: true, count: result.count, spreadHours: result.spreadHours, intervalMs: result.intervalMs });
  });

  app.post("/api/guests/bulk-send", requireAuth, async (req, res) => {
    const userId = req.session.userId!;        // guest owner (for IDOR + queue ownership)
    const quotaUserId = req.effectiveUserId!;  // quota + event details (primary for secondary users)
    const settingsUserId = req.session.userId!;

    // Mandatory event details validation — merge personal (sender/occasion) from session user
    const eventDetails = await getMergedEventDetails(userId, quotaUserId);
    const missingFields: string[] = [];
    if (!eventDetails?.event_date) missingFields.push("تاريخ المناسبة");
    if (!eventDetails?.event_time) missingFields.push("وقت المناسبة");
    if (!eventDetails?.event_venue_name) missingFields.push("اسم المكان");
    if (!eventDetails?.event_sender) missingFields.push("اسم المُرسِل/الداعي");
    if (!eventDetails?.event_occasion) missingFields.push("اسم المناسبة");
    if (missingFields.length > 0) {
      return res.status(422).json({ message: `يرجى إكمال تفاصيل المناسبة قبل الإرسال: ${missingFields.join("، ")}` });
    }

    try {
      const user = await storage.getUser(quotaUserId);
      if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });

      const parsed = z.object({
        ids: z.array(z.number()),
        accountId: z.number().optional(),
        useSystem: z.boolean().optional(),
        spread24h: z.boolean().optional(),
        spreadHours: z.number().int().min(0).max(168).optional(),
        warmupOverride: z.boolean().optional(),
      }).parse(req.body);

      const { ids, accountId, useSystem, warmupOverride } = parsed;
      const userRequestedSpreadHours = parsed.spreadHours || 0;

      const remaining = user.messageQuota === -1 ? Infinity : (user.messageQuota - user.messagesSent);
      if (remaining <= 0) return res.status(403).json({ message: "رصيد الرسائل منتهي" });

      let finalUseSystem = !!useSystem;
      let finalAccountId: number | undefined = accountId;

      if (useSystem) {
        const canSend = await isSystemSendable();
        if (!canSend) {
          const personalSession = waManager.getConnectedSessionForUser(settingsUserId);
          if (!personalSession) {
            return res.status(400).json({ message: "لا يوجد أي طريقة إرسال متاحة. فعّل Green API أو اربط رقم المشروع أو رقمك الشخصي." });
          }
          finalUseSystem = false;
          finalAccountId = personalSession.id;
        }
      } else {
        if (accountId) {
          const acct = await storage.getWhatsappAccount(accountId);
          if (!acct || acct.userId !== settingsUserId) {
            return res.status(403).json({ message: "لا يحق لك استخدام هذا الحساب" });
          }
        }
        const waSession = accountId ? waManager.getSession(accountId) : waManager.getConnectedSessionForUser(settingsUserId);
        if (!waSession || waSession.status !== 'connected') {
          return res.status(400).json({ message: "لا يوجد حساب واتساب متصل. اربط رقمك أو استخدم رقم المشروع." });
        }
        finalAccountId = waSession.id;
      }

      // IDOR prevention: only allow IDs that belong to the current user
      const userGuests = await storage.getGuests(userId);
      const ownedGuestIds = new Set(userGuests.map(g => g.id));
      const validatedIds = ids.filter((id: number) => ownedGuestIds.has(id));

      let actualIds = validatedIds.slice(0, remaining === Infinity ? validatedIds.length : (remaining as number));
      let warmupActive = false;
      const WARMUP_DAILY_LIMIT = 10;

      if (!finalUseSystem && warmupOverride === true) {
        const todayKey = new Date().toISOString().split('T')[0];
        const warmupDateStr = await storage.getSetting(userId, "warmup_sent_date");
        let warmupSentToday = 0;
        if (warmupDateStr === todayKey) {
          warmupSentToday = parseInt(await storage.getSetting(userId, "warmup_sent_count") || "0");
        } else {
          await storage.setSetting(userId, "warmup_sent_date", todayKey);
          await storage.setSetting(userId, "warmup_sent_count", "0");
        }
        const remainingWarmupQuota = WARMUP_DAILY_LIMIT - warmupSentToday;
        if (remainingWarmupQuota <= 0) {
          return res.status(429).json({ message: `اكتملت الحصة اليومية لوضع الإحماء (${WARMUP_DAILY_LIMIT} رسائل). حاول غداً.`, warmupLimit: true });
        }
        actualIds = actualIds.slice(0, remainingWarmupQuota);
        warmupActive = true;
      }

      const actualTotal = actualIds.length;
      const appBaseUrl = `${req.protocol}://${req.get('host')}`;
      const templateId = await storage.getSetting(settingsUserId, "message_template_id") || "formal";
      const templateId2 = !finalUseSystem ? (await storage.getSetting(settingsUserId, "message_template_id_2") || null) : null;

      // Server-side guard: personal mode requires two templates
      if (!finalUseSystem && !templateId2) {
        return res.status(422).json({ message: "طريقة الإرسال الشخصي تتطلب اختيار قالبَين. يرجى تحديد القالب الثاني قبل الإرسال." });
      }

      // Compute effective spread hours
      let adminInviteHours = 24;
      try {
        const adminUser = await storage.getAdminUser();
        if (adminUser) {
          const h = parseInt(await storage.getSetting(adminUser.id, "admin_delay_user_invite_hours") || "24");
          adminInviteHours = isNaN(h) ? 24 : h;
        }
      } catch {}
      const effectiveSpreadHours = !finalUseSystem
        ? Math.max(adminInviteHours, userRequestedSpreadHours > 0 ? userRequestedSpreadHours : 0, 1)
        : 0;

      // Per-message interval (ms)
      let perMsgMs: number;
      if (finalUseSystem) {
        perMsgMs = waManager.getSystemDelay();
      } else {
        perMsgMs = Math.max(60000, Math.floor(effectiveSpreadHours * 60 * 60 * 1000 / Math.max(actualTotal, 1)));
      }

      // Clear existing queue and create new batch
      await storage.clearUserSendQueue(userId);
      const now = Date.now();
      const queueEntries: import("@shared/schema").InsertSendQueueEntry[] = actualIds.map((guestId, idx) => {
        const assignedTemplate = (!finalUseSystem && templateId2)
          ? (idx % 2 === 0 ? templateId : templateId2)
          : templateId;
        return {
        userId,
        guestId,
        scheduledAt: new Date(now + idx * perMsgMs),
        status: 'pending',
        templateId: assignedTemplate,
        accountId: finalAccountId ?? null,
        useSystem: finalUseSystem,
        warmupActive,
        attempts: 0,
        lastError: null,
        baseUrl: appBaseUrl,
        intervalMs: perMsgMs,
        settingsUserId: settingsUserId !== userId ? settingsUserId : null,
        };
      });
      await storage.createSendQueueBatch(queueEntries);

      // Persist spread hours and batch start time for the progress endpoint and stats continuity
      await storage.setSetting(userId, "bulk_send_spread_hours", String(effectiveSpreadHours));
      await storage.setSetting(userId, "send_queue_batch_start", new Date().toISOString());

      console.log(`[Queue] Created ${actualTotal} entries for user ${userId}, perMsg=${Math.round(perMsgMs/1000)}s, spread=${effectiveSpreadHours}h, system=${finalUseSystem}`);
      return res.json({ queued: true, count: actualTotal, delay: perMsgMs, effectiveSpreadHours, warmupActive });
    } catch (e: any) {
      return res.status(400).json({ message: e?.message || "بيانات غير صالحة" });
    }
  });

  // ========== MESSAGE TEMPLATE ROUTES ==========

  app.get("/api/message-templates", requireAuth, async (_req, res) => {
    res.json(MESSAGE_TEMPLATES.map(t => ({ id: t.id, name: t.name, template: t.template })));
  });

  app.get("/api/message-template/selected", requireAuth, async (req, res) => {
    const uid = req.session.userId!;
    const selected = await storage.getSetting(uid, "message_template_id");
    const selected2 = await storage.getSetting(uid, "message_template_id_2");
    res.json({ templateId: selected || "formal", templateId2: selected2 || null });
  });

  app.put("/api/message-template/selected", requireAuth, async (req, res) => {
    const body = z.object({ templateId: z.string().optional(), templateId2: z.string().optional() }).parse(req.body);
    const VALID_IDS = MESSAGE_TEMPLATES.map(t => t.id);
    const uid = req.session.userId!;

    if (body.templateId !== undefined) {
      const templateId = body.templateId;
      if (templateId === "custom") {
        const between = await storage.getSetting(uid, "custom_msg_between");
        if (!between || !between.trim()) return res.status(400).json({ message: "لازم تكتب وسط الرسالة أول" });
      } else if (!VALID_IDS.includes(templateId)) {
        return res.status(400).json({ message: "قالب غير صالح" });
      }
      await storage.setSetting(uid, "message_template_id", templateId);
    }

    if (body.templateId2 !== undefined) {
      const templateId2 = body.templateId2;
      if (templateId2 === "custom2") {
        const between2 = await storage.getSetting(uid, "custom_msg_between_2");
        if (!between2 || !between2.trim()) return res.status(400).json({ message: "لازم تكتب وسط الرسالة الثانية أول" });
      } else if (!VALID_IDS.includes(templateId2)) {
        return res.status(400).json({ message: "القالب الثاني غير صالح" });
      }
      await storage.setSetting(uid, "message_template_id_2", templateId2);
    }

    const tmplAutoResume = await autoResumeQueue(uid).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));
    res.json({ success: true, autoResumed: tmplAutoResume.resumed, resumedCount: tmplAutoResume.count, resumeSpreadHours: tmplAutoResume.spreadHours });
  });

  app.get("/api/message-template/custom", requireAuth, async (req, res) => {
    const uid = req.session.userId!;
    const beforeName = await storage.getSetting(uid, "custom_msg_before_name") || "";
    const between = await storage.getSetting(uid, "custom_msg_between") || "";
    const afterLink = await storage.getSetting(uid, "custom_msg_after_link") || "";
    res.json({ beforeName, between, afterLink });
  });

  app.put("/api/message-template/custom", requireAuth, async (req, res) => {
    const { beforeName, between, afterLink } = z.object({
      beforeName: z.string(),
      between: z.string().min(1, "لازم تكتب وسط الرسالة"),
      afterLink: z.string(),
    }).parse(req.body);
    const customTmplUserId = req.session.userId!;
    await storage.setSetting(customTmplUserId, "custom_msg_before_name", beforeName);
    await storage.setSetting(customTmplUserId, "custom_msg_between", between);
    await storage.setSetting(customTmplUserId, "custom_msg_after_link", afterLink);
    const customTmplAutoResume = await autoResumeQueue(req.session.userId!).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));
    res.json({ success: true, autoResumed: customTmplAutoResume.resumed, resumedCount: customTmplAutoResume.count, resumeSpreadHours: customTmplAutoResume.spreadHours });
  });

  app.get("/api/message-template/custom2", requireAuth, async (req, res) => {
    const uid = req.session.userId!;
    const beforeName = await storage.getSetting(uid, "custom_msg_before_name_2") || "";
    const between = await storage.getSetting(uid, "custom_msg_between_2") || "";
    const afterLink = await storage.getSetting(uid, "custom_msg_after_link_2") || "";
    res.json({ beforeName, between, afterLink });
  });

  app.put("/api/message-template/custom2", requireAuth, async (req, res) => {
    const { beforeName, between, afterLink } = z.object({
      beforeName: z.string(),
      between: z.string().min(1, "لازم تكتب وسط الرسالة"),
      afterLink: z.string(),
    }).parse(req.body);
    const uid = req.session.userId!;
    await storage.setSetting(uid, "custom_msg_before_name_2", beforeName);
    await storage.setSetting(uid, "custom_msg_between_2", between);
    await storage.setSetting(uid, "custom_msg_after_link_2", afterLink);
    const autoResume = await autoResumeQueue(uid).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));
    res.json({ success: true, autoResumed: autoResume.resumed, resumedCount: autoResume.count, resumeSpreadHours: autoResume.spreadHours });
  });

  // ========== REMINDER SETTINGS ROUTES ==========

  // Returns admin-configured send delay info to authenticated users (for display in dashboard)
  app.get("/api/send-delay-info", requireAuth, async (req, res) => {
    try {
      const admin = await storage.getAdminUser();
      if (!admin) return res.json({ systemInviteMin: 45, systemInviteMax: 60, userInviteHours: 24 });
      const systemInvite = parseInt(await storage.getSetting(admin.id, "admin_delay_system_invite") || "45");
      const userInviteHours = parseInt(await storage.getSetting(admin.id, "admin_delay_user_invite_hours") || "24");
      res.json({
        systemInviteMin: isNaN(systemInvite) ? 45 : systemInvite,
        systemInviteMax: isNaN(systemInvite) ? 60 : Math.round(systemInvite * 1.33),
        userInviteHours: isNaN(userInviteHours) ? 24 : userInviteHours,
      });
    } catch {
      res.json({ systemInviteMin: 45, systemInviteMax: 60, userInviteHours: 24 });
    }
  });

  app.get("/api/reminder-settings", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!;
    const enabled = await storage.getSetting(userId, "auto_reminder_enabled");
    const sent = await storage.getSetting(userId, "reminder_sent");
    const sentCount = await storage.getSetting(userId, "reminder_sent_count");
    const time = await storage.getSetting(userId, "reminder_time");
    const templateId = await storage.getSetting(userId, "reminder_template_id");
    const customMsg = await storage.getSetting(userId, "reminder_custom_msg");
    const date = await storage.getSetting(userId, "reminder_date");
    res.json({
      enabled: enabled !== "false",
      sent: sent === "true",
      sentCount: parseInt(sentCount || "0"),
      time: time || "09:00",
      templateId: templateId || "formal_reminder",
      customMsg: customMsg || "",
      date: date || "",
    });
  });

  app.get("/api/reminder-templates", requireAuth, (_req, res) => {
    res.json(REMINDER_TEMPLATES);
  });

  app.put("/api/reminder-settings", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!;
    const schema = z.object({
      enabled: z.boolean().optional(),
      time: z.string().regex(/^\d{2}:\d{2}$/).optional(),
      templateId: z.string().optional(),
      customMsg: z.string().optional(),
      date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().or(z.literal("").optional()),
    });
    const data = schema.parse(req.body);
    if (data.enabled !== undefined) {
      await storage.setSetting(userId, "auto_reminder_enabled", data.enabled ? "true" : "false");
    }
    if (data.time !== undefined) {
      await storage.setSetting(userId, "reminder_time", data.time);
      await storage.setSetting(userId, "reminder_sent", "false");
    }
    if (data.templateId !== undefined) {
      await storage.setSetting(userId, "reminder_template_id", data.templateId);
    }
    if (data.customMsg !== undefined) {
      await storage.setSetting(userId, "reminder_custom_msg", data.customMsg);
    }
    if (data.date !== undefined) {
      if (data.date) {
        // Validate: reminder date must be at least 2 days before event date
        const eventDateStr = await storage.getSetting(userId, "event_date");
        if (eventDateStr) {
          const parseDate = (s: string) => {
            const m1 = s.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
            if (m1) return new Date(parseInt(m1[1]), parseInt(m1[2]) - 1, parseInt(m1[3]));
            const m2 = s.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
            if (m2) return new Date(parseInt(m2[3]), parseInt(m2[2]) - 1, parseInt(m2[1]));
            const d = new Date(s); return isNaN(d.getTime()) ? null : d;
          };
          const eventDate = parseDate(eventDateStr);
          if (eventDate) {
            const cutoff = new Date(eventDate);
            cutoff.setDate(cutoff.getDate() - 2);
            const selected = new Date(data.date);
            if (selected > cutoff) {
              return res.status(400).json({ message: "تاريخ التذكير يجب أن يكون قبل المناسبة بيومين على الأقل" });
            }
          }
        }
      }
      await storage.setSetting(userId, "reminder_date", data.date);
      await storage.setSetting(userId, "reminder_sent", "false");
      await storage.setSetting(userId, "reminder_disconnect_alert_sent", "false");
    }
    res.json({ success: true });
    sendScheduledReminders().catch(err => console.error("Reminder trigger after settings save failed:", err));
  });

  app.post("/api/reminder-settings/reset", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!;
    await storage.setSetting(userId, "reminder_sent", "false");
    await storage.setSetting(userId, "reminder_sent_count", "0");
    res.json({ success: true });
  });

  app.post("/api/reminder-settings/send-now", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!;
    const guests = await storage.getGuests(userId);
    const eligibleGuests = guests.filter(g => g.status !== "declined");
    if (eligibleGuests.length === 0) {
      return res.json({ success: true, sent: 0 });
    }
    const templateId = await storage.getSetting(userId, "reminder_template_id") || "formal_reminder";
    res.json({ success: true, sent: eligibleGuests.length });
    (async () => {
      let count = 0;
      for (const guest of eligibleGuests) {
        try {
          const message = await buildReminderMessage(templateId, userId, guest.name);
          let sent = false;
          const waSession = waManager.getConnectedSessionForUser(userId);
          if (waSession) {
            try {
              await waSession.sendMessage(guest.phoneNumber, message);
              sent = true;
            } catch {}
          }
          if (!sent) {
            sent = await sendViaAnySystem(guest.phoneNumber, message);
          }
          if (sent) count++;
        } catch (err) {
          console.error(`Reminder send-now error for guest ${guest.id}:`, err);
        }
        await new Promise(r => setTimeout(r, 5000));
      }
      await storage.setSetting(userId, "reminder_sent", "true");
      await storage.setSetting(userId, "reminder_sent_count", String(count));
      console.log(`Reminder send-now: sent ${count}/${eligibleGuests.length} for user ${userId}`);
    })();
  });

  app.get("/api/settings/promo-footer", requireAuth, async (req, res) => {
    const val = await storage.getSetting(req.effectiveUserId!, "promo_footer_enabled");
    res.json({ enabled: val === "true" });
  });

  app.put("/api/settings/promo-footer", requireAuth, async (req, res) => {
    const { enabled } = z.object({ enabled: z.boolean() }).parse(req.body);
    await storage.setSetting(req.effectiveUserId!, "promo_footer_enabled", enabled ? "true" : "false");
    res.json({ enabled });
  });

  // Custom footer: only hall role can set it; read-only returns hall's footer for regular users
  app.get("/api/settings/custom-footer", requireAuth, async (req, res) => {
    const user = await storage.getUser(req.effectiveUserId!);
    const sourceId = user?.role === "hall" ? user.id : user?.parentHallId || null;
    const val = sourceId ? await storage.getSetting(sourceId, "custom_footer") : null;
    const enabledVal = sourceId ? await storage.getSetting(sourceId, "custom_footer_enabled") : null;
    const enabled = enabledVal !== "false";
    res.json({ text: val || "", enabled });
  });

  app.put("/api/settings/custom-footer", requireAuth, async (req, res) => {
    const user = await storage.getUser(req.effectiveUserId!);
    if (user?.role !== "hall") return res.status(403).json({ message: "هذا الإعداد للقاعات فقط" });
    const { text, enabled } = z.object({ text: z.string().max(300), enabled: z.boolean().optional() }).parse(req.body);
    await storage.setSetting(req.effectiveUserId!, "custom_footer", text);
    if (enabled !== undefined) {
      await storage.setSetting(req.effectiveUserId!, "custom_footer_enabled", enabled ? "true" : "false");
    }
    const enabledVal = await storage.getSetting(req.effectiveUserId!, "custom_footer_enabled");
    res.json({ text, enabled: enabledVal !== "false" });
  });

  // ========== EVENT DETAILS ROUTES (user-scoped) ==========

  // Alias to the global constant for use within route handlers
  const PERSONAL_EVENT_FIELDS = PERSONAL_EVENT_FIELDS_GLOBAL;

  app.get("/api/event-details", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const effectiveUserId = req.effectiveUserId!;
    const isSecondary = sessionUserId !== effectiveUserId;

    // Get shared fields from effective (primary) user
    const sharedDetails = await storage.getEventDetails(effectiveUserId);

    if (isSecondary) {
      // Get personal fields from the secondary user's own settings
      const personalDetails = await storage.getEventDetails(sessionUserId);
      const merged = { ...(sharedDetails || {}) };
      for (const field of PERSONAL_EVENT_FIELDS) {
        if (personalDetails?.[field] !== undefined) {
          merged[field] = personalDetails[field];
        } else {
          delete merged[field]; // secondary user has their own empty values
        }
      }
      res.json(merged);
    } else {
      res.json(sharedDetails);
    }
  });

  app.put("/api/event-details", requireAuth, async (req, res) => {
    const sessionUserId = req.session.userId!;
    const effectiveUserId = req.effectiveUserId!;
    const isSecondary = sessionUserId !== effectiveUserId;

    const input = z.object({
      event_date: z.string().optional(),
      event_time: z.string().optional(),
      event_venue_name: z.string().optional(),
      event_venue_address: z.string().optional(),
      event_map_link: z.string().optional(),
      include_qr: z.enum(["true", "false"]).optional(),
      event_sender: z.string().optional(),
      event_occasion: z.string().optional(),
    }).parse(req.body);

    const currentUser = await storage.getUser(effectiveUserId);

    // Separate personal fields from shared fields
    const personalDetails: Record<string, string> = {};
    const sharedDetails: Record<string, string> = {};

    for (const [key, val] of Object.entries(input)) {
      if (val === undefined) continue;
      if (PERSONAL_EVENT_FIELDS.includes(key as typeof PERSONAL_EVENT_FIELDS[number])) {
        personalDetails[key] = String(val);
      } else {
        if ((key === "event_venue_name" || key === "event_map_link") && currentUser?.parentHallId) {
          continue;
        }
        sharedDetails[key] = String(val);
      }
    }

    if (sharedDetails.include_qr === "false") {
      const existing = await storage.getEventDetails(effectiveUserId);
      if (existing?.include_qr === "true") {
        delete sharedDetails.include_qr;
      }
    }

    // Save personal fields to session user, shared fields to effective user
    if (isSecondary) {
      if (Object.keys(personalDetails).length > 0) {
        await storage.setEventDetails(sessionUserId, personalDetails);
      }
      if (Object.keys(sharedDetails).length > 0) {
        await storage.setEventDetails(effectiveUserId, sharedDetails);
      }
    } else {
      const allDetails = { ...personalDetails, ...sharedDetails };
      if (Object.keys(allDetails).length > 0) {
        await storage.setEventDetails(effectiveUserId, allDetails);
      }
    }

    if (input.event_date) {
      await storage.setSetting(effectiveUserId, "reminder_sent", "false");
    }

    // Return merged view (same as GET)
    const sharedResult = await storage.getEventDetails(effectiveUserId);
    if (isSecondary) {
      const personalResult = await storage.getEventDetails(sessionUserId);
      const merged = { ...(sharedResult || {}) };
      for (const field of PERSONAL_EVENT_FIELDS) {
        if (personalResult?.[field] !== undefined) {
          merged[field] = personalResult[field];
        } else {
          delete merged[field];
        }
      }
      res.json(merged);
    } else {
      res.json(sharedResult);
    }

    if (input.event_date) {
      sendScheduledReminders().catch(err => console.error("Reminder trigger after event date save failed:", err));
    }
  });

  app.get("/api/message-preview", requireAuth, async (req, res) => {
    const dataUserId = req.effectiveUserId!;
    const previewSettingsUserId = req.session.userId!;
    const guests = await storage.getGuests(req.session.userId!);
    const sampleGuest = guests.find(g => !g.sentAt) || guests[0];
    if (!sampleGuest) {
      return res.json({ message: "لا يوجد ضيوف بعد", guestName: "" });
    }
    const templateId = await storage.getSetting(previewSettingsUserId, "message_template_id") || "formal";
    const appBaseUrl = `${req.protocol}://${req.get('host')}`;
    const inviteLink = `${appBaseUrl}/i/${sampleGuest.inviteSlug || sampleGuest.id}`;
    const message = await buildMessage(templateId, dataUserId, sampleGuest.name, inviteLink, false, previewSettingsUserId, previewSettingsUserId);
    res.json({ message, guestName: sampleGuest.name });
  });

  // ========== MEDIA ROUTES (user-scoped) ==========

  app.post(api.media.upload.path, requireAuth, (req, res, next) => {
    mediaUpload.single('file')(req, res, (err: any) => {
      if (err) {
        if (err.code === 'LIMIT_FILE_SIZE') {
          return res.status(400).json({ message: "حجم الملف يتجاوز 15 ميجابايت" });
        }
        console.error("Multer error:", err);
        return res.status(400).json({ message: "خطأ في رفع الملف. حاول مرة أخرى." });
      }
      next();
    });
  }, async (req, res) => {
    try {
      if (!req.file) {
        return res.status(400).json({ message: "لم يتم رفع ملف" });
      }

      const ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'video/mp4', 'video/quicktime', 'video/x-matroska'];
      if (!ALLOWED_MIMES.includes(req.file.mimetype)) {
        return res.status(400).json({ message: "نوع الملف غير مدعوم. يُسمح بـ JPG، PNG، WebP، GIF، MP4 فقط." });
      }

      const userId = req.session.userId!;
      const fileType = req.file.mimetype.startsWith('image/') ? 'image' : 'video';
      const fileUrl = `/api/media/file/${userId}/${fileType}`;

      await storage.saveMediaFile(userId, req.file.buffer, req.file.mimetype, fileType, req.file.size);
      await storage.setSetting(userId, "global_media_url", fileUrl);
      await storage.setSetting(userId, "global_media_type", fileType);
      await storage.setSetting(userId, "global_media_uploaded_at", new Date().toISOString());

      // Auto-resume if queue is paused — media is now available for sending
      const mediaAutoResume = await autoResumeQueue(req.session.userId!).catch(() => ({ resumed: false, count: 0, intervalMs: 60000, spreadHours: 0 }));

      res.json({ url: fileUrl, type: fileType, autoResumed: mediaAutoResume.resumed, resumedCount: mediaAutoResume.count, resumeSpreadHours: mediaAutoResume.spreadHours });
    } catch (err: any) {
      console.error("Media upload error:", err);
      res.status(500).json({ message: "فشل حفظ الملف. حاول مرة أخرى." });
    }
  });

  async function serveMediaFile(req: any, res: any) {
    try {
      const userId = parseInt(req.params.userId);
      if (isNaN(userId)) return res.status(400).end();
      const file = await storage.getMediaFile(userId);
      if (!file) return res.status(404).end();
      res.setHeader('Content-Type', file.mimeType);
      res.setHeader('Cache-Control', 'public, max-age=3600');
      res.send(file.data);
    } catch (err) {
      console.error("Media serve error:", err);
      res.status(500).end();
    }
  }
  app.get("/api/media/file/:userId", serveMediaFile);
  app.get("/api/media/file/:userId/:fileType", serveMediaFile);

  app.get(api.media.getGlobal.path, requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const url = await storage.getSetting(userId, "global_media_url");
    const type = await storage.getSetting(userId, "global_media_type") as 'image' | 'video' | undefined;
    res.json({ url: url || null, type: type || null });
  });

  // ========== WHATSAPP ROUTES (user-scoped) ==========

  app.get("/api/scanner-code", requireAuth, async (req, res) => {
    const userId = req.effectiveUserId!;
    const allUsers = await storage.getAllUsers();
    const scanners = [];
    for (const u of allUsers) {
      if (u.role === 'scanner') {
        const linked = await storage.getSetting(u.id, "linked_user_id");
        if (linked === String(userId)) {
          scanners.push({ id: u.id, name: u.name, accessCode: u.accessCode });
        }
      }
    }
    res.json({ scanners });
  });

  app.post("/api/scanner-code/add", requireAuth, async (req, res) => {
    try {
      const sessionUser = await storage.getUser(req.session.userId!);
      if (sessionUser?.primaryUserId) {
        return res.status(403).json({ message: "الحساب المشترك لا يستطيع إضافة بوابين" });
      }
      const userId = req.effectiveUserId!;
      const user = await storage.getUser(userId);
      if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });
      if (user.parentHallId !== null) {
        return res.status(403).json({ message: "يتم إنشاء البواب من قِبل القاعة تلقائياً" });
      }
      const existingScanners = await storage.getScannersForUser(userId);
      const maxScanners = user.isAdmin ? 3 : 2;
      if (existingScanners.length >= maxScanners) {
        return res.status(400).json({ message: `الحد الأقصى لعدد البوابين ${maxScanners}` });
      }
      const scannerNum = existingScanners.length + 1;
      const scannerCode = generateAccessCode();
      const scanner = await storage.createUser({
        name: `بواب ${scannerNum} - ${user.name}`,
        phoneNumber: "",
        accessCode: scannerCode,
        messageQuota: 0,
        messagesSent: 0,
        isAdmin: false,
        role: "scanner",
      });
      await storage.setSetting(scanner.id, "linked_user_id", String(userId));
      res.json({ success: true, scanner: { id: scanner.id, name: scanner.name, accessCode: scannerCode } });
    } catch (err: any) {
      res.status(500).json({ message: err.message || "فشل إنشاء حساب البواب" });
    }
  });

  app.delete("/api/scanner-code/:scannerId", requireAuth, async (req, res) => {
    try {
      const sessionUser = await storage.getUser(req.session.userId!);
      if (sessionUser?.primaryUserId) {
        return res.status(403).json({ message: "الحساب المشترك لا يستطيع حذف البوابين" });
      }
      const userId = req.effectiveUserId!;
      const scannerId = Number(req.params.scannerId);
      const scanner = await storage.getUser(scannerId);
      if (!scanner || scanner.role !== "scanner") {
        return res.status(404).json({ message: "البواب غير موجود" });
      }
      const linked = await storage.getSetting(scannerId, "linked_user_id");
      if (linked !== String(userId)) {
        return res.status(403).json({ message: "غير مصرح" });
      }
      await storage.deleteAllUserData(scannerId);
      res.json({ success: true });
    } catch (err: any) {
      res.status(500).json({ message: err.message || "فشل حذف البواب" });
    }
  });

  app.post("/api/scanner-code/send", requireAuth, async (req, res) => {
    try {
      const userId = req.effectiveUserId!;
      const user = await storage.getUser(userId);
      if (!user) return res.status(401).json({ message: "غير مسجل الدخول" });

      const allUsers = await storage.getAllUsers();
      const scanners = [];
      for (const u of allUsers) {
        if (u.role === 'scanner') {
          const linked = await storage.getSetting(u.id, "linked_user_id");
          if (linked === String(userId)) {
            scanners.push({ id: u.id, name: u.name, accessCode: u.accessCode });
          }
        }
      }

      if (scanners.length === 0) {
        return res.status(404).json({ message: "لا يوجد حساب حارس بوابة" });
      }

      const appUrl = `${req.protocol}://${req.get('host')}`;
      let msg = `🚪 *كود حارس البوابة*\n\nأعطِ هذا الكود لحارس البوابة عشان يمسح باركود الضيوف عند الدخول:\n`;
      for (const s of scanners) {
        msg += `\n🔑 الكود: *${s.accessCode}*`;
        msg += `\n🔗 رابط مباشر:\n${appUrl}/login?code=${s.accessCode}\n`;
      }

      let sent = false;
      const waSession = waManager.getConnectedSessionForUser(userId);
      if (waSession) {
        try {
          await waSession.sendMessage(user.phoneNumber, msg);
          sent = true;
        } catch {}
      }
      if (!sent) {
        sent = await sendViaAnySystem(user.phoneNumber, msg, undefined, undefined, true);
      }
      if (!sent) {
        return res.status(503).json({ message: "لا يوجد أي طريقة إرسال متاحة لإرسال الكود." });
      }
      res.json({ success: true, message: "تم إرسال كود الحارس على الواتساب" });
    } catch (err) {
      console.error("Failed to send scanner code:", err);
      res.status(500).json({ message: "فشل إرسال الكود" });
    }
  });

  app.get("/api/dashboard-stats", requireAuth, async (req, res) => {
    const userId = req.session.userId!;        // own guest list owner
    const quotaUserId = req.effectiveUserId!;  // primary user for quota and event settings
    const user = await storage.getUser(quotaUserId);
    const eventDetails = await storage.getEventDetails(quotaUserId);
    const settingQr = eventDetails?.include_qr === "true";
    // Fallback: if setting missing but a scanner account exists, treat as QR-enabled
    const scanners = settingQr ? [] : await storage.getScannersForUser(quotaUserId);
    const includeQr = settingQr || scanners.length > 0;
    // Persist the setting if it was missing (auto-repair)
    if (!settingQr && scanners.length > 0) {
      await storage.setSetting(quotaUserId, "include_qr", "true");
    }

    // Aggregate guests from all users in the shared account group
    const ownGuests = await storage.getGuests(userId);
    const linkedUsers = await storage.getLinkedUsers(quotaUserId); // all secondaries of primary
    let combinedGuests = [...ownGuests];
    for (const lu of linkedUsers) {
      if (lu.id !== userId) {
        const luGuests = await storage.getGuests(lu.id);
        combinedGuests = combinedGuests.concat(luGuests);
      }
    }
    // If current user is secondary, also include primary user's guests
    if (userId !== quotaUserId) {
      const primaryGuests = await storage.getGuests(quotaUserId);
      combinedGuests = combinedGuests.concat(primaryGuests);
    }
    const guests = combinedGuests;

    const totalGuests = guests.length;
    const sentCount = guests.filter(g => g.sentAt !== null).length;
    const pendingCount = guests.filter(g => g.sentAt === null).length;
    const confirmedCount = guests.filter(g => g.status === 'confirmed').length;
    const declinedCount = guests.filter(g => g.status === 'declined').length;
    const checkedInCount = guests.filter(g => g.checkedIn).length;
    
    const quota = user?.messageQuota ?? 0;
    const sent = user?.messagesSent ?? 0;
    res.json({
      quota,
      sent,
      remaining: quota === -1 ? -1 : Math.max(0, quota - sent),
      totalGuests,
      sentCount,
      pendingCount,
      confirmedCount,
      declinedCount,
      checkedInCount,
      includeQr,
    });
  });

  app.get("/api/whatsapp/accounts", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const allStatuses = waManager.getAllStatuses();
    const userAccounts = allStatuses.filter(s => waManager.getSessionUserId(s.id) === userId);
    const dbAccounts = await storage.getWhatsappAccounts(userId);
    const dbMap = new Map(dbAccounts.map(a => [a.id, a]));
    const accountsWithMeta = userAccounts.map(s => ({
      ...s,
      createdAt: dbMap.get(s.id)?.createdAt?.toISOString() || null,
    }));
    res.json(accountsWithMeta);
  });

  app.post("/api/whatsapp/accounts", requireAuth, async (req, res) => {
    try {
      const { name } = z.object({ name: z.string().min(1).max(50) }).parse(req.body);
      const userId = req.session.userId!;
      const account = await storage.createWhatsappAccount({ name, userId });
      const session = waManager.addSession(account.id, account.name, userId);
      wireUserSessionCallbacks(session, userId);
      await session.start();
      res.status(201).json(session.getStatus());
    } catch (err) {
      console.error("Failed to create WhatsApp account:", err);
      res.status(400).json({ message: "فشل إنشاء الحساب" });
    }
  });

  app.delete("/api/whatsapp/accounts/:id", requireAuth, async (req, res) => {
    const id = Number(req.params.id);
    const userId = req.session.userId!;
    const account = await storage.getWhatsappAccount(id);
    if (!account || account.userId !== userId) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    // Block deletion while there are pending sends in the DB queue (queue is keyed by session userId)
    const qStats = await storage.getSendQueueStats(req.session.userId!);
    if (qStats.pending > 0) {
      return res.status(409).json({ message: "لا يمكن حذف حساب واتساب أثناء الإرسال الجماعي. أوقف الإرسال أولاً ثم أعد المحاولة." });
    }
    await waManager.removeSession(id);
    await storage.deleteWhatsappAccount(id);
    res.status(204).send();
  });

  app.post("/api/whatsapp/accounts/:id/reconnect", requireAuth, async (req, res) => {
    const id = Number(req.params.id);
    const userId = req.session.userId!;
    const account = await storage.getWhatsappAccount(id);
    if (!account || account.userId !== userId) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    // Block reconnect while there are pending sends in the DB queue
    const qStats2 = await storage.getSendQueueStats(req.session.userId!);
    if (qStats2.pending > 0) {
      return res.status(409).json({ message: "لا يمكن إعادة ربط الرقم أثناء الإرسال الجماعي. أوقف الإرسال أولاً ثم أعد المحاولة." });
    }
    const session = waManager.getSession(id);
    if (!session) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    await waManager.reconnectSession(id);
    res.json(session.getStatus());
  });

  app.post("/api/whatsapp/accounts/:id/pairing-code", requireAuth, async (req, res) => {
    const id = Number(req.params.id);
    const userId = req.session.userId!;
    const account = await storage.getWhatsappAccount(id);
    if (!account || account.userId !== userId) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    const session = waManager.getSession(id);
    if (!session) {
      return res.status(404).json({ message: "الجلسة غير موجودة" });
    }
    if (session.status !== 'qr_ready') {
      return res.status(400).json({ message: "الاتصال غير جاهز. انتظر ظهور QR ثم حاول مجدداً." });
    }
    if (!session.sock) {
      return res.status(400).json({ message: "الجلسة غير جاهزة" });
    }
    let digits: string;
    try {
      const { phoneNumber } = z.object({ phoneNumber: z.string().min(5) }).parse(req.body);
      digits = phoneNumber.replace(/[^0-9]/g, '');
      // E.164-style: international numbers are 7–15 digits (without leading +)
      if (digits.length < 7 || digits.length > 15) {
        return res.status(400).json({ message: "رقم الهاتف غير صالح. أدخل الرقم بصيغة دولية (7-15 رقم)" });
      }
    } catch {
      return res.status(400).json({ message: "رقم الهاتف غير صالح" });
    }
    try {
      const code = await session.sock.requestPairingCode(digits);
      res.json({ code });
    } catch (err: unknown) {
      const message = err instanceof Error ? err.message : "فشل طلب كود الربط";
      console.error("Pairing code error:", err);
      res.status(500).json({ message });
    }
  });

  app.get(api.whatsapp.status.path, requireAuth, (req, res) => {
    const userId = req.session.userId!;
    const statuses = waManager.getAllStatuses().filter(s => waManager.getSessionUserId(s.id) === userId);
    if (statuses.length === 0) {
      return res.json({ status: 'disconnected' as const });
    }
    const connected = statuses.find(s => s.status === 'connected');
    const qrReady = statuses.find(s => s.status === 'qr_ready');
    const best = connected || qrReady || statuses[0];
    res.json({ status: best.status, qr: best.qr });
  });

  // ========== WOOCOMMERCE WEBHOOK ==========

  const WEBHOOK_SECRET = process.env.WOOCOMMERCE_WEBHOOK_SECRET || "";

  const ALLOWED_PRODUCT_ID = 5688;

  // Green API incoming webhook — acknowledge receipt and optionally verify auth header
  app.post("/api/webhook/green-api", async (req, res) => {
    const eventType = typeof req.body?.typeWebhook === 'string' ? req.body.typeWebhook : 'unknown';
    const adminId = await getAdminId().catch(() => null);
    if (adminId) {
      const expectedToken = await storage.getSetting(adminId, "green_api_webhook_auth_token").catch(() => null);
      if (expectedToken) {
        const authHeader = req.headers["authorization"] || "";
        if (authHeader !== expectedToken) {
          console.warn(`[Green API webhook] Auth header mismatch — event=${eventType}`);
        }
      }
    }
    console.log(`[Green API webhook] event=${eventType}`);
    res.json({ received: true });
  });

  // ========== WOO OTP VERIFICATION ==========
  // In-memory stores — ephemeral, no DB needed (OTPs are short-lived)
  const wooOtpStore = new Map<string, { code: string; expiresAt: Date; sentAt: Date }>();
  const wooTokenStore = new Map<string, { phone: string; expiresAt: Date }>();
  // Auto-login tokens: one-time, 15-minute, tied to phone + optional OTP
  const autoLoginTokenStore = new Map<string, { userId: number; phone: string; expiresAt: Date; otp?: string; otpSentAt?: Date }>();
  // Cleanup stale entries every 15 min
  setInterval(() => {
    const now = new Date();
    for (const [k, v] of wooOtpStore) { if (v.expiresAt < now) wooOtpStore.delete(k); }
    for (const [k, v] of wooTokenStore) { if (v.expiresAt < now) wooTokenStore.delete(k); }
    for (const [k, v] of autoLoginTokenStore) { if (v.expiresAt < now) autoLoginTokenStore.delete(k); }
  }, 15 * 60 * 1000);

  // CORS helper for WooCommerce cross-origin requests
  function setCorsForWoo(res: Response) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type");
    res.header("Access-Control-Allow-Methods", "POST, OPTIONS");
  }

  // OPTIONS preflight for WooCommerce endpoints
  app.options("/api/woo/:path", (req, res) => { setCorsForWoo(res); res.sendStatus(204); });

  // POST /api/woo/request-otp — sends OTP via any available channel (ChatBerry → Green API → Baileys)
  app.post("/api/woo/request-otp", async (req, res) => {
    setCorsForWoo(res);
    try {
      const { phone } = req.body;
      if (!phone) return res.status(400).json({ message: "رقم الهاتف مطلوب" });
      const cleanPhone = String(phone).replace(/[^0-9]/g, "");
      if (cleanPhone.length < 9) return res.status(400).json({ message: "رقم الهاتف غير صحيح" });

      // Rate limit: 60s cooldown between OTP requests for same number
      const existing = wooOtpStore.get(cleanPhone);
      if (existing) {
        const secondsSinceSent = (Date.now() - existing.sentAt.getTime()) / 1000;
        if (secondsSinceSent < 60) {
          const wait = Math.ceil(60 - secondsSinceSent);
          return res.status(429).json({ message: `انتظر ${wait} ثانية قبل إعادة الإرسال` });
        }
      }

      const otp = Math.floor(100000 + Math.random() * 900000).toString();
      const now = new Date();
      wooOtpStore.set(cleanPhone, { code: otp, expiresAt: new Date(now.getTime() + 5 * 60 * 1000), sentAt: now });

      const msg = `🔐 كود التحقق لإتمام الشراء في إنفايتنا:\n\n*${otp}*\n\nصالح لمدة 5 دقائق\nإذا لم تطلب هذا الكود تجاهل الرسالة`;
      const sent = await sendViaAnySystem(cleanPhone, msg, undefined, undefined, true);
      if (!sent) {
        wooOtpStore.delete(cleanPhone);
        return res.status(503).json({ message: "تعذر إرسال الكود — تأكد من إعداد قناة إرسال (ChatBerry أو Green API أو رقم المشروع) في لوحة التحكم" });
      }

      console.log(`[WooOTP] OTP sent to ${cleanPhone}`);
      res.json({ success: true, message: "تم إرسال كود التحقق على واتساب ✅" });
    } catch (err) {
      console.error("[WooOTP] request-otp error:", err);
      res.status(500).json({ message: "خطأ في الخادم" });
    }
  });

  // POST /api/woo/verify-otp — verifies OTP and returns a short-lived token
  app.post("/api/woo/verify-otp", async (req, res) => {
    setCorsForWoo(res);
    try {
      const { phone, code } = req.body;
      if (!phone || !code) return res.status(400).json({ message: "البيانات ناقصة" });
      const cleanPhone = String(phone).replace(/[^0-9]/g, "");
      const entry = wooOtpStore.get(cleanPhone);

      if (!entry) return res.status(400).json({ message: "لم يتم طلب كود لهذا الرقم" });
      if (new Date() > entry.expiresAt) {
        wooOtpStore.delete(cleanPhone);
        return res.status(400).json({ message: "انتهت صلاحية الكود — اطلب كوداً جديداً" });
      }
      if (entry.code !== String(code).trim()) {
        return res.status(400).json({ message: "الكود غير صحيح ❌" });
      }

      // Generate a 30-min verification token (one-time, consumed by webhook)
      const token = crypto.randomBytes(24).toString("hex");
      wooTokenStore.set(token, { phone: cleanPhone, expiresAt: new Date(Date.now() + 30 * 60 * 1000) });
      wooOtpStore.delete(cleanPhone);

      console.log(`[WooOTP] Phone ${cleanPhone} verified successfully`);
      res.json({ success: true, token, message: "تم التحقق من رقمك بنجاح ✅" });
    } catch (err) {
      console.error("[WooOTP] verify-otp error:", err);
      res.status(500).json({ message: "خطأ في الخادم" });
    }
  });

  app.post("/api/webhook/woocommerce", async (req, res) => {
    try {
      const { customer_phone, customer_name, quantity, order_id, date, time, hallname, delivery_method, qr_option, product_id, line_items, woo_verified_token } = req.body;

      // Check if OTP verification is required by admin setting
      const adminId = await getAdminId().catch(() => null);
      const otpRequired = adminId ? await storage.getSetting(adminId, "woo_otp_required").catch(() => "") : "";
      if (otpRequired === "true") {
        if (!woo_verified_token) {
          return res.status(400).json({ message: "التحقق من رقم الواتساب مطلوب قبل إتمام الشراء" });
        }
        const tokenEntry = wooTokenStore.get(String(woo_verified_token));
        if (!tokenEntry || new Date() > tokenEntry.expiresAt) {
          return res.status(400).json({ message: "رمز التحقق منتهي الصلاحية — يرجى التحقق من الرقم مجدداً" });
        }
        wooTokenStore.delete(String(woo_verified_token)); // One-time use
      }
      
      let hasTargetProduct = true;
      if (product_id !== undefined) {
        hasTargetProduct = Number(product_id) === ALLOWED_PRODUCT_ID;
      } else if (Array.isArray(line_items)) {
        hasTargetProduct = line_items.some((item: any) => Number(item.product_id) === ALLOWED_PRODUCT_ID);
      }

      if (!hasTargetProduct) {
        return res.status(200).json({ message: "تم تجاهل الطلب - منتج غير مطابق", skipped: true });
      }

      const admins = await storage.getAllUsers();
      const adminUser = admins.find(u => u.isAdmin);
      const phoneFieldName = adminUser ? await storage.getSetting(adminUser.id, "webhook_phone_field") : "";
      const customPhone = phoneFieldName ? req.body[phoneFieldName] : undefined;
      const rawPhone = customPhone || customer_phone;
      if (!rawPhone) {
        return res.status(400).json({ message: "رقم الهاتف مطلوب" });
      }

      const phone = String(rawPhone).replace(/[^0-9]/g, '');
      const name = customer_name || "مستخدم جديد";
      const msgCount = parseInt(quantity) || 500;
      const includeQr = qr_option
        ? /^(true|yes|1|مع|with)/i.test(String(qr_option).trim()) || String(qr_option).includes("مع")
        : false;

      let user = await storage.getUserByPhone(phone);
      if (user) {
        await storage.updateUser(user.id, { messagesSent: 0, messageQuota: msgCount });
        user = await storage.getUser(user.id);
      } else {
        const code = generateAccessCode();
        user = await storage.createUser({
          name,
          phoneNumber: phone,
          accessCode: code,
          messageQuota: msgCount,
          messagesSent: 0,
          isAdmin: false,
        });
      }

      let createdScannerCode: string | null = null;
      if (user) {
        const eventDetails: Record<string, string> = {};
        if (date) eventDetails.event_date = String(date);
        if (time) eventDetails.event_time = String(time);
        if (hallname) eventDetails.event_venue_name = String(hallname);
        if (delivery_method) eventDetails.event_delivery_method = String(delivery_method);
        eventDetails.include_qr = includeQr ? "true" : "false";
        await storage.setEventDetails(user.id, eventDetails);

        if (includeQr) {
          const existingUsers = await storage.getAllUsers();
          let hasScanner = false;
          for (const u of existingUsers) {
            if (u.role !== 'scanner') continue;
            const linked = await storage.getSetting(u.id, "linked_user_id");
            if (linked === String(user!.id)) { hasScanner = true; break; }
          }
          
          if (!hasScanner) {
            const scannerCode = generateAccessCode();
            const scanner = await storage.createUser({
              name: `ماسح - ${name}`,
              phoneNumber: "",
              accessCode: scannerCode,
              messageQuota: 0,
              messagesSent: 0,
              isAdmin: false,
              role: "scanner",
            });
            await storage.setSetting(scanner.id, "linked_user_id", String(user.id));
            createdScannerCode = scannerCode;
          }
        }
      }

      if (user) {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        let msg = `🎉 مرحباً *${user.name}*!\n\nتم تفعيل حسابك في إنفايتنا نظام إدارة الدعوات بنجاح.\n\n🔑 كود الدخول: *${user.accessCode}*\n📱 عدد الرسائل: *${msgCount}*`;
        if (date) msg += `\n📅 التاريخ: *${date}*`;
        if (time) msg += `\n🕐 الوقت: *${time}*`;
        if (hallname) msg += `\n📍 المكان: *${hallname}*`;
        if (createdScannerCode) msg += `\n\n🚪 كود حارس البوابة: *${createdScannerCode}*\nأعطه لحارس البوابة ليمسح باركود الضيوف عند الدخول`;
        msg += `\n\n🔗 ادخل من هنا:\n${appUrl}/login?code=${user.accessCode}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم\n\nشكراً لاختياركم إنفايتنا!`;

        const welcomeSent = await sendViaAnySystem(phone, msg).catch(() => false);
        if (!welcomeSent) {
          console.log(`WooCommerce welcome message could not be sent to ${phone} — queuing`);
          await storage.setSetting(user.id, 'welcome_pending', 'true');
          await storage.setSetting(user.id, 'welcome_pending_msg', msg);
        }
      }

      // Generate one-time auto-login token (15 min, tied to phone)
      let loginUrl: string | undefined;
      if (user) {
        const autoToken = crypto.randomBytes(32).toString("hex");
        autoLoginTokenStore.set(autoToken, {
          userId: user.id,
          phone: phone,
          expiresAt: new Date(Date.now() + 15 * 60 * 1000),
        });
        const appUrl = `${req.protocol}://${req.get('host')}`;
        loginUrl = `${appUrl}/auto-login?token=${autoToken}`;
      }

      res.json({ 
        success: true, 
        userId: user?.id, 
        accessCode: user?.accessCode,
        messageQuota: user?.messageQuota,
        includeQr,
        login_url: loginUrl,
      });
    } catch (err) {
      console.error("WooCommerce webhook error:", err);
      res.status(500).json({ message: "خطأ في معالجة الطلب" });
    }
  });

  // ========== AUTO-LOGIN VERIFICATION ==========

  // Step 1: Page loads → send 4-digit OTP to the phone linked to the token
  app.post("/api/auth/auto-login/send-otp", async (req, res) => {
    try {
      const { token } = z.object({ token: z.string().min(1) }).parse(req.body);

      const entry = autoLoginTokenStore.get(token);
      if (!entry) return res.status(400).json({ message: "الرابط غير صالح أو منتهي الصلاحية" });
      if (new Date() > entry.expiresAt) {
        autoLoginTokenStore.delete(token);
        return res.status(400).json({ message: "انتهت صلاحية الرابط — يرجى تسجيل الدخول بكود الدخول" });
      }

      // Rate-limit: don't resend if sent within last 60 seconds
      if (entry.otpSentAt && Date.now() - entry.otpSentAt.getTime() < 60_000) {
        return res.status(429).json({ message: "انتظر دقيقة قبل طلب كود جديد", retryAfter: 60 });
      }

      const otp = String(Math.floor(1000 + Math.random() * 9000));
      entry.otp = otp;
      entry.otpSentAt = new Date();

      const msg = `🔐 كود دخول إنفايتنا: *${otp}*\n\nأدخل هذا الكود في الصفحة للدخول لحسابك مباشرة.\nصالح لمرة واحدة فقط.`;
      await sendViaAnySystem(entry.phone, msg, undefined, undefined, true).catch(() => null);

      res.json({ success: true });
    } catch (err: any) {
      if (err?.name === "ZodError") return res.status(400).json({ message: "بيانات غير صحيحة" });
      console.error("Auto-login send-otp error:", err);
      res.status(500).json({ message: "تعذّر إرسال الكود" });
    }
  });

  // Step 2: User enters the 4-digit OTP → verify and open session
  app.post("/api/auth/auto-login", async (req, res) => {
    try {
      const { token, otp } = z.object({
        token: z.string().min(1),
        otp: z.string().min(4).max(4),
      }).parse(req.body);

      const entry = autoLoginTokenStore.get(token);
      if (!entry) return res.status(400).json({ message: "الرابط غير صالح أو منتهي الصلاحية" });
      if (new Date() > entry.expiresAt) {
        autoLoginTokenStore.delete(token);
        return res.status(400).json({ message: "انتهت صلاحية الرابط — يرجى تسجيل الدخول بكود الدخول" });
      }
      if (!entry.otp) return res.status(400).json({ message: "أرسل الكود أولاً" });
      if (entry.otp !== otp.trim()) return res.status(400).json({ message: "الكود غير صحيح" });

      // One-time: delete token immediately
      autoLoginTokenStore.delete(token);

      const user = await storage.getUser(entry.userId);
      if (!user) return res.status(404).json({ message: "الحساب غير موجود" });

      req.session.userId = user.id;
      await new Promise<void>((resolve, reject) =>
        req.session.save(err => (err ? reject(err) : resolve()))
      );

      res.json({ success: true, role: user.role, isAdmin: user.isAdmin });
    } catch (err: any) {
      if (err?.name === "ZodError") return res.status(400).json({ message: "بيانات غير صحيحة" });
      console.error("Auto-login error:", err);
      res.status(500).json({ message: "خطأ في تسجيل الدخول" });
    }
  });

  app.get("/api/external/check-status", (req, res) => {
    res.json({ status: "ok", timestamp: new Date().toISOString(), app: "invatna" });
  });

  // ========== SYSTEM WHATSAPP ROUTES ==========

  app.get("/api/system-whatsapp/status", (req, res) => {
    const systemSession = waManager.getSystemSessionAny();
    if (!systemSession) {
      return res.json({ exists: false, status: 'disconnected' });
    }
    res.json({ exists: true, ...systemSession.getStatus() });
  });

  app.post("/api/system-whatsapp/reconnect", requireAdmin, async (req, res) => {
    const systemSession = waManager.getSystemSessionAny();
    if (!systemSession) {
      const account = await storage.createWhatsappAccount({ name: "رقم المشروع", userId: 0 });
      const session = waManager.addSession(account.id, account.name, 0);
      await session.start();
      return res.status(201).json(session.getStatus());
    }
    await waManager.reconnectSession(systemSession.id);
    const updated = waManager.getSession(systemSession.id);
    res.json(updated?.getStatus() || { status: 'disconnected' });
  });

  app.get("/api/admin/system-whatsapp", requireAdmin, (req, res) => {
    const systemSession = waManager.getSystemSessionAny();
    if (!systemSession) {
      return res.json({ exists: false, status: 'disconnected' });
    }
    res.json({ exists: true, ...systemSession.getStatus() });
  });

  app.post("/api/admin/system-whatsapp", requireAdmin, async (req, res) => {
    const existing = waManager.getSystemSessionAny();
    if (existing) {
      return res.json(existing.getStatus());
    }
    const account = await storage.createWhatsappAccount({ name: "رقم المشروع", userId: 0 });
    const session = waManager.addSession(account.id, account.name, 0);
    await session.start();
    res.status(201).json(session.getStatus());
  });

  app.post("/api/admin/system-whatsapp/reconnect", requireAdmin, async (req, res) => {
    const systemSession = waManager.getSystemSessionAny();
    if (!systemSession) {
      return res.status(404).json({ message: "رقم المشروع غير موجود" });
    }
    await waManager.reconnectSession(systemSession.id);
    const updated = waManager.getSession(systemSession.id);
    res.json(updated?.getStatus() || { status: 'disconnected' });
  });

  // ========== ADMIN ROUTES ==========

  app.get("/api/admin/users", requireAdmin, async (req, res) => {
    const allUsers = (await storage.getAllUsers()).filter(u => u.role !== "hall");
    // Build a map: primaryUserId → count of linked secondary users
    const linkedCountMap = new Map<number, number>();
    const primaryNameMap = new Map<number, string>();
    // Build scanner code map via linked_user_id settings (scanners are linked via settings, not primaryUserId)
    const scannerCodeMap = new Map<number, string>(); // primaryUserId → scanner accessCode
    const scannerUsers = allUsers.filter(u => u.role === "scanner");
    await Promise.all(scannerUsers.map(async (scanner) => {
      const linkedUserId = await storage.getSetting(scanner.id, "linked_user_id");
      if (linkedUserId) {
        const primaryId = parseInt(linkedUserId);
        if (!isNaN(primaryId) && !scannerCodeMap.has(primaryId)) {
          scannerCodeMap.set(primaryId, scanner.accessCode);
        }
      }
    }));
    for (const u of allUsers) {
      if (u.primaryUserId) {
        linkedCountMap.set(u.primaryUserId, (linkedCountMap.get(u.primaryUserId) || 0) + 1);
      }
    }
    for (const u of allUsers) {
      primaryNameMap.set(u.id, u.name);
    }
    res.json(allUsers.map(u => ({
      id: u.id,
      name: u.name,
      phoneNumber: u.phoneNumber,
      accessCode: u.accessCode,
      messageQuota: u.messageQuota,
      messagesSent: u.messagesSent,
      messagesRemaining: u.messageQuota === -1 ? -1 : Math.max(0, u.messageQuota - u.messagesSent),
      isAdmin: u.isAdmin,
      role: u.role,
      createdAt: u.createdAt,
      primaryUserId: u.primaryUserId ?? null,
      primaryUserName: u.primaryUserId ? (primaryNameMap.get(u.primaryUserId) ?? null) : null,
      linkedCount: linkedCountMap.get(u.id) || 0,
      scannerCode: scannerCodeMap.get(u.id) ?? null,
    })));
  });

  app.post("/api/admin/users", requireAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().min(1).max(100),
        phoneNumber: z.string().min(1).max(20),
        messageQuota: z.number().min(-1),
        isAdmin: z.boolean().optional(),
        withQr: z.boolean().optional(),
      }).parse(req.body);

      if (!input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;

      const code = generateAccessCode();
      const user = await storage.createUser({
        name: input.name,
        phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
        accessCode: code,
        messageQuota: input.messageQuota,
        messagesSent: 0,
        isAdmin: input.isAdmin || false,
      });

      // Create scanner account if withQr is requested
      let scannerCode: string | null = null;
      if (input.withQr) {
        await storage.setSetting(user.id, "include_qr", "true");
        const existingUsers = await storage.getAllUsers();
        let hasScanner = false;
        for (const u of existingUsers) {
          if (u.role !== 'scanner') continue;
          const linked = await storage.getSetting(u.id, "linked_user_id");
          if (linked === String(user.id)) { hasScanner = true; break; }
        }
        if (!hasScanner) {
          const sc = generateAccessCode();
          const scanner = await storage.createUser({
            name: `ماسح - ${input.name}`,
            phoneNumber: "",
            accessCode: sc,
            messageQuota: 0,
            messagesSent: 0,
            isAdmin: false,
            role: "scanner",
          });
          await storage.setSetting(scanner.id, "linked_user_id", String(user.id));
          scannerCode = sc;
        }
      }

      // Send welcome WhatsApp message with access code via Green API
      let adminUserWhatsappSent = false;
      try {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        const msgCount = input.messageQuota === -1 ? "غير محدود" : String(input.messageQuota);
        const userPhone = input.phoneNumber.replace(/[^0-9]/g, '');
        const msg = `🎉 مرحباً *${input.name}*!\n\nتم إنشاء حسابك في إنفايتنا نظام إدارة دعوات المناسبات.\n\n🔑 كود الدخول: *${code}*\n📱 رصيد الرسائل: *${msgCount}*\n\n🔗 ادخل من هنا:\n${appUrl}/login?code=${code}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم\n\nشكراً لانضمامكم إلى إنفايتنا!`;
        const greenResult = await sendViaGreenAPI(userPhone, msg).catch(() => ({ ok: false, error: "exception" }));
        adminUserWhatsappSent = greenResult.ok;
        if (!adminUserWhatsappSent) {
          console.log(`Admin-created user welcome message could not be sent via Green API to ${userPhone}: ${greenResult.error}`);
        }
      } catch (e) {
        console.error("Error sending admin user welcome message:", e);
      }

      res.status(201).json({ ...user, messagesRemaining: user.messageQuota === -1 ? -1 : user.messageQuota - user.messagesSent, whatsappSent: adminUserWhatsappSent, scannerCode });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.put("/api/admin/users/:id", requireAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().min(1).max(100).optional(),
        phoneNumber: z.string().min(1).max(20).optional(),
        messageQuota: z.number().min(-1).optional(),
        messagesSent: z.number().min(0).optional(),
        isAdmin: z.boolean().optional(),
      }).parse(req.body);

      if (input.phoneNumber && !input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;

      const userId = Number(req.params.id);

      // Protect primary admin (lowest-id admin) from phone number or isAdmin changes
      const allAdminsForCheck = (await storage.getAllUsers()).filter(u => u.isAdmin).sort((a, b) => a.id - b.id);
      const primaryAdminIdForCheck = allAdminsForCheck[0]?.id;
      if (userId === primaryAdminIdForCheck) {
        if (input.phoneNumber !== undefined) {
          return res.status(400).json({ message: "لا يمكن تغيير رقم هاتف الأدمن الأصلي" });
        }
        if (input.isAdmin === false) {
          return res.status(400).json({ message: "لا يمكن إزالة صلاحية الأدمن الأصلي" });
        }
      }

      const updates: any = { ...input };
      if (updates.phoneNumber) updates.phoneNumber = updates.phoneNumber.replace(/[^0-9]/g, '');


      if (input.messageQuota !== undefined && input.messageQuota !== -1) {
        const currentUser = await storage.getUser(userId);
        if (currentUser && currentUser.messageQuota !== -1 && currentUser.role === "user" && !currentUser.isAdmin) {
          updates.messagesSent = 0;
        }
      }

      const user = await storage.updateUser(userId, updates);
      res.json({ ...user, messagesRemaining: user.messageQuota === -1 ? -1 : Math.max(0, user.messageQuota - user.messagesSent) });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.delete("/api/admin/users/:id", requireAdmin, async (req, res) => {
    const id = Number(req.params.id);
    if (id === req.session.userId) {
      return res.status(400).json({ message: "لا يمكنك حذف نفسك" });
    }
    // Protect primary admin (lowest-id) from deletion under any circumstance
    const allAdminsForDel = (await storage.getAllUsers()).filter(u => u.isAdmin).sort((a, b) => a.id - b.id);
    if (id === allAdminsForDel[0]?.id) {
      return res.status(400).json({ message: "لا يمكن حذف حساب الأدمن الأصلي للنظام" });
    }
    const userAccounts = await storage.getWhatsappAccounts(id);
    for (const acc of userAccounts) {
      await waManager.removeSession(acc.id);
    }
    
    const allUsers = await storage.getAllUsers();
    for (const u of allUsers) {
      if (u.role !== 'scanner') continue;
      const linked = await storage.getSetting(u.id, "linked_user_id");
      if (linked === String(id)) {
        await storage.deleteAllUserData(u.id);
      }
    }
    
    await storage.deleteAllUserData(id);
    res.status(204).send();
  });

  app.post("/api/admin/users/:id/add-quota", requireAdmin, async (req, res) => {
    const id = Number(req.params.id);
    const { amount } = z.object({ amount: z.number().min(1) }).parse(req.body);
    await storage.addMessageQuota(id, amount);
    const user = await storage.getUser(id);
    res.json(user);
  });

  app.post("/api/admin/make-unlimited", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    await storage.updateUser(adminId, { messageQuota: -1 });
    res.json({ success: true });
  });

  // ========== ADMIN HALL ENDPOINTS ==========

  app.get("/api/admin/halls", requireAdmin, async (req, res) => {
    const halls = await storage.getAllHalls();
    const result = await Promise.all(halls.map(async (h) => {
      const clients = await storage.getHallClients(h.id);
      const venueName = await storage.getSetting(h.id, "venue_name");
      const mfPaymentFailedAt = await storage.getSetting(h.id, "mf_payment_failed_at");
      const totalQuota = h.messageQuota;
      const allocatedQuota = h.messagesSent;
      const remainingQuota = totalQuota === -1 ? -1 : totalQuota - allocatedQuota;
      return {
        id: h.id,
        name: h.name,
        phoneNumber: h.phoneNumber,
        accessCode: h.accessCode,
        messageQuota: totalQuota,
        messagesSent: allocatedQuota,
        messagesRemaining: remainingQuota,
        isSuspended: h.isSuspended,
        venueName: venueName || "",
        clientCount: clients.length,
        createdAt: h.createdAt,
        subscriptionExpiresAt: h.subscriptionExpiresAt ? h.subscriptionExpiresAt.toISOString() : null,
        subscriptionStartedAt: h.subscriptionStartedAt ? new Date(h.subscriptionStartedAt).toISOString() : null,
        subscriptionType: (h as any).subscriptionType || "monthly",
        balanceResetAt: (h as any).balanceResetAt ? new Date((h as any).balanceResetAt).toISOString() : null,
        paymentWarningAt: h.paymentWarningAt ? new Date(h.paymentWarningAt).toISOString() : null,
        hallNotifiedAt: h.hallNotifiedAt ? new Date(h.hallNotifiedAt).toISOString() : null,
        hallPassword: h.hallPassword || null,
        mfPaymentFailedAt: mfPaymentFailedAt || null,
        hasBranches: h.hasBranches ?? false,
        googleMapsUrl: h.googleMapsUrl || null,
      };
    }));
    res.json(result);
  });

  app.post("/api/admin/halls", requireAdminOrCoAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().min(1).max(100),
        phoneNumber: z.string().min(1).max(20),
        messageQuota: z.number().min(0),
        venueName: z.string().min(1).max(200),
        hallPassword: z.string().optional(),
        subscriptionExpiresAt: z.string().optional(),
        hasBranches: z.boolean().optional(),
        googleMapsUrl: z.string().nullable().optional(),
      }).parse(req.body);

      if (!input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;

      const code = generateAccessCode();
      const hallData: any = {
        name: input.name,
        phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
        accessCode: code,
        messageQuota: input.messageQuota,
        messagesSent: 0,
        isAdmin: false,
        role: "hall",
      };
      if (input.hallPassword) hallData.hallPassword = input.hallPassword;
      if (input.subscriptionExpiresAt) hallData.subscriptionExpiresAt = new Date(input.subscriptionExpiresAt);
      if (input.hasBranches !== undefined) hallData.hasBranches = input.hasBranches;
      if (input.googleMapsUrl !== undefined) hallData.googleMapsUrl = input.googleMapsUrl || null;
      const hall = await storage.createUser(hallData);
      await storage.setSetting(hall.id, "venue_name", input.venueName);

      // Send welcome WhatsApp message with access code to the new hall account
      try {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        const msgCount = input.messageQuota === -1 ? "غير محدود" : String(input.messageQuota);
        const hallPhone = input.phoneNumber.replace(/[^0-9]/g, '');
        const msg = `🎉 مرحباً *${input.name}*!\n\nتم إنشاء حساب قاعتك في إنفايتنا نظام إدارة دعوات المناسبات.\n\n🔑 كود الدخول: *${code}*\n🏛️ اسم القاعة: *${input.venueName}*\n📱 رصيد الرسائل: *${msgCount}*\n\n🔗 ادخل من هنا:\n${appUrl}/login?code=${code}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم\n\nشكراً لانضمامكم إلى إنفايتنا!`;
        const sent = await sendViaAnySystem(hallPhone, msg).catch(() => false);
        if (!sent) {
          console.log(`Hall welcome message could not be sent to ${hallPhone}`);
        }
      } catch (e) {
        console.error("Error sending hall welcome message:", e);
      }

      res.status(201).json({ ...hall, venueName: input.venueName, messagesRemaining: hall.messageQuota === -1 ? -1 : hall.messageQuota - hall.messagesSent });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.put("/api/admin/halls/:id", requireAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().max(100).optional(),
        phoneNumber: z.string().max(20).optional(),
        messageQuota: z.number().min(-1).optional(),
        venueName: z.string().max(200).nullable().optional(),
        hallPassword: z.string().max(100).nullable().optional(),
        subscriptionExpiresAt: z.string().nullable().optional(),
        subscriptionType: z.enum(["monthly", "annual"]).optional(),
        hasBranches: z.boolean().optional(),
        googleMapsUrl: z.string().nullable().optional(),
      }).parse(req.body);

      const hallId = Number(req.params.id);
      const currentHall = await storage.getUser(hallId);
      const quotaChanged = input.messageQuota !== undefined && currentHall && input.messageQuota !== currentHall.messageQuota;

      const updates: any = {};
      if (input.name && input.name.trim()) updates.name = input.name.trim();
      if (input.phoneNumber && input.phoneNumber.trim()) {
        let ph = input.phoneNumber.trim();
        if (!ph.startsWith('+')) ph = '+' + ph;
        updates.phoneNumber = ph.replace(/[^0-9]/g, '');
      }
      if (input.messageQuota !== undefined) updates.messageQuota = input.messageQuota;
      if (quotaChanged) updates.messagesSent = 0;
      if (input.hallPassword !== undefined) updates.hallPassword = input.hallPassword || null;
      if (input.subscriptionExpiresAt !== undefined) {
        updates.subscriptionExpiresAt = input.subscriptionExpiresAt ? new Date(input.subscriptionExpiresAt) : null;
      }
      if (input.subscriptionType !== undefined) updates.subscriptionType = input.subscriptionType;
      if (input.hasBranches !== undefined) updates.hasBranches = input.hasBranches;
      if (input.googleMapsUrl !== undefined) updates.googleMapsUrl = input.googleMapsUrl || null;
      const hall = await storage.updateUser(hallId, updates);
      if (quotaChanged) {
        const clients = await storage.getHallClients(hallId);
        for (const client of clients) {
          await storage.updateUser(client.id, { messagesSent: 0 });
        }
      }
      if (input.venueName !== undefined) await storage.setSetting(hall.id, "venue_name", input.venueName || "");
      const venueName = await storage.getSetting(hall.id, "venue_name");
      res.json({ ...hall, venueName: venueName || "", messagesRemaining: hall.messageQuota === -1 ? -1 : hall.messageQuota - hall.messagesSent });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // Hall branches management (admin only)
  app.get("/api/admin/halls/:id/branches", requireAdminOrCoAdmin, async (req, res) => {
    const hallId = Number(req.params.id);
    const branches = await storage.getHallBranches(hallId);
    res.json(branches);
  });

  app.post("/api/admin/halls/:id/branches", requireAdmin, async (req, res) => {
    try {
      const hallId = Number(req.params.id);
      const hall = await storage.getUser(hallId);
      if (!hall || hall.role !== "hall") return res.status(404).json({ message: "القاعة غير موجودة" });
      const { name, googleMapsUrl } = z.object({
        name: z.string().min(1).max(200),
        googleMapsUrl: z.string().optional(),
      }).parse(req.body);
      // Auto-enable branches on the hall if not already set
      if (!hall.hasBranches) {
        await storage.updateUser(hallId, { hasBranches: true, googleMapsUrl: null });
      }
      const branch = await storage.createHallBranch(hallId, name, googleMapsUrl);
      res.status(201).json(branch);
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.put("/api/admin/halls/:id/branches/:branchId", requireAdmin, async (req, res) => {
    try {
      const branchId = Number(req.params.branchId);
      const hallId = Number(req.params.id);
      const hall = await storage.getUser(hallId);
      if (!hall || hall.role !== "hall") return res.status(404).json({ message: "القاعة غير موجودة" });
      const { name, googleMapsUrl } = z.object({
        name: z.string().min(1).max(200),
        googleMapsUrl: z.string().optional(),
      }).parse(req.body);
      const existing = await storage.getHallBranch(branchId);
      if (!existing || existing.hallId !== hallId) return res.status(404).json({ message: "الفرع غير موجود" });
      const branch = await storage.updateHallBranch(branchId, name, googleMapsUrl);
      res.json(branch);
    } catch {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.delete("/api/admin/halls/:id/branches/:branchId", requireAdmin, async (req, res) => {
    const branchId = Number(req.params.branchId);
    const hallId = Number(req.params.id);
    const hall = await storage.getUser(hallId);
    if (!hall || !hall.hasBranches) return res.status(400).json({ message: "هذه القاعة لا تدعم الفروع" });
    const existing = await storage.getHallBranch(branchId);
    if (!existing || existing.hallId !== hallId) return res.status(404).json({ message: "الفرع غير موجود" });
    await storage.deleteHallBranch(branchId);
    res.json({ success: true });
  });

  app.post("/api/admin/halls/:id/reset-balance", requireAdmin, async (req, res) => {
    try {
      const hallId = Number(req.params.id);
      const hall = await storage.getUser(hallId);
      if (!hall || hall.role !== "hall") return res.status(404).json({ message: "القاعة غير موجودة" });
      await storage.resetHallBalance(hallId);
      const updatedHall = await storage.getUser(hallId);
      res.json({ success: true, messagesRemaining: updatedHall!.messageQuota === -1 ? -1 : updatedHall!.messageQuota - updatedHall!.messagesSent });
    } catch (err) {
      res.status(400).json({ message: "فشل التصفير" });
    }
  });

  app.post("/api/admin/halls/:id/suspend", requireAdmin, async (req, res) => {
    const { suspended } = z.object({ suspended: z.boolean() }).parse(req.body);
    const hallId = Number(req.params.id);
    const hall = await storage.suspendUser(hallId, suspended);
    await storage.cascadeSuspendHallClients(hallId, suspended);
    res.json({ success: true, isSuspended: hall.isSuspended });
  });

  app.post("/api/admin/halls/:id/confirm-payment", requireAdmin, async (req, res) => {
    try {
      const { subscriptionType = "monthly" } = z.object({ subscriptionType: z.enum(["monthly", "annual"]).default("monthly") }).parse(req.body || {});
      const hall = await storage.confirmHallPayment(Number(req.params.id), subscriptionType);
      res.json({
        success: true,
        subscriptionType: hall.subscriptionType,
        subscriptionExpiresAt: hall.subscriptionExpiresAt ? (hall.subscriptionExpiresAt as Date).toISOString() : null,
        subscriptionStartedAt: hall.subscriptionStartedAt ? (hall.subscriptionStartedAt as Date).toISOString() : null,
        isSuspended: hall.isSuspended,
      });
    } catch (e: any) {
      res.status(400).json({ error: e.message });
    }
  });

  app.delete("/api/admin/halls/:id", requireAdmin, async (req, res) => {
    const hallId = Number(req.params.id);
    const clients = await storage.getHallClients(hallId);
    for (const client of clients) {
      const clientAccounts = await storage.getWhatsappAccounts(client.id);
      for (const acc of clientAccounts) {
        await waManager.removeSession(acc.id);
      }
      await storage.deleteAllUserData(client.id);
    }
    const hallAccounts = await storage.getWhatsappAccounts(hallId);
    for (const acc of hallAccounts) {
      await waManager.removeSession(acc.id);
    }
    await storage.deleteAllUserData(hallId);
    res.status(204).send();
  });

  // ========== CO-ADMIN SAFE USER LIST ==========
  // API contract: Co-Admins use GET /api/co-admin/users (not /api/admin/users) to
  // prevent privilege escalation — this endpoint filters out isAdmin and isCoAdmin accounts
  // so co-admins cannot see or copy admin access codes. Regular admins may also use this
  // endpoint to get the same filtered view.
  app.get("/api/co-admin/users", requireAdminOrCoAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const safeUsers = allUsers.filter(u => !u.isAdmin && !u.isCoAdmin);
    res.json(safeUsers.map(u => ({
      id: u.id,
      name: u.name,
      phoneNumber: u.phoneNumber,
      accessCode: u.accessCode,
      messageQuota: u.messageQuota,
      messagesSent: u.messagesSent,
      role: u.role,
      parentHallId: u.parentHallId,
      isSuspended: u.isSuspended,
    })));
  });

  // ========== EXTRA ADMINS MANAGEMENT ==========

  app.get("/api/admin/extra-admins", requireAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const allAdmins = allUsers.filter(u => u.isAdmin).sort((a, b) => a.id - b.id);
    // Exclude primary admin (lowest id) — extra admins are secondary admin accounts
    const primaryAdminId = allAdmins[0]?.id;
    const extraAdmins = allAdmins.filter(u => u.id !== primaryAdminId);
    res.json(extraAdmins.map(u => ({
      id: u.id, name: u.name, phoneNumber: u.phoneNumber,
      accessCode: u.accessCode, createdAt: u.createdAt,
    })));
  });

  app.post("/api/admin/extra-admins", requireAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().min(1).max(100),
        phoneNumber: z.string().min(1).max(20),
      }).parse(req.body);
      if (!input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;
      const code = generateAccessCode();
      const newAdmin = await storage.createUser({
        name: input.name,
        phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
        accessCode: code,
        messageQuota: -1,
        messagesSent: 0,
        isAdmin: true,
        isCoAdmin: false,
        phoneVerified: true,
      });
      await storage.addAuditLog({ action: "create_extra_admin", details: `إنشاء أدمن إضافي: ${input.name}`, ip: req.ip || "", actorId: req.session.userId! }).catch(() => {});
      try {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        const msg = `🔑 تم إنشاء حساب أدمن في إنفايتنا\n\nالاسم: *${input.name}*\nكود الدخول: *${code}*\n\n${appUrl}/login?code=${code}`;
        await sendViaAnySystem(input.phoneNumber.replace(/[^0-9]/g, ''), msg).catch(() => {});
      } catch {}
      res.status(201).json({ id: newAdmin.id, name: newAdmin.name, phoneNumber: newAdmin.phoneNumber, accessCode: code });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.delete("/api/admin/extra-admins/:id", requireAdmin, async (req, res) => {
    const targetId = Number(req.params.id);
    const requesterId = req.session.userId!;
    const target = await storage.getUser(targetId);
    if (!target || !target.isAdmin) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    const allUsers = await storage.getAllUsers();
    const allAdmins = allUsers.filter(u => u.isAdmin).sort((a, b) => a.id - b.id);
    // Prevent deleting last admin (checked first to show the correct message)
    if (allAdmins.length <= 1) {
      return res.status(400).json({ message: "لا يمكن حذف آخر حساب أدمن في النظام" });
    }
    // Protect original (primary) admin — the one with the lowest id
    const primaryAdminId = allAdmins[0]?.id;
    if (targetId === primaryAdminId) {
      return res.status(400).json({ message: "لا يمكن حذف الأدمن الأصلي للنظام" });
    }
    await storage.deleteAllUserData(targetId);
    await storage.addAuditLog({ action: "delete_extra_admin", details: `حذف أدمن: ${target.name}`, ip: req.ip || "", actorId: requesterId }).catch(() => {});
    res.status(204).send();
  });

  // ========== CO-ADMIN MANAGEMENT ==========

  app.get("/api/admin/co-admins", requireAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const coAdmins = allUsers.filter(u => u.isCoAdmin);
    res.json(coAdmins.map(u => ({
      id: u.id, name: u.name, phoneNumber: u.phoneNumber,
      accessCode: u.accessCode, createdAt: u.createdAt,
    })));
  });

  app.post("/api/admin/co-admins", requireAdmin, async (req, res) => {
    try {
      const input = z.object({
        name: z.string().min(1).max(100),
        phoneNumber: z.string().min(1).max(20),
      }).parse(req.body);
      if (!input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;
      const code = generateAccessCode();
      const newCoAdmin = await storage.createUser({
        name: input.name,
        phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
        accessCode: code,
        messageQuota: -1,
        messagesSent: 0,
        isAdmin: false,
        isCoAdmin: true,
        phoneVerified: true,
      });
      await storage.addAuditLog({ action: "create_co_admin", details: `إنشاء Co-Admin: ${input.name}`, ip: req.ip || "", actorId: req.session.userId! }).catch(() => {});
      try {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        const msg = `🔑 تم إنشاء حساب مساعد أدمن في إنفايتنا\n\nالاسم: *${input.name}*\nكود الدخول: *${code}*\nالصلاحيات: إضافة قاعات + إعادة توليد كودات المستخدمين\n\n${appUrl}/login?code=${code}`;
        await sendViaAnySystem(input.phoneNumber.replace(/[^0-9]/g, ''), msg).catch(() => {});
      } catch {}
      res.status(201).json({ id: newCoAdmin.id, name: newCoAdmin.name, phoneNumber: newCoAdmin.phoneNumber, accessCode: code });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.delete("/api/admin/co-admins/:id", requireAdmin, async (req, res) => {
    const targetId = Number(req.params.id);
    const target = await storage.getUser(targetId);
    if (!target || !target.isCoAdmin) {
      return res.status(404).json({ message: "الحساب غير موجود" });
    }
    await storage.deleteAllUserData(targetId);
    await storage.addAuditLog({ action: "delete_co_admin", details: `حذف Co-Admin: ${target.name}`, ip: req.ip || "", actorId: req.session.userId! }).catch(() => {});
    res.status(204).send();
  });

  // Regen user access code — accessible by admin and co-admin
  app.post("/api/admin/users/:id/regen-code", requireAdminOrCoAdmin, async (req, res) => {
    const targetId = Number(req.params.id);
    const target = await storage.getUser(targetId);
    if (!target) return res.status(404).json({ message: "المستخدم غير موجود" });
    if (target.isAdmin || target.isCoAdmin) {
      return res.status(400).json({ message: "لا يمكن إعادة توليد كود حساب الأدمن" });
    }
    const newCode = generateAccessCode();
    await storage.updateUser(targetId, { accessCode: newCode });
    await storage.addAuditLog({ action: "regen_user_code", details: `إعادة توليد كود: ${target.name}`, ip: req.ip || "", actorId: req.session.userId! }).catch(() => {});
    let sent = false;
    if (target.phoneNumber) {
      const loginUrl = `${req.protocol}://${req.get('host')}/login?code=${newCode}`;
      const msg = `🔑 كود الدخول الجديد لحسابك في إنفايتنا:\n\n*${newCode}*\n\nأو ادخل مباشرة:\n${loginUrl}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم`;
      try { sent = await sendViaAnySystem(target.phoneNumber, msg); } catch {}
    }
    res.json({ sent, newCode });
  });

  // ========== HALL DASHBOARD ENDPOINTS ==========

  app.get("/api/hall/me", requireHall, async (req, res) => {
    const hall = await storage.getUser(req.session.userId!);
    if (!hall) return res.status(404).json({ message: "Not found" });
    const venueName = await storage.getSetting(hall.id, "venue_name");
    const totalQuota = hall.messageQuota;
    const allocatedQuota = hall.messagesSent;
    const remainingQuota = totalQuota === -1 ? -1 : totalQuota - allocatedQuota;
    res.json({
      id: hall.id,
      name: hall.name,
      phoneNumber: hall.phoneNumber,
      accessCode: hall.accessCode,
      messageQuota: totalQuota,
      messagesSent: allocatedQuota,
      messagesRemaining: remainingQuota,
      isSuspended: hall.isSuspended,
      venueName: venueName || "",
      subscriptionExpiresAt: hall.subscriptionExpiresAt ? hall.subscriptionExpiresAt.toISOString() : null,
      billingStatus: hall.billingStatus || null,
      nextBillingAt: hall.nextBillingAt ? hall.nextBillingAt.toISOString() : null,
      hasBranches: hall.hasBranches ?? false,
      googleMapsUrl: hall.googleMapsUrl || null,
    });
  });

  app.get("/api/hall/branches", requireHall, async (req, res) => {
    const branches = await storage.getHallBranches(req.session.userId!);
    res.json(branches);
  });

  app.get("/api/hall/stats", requireHall, requireHallBilling, async (req, res) => {
    const stats = await storage.getHallStats(req.session.userId!);
    res.json(stats);
  });

  app.put("/api/hall/password", requireHall, async (req, res) => {
    const { password } = z.object({ password: z.string().max(100) }).parse(req.body);
    await storage.updateUser(req.session.userId!, { hallPassword: password || null });
    res.json({ success: true });
  });

  app.get("/api/hall/today-stats", requireHall, requireHallBilling, async (req, res) => {
    const hallId = req.session.userId!;
    const clients = await storage.getHallClients(hallId);
    const todayStr = new Date().toDateString(); // e.g. "Mon Mar 30 2026" — timezone-local
    // Find clients with event_date = today using flexible parsing (same as weekly schedule)
    const todayClientIds: number[] = [];
    for (const c of clients) {
      const eventDateRaw = await storage.getSetting(c.id, "event_date");
      if (!eventDateRaw) continue;
      let d = new Date(eventDateRaw);
      if (isNaN(d.getTime())) {
        const m1 = eventDateRaw.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
        if (m1) d = new Date(parseInt(m1[1]), parseInt(m1[2]) - 1, parseInt(m1[3]));
        else {
          const m2 = eventDateRaw.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
          if (m2) d = new Date(parseInt(m2[3]), parseInt(m2[2]) - 1, parseInt(m2[1]));
        }
      }
      if (!isNaN(d.getTime()) && d.toDateString() === todayStr) todayClientIds.push(c.id);
    }
    if (todayClientIds.length === 0) {
      return res.json({ todayTotal: 0, todayConfirmed: 0, todayCheckedIn: 0, hasEventsToday: false });
    }
    const allGuests = await storage.getHallAllGuests(hallId);
    const todayGuests = allGuests.filter(g => todayClientIds.includes(g.userId));
    const todayTotal = todayGuests.length;
    const todayConfirmed = todayGuests.filter(g => g.status === "confirmed").length;
    const todayCheckedIn = todayGuests.filter(g => g.checkedIn).length;
    res.json({ todayTotal, todayConfirmed, todayCheckedIn, hasEventsToday: true });
  });

  app.get("/api/hall/guests", requireHall, requireHallBilling, async (req, res) => {
    const guests = await storage.getHallAllGuests(req.session.userId!);
    const clientGuestCounts: Record<number, number> = {};
    const masked = guests.map(g => {
      clientGuestCounts[g.clientId] = (clientGuestCounts[g.clientId] || 0) + 1;
      const phone: string = g.phoneNumber || "";
      const prefix = phone.startsWith("+") ? phone.slice(0, Math.min(4, Math.max(0, phone.length - 4))) : "";
      const last4 = phone.slice(-4);
      const dots = "•".repeat(Math.max(0, phone.length - prefix.length - 4));
      return {
        ...g,
        name: `ضيف`,
        phoneNumber: phone ? `${prefix}${dots}${last4}` : "",
        clientGuestIndex: clientGuestCounts[g.clientId],
      };
    });
    res.json(masked);
  });

  app.get("/api/hall/clients", requireHall, requireHallBilling, async (req, res) => {
    const hallId = req.session.userId!;
    const clients = await storage.getHallClients(hallId);
    // Build a map of primary user names for secondaries
    const primaryIds = [...new Set(clients.filter(c => c.primaryUserId).map(c => c.primaryUserId!))];
    const primaryMap = new Map<number, string>();
    for (const pid of primaryIds) {
      const p = await storage.getUser(pid);
      if (p) primaryMap.set(pid, p.name);
    }
    // Pre-fetch all branches for this hall in one query and build a lookup map
    const hallBranches = await storage.getHallBranches(hallId);
    const branchMap = new Map<number, string>(hallBranches.map(b => [b.id, b.name]));

    const result = await Promise.all(clients.map(async c => {
      const scanners = await storage.getScannersForUser(c.id);
      const eventDetails = await storage.getEventDetails(c.id);
      let branchName: string | null = null;
      if (eventDetails.selected_branch_id) {
        const branchId = parseInt(eventDetails.selected_branch_id);
        if (!isNaN(branchId)) branchName = branchMap.get(branchId) ?? null;
      }
      return {
        id: c.id,
        name: c.name,
        phoneNumber: c.phoneNumber,
        messageQuota: c.messageQuota,
        messagesSent: c.messagesSent,
        messagesRemaining: c.messageQuota - c.messagesSent,
        currentMonthQuota: c.currentMonthQuota,
        isSuspended: c.isSuspended,
        createdAt: c.createdAt,
        primaryUserId: c.primaryUserId ?? null,
        primaryUserName: c.primaryUserId ? (primaryMap.get(c.primaryUserId) ?? null) : null,
        scanners: scanners.map(s => ({ id: s.id, name: s.name, accessCode: s.accessCode })),
        eventDate: eventDetails.event_date || "",
        eventTime: eventDetails.event_time || "",
        includeQr: eventDetails.include_qr === "true",
        mapLink: eventDetails.event_map_link || "",
        branchName,
      };
    }));
    res.json(result);
  });

  app.get("/api/hall/clients/archived", requireHall, async (req, res) => {
    const archived = await storage.getHallArchivedClients(req.session.userId!);
    const result = archived.map(c => ({
      id: c.id,
      name: c.name,
      messagesRemaining: c.messageQuota - c.messagesSent,
      archivedAt: c.archivedAt ?? null,
    }));
    res.json(result);
  });

  app.post("/api/hall/clients", requireHall, requireHallBilling, async (req, res) => {
    try {
      const hall = await storage.getUser(req.session.userId!);
      if (!hall) return res.status(404).json({ message: "Not found" });

      const input = z.object({
        name: z.string().min(1).max(100),
        phoneNumber: z.string().min(1).max(20),
        messageQuota: z.number().min(0),
        includeQr: z.boolean().optional(),
        eventDate: z.string().min(1).max(50),
        eventTime: z.string().max(50).optional(),
        branchId: z.number().optional(),
      }).parse(req.body);

      // If hall has branches, validate branchId BEFORE any user creation
      let resolvedBranch: Awaited<ReturnType<typeof storage.getHallBranch>> | undefined;
      if (hall.hasBranches) {
        if (!input.branchId) {
          return res.status(400).json({ message: "يجب اختيار الفرع" });
        }
        resolvedBranch = await storage.getHallBranch(input.branchId);
        if (!resolvedBranch || resolvedBranch.hallId !== hall.id) {
          return res.status(403).json({ message: "الفرع المحدد غير مرتبط بهذه القاعة" });
        }
      }

      if (!input.phoneNumber.startsWith('+')) input.phoneNumber = '+' + input.phoneNumber;

      // Prevent duplicate phone numbers within the same hall (active only — archived/deleted don't count)
      const normalizedPhone = input.phoneNumber.replace(/[^0-9]/g, '');
      const existingWithPhone = await storage.getUsersByPhone(normalizedPhone);
      const duplicate = existingWithPhone.find(
        u => u.parentHallId === hall.id && u.role === "user" && !u.isArchived
      );
      if (duplicate) {
        return res.status(400).json({ message: "يوجد عميل مسجّل بهذا الرقم مسبقاً في القاعة" });
      }

      if (hall.messageQuota !== -1) {
        const hallRemaining = hall.messageQuota - hall.messagesSent;
        if (input.messageQuota > hallRemaining) {
          return res.status(400).json({ message: `رصيد القاعة غير كافٍ. المتبقي: ${hallRemaining}` });
        }
      }

      const code = generateAccessCode();
      const venueName = await storage.getSetting(hall.id, "venue_name") || "";

      const client = await storage.createUser({
        name: input.name,
        phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
        accessCode: code,
        messageQuota: input.messageQuota,
        messagesSent: 0,
        isAdmin: false,
        role: "user",
        parentHallId: hall.id,
        currentMonthQuota: input.messageQuota,
      });

      if (venueName) {
        await storage.setSetting(client.id, "event_venue_name", venueName);
      }
      if (input.eventDate) {
        await storage.setSetting(client.id, "event_date", input.eventDate);
      }
      if (input.eventTime) {
        await storage.setSetting(client.id, "event_time", input.eventTime);
      }
      // Copy Google Maps link to client settings (from branch or hall)
      if (hall.hasBranches && resolvedBranch) {
        if (resolvedBranch.googleMapsUrl) {
          await storage.setSetting(client.id, "event_map_link", resolvedBranch.googleMapsUrl);
        }
        await storage.setSetting(client.id, "selected_branch_id", String(resolvedBranch.id));
      } else if (hall.googleMapsUrl) {
        await storage.setSetting(client.id, "event_map_link", hall.googleMapsUrl);
      } else {
        const hallMapLink = await storage.getSetting(hall.id, "venue_map_link");
        if (hallMapLink) {
          await storage.setSetting(client.id, "event_map_link", hallMapLink);
        }
      }
      await storage.setSetting(client.id, "include_qr", input.includeQr ? "true" : "false");

      // Create scanner account automatically if QR is enabled
      if (input.includeQr) {
        const scannerCode = generateAccessCode();
        const scanner = await storage.createUser({
          name: `بواب - ${input.name}`,
          phoneNumber: input.phoneNumber.replace(/[^0-9]/g, ''),
          accessCode: scannerCode,
          messageQuota: 0,
          messagesSent: 0,
          isAdmin: false,
          role: "scanner",
          parentHallId: hall.id,
        });
        await storage.setSetting(scanner.id, "linked_user_id", String(client.id));
      }

      await storage.adjustHallAllocated(hall.id, input.messageQuota);

      // Send welcome WhatsApp message with access code to the new client
      let whatsappSent = false;
      try {
        const appUrl = `${req.protocol}://${req.get('host')}`;
        const hallName = hall.name || "القاعة";
        const msgCount = input.messageQuota === -1 ? "غير محدود" : String(input.messageQuota);
        const clientPhone = input.phoneNumber.replace(/[^0-9]/g, '');
        const msg = `🎉 مرحباً *${input.name}*!\n\nتم إنشاء حسابك في إنفايتنا نظام إدارة دعوات المناسبات.\n\n🔑 كود الدخول: *${code}*\n📱 رصيد الرسائل: *${msgCount}*\n\n🔗 ادخل من هنا:\n${appUrl}/login?code=${code}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم\n\nتم إنشاء الحساب بواسطة: *${hallName}*`;
        const greenResult = await sendViaGreenAPI(clientPhone, msg).catch(() => ({ ok: false, error: "exception" }));
        whatsappSent = greenResult.ok;
        if (!whatsappSent) {
          console.log(`Hall client welcome message could not be sent via Green API to ${clientPhone}: ${greenResult.error}`);
        }
      } catch (e) {
        console.error("Error sending hall client welcome message:", e);
      }

      res.status(201).json({ ...client, messagesRemaining: client.messageQuota - client.messagesSent, whatsappSent });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // Toggle QR for a client: enable creates scanner, disable deletes all scanners for client
  app.post("/api/hall/clients/:id/toggle-qr", requireHall, requireHallBilling, async (req, res) => {
    try {
      const clientId = Number(req.params.id);
      const hall = await storage.getUser(req.session.userId!);
      if (!hall) return res.status(404).json({ message: "Not found" });

      const client = await storage.getUser(clientId);
      if (!client || client.parentHallId !== hall.id) {
        return res.status(403).json({ message: "غير مصرح" });
      }

      const { enable } = z.object({ enable: z.boolean() }).parse(req.body);

      await storage.setSetting(clientId, "include_qr", enable ? "true" : "false");

      if (enable) {
        // Create a scanner account if none exists
        const existingScanners = await storage.getScannersForUser(clientId);
        if (existingScanners.length === 0) {
          const scannerCode = generateAccessCode();
          const scanner = await storage.createUser({
            name: `بواب - ${client.name}`,
            phoneNumber: client.phoneNumber,
            accessCode: scannerCode,
            messageQuota: 0,
            messagesSent: 0,
            isAdmin: false,
            role: "scanner",
            parentHallId: hall.id,
          });
          await storage.setSetting(scanner.id, "linked_user_id", String(clientId));
        }
        res.json({ success: true, qrEnabled: true });
      } else {
        // Delete all scanner accounts for this client
        const scanners = await storage.getScannersForUser(clientId);
        for (const s of scanners) {
          await waManager.removeSession(s.id);
          await storage.deleteAllUserData(s.id);
          await storage.deleteUser(s.id);
        }
        res.json({ success: true, qrEnabled: false });
      }
    } catch (err: any) {
      res.status(400).json({ message: err.message || "خطأ" });
    }
  });

  // Add a scanner account for a client (max 2 per client)
  app.post("/api/hall/clients/:id/add-scanner", requireHall, requireHallBilling, async (req, res) => {
    try {
      const clientId = Number(req.params.id);
      const hall = await storage.getUser(req.session.userId!);
      if (!hall) return res.status(404).json({ message: "Not found" });
      const client = await storage.getUser(clientId);
      if (!client || client.parentHallId !== hall.id) return res.status(403).json({ message: "غير مصرح" });
      const existingScanners = await storage.getScannersForUser(clientId);
      if (existingScanners.length >= 2) return res.status(400).json({ message: "الحد الأقصى لعدد البوابين 2" });
      const scannerCode = generateAccessCode();
      const scannerNum = existingScanners.length + 1;
      const scanner = await storage.createUser({
        name: `بواب ${scannerNum} - ${client.name}`,
        phoneNumber: client.phoneNumber,
        accessCode: scannerCode,
        messageQuota: 0,
        messagesSent: 0,
        isAdmin: false,
        role: "scanner",
        parentHallId: hall.id,
      });
      await storage.setSetting(scanner.id, "linked_user_id", String(clientId));
      await storage.setSetting(clientId, "include_qr", "true");
      res.json({ success: true, scanner: { id: scanner.id, name: scanner.name, accessCode: scannerCode } });
    } catch (err: any) {
      res.status(400).json({ message: err.message || "خطأ" });
    }
  });

  // Delete a specific scanner account (hall only)
  app.delete("/api/hall/scanners/:scannerId", requireHall, requireHallBilling, async (req, res) => {
    try {
      const scannerId = Number(req.params.scannerId);
      const hall = await storage.getUser(req.session.userId!);
      if (!hall) return res.status(404).json({ message: "Not found" });
      const scanner = await storage.getUser(scannerId);
      if (!scanner || scanner.role !== "scanner" || scanner.parentHallId !== hall.id) {
        return res.status(403).json({ message: "غير مصرح" });
      }
      await waManager.removeSession(scannerId);
      await storage.deleteAllUserData(scannerId);
      await storage.deleteUser(scannerId);
      res.json({ success: true });
    } catch (err: any) {
      res.status(400).json({ message: err.message || "خطأ" });
    }
  });

  // Hall today's schedule: clients with events today
  app.get("/api/hall/weekly", requireHall, requireHallBilling, async (req, res) => {
    try {
      const hallId = req.session.userId!;
      const clients = await storage.getHallClients(hallId);

      // Build primaryUserName map for secondary clients
      const weeklyPrimaryIds = [...new Set(clients.map((c: any) => c.primaryUserId).filter(Boolean))] as number[];
      const weeklyPrimaryNameMap = new Map<number, string>();
      for (const pid of weeklyPrimaryIds) {
        const p = await storage.getUser(pid);
        if (p) weeklyPrimaryNameMap.set(pid, p.name);
      }

      // Pre-fetch all branches for this hall once and build a lookup map
      const weeklyHallBranches = await storage.getHallBranches(hallId);
      const weeklyBranchMap = new Map<number, string>(weeklyHallBranches.map(b => [b.id, b.name]));

      const now = new Date();
      now.setHours(0, 0, 0, 0);
      const weekEnd = new Date(now);
      weekEnd.setDate(weekEnd.getDate() + 7);
      const result = [];
      for (const client of clients) {
        const eventDetails = await storage.getEventDetails(client.id);
        if (!eventDetails.event_date) continue;
        let eventDate: Date | null = null;
        const d = new Date(eventDetails.event_date);
        if (!isNaN(d.getTime())) { eventDate = d; }
        else {
          const m1 = eventDetails.event_date.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
          if (m1) eventDate = new Date(parseInt(m1[1]), parseInt(m1[2]) - 1, parseInt(m1[3]));
          else {
            const m2 = eventDetails.event_date.match(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);
            if (m2) eventDate = new Date(parseInt(m2[3]), parseInt(m2[2]) - 1, parseInt(m2[1]));
          }
        }
        if (!eventDate) continue;
        eventDate.setHours(0, 0, 0, 0);
        if (eventDate < now || eventDate >= weekEnd) continue;
        const guests = await storage.getGuests(client.id);
        const totalGuests = guests.length;
        const sentGuests = guests.filter((g: any) => g.status !== "pending").length;
        const confirmedGuests = guests.filter((g: any) => g.status === "confirmed").length;
        const scanners = await storage.getScannersForUser(client.id);
        const isToday = eventDate.toDateString() === new Date().toDateString();
        const dayLabel = eventDate.toLocaleDateString("ar-SA", { weekday: "long", day: "numeric", month: "long" });
        let branchName: string | null = null;
        let branchId: number | null = null;
        if (eventDetails.selected_branch_id) {
          const parsedBranchId = parseInt(eventDetails.selected_branch_id);
          if (!isNaN(parsedBranchId)) {
            branchId = parsedBranchId;
            branchName = weeklyBranchMap.get(parsedBranchId) ?? null;
          }
        }
        result.push({
          id: client.id,
          name: client.name,
          primaryUserId: client.primaryUserId ?? null,
          primaryUserName: client.primaryUserId ? (weeklyPrimaryNameMap.get(client.primaryUserId) ?? null) : null,
          eventTime: eventDetails.event_time || "",
          eventDate: eventDetails.event_date,
          eventDateObj: eventDate.toISOString(),
          dayLabel,
          isToday,
          totalGuests,
          sentGuests,
          confirmedGuests,
          branchId,
          branchName,
          scanners: scanners.map((s: any) => ({ id: s.id, name: s.name, accessCode: s.accessCode })),
        });
      }
      result.sort((a, b) => new Date(a.eventDateObj).getTime() - new Date(b.eventDateObj).getTime());
      res.json(result);
    } catch (err: any) {
      res.status(500).json({ message: err.message || "خطأ" });
    }
  });

  app.put("/api/hall/clients/:id", requireHall, requireHallBilling, async (req, res) => {
    try {
      const clientId = Number(req.params.id);
      const hall = await storage.getUser(req.session.userId!);
      if (!hall) return res.status(404).json({ message: "Not found" });

      const client = await storage.getUser(clientId);
      if (!client || client.parentHallId !== hall.id) {
        return res.status(403).json({ message: "غير مصرح" });
      }

      const input = z.object({
        name: z.string().min(1).max(100).optional(),
        phoneNumber: z.string().optional(),
        messageQuota: z.number().min(0).optional(),
        eventDate: z.string().optional(),
        eventTime: z.string().optional(),
        includeQr: z.boolean().optional(),
        mapLink: z.string().optional(),
      }).parse(req.body);

      if (input.messageQuota !== undefined) {
        const diff = input.messageQuota - client.messageQuota;
        if (diff > 0) {
          if (hall.messageQuota !== -1) {
            const hallRemaining = hall.messageQuota - hall.messagesSent;
            if (diff > hallRemaining) {
              return res.status(400).json({ message: `رصيد القاعة غير كافٍ. المتبقي: ${hallRemaining}` });
            }
          }
          await storage.adjustHallAllocated(hall.id, diff);
          await storage.adjustClientMonthQuota(clientId, diff);
        } else if (diff < 0) {
          await storage.adjustHallAllocated(hall.id, diff);
          await storage.adjustClientMonthQuota(clientId, diff);
        }
      }

      const updates: Partial<Omit<User, 'id' | 'createdAt'>> = {};
      if (input.name) updates.name = input.name;
      if (input.phoneNumber) {
        const normalizedPhone = input.phoneNumber.replace(/[^0-9]/g, '');
        const existingWithPhone = await storage.getUsersByPhone(normalizedPhone);
        const duplicate = existingWithPhone.find(
          u => u.parentHallId === hall.id && u.role === "user" && u.id !== clientId
        );
        if (duplicate) {
          return res.status(400).json({ message: "يوجد عميل مسجّل بهذا الرقم مسبقاً في القاعة" });
        }
        updates.phoneNumber = normalizedPhone;
      }
      if (input.messageQuota !== undefined) updates.messageQuota = input.messageQuota;

      const updated = await storage.updateUser(clientId, updates);

      // Update event settings
      if (input.eventDate !== undefined) await storage.setSetting(clientId, "event_date", input.eventDate);
      if (input.eventTime !== undefined) await storage.setSetting(clientId, "event_time", input.eventTime);
      if (input.mapLink !== undefined) await storage.setSetting(clientId, "event_map_link", input.mapLink);
      if (input.includeQr !== undefined) {
        await storage.setSetting(clientId, "include_qr", input.includeQr ? "true" : "false");
        if (input.includeQr) {
          const existingScanners = await storage.getScannersForUser(clientId);
          if (existingScanners.length === 0) {
            const scannerCode = generateAccessCode();
            const scanner = await storage.createUser({
              name: `بواب - ${updated.name}`,
              phoneNumber: updated.phoneNumber,
              accessCode: scannerCode,
              messageQuota: 0,
              messagesSent: 0,
              isAdmin: false,
              role: "scanner",
              parentHallId: hall.id,
            });
            await storage.setSetting(scanner.id, "linked_user_id", String(clientId));
          }
        }
      }

      res.json({ ...updated, messagesRemaining: updated.messageQuota - updated.messagesSent });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  // Hall venue map link setting
  app.get("/api/hall/venue-map-link", requireHall, requireHallBilling, async (req, res) => {
    const hall = await storage.getUser(req.session.userId!);
    if (!hall) return res.status(404).json({ message: "Not found" });
    const mapLink = await storage.getSetting(req.session.userId!, "venue_map_link");
    // Locked if: admin set googleMapsUrl (no branches), OR hall has branches (per-client URL)
    const isLockedByAdmin = !!(hall.googleMapsUrl && !hall.hasBranches);
    const isLockedByBranches = !!hall.hasBranches;
    res.json({
      mapLink: isLockedByAdmin ? (hall.googleMapsUrl || "") : (mapLink || ""),
      isLockedByAdmin,
      isLockedByBranches,
      adminMapUrl: isLockedByAdmin ? (hall.googleMapsUrl || "") : null,
    });
  });

  app.put("/api/hall/venue-map-link", requireHall, async (req, res) => {
    // Halls cannot edit this — only admin can set it via the admin panel
    return res.status(403).json({ message: "رابط الخريطة يُحدد من قِبل الإدارة فقط ولا يمكن تعديله" });
  });

  app.post("/api/hall/clients/:id/suspend", requireHall, requireHallBilling, async (req, res) => {
    const clientId = Number(req.params.id);
    const hall = await storage.getUser(req.session.userId!);
    if (!hall) return res.status(404).json({ message: "Not found" });
    const client = await storage.getUser(clientId);
    if (!client || client.parentHallId !== hall.id) {
      return res.status(403).json({ message: "غير مصرح" });
    }
    const { suspended } = z.object({ suspended: z.boolean() }).parse(req.body);
    const updated = await storage.suspendUser(clientId, suspended);
    res.json({ success: true, isSuspended: updated.isSuspended });
  });

  app.delete("/api/hall/clients/:id", requireHall, requireHallBilling, async (req, res) => {
    const clientId = Number(req.params.id);
    const hall = await storage.getUser(req.session.userId!);
    if (!hall) return res.status(404).json({ message: "Not found" });
    const client = await storage.getUser(clientId);
    if (!client || client.parentHallId !== hall.id) {
      return res.status(403).json({ message: "غير مصرح" });
    }
    // Idempotent: already archived → nothing to refund
    if (client.isArchived) {
      return res.json({ refunded: 0, kept: client.messageQuota });
    }
    // Block archiving if client has an active discount code and still has messages remaining
    const clientDiscountCode = await storage.getSetting(clientId, "discount_code");
    if (clientDiscountCode) {
      const remaining = Math.max(0, (client.messageQuota ?? 0) - (client.messagesSent ?? 0));
      if (remaining > 0) {
        return res.status(400).json({
          message: `لا يمكن أرشفة هذا العميل لأنه يملك كود خصم نشط ولا يزال لديه ${remaining} دعوة متبقية. يمكن الأرشفة بعد استنفاد رصيده.`
        });
      }
    }
    // Only refund quota allocated this month, capped by what the hall actually has allocated
    const refundQuota = Math.min(client.currentMonthQuota, hall.messagesSent);
    if (refundQuota > 0) {
      await storage.adjustHallAllocated(hall.id, -refundQuota);
    }
    // Stop WhatsApp sessions but keep the account (archive only); clear currentMonthQuota atomically
    const clientAccounts = await storage.getWhatsappAccounts(clientId);
    for (const acc of clientAccounts) {
      await waManager.removeSession(acc.id);
    }
    await storage.updateUser(clientId, { isArchived: true, archivedAt: new Date(), currentMonthQuota: 0 });
    res.json({ refunded: refundQuota, kept: client.messageQuota - refundQuota });
  });

  // Permanent delete — removes client + scanners + all their data, refunds current-month quota only
  app.delete("/api/hall/clients/:id/permanent", requireHall, requireHallBilling, async (req, res) => {
    const clientId = Number(req.params.id);
    const hall = await storage.getUser(req.session.userId!);
    if (!hall) return res.status(404).json({ message: "Not found" });
    const client = await storage.getUser(clientId);
    if (!client || client.parentHallId !== hall.id) {
      return res.status(403).json({ message: "غير مصرح" });
    }

    // Block deletion if client has a discount code and event date is within 30 days
    const discountCode = await storage.getSetting(clientId, "discount_code");
    if (discountCode) {
      const eventDetails = await storage.getEventDetails(clientId);
      const eventDateStr = eventDetails["event_date"];
      if (eventDateStr) {
        const eventDate = new Date(eventDateStr);
        if (!isNaN(eventDate.getTime())) {
          const unlockDate = new Date(eventDate.getTime() + 30 * 24 * 60 * 60 * 1000);
          if (new Date() < unlockDate) {
            const unlockDateStr = unlockDate.toLocaleDateString("ar-SA");
            return res.status(403).json({
              message: `لا يمكن حذف هذا الحساب لأنه حصل على كود خصم. يمكن الحذف بعد ${unlockDateStr} (30 يوم من تاريخ المناسبة). الرصيد غير مسترد.`
            });
          }
        }
      }
    }

    // Refund current-month quota only if no discount code was issued (non-refundable after code claim)
    const refundQuota = discountCode ? 0 : Math.min(client.currentMonthQuota ?? 0, hall.messagesSent ?? 0);
    if (refundQuota > 0) {
      await storage.adjustHallAllocated(hall.id, -refundQuota);
    }
    // Stop WA sessions for client
    const clientAccounts = await storage.getWhatsappAccounts(clientId);
    for (const acc of clientAccounts) {
      await waManager.removeSession(acc.id);
    }
    // Delete scanners (their own user records + data)
    const scanners = await storage.getScannersForUser(clientId);
    for (const scanner of scanners) {
      const scannerAccounts = await storage.getWhatsappAccounts(scanner.id);
      for (const acc of scannerAccounts) {
        await waManager.removeSession(acc.id);
      }
      await storage.deleteAllUserData(scanner.id);
    }
    // Permanently delete the client
    await storage.deleteAllUserData(clientId);
    res.json({ refunded: refundQuota });
  });

  app.post("/api/hall/clients/:id/regen-code", requireHall, requireHallBilling, async (req, res) => {
    const clientId = Number(req.params.id);
    const hallId = req.session.userId!;
    const client = await storage.getUser(clientId);
    if (!client || client.parentHallId !== hallId) {
      return res.status(403).json({ message: "غير مصرح" });
    }
    const newCode = generateAccessCode();
    await storage.updateUser(clientId, { accessCode: newCode });
    let sent = false;
    let sendError: string | undefined;
    if (client.phoneNumber) {
      const loginUrl = `${req.protocol}://${req.get('host')}/login?code=${newCode}`;
      const msg = `🔑 كود الدخول الجديد لحسابك في إنفايتنا:\n\n*${newCode}*\n\nأو ادخل مباشرة:\n${loginUrl}\n\n💡 لتجربة كاملة افتح الرابط في متصفح كروم`;
      try {
        const regenResult = await sendViaGreenAPI(client.phoneNumber, msg);
        sent = regenResult.ok;
        if (!sent) sendError = regenResult.error || "لم يتم الإرسال عبر Green API";
      } catch (e: any) {
        sendError = e?.message || "فشل الإرسال";
      }
    }
    res.json({ sent, error: sendError });
  });

  app.get("/api/admin/settings", requireAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    const adminId = admin ? admin.id : req.session.userId!;
    const [webhookPhoneField, singleSendEnabled, wooOtpRequired] = await Promise.all([
      storage.getSetting(adminId, "webhook_phone_field"),
      storage.getSetting(adminId, "single_send_enabled"),
      storage.getSetting(adminId, "woo_otp_required"),
    ]);
    res.json({
      webhookPhoneField: webhookPhoneField || "",
      singleSendEnabled: singleSendEnabled !== "false",
      wooOtpRequired: wooOtpRequired === "true",
    });
  });

  app.put("/api/admin/settings", requireAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    const adminId = admin ? admin.id : req.session.userId!;
    const { webhookPhoneField, singleSendEnabled, wooOtpRequired } = z.object({
      webhookPhoneField: z.string().optional(),
      singleSendEnabled: z.boolean().optional(),
      wooOtpRequired: z.boolean().optional(),
    }).parse(req.body);
    if (webhookPhoneField !== undefined) await storage.setSetting(adminId, "webhook_phone_field", webhookPhoneField.trim());
    if (singleSendEnabled !== undefined) await storage.setSetting(adminId, "single_send_enabled", singleSendEnabled ? "true" : "false");
    if (wooOtpRequired !== undefined) await storage.setSetting(adminId, "woo_otp_required", wooOtpRequired ? "true" : "false");
    res.json({ success: true });
  });

  // Public endpoint for users to check if single send feature is enabled
  app.get("/api/settings/single-send", requireAuth, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    if (!admin) return res.json({ enabled: true });
    const val = await storage.getSetting(admin.id, "single_send_enabled");
    res.json({ enabled: val !== "false" });
  });

  // Save / load preferred send method per user
  app.get("/api/settings/send-method", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const method = await storage.getSetting(userId, "preferred_send_method");
    // Return null when no preference has been explicitly saved (no default)
    res.json({ method: (method === "system" || method === "personal") ? method : null });
  });

  app.put("/api/settings/send-method", requireAuth, async (req, res) => {
    const userId = req.session.userId!;
    const { method } = z.object({ method: z.enum(["system", "personal"]) }).parse(req.body);
    await storage.setSetting(userId, "preferred_send_method", method);
    res.json({ success: true });
  });

  app.get("/api/admin/stats", requireAdminOrCoAdmin, async (req, res) => {
    const allUsers = await storage.getAllUsers();
    const totalUsers = allUsers.filter(u => !u.isAdmin).length;
    const totalMessages = allUsers.reduce((sum, u) => sum + u.messagesSent, 0);
    const totalQuota = allUsers.reduce((sum, u) => sum + u.messageQuota, 0);
    res.json({ totalUsers, totalMessages, totalQuota, totalRemaining: totalQuota - totalMessages });
  });

  app.post("/api/admin/scanners", requireAdmin, async (req, res) => {
    try {
      const { name, linkedUserId } = z.object({
        name: z.string().min(1).max(100),
        linkedUserId: z.number(),
      }).parse(req.body);

      const code = generateAccessCode();
      const scanner = await storage.createUser({
        name,
        phoneNumber: "",
        accessCode: code,
        messageQuota: 0,
        messagesSent: 0,
        isAdmin: false,
        role: "scanner",
      });
      await storage.setSetting(scanner.id, "linked_user_id", String(linkedUserId));
      res.status(201).json({ ...scanner, linkedUserId });
    } catch (err) {
      res.status(400).json({ message: "بيانات غير صالحة" });
    }
  });

  app.delete("/api/admin/scanners/:id", requireAdmin, async (req, res) => {
    const id = Number(req.params.id);
    const user = await storage.getUser(id);
    if (!user || user.role !== "scanner") {
      return res.status(404).json({ message: "الماسح غير موجود" });
    }
    await storage.deleteAllUserData(id);
    res.status(204).send();
  });

  app.post("/api/admin/trigger-reminders", requireAdmin, async (req, res) => {
    try {
      res.json({ success: true, message: "جارٍ تشغيل وظيفة التذكير..." });
      await sendScheduledReminders();
      console.log("Manual reminder trigger completed by admin");
    } catch (err) {
      console.error("Manual reminder trigger failed:", err);
    }
  });

  // ========== QR CHECK-IN ROUTES (scanner-scoped) ==========

  async function requireScanner(req: Request, res: Response, next: NextFunction) {
    if (!req.session.userId) {
      return res.status(401).json({ message: "غير مسجل الدخول" });
    }
    const user = await storage.getUser(req.session.userId);
    if (!user || user.role !== "scanner") {
      return res.status(403).json({ message: "هذا الحساب ليس حساب ماسح" });
    }
    next();
  }

  app.post("/api/scanner/check-in", requireScanner, async (req, res) => {
    try {
      const { token } = z.object({ token: z.string().min(1) }).parse(req.body);
      const result = await storage.checkInGuestAtomic(token);
      if (!result) {
        return res.status(404).json({ 
          success: false, 
          status: "invalid",
          message: "باركود غير صالح",
          color: "red"
        });
      }
      if (result.alreadyCheckedIn) {
        const checkedInTime = result.guest.checkedInAt 
          ? new Date(result.guest.checkedInAt).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' })
          : '';
        return res.status(409).json({ 
          success: false, 
          status: "already_used",
          message: `تم مسح هذا الباركود مسبقاً${checkedInTime ? ' الساعة ' + checkedInTime : ''}`,
          guestName: result.guest.name,
          color: "red"
        });
      }
      res.json({ 
        success: true, 
        status: "checked_in",
        message: "تم تسجيل الدخول بنجاح",
        guestName: result.guest.name,
        guestCount: result.guest.guestCount ?? 1,
        additionalNames: result.guest.additionalNames || null,
        color: "green"
      });
    } catch (err) {
      res.status(400).json({ 
        success: false, 
        status: "error",
        message: "خطأ في قراءة الباركود",
        color: "red"
      });
    }
  });

  app.post("/api/scanner/guests/:id/check-in", requireScanner, async (req, res) => {
    const scannerId = req.session.userId!;
    const guestId = Number(req.params.id);
    if (isNaN(guestId)) return res.status(400).json({ message: "معرّف ضيف غير صالح" });
    const linkedUserIdStr = await storage.getSetting(scannerId, "linked_user_id");
    if (!linkedUserIdStr) return res.status(400).json({ message: "لم يتم ربط الماسح بمستخدم" });
    const linkedUserId = parseInt(linkedUserIdStr);
    const allUserIds = [linkedUserId, ...( await storage.getLinkedUsers(linkedUserId)).map(u => u.id)];
    const guest = await storage.getGuest(guestId);
    if (!guest || !allUserIds.includes(guest.userId)) {
      return res.status(404).json({ success: false, message: "الضيف غير موجود" });
    }
    if (guest.checkedIn) {
      const checkedInTime = guest.checkedInAt
        ? new Date(guest.checkedInAt).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' })
        : '';
      return res.status(409).json({
        success: false,
        status: "already_used",
        message: `تم تسجيل هذا الضيف مسبقاً${checkedInTime ? ' الساعة ' + checkedInTime : ''}`,
        guestName: guest.name,
      });
    }
    const updated = await storage.checkInGuest(guestId);
    res.json({
      success: true,
      status: "checked_in",
      guestName: updated.name,
      checkedInAt: updated.checkedInAt,
    });
  });

  app.get("/api/scanner/stats", requireScanner, async (req, res) => {
    const scannerId = req.session.userId!;
    const linkedUserIdStr = await storage.getSetting(scannerId, "linked_user_id");
    if (!linkedUserIdStr) {
      return res.status(400).json({ message: "لم يتم ربط الماسح بمستخدم" });
    }
    const linkedUserId = parseInt(linkedUserIdStr);
    // Gather guests from linked user + any secondary users linked to them (shared account)
    const allUserIds = [linkedUserId];
    const linked = await storage.getLinkedUsers(linkedUserId);
    allUserIds.push(...linked.map(u => u.id));
    let allGuests: import("@shared/schema").Guest[] = [];
    for (const uid of allUserIds) {
      allGuests = allGuests.concat(await storage.getGuests(uid));
    }
    const total = allGuests.length;
    const checkedIn = allGuests.filter(g => g.checkedIn).length;
    const recentCheckins = allGuests
      .filter(g => g.checkedIn && g.checkedInAt)
      .sort((a, b) => new Date(b.checkedInAt!).getTime() - new Date(a.checkedInAt!).getTime())
      .slice(0, 10);
    res.json({ 
      total,
      checkedIn,
      notCheckedIn: total - checkedIn,
      recent: recentCheckins.map(g => ({
        id: g.id,
        name: g.name,
        checkedInAt: g.checkedInAt,
      }))
    });
  });

  app.get("/api/scanner/guests", requireScanner, async (req, res) => {
    const scannerId = req.session.userId!;
    const linkedUserIdStr = await storage.getSetting(scannerId, "linked_user_id");
    if (!linkedUserIdStr) {
      return res.status(400).json({ message: "لم يتم ربط الماسح بمستخدم" });
    }
    const linkedUserId = parseInt(linkedUserIdStr);
    // Gather guests from linked user + any secondary users linked to them (shared account)
    const allUserIds = [linkedUserId];
    const linked = await storage.getLinkedUsers(linkedUserId);
    allUserIds.push(...linked.map(u => u.id));
    let allGuests: import("@shared/schema").Guest[] = [];
    for (const uid of allUserIds) {
      allGuests = allGuests.concat(await storage.getGuests(uid));
    }
    res.json(allGuests.map(g => ({
      id: g.id,
      name: g.name,
      checkedIn: g.checkedIn,
      checkedInAt: g.checkedInAt,
      status: g.status,
    })));
  });

  // ========== INIT ==========

  waManager.onContactsReceived = async (accountId: number, senderJid: string, contacts) => {
    try {
      const userId = waManager.getSessionUserId(accountId);
      if (userId === undefined || userId === 0) return;
      
      const user = await storage.getUser(userId);
      if (!user) return;

      const senderPhone = senderJid.replace("@s.whatsapp.net", "").replace(/[^0-9]/g, '');
      const userPhone = user.phoneNumber.replace(/[^0-9]/g, '');
      
      const session = waManager.getSession(accountId);
      const ownerJidPhone = session?.connectedJid?.replace("@s.whatsapp.net", "").replace(/[^0-9]/g, '');
      
      if (ownerJidPhone && senderPhone === ownerJidPhone) {
        // pass - exact JID match with account owner
      } else if (userPhone.length >= 7 && senderPhone.length >= 7) {
        const matchLen = Math.min(userPhone.length, senderPhone.length, 10);
        if (userPhone.slice(-matchLen) !== senderPhone.slice(-matchLen)) return;
      } else {
        return;
      }

      let added = 0;
      const existingGuests = await storage.getGuests(userId);
      const existingPhones = new Set(existingGuests.map(g => g.phoneNumber.replace(/[^0-9]/g, '').slice(-9)));

      for (const contact of contacts) {
        const cleanPhone = contact.phoneNumber.replace(/[^0-9+]/g, '');
        const phoneDigits = cleanPhone.replace(/[^0-9]/g, '');
        if (phoneDigits.length < 7) continue;
        if (existingPhones.has(phoneDigits.slice(-9))) continue;
        
        await storage.createGuest({
          userId,
          name: contact.name,
          phoneNumber: cleanPhone,
          status: "pending",
        });
        existingPhones.add(phoneDigits.slice(-9));
        added++;
      }

      if (added > 0) {
        const session = waManager.getSession(accountId);
        if (session?.status === 'connected') {
          await session.sendMessage(senderPhone, `تم إضافة ${added} ضيف بنجاح إلى قائمة الدعوات`);
        }
        console.log(`Added ${added} guests via WhatsApp contacts from user ${userId}`);
      }
    } catch (err) {
      console.error("Error processing WhatsApp contacts:", err);
    }
  };

  for (const session of waManager.getAllSessions()) {
    session.onContactsReceived = waManager.onContactsReceived;
  }

  try {
    const accounts = await storage.getAllWhatsappAccounts();
    const systemAccounts: typeof accounts = [];
    const userAccounts: typeof accounts = [];
    for (const account of accounts) {
      const loadedSession = waManager.addSession(account.id, account.name, account.userId);
      if (account.userId === 0) {
        systemAccounts.push(account);
      } else {
        wireUserSessionCallbacks(loadedSession, account.userId);
        userAccounts.push(account);
      }
    }
    if (accounts.length > 0) {
      console.log(`Loaded ${accounts.length} WhatsApp account(s) from database (${systemAccounts.length} system, ${userAccounts.length} user)`);
    }
    if (process.env.NODE_ENV === "production") {
      for (let i = 0; i < systemAccounts.length; i++) {
        const session = waManager.getSession(systemAccounts[i].id);
        if (session) {
          if (i > 0) await new Promise(r => setTimeout(r, 5000));
          session.start();
        }
      }
      // Auto-start user accounts — retry loop in case DB pool isn't ready at startup
      (async () => {
        const autoStartDelays = [0, 10000, 30000, 60000];
        for (let attempt = 0; attempt < autoStartDelays.length; attempt++) {
          if (autoStartDelays[attempt] > 0) {
            await new Promise(r => setTimeout(r, autoStartDelays[attempt]));
          }
          try {
            const { rows: savedCredsRows } = await poolInstance().query(
              `SELECT DISTINCT account_id FROM whatsapp_auth_data WHERE data_key = 'creds'`
            );
            const savedIds = new Set(savedCredsRows.map((r: any) => r.account_id as number));
            let startedCount = 0;
            for (const account of userAccounts) {
              if (savedIds.has(account.id)) {
                const session = waManager.getSession(account.id);
                if (session) {
                  if (startedCount > 0 || systemAccounts.length > 0) await new Promise(r => setTimeout(r, 5000));
                  session.start();
                  startedCount++;
                  console.log(`[Startup] Auto-starting user WhatsApp account ${account.id} (${account.name}) — has saved session`);
                }
              }
            }
            if (userAccounts.length - startedCount > 0) {
              console.log(`${userAccounts.length - startedCount} user account(s) skipped (no saved session).`);
            }
            break; // success — no more retries needed
          } catch (e) {
            if (attempt < autoStartDelays.length - 1) {
              console.warn(`[Startup] Auto-start attempt ${attempt + 1} failed, retrying in ${autoStartDelays[attempt + 1] / 1000}s...`, (e as any)?.message);
            } else {
              console.error("Failed to auto-start user WhatsApp accounts after all retries:", e);
              // Notify admin via Green API
              try {
                const greenAPI = (global as any).__sendViaGreenAPI;
                if (greenAPI) {
                  const adminList = await storage.getAllUsers();
                  const adminUser = adminList.find(u => u.isAdmin);
                  if (adminUser?.phoneNumber) {
                    await greenAPI(adminUser.phoneNumber, `⚠️ [إنفايتنا] فشل إعادة تشغيل حسابات واتساب تلقائياً بعد إعادة تشغيل السيرفر. يرجى مطالبة المستخدمين بإعادة الربط يدوياً.`);
                  }
                }
              } catch {}
            }
          }
        }
      })();
    } else {
      if (systemAccounts.length > 0) {
        console.log(`[dev] Skipping auto-start for ${systemAccounts.length} system WhatsApp account(s) — only starts in production to avoid session conflict.`);
      }
      if (userAccounts.length > 0) {
        console.log(`${userAccounts.length} user WhatsApp account(s) loaded but not auto-started. Users can reconnect from their dashboard.`);
      }
    }

    // Note: bulk_send_active flags are intentionally NOT cleared on startup.
    // The resume mechanism below (setTimeout) detects and resumes any interrupted sends.
  } catch (err) {
    console.error("Failed to load WhatsApp accounts from DB:", err);
  }

  // ========== WHATSAPP CLOUD API ==========

  async function getAdminId(): Promise<number> {
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    return admin ? admin.id : 1;
  }

  /**
   * Converts a local phone number (starting with 0) to international format.
   * Also strips the '+' from numbers that already have a country code.
   *
   * Rules (by local prefix + digit count):
   *  10-digit local:  05X → Saudi Arabia 966 | 07X → Jordan 962
   *                   09X → Syria 963        | 03X → Lebanon 961
   *                   06X → UAE 971          | 08X → Palestine 970
   *  11-digit local:  01X → Egypt 20         | 07X → Iraq 964
   *                   06X → Algeria 213      | 08X → Algeria 213
   *  9-digit local:   03X → Lebanon 961      | 09X → Tunisia 216
   */
  function normalizePhoneForWhatsApp(to: string): string {
    // Remove everything except digits
    let phone = to.replace(/[^0-9]/g, "");

    if (!phone.startsWith("0")) return phone; // Already international (no +)

    const len = phone.length;
    const p2 = phone.slice(0, 2); // first two digits e.g. "05", "07"

    const local = phone.slice(1); // remove leading 0

    if (len === 10) {
      if (p2 === "05") return "966" + local;  // Saudi Arabia 🇸🇦
      if (p2 === "07") return "962" + local;  // Jordan       🇯🇴
      if (p2 === "09") return "963" + local;  // Syria        🇸🇾
      if (p2 === "03") return "961" + local;  // Lebanon      🇱🇧
      if (p2 === "06") return "971" + local;  // UAE          🇦🇪
      if (p2 === "08") return "970" + local;  // Palestine    🇵🇸
      if (p2 === "02") return "967" + local;  // Yemen        🇾🇪
    }

    if (len === 11) {
      if (p2 === "01") return "20"  + local;  // Egypt        🇪🇬
      if (p2 === "07") return "964" + local;  // Iraq         🇮🇶
      if (p2 === "06") return "213" + local;  // Algeria      🇩🇿
      if (p2 === "05") return "212" + local;  // Morocco      🇲🇦
    }

    if (len === 9) {
      if (p2 === "09") return "216" + local;  // Tunisia      🇹🇳
      if (p2 === "03") return "961" + local;  // Lebanon      🇱🇧
      if (p2 === "07") return "967" + local;  // Yemen        🇾🇪
    }

    // Fallback: strip leading 0 only (better than sending with 0)
    return local;
  }

  async function sendViaGreenAPI(to: string, message: string, mediaUrl?: string, appBaseUrl?: string): Promise<{ ok: boolean; error?: string }> {
    const adminId = await getAdminId();
    const instanceId = await storage.getSetting(adminId, "green_api_id_instance");
    const token = await storage.getSetting(adminId, "green_api_token");
    const enabled = await storage.getSetting(adminId, "green_api_enabled");
    if (!instanceId || !token || enabled !== "true") return { ok: false, error: "Green API not configured" };
    try {
      const phone = normalizePhoneForWhatsApp(to);
      const TIMEOUT_MS = 10000;
      const fetchWithTimeout = (url: string, body: object) => {
        const ctrl = new AbortController();
        const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
        return fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
          signal: ctrl.signal,
        }).finally(() => clearTimeout(timer));
      };
      if (mediaUrl && appBaseUrl) {
        const fullUrl = mediaUrl.startsWith("http") ? mediaUrl : `${appBaseUrl}${mediaUrl}`;
        const lastPart = mediaUrl.split("/").pop() || "file";
        const fileName = (lastPart === 'image' || lastPart === 'video')
          ? (lastPart === 'video' ? 'invitation.mp4' : 'invitation.jpg')
          : lastPart;
        const res = await fetchWithTimeout(
          `https://api.green-api.com/waInstance${instanceId}/sendFileByUrl/${token}`,
          { chatId: `${phone}@c.us`, urlFile: fullUrl, fileName, caption: message }
        );
        if (!res.ok) {
          const errBody = await res.json().catch(() => ({})) as { message?: string };
          return { ok: false, error: errBody?.message || `HTTP ${res.status}` };
        }
        return { ok: true };
      } else {
        const res = await fetchWithTimeout(
          `https://api.green-api.com/waInstance${instanceId}/sendMessage/${token}`,
          { chatId: `${phone}@c.us`, message }
        );
        if (!res.ok) {
          const errBody = await res.json().catch(() => ({})) as { message?: string };
          return { ok: false, error: errBody?.message || `HTTP ${res.status}` };
        }
        return { ok: true };
      }
    } catch (err: unknown) {
      return { ok: false, error: err instanceof Error ? err.message : "Network error" };
    }
  }

  async function isSystemSendable(): Promise<boolean> {
    if (waManager.getSystemSession()) return true;
    const adminId = await getAdminId();
    const metaEnabled = await storage.getSetting(adminId, "meta_cloud_enabled");
    if (metaEnabled === "true") return true;
    const greenEnabled = await storage.getSetting(adminId, "green_api_enabled");
    if (greenEnabled === "true") return true;
    const cbEnabled = await storage.getSetting(adminId, "chatberry_enabled");
    if (cbEnabled === "true") return true;
    return false;
  }

  // Baileys-only sender — used for OTP codes, access code welcome messages, and code recovery
  async function sendViaBaileysDirect(phone: string, message: string, urgent = false): Promise<boolean> {
    try {
      await waManager.sendViaSystem(phone, message, undefined, urgent);
      console.log(`[sendViaBaileys] channel=Baileys phone=${phone}${urgent ? ' [urgent]' : ''}`);
      return true;
    } catch (e) {
      console.error(`[sendViaBaileys] Baileys failed for ${phone}:`, e);
      return false;
    }
  }

  // General system sender — Meta Cloud first; ChatBerry second; Green API third; Baileys fallback
  async function sendViaAnySystem(phone: string, message: string, mediaUrl?: string, appBaseUrl?: string, urgent = false): Promise<boolean> {
    // 1. Try Meta Cloud API first — official WhatsApp Business API
    const mcResult = await sendViaMetaCloud(phone, message);
    if (mcResult.ok) {
      console.log(`[sendViaAnySystem] channel=MetaCloud phone=${phone}`);
      return true;
    }
    if (mcResult.error !== "Meta Cloud not configured") {
      console.warn(`[sendViaAnySystem] MetaCloud failed for ${phone}: ${mcResult.error}, continuing...`);
    }

    // 2. Try ChatBerry
    const cbResult = await sendViaChatBerry(phone, message, mediaUrl, appBaseUrl);
    if (cbResult.ok) {
      console.log(`[sendViaAnySystem] channel=ChatBerry phone=${phone}`);
      return true;
    }
    if (cbResult.error !== "ChatBerry not configured") {
      console.warn(`[sendViaAnySystem] ChatBerry failed for ${phone}: ${cbResult.error}, continuing...`);
    }

    // 3. Try Green API
    const greenResult = await sendViaGreenAPI(phone, message, mediaUrl, appBaseUrl);
    if (greenResult.ok) {
      console.log(`[sendViaAnySystem] channel=GreenAPI phone=${phone}`);
      return true;
    }
    if (greenResult.error !== "Green API not configured") {
      console.warn(`[sendViaAnySystem] GreenAPI failed for ${phone}: ${greenResult.error}, trying Baileys...`);
    }

    // 4. Fallback to Baileys queue
    try {
      await waManager.sendViaSystem(phone, message, mediaUrl, urgent);
      console.log(`[sendViaAnySystem] channel=Baileys(fallback) phone=${phone}${urgent ? ' [urgent]' : ''}`);
      return true;
    } catch (e) {
      console.warn(`[sendViaAnySystem] Baileys also failed for ${phone}`);
    }

    console.error(`[sendViaAnySystem] All channels failed for ${phone}`);
    return false;
  }

  // Used for bulk-send system-bot sends — bypasses the Baileys queue so the bulk loop
  // owns all timing and avoids double-delay (queue delay on top of loop delay).
  // ChatBerry first; Green API second; Baileys session direct (no queue) as fallback.
  async function sendViaSystemDirect(phone: string, message: string, mediaUrl?: string, appBaseUrl?: string): Promise<boolean> {
    // 1. Meta Cloud API (top priority — official)
    const mcResult = await sendViaMetaCloud(phone, message);
    if (mcResult.ok) {
      console.log(`[sendViaSystemDirect] channel=MetaCloud phone=${phone}`);
      return true;
    }
    if (mcResult.error !== "Meta Cloud not configured") {
      console.warn(`[sendViaSystemDirect] MetaCloud failed for ${phone}: ${mcResult.error}, continuing...`);
    }

    // 2. ChatBerry
    const cbResult = await sendViaChatBerry(phone, message, mediaUrl, appBaseUrl);
    if (cbResult.ok) {
      console.log(`[sendViaSystemDirect] channel=ChatBerry phone=${phone}`);
      return true;
    }
    if (cbResult.error !== "ChatBerry not configured") {
      console.warn(`[sendViaSystemDirect] ChatBerry failed for ${phone}: ${cbResult.error}, continuing...`);
    }

    // 3. Green API
    const greenResult = await sendViaGreenAPI(phone, message, mediaUrl, appBaseUrl);
    if (greenResult.ok) {
      console.log(`[sendViaSystemDirect] channel=GreenAPI phone=${phone}`);
      return true;
    }
    if (greenResult.error !== "Green API not configured") {
      console.warn(`[sendViaSystemDirect] GreenAPI failed for ${phone}: ${greenResult.error}, trying Baileys direct...`);
    }
    const sysSession = waManager.getSystemSession();
    if (sysSession) {
      try {
        await sysSession.sendMessage(phone, message, mediaUrl);
        console.log(`[sendViaSystemDirect] channel=Baileys(direct) phone=${phone}`);
        return true;
      } catch (e) {
        console.warn(`[sendViaSystemDirect] Baileys direct failed for ${phone}`);
      }
    }
    console.error(`[sendViaSystemDirect] All channels failed for ${phone}`);
    return false;
  }

  // Poll until account reconnects or timeout — used by bulk-send to pause instead of fallback
  // Checks stop flag every 5 seconds so that stop button works even during disconnect wait
  async function waitForAccountReconnect(accountId: number, maxWaitMs: number, userId?: number): Promise<boolean> {
    const CHUNK_MS = 5000;
    const start = Date.now();
    while (Date.now() - start < maxWaitMs) {
      await new Promise(r => setTimeout(r, CHUNK_MS));
      // Honour stop request immediately
      if (userId) {
        const stopFlag = await storage.getSetting(userId, "bulk_send_stop_requested");
        if (stopFlag === "true") return false;
      }
      const s = waManager.getSession(accountId);
      if (s && s.status === 'connected') return true;
    }
    return false;
  }

  // Wire disconnect notification + event-active check to a user WhatsApp session
  function wireUserSessionCallbacks(session: import("./whatsapp").WhatsAppSession, userId: number) {
    if (userId === 0) return; // System accounts — skip
    session.onPermanentDisconnect = (accountId: number) => {
      (async () => {
        try {
          // Cooldown: skip if we already notified about this account in the last 24h
          const cooldownKey = `disconnect_notified_${accountId}`;
          const lastNotified = await storage.getSetting(userId, cooldownKey).catch(() => null);
          if (lastNotified) {
            const msSince = Date.now() - new Date(lastNotified).getTime();
            if (msSince < 24 * 60 * 60 * 1000) {
              console.log(`[WhatsApp] Disconnect notification for account ${accountId} suppressed (cooldown, ${Math.round(msSince / 60000)}min ago)`);
              return;
            }
          }
          // Also skip if a bulk send finished in the last 10 minutes (disconnect is likely due to bulk send)
          const bulkEndKey = `bulk_send_completed_at`;
          const bulkEndAt = await storage.getSetting(userId, bulkEndKey).catch(() => null);
          if (bulkEndAt) {
            const msSinceBulk = Date.now() - new Date(bulkEndAt).getTime();
            if (msSinceBulk < 10 * 60 * 1000) {
              console.log(`[WhatsApp] Disconnect notification for account ${accountId} suppressed (bulk send just finished ${Math.round(msSinceBulk / 60000)}min ago)`);
              return;
            }
          }
          const user = await storage.getUser(userId);
          if (!user?.phoneNumber) return;
          await storage.setSetting(userId, cooldownKey, new Date().toISOString());
          const msg = `⚠️ انقطع اتصال رقمك بإنفايتنا. يرجى الدخول وإعادة الربط لضمان استمرار إرسال الدعوات.`;
          await sendViaAnySystem(user.phoneNumber, msg);
          console.log(`[WhatsApp] Sent permanent disconnect notification to user ${userId} (account ${accountId})`);
        } catch (e) {
          console.error(`[WhatsApp] Failed to notify user ${userId} of disconnect:`, e);
        }
      })();
    };
    session.onCheckEventActive = async () => {
      try {
        const eventDateStr = await storage.getSetting(userId, "event_date");
        if (!eventDateStr) return true; // No event date — conservative: keep auth
        return new Date(eventDateStr) > new Date();
      } catch {
        return true; // On error: conservative, keep auth
      }
    };
  }

  app.get("/api/admin/green-api", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const enabled = await storage.getSetting(adminId, "green_api_enabled");
    const idInstance = await storage.getSetting(adminId, "green_api_id_instance");
    const hasToken = !!(await storage.getSetting(adminId, "green_api_token"));
    let webhookAuthToken = await storage.getSetting(adminId, "green_api_webhook_auth_token");
    if (!webhookAuthToken) {
      const { randomBytes } = await import("crypto");
      webhookAuthToken = randomBytes(32).toString("hex");
      await storage.setSetting(adminId, "green_api_webhook_auth_token", webhookAuthToken);
    }
    res.json({ enabled: enabled === "true", idInstance: idInstance || "", hasToken, webhookAuthToken });
  });

  app.post("/api/admin/green-api/regenerate-webhook-token", requireAdmin, async (req, res) => {
    const { randomBytes } = await import("crypto");
    const adminId = await getAdminId();
    const newToken = randomBytes(32).toString("hex");
    await storage.setSetting(adminId, "green_api_webhook_auth_token", newToken);
    res.json({ webhookAuthToken: newToken });
  });

  app.put("/api/admin/green-api", requireAdmin, async (req, res) => {
    const { idInstance, apiToken } = z.object({
      idInstance: z.string().min(1),
      apiToken: z.string().min(1),
    }).parse(req.body);
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "green_api_id_instance", idInstance.trim());
    await storage.setSetting(adminId, "green_api_token", apiToken.trim());
    await storage.setSetting(adminId, "green_api_enabled", "true");
    res.json({ success: true });
  });

  app.delete("/api/admin/green-api", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "green_api_enabled", "false");
    await storage.setSetting(adminId, "green_api_id_instance", "");
    await storage.setSetting(adminId, "green_api_token", "");
    res.json({ success: true });
  });

  app.post("/api/admin/green-api/test", requireAdmin, async (req, res) => {
    const { phoneNumber } = z.object({ phoneNumber: z.string().min(5) }).parse(req.body);
    const result = await sendViaGreenAPI(phoneNumber, "✅ اختبار ناجح من إنفايتنا!\n\nGreen API يعمل بشكل صحيح.");
    if (result.ok) {
      res.json({ success: true, message: "تم إرسال رسالة الاختبار بنجاح" });
    } else {
      res.status(400).json({ success: false, message: result.error || "فشل الإرسال" });
    }
  });

  (global as any).__sendViaGreenAPI = sendViaGreenAPI;

  // =========================================
  // CHATBERRY INTEGRATION
  // =========================================

  // =========================================
  // META CLOUD API (WhatsApp Business Platform)
  // =========================================
  async function sendViaMetaCloud(to: string, message: string): Promise<{ ok: boolean; error?: string }> {
    const adminId = await getAdminId();
    const phoneNumberId = await storage.getSetting(adminId, "meta_cloud_phone_number_id");
    const accessToken = await storage.getSetting(adminId, "meta_cloud_access_token");
    const enabled = await storage.getSetting(adminId, "meta_cloud_enabled");
    if (!phoneNumberId || !accessToken || enabled !== "true") return { ok: false, error: "Meta Cloud not configured" };
    try {
      const phone = to.replace(/[^0-9]/g, '');
      const TIMEOUT_MS = 12000;
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
      const res = await fetch(`https://graph.facebook.com/v20.0/${phoneNumberId}/messages`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${accessToken}`,
        },
        body: JSON.stringify({
          messaging_product: "whatsapp",
          to: phone,
          type: "text",
          text: { body: message },
        }),
        signal: ctrl.signal,
      }).finally(() => clearTimeout(timer));
      if (!res.ok) {
        const errBody = await res.json().catch(() => ({})) as any;
        return { ok: false, error: errBody?.error?.message || `HTTP ${res.status}` };
      }
      return { ok: true };
    } catch (err: unknown) {
      return { ok: false, error: err instanceof Error ? err.message : "Network error" };
    }
  }

  app.get("/api/admin/meta-cloud", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const enabled = await storage.getSetting(adminId, "meta_cloud_enabled");
    const hasToken = !!(await storage.getSetting(adminId, "meta_cloud_access_token"));
    const phoneNumberId = (await storage.getSetting(adminId, "meta_cloud_phone_number_id")) || "";
    res.json({ enabled: enabled === "true", hasToken, phoneNumberId });
  });

  app.put("/api/admin/meta-cloud", requireAdmin, async (req, res) => {
    const { accessToken, phoneNumberId } = z.object({
      accessToken: z.string().min(1).optional(),
      phoneNumberId: z.string().min(1),
    }).parse(req.body);
    const adminId = await getAdminId();
    if (accessToken) await storage.setSetting(adminId, "meta_cloud_access_token", accessToken.trim());
    await storage.setSetting(adminId, "meta_cloud_phone_number_id", phoneNumberId.trim());
    await storage.setSetting(adminId, "meta_cloud_enabled", "true");
    res.json({ success: true });
  });

  app.delete("/api/admin/meta-cloud", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "meta_cloud_enabled", "false");
    await storage.setSetting(adminId, "meta_cloud_access_token", "");
    await storage.setSetting(adminId, "meta_cloud_phone_number_id", "");
    res.json({ success: true });
  });

  app.post("/api/admin/meta-cloud/test", requireAdmin, async (req, res) => {
    const { phoneNumber } = z.object({ phoneNumber: z.string().min(5) }).parse(req.body);
    const normalizedPhone = phoneNumber.startsWith('+') ? phoneNumber : '+' + phoneNumber;
    const result = await sendViaMetaCloud(normalizedPhone, "✅ اختبار ناجح من إنفايتنا!\n\nMeta Cloud API يعمل بشكل صحيح 🎉");
    if (result.ok) {
      res.json({ success: true, message: "تم إرسال رسالة الاختبار بنجاح" });
    } else {
      res.status(400).json({ success: false, message: result.error || "فشل الإرسال" });
    }
  });

  async function sendViaChatBerry(to: string, message: string, mediaUrl?: string, appBaseUrl?: string): Promise<{ ok: boolean; error?: string }> {
    const adminId = await getAdminId();
    const token = await storage.getSetting(adminId, "chatberry_token");
    const enabled = await storage.getSetting(adminId, "chatberry_enabled");
    const apiUrl = (await storage.getSetting(adminId, "chatberry_api_url")) || "https://api.chatberry.co";
    if (!token || enabled !== "true") return { ok: false, error: "ChatBerry not configured" };
    try {
      const phone = normalizePhoneForWhatsApp(to);
      const TIMEOUT_MS = 10000;
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
      const body: Record<string, unknown> = { phone, message };
      if (mediaUrl && appBaseUrl) {
        body.media_url = mediaUrl.startsWith("http") ? mediaUrl : `${appBaseUrl}${mediaUrl}`;
      }
      const res = await fetch(`${apiUrl}/v1/messages/send`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
        body: JSON.stringify(body),
        signal: ctrl.signal,
      }).finally(() => clearTimeout(timer));
      if (!res.ok) {
        const errBody = await res.json().catch(() => ({})) as { message?: string };
        return { ok: false, error: errBody?.message || `HTTP ${res.status}` };
      }
      return { ok: true };
    } catch (err: unknown) {
      return { ok: false, error: err instanceof Error ? err.message : "Network error" };
    }
  }

  app.get("/api/admin/chatberry", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const enabled = await storage.getSetting(adminId, "chatberry_enabled");
    const hasToken = !!(await storage.getSetting(adminId, "chatberry_token"));
    const apiUrl = (await storage.getSetting(adminId, "chatberry_api_url")) || "";
    res.json({ enabled: enabled === "true", hasToken, apiUrl });
  });

  app.put("/api/admin/chatberry", requireAdmin, async (req, res) => {
    const { apiToken, apiUrl } = z.object({
      apiToken: z.string().min(1).optional(),
      apiUrl: z.string().optional(),
    }).parse(req.body);
    const adminId = await getAdminId();
    if (apiToken) await storage.setSetting(adminId, "chatberry_token", apiToken.trim());
    if (apiUrl !== undefined) await storage.setSetting(adminId, "chatberry_api_url", apiUrl.trim());
    await storage.setSetting(adminId, "chatberry_enabled", "true");
    // Disable Green API when ChatBerry is enabled (mutual exclusivity)
    await storage.setSetting(adminId, "green_api_enabled", "false");
    res.json({ success: true });
  });

  app.delete("/api/admin/chatberry", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "chatberry_enabled", "false");
    await storage.setSetting(adminId, "chatberry_token", "");
    res.json({ success: true });
  });

  app.post("/api/admin/chatberry/test", requireAdmin, async (req, res) => {
    const { phoneNumber } = z.object({ phoneNumber: z.string().min(5) }).parse(req.body);
    const result = await sendViaChatBerry(phoneNumber, "✅ اختبار ناجح من إنفايتنا!\n\nChatBerry يعمل بشكل صحيح.");
    if (result.ok) {
      res.json({ success: true, message: "تم إرسال رسالة الاختبار بنجاح" });
    } else {
      res.status(400).json({ success: false, message: result.error || "فشل الإرسال" });
    }
  });

  // Warmup auto-resume is now handled automatically by the DB-driven queue scheduler.
  // When a warmup entry hits its daily limit, it's rescheduled for the next day in the DB.
  // The scheduler picks it up automatically on the next day — no manual resume needed.
  (global as any).__triggerWarmupAutoResume = async () => {
    // No-op: the DB send_queue scheduler handles warmup continuation automatically.
  };

  // =========================================
  // MYFATOORAH PAYMENT INTEGRATION
  // =========================================

  type MyfatoorahConfig = { token: string; country: string; amount: number; testMode: boolean; gatewayId: number };

  async function getMyfatoorahConfig(): Promise<MyfatoorahConfig | null> {
    const adminId = await getAdminId();
    const token = await storage.getSetting(adminId, "myfatoorah_token");
    const country = await storage.getSetting(adminId, "myfatoorah_country") || "SAU";
    const amount = parseFloat(await storage.getSetting(adminId, "myfatoorah_amount") || "0");
    const testMode = (await storage.getSetting(adminId, "myfatoorah_test_mode")) === "true";
    const gatewayId = parseInt(await storage.getSetting(adminId, "myfatoorah_gateway_id") || "0");
    if (!token || !amount) return null;
    // Respect admin's toggle — if explicitly disabled, return null
    const activeStr = await storage.getSetting(adminId, "myfatoorah_active");
    if (activeStr === "false") return null;
    return { token, country, amount, testMode, gatewayId };
  }

  function getMyfatoorahBaseUrl(testMode: boolean): string {
    return testMode ? "https://apitest.myfatoorah.com" : "https://api.myfatoorah.com";
  }

  const MYFATOORAH_CURRENCY_MAP: Record<string, string> = {
    SAU: "SAR", KWT: "KWD", UAE: "AED", BHR: "BHD",
    OMN: "OMR", QAT: "QAR", EGY: "EGP", JOR: "JOD",
  };
  const MYFATOORAH_DISPLAY_CURRENCY: Record<string, string> = {
    SAU: "ر.س", KWT: "د.ك", UAE: "د.إ", BHR: "د.ب",
    OMN: "ر.ع", QAT: "ر.ق", EGY: "ج.م", JOR: "د.أ",
  };

  interface MyFatoorahPaymentResult {
    IsSuccess: boolean;
    Message: string;
    Data: {
      InvoiceId: number;
      PaymentURL: string;
      CustomerReference: string;
    };
  }

  interface MyFatoorahStatusResult {
    IsSuccess: boolean;
    Message: string;
    Data: {
      InvoiceStatus: string;
      CustomerReference: string;
      InvoiceId: number;
      RecurringId?: string | null;
      PaymentGateway?: string | null;
    };
  }

  interface MyFatoorahDirectPaymentResult {
    IsSuccess: boolean;
    Message: string;
    Data: {
      InvoiceStatus: string;
      InvoiceId: number;
    } | null;
  }

  async function myfatoorahPost<T>(url: string, token: string, body: unknown): Promise<T> {
    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
      body: JSON.stringify(body),
    });
    return response.json() as Promise<T>;
  }

  async function activateHallSubscription(hallId: number, hallName: string, recurringId?: string | null): Promise<void> {
    const now = new Date();
    const hall = await storage.getUser(hallId);
    if (!hall) return;
    let newExpiry: Date;
    if (hall.subscriptionExpiresAt && new Date(hall.subscriptionExpiresAt) > now) {
      newExpiry = new Date(hall.subscriptionExpiresAt);
      newExpiry.setMonth(newExpiry.getMonth() + 1);
    } else {
      newExpiry = new Date();
      newExpiry.setMonth(newExpiry.getMonth() + 1);
    }
    const updates: Parameters<typeof storage.updateUser>[1] = {
      subscriptionExpiresAt: newExpiry,
      subscriptionStartedAt: now,
      billingStatus: "active",
      nextBillingAt: newExpiry,
      paymentWarningAt: null,
      hallNotifiedAt: null,
      isSuspended: false,
    };
    // Store MyFatoorah recurring token for automatic renewal
    if (recurringId) {
      updates.myfatoorahToken = recurringId;
    }
    await storage.updateUser(hallId, updates);
    await storage.resetHallBalance(hallId);
    await storage.deleteSetting(hallId, "myfatoorah_pending_invoice");
    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    if (admin?.phoneNumber) {
      const msg = `✅ *تجديد اشتراك تلقائي*\n\nقاعة *${hallName}* جددت اشتراكها عبر بوابة الدفع.\nالاشتراك مجدد حتى: ${newExpiry.toLocaleDateString("ar-SA")}.`;
      try { await sendViaAnySystem(admin.phoneNumber, msg); } catch (_) { /* best-effort */ }
    }
  }

  // Admin: get MyFatoorah config
  app.get("/api/admin/myfatoorah", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    const token = await storage.getSetting(adminId, "myfatoorah_token");
    const country = await storage.getSetting(adminId, "myfatoorah_country") || "SAU";
    const amount = await storage.getSetting(adminId, "myfatoorah_amount") || "";
    const testMode = (await storage.getSetting(adminId, "myfatoorah_test_mode")) === "true";
    const gatewayId = await storage.getSetting(adminId, "myfatoorah_gateway_id") || "0";
    const activeStr = await storage.getSetting(adminId, "myfatoorah_active");
    // active defaults to true once token+amount are set; can be explicitly disabled
    const active = activeStr !== "false";
    const configured = !!(token && amount);
    const enabled = configured && active;
    res.json({ enabled, configured, active, hasToken: !!token, country, amount, testMode, gatewayId });
  });

  // Admin: save MyFatoorah config
  app.put("/api/admin/myfatoorah", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    const body = z.object({
      token: z.string().optional(),
      country: z.string().min(2).max(10),
      amount: z.number().positive(),
      testMode: z.boolean().default(false),
      gatewayId: z.number().int().min(0).default(0),
    }).parse(req.body);
    if (body.token && body.token.trim()) await storage.setSetting(adminId, "myfatoorah_token", body.token.trim());
    await storage.setSetting(adminId, "myfatoorah_country", body.country);
    await storage.setSetting(adminId, "myfatoorah_amount", String(body.amount));
    await storage.setSetting(adminId, "myfatoorah_test_mode", body.testMode ? "true" : "false");
    await storage.setSetting(adminId, "myfatoorah_gateway_id", String(body.gatewayId));
    res.json({ success: true });
  });

  // Admin: toggle MyFatoorah on/off (keeps all settings)
  app.patch("/api/admin/myfatoorah", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    const { active } = z.object({ active: z.boolean() }).parse(req.body);
    await storage.setSetting(adminId, "myfatoorah_active", active ? "true" : "false");

    if (!active) {
      // Unsuspend halls that were suspended by billing failures so they work normally while disabled
      const halls = await storage.getAllHalls();
      for (const hall of halls) {
        if (hall.isSuspended && hall.billingStatus === "expired") {
          await storage.updateUser(hall.id, { isSuspended: false, billingStatus: null });
          await storage.cascadeSuspendHallClients(hall.id, false);
        }
      }
    }

    res.json({ success: true, active });
  });

  // Admin: remove MyFatoorah config (hard reset — clears everything)
  app.delete("/api/admin/myfatoorah", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    await storage.setSetting(adminId, "myfatoorah_token", "");
    await storage.setSetting(adminId, "myfatoorah_amount", "");

    // Unsuspend halls that were suspended specifically by MyFatoorah billing failures
    // so they can operate normally once payment gating is disabled
    const halls = await storage.getAllHalls();
    for (const hall of halls) {
      if (hall.isSuspended && hall.billingStatus === "expired") {
        await storage.updateUser(hall.id, { isSuspended: false, billingStatus: null });
        await storage.cascadeSuspendHallClients(hall.id, false);
      }
    }

    res.json({ success: true });
  });

  // Admin: get all halls with billing status (for payment management table)
  app.get("/api/admin/myfatoorah/halls", requireAdmin, async (req, res) => {
    const halls = await storage.getAllHalls();
    const result = halls.map(h => ({
      id: h.id,
      name: h.name,
      phoneNumber: h.phoneNumber,
      billingStatus: h.billingStatus || null,
      subscriptionExpiresAt: h.subscriptionExpiresAt || null,
      nextBillingAt: h.nextBillingAt || null,
      isSuspended: h.isSuspended,
    }));
    res.json(result);
  });

  // Admin: get send delay settings
  app.get("/api/admin/send-delays", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    const systemInvite = parseInt(await storage.getSetting(adminId, "admin_delay_system_invite") || "45");
    const systemReminder = parseInt(await storage.getSetting(adminId, "admin_delay_system_reminder") || "45");
    const userInviteHours = parseInt(await storage.getSetting(adminId, "admin_delay_user_invite_hours") || "24");
    const userReminderHours = parseInt(await storage.getSetting(adminId, "admin_delay_user_reminder_hours") || "12");
    res.json({ systemInvite, systemReminder, userInviteHours, userReminderHours });
  });

  // Admin: save send delay settings
  app.put("/api/admin/send-delays", requireAdmin, async (req, res) => {
    const adminId = req.session.userId!;
    const body = z.object({
      systemInvite: z.number().int().min(5).max(600),
      systemReminder: z.number().int().min(5).max(600),
      userInviteHours: z.number().int().min(1).max(168),
      userReminderHours: z.number().int().min(1).max(168),
    }).parse(req.body);
    await storage.setSetting(adminId, "admin_delay_system_invite", String(body.systemInvite));
    await storage.setSetting(adminId, "admin_delay_system_reminder", String(body.systemReminder));
    await storage.setSetting(adminId, "admin_delay_user_invite_hours", String(body.userInviteHours));
    await storage.setSetting(adminId, "admin_delay_user_reminder_hours", String(body.userReminderHours));
    // Apply system invite delay immediately (affects next bulk send)
    WhatsAppManager.setSystemInviteDelay(body.systemInvite);
    console.log(`[Timing] Admin updated delays — system: ${body.systemInvite}s invite / ${body.systemReminder}s reminder; personal: ${body.userInviteHours}h invite / ${body.userReminderHours}h reminder`);
    res.json({ success: true });
  });

  // ========== SYSTEM MESSAGE SETTINGS ==========

  const SYSTEM_MESSAGES_META = [
    {
      key: "otp",
      name: "رمز التحقق (OTP)",
      description: "يُرسل لأي مستخدم عند تسجيل الدخول للتحقق من رقم الهاتف",
      preview: "🔐 كود التحقق الخاص بك في إنفايتنا:\n\n*123456*\n\nصالح لمدة 5 دقائق فقط. لا تشاركه مع أحد.",
    },
    {
      key: "recover_code",
      name: "استعادة كود الدخول",
      description: "يُرسل عند طلب استعادة كود الحساب عبر رقم الهاتف",
      preview: "🔑 كود الدخول إلى إنفايتنا:\n\n*ABCD1234*\n\nأو ادخل من الرابط المباشر",
    },
    {
      key: "demo_invite",
      name: "الدعوة التجريبية",
      description: "يُرسل عند طلب تجربة الدعوة من الصفحة الرئيسية",
      preview: "✨ *دعوة تجريبية من إنفايتنا* ✨\n\nمرحباً!\nهذه دعوة تجريبية لعرض طريقة عمل المنصة.",
    },
    {
      key: "disconnect_alert",
      name: "تنبيه انقطاع الواتساب",
      description: "يُرسل للمستخدم قبل التذكير التلقائي إذا كان رقمه الشخصي غير متصل",
      preview: "⚠️ تنبيه عاجل من إنفايتنا\n\nتذكير مناسبتك مجدول خلال ساعة!\nرقمك الشخصي غير متصل الآن.",
    },
    {
      key: "subscription_reminder_admin",
      name: "تذكير تجديد الاشتراك (للأدمن)",
      description: "يُرسل للأدمن قبل 3 أيام من انتهاء اشتراك إحدى القاعات",
      preview: "⚠️ *تذكير تجديد اشتراك قاعة*\n\nالقاعة: *اسم القاعة*\nالاشتراك ينتهي خلال 3 أيام",
    },
    {
      key: "subscription_reminder_hall",
      name: "تذكيرات الاشتراك (للقاعة — دفع يدوي)",
      description: "يُرسل للقاعة تذكيرات قبل 7 أيام، 3 أيام، يوم الانتهاء، وبعد يوم وثلاثة أيام من الانتهاء (للاشتراك اليدوي فقط)",
      preview: "🔔 *تذكير — اشتراكك ينتهي قريباً*\n\nنظام إنفايتنا الخاص بك سينتهي خلال 7 أيام.\n\nيرجى التواصل مع المشرف لتجديد الاشتراك.",
    },
    {
      key: "pending_welcomes_flushed",
      name: "إشعار إرسال الرسائل المعلّقة",
      description: "يُرسل للأدمن عند إعادة الاتصال بالواتساب وإرسال رسائل الترحيب المعلّقة",
      preview: "✅ رجع اتصال رقم المشروع.\n\nتم إرسال 5 رسائل ترحيب كانت معلّقة.",
    },
    {
      key: "subscription_reminder_admin_7days",
      name: "تذكير مبكر للأدمن — 7 أيام قبل الانتهاء",
      description: "يُرسل للأدمن قبل 7 أيام من انتهاء اشتراك قاعة تعمل بالدفع اليدوي",
      preview: "🔔 *تذكير مبكر — اشتراك قاعة*\n\nالقاعة: *اسم القاعة*\nالاشتراك ينتهي خلال 7 أيام\n\nيرجى التحضير لتجديد الاشتراك.",
    },
    {
      key: "subscription_reminder_admin_day0",
      name: "تذكير للأدمن — يوم الانتهاء",
      description: "يُرسل للأدمن في يوم انتهاء اشتراك قاعة تعمل بالدفع اليدوي",
      preview: "⚠️ *اشتراك قاعة ينتهي اليوم*\n\nالقاعة: *اسم القاعة*\nاشتراكها ينتهي اليوم!\n\nيرجى تأكيد الدفع من لوحة الأدمن.",
    },
    {
      key: "subscription_reminder_admin_1day_after",
      name: "تنبيه للأدمن — بعد يوم من الانتهاء",
      description: "يُرسل للأدمن بعد يوم من انتهاء اشتراك قاعة بدون تجديد (دفع يدوي)",
      preview: "🚨 *قاعة لم تجدد اشتراكها*\n\nالقاعة: *اسم القاعة*\nمضى يوم على انتهاء اشتراكها ولم تجدد.\n\nيرجى المتابعة.",
    },
    {
      key: "subscription_reminder_admin_3days_after",
      name: "تنبيه للأدمن — بعد 3 أيام من الانتهاء",
      description: "يُرسل للأدمن بعد 3 أيام من انتهاء اشتراك قاعة بدون تجديد (دفع يدوي)",
      preview: "🚨 *قاعة لا تزال بدون تجديد*\n\nالقاعة: *اسم القاعة*\nمضت 3 أيام على انتهاء اشتراكها ولم تجدد.\n\nيرجى اتخاذ إجراء.",
    },
  ];

  app.get("/api/admin/system-messages", requireAdmin, async (req, res) => {
    const savedSettings = await storage.getAllSystemMessageSettings();
    const result = SYSTEM_MESSAGES_META.map(msg => ({
      ...msg,
      enabled: savedSettings[msg.key] !== undefined ? savedSettings[msg.key] : true,
    }));
    res.json(result);
  });

  app.patch("/api/admin/system-messages/:key", requireAdmin, async (req, res) => {
    const key = req.params.key;
    const validKeys = SYSTEM_MESSAGES_META.map(m => m.key);
    if (!validKeys.includes(key)) {
      return res.status(404).json({ message: "مفتاح الرسالة غير موجود" });
    }
    const { enabled } = z.object({ enabled: z.boolean() }).parse(req.body);
    await storage.setSystemMessageEnabled(key, enabled);
    res.json({ success: true, key, enabled });
  });

  app.post("/api/admin/system-messages/:key/test", requireAdmin, async (req, res) => {
    const key = req.params.key;
    const validKeys = SYSTEM_MESSAGES_META.map(m => m.key);
    if (!validKeys.includes(key)) {
      return res.status(404).json({ message: "مفتاح الرسالة غير موجود" });
    }
    const adminUser = await storage.getUser(req.session.userId!);
    if (!adminUser?.phoneNumber) {
      return res.status(400).json({ message: "لا يوجد رقم هاتف للأدمن" });
    }
    const meta = SYSTEM_MESSAGES_META.find(m => m.key === key)!;
    const testMsg = `🧪 *اختبار رسالة النظام*\n📌 النوع: ${meta.name}\n\n${meta.preview}`;
    try {
      const sent = await sendViaAnySystem(adminUser.phoneNumber, testMsg, undefined, undefined, true);
      if (!sent) {
        return res.status(503).json({ message: "فشل إرسال رسالة الاختبار. تأكد من اتصال واتساب رقم المشروع." });
      }
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ message: "حدث خطأ أثناء إرسال الاختبار" });
    }
  });

  // Hall: check if payment is available
  app.get("/api/hall/payment/status", async (req, res) => {
    if (!req.session.userId) return res.status(401).json({ available: false });
    const hallUser = await storage.getUser(req.session.userId);
    if (!hallUser || hallUser.role !== "hall") return res.status(403).json({ available: false });
    const config = await getMyfatoorahConfig();
    res.json({
      available: !!config,
      amount: config?.amount || 0,
      currency: MYFATOORAH_DISPLAY_CURRENCY[config?.country || "SAU"] || "ر.س",
    });
  });

  // Hall: initiate payment (subscribe)
  app.post("/api/hall/subscribe", async (req, res) => {
    if (!req.session.userId) return res.status(401).json({ message: "غير مصرح" });
    const hallUser = await storage.getUser(req.session.userId);
    if (!hallUser || hallUser.role !== "hall") return res.status(403).json({ message: "غير مصرح" });
    const config = await getMyfatoorahConfig();
    if (!config) return res.status(503).json({ message: "نظام الدفع غير مُهيّأ، تواصل مع المدير" });

    const baseUrl = req.protocol + "://" + req.get("host");
    const callbackUrl = `${baseUrl}/api/hall/myfatoorah-callback`;
    const errorUrl = `${baseUrl}/hall?payment=error`;
    const currency = MYFATOORAH_CURRENCY_MAP[config.country] || "SAR";

    try {
      const apiUrl = getMyfatoorahBaseUrl(config.testMode);
      const payload = {
        PaymentMethodId: config.gatewayId || 0,
        CustomerName: hallUser.name,
        DisplayCurrencyIso: currency,
        CustomerMobile: hallUser.phoneNumber.replace(/^\+/, ""),
        CustomerEmail: `hall${hallUser.id}@invatna.app`,
        InvoiceValue: config.amount,
        CallBackUrl: callbackUrl,
        ErrorUrl: errorUrl,
        Language: "AR",
        CustomerReference: `hall:${hallUser.id}`,
        UserDefinedField: "subscription",
        IsRecurring: true,
        RecurringModel: {
          RecurringType: "Custom",
          IntervalDays: 30,
          Iteration: 12,
        },
        InvoiceItems: [{ ItemName: "اشتراك شهري - إنفايتنا", Quantity: 1, UnitPrice: config.amount }],
      };

      const data = await myfatoorahPost<MyFatoorahPaymentResult>(
        `${apiUrl}/v2/ExecutePayment`,
        config.token,
        payload,
      );

      if (!data.IsSuccess) {
        console.error("MyFatoorah ExecutePayment failed:", data.Message);
        return res.status(400).json({ message: data.Message || "فشل إنشاء فاتورة الدفع" });
      }

      // Store invoice ID to verify on callback (security: bind invoiceId to hallId)
      await storage.setSetting(hallUser.id, "myfatoorah_pending_invoice", String(data.Data.InvoiceId));
      await storage.updateUser(hallUser.id, { billingStatus: "pending" });

      res.json({ paymentUrl: data.Data.PaymentURL, invoiceId: data.Data.InvoiceId });
    } catch (err) {
      console.error("MyFatoorah payment initiation error:", err);
      res.status(500).json({ message: "حدث خطأ أثناء إنشاء رابط الدفع" });
    }
  });

  // Payment callback — MyFatoorah redirects browser here after payment
  app.get("/api/hall/myfatoorah-callback", async (req, res) => {
    const paymentId = String(req.query.paymentId || "");
    if (!paymentId) return res.redirect("/hall?payment=error");

    const config = await getMyfatoorahConfig();
    if (!config) return res.redirect("/hall?payment=error");

    try {
      const apiUrl = getMyfatoorahBaseUrl(config.testMode);
      const verifyData = await myfatoorahPost<MyFatoorahStatusResult>(
        `${apiUrl}/v2/GetPaymentStatus`,
        config.token,
        { Key: paymentId, KeyType: "PaymentId" },
      );

      if (!verifyData.IsSuccess || verifyData.Data?.InvoiceStatus !== "Paid") {
        console.error("MyFatoorah verification failed:", verifyData.Message, verifyData.Data?.InvoiceStatus);
        return res.redirect("/hall?payment=error");
      }

      // Identify hall from CustomerReference (server-side, no trust in query params)
      const ref = verifyData.Data?.CustomerReference || "";
      const match = ref.match(/^hall:(\d+)$/);
      if (!match) return res.redirect("/hall?payment=error");
      const hallId = parseInt(match[1]);

      // Verify the stored pending invoice matches to prevent replay attacks
      const storedInvoice = await storage.getSetting(hallId, "myfatoorah_pending_invoice");
      if (storedInvoice !== String(verifyData.Data.InvoiceId)) {
        console.error(`MyFatoorah: invoice mismatch for hall ${hallId}: expected ${storedInvoice}, got ${verifyData.Data.InvoiceId}`);
        return res.redirect("/hall?payment=error");
      }

      const hallUser = await storage.getUser(hallId);
      if (!hallUser || hallUser.role !== "hall") return res.redirect("/hall?payment=error");

      // Extract recurring token if MyFatoorah provided one
      const recurringId = verifyData.Data.RecurringId || null;
      await activateHallSubscription(hallId, hallUser.name, recurringId);
      return res.redirect("/hall?payment=success");
    } catch (err) {
      console.error("MyFatoorah callback error:", err);
      return res.redirect("/hall?payment=error");
    }
  });

  // MyFatoorah server-to-server webhook (POST) — called by MyFatoorah on payment status change
  // No session required; verify by fetching payment status from MyFatoorah API
  app.post("/api/hall/myfatoorah-callback", async (req, res) => {
    const paymentId = String(req.body?.PaymentId || req.query.paymentId || "");
    if (!paymentId) return res.status(400).json({ success: false, message: "Missing PaymentId" });

    const config = await getMyfatoorahConfig();
    if (!config) return res.status(503).json({ success: false, message: "MyFatoorah not configured" });

    try {
      const apiUrl = getMyfatoorahBaseUrl(config.testMode);
      const verifyData = await myfatoorahPost<MyFatoorahStatusResult>(
        `${apiUrl}/v2/GetPaymentStatus`,
        config.token,
        { Key: paymentId, KeyType: "PaymentId" },
      );

      if (!verifyData.IsSuccess || verifyData.Data?.InvoiceStatus !== "Paid") {
        console.log(`MyFatoorah webhook: non-Paid status for paymentId ${paymentId}: ${verifyData.Data?.InvoiceStatus}`);
        return res.json({ success: true, message: "noted" });
      }

      // Identify hall from CustomerReference
      const ref = verifyData.Data?.CustomerReference || "";
      const match = ref.match(/^hall:(\d+)$/);
      if (!match) return res.json({ success: true, message: "not a hall payment" });
      const hallId = parseInt(match[1]);

      const hallUser = await storage.getUser(hallId);
      if (!hallUser || hallUser.role !== "hall") return res.json({ success: true, message: "hall not found" });

      // Idempotency: check pending invoice (skip if already processed)
      const storedInvoice = await storage.getSetting(hallId, "myfatoorah_pending_invoice");
      if (storedInvoice && storedInvoice === String(verifyData.Data.InvoiceId)) {
        const recurringId = verifyData.Data.RecurringId || null;
        await activateHallSubscription(hallId, hallUser.name, recurringId);
        console.log(`MyFatoorah webhook: activated subscription for hall ${hallId} (${hallUser.name})`);
      }

      return res.json({ success: true });
    } catch (err) {
      console.error("MyFatoorah webhook error:", err);
      return res.status(500).json({ success: false, message: "internal error" });
    }
  });

  // Hall: cancel subscription
  async function cancelHallSubscription(req: Request, res: Response) {
    if (!req.session.userId) return res.status(401).json({ message: "غير مصرح" });
    const hallUser = await storage.getUser(req.session.userId);
    if (!hallUser || hallUser.role !== "hall") return res.status(403).json({ message: "غير مصرح" });

    // Mark as cancelled — hall retains access until nextBillingAt / subscriptionExpiresAt
    await storage.updateUser(hallUser.id, { billingStatus: "cancelled" });

    const allUsers = await storage.getAllUsers();
    const admin = allUsers.find(u => u.isAdmin);
    if (admin?.phoneNumber) {
      const expDate = hallUser.subscriptionExpiresAt
        ? new Date(hallUser.subscriptionExpiresAt).toLocaleDateString("ar-SA-u-ca-gregory")
        : "غير محدد";
      const msg = `ℹ️ *إلغاء اشتراك قاعة*\n\nقاعة *${hallUser.name}* ألغت اشتراكها.\nتاريخ انتهاء الاشتراك الحالي: ${expDate}.`;
      try { await sendViaAnySystem(admin.phoneNumber, msg); } catch (_) { /* best-effort */ }
    }

    res.json({ success: true });
  }

  // Support both POST (spec) and DELETE (RESTful) for cancel
  app.post("/api/hall/cancel-subscription", cancelHallSubscription);
  app.delete("/api/hall/cancel-subscription", cancelHallSubscription);


  try {
    const admins = await storage.getAllUsers();
    if (admins.filter(u => u.isAdmin).length === 0) {
      const code = "3A2EE8F8";
      await storage.createUser({
        name: "مدير النظام",
        phoneNumber: "966557548465",
        accessCode: code,
        messageQuota: 999999,
        messagesSent: 0,
        isAdmin: true,
      });
      console.log(`Admin account created. Access code: ${code}`);
    }
  } catch (err) {
    console.error("Failed to seed admin:", err);
  }

  // Load admin delay settings and apply to WhatsAppManager on startup
  (async () => {
    try {
      const admin = await storage.getAdminUser();
      if (admin) {
        const sec = parseInt(await storage.getSetting(admin.id, "admin_delay_system_invite") || "45");
        WhatsAppManager.setSystemInviteDelay(sec);
        const userHours = parseInt(await storage.getSetting(admin.id, "admin_delay_user_invite_hours") || "24");
        console.log(`[Timing] Loaded delays — system: ${sec}s, personal: ${userHours}h spread`);
      }
    } catch (e) {
      console.error("[Timing] Failed to load delay settings:", e);
    }
  })();

  // ========== SECURITY ADMIN ROUTES ==========

  // IP ban management
  app.get("/api/admin/ip-bans", requireAdmin, async (_req, res) => {
    const bans = await storage.getAllIpBans();
    res.json(bans);
  });

  app.post("/api/admin/ip-bans", requireAdmin, async (req, res) => {
    const ip = getClientIp(req);
    const { targetIp, reason } = z.object({ targetIp: z.string().min(1), reason: z.string().optional() }).parse(req.body);
    await storage.banIp(targetIp, reason || "حظر يدوي", true);
    await storage.addAuditLog({ action: "ip_manual_banned", details: `حظر يدوي لـ ${targetIp}`, ip, actorId: req.session.userId }).catch(() => {});
    res.json({ success: true });
  });

  app.delete("/api/admin/ip-bans/:targetIp", requireAdmin, async (req, res) => {
    const ip = getClientIp(req);
    const targetIp = decodeURIComponent(req.params.targetIp);
    await storage.unbanIp(targetIp);
    loginAttempts.delete(targetIp);
    await storage.addAuditLog({ action: "ip_unbanned", details: `فك حظر IP: ${targetIp}`, ip, actorId: req.session.userId }).catch(() => {});
    res.json({ success: true });
  });

  // Audit log
  app.get("/api/admin/audit-logs", requireAdmin, async (req, res) => {
    const limit = Math.min(Number(req.query.limit) || 100, 500);
    const logs = await storage.getAuditLogs(limit);
    res.json(logs);
  });

  // Admin 2FA (TOTP) setup
  app.get("/api/admin/2fa/status", requireAdmin, async (req, res) => {
    const enabled = await storage.getSetting(req.session.userId!, "admin_totp_enabled").catch(() => undefined);
    res.json({ enabled: enabled === "true" });
  });

  app.post("/api/admin/2fa/setup", requireAdmin, async (req, res) => {
    const secret = authenticator.generateSecret();
    await storage.setSetting(req.session.userId!, "admin_totp_secret_pending", secret);
    const user = await storage.getUser(req.session.userId!);
    const otpauth = authenticator.keyuri(user?.name || "Admin", "إنفايتنا", secret);
    const qrDataUrl = await QRCode.toDataURL(otpauth);
    res.json({ secret, qrDataUrl });
  });

  app.post("/api/admin/2fa/enable", requireAdmin, async (req, res) => {
    const { token } = z.object({ token: z.string().min(6).max(6) }).parse(req.body);
    const pendingSecret = await storage.getSetting(req.session.userId!, "admin_totp_secret_pending");
    if (!pendingSecret) return res.status(400).json({ message: "يجب إنشاء المفتاح أولاً" });
    const valid = authenticator.verify({ token, secret: pendingSecret });
    if (!valid) return res.status(401).json({ message: "كود التحقق غير صحيح" });
    await storage.setSetting(req.session.userId!, "admin_totp_secret", pendingSecret);
    await storage.setSetting(req.session.userId!, "admin_totp_enabled", "true");
    await storage.deleteSetting(req.session.userId!, "admin_totp_secret_pending");
    await storage.addAuditLog({ action: "2fa_enabled", details: "تم تفعيل المصادقة الثنائية", ip: getClientIp(req), actorId: req.session.userId }).catch(() => {});
    res.json({ success: true });
  });

  app.post("/api/admin/2fa/disable", requireAdmin, async (req, res) => {
    const { password } = z.object({ password: z.string().min(1) }).parse(req.body);
    if (password !== ADMIN_PASSWORD) return res.status(401).json({ message: "كلمة المرور غير صحيحة" });
    await storage.setSetting(req.session.userId!, "admin_totp_enabled", "false");
    await storage.setSetting(req.session.userId!, "admin_totp_secret", "");
    await storage.addAuditLog({ action: "2fa_disabled", details: "تم إلغاء تفعيل المصادقة الثنائية", ip: getClientIp(req), actorId: req.session.userId }).catch(() => {});
    res.json({ success: true });
  });

  setupCleanupJobs(uploadDir);

  // Backfill invite slugs for existing guests that don't have one
  setTimeout(() => backfillInviteSlugs(), 5000);

  // ========== DB-DRIVEN SEND QUEUE SCHEDULER ==========
  // Processes send_queue table every 15 seconds — one entry per user per tick.
  // This replaces the old in-memory async IIFE loop, making bulk sends resilient to
  // server restarts, internet drops, and disconnections.

  let sendQueueRunning = false;

  async function processSendQueueEntry(entry: {
    id: number; userId: number; guestId: number; scheduledAt: Date;
    templateId: string; accountId: number | null; useSystem: boolean;
    warmupActive: boolean; attempts: number; lastError: string | null;
    baseUrl: string; intervalMs: number; settingsUserId?: number | null;
  }): Promise<void> {
    const { id, userId, guestId, accountId, useSystem, templateId, baseUrl, warmupActive, attempts } = entry;
    const suid = entry.settingsUserId ?? userId;
    // MAX_RETRY_ATTEMPTS=2: 1 initial attempt + 2 retries = 3 total executions before permanent failure
    const MAX_RETRY_ATTEMPTS = 2;

    const user = await storage.getUser(userId);
    if (!user) {
      await storage.markSendQueueEntry(id, 'failed', 'المستخدم غير موجود');
      return;
    }
    // For secondary users, quota belongs to the primary user
    const quotaUser = user.primaryUserId ? (await storage.getUser(user.primaryUserId) ?? user) : user;
    if (quotaUser.messageQuota !== -1 && quotaUser.messageQuota - quotaUser.messagesSent <= 0) {
      await storage.markSendQueueEntry(id, 'failed', 'رصيد الرسائل منتهي');
      return;
    }

    // Warmup daily limit check
    if (warmupActive && !useSystem) {
      const WARMUP_DAILY_LIMIT = 10;
      const todayKey = new Date().toISOString().split('T')[0];
      const warmupDate = await storage.getSetting(userId, 'warmup_sent_date');
      let warmupSentToday = 0;
      if (warmupDate === todayKey) {
        warmupSentToday = parseInt(await storage.getSetting(userId, 'warmup_sent_count') || '0');
      }
      if (warmupSentToday >= WARMUP_DAILY_LIMIT) {
        // Reschedule for tomorrow: same time-of-day + 24h
        const nextDay = new Date(entry.scheduledAt.getTime() + 24 * 60 * 60 * 1000);
        await storage.rescheduleSendQueueEntry(id, nextDay, attempts);
        console.log(`[Queue] Warmup limit reached for user ${userId}, rescheduled to ${nextDay.toISOString().slice(0, 10)}`);
        return;
      }
    }

    const guest = await storage.getGuest(guestId);
    // IDOR prevention: ensure this guest belongs to the queue entry's user
    if (!guest || guest.userId !== userId) {
      await storage.markSendQueueEntry(id, 'failed', 'الضيف غير موجود أو لا يخص هذا المستخدم');
      return;
    }
    if (guest.sentAt) {
      // Already sent — skip entry
      await storage.markSendQueueEntry(id, 'sent');
      return;
    }

    const inviteLink = `${baseUrl}/i/${guest.inviteSlug || guest.id}`;
    // Use quotaUser.id for event details (primary for secondary users) while suid provides personal settings
    const message = await buildMessage(templateId, quotaUser.id, guest.name, inviteLink, useSystem, suid, suid);
    const mediaUrl = await storage.getSetting(suid, 'global_media_url') || undefined;

    let sent = false;
    let lastError: string | undefined;

    try {
      if (useSystem) {
        const ok = await sendViaSystemDirect(guest.phoneNumber, message, mediaUrl, mediaUrl ? baseUrl : undefined);
        if (ok) sent = true;
        else lastError = 'فشل إرسال الرسالة عبر رقم المشروع';
      } else {
        // Defense-in-depth: verify accountId still belongs to the settings user
        if (accountId) {
          const acct = await storage.getWhatsappAccount(accountId);
          if (!acct || acct.userId !== suid) {
            await storage.markSendQueueEntry(id, 'failed', 'حساب الواتساب لا يخص المستخدم');
            return;
          }
        }
        const session = accountId ? waManager.getSession(accountId) : waManager.getConnectedSessionForUser(suid);
        if (!session || session.status !== 'connected') {
          // Not connected — reschedule in 60s (uniform retry policy), up to MAX_RETRY_ATTEMPTS
          if (attempts < MAX_RETRY_ATTEMPTS) {
            const nextRetry = new Date(Date.now() + 60 * 1000);
            await storage.rescheduleSendQueueEntry(id, nextRetry, attempts + 1);
            console.log(`[Queue] Account ${accountId ?? 'auto'} not connected for user ${userId} — retry ${attempts + 1}/${MAX_RETRY_ATTEMPTS} in 60s`);
          } else {
            await storage.markSendQueueEntry(id, 'failed', 'الحساب غير متصل');
          }
          return;
        }
        let mediaBuffer: Buffer | undefined;
        if (mediaUrl?.startsWith('/api/media/file/')) {
          const mf = await storage.getMediaFile(suid);
          if (mf) mediaBuffer = Buffer.from(mf.data);
        }
        await session.sendMessage(guest.phoneNumber, message, mediaUrl, mediaBuffer);
        sent = true;
      }
    } catch (e: any) {
      lastError = String(e?.message || e).slice(0, 200);
      if (attempts < MAX_RETRY_ATTEMPTS) {
        const nextRetry = new Date(Date.now() + 60 * 1000);
        await storage.rescheduleSendQueueEntry(id, nextRetry, attempts + 1);
        console.warn(`[Queue] Entry ${id} failed (attempt ${attempts + 1}/${MAX_RETRY_ATTEMPTS}), retrying in 60s. Error: ${lastError}`);
      } else {
        await storage.markSendQueueEntry(id, 'failed', lastError);
        console.error(`[Queue] Entry ${id} permanently failed after ${MAX_RETRY_ATTEMPTS} attempts. Error: ${lastError}`);
      }
      return;
    }

    if (!sent) {
      if (attempts < MAX_RETRY_ATTEMPTS) {
        await storage.rescheduleSendQueueEntry(id, new Date(Date.now() + 60 * 1000), attempts + 1);
      } else {
        await storage.markSendQueueEntry(id, 'failed', lastError || 'فشل الإرسال');
      }
      return;
    }

    await storage.markSendQueueEntry(id, 'sent');
    await storage.markGuestSent(guestId);
    await storage.incrementMessagesSent(quotaUser.id);
    // Track when sending last happened — used to suppress spurious disconnect notifications
    await storage.setSetting(userId, 'bulk_send_completed_at', new Date().toISOString()).catch(() => {});
    if (warmupActive) {
      const todayKey = new Date().toISOString().split('T')[0];
      const prevCount = parseInt(await storage.getSetting(userId, 'warmup_sent_count') || '0');
      await storage.setSetting(userId, 'warmup_sent_count', String(prevCount + 1));
      await storage.setSetting(userId, 'warmup_sent_date', todayKey);
    }
    console.log(`[Queue] Sent to guest ${guestId} for user ${userId}`);
  }

  async function processSendQueue(): Promise<void> {
    if (sendQueueRunning) return;
    sendQueueRunning = true;
    try {
      // Fetch all overdue pending entries (sorted by scheduled_at ASC, max 200)
      const dueEntries = await storage.getDueSendQueueEntries();
      // Process at most ONE entry per user per tick to preserve cadence.
      // Any additional overdue entries for a user are rescheduled from now+intervalMs
      // to restore the pre-computed spacing (handles backlog after downtime).
      const processedUsers = new Map<number, number>(); // userId → lastScheduledMs
      for (const entry of dueEntries) {
        if (!processedUsers.has(entry.userId)) {
          // First due entry for this user in this tick — process it
          processedUsers.set(entry.userId, Date.now());
          try {
            await processSendQueueEntry(entry);
          } catch (e) {
            console.error(`[Queue] Unexpected error processing entry ${entry.id}:`, e);
          }
        } else {
          // Subsequent overdue entry for this user — reschedule from last send + intervalMs
          // This restores the pre-computed cadence rather than bursting all overdue entries
          const lastMs = processedUsers.get(entry.userId)!;
          const nextMs = lastMs + entry.intervalMs;
          processedUsers.set(entry.userId, nextMs);
          await storage.rescheduleSendQueueEntry(entry.id, new Date(nextMs), entry.attempts);
        }
      }
      // Periodic cleanup of old completed entries (runs opportunistically)
      if (Math.random() < 0.01) { // ~1% chance per tick = every ~25 minutes on average
        await storage.deleteOldSendQueueEntries().catch(() => {});
      }
    } finally {
      sendQueueRunning = false;
    }
  }

  // Export processSendQueue so cleanup.ts can start the scheduler there
  (global as any).__processSendQueue = processSendQueue;

  // Note: Old bulk_send_active flags in settings table are kept for backward compatibility
  // but are no longer used by the send queue system.

  // =========================================
  // INVITNA AUTO STORE URL (admin setting)
  // =========================================
  app.get("/api/admin/invitna-store-url", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const url = (await storage.getSetting(adminId, "invitna_auto_store_url")) || "https://invitna-web.replit.app/";
    res.json({ url });
  });

  app.put("/api/admin/invitna-store-url", requireAdmin, async (req, res) => {
    const { url } = z.object({ url: z.string().url().min(1) }).parse(req.body);
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "invitna_auto_store_url", url.trim());
    res.json({ success: true });
  });

  // =========================================
  // DISCOUNT ISSUE URL (admin setting)
  // =========================================
  app.get("/api/admin/discount-issue-url", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const url = (await storage.getSetting(adminId, "discount_issue_url")) || "";
    const secretConfigured = !!(
      process.env.MANAGER_REDEMPTION_SECRET ||
      Object.entries(process.env).find(([k]) => k.startsWith("MANAGER_REDEMPTION"))?.[1]
    );
    res.json({ url, secretConfigured });
  });

  app.put("/api/admin/discount-issue-url", requireAdmin, async (req, res) => {
    const { url } = z.object({
      url: z.union([z.string().url(), z.literal("")]),
    }).parse(req.body);
    const adminId = await getAdminId();
    await storage.setSetting(adminId, "discount_issue_url", url.trim());
    res.json({ success: true });
  });

  app.post("/api/admin/test-discount-connection", requireAdmin, async (req, res) => {
    const adminId = await getAdminId();
    const issueUrl = (await storage.getSetting(adminId, "discount_issue_url")) || "https://invitna-web.replit.app/api/manager-redemptions/issue";
    const secret = process.env.MANAGER_REDEMPTION_SECRET ||
      Object.entries(process.env).find(([k]) => k.startsWith("MANAGER_REDEMPTION"))?.[1];

    if (!secret) {
      return res.json({ success: false, reason: "مفتاح MANAGER_REDEMPTION_SECRET غير موجود في متغيرات البيئة" });
    }

    try {
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 10000);
      const extRes = await fetch(issueUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Manager-Secret": secret,
        },
        body: JSON.stringify({ managerUserId: "test", managerPhone: "", customerName: "test", dryRun: true }),
        signal: ctrl.signal,
      }).finally(() => clearTimeout(timer));
      clearTimeout(timer);

      const text = await extRes.text().catch(() => "");
      if (extRes.ok) {
        return res.json({ success: true, status: extRes.status, body: text.slice(0, 200) });
      }
      const reason = extRes.status === 401 || extRes.status === 403
        ? `الخادم رفض الطلب (${extRes.status}) — تحقق من مفتاح MANAGER_REDEMPTION_SECRET`
        : extRes.status === 404
        ? `الرابط غير موجود (404) — تحقق من صحة الرابط`
        : `الخادم رد بكود ${extRes.status}`;
      return res.json({ success: false, reason, status: extRes.status, body: text.slice(0, 200) });
    } catch (err: unknown) {
      const isTimeout = err instanceof Error && err.name === "AbortError";
      const msg = isTimeout ? "انتهت مهلة الاتصال (10 ثوان)" : (err instanceof Error ? err.message : "خطأ شبكي");
      return res.json({ success: false, reason: msg });
    }
  });

  // =========================================
  // DISCOUNT CODE — hall clients only
  // =========================================
  app.get("/api/discount/status", requireAuth, async (req, res) => {
    const user = await storage.getUser(req.session.userId!);
    if (!user || !user.parentHallId) return res.status(403).json({ message: "غير متاح" });
    const adminId = await getAdminId();
    const storeUrl = (await storage.getSetting(adminId, "invitna_auto_store_url")) || "https://invitna-web.replit.app/";
    const secretExists = !!(
      process.env.MANAGER_REDEMPTION_SECRET ||
      Object.entries(process.env).find(([k]) => k.startsWith("MANAGER_REDEMPTION"))?.[1]
    );
    const isConfigured = secretExists;
    const code = await storage.getSetting(user.id, "discount_code");
    const expiresAt = (await storage.getSetting(user.id, "discount_code_expires_at")) || null;
    if (code) {
      res.json({ hasClaim: true, code, expiresAt, storeUrl, isConfigured });
    } else {
      res.json({ hasClaim: false, code: null, expiresAt: null, storeUrl, isConfigured });
    }
  });

  app.post("/api/discount/claim", requireAuth, async (req, res) => {
    const user = await storage.getUser(req.session.userId!);
    if (!user || !user.parentHallId) return res.status(403).json({ message: "غير متاح لهذا الحساب" });

    const existingCode = await storage.getSetting(user.id, "discount_code");
    const adminId = await getAdminId();
    const storeUrl = (await storage.getSetting(adminId, "invitna_auto_store_url")) || "https://invitna-web.replit.app/";

    if (existingCode) {
      const existingExpiry = (await storage.getSetting(user.id, "discount_code_expires_at")) || null;
      return res.json({ success: true, code: existingCode, expiresAt: existingExpiry, storeUrl, alreadyHad: true });
    }

    // Server-side gate: hall must have allocated at least 25 invitations to this client
    if ((user.messageQuota ?? 0) < 25) {
      return res.status(403).json({ message: "يجب أن يكون رصيد الحساب 25 دعوة على الأقل للحصول على كود الخصم" });
    }

    // Rolling 30-day limit: max 50 codes per hall
    // Uses a durable hall-level event log so deleted client accounts cannot evade the limit
    const MONTHLY_CODE_LIMIT = 50;
    const hallId = user.parentHallId!;
    const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

    const hallEventsRaw = await storage.getSetting(hallId, "discount_code_events");
    let hallEvents: string[] = [];
    try { hallEvents = hallEventsRaw ? JSON.parse(hallEventsRaw) : []; } catch { hallEvents = []; }
    const eventsInWindow = hallEvents.filter(ts => {
      const d = new Date(ts);
      return !isNaN(d.getTime()) && d >= thirtyDaysAgo;
    });
    const codesThisMonth = eventsInWindow.length;

    if (codesThisMonth >= MONTHLY_CODE_LIMIT) {
      const sortedEvents = [...eventsInWindow].sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
      const earliestDate = sortedEvents.length > 0 ? new Date(sortedEvents[0]) : null;
      const renewalDate = earliestDate
        ? new Date(earliestDate.getTime() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString("ar-SA")
        : "قريباً";
      return res.status(429).json({ message: `تم الوصول للحد الأقصى (${MONTHLY_CODE_LIMIT} كود شهرياً). يتجدد الحد بتاريخ ${renewalDate}` });
    }

    // Resolve the secret — try the primary key, then scan for any key with MANAGER_REDEMPTION prefix
    const secret = process.env.MANAGER_REDEMPTION_SECRET ||
      Object.entries(process.env).find(([k]) => k.startsWith("MANAGER_REDEMPTION"))?.[1];
    if (!secret) {
      return res.status(500).json({ message: "لم يتم إعداد مفتاح الخدمة — تواصل مع الإدارة" });
    }

    const issueUrl = (await storage.getSetting(adminId, "discount_issue_url")) || "https://invitna-web.replit.app/api/manager-redemptions/issue";

    try {
      const TIMEOUT_MS = 15000;
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
      let extRes: Response;
      try {
        extRes = await fetch(issueUrl, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-Manager-Secret": secret,
          },
          body: JSON.stringify({
            managerUserId: String(user.id),
            managerPhone: user.phoneNumber || "",
            customerName: user.name || user.username || "",
          }),
          signal: ctrl.signal,
        });
      } catch (fetchErr: unknown) {
        clearTimeout(timer);
        const isTimeout = fetchErr instanceof Error && fetchErr.name === "AbortError";
        if (isTimeout) {
          return res.status(502).json({ message: "انتهت مهلة الاتصال بخدمة الكوبونات (15 ثانية) — تأكد من صحة الرابط" });
        }
        const netMsg = fetchErr instanceof Error ? fetchErr.message : "خطأ شبكي";
        return res.status(502).json({ message: `فشل الاتصال بخدمة الكوبونات (خطأ شبكي): ${netMsg}` });
      }
      clearTimeout(timer);

      if (!extRes.ok && extRes.status >= 500) {
        const bodyText = await extRes.text().catch(() => "");
        return res.status(502).json({ message: `خدمة الكوبونات ردّت بخطأ ${extRes.status} — ${bodyText.slice(0, 150) || "لا تفاصيل"}` });
      }

      interface ExternalClaimResponse {
        code?: string;
        couponCode?: string;
        discountCode?: string;
        expiresAt?: string;
        expires_at?: string;
        expiry?: string;
        message?: string;
        error?: string;
      }
      let data: ExternalClaimResponse;
      try {
        data = await extRes.json();
      } catch {
        return res.status(502).json({ message: `خدمة الكوبونات ردّت بكود ${extRes.status} لكن بيانات غير صالحة (ليست JSON)` });
      }

      const code = data.code || data.couponCode || data.discountCode;

      if (!code) {
        const msg = data.message || data.error || `الخدمة ردّت بكود ${extRes.status} دون إرسال كود خصم`;
        return res.status(502).json({ message: msg });
      }

      // Always use 3-day expiry from generation date (regardless of external service)
      const threeDaysFromNow = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();
      const generatedAt = new Date().toISOString();

      await storage.setSetting(user.id, "discount_code", String(code));
      await storage.setSetting(user.id, "discount_code_expires_at", threeDaysFromNow);
      await storage.setSetting(user.id, "discount_code_generated_at", generatedAt);

      // Persist issuance in hall-level durable event log (survives client account deletion)
      const updatedEventsRaw = await storage.getSetting(hallId, "discount_code_events");
      let updatedEvents: string[] = [];
      try { updatedEvents = updatedEventsRaw ? JSON.parse(updatedEventsRaw) : []; } catch { updatedEvents = []; }
      updatedEvents.push(generatedAt);
      // Trim entries older than 90 days to prevent unbounded growth
      const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
      updatedEvents = updatedEvents.filter(ts => { const d = new Date(ts); return !isNaN(d.getTime()) && d >= ninetyDaysAgo; });
      await storage.setSetting(hallId, "discount_code_events", JSON.stringify(updatedEvents));

      res.json({ success: true, code: String(code), expiresAt: threeDaysFromNow, storeUrl, alreadyHad: false });
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "خطأ غير متوقع";
      res.status(502).json({ message: `خطأ غير متوقع أثناء استلام الكود: ${msg}` });
    }
  });

  return httpServer;
}

function deleteUploadFile(fileUrl: string, uploadDir: string) {
  if (fileUrl?.startsWith('/uploads/')) {
    const filePath = path.join(uploadDir, fileUrl.replace('/uploads/', ''));
    try {
      if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
    } catch (e) {}
  }
}

async function cleanupUserMedia(userId: number, _uploadDir: string) {
  const mediaUrl = await storage.getSetting(userId, "global_media_url");
  if (mediaUrl) {
    await storage.deleteMediaFile(userId);
    await storage.cleanupOldMedia(userId);
  }
}
