import { Router } from "express";
import { z } from "zod";
import {
  User,
  Property,
  Booking,
  City,
  ExchangeRate,
  ContactMessage,
  Review,
  Coupon,
  Wallet,
  WalletTxn,
  WalletTopUp,
  WithdrawalRequest,
  LedgerEntry,
  Refund,
  ActivityLog,
  PasswordResetToken,
} from "@/db/models";
import { asyncHandler, AppError } from "@/lib/errors";
import { requireAuth, requireStaff } from "@/middleware/auth";
import { hasAdminPermission, isStaffRole, normalizeStaffRole, permissionsForRole } from "@/lib/auth/rbac";
import { resolveAdminRoutePermission } from "@/lib/auth/adminRoutePermissions";
import { toNumber, toPlain, serializeBooking, serializeProperty } from "@/lib/serialize";
import { notifyUser } from "@/services/notifications";
import { getCommerceSettings, setCommerceSettings, DEFAULT_COMMERCE } from "@/services/commerce";
import { creditWallet, debitWallet, ensureWallet, getWalletBalance } from "@/services/wallet";
import { ensureLoyalty, adjustLoyalty } from "@/services/loyalty";
import { sendMail } from "@/services/email";
import { env } from "@/config/env";
import { WITHDRAWAL_STATUSES, LEDGER_ENTRY_TYPES, STAFF_ROLE_VALUES } from "@/db/types";
import {
  postWithdrawalLedger,
  recordRefund,
  withDbTransaction,
  getReconciliationSummary,
  backfillLedgerEntries,
  syncBookingToMysql,
  sumLedgerByTypePosted,
} from "@/services/ledger";
import { logActivity, listActivityMysql, FINANCE_ACTIVITY_ACTIONS } from "@/services/activityLog";
import { createRawToken, hashToken } from "@/lib/crypto";
import { PRIMARY_SUPER_ADMIN_EMAIL } from "@/services/adminMigration";
import {
  isAuthMysql,
  isBookingsMysql,
  isCitiesMysql,
  isContactMysql,
  isLedgerMysql,
  isMysqlActive,
  isPropertiesMysql,
  isRefundsMysql,
  isReviewsMysql,
  isWalletsMysql,
  isWithdrawalsMysql,
} from "@/db/activeDatabase";
import { sqlQuery, sqlExecute } from "@/db/mysql/pool";
import { createId } from "@/db/ids";
import { isFinancialDualWriteEnabled, withFinancialDualWrite } from "@/db/dualWriteFinancial";
import {
  findUserByIdMysql,
  findUserByEmailMysql,
  findUsersByIdsMysql,
  createUserMysql,
  updateUserMysql,
} from "@/db/mysql/users";
import {
  listBookingsMysql,
  bookingToApi,
  findBookingByIdMysql,
} from "@/db/mysql/bookings";
import {
  listPropertiesMysql,
  propertyToApi,
  findPropertiesByIdsMysql,
  findPropertyByIdMysql,
  updatePropertyMysql,
} from "@/db/mysql/properties";
import { findCitiesByIdsMysql, cityRowToApi } from "@/db/mysql/cities";
import {
  listReviewsMysql,
  reviewToApi,
  findReviewByIdMysql,
  deleteReviewMysql,
} from "@/db/mysql/reviews";
import { listContactMysql, contactToApi, setContactReadMysql } from "@/db/mysql/contact";
import {
  listWalletsMysql,
  walletToApi,
  listWalletTopUpsMysql,
  walletTopUpToApi,
  findWalletTopUpByIdMysql,
  listWalletTxnsMysql,
  walletTxnToApi,
} from "@/db/mysql/wallets";
import {
  upsertWalletTopUpMysql,
  upsertWithdrawalMysql,
  upsertBookingMysql,
  insertRefundMysql,
  insertLedgerEntryMysql,
} from "@/db/mysql/financialWrites";
import {
  listWithdrawalsMysql,
  countWithdrawalsMysql,
  findWithdrawalByIdMysql,
  withdrawalToApi,
} from "@/db/mysql/withdrawals";
import { listRefundsMysql, refundToApi } from "@/db/mysql/refunds";
import { listLedgerEntriesMysql, ledgerEntryToApi } from "@/db/mysql/ledger";
import { createPasswordResetTokenMysql } from "@/db/mysql/authTokens";

export const adminRouter = Router();

adminRouter.use(requireAuth, requireStaff());
adminRouter.use((req, _res, next) => {
  const needed = resolveAdminRoutePermission(req.method, req.path);
  if (!needed) return next();
  const list = Array.isArray(needed) ? needed : [needed];
  const ok = list.every((p) => hasAdminPermission(req.user!.role, p));
  if (!ok) return next(new AppError(403, "Forbidden"));
  next();
});

adminRouter.get(
  "/analytics",
  asyncHandler(async (_req, res) => {
    const now = new Date();
    const startOfThisMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
    const startOfLastMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
    const startOfNextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));

    let users: number;
    let owners: number;
    let properties: number;
    let bookings: number;
    let cities: number;
    let paidBookings: Array<{
      payment?: { amount?: number; createdAt?: Date } | null;
      totalTnd?: number;
      totalLyd?: number;
      createdAt: Date;
    }>;
    let usersThisMonth: number;
    let usersLastMonth: number;
    let ownersThisMonth: number;
    let ownersLastMonth: number;
    let propertiesThisMonth: number;
    let propertiesLastMonth: number;
    let bookingsThisMonth: number;
    let bookingsLastMonth: number;

    if (isAuthMysql() && isPropertiesMysql() && isBookingsMysql() && isCitiesMysql()) {
      type CountRow = import("mysql2/promise").RowDataPacket;
      const countSql = async (sql: string, params: unknown[] = []) => {
        const rows = await sqlQuery<CountRow[]>(sql, params);
        return Number(rows[0]?.n || 0);
      };
      [
        users,
        owners,
        properties,
        bookings,
        cities,
        usersThisMonth,
        usersLastMonth,
        ownersThisMonth,
        ownersLastMonth,
        propertiesThisMonth,
        propertiesLastMonth,
        bookingsThisMonth,
        bookingsLastMonth,
      ] = await Promise.all([
        countSql(`SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL`),
        countSql(`SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL AND role = 'OWNER'`),
        countSql(`SELECT COUNT(*) AS n FROM properties WHERE deleted_at IS NULL`),
        countSql(`SELECT COUNT(*) AS n FROM bookings WHERE deleted_at IS NULL`),
        countSql(`SELECT COUNT(*) AS n FROM cities WHERE deleted_at IS NULL`),
        countSql(`SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL AND created_at >= ?`, [
          startOfThisMonth,
        ]),
        countSql(
          `SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?`,
          [startOfLastMonth, startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL AND role = 'OWNER' AND created_at >= ?`,
          [startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM users WHERE deleted_at IS NULL AND role = 'OWNER' AND created_at >= ? AND created_at < ?`,
          [startOfLastMonth, startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM properties WHERE deleted_at IS NULL AND created_at >= ?`,
          [startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM properties WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?`,
          [startOfLastMonth, startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM bookings WHERE deleted_at IS NULL AND created_at >= ?`,
          [startOfThisMonth],
        ),
        countSql(
          `SELECT COUNT(*) AS n FROM bookings WHERE deleted_at IS NULL AND created_at >= ? AND created_at < ?`,
          [startOfLastMonth, startOfThisMonth],
        ),
      ]);
      const paidRows = await listBookingsMysql({ paymentStatus: "PAID", take: 5000 });
      paidBookings = paidRows.map((b) => ({
        payment: b.payment
          ? {
              amount: Number((b.payment as any).amount ?? b.totalLyd),
              createdAt: (b.payment as any).createdAt
                ? new Date((b.payment as any).createdAt)
                : b.createdAt,
            }
          : { amount: b.totalLyd, createdAt: b.createdAt },
        totalTnd: b.totalTnd,
        totalLyd: b.totalLyd,
        createdAt: b.createdAt,
      }));
    } else {
      [
        users,
        owners,
        properties,
        bookings,
        cities,
        paidBookings,
        usersThisMonth,
        usersLastMonth,
        ownersThisMonth,
        ownersLastMonth,
        propertiesThisMonth,
        propertiesLastMonth,
        bookingsThisMonth,
        bookingsLastMonth,
      ] = await Promise.all([
        User.countDocuments({ deletedAt: null }),
        User.countDocuments({ role: "OWNER", deletedAt: null }),
        Property.countDocuments({ deletedAt: null }),
        Booking.countDocuments({ deletedAt: null }),
        City.countDocuments({ deletedAt: null }),
        Booking.find({ "payment.status": "PAID", deletedAt: null })
          .select("payment.amount payment.createdAt totalTnd totalLyd createdAt")
          .lean(),
        User.countDocuments({ deletedAt: null, createdAt: { $gte: startOfThisMonth } }),
        User.countDocuments({
          deletedAt: null,
          createdAt: { $gte: startOfLastMonth, $lt: startOfThisMonth },
        }),
        User.countDocuments({
          role: "OWNER",
          deletedAt: null,
          createdAt: { $gte: startOfThisMonth },
        }),
        User.countDocuments({
          role: "OWNER",
          deletedAt: null,
          createdAt: { $gte: startOfLastMonth, $lt: startOfThisMonth },
        }),
        Property.countDocuments({ deletedAt: null, createdAt: { $gte: startOfThisMonth } }),
        Property.countDocuments({
          deletedAt: null,
          createdAt: { $gte: startOfLastMonth, $lt: startOfThisMonth },
        }),
        Booking.countDocuments({ deletedAt: null, createdAt: { $gte: startOfThisMonth } }),
        Booking.countDocuments({
          deletedAt: null,
          createdAt: { $gte: startOfLastMonth, $lt: startOfThisMonth },
        }),
      ]);
    }

    const paidPayments = paidBookings
      .filter((b) => b.payment)
      .map((b) => {
        const createdAt = b.payment!.createdAt ?? b.createdAt;
        return {
          amountLyd: toNumber(b.payment!.amount ?? b.totalLyd),
          amountTnd: toNumber(b.totalTnd),
          createdAt: createdAt instanceof Date ? createdAt : new Date(createdAt),
        };
      });

    const revenueLyd = paidPayments.reduce((s, p) => s + p.amountLyd, 0);
    const revenueTnd = paidPayments.reduce((s, p) => s + p.amountTnd, 0);

    const byMonth = new Map<string, { totalLyd: number; totalTnd: number }>();
    let revenueThisMonthLyd = 0;
    let revenueLastMonthLyd = 0;

    for (const p of paidPayments) {
      const key = `${p.createdAt.getUTCFullYear()}-${String(p.createdAt.getUTCMonth() + 1).padStart(2, "0")}`;
      const prev = byMonth.get(key) ?? { totalLyd: 0, totalTnd: 0 };
      byMonth.set(key, {
        totalLyd: prev.totalLyd + p.amountLyd,
        totalTnd: prev.totalTnd + p.amountTnd,
      });

      if (p.createdAt >= startOfThisMonth && p.createdAt < startOfNextMonth) {
        revenueThisMonthLyd += p.amountLyd;
      } else if (p.createdAt >= startOfLastMonth && p.createdAt < startOfThisMonth) {
        revenueLastMonthLyd += p.amountLyd;
      }
    }

    /** Always return last 12 calendar months so the chart is never a single bar. */
    const revenueByMonth: Array<{ month: string; total: number; totalTnd: number }> = [];
    for (let i = 11; i >= 0; i -= 1) {
      const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - i, 1));
      const month = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
      const entry = byMonth.get(month) ?? { totalLyd: 0, totalTnd: 0 };
      revenueByMonth.push({
        month,
        total: Math.round(entry.totalLyd * 100) / 100,
        totalTnd: Math.round(entry.totalTnd * 100) / 100,
      });
    }

    const pctChange = (current: number, previous: number) => {
      if (previous === 0) return current > 0 ? 100 : 0;
      return Math.round(((current - previous) / previous) * 1000) / 10;
    };

    res.json({
      analytics: {
        users,
        owners,
        properties,
        bookings,
        cities,
        revenueLyd: Math.round(revenueLyd * 100) / 100,
        revenueTnd: Math.round(revenueTnd * 100) / 100,
        revenueByMonth,
        generatedAt: now.toISOString(),
        trends: {
          users: pctChange(usersThisMonth, usersLastMonth),
          owners: pctChange(ownersThisMonth, ownersLastMonth),
          properties: pctChange(propertiesThisMonth, propertiesLastMonth),
          bookings: pctChange(bookingsThisMonth, bookingsLastMonth),
          revenueLyd: pctChange(revenueThisMonthLyd, revenueLastMonthLyd),
        },
        period: {
          usersThisMonth,
          usersLastMonth,
          ownersThisMonth,
          ownersLastMonth,
          propertiesThisMonth,
          propertiesLastMonth,
          bookingsThisMonth,
          bookingsLastMonth,
          revenueThisMonthLyd: Math.round(revenueThisMonthLyd * 100) / 100,
          revenueLastMonthLyd: Math.round(revenueLastMonthLyd * 100) / 100,
        },
      },
      _db: isBookingsMysql() ? "mysql" : "mongodb",
    });
  }),
);

adminRouter.get(
  "/users",
  asyncHandler(async (req, res) => {
    const role = typeof req.query.role === "string" ? req.query.role : undefined;
    if (isAuthMysql()) {
      const where = [
        "deleted_at IS NULL",
        "role NOT IN ('SUPER_ADMIN','FINANCE_ADMIN','OPERATIONS_ADMIN','VIEWER','ADMIN')",
      ];
      const params: unknown[] = [];
      if (role === "CUSTOMER" || role === "OWNER") {
        where.push("role = ?");
        params.push(role);
      }
      params.push(200);
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT id, email, full_name, role, status, locale, created_at, email_verified_at, phone,
                passport_url, owner_verification_status, trusted_owner, owner_verified_at
         FROM users
         WHERE ${where.join(" AND ")}
         ORDER BY created_at DESC
         LIMIT ?`,
        params,
      );
      res.json({
        users: rows.map((r) => ({
          id: String(r.id),
          email: String(r.email),
          fullName: String(r.full_name),
          role: String(r.role),
          status: String(r.status),
          locale: String(r.locale || "ar"),
          createdAt: r.created_at,
          emailVerifiedAt: r.email_verified_at,
          phone: r.phone,
          passportUrl: r.passport_url,
          ownerVerificationStatus: r.owner_verification_status,
          trustedOwner: Boolean(r.trusted_owner),
          ownerVerifiedAt: r.owner_verified_at,
        })),
        _db: "mysql",
      });
      return;
    }

    // Marketplace users only (staff managed under /team)
    const staffFilter = {
      role: { $nin: ["SUPER_ADMIN", "FINANCE_ADMIN", "OPERATIONS_ADMIN", "VIEWER", "ADMIN"] },
    };
    const users = await User.find({
      deletedAt: null,
      ...staffFilter,
      ...(role && (role === "CUSTOMER" || role === "OWNER") ? { role } : {}),
    })
      .select(
        "email fullName role status locale createdAt emailVerifiedAt phone passportUrl ownerVerificationStatus trustedOwner ownerVerifiedAt",
      )
      .sort({ createdAt: -1 })
      .limit(200)
      .lean();
    res.json({ users: users.map(toPlain) });
  }),
);

adminRouter.patch(
  "/users/:id",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        status: z.enum(["PENDING_EMAIL_VERIFICATION", "ACTIVE", "SUSPENDED"]).optional(),
        role: z.enum(["CUSTOMER", "OWNER"]).optional(),
        ownerVerificationStatus: z.enum(["NONE", "PENDING", "APPROVED", "REJECTED"]).optional(),
        trustedOwner: z.boolean().optional(),
      })
      .parse(req.body);

    if (isAuthMysql()) {
      const target = await findUserByIdMysql(req.params.id);
      if (!target || target.deletedAt) throw new AppError(404, "User not found");
      if (isStaffRole(target.role)) {
        throw new AppError(400, "Manage staff members from Team page");
      }
      const patch: Parameters<typeof updateUserMysql>[1] = { ...body };
      if (body.ownerVerificationStatus === "APPROVED") {
        patch.trustedOwner = true;
        patch.ownerVerifiedAt = new Date();
      }
      if (body.ownerVerificationStatus === "REJECTED" || body.ownerVerificationStatus === "NONE") {
        patch.trustedOwner = false;
      }
      if (body.ownerVerificationStatus === "PENDING") {
        patch.trustedOwner = false;
      }
      const user = await updateUserMysql(req.params.id, patch);
      if (!user) throw new AppError(404, "User not found");
      await logActivity({
        actor: req.user!,
        action: "user.update",
        entityType: "user",
        entityId: user.id,
        meta: body,
      });
      res.json({
        user: {
          id: user.id,
          email: user.email,
          fullName: user.fullName,
          role: user.role,
          status: user.status,
          passportUrl: user.passportUrl,
          ownerVerificationStatus: user.ownerVerificationStatus,
          trustedOwner: user.trustedOwner,
          ownerVerifiedAt: user.ownerVerifiedAt,
        },
        _db: "mysql",
      });
      return;
    }

    const target = await User.findById(req.params.id);
    if (!target) throw new AppError(404, "User not found");
    if (isStaffRole(target.role)) {
      throw new AppError(400, "Manage staff members from Team page");
    }

    const $set: Record<string, unknown> = { ...body };
    if (body.ownerVerificationStatus === "APPROVED") {
      $set.trustedOwner = true;
      $set.ownerVerifiedAt = new Date();
    }
    if (body.ownerVerificationStatus === "REJECTED" || body.ownerVerificationStatus === "NONE") {
      $set.trustedOwner = false;
    }
    if (body.ownerVerificationStatus === "PENDING") {
      $set.trustedOwner = false;
    }

    const user = await User.findByIdAndUpdate(req.params.id, { $set }, { new: true })
      .select(
        "email fullName role status passportUrl ownerVerificationStatus trustedOwner ownerVerifiedAt",
      )
      .lean();
    if (!user) throw new AppError(404, "User not found");

    await logActivity({
      actor: req.user!,
      action: "user.update",
      entityType: "user",
      entityId: String(user._id),
      meta: body,
    });

    res.json({ user: toPlain(user) });
  }),
);

adminRouter.get(
  "/bookings",
  asyncHandler(async (req, res) => {
    const status = typeof req.query.status === "string" ? req.query.status : undefined;

    if (isBookingsMysql()) {
      const bookings = await listBookingsMysql({ status: status || undefined, take: 100 });
      const propertyIds = [...new Set(bookings.map((b) => b.propertyId))];
      const customerIds = [...new Set(bookings.map((b) => b.customerId))];
      const ownerIds = [...new Set(bookings.map((b) => b.ownerId))];
      const [properties, customers, owners] = await Promise.all([
        isPropertiesMysql()
          ? findPropertiesByIdsMysql(propertyIds)
          : Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr").lean(),
        isAuthMysql()
          ? findUsersByIdsMysql(customerIds)
          : User.find({ _id: { $in: customerIds } }).select("fullName email").lean(),
        isAuthMysql()
          ? findUsersByIdsMysql(ownerIds)
          : User.find({ _id: { $in: ownerIds } }).select("fullName email").lean(),
      ]);
      const propertyMap = new Map(
        (properties as any[]).map((p) => {
          const api = p.titleEn != null && p.id ? p : propertyToApi(p);
          return [
            String(api.id || api._id),
            { id: api.id || api._id, titleEn: api.titleEn, titleAr: api.titleAr },
          ];
        }),
      );
      const customerMap = new Map(
        (customers as any[]).map((c) => [
          String(c.id || c._id),
          { id: c.id || c._id, fullName: c.fullName, email: c.email },
        ]),
      );
      const ownerMap = new Map(
        (owners as any[]).map((o) => [
          String(o.id || o._id),
          { id: o.id || o._id, fullName: o.fullName, email: o.email },
        ]),
      );
      res.json({
        bookings: bookings.map((b) =>
          serializeBooking({
            ...bookingToApi(b),
            property: propertyMap.get(b.propertyId),
            customer: customerMap.get(b.customerId),
            owner: ownerMap.get(b.ownerId),
          }),
        ),
        _db: "mysql",
      });
      return;
    }

    const bookings = await Booking.find({
      deletedAt: null,
      ...(status ? { status } : {}),
    })
      .sort({ createdAt: -1 })
      .limit(100)
      .lean();
    const propertyIds = [...new Set(bookings.map((b) => b.propertyId))];
    const customerIds = [...new Set(bookings.map((b) => b.customerId))];
    const ownerIds = [...new Set(bookings.map((b) => b.ownerId))];
    const [properties, customers, owners] = await Promise.all([
      Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr").lean(),
      User.find({ _id: { $in: customerIds } }).select("fullName email").lean(),
      User.find({ _id: { $in: ownerIds } }).select("fullName email").lean(),
    ]);
    const propertyMap = new Map(
      properties.map((p) => [p._id, { id: p._id, titleEn: p.titleEn, titleAr: p.titleAr }]),
    );
    const customerMap = new Map(
      customers.map((c) => [c._id, { id: c._id, fullName: c.fullName, email: c.email }]),
    );
    const ownerMap = new Map(
      owners.map((o) => [o._id, { id: o._id, fullName: o.fullName, email: o.email }]),
    );

    res.json({
      bookings: bookings.map((b) =>
        serializeBooking({
          ...b,
          property: propertyMap.get(b.propertyId),
          customer: customerMap.get(b.customerId),
          owner: ownerMap.get(b.ownerId),
        }),
      ),
    });
  }),
);

adminRouter.get(
  "/payments",
  asyncHandler(async (_req, res) => {
    if (isBookingsMysql()) {
      const bookings = await listBookingsMysql({ hasPayment: true, take: 100 });
      const propertyIds = [...new Set(bookings.map((b) => b.propertyId))];
      const customerIds = [...new Set(bookings.map((b) => b.customerId))];
      const [properties, customers] = await Promise.all([
        isPropertiesMysql()
          ? findPropertiesByIdsMysql(propertyIds)
          : Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr").lean(),
        isAuthMysql()
          ? findUsersByIdsMysql(customerIds)
          : User.find({ _id: { $in: customerIds } }).select("fullName email").lean(),
      ]);
      const propertyMap = new Map(
        (properties as any[]).map((p) => {
          const api = p.titleEn != null && p.id ? p : propertyToApi(p);
          return [
            String(api.id || api._id),
            { id: api.id || api._id, titleEn: api.titleEn, titleAr: api.titleAr },
          ];
        }),
      );
      const customerMap = new Map(
        (customers as any[]).map((c) => [
          String(c.id || c._id),
          { id: c.id || c._id, fullName: c.fullName, email: c.email },
        ]),
      );
      const payments = bookings
        .filter((b) => b.payment)
        .map((b) => {
          const prop = propertyMap.get(b.propertyId);
          const customer = customerMap.get(b.customerId);
          const payment = b.payment as Record<string, unknown>;
          return {
            ...toPlain({ _id: payment.id || b.id, ...payment }),
            amount: toNumber((payment.amount as number) ?? b.totalLyd),
            booking: {
              id: b.id,
              status: b.status,
              customerId: b.customerId,
              customer: customer ?? null,
              property: prop ?? null,
            },
          };
        });
      res.json({ payments, _db: "mysql" });
      return;
    }

    const bookings = await Booking.find({ payment: { $exists: true, $ne: null } })
      .sort({ createdAt: -1 })
      .limit(100)
      .lean();
    const propertyIds = [...new Set(bookings.map((b) => b.propertyId))];
    const customerIds = [...new Set(bookings.map((b) => b.customerId))];
    const [properties, customers] = await Promise.all([
      Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr").lean(),
      User.find({ _id: { $in: customerIds } }).select("fullName email").lean(),
    ]);
    const propertyMap = new Map(
      properties.map((p) => [String(p._id), { id: p._id, titleEn: p.titleEn, titleAr: p.titleAr }]),
    );
    const customerMap = new Map(
      customers.map((c) => [String(c._id), { id: c._id, fullName: c.fullName, email: c.email }]),
    );

    const payments = bookings
      .filter((b) => b.payment)
      .map((b) => {
        const prop = propertyMap.get(b.propertyId);
        const customer = customerMap.get(b.customerId);
        return {
          ...toPlain(b.payment),
          amount: toNumber(b.payment!.amount),
          booking: {
            id: b._id,
            status: b.status,
            customerId: b.customerId,
            customer: customer ?? null,
            property: prop ?? null,
          },
        };
      });

    res.json({ payments });
  }),
);

adminRouter.get(
  "/properties",
  asyncHandler(async (req, res) => {
    const status = typeof req.query.status === "string" ? req.query.status : undefined;
    if (isPropertiesMysql()) {
      const { properties } = await listPropertiesMysql({
        status: status || undefined,
        take: 200,
        skip: 0,
      });
      const ownerIds = [...new Set(properties.map((p) => p.ownerId))];
      const cityIds = [...new Set(properties.map((p) => p.cityId))];
      const owners = isAuthMysql()
        ? await findUsersByIdsMysql(ownerIds)
        : await User.find({ _id: { $in: ownerIds } }).select("fullName email").lean();
      const ownerMap = new Map(
        (owners as any[]).map((o) => [
          String(o.id || o._id),
          { id: o.id || o._id, fullName: o.fullName, email: o.email },
        ]),
      );
      const cities = isCitiesMysql()
        ? await findCitiesByIdsMysql(cityIds)
        : await City.find({ _id: { $in: cityIds } }).select("nameEn nameAr").lean();
      const cityMap = new Map(
        isCitiesMysql()
          ? (cities as any[]).map((c) => [c.id, cityRowToApi(c)])
          : (cities as any[]).map((c) => [
              String(c._id),
              { id: c._id, nameEn: c.nameEn, nameAr: c.nameAr },
            ]),
      );
      res.json({
        properties: properties.map((p) =>
          serializeProperty({
            ...propertyToApi(p),
            owner: ownerMap.get(p.ownerId),
            city: cityMap.get(p.cityId),
          }),
        ),
        _db: "mysql",
      });
      return;
    }

    const properties = await Property.find({
      deletedAt: null,
      ...(status ? { status } : {}),
    })
      .sort({ createdAt: -1 })
      .limit(200)
      .lean();

    const ownerIds = [...new Set(properties.map((p) => p.ownerId))];
    const cityIds = [...new Set(properties.map((p) => p.cityId))];
    const [owners, cities] = await Promise.all([
      User.find({ _id: { $in: ownerIds } }).select("fullName email").lean(),
      City.find({ _id: { $in: cityIds } }).select("nameEn nameAr").lean(),
    ]);
    const ownerMap = new Map(
      owners.map((o) => [String(o._id), { id: o._id, fullName: o.fullName, email: o.email }]),
    );
    const cityMap = new Map(
      cities.map((c) => [String(c._id), { id: c._id, nameEn: c.nameEn, nameAr: c.nameAr }]),
    );

    res.json({
      properties: properties.map((p) => {
        const owner = ownerMap.get(p.ownerId);
        const city = cityMap.get(p.cityId);
        return serializeProperty({
          ...p,
          owner,
          city,
        });
      }),
    });
  }),
);

adminRouter.patch(
  "/properties/:id/feature",
  asyncHandler(async (req, res) => {
    const featured = z.boolean().parse(req.body.featured);
    if (isPropertiesMysql()) {
      const property = await updatePropertyMysql(req.params.id, { featured });
      if (!property) throw new AppError(404, "Property not found");
      res.json({ property: { id: property.id, featured: property.featured }, _db: "mysql" });
      return;
    }
    const property = await Property.findByIdAndUpdate(
      req.params.id,
      { $set: { featured } },
      { new: true },
    ).lean();
    if (!property) throw new AppError(404, "Property not found");
    res.json({ property: { id: property._id, featured: property.featured } });
  }),
);

adminRouter.patch(
  "/properties/:id/status",
  asyncHandler(async (req, res) => {
    const status = z.enum(["DRAFT", "PUBLISHED", "PAUSED", "REJECTED"]).parse(req.body.status);
    if (isPropertiesMysql()) {
      const property = await updatePropertyMysql(req.params.id, { status });
      if (!property) throw new AppError(404, "Property not found");
      await logActivity({
        actor: req.user!,
        action: "property.status",
        entityType: "property",
        entityId: property.id,
        meta: { status },
      });
      await notifyUser({
        userId: property.ownerId,
        titleAr: "تحديث حالة العقار",
        titleEn: "Property status updated",
        messageAr: `أصبحت حالة العقار: ${status}`,
        messageEn: `Property status is now: ${status}`,
        link: `/dashboard/owner`,
      });
      res.json({ property: { id: property.id, status: property.status }, _db: "mysql" });
      return;
    }
    const property = await Property.findByIdAndUpdate(
      req.params.id,
      { $set: { status } },
      { new: true },
    ).lean();
    if (!property) throw new AppError(404, "Property not found");
    await logActivity({
      actor: req.user!,
      action: "property.status",
      entityType: "property",
      entityId: String(property._id),
      meta: { status },
    });
    await notifyUser({
      userId: property.ownerId,
      titleAr: "تحديث حالة العقار",
      titleEn: "Property status updated",
      messageAr: `أصبحت حالة العقار: ${status}`,
      messageEn: `Property status is now: ${status}`,
      link: `/dashboard/owner`,
    });
    res.json({ property: { id: property._id, status: property.status } });
  }),
);

adminRouter.get(
  "/reviews",
  asyncHandler(async (_req, res) => {
    if (isReviewsMysql()) {
      const reviews = await listReviewsMysql({ take: 100 });
      const propertyIds = [...new Set(reviews.map((r) => r.propertyId))];
      const authorIds = [...new Set(reviews.map((r) => r.authorId))];
      const [properties, authors] = await Promise.all([
        isPropertiesMysql()
          ? findPropertiesByIdsMysql(propertyIds)
          : Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr ownerId").lean(),
        isAuthMysql()
          ? findUsersByIdsMysql(authorIds)
          : User.find({ _id: { $in: authorIds } }).select("fullName email").lean(),
      ]);
      const propertyMap = new Map(
        (properties as any[]).map((p) => {
          const api = p.titleEn != null && p.id ? p : propertyToApi(p);
          return [
            String(api.id || api._id),
            {
              id: api.id || api._id,
              titleEn: api.titleEn,
              titleAr: api.titleAr,
              ownerId: api.ownerId,
            },
          ];
        }),
      );
      const authorMap = new Map(
        (authors as any[]).map((a) => [
          String(a.id || a._id),
          { id: a.id || a._id, fullName: a.fullName, email: a.email },
        ]),
      );
      res.json({
        reviews: reviews.map((r) => ({
          ...reviewToApi(r),
          property: propertyMap.get(r.propertyId) ?? null,
          author: authorMap.get(r.authorId) ?? null,
        })),
        _db: "mysql",
      });
      return;
    }

    const reviews = await Review.find().sort({ createdAt: -1 }).limit(100).lean();
    const propertyIds = [...new Set(reviews.map((r) => r.propertyId))];
    const authorIds = [...new Set(reviews.map((r) => r.authorId))];
    const [properties, authors] = await Promise.all([
      Property.find({ _id: { $in: propertyIds } }).select("titleEn titleAr ownerId").lean(),
      User.find({ _id: { $in: authorIds } }).select("fullName email").lean(),
    ]);
    const propertyMap = new Map(
      properties.map((p) => [
        String(p._id),
        { id: p._id, titleEn: p.titleEn, titleAr: p.titleAr, ownerId: p.ownerId },
      ]),
    );
    const authorMap = new Map(
      authors.map((a) => [String(a._id), { id: a._id, fullName: a.fullName, email: a.email }]),
    );

    res.json({
      reviews: reviews.map((r) => ({
        ...toPlain(r),
        property: propertyMap.get(r.propertyId) ?? null,
        author: authorMap.get(r.authorId) ?? null,
      })),
    });
  }),
);

adminRouter.delete(
  "/reviews/:id",
  asyncHandler(async (req, res) => {
    if (isReviewsMysql()) {
      const review = await findReviewByIdMysql(req.params.id);
      if (!review) throw new AppError(404, "Review not found");
      let ownerId: string | undefined;
      let titleEn = "";
      if (isPropertiesMysql()) {
        const property = await findPropertyByIdMysql(review.propertyId);
        ownerId = property?.ownerId;
        titleEn = property?.titleEn || "";
      } else {
        const property = await Property.findById(review.propertyId).select("ownerId titleEn").lean();
        ownerId = property?.ownerId;
        titleEn = property?.titleEn || "";
      }
      await deleteReviewMysql(review.id);
      await notifyUser({
        userId: review.authorId,
        titleAr: "تم حذف تقييمك",
        titleEn: "Your review was removed",
        messageAr: "أزال المشرف أحد تقييماتك",
        messageEn: "An admin removed one of your reviews",
        link: `/dashboard/customer`,
      });
      if (ownerId) {
        await notifyUser({
          userId: ownerId,
          titleAr: "تم حذف تقييم",
          titleEn: "A review was removed",
          messageAr: `حذف المشرف تقييماً على: ${titleEn}`,
          messageEn: `An admin removed a review on: ${titleEn}`,
          link: `/dashboard/owner#comments`,
        });
      }
      res.json({ ok: true, _db: "mysql" });
      return;
    }

    const review = await Review.findById(req.params.id);
    if (!review) throw new AppError(404, "Review not found");

    const property = await Property.findById(review.propertyId).select("ownerId titleEn").lean();
    await Review.deleteOne({ _id: review._id });

    await notifyUser({
      userId: review.authorId,
      titleAr: "تم حذف تقييمك",
      titleEn: "Your review was removed",
      messageAr: "أزال المشرف أحد تقييماتك",
      messageEn: "An admin removed one of your reviews",
      link: `/dashboard/customer`,
    });
    if (property?.ownerId) {
      await notifyUser({
        userId: property.ownerId,
        titleAr: "تم حذف تقييم",
        titleEn: "A review was removed",
        messageAr: `حذف المشرف تقييماً على: ${property.titleEn}`,
        messageEn: `An admin removed a review on: ${property.titleEn}`,
        link: `/dashboard/owner#comments`,
      });
    }

    res.json({ ok: true });
  }),
);

adminRouter.get(
  "/exchange-rates",
  asyncHandler(async (_req, res) => {
    if (isMysqlActive()) {
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT * FROM exchange_rates
         WHERE from_currency = 'TND' AND to_currency = 'LYD'
         ORDER BY valid_from DESC
         LIMIT 20`,
      );
      res.json({
        rates: rows.map((r) => ({
          id: String(r.id),
          fromCurrency: String(r.from_currency),
          toCurrency: String(r.to_currency),
          rate: toNumber(r.rate),
          validFrom: r.valid_from,
          validTo: r.valid_to,
          updatedById: r.updated_by_id,
          createdAt: r.created_at,
        })),
        _db: "mysql",
      });
      return;
    }

    const rates = await ExchangeRate.find({ fromCurrency: "TND", toCurrency: "LYD" })
      .sort({ validFrom: -1 })
      .limit(20)
      .lean();
    res.json({
      rates: rates.map((r) => ({ ...toPlain(r), rate: toNumber(r.rate) })),
    });
  }),
);

adminRouter.post(
  "/exchange-rates",
  asyncHandler(async (req, res) => {
    const rate = z.coerce.number().positive().parse(req.body.rate);
    if (isMysqlActive()) {
      const now = new Date();
      await sqlExecute(
        `UPDATE exchange_rates SET valid_to = ?
         WHERE from_currency = 'TND' AND to_currency = 'LYD' AND valid_to IS NULL`,
        [now],
      );
      const id = createId();
      await sqlExecute(
        `INSERT INTO exchange_rates
          (id, from_currency, to_currency, rate, valid_from, valid_to, updated_by_id, created_at)
         VALUES (?,?,?,?,?,NULL,?,?)`,
        [id, "TND", "LYD", rate, now, req.user!.id, now],
      );
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT * FROM exchange_rates WHERE id = ? LIMIT 1`,
        [id],
      );
      const r = rows[0];
      res.status(201).json({
        rate: {
          id: String(r.id),
          fromCurrency: String(r.from_currency),
          toCurrency: String(r.to_currency),
          rate: toNumber(r.rate),
          validFrom: r.valid_from,
          validTo: r.valid_to,
          updatedById: r.updated_by_id,
          createdAt: r.created_at,
        },
        _db: "mysql",
      });
      return;
    }

    await ExchangeRate.updateMany(
      {
        fromCurrency: "TND",
        toCurrency: "LYD",
        $or: [{ validTo: null }, { validTo: { $exists: false } }],
      },
      { $set: { validTo: new Date() } },
    );
    const created = await ExchangeRate.create({
      fromCurrency: "TND",
      toCurrency: "LYD",
      rate,
      updatedById: req.user!.id,
    });
    res.status(201).json({ rate: { ...toPlain(created.toObject()), rate: toNumber(created.rate) } });
  }),
);

adminRouter.get(
  "/contact-messages",
  asyncHandler(async (_req, res) => {
    if (isContactMysql()) {
      const messages = await listContactMysql(100);
      res.json({ messages: messages.map(contactToApi), _db: "mysql" });
      return;
    }
    const messages = await ContactMessage.find().sort({ createdAt: -1 }).limit(100).lean();
    res.json({ messages: messages.map(toPlain) });
  }),
);

adminRouter.patch(
  "/contact-messages/:id",
  asyncHandler(async (req, res) => {
    const body = z.object({ read: z.boolean() }).parse(req.body);
    if (isContactMysql()) {
      const message = await setContactReadMysql(req.params.id, body.read);
      if (!message) throw new AppError(404, "Message not found");
      res.json({ message: contactToApi(message), _db: "mysql" });
      return;
    }
    const message = await ContactMessage.findByIdAndUpdate(
      req.params.id,
      { $set: { readAt: body.read ? new Date() : null } },
      { new: true },
    ).lean();
    if (!message) throw new AppError(404, "Message not found");
    res.json({ message: toPlain(message) });
  }),
);

adminRouter.get(
  "/commerce-settings",
  asyncHandler(async (_req, res) => {
    const settings = await getCommerceSettings();
    res.json({ settings });
  }),
);

adminRouter.patch(
  "/commerce-settings",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        walletEnabled: z.boolean().optional(),
        loyaltyEnabled: z.boolean().optional(),
        couponsEnabled: z.boolean().optional(),
        pointsPerLyd: z.coerce.number().min(0).optional(),
        tndPerPoint: z.coerce.number().min(0).optional(),
        minRedeemPoints: z.coerce.number().int().min(0).optional(),
        maxRedeemPercentOfSubtotal: z.coerce.number().min(0).max(100).optional(),
        ownerRejectRefundPercent: z.coerce.number().min(0).max(100).optional(),
        refundTiers: z
          .array(
            z.object({
              hoursBeforeCheckIn: z.coerce.number().min(0),
              refundPercent: z.coerce.number().min(0).max(100),
            }),
          )
          .optional(),
      })
      .parse(req.body);
    const settings = await setCommerceSettings(body);
    res.json({ settings });
  }),
);

adminRouter.get(
  "/refund-policy",
  asyncHandler(async (_req, res) => {
    const settings = await getCommerceSettings();
    res.json({
      refundTiers: settings.refundTiers,
      ownerRejectRefundPercent: settings.ownerRejectRefundPercent,
      defaults: DEFAULT_COMMERCE.refundTiers,
    });
  }),
);

adminRouter.put(
  "/refund-policy",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        refundTiers: z
          .array(
            z.object({
              hoursBeforeCheckIn: z.coerce.number().min(0),
              refundPercent: z.coerce.number().min(0).max(100),
            }),
          )
          .min(1),
        ownerRejectRefundPercent: z.coerce.number().min(0).max(100).optional(),
      })
      .parse(req.body);
    const settings = await setCommerceSettings({
      refundTiers: body.refundTiers,
      ...(body.ownerRejectRefundPercent != null
        ? { ownerRejectRefundPercent: body.ownerRejectRefundPercent }
        : {}),
    });
    res.json({
      refundTiers: settings.refundTiers,
      ownerRejectRefundPercent: settings.ownerRejectRefundPercent,
    });
  }),
);

adminRouter.get(
  "/coupons",
  asyncHandler(async (_req, res) => {
    if (isMysqlActive()) {
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT * FROM coupons ORDER BY created_at DESC LIMIT 100`,
      );
      res.json({
        coupons: rows.map((r) => ({
          id: String(r.id),
          code: String(r.code),
          type: String(r.type),
          value: toNumber(r.value),
          maxUses: r.max_uses == null ? null : Number(r.max_uses),
          usedCount: Number(r.used_count || 0),
          minNights: r.min_nights == null ? null : Number(r.min_nights),
          minSubtotalTnd: r.min_subtotal_tnd == null ? null : toNumber(r.min_subtotal_tnd),
          validFrom: r.valid_from,
          validTo: r.valid_to,
          active: Boolean(r.active),
          note: r.note,
          createdAt: r.created_at,
          updatedAt: r.updated_at,
        })),
        _db: "mysql",
      });
      return;
    }
    const coupons = await Coupon.find().sort({ createdAt: -1 }).limit(100).lean();
    res.json({ coupons: coupons.map(toPlain) });
  }),
);

adminRouter.post(
  "/coupons",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        code: z.string().min(2).max(40),
        type: z.enum(["PERCENT", "FIXED_TND"]),
        value: z.coerce.number().positive(),
        maxUses: z.coerce.number().int().positive().optional().nullable(),
        minNights: z.coerce.number().int().positive().optional().nullable(),
        minSubtotalTnd: z.coerce.number().positive().optional().nullable(),
        validFrom: z.string().optional().nullable(),
        validTo: z.string().optional().nullable(),
        active: z.boolean().optional(),
        note: z.string().optional().nullable(),
      })
      .parse(req.body);
    if (body.type === "PERCENT" && body.value > 100) {
      throw new AppError(400, "Percent cannot exceed 100");
    }
    if (isMysqlActive()) {
      const id = createId();
      const now = new Date();
      const code = body.code.trim().toUpperCase();
      await sqlExecute(
        `INSERT INTO coupons
          (id, code, type, value, max_uses, used_count, min_nights, min_subtotal_tnd,
           valid_from, valid_to, active, note, created_at, updated_at)
         VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?,?)`,
        [
          id,
          code,
          body.type,
          body.value,
          body.maxUses ?? null,
          body.minNights ?? null,
          body.minSubtotalTnd ?? null,
          body.validFrom ? new Date(body.validFrom) : null,
          body.validTo ? new Date(body.validTo) : null,
          body.active ?? true ? 1 : 0,
          body.note ?? null,
          now,
          now,
        ],
      );
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT * FROM coupons WHERE id = ? LIMIT 1`,
        [id],
      );
      const r = rows[0];
      res.status(201).json({
        coupon: {
          id: String(r.id),
          code: String(r.code),
          type: String(r.type),
          value: toNumber(r.value),
          maxUses: r.max_uses == null ? null : Number(r.max_uses),
          usedCount: Number(r.used_count || 0),
          minNights: r.min_nights == null ? null : Number(r.min_nights),
          minSubtotalTnd: r.min_subtotal_tnd == null ? null : toNumber(r.min_subtotal_tnd),
          validFrom: r.valid_from,
          validTo: r.valid_to,
          active: Boolean(r.active),
          note: r.note,
          createdAt: r.created_at,
          updatedAt: r.updated_at,
        },
        _db: "mysql",
      });
      return;
    }
    const coupon = await Coupon.create({
      code: body.code.trim().toUpperCase(),
      type: body.type,
      value: body.value,
      maxUses: body.maxUses ?? undefined,
      minNights: body.minNights ?? undefined,
      minSubtotalTnd: body.minSubtotalTnd ?? undefined,
      validFrom: body.validFrom ? new Date(body.validFrom) : undefined,
      validTo: body.validTo ? new Date(body.validTo) : undefined,
      active: body.active ?? true,
      note: body.note ?? undefined,
    });
    res.status(201).json({ coupon: toPlain(coupon.toObject()) });
  }),
);

adminRouter.patch(
  "/coupons/:id",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        active: z.boolean().optional(),
        maxUses: z.coerce.number().int().positive().optional().nullable(),
        validTo: z.string().optional().nullable(),
        note: z.string().optional().nullable(),
      })
      .parse(req.body);
    if (isMysqlActive()) {
      const fields: string[] = [];
      const params: unknown[] = [];
      if (body.active != null) {
        fields.push("active = ?");
        params.push(body.active ? 1 : 0);
      }
      if (body.maxUses !== undefined) {
        fields.push("max_uses = ?");
        params.push(body.maxUses);
      }
      if (body.validTo !== undefined) {
        fields.push("valid_to = ?");
        params.push(body.validTo ? new Date(body.validTo) : null);
      }
      if (body.note !== undefined) {
        fields.push("note = ?");
        params.push(body.note);
      }
      if (fields.length === 0) {
        const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
          `SELECT * FROM coupons WHERE id = ? LIMIT 1`,
          [req.params.id],
        );
        if (!rows[0]) throw new AppError(404, "Coupon not found");
        const r = rows[0];
        res.json({
          coupon: {
            id: String(r.id),
            code: String(r.code),
            type: String(r.type),
            value: toNumber(r.value),
            maxUses: r.max_uses == null ? null : Number(r.max_uses),
            usedCount: Number(r.used_count || 0),
            active: Boolean(r.active),
            note: r.note,
            validTo: r.valid_to,
          },
        });
        return;
      }
      fields.push("updated_at = ?");
      params.push(new Date(), req.params.id);
      const result = await sqlExecute(
        `UPDATE coupons SET ${fields.join(", ")} WHERE id = ?`,
        params,
      );
      if (result.affectedRows === 0) throw new AppError(404, "Coupon not found");
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT * FROM coupons WHERE id = ? LIMIT 1`,
        [req.params.id],
      );
      const r = rows[0];
      res.json({
        coupon: {
          id: String(r.id),
          code: String(r.code),
          type: String(r.type),
          value: toNumber(r.value),
          maxUses: r.max_uses == null ? null : Number(r.max_uses),
          usedCount: Number(r.used_count || 0),
          active: Boolean(r.active),
          note: r.note,
          validTo: r.valid_to,
        },
        _db: "mysql",
      });
      return;
    }
    const coupon = await Coupon.findById(req.params.id);
    if (!coupon) throw new AppError(404, "Coupon not found");
    if (body.active != null) coupon.active = body.active;
    if (body.maxUses !== undefined) coupon.maxUses = body.maxUses ?? undefined;
    if (body.validTo !== undefined) {
      coupon.validTo = body.validTo ? new Date(body.validTo) : undefined;
    }
    if (body.note !== undefined) coupon.note = body.note ?? undefined;
    await coupon.save();
    res.json({ coupon: toPlain(coupon.toObject()) });
  }),
);

adminRouter.get(
  "/wallets",
  asyncHandler(async (_req, res) => {
    if (isWalletsMysql()) {
      const wallets = await listWalletsMysql({ take: 100 });
      const userIds = wallets.map((w) => w.userId);
      let userMap = new Map<string, any>();
      if (isAuthMysql()) {
        const users = await findUsersByIdsMysql(userIds);
        userMap = new Map(users.map((u) => [u.id, u]));
      } else {
        const users = await User.find({ _id: { $in: userIds } })
          .select("fullName email")
          .lean();
        userMap = new Map(users.map((u: any) => [u._id, u]));
      }
      res.json({
        wallets: wallets.map((w) => {
          const u = userMap.get(w.userId);
          return {
            ...walletToApi(w),
            balanceLyd: toNumber(w.balanceLyd),
            user: u
              ? {
                  id: u.id || u._id,
                  fullName: u.fullName,
                  email: u.email,
                }
              : null,
          };
        }),
        _db: "mysql",
      });
      return;
    }
    const wallets = await Wallet.find().sort({ updatedAt: -1 }).limit(100).lean();
    const userIds = wallets.map((w) => w.userId);
    const users = await User.find({ _id: { $in: userIds } })
      .select("fullName email")
      .lean();
    const userMap = new Map<string, any>(users.map((u: any) => [u._id, u]));
    res.json({
      wallets: wallets.map((w) => ({
        ...toPlain(w),
        balanceLyd: toNumber(w.balanceLyd),
        user: userMap.get(w.userId)
          ? {
              id: userMap.get(w.userId)._id,
              fullName: userMap.get(w.userId).fullName,
              email: userMap.get(w.userId).email,
            }
          : null,
      })),
      _db: "mongodb",
    });
  }),
);

adminRouter.get(
  "/wallet/topups",
  asyncHandler(async (req, res) => {
    const status = typeof req.query.status === "string" ? req.query.status : undefined;
    if (isWalletsMysql()) {
      const topups = await listWalletTopUpsMysql({ status, take: 100 });
      const userIds = [...new Set(topups.map((t) => t.userId))];
      let userMap = new Map<string, any>();
      if (isAuthMysql()) {
        const users = await findUsersByIdsMysql(userIds);
        userMap = new Map(users.map((u) => [u.id, u]));
      } else {
        const users = await User.find({ _id: { $in: userIds } })
          .select("fullName email")
          .lean();
        userMap = new Map(users.map((u: any) => [u._id, u]));
      }
      res.json({
        topups: topups.map((t) => {
          const u = userMap.get(t.userId);
          return {
            ...walletTopUpToApi(t),
            amountLyd: toNumber(t.amountLyd),
            user: u
              ? { id: u.id || u._id, fullName: u.fullName, email: u.email }
              : null,
          };
        }),
        _db: "mysql",
      });
      return;
    }
    const topups = await WalletTopUp.find(status ? { status } : {})
      .sort({ createdAt: -1 })
      .limit(100)
      .lean();
    const userIds = [...new Set(topups.map((t) => t.userId))];
    const users = await User.find({ _id: { $in: userIds } })
      .select("fullName email")
      .lean();
    const userMap = new Map<string, any>(users.map((u: any) => [u._id, u]));
    res.json({
      topups: topups.map((t) => ({
        ...toPlain(t),
        amountLyd: toNumber(t.amountLyd),
        user: userMap.get(t.userId)
          ? {
              id: userMap.get(t.userId)._id,
              fullName: userMap.get(t.userId).fullName,
              email: userMap.get(t.userId).email,
            }
          : null,
      })),
      _db: "mongodb",
    });
  }),
);

adminRouter.post(
  "/wallet/topups/:id/approve",
  asyncHandler(async (req, res) => {
    if (isWalletsMysql()) {
      const topUp = await findWalletTopUpByIdMysql(req.params.id);
      if (!topUp) throw new AppError(404, "Top-up not found");
      if (topUp.status !== "PENDING") throw new AppError(409, "Already reviewed");

      const reviewedAt = new Date();
      await upsertWalletTopUpMysql({
        id: topUp.id,
        userId: topUp.userId,
        amountLyd: topUp.amountLyd,
        bankName: topUp.bankName,
        reference: topUp.reference,
        status: "APPROVED",
        reviewedBy: req.user!.id,
        reviewedAt,
        reviewNote: topUp.reviewNote,
        createdAt: topUp.createdAt,
        updatedAt: reviewedAt,
      });

      await creditWallet({
        userId: topUp.userId,
        amountLyd: topUp.amountLyd,
        type: "TOPUP",
        topUpId: topUp.id,
        note: `Approved bank transfer — ${topUp.bankName}`,
      });

      await notifyUser({
        userId: topUp.userId,
        titleAr: "تم اعتماد شحن المحفظة",
        titleEn: "Wallet top-up approved",
        messageAr: `أُضيف ${topUp.amountLyd} د.ل إلى محفظتك`,
        messageEn: `${topUp.amountLyd} LYD added to your wallet`,
        link: `/dashboard/customer#wallet`,
      });

      await logActivity({
        actor: req.user!,
        action: "wallet.topup.approve",
        entityType: "wallet_topup",
        entityId: topUp.id,
        meta: { amountLyd: topUp.amountLyd },
      });

      const updated = await findWalletTopUpByIdMysql(topUp.id);
      const { balanceLyd } = await getWalletBalance(topUp.userId);
      return res.json({
        topUp: walletTopUpToApi(updated!),
        balanceLyd,
        _db: "mysql",
      });
    }

    const topUp = await WalletTopUp.findById(req.params.id);
    if (!topUp) throw new AppError(404, "Top-up not found");
    if (topUp.status !== "PENDING") throw new AppError(409, "Already reviewed");

    topUp.status = "APPROVED";
    topUp.reviewedBy = req.user!.id;
    topUp.reviewedAt = new Date();
    await topUp.save();

    try {
      if (isFinancialDualWriteEnabled()) {
        await upsertWalletTopUpMysql({
          id: String(topUp._id),
          userId: topUp.userId,
          amountLyd: toNumber(topUp.amountLyd),
          bankName: topUp.bankName,
          reference: topUp.reference,
          status: "APPROVED",
          reviewedBy: topUp.reviewedBy,
          reviewedAt: topUp.reviewedAt,
          reviewNote: topUp.reviewNote,
          createdAt: (topUp as any).createdAt,
          updatedAt: (topUp as any).updatedAt,
        });
      }
    } catch (e) {
      console.error("[dual-write-financial] FAIL topup.approve:", e);
      topUp.status = "PENDING";
      topUp.reviewedBy = undefined as any;
      topUp.reviewedAt = undefined as any;
      await topUp.save();
      throw e;
    }

    await creditWallet({
      userId: topUp.userId,
      amountLyd: topUp.amountLyd,
      type: "TOPUP",
      topUpId: String(topUp._id),
      note: `Approved bank transfer — ${topUp.bankName}`,
    });

    await notifyUser({
      userId: topUp.userId,
      titleAr: "تم اعتماد شحن المحفظة",
      titleEn: "Wallet top-up approved",
      messageAr: `أُضيف ${topUp.amountLyd} د.ل إلى محفظتك`,
      messageEn: `${topUp.amountLyd} LYD added to your wallet`,
      link: `/dashboard/customer#wallet`,
    });

    const { balanceLyd } = await getWalletBalance(topUp.userId);
    res.json({ topUp: toPlain(topUp.toObject()), balanceLyd });
  }),
);

adminRouter.post(
  "/wallet/topups/:id/reject",
  asyncHandler(async (req, res) => {
    const body = z.object({ note: z.string().optional() }).parse(req.body ?? {});
    if (isWalletsMysql()) {
      const topUp = await findWalletTopUpByIdMysql(req.params.id);
      if (!topUp) throw new AppError(404, "Top-up not found");
      if (topUp.status !== "PENDING") throw new AppError(409, "Already reviewed");

      const reviewedAt = new Date();
      await upsertWalletTopUpMysql({
        id: topUp.id,
        userId: topUp.userId,
        amountLyd: topUp.amountLyd,
        bankName: topUp.bankName,
        reference: topUp.reference,
        status: "REJECTED",
        reviewedBy: req.user!.id,
        reviewedAt,
        reviewNote: body.note ?? null,
        createdAt: topUp.createdAt,
        updatedAt: reviewedAt,
      });

      await notifyUser({
        userId: topUp.userId,
        titleAr: "رُفض طلب الشحن",
        titleEn: "Wallet top-up rejected",
        messageAr: body.note || "تم رفض طلب شحن المحفظة",
        messageEn: body.note || "Your wallet top-up was rejected",
        link: `/dashboard/customer#wallet`,
      });

      await logActivity({
        actor: req.user!,
        action: "wallet.topup.reject",
        entityType: "wallet_topup",
        entityId: topUp.id,
        meta: { note: body.note },
      });

      const updated = await findWalletTopUpByIdMysql(topUp.id);
      return res.json({ topUp: walletTopUpToApi(updated!), _db: "mysql" });
    }

    const topUp = await WalletTopUp.findById(req.params.id);
    if (!topUp) throw new AppError(404, "Top-up not found");
    if (topUp.status !== "PENDING") throw new AppError(409, "Already reviewed");

    topUp.status = "REJECTED";
    topUp.reviewedBy = req.user!.id;
    topUp.reviewedAt = new Date();
    topUp.reviewNote = body.note;
    await topUp.save();

    await notifyUser({
      userId: topUp.userId,
      titleAr: "رُفض طلب الشحن",
      titleEn: "Wallet top-up rejected",
      messageAr: body.note || "تم رفض طلب شحن المحفظة",
      messageEn: body.note || "Your wallet top-up was rejected",
      link: `/dashboard/customer#wallet`,
    });

    res.json({ topUp: toPlain(topUp.toObject()) });
  }),
);

adminRouter.post(
  "/wallet/:userId/adjust",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        amountLyd: z.coerce.number(),
        note: z.string().min(2).max(200),
      })
      .parse(req.body);
    if (body.amountLyd === 0) throw new AppError(400, "Amount required");

    const user = isAuthMysql()
      ? await findUsersByIdsMysql([req.params.userId]).then((r) => r[0])
      : await User.findById(req.params.userId).lean();
    if (!user || (user as any).deletedAt) throw new AppError(404, "User not found");
    const userId = String((user as any).id || (user as any)._id);

    await ensureWallet(userId);
    if (body.amountLyd > 0) {
      await creditWallet({
        userId,
        amountLyd: body.amountLyd,
        type: "ADMIN_ADJUST",
        note: body.note,
      });
    } else {
      await debitWallet({
        userId,
        amountLyd: Math.abs(body.amountLyd),
        type: "ADMIN_ADJUST",
        note: body.note,
      });
    }
    const { balanceLyd } = await getWalletBalance(userId);
    res.json({ balanceLyd, _db: isWalletsMysql() ? "mysql" : "mongodb" });
  }),
);

adminRouter.get(
  "/wallet/transactions",
  asyncHandler(async (req, res) => {
    const userId = typeof req.query.userId === "string" ? req.query.userId : undefined;
    if (isWalletsMysql()) {
      const txns = await listWalletTxnsMysql({ userId, take: 100 });
      res.json({
        transactions: txns.map((t) => ({
          ...walletTxnToApi(t),
          amountLyd: toNumber(t.amountLyd),
          balanceAfter: toNumber(t.balanceAfter),
        })),
        _db: "mysql",
      });
      return;
    }
    const txns = await WalletTxn.find(userId ? { userId } : {})
      .sort({ createdAt: -1 })
      .limit(100)
      .lean();
    res.json({
      transactions: txns.map((t) => ({
        ...toPlain(t),
        amountLyd: toNumber(t.amountLyd),
        balanceAfter: toNumber(t.balanceAfter),
      })),
      _db: "mongodb",
    });
  }),
);

adminRouter.post(
  "/loyalty/:userId/adjust",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        delta: z.coerce.number().int(),
        note: z.string().min(2).max(200),
      })
      .parse(req.body);
    await ensureLoyalty(req.params.userId);
    const { account } = await adjustLoyalty({
      userId: req.params.userId,
      delta: body.delta,
      type: "ADJUST",
      note: body.note,
    });
    res.json({ points: account.points });
  }),
);

adminRouter.get(
  "/withdrawals/pending-count",
  asyncHandler(async (_req, res) => {
    const count = isWithdrawalsMysql()
      ? await countWithdrawalsMysql({ status: "PENDING" })
      : await WithdrawalRequest.countDocuments({ status: "PENDING" });
    res.json({ count, _db: isWithdrawalsMysql() ? "mysql" : "mongodb" });
  }),
);

adminRouter.get(
  "/withdrawals",
  asyncHandler(async (req, res) => {
    const statusRaw = typeof req.query.status === "string" ? req.query.status : undefined;
    const status =
      statusRaw && (WITHDRAWAL_STATUSES as readonly string[]).includes(statusRaw)
        ? statusRaw
        : undefined;

    let rows: any[];
    let pendingCount: number;
    if (isWithdrawalsMysql()) {
      rows = (await listWithdrawalsMysql({ status, take: 200 })).map(withdrawalToApi);
      pendingCount = await countWithdrawalsMysql({ status: "PENDING" });
    } else {
      rows = await WithdrawalRequest.find(status ? { status } : {})
        .sort({ createdAt: -1 })
        .limit(200)
        .lean();
      pendingCount = await WithdrawalRequest.countDocuments({ status: "PENDING" });
    }

    const ownerIds = [...new Set(rows.map((r) => r.ownerId).filter(Boolean))] as string[];
    let ownerMap = new Map<string, { id: string; fullName?: string; email?: string }>();
    if (isAuthMysql()) {
      const owners = await findUsersByIdsMysql(ownerIds);
      ownerMap = new Map(
        owners.map((u) => [u.id, { id: u.id, fullName: u.fullName, email: u.email }]),
      );
    } else {
      const owners = await User.find({ _id: { $in: ownerIds } })
        .select("fullName email")
        .lean();
      ownerMap = new Map(
        owners.map((u: any) => [
          String(u._id),
          { id: String(u._id), fullName: u.fullName, email: u.email },
        ]),
      );
    }

    res.json({
      withdrawals: rows.map((r) => {
        const owner = ownerMap.get(String(r.ownerId));
        return {
          ...(isWithdrawalsMysql() ? r : toPlain(r)),
          amountTnd: toNumber(r.amountTnd),
          amountLyd: toNumber(r.amountLyd),
          exchangeRateRate: toNumber(r.exchangeRateRate),
          owner: owner
            ? { id: owner.id, fullName: owner.fullName, email: owner.email }
            : null,
        };
      }),
      pendingCount,
      _db: isWithdrawalsMysql() ? "mysql" : "mongodb",
    });
  }),
);

adminRouter.patch(
  "/withdrawals/:id",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        status: z.enum(["APPROVED", "REJECTED"]),
        rejectionReason: z.string().max(500).optional(),
      })
      .parse(req.body);

    if (isWithdrawalsMysql()) {
      const row = await findWithdrawalByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Withdrawal request not found");
      if (row.status !== "PENDING") throw new AppError(409, "Already reviewed");

      const reviewedAt = new Date();
      const rejectionReason =
        body.status === "REJECTED" ? body.rejectionReason?.trim() || null : null;
      await upsertWithdrawalMysql({
        id: row.id,
        ownerId: row.ownerId,
        amountTnd: row.amountTnd,
        amountLyd: row.amountLyd,
        exchangeRateRate: row.exchangeRateRate,
        method: row.method,
        status: body.status,
        note: row.note,
        rejectionReason,
        reviewedBy: req.user!.id,
        reviewedAt,
        createdAt: row.createdAt,
        updatedAt: reviewedAt,
      });

      const amountLyd = toNumber(row.amountLyd);
      const amountTnd = toNumber(row.amountTnd);

      await logActivity({
        actor: req.user!,
        action: body.status === "APPROVED" ? "withdrawal.approve" : "withdrawal.reject",
        entityType: "withdrawal",
        entityId: row.id,
        meta: { amountLyd, amountTnd, rejectionReason },
      });

      const owner = isAuthMysql()
        ? await findUsersByIdsMysql([row.ownerId])
            .then((rows) => rows[0])
        : await User.findById(row.ownerId).select("fullName email").lean();

      if (body.status === "APPROVED") {
        await postWithdrawalLedger({
          _id: row.id,
          ownerId: row.ownerId,
          amountLyd,
          amountTnd,
        });
        await notifyUser({
          userId: row.ownerId,
          titleAr: "تمت الموافقة على طلب السحب",
          titleEn: "Withdrawal approved",
          messageAr: `تمت الموافقة على سحب ${amountLyd} د.ل. سيتم التحويل قريبًا.`,
          messageEn: `Your withdrawal of ${amountLyd} LYD was approved. Transfer will follow soon.`,
          link: "/dashboard/owner#earnings",
        });
        if ((owner as any)?.email) {
          void sendMail({
            to: (owner as any).email,
            subject: "تمت الموافقة على طلب سحب الأرباح — سفر ليبيا",
            text: [
              `مرحباً ${(owner as any).fullName || ""}،`,
              `تمت الموافقة على طلب سحب أرباحك بمبلغ ${amountLyd} LYD (${amountTnd} TND).`,
              "سيتم تنفيذ التحويل حسب طريقة الاستلام التي اخترتها.",
              `${env.FRONTEND_URL}/dashboard/owner#earnings`,
            ].join("\n"),
            html: `<p>مرحباً ${(owner as any).fullName || ""}،</p>
<p>تمت الموافقة على طلب سحب أرباحك بمبلغ <strong>${amountLyd} LYD</strong> (${amountTnd} TND).</p>
<p>سيتم تنفيذ التحويل حسب طريقة الاستلام التي اخترتها.</p>
<p><a href="${env.FRONTEND_URL}/dashboard/owner#earnings">لوحة المضيف</a></p>`,
          }).catch(() => undefined);
        }
      } else {
        const reason = rejectionReason || "";
        await notifyUser({
          userId: row.ownerId,
          titleAr: "رُفض طلب السحب",
          titleEn: "Withdrawal rejected",
          messageAr: reason
            ? `رُفض طلب سحب ${amountLyd} د.ل. السبب: ${reason}`
            : `رُفض طلب سحب ${amountLyd} د.ل.`,
          messageEn: reason
            ? `Withdrawal of ${amountLyd} LYD was rejected. Reason: ${reason}`
            : `Withdrawal of ${amountLyd} LYD was rejected.`,
          link: "/dashboard/owner#earnings",
        });
        if ((owner as any)?.email) {
          void sendMail({
            to: (owner as any).email,
            subject: "رُفض طلب سحب الأرباح — سفر ليبيا",
            text: [
              `مرحباً ${(owner as any).fullName || ""}،`,
              `رُفض طلب سحب أرباحك بمبلغ ${amountLyd} LYD (${amountTnd} TND).`,
              reason ? `السبب: ${reason}` : "",
              `${env.FRONTEND_URL}/dashboard/owner#earnings`,
            ]
              .filter(Boolean)
              .join("\n"),
            html: `<p>مرحباً ${(owner as any).fullName || ""}،</p>
<p>رُفض طلب سحب أرباحك بمبلغ <strong>${amountLyd} LYD</strong> (${amountTnd} TND).</p>
${reason ? `<p>السبب: ${reason}</p>` : ""}
<p><a href="${env.FRONTEND_URL}/dashboard/owner#earnings">لوحة المضيف</a></p>`,
          }).catch(() => undefined);
        }
      }

      const updated = await findWithdrawalByIdMysql(row.id);
      return res.json({
        withdrawal: {
          ...withdrawalToApi(updated!),
          amountTnd,
          amountLyd,
        },
        _db: "mysql",
      });
    }

    const row = await WithdrawalRequest.findById(req.params.id);
    if (!row) throw new AppError(404, "Withdrawal request not found");
    if (row.status !== "PENDING") throw new AppError(409, "Already reviewed");

    const prev = {
      status: row.status,
      reviewedBy: row.reviewedBy,
      reviewedAt: row.reviewedAt,
      rejectionReason: row.rejectionReason,
    };

    await withFinancialDualWrite({
      site: "withdrawal.review",
      mongoWrite: async () => {
        row.status = body.status;
        row.reviewedBy = req.user!.id;
        row.reviewedAt = new Date();
        if (body.status === "REJECTED") {
          row.rejectionReason = body.rejectionReason?.trim() || undefined;
        }
        await row.save();
        return row;
      },
      mysqlWrite: async (doc) => {
        await upsertWithdrawalMysql({
          id: String(doc._id),
          ownerId: doc.ownerId,
          amountTnd: toNumber(doc.amountTnd),
          amountLyd: toNumber(doc.amountLyd),
          exchangeRateRate: toNumber(doc.exchangeRateRate),
          method: doc.method,
          status: doc.status,
          note: doc.note || null,
          rejectionReason: doc.rejectionReason || null,
          reviewedBy: doc.reviewedBy || null,
          reviewedAt: doc.reviewedAt || null,
          createdAt: (doc as any).createdAt,
          updatedAt: (doc as any).updatedAt,
        });
      },
      mongoCompensate: async (doc) => {
        doc.status = prev.status as any;
        doc.reviewedBy = prev.reviewedBy;
        doc.reviewedAt = prev.reviewedAt;
        doc.rejectionReason = prev.rejectionReason;
        await doc.save();
      },
    });

    const amountLyd = toNumber(row.amountLyd);
    const amountTnd = toNumber(row.amountTnd);

    await logActivity({
      actor: req.user!,
      action: body.status === "APPROVED" ? "withdrawal.approve" : "withdrawal.reject",
      entityType: "withdrawal",
      entityId: String(row._id),
      meta: {
        amountLyd,
        amountTnd,
        rejectionReason: row.rejectionReason,
      },
    });

    const owner = isAuthMysql()
      ? await findUsersByIdsMysql([row.ownerId])
          .then((rows) => rows[0])
      : await User.findById(row.ownerId).select("fullName email").lean();

    if (body.status === "APPROVED") {
      await postWithdrawalLedger({
        _id: String(row._id),
        ownerId: row.ownerId,
        amountLyd,
        amountTnd,
      });
      await notifyUser({
        userId: row.ownerId,
        titleAr: "تمت الموافقة على طلب السحب",
        titleEn: "Withdrawal approved",
        messageAr: `تمت الموافقة على سحب ${amountLyd} د.ل. سيتم التحويل قريبًا.`,
        messageEn: `Your withdrawal of ${amountLyd} LYD was approved. Transfer will follow soon.`,
        link: "/dashboard/owner#earnings",
      });
      if ((owner as any)?.email) {
        void sendMail({
          to: (owner as any).email,
          subject: "تمت الموافقة على طلب سحب الأرباح — سفر ليبيا",
          text: [
            `مرحباً ${(owner as any).fullName || ""}،`,
            `تمت الموافقة على طلب سحب أرباحك بمبلغ ${amountLyd} LYD (${amountTnd} TND).`,
            "سيتم تنفيذ التحويل حسب طريقة الاستلام التي اخترتها.",
            `${env.FRONTEND_URL}/dashboard/owner#earnings`,
          ].join("\n"),
          html: `<p>مرحباً ${(owner as any).fullName || ""}،</p>
<p>تمت الموافقة على طلب سحب أرباحك بمبلغ <strong>${amountLyd} LYD</strong> (${amountTnd} TND).</p>
<p>سيتم تنفيذ التحويل حسب طريقة الاستلام التي اخترتها.</p>
<p><a href="${env.FRONTEND_URL}/dashboard/owner#earnings">لوحة المضيف</a></p>`,
        }).catch(() => undefined);
      }
    } else {
      const reason = row.rejectionReason || "";
      await notifyUser({
        userId: row.ownerId,
        titleAr: "رُفض طلب السحب",
        titleEn: "Withdrawal rejected",
        messageAr: reason
          ? `رُفض طلب سحب ${amountLyd} د.ل. السبب: ${reason}`
          : `رُفض طلب سحب ${amountLyd} د.ل.`,
        messageEn: reason
          ? `Withdrawal of ${amountLyd} LYD was rejected. Reason: ${reason}`
          : `Withdrawal of ${amountLyd} LYD was rejected.`,
        link: "/dashboard/owner#earnings",
      });
      if ((owner as any)?.email) {
        void sendMail({
          to: (owner as any).email,
          subject: "رُفض طلب سحب الأرباح — سفر ليبيا",
          text: [
            `مرحباً ${(owner as any).fullName || ""}،`,
            `رُفض طلب سحب أرباحك بمبلغ ${amountLyd} LYD (${amountTnd} TND).`,
            reason ? `السبب: ${reason}` : "",
            `${env.FRONTEND_URL}/dashboard/owner#earnings`,
          ]
            .filter(Boolean)
            .join("\n"),
          html: `<p>مرحباً ${(owner as any).fullName || ""}،</p>
<p>رُفض طلب سحب أرباحك بمبلغ <strong>${amountLyd} LYD</strong> (${amountTnd} TND).</p>
${reason ? `<p>السبب: ${reason}</p>` : ""}
<p><a href="${env.FRONTEND_URL}/dashboard/owner#earnings">لوحة المضيف</a></p>`,
        }).catch(() => undefined);
      }
    }

    res.json({
      withdrawal: {
        ...toPlain(row.toObject()),
        amountTnd,
        amountLyd,
      },
      _db: "mongodb",
    });
  }),
);

adminRouter.get(
  "/refunds",
  asyncHandler(async (req, res) => {
    const bookingId =
      typeof req.query.bookingId === "string" ? req.query.bookingId : undefined;
    let rows: any[];
    if (isRefundsMysql()) {
      rows = (await listRefundsMysql({ bookingId, take: 200 })).map(refundToApi);
    } else {
      const filter: Record<string, unknown> = {};
      if (bookingId) filter.bookingId = bookingId;
      rows = await Refund.find(filter).sort({ createdAt: -1 }).limit(200).lean();
    }
    res.json({
      refunds: rows.map((r) => ({
        ...(isRefundsMysql() ? r : toPlain(r)),
        amountLyd: toNumber(r.amountLyd),
        amountTnd: toNumber(r.amountTnd),
        platformFeeClawbackTnd: toNumber(r.platformFeeClawbackTnd),
        ownerPayoutClawbackTnd: toNumber(r.ownerPayoutClawbackTnd),
        walletCreditedLyd: toNumber(r.walletCreditedLyd),
      })),
      _db: isRefundsMysql() ? "mysql" : "mongodb",
    });
  }),
);

adminRouter.get(
  "/reconciliation/summary",
  asyncHandler(async (_req, res) => {
    await backfillLedgerEntries();
    const summary = await getReconciliationSummary();
    const ledgerSumsByType = await sumLedgerByTypePosted();
    res.json({
      summary,
      ledgerSumsByType,
      _db: isLedgerMysql() ? "mysql" : "mongodb",
    });
  }),
);

adminRouter.get(
  "/reconciliation/ledger",
  asyncHandler(async (req, res) => {
    const typeRaw = typeof req.query.type === "string" ? req.query.type : undefined;
    const type =
      typeRaw && (LEDGER_ENTRY_TYPES as readonly string[]).includes(typeRaw) ? typeRaw : undefined;
    const from = typeof req.query.from === "string" ? new Date(req.query.from) : undefined;
    const to = typeof req.query.to === "string" ? new Date(req.query.to) : undefined;
    let toEnd: Date | undefined;
    if (to && !Number.isNaN(to.getTime())) {
      toEnd = new Date(to);
      toEnd.setHours(23, 59, 59, 999);
    }

    let rows: any[];
    if (isLedgerMysql()) {
      const mysqlRows = await listLedgerEntriesMysql({
        type,
        from: from && !Number.isNaN(from.getTime()) ? from : undefined,
        to: toEnd,
        take: 300,
      });
      rows = mysqlRows.map(ledgerEntryToApi);
    } else {
      const filter: Record<string, unknown> = {};
      if (type) filter.type = type;
      if (from || toEnd) {
        filter.createdAt = {};
        if (from && !Number.isNaN(from.getTime())) (filter.createdAt as any).$gte = from;
        if (toEnd) (filter.createdAt as any).$lte = toEnd;
      }
      rows = await LedgerEntry.find(filter).sort({ createdAt: -1 }).limit(300).lean();
    }

    const partyIds = [...new Set(rows.map((r) => r.partyUserId).filter(Boolean))] as string[];
    let userMap = new Map<string, { id: string; fullName?: string; email?: string }>();
    if (isAuthMysql()) {
      const users = await findUsersByIdsMysql(partyIds);
      userMap = new Map(
        users.map((u) => [u.id, { id: u.id, fullName: u.fullName, email: u.email }]),
      );
    } else {
      const users = await User.find({ _id: { $in: partyIds } })
        .select("fullName email")
        .lean();
      userMap = new Map(
        users.map((u: any) => [
          String(u._id),
          { id: String(u._id), fullName: u.fullName, email: u.email },
        ]),
      );
    }

    res.json({
      entries: rows.map((r) => {
        const party = r.partyUserId ? userMap.get(String(r.partyUserId)) : null;
        return {
          ...(isLedgerMysql() ? r : toPlain(r)),
          amountLyd: toNumber(r.amountLyd),
          amountTnd: toNumber(r.amountTnd),
          party: party
            ? { id: party.id, fullName: party.fullName, email: party.email }
            : null,
        };
      }),
      _db: isLedgerMysql() ? "mysql" : "mongodb",
    });
  }),
);

adminRouter.get(
  "/reconciliation/ledger/export",
  asyncHandler(async (req, res) => {
    const typeRaw = typeof req.query.type === "string" ? req.query.type : undefined;
    const type =
      typeRaw && (LEDGER_ENTRY_TYPES as readonly string[]).includes(typeRaw) ? typeRaw : undefined;
    const from = typeof req.query.from === "string" ? new Date(req.query.from) : undefined;
    const to = typeof req.query.to === "string" ? new Date(req.query.to) : undefined;
    let toEnd: Date | undefined;
    if (to && !Number.isNaN(to.getTime())) {
      toEnd = new Date(to);
      toEnd.setHours(23, 59, 59, 999);
    }

    let rows: any[];
    if (isLedgerMysql()) {
      const mysqlRows = await listLedgerEntriesMysql({
        type,
        from: from && !Number.isNaN(from.getTime()) ? from : undefined,
        to: toEnd,
        take: 5000,
      });
      rows = mysqlRows.map(ledgerEntryToApi);
    } else {
      const filter: Record<string, unknown> = {};
      if (type) filter.type = type;
      if (from || toEnd) {
        filter.createdAt = {};
        if (from && !Number.isNaN(from.getTime())) (filter.createdAt as any).$gte = from;
        if (toEnd) (filter.createdAt as any).$lte = toEnd;
      }
      rows = await LedgerEntry.find(filter).sort({ createdAt: -1 }).limit(5000).lean();
    }

    const summary = await getReconciliationSummary();
    const partyIds = [...new Set(rows.map((r) => r.partyUserId).filter(Boolean))] as string[];
    let userMap = new Map<string, any>();
    if (isAuthMysql()) {
      const users = await findUsersByIdsMysql(partyIds);
      userMap = new Map(users.map((u) => [u.id, u]));
    } else {
      const users = await User.find({ _id: { $in: partyIds } })
        .select("fullName email")
        .lean();
      userMap = new Map(users.map((u: any) => [String(u._id), u]));
    }

    const esc = (v: unknown) => {
      const s = String(v ?? "");
      if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
      return s;
    };

    const header = [
      "type",
      "direction",
      "amountLyd",
      "amountTnd",
      "partyName",
      "partyEmail",
      "status",
      "bookingId",
      "createdAt",
    ];
    const lines = [header.join(",")];
    for (const r of rows) {
      const party = r.partyUserId ? userMap.get(String(r.partyUserId)) : null;
      lines.push(
        [
          r.type,
          r.direction,
          toNumber(r.amountLyd),
          toNumber(r.amountTnd),
          party?.fullName || "",
          party?.email || "",
          r.status,
          r.bookingId || "",
          r.createdAt ? new Date(r.createdAt as any).toISOString() : "",
        ]
          .map(esc)
          .join(","),
      );
    }
    lines.push("");
    lines.push("# reconciliation summary");
    lines.push(`# revenueLyd,${summary.revenueLyd}`);
    lines.push(`# refundsLyd,${summary.refundsLyd}`);
    lines.push(`# approvedWithdrawalsLyd,${summary.approvedWithdrawalsLyd}`);
    lines.push(`# netBalanceLyd,${summary.netBalanceLyd}`);
    lines.push(`# platformFeeLyd,${summary.platformFeeLyd}`);
    lines.push(`# platformProfitLyd,${summary.platformProfitLyd}`);
    lines.push(`# ledgerDb,${isLedgerMysql() ? "mysql" : "mongodb"}`);

    const csv = lines.join("\n");
    res.setHeader("Content-Type", "text/csv; charset=utf-8");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename="safar-ledger-${new Date().toISOString().slice(0, 10)}.csv"`,
    );
    res.send("\uFEFF" + csv);
  }),
);

adminRouter.post(
  "/bookings/:id/refund",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        type: z.enum(["FULL", "PARTIAL"]),
        amountLyd: z.number().positive().optional(),
        reasonCode: z.enum([
          "GUEST_CANCEL",
          "HOST_CANCEL",
          "PROPERTY_ISSUE",
          "PAYMENT_ERROR",
          "ADMIN_ADJUSTMENT",
          "OTHER",
        ]),
        reasonNote: z.string().max(500).optional(),
      })
      .parse(req.body);

    if (body.type === "PARTIAL" && !(body.amountLyd && body.amountLyd > 0)) {
      throw new AppError(400, "Partial refund requires amountLyd");
    }

    if (isBookingsMysql()) {

      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Booking not found");

      const paymentStatus =
        row.paymentStatus ||
        (row.payment && typeof row.payment === "object"
          ? String((row.payment as any).status || "")
          : "");
      if (paymentStatus !== "PAID" && paymentStatus !== "PARTIALLY_REFUNDED") {
        throw new AppError(400, "Booking is not eligible for refund");
      }

      const totalLyd = toNumber(row.totalLyd);
      const already = toNumber(row.refundLyd);
      const remaining = Math.max(0, Math.round((totalLyd - already) * 100) / 100);
      if (remaining <= 0) throw new AppError(400, "Booking already fully refunded");

      const refundLyd =
        body.type === "FULL" ? remaining : Math.round(body.amountLyd! * 100) / 100;
      if (refundLyd > remaining + 0.001) {
        throw new AppError(400, "Refund exceeds remaining paid amount");
      }

      const rate = toNumber(row.exchangeRateRate) > 0 ? toNumber(row.exchangeRateRate) : 1;
      const refundTnd = Math.round((refundLyd / rate) * 100) / 100;
      const share = totalLyd > 0 ? refundLyd / totalLyd : 0;
      const platformFeeClawbackTnd =
        Math.round(toNumber(row.platformFeeTnd) * share * 100) / 100;
      const ownerPayoutClawbackTnd =
        Math.round(toNumber(row.ownerPayoutTnd) * share * 100) / 100;
      const newRefundTotal = Math.round((already + refundLyd) * 100) / 100;
      const refundPercent =
        totalLyd > 0 ? Math.round((newRefundTotal / totalLyd) * 10000) / 100 : 0;
      const nextPaymentStatus =
        newRefundTotal >= totalLyd - 0.001 ? "REFUNDED" : "PARTIALLY_REFUNDED";
      const nextStatus =
        body.type === "FULL" || newRefundTotal >= totalLyd - 0.001
          ? "CANCELLED"
          : row.status;

      const refundId = createId();
      const ledgerId = createId();
      const now = new Date();
      const payment = {
        ...(row.payment || {}),
        status: nextPaymentStatus,
        updatedAt: now,
        metadata: {
          ...((row.payment as any)?.metadata || {}),
          refundLyd: newRefundTotal,
          refundPercent,
          lastRefundId: refundId,
          lastRefundReason: body.reasonCode,
        },
      };

      await insertRefundMysql({
        id: refundId,
        bookingId: row.id,
        customerId: row.customerId,
        ownerId: row.ownerId,
        type: body.type,
        amountLyd: refundLyd,
        amountTnd: refundTnd,
        reasonCode: body.reasonCode,
        reasonNote: body.reasonNote,
        platformFeeClawbackTnd,
        ownerPayoutClawbackTnd,
        createdBy: req.user!.id,
        walletCreditedLyd: 0,
        source: "ADMIN",
        createdAt: now,
        updatedAt: now,
      });
      await insertLedgerEntryMysql({
        id: ledgerId,
        type: "REFUND",
        direction: "OUT",
        bookingId: row.id,
        refundId,
        amountLyd: refundLyd,
        amountTnd: refundTnd,
        partyUserId: row.customerId,
        partyRole: "CUSTOMER",
        status: "POSTED",
        meta: {
          platformFeeClawbackTnd,
          ownerPayoutClawbackTnd,
          reasonCode: body.reasonCode,
          source: "ADMIN",
        },
        createdAt: now,
        updatedAt: now,
      });

      let walletCreditedLyd = 0;
      const settings = await getCommerceSettings();
      if (settings.walletEnabled && refundLyd > 0) {
        const { txn } = await creditWallet({
          userId: row.customerId,
          amountLyd: refundLyd,
          type: "REFUND",
          bookingId: row.id,
          note: `Refund ${body.type} (${body.reasonCode})`,
          meta: { source: "ADMIN", reasonCode: body.reasonCode, refundId },
        });
        walletCreditedLyd = Math.abs(toNumber(txn.amountLyd));
        await insertRefundMysql({
          id: refundId,
          bookingId: row.id,
          customerId: row.customerId,
          ownerId: row.ownerId,
          type: body.type,
          amountLyd: refundLyd,
          amountTnd: refundTnd,
          reasonCode: body.reasonCode,
          reasonNote: body.reasonNote,
          platformFeeClawbackTnd,
          ownerPayoutClawbackTnd,
          createdBy: req.user!.id,
          walletCreditedLyd,
          source: "ADMIN",
          createdAt: now,
          updatedAt: new Date(),
        });
      }

      await upsertBookingMysql({
        id: row.id,
        propertyId: row.propertyId,
        customerId: row.customerId,
        ownerId: row.ownerId,
        checkIn: row.checkIn,
        checkOut: row.checkOut,
        guests: row.guests,
        nights: row.nights,
        status: nextStatus,
        exchangeFromCurrency: row.exchangeFromCurrency,
        exchangeToCurrency: row.exchangeToCurrency,
        exchangeRateRate: row.exchangeRateRate,
        exchangeRateLockedAt: row.exchangeRateLockedAt,
        exchangeRateExpiresAt: row.exchangeRateExpiresAt,
        subtotalTnd: row.subtotalTnd,
        cleaningFeeTnd: row.cleaningFeeTnd,
        platformFeeTnd: row.platformFeeTnd,
        taxesTnd: row.taxesTnd,
        discountTnd: row.discountTnd,
        totalTnd: row.totalTnd,
        totalLyd: row.totalLyd,
        ownerPayoutTnd: row.ownerPayoutTnd,
        couponCode: row.couponCode,
        walletPaidLyd: row.walletPaidLyd,
        pointsEarned: row.pointsEarned,
        pointsRedeemed: row.pointsRedeemed,
        pointsDiscountTnd: row.pointsDiscountTnd,
        refundLyd: newRefundTotal,
        refundPercent,
        cancelledBy: nextStatus === "CANCELLED" ? row.cancelledBy || "ADMIN" : row.cancelledBy,
        cancelledAt: nextStatus === "CANCELLED" ? row.cancelledAt || now : row.cancelledAt,
        payment,
        invoice: row.invoice,
        createdAt: row.createdAt,
        updatedAt: now,
      });

      await notifyUser({
        userId: row.customerId,
        titleAr: "تم استرداد مبلغ من حجزك",
        titleEn: "Booking refund processed",
        messageAr: `تم استرداد ${refundLyd} د.ل إلى محفظتك`,
        messageEn: `${refundLyd} LYD refunded to your wallet`,
        link: `/dashboard/customer?booking=${row.id}`,
      });

      await logActivity({
        actor: req.user!,
        action: "refund.process",
        entityType: "booking",
        entityId: row.id,
        meta: {
          refundId,
          type: body.type,
          amountLyd: refundLyd,
          reasonCode: body.reasonCode,
        },
      });

      return res.json({
        refund: {
          id: refundId,
          bookingId: row.id,
          type: body.type,
          amountLyd: refundLyd,
          amountTnd: refundTnd,
          reasonCode: body.reasonCode,
        },
        bookingStatus: nextStatus,
        paymentStatus: nextPaymentStatus,
        _db: "mysql",
      });
    }

    const result = await withDbTransaction(async (session) => {
      const query = Booking.findOne({ _id: req.params.id, deletedAt: null });
      if (session) query.session(session);
      const booking = await query;
      if (!booking) throw new AppError(404, "Booking not found");

      const paymentStatus = booking.payment?.status;
      if (paymentStatus !== "PAID" && paymentStatus !== "PARTIALLY_REFUNDED") {
        throw new AppError(400, "Booking is not eligible for refund");
      }
      const remaining = Math.max(0, toNumber(booking.totalLyd) - toNumber(booking.refundLyd));
      if (remaining <= 0) throw new AppError(400, "Booking already fully refunded");

      const amountLyd = body.type === "FULL" ? remaining : body.amountLyd!;

      return recordRefund({
        booking,
        type: body.type,
        amountLyd,
        reasonCode: body.reasonCode,
        reasonNote: body.reasonNote,
        createdBy: req.user!.id,
        source: "ADMIN",
        session,
      });
    });

    // Session path skips in-txn MySQL mirror — sync after commit; compensate Mongo on fail.
    try {
      if (isFinancialDualWriteEnabled()) {
        const booking = await Booking.findById(result.refund.bookingId);
        const ledger = await LedgerEntry.findOne({ refundId: String(result.refund._id) });
        await insertRefundMysql({
          id: String(result.refund._id),
          bookingId: result.refund.bookingId,
          customerId: result.refund.customerId,
          ownerId: result.refund.ownerId,
          type: result.refund.type,
          amountLyd: toNumber(result.refund.amountLyd),
          amountTnd: toNumber(result.refund.amountTnd),
          reasonCode: result.refund.reasonCode,
          reasonNote: result.refund.reasonNote,
          platformFeeClawbackTnd: toNumber(result.refund.platformFeeClawbackTnd),
          ownerPayoutClawbackTnd: toNumber(result.refund.ownerPayoutClawbackTnd),
          createdBy: result.refund.createdBy,
          walletCreditedLyd: toNumber(result.refund.walletCreditedLyd),
          source: result.refund.source,
          createdAt: (result.refund as any).createdAt,
          updatedAt: (result.refund as any).updatedAt,
        });
        if (ledger) {
          await insertLedgerEntryMysql({
            id: String(ledger._id),
            type: "REFUND",
            direction: "OUT",
            bookingId: result.refund.bookingId,
            refundId: String(result.refund._id),
            amountLyd: toNumber(ledger.amountLyd),
            amountTnd: toNumber(ledger.amountTnd),
            partyUserId: ledger.partyUserId,
            partyRole: ledger.partyRole,
            status: ledger.status,
            meta: ledger.meta as any,
            createdAt: (ledger as any).createdAt,
            updatedAt: (ledger as any).updatedAt,
          });
        }
        if (booking) await syncBookingToMysql(booking as any);
      }
    } catch (e) {
      console.error("[dual-write-financial] FAIL admin.refund.postCommit:", e);
      try {
        const credited = toNumber(result.refund.walletCreditedLyd);
        if (credited > 0) {
          await debitWallet({
            userId: result.refund.customerId,
            amountLyd: credited,
            type: "ADMIN_ADJUST",
            bookingId: result.refund.bookingId,
            note: `Compensate failed dual-write refund ${result.refund._id}`,
            meta: { compensate: true, refundId: String(result.refund._id) },
          });
        }
      } catch (ce) {
        console.error("[dual-write-financial] FAIL admin.refund.compensate.wallet:", ce);
      }
      try {
        await LedgerEntry.deleteOne({ refundId: String(result.refund._id) });
        await Refund.deleteOne({ _id: result.refund._id });
        const booking = await Booking.findById(result.refund.bookingId);
        if (booking) {
          const refundLyd = toNumber(result.refund.amountLyd);
          booking.refundLyd = Math.max(0, toNumber(booking.refundLyd) - refundLyd);
          const totalLyd = toNumber(booking.totalLyd);
          booking.refundPercent =
            totalLyd > 0 ? Math.round((toNumber(booking.refundLyd) / totalLyd) * 10000) / 100 : 0;
          if (booking.payment) {
            booking.payment.status =
              toNumber(booking.refundLyd) > 0.001 ? "PARTIALLY_REFUNDED" : "PAID";
          }
          await booking.save();
        }
      } catch (ce) {
        console.error("[dual-write-financial] FAIL admin.refund.compensate.mongo:", ce);
      }
      try {
        await deleteRefundMysql(String(result.refund._id));
        const rows = await sqlQuery<(import("mysql2").RowDataPacket & { id: string })[]>(
          `SELECT id FROM ledger_entries WHERE refund_id = ? LIMIT 5`,
          [String(result.refund._id)],
        );
        for (const r of rows) {
          await deleteLedgerEntryMysql(String(r.id));
        }
      } catch {
        /* ignore partial mysql cleanup */
      }
      throw e;
    }

    await notifyUser({
      userId: result.refund.customerId,
      titleAr: "تم استرداد مبلغ من حجزك",
      titleEn: "Booking refund processed",
      messageAr: `تم استرداد ${result.refundLyd} د.ل إلى محفظتك`,
      messageEn: `${result.refundLyd} LYD refunded to your wallet`,
      link: `/dashboard/customer?booking=${result.refund.bookingId}`,
    });

    await logActivity({
      actor: req.user!,
      action: "refund.process",
      entityType: "booking",
      entityId: result.refund.bookingId,
      meta: {
        refundId: String(result.refund._id),
        type: result.refund.type,
        amountLyd: result.refundLyd,
        reasonCode: result.refund.reasonCode,
      },
    });

    res.json({
      refund: {
        id: String(result.refund._id),
        bookingId: result.refund.bookingId,
        type: result.refund.type,
        amountLyd: result.refundLyd,
        amountTnd: result.refundTnd,
        reasonCode: result.refund.reasonCode,
      },
      bookingStatus: result.bookingStatus,
      paymentStatus: result.paymentStatus,
    });
  }),
);

adminRouter.get(
  "/me",
  asyncHandler(async (req, res) => {
    res.json({
      user: {
        id: req.user!.id,
        email: req.user!.email,
        fullName: req.user!.fullName,
        role: normalizeStaffRole(req.user!.role),
        status: req.user!.status,
      },
      permissions: permissionsForRole(req.user!.role),
    });
  }),
);

adminRouter.get(
  "/team",
  asyncHandler(async (_req, res) => {
    if (isAuthMysql()) {
      const staffRoles = [...STAFF_ROLE_VALUES, "ADMIN"];
      const ph = staffRoles.map(() => "?").join(",");
      const rows = await sqlQuery<import("mysql2/promise").RowDataPacket[]>(
        `SELECT id, email, full_name, role, status, created_at, email_verified_at
         FROM users
         WHERE deleted_at IS NULL AND role IN (${ph})
         ORDER BY created_at DESC`,
        staffRoles,
      );
      res.json({
        members: rows.map((m) => ({
          id: String(m.id),
          email: String(m.email),
          fullName: String(m.full_name),
          role: normalizeStaffRole(String(m.role)),
          status: String(m.status),
          createdAt: m.created_at,
          emailVerifiedAt: m.email_verified_at,
        })),
        _db: "mysql",
      });
      return;
    }

    const members = await User.find({
      deletedAt: null,
      role: { $in: [...STAFF_ROLE_VALUES, "ADMIN"] },
    })
      .select("email fullName role status createdAt emailVerifiedAt")
      .sort({ createdAt: -1 })
      .lean();
    res.json({
      members: members.map((m) => ({
        ...toPlain(m),
        role: normalizeStaffRole(m.role),
      })),
    });
  }),
);

adminRouter.post(
  "/team",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        email: z.string().email(),
        fullName: z.string().min(2).max(120).optional(),
        role: z.enum(["SUPER_ADMIN", "FINANCE_ADMIN", "OPERATIONS_ADMIN", "VIEWER"]),
      })
      .parse(req.body);

    const email = body.email.toLowerCase().trim();
    if (email === PRIMARY_SUPER_ADMIN_EMAIL && body.role !== "SUPER_ADMIN") {
      throw new AppError(400, "Primary admin must remain SUPER_ADMIN");
    }

    if (isAuthMysql()) {
      let user = await findUserByEmailMysql(email);
      if (user && isStaffRole(user.role) && !user.deletedAt) {
        throw new AppError(409, "Staff member already exists");
      }
      if (user && !isStaffRole(user.role)) {
        throw new AppError(409, "Email belongs to a marketplace user");
      }
      if (!user) {
        user = await createUserMysql({
          email,
          fullName: body.fullName || email.split("@")[0],
          role: body.role,
          status: "ACTIVE",
          locale: "ar",
          emailVerifiedAt: new Date(),
        });
      } else {
        user = (await updateUserMysql(user.id, {
          role: body.role,
          status: "ACTIVE",
          deletedAt: null,
          fullName: body.fullName || user.fullName,
          emailVerifiedAt: user.emailVerifiedAt ?? new Date(),
        }))!;
      }
      const raw = createRawToken();
      await createPasswordResetTokenMysql({
        userId: user.id,
        tokenHash: hashToken(raw),
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      });
      const link = `${env.FRONTEND_URL}/reset-password?token=${raw}`;
      void sendMail({
        to: email,
        subject: "دعوة للانضمام لفريق سفر ليبيا",
        text: [
          `مرحباً ${user.fullName}،`,
          `تمت دعوتك لفريق عمل سفر ليبيا بدور: ${body.role}`,
          `فعّل حسابك عبر تعيين كلمة مرور من الرابط (صالح 7 أيام):`,
          link,
          `ثم سجّل الدخول من: ${env.FRONTEND_URL}/admin/login`,
        ].join("\n"),
        html: `<p>مرحباً ${user.fullName}،</p>
<p>تمت دعوتك لفريق عمل سفر ليبيا بدور: <strong>${body.role}</strong></p>
<p><a href="${link}">تعيين كلمة المرور وتفعيل الحساب</a> (صالح 7 أيام)</p>
<p>بعدها سجّل الدخول من <a href="${env.FRONTEND_URL}/admin/login">لوحة المشرف</a>.</p>`,
      }).catch(() => undefined);

      await logActivity({
        actor: req.user!,
        action: "team.invite",
        entityType: "user",
        entityId: user.id,
        meta: { email, role: body.role },
      });

      res.status(201).json({
        member: {
          id: user.id,
          email: user.email,
          fullName: user.fullName,
          role: user.role,
          status: user.status,
          createdAt: user.createdAt,
        },
        inviteSent: true,
        _db: "mysql",
      });
      return;
    }

    let user = await User.findOne({ email });
    if (user && isStaffRole(user.role) && !user.deletedAt) {
      throw new AppError(409, "Staff member already exists");
    }
    if (user && !isStaffRole(user.role)) {
      throw new AppError(409, "Email belongs to a marketplace user");
    }

    if (!user) {
      user = await User.create({
        email,
        fullName: body.fullName || email.split("@")[0],
        role: body.role,
        status: "ACTIVE",
        locale: "ar",
        emailVerifiedAt: new Date(),
      });
    } else {
      user.role = body.role as any;
      user.status = "ACTIVE";
      user.deletedAt = undefined;
      user.fullName = body.fullName || user.fullName;
      user.emailVerifiedAt = user.emailVerifiedAt ?? new Date();
      await user.save();
    }

    const raw = createRawToken();
    await PasswordResetToken.create({
      userId: user._id,
      tokenHash: hashToken(raw),
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    });
    const link = `${env.FRONTEND_URL}/reset-password?token=${raw}`;
    void sendMail({
      to: email,
      subject: "دعوة للانضمام لفريق سفر ليبيا",
      text: [
        `مرحباً ${user.fullName}،`,
        `تمت دعوتك لفريق عمل سفر ليبيا بدور: ${body.role}`,
        `فعّل حسابك عبر تعيين كلمة مرور من الرابط (صالح 7 أيام):`,
        link,
        `ثم سجّل الدخول من: ${env.FRONTEND_URL}/admin/login`,
      ].join("\n"),
      html: `<p>مرحباً ${user.fullName}،</p>
<p>تمت دعوتك لفريق عمل سفر ليبيا بدور: <strong>${body.role}</strong></p>
<p><a href="${link}">تعيين كلمة المرور وتفعيل الحساب</a> (صالح 7 أيام)</p>
<p>بعدها سجّل الدخول من <a href="${env.FRONTEND_URL}/admin/login">لوحة المشرف</a>.</p>`,
    }).catch(() => undefined);

    await logActivity({
      actor: req.user!,
      action: "team.invite",
      entityType: "user",
      entityId: String(user._id),
      meta: { email, role: body.role },
    });

    res.status(201).json({
      member: {
        id: String(user._id),
        email: user.email,
        fullName: user.fullName,
        role: user.role,
        status: user.status,
        createdAt: (user as any).createdAt,
      },
      inviteSent: true,
    });
  }),
);

adminRouter.patch(
  "/team/:id",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        role: z.enum(["SUPER_ADMIN", "FINANCE_ADMIN", "OPERATIONS_ADMIN", "VIEWER"]).optional(),
        status: z.enum(["ACTIVE", "SUSPENDED"]).optional(),
        fullName: z.string().min(2).max(120).optional(),
      })
      .parse(req.body);

    if (isAuthMysql()) {
      const member = await findUserByIdMysql(req.params.id);
      if (!member || !isStaffRole(member.role)) {
        throw new AppError(404, "Staff member not found");
      }
      if (member.email === PRIMARY_SUPER_ADMIN_EMAIL) {
        if (body.role && body.role !== "SUPER_ADMIN") {
          throw new AppError(400, "Cannot change primary admin role");
        }
        if (body.status === "SUSPENDED") {
          throw new AppError(400, "Cannot suspend primary admin");
        }
      }
      if (member.id === req.user!.id && body.status === "SUSPENDED") {
        throw new AppError(400, "Cannot suspend yourself");
      }
      const updated = await updateUserMysql(member.id, {
        ...(body.role ? { role: body.role } : {}),
        ...(body.status ? { status: body.status } : {}),
        ...(body.fullName ? { fullName: body.fullName } : {}),
      });
      await logActivity({
        actor: req.user!,
        action: body.status === "SUSPENDED" ? "team.suspend" : "team.update",
        entityType: "user",
        entityId: member.id,
        meta: body,
      });
      res.json({
        member: {
          id: updated!.id,
          email: updated!.email,
          fullName: updated!.fullName,
          role: normalizeStaffRole(updated!.role),
          status: updated!.status,
        },
        _db: "mysql",
      });
      return;
    }

    const member = await User.findById(req.params.id);
    if (!member || !isStaffRole(member.role)) {
      throw new AppError(404, "Staff member not found");
    }
    if (member.email === PRIMARY_SUPER_ADMIN_EMAIL) {
      if (body.role && body.role !== "SUPER_ADMIN") {
        throw new AppError(400, "Cannot change primary admin role");
      }
      if (body.status === "SUSPENDED") {
        throw new AppError(400, "Cannot suspend primary admin");
      }
    }
    if (String(member._id) === req.user!.id && body.status === "SUSPENDED") {
      throw new AppError(400, "Cannot suspend yourself");
    }

    if (body.role) member.role = body.role as any;
    if (body.status) member.status = body.status;
    if (body.fullName) member.fullName = body.fullName;
    await member.save();

    await logActivity({
      actor: req.user!,
      action: body.status === "SUSPENDED" ? "team.suspend" : "team.update",
      entityType: "user",
      entityId: String(member._id),
      meta: body,
    });

    res.json({
      member: {
        id: String(member._id),
        email: member.email,
        fullName: member.fullName,
        role: normalizeStaffRole(member.role),
        status: member.status,
      },
    });
  }),
);

adminRouter.get(
  "/activity",
  asyncHandler(async (req, res) => {
    const actorId = typeof req.query.actorId === "string" ? req.query.actorId : undefined;
    const action = typeof req.query.action === "string" ? req.query.action : undefined;

    // FINANCE_ADMIN sees finance actions only (unless SUPER)
    const role = normalizeStaffRole(req.user!.role);
    if (role === "FINANCE_ADMIN") {
      if (action && !(FINANCE_ACTIVITY_ACTIONS as readonly string[]).includes(action)) {
        return res.json({ entries: [] });
      }
    }

    if (isMysqlActive()) {
      const actionFilter =
        role === "FINANCE_ADMIN"
          ? action
            ? action
            : [...FINANCE_ACTIVITY_ACTIONS]
          : action;
      const rows = await listActivityMysql({
        actorId,
        action: actionFilter,
        take: 300,
      });
      res.json({ entries: rows, _db: "mysql" });
      return;
    }

    const filter: Record<string, unknown> = {};
    if (actorId) filter.actorId = actorId;
    if (action) filter.action = action;
    if (role === "FINANCE_ADMIN") {
      filter.action = action ? action : { $in: [...FINANCE_ACTIVITY_ACTIONS] };
    }

    const rows = await ActivityLog.find(filter).sort({ createdAt: -1 }).limit(300).lean();
    res.json({
      entries: rows.map((r) => ({
        ...toPlain(r),
      })),
    });
  }),
);
