import { Router } from "express";
import { z } from "zod";
import { Booking, Property, City, User } from "@/db/models";
import { createId } from "@/db/ids";
import { env } from "@/config/env";
import { AppError, asyncHandler } from "@/lib/errors";
import { requireAuth, requireRoles, requireActiveAccount } from "@/middleware/auth";
import { calcQuoteTnd, convertTndToLyd } from "@/services/pricing";
import { getActiveExchangeRate } from "@/services/exchange";
import { notifyUser } from "@/services/notifications";
import { serializeBooking, toNumber } from "@/lib/serialize";
import {
  clearCustomerPendingQuotes,
  findBlockingBooking,
} from "@/services/availability";
import { sendMail, publicFrontendUrl } from "@/services/email";
import { buildInvoiceEmailHtml, buildInvoiceEmailText } from "@/services/emailTemplates";
import { resolveCouponDiscount, recordCouponRedemption } from "@/services/coupons";
import { pointsToDiscountTnd, earnPointsForPayment, adjustLoyalty } from "@/services/loyalty";
import { debitWallet, creditWallet, getWalletBalance } from "@/services/wallet";
import { applyRefundToWallet, previewCancelRefund } from "@/services/refunds";
import { getCommerceSettings } from "@/services/commerce";
import { buildInvoiceSnapshot } from "@/services/invoices";
import { postBookingPaymentLedger, syncBookingToMysql } from "@/services/ledger";
import { getPaymentGateway, getPaymentGatewayProviderId } from "@/services/payments";
import { isStaffRole } from "@/lib/auth/rbac";
import { isFinancialDualWriteEnabled } from "@/db/dualWriteFinancial";
import {
  isAuthMysql,
  isBookingsMysql,
  isCitiesMysql,
  isMysqlActive,
  isPropertiesMysql,
} from "@/db/activeDatabase";
import {
  bookingToApi,
  findBookingByIdMysql,
  listBookingsMysql,
} from "@/db/mysql/bookings";
import { findPropertiesByIdsMysql, findPropertyByIdMysql, propertyToApi } from "@/db/mysql/properties";
import { findCitiesByIdsMysql, cityRowToApi, findCityByIdMysql } from "@/db/mysql/cities";
import { findUsersByIdsMysql } from "@/db/mysql/users";
import { upsertBookingMysql } from "@/db/mysql/financialWrites";
import { withMysqlNamedLock } from "@/db/mysql/pool";

export const bookingsRouter = Router();

async function sendBookingInvoiceEmail(args: {
  bookingId: string;
  invoice: {
    guestName: string;
    invoiceNumber: string;
    issuedAt: Date | string;
    propertyTitleAr?: string;
    propertyTitleEn?: string;
    propertyAddress?: string | null;
    cityNameAr?: string | null;
    cityNameEn?: string | null;
    hostName?: string | null;
    checkIn: Date | string;
    checkOut: Date | string;
    nights: number;
    guests: number;
    subtotalTnd: number;
    cleaningFeeTnd: number;
    platformFeeTnd: number;
    taxesTnd: number;
    totalTnd: number;
    totalLyd: number;
    exchangeRateRate: number;
    paymentProvider?: string;
  };
  bookingStatus: string;
  customer: { email?: string | null; phone?: string | null } | null | undefined;
}) {
  const to = args.customer?.email?.trim();
  if (!to) {
    console.warn(`[invoice-email] skipped — no customer email for booking ${args.bookingId}`);
    return { sent: false as const, reason: "no_email" as const };
  }

  const inv = args.invoice;
  const invoiceParams = {
    guestName: inv.guestName,
    guestEmail: to,
    guestPhone: args.customer?.phone ?? null,
    invoiceNumber: inv.invoiceNumber,
    issuedAt: inv.issuedAt,
    propertyTitle: inv.propertyTitleAr || inv.propertyTitleEn || "",
    propertyAddress: inv.propertyAddress ?? null,
    cityName: inv.cityNameAr || inv.cityNameEn || null,
    hostName: inv.hostName ?? null,
    checkIn: inv.checkIn,
    checkOut: inv.checkOut,
    nights: inv.nights,
    guests: inv.guests,
    subtotalTnd: inv.subtotalTnd,
    cleaningFeeTnd: inv.cleaningFeeTnd,
    platformFeeTnd: inv.platformFeeTnd,
    taxesTnd: inv.taxesTnd,
    totalTnd: inv.totalTnd,
    totalLyd: inv.totalLyd,
    exchangeRate: inv.exchangeRateRate,
    bookingStatus: args.bookingStatus,
    paymentProvider: inv.paymentProvider,
    dashboardUrl: publicFrontendUrl()
      ? `${publicFrontendUrl()}/dashboard/customer?booking=${args.bookingId}`
      : undefined,
  };

  try {
    const mail = await sendMail({
      to,
      subject: `Safar Libya invoice ${inv.invoiceNumber}`,
      text: buildInvoiceEmailText(invoiceParams),
      html: buildInvoiceEmailHtml(invoiceParams),
    });
    console.log(
      `[invoice-email] booking=${args.bookingId} to=${to} delivered=${mail.delivered} mode=${mail.mode}`,
    );
    return { sent: mail.delivered, reason: mail.delivered ? ("ok" as const) : ("smtp_fallback" as const) };
  } catch (err) {
    console.error(`[invoice-email] booking=${args.bookingId} failed`, err);
    return { sent: false as const, reason: "error" as const };
  }
}

async function hydrateBookings(bookings: any[]) {
  const propertyIds = [...new Set(bookings.map((b) => b.propertyId))];
  const userIds = [
    ...new Set(bookings.flatMap((b) => [b.customerId, b.ownerId]).filter(Boolean)),
  ];

  let propertyMap = new Map<string, any>();
  let userMap = new Map<string, any>();

  if (isPropertiesMysql()) {
    const properties = await findPropertiesByIdsMysql(propertyIds);
    const cityIds = [...new Set(properties.map((p) => p.cityId))];
    const cityMap = new Map<string, any>();
    if (isCitiesMysql()) {
      for (const c of await findCitiesByIdsMysql(cityIds)) {
        cityMap.set(c.id, cityRowToApi(c));
      }
    } else {
      const cities = await City.find({ _id: { $in: cityIds } }).lean();
      for (const c of cities) cityMap.set(c._id, c);
    }
    propertyMap = new Map(
      properties.map((p) => {
        const api = propertyToApi(p);
        return [
          p.id,
          {
            ...api,
            city: cityMap.get(p.cityId),
            images: [...(api.images || [])].sort(
              (a: any, b: any) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0),
            ),
          },
        ];
      }),
    );
  } else {
    const properties = await Property.find({ _id: { $in: propertyIds } }).lean();
    const cityIds = [...new Set(properties.map((p) => p.cityId))];
    const cities = await City.find({ _id: { $in: cityIds } }).lean();
    const cityMap = new Map(cities.map((c) => [c._id, c]));
    propertyMap = new Map(
      properties.map((p) => [
        p._id,
        {
          ...p,
          city: cityMap.get(p.cityId),
          images: [...(p.images || [])].sort(
            (a: any, b: any) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0),
          ),
        },
      ]),
    );
  }

  if (isAuthMysql()) {
    const users = await findUsersByIdsMysql(userIds);
    userMap = new Map(
      users.map((u) => [
        u.id,
        { id: u.id, fullName: u.fullName, email: u.email, phone: u.phone ?? null },
      ]),
    );
  } else {
    const users = await User.find({ _id: { $in: userIds } })
      .select("fullName email phone")
      .lean();
    userMap = new Map(
      users.map((u) => [
        u._id,
        { id: u._id, fullName: u.fullName, email: u.email, phone: u.phone ?? null },
      ]),
    );
  }

  return bookings.map((b) => ({
    ...b,
    property: propertyMap.get(b.propertyId),
    customer: userMap.get(b.customerId),
    owner: userMap.get(b.ownerId),
  }));
}

bookingsRouter.get(
  "/",
  requireAuth,
  asyncHandler(async (req, res) => {
    const as = z.enum(["customer", "owner"]).default("customer").parse(req.query.as ?? "customer");
    const ownerMode = as === "owner" || req.user!.role === "OWNER";
    const ownerId =
      ownerMode && isStaffRole(req.user!.role) && req.query.ownerId
        ? String(req.query.ownerId)
        : req.user!.id;

    let bookings: any[];
    if (isBookingsMysql()) {
      const rows = await listBookingsMysql({
        ownerId: ownerMode ? ownerId : undefined,
        customerId: ownerMode ? undefined : req.user!.id,
        take: 50,
      });
      bookings = rows.map(bookingToApi);
    } else {
      const where = ownerMode
        ? { ownerId }
        : { customerId: req.user!.id };
      bookings = await Booking.find({ ...where, deletedAt: null })
        .sort({ createdAt: -1 })
        .limit(50)
        .lean();
    }

    const hydrated = await hydrateBookings(bookings);
    res.json({
      bookings: hydrated.map(serializeBooking),
      _db: isBookingsMysql() ? "mysql" : "mongodb",
    });
  }),
);

bookingsRouter.get(
  "/:id",
  requireAuth,
  asyncHandler(async (req, res) => {
    let booking: any;
    if (isBookingsMysql()) {
      const row = await findBookingByIdMysql(req.params.id);
      booking = row ? bookingToApi(row) : null;
    } else {
      booking = await Booking.findOne({ _id: req.params.id, deletedAt: null }).lean();
    }
    if (!booking) throw new AppError(404, "Booking not found");
    const allowed =
      isStaffRole(req.user!.role) ||
      booking.customerId === req.user!.id ||
      booking.ownerId === req.user!.id;
    if (!allowed) throw new AppError(403, "Forbidden");

    const [hydrated] = await hydrateBookings([booking]);
    res.json({
      booking: serializeBooking(hydrated),
      _db: isBookingsMysql() ? "mysql" : "mongodb",
    });
  }),
);

bookingsRouter.post(
  "/quote",
  requireAuth,
  requireRoles("CUSTOMER", "ADMIN"),
  requireActiveAccount(),
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        propertyId: z.string().min(1),
        checkIn: z.string(),
        checkOut: z.string(),
        guests: z.coerce.number().int().min(1).max(20),
        couponCode: z.string().optional(),
        redeemPoints: z.coerce.number().int().min(0).optional(),
      })
      .parse(req.body);

    const checkIn = new Date(body.checkIn);
    const checkOut = new Date(body.checkOut);
    if (!(checkOut > checkIn)) throw new AppError(400, "Invalid dates");
    const nights = Math.ceil((checkOut.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24));
    if (nights <= 0) throw new AppError(400, "Invalid nights");

    if (isBookingsMysql() && isPropertiesMysql()) {
      const property = await findPropertyByIdMysql(body.propertyId);
      if (!property || property.status !== "PUBLISHED") {
        throw new AppError(404, "Property not available");
      }
      if (body.guests > property.maxGuests) throw new AppError(400, "Too many guests");

      await clearCustomerPendingQuotes(req.user!.id, property.id);

      const overlap = await findBlockingBooking({
        propertyId: property.id,
        checkIn,
        checkOut,
        excludeCustomerId: req.user!.id,
      });
      if (overlap) {
        throw new AppError(409, "Selected dates unavailable", {
          code: "DATES_UNAVAILABLE",
          reason: (overlap as { reason?: string }).reason || "BOOKED",
        });
      }

      const exchange = await getActiveExchangeRate("TND", "LYD");
      const subtotalTnd = Math.round(toNumber(property.basePriceTnd) * nights * 100) / 100;

      const couponResult = await resolveCouponDiscount({
        code: body.couponCode,
        nights,
        subtotalTnd,
        userId: req.user!.id,
      });

      let pointsRedeemed = 0;
      let pointsDiscountTnd = 0;
      if (body.redeemPoints && body.redeemPoints > 0) {
        const pts = await pointsToDiscountTnd({
          userId: req.user!.id,
          pointsToRedeem: body.redeemPoints,
          subtotalTnd: Math.max(0, subtotalTnd - couponResult.discountTnd),
        });
        pointsRedeemed = pts.pointsRedeemed;
        pointsDiscountTnd = pts.discountTnd;
      }

      const discountTnd =
        Math.round((couponResult.discountTnd + pointsDiscountTnd) * 100) / 100;

      const quote = calcQuoteTnd({
        basePriceTnd: toNumber(property.basePriceTnd),
        nights,
        cleaningFeeTnd: toNumber(property.cleaningFeeTnd),
        platformFeeRateTnd: Number(env.PLATFORM_FEE_RATE_TND),
        taxesRateTnd: Number(env.TAXES_RATE_TND),
        discountTnd,
      });
      const totalLyd = convertTndToLyd(quote.totalTnd, toNumber(exchange.rate));
      const lockedAt = new Date();
      const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
      const bookingId = createId();
      const payment = {
        _id: createId(),
        provider: getPaymentGatewayProviderId(),
        status: "PENDING",
        amount: totalLyd,
        currency: "LYD",
        metadata: { note: `Pending via ${getPaymentGatewayProviderId()}` },
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      await upsertBookingMysql({
        id: bookingId,
        propertyId: property.id,
        customerId: req.user!.id,
        ownerId: property.ownerId,
        checkIn,
        checkOut,
        guests: body.guests,
        nights,
        status: "PENDING_PAYMENT",
        exchangeRateRate: toNumber(exchange.rate),
        exchangeRateLockedAt: lockedAt,
        exchangeRateExpiresAt: expiresAt,
        subtotalTnd: quote.subtotalTnd,
        cleaningFeeTnd: quote.cleaningFeeTnd,
        platformFeeTnd: quote.platformFeeTnd,
        taxesTnd: quote.taxesTnd,
        discountTnd: quote.discountTnd,
        totalTnd: quote.totalTnd,
        totalLyd,
        ownerPayoutTnd: quote.ownerPayoutTnd,
        couponCode: couponResult.code,
        pointsRedeemed,
        pointsDiscountTnd,
        payment,
        createdAt: lockedAt,
        updatedAt: lockedAt,
      });

      await notifyUser({
        userId: property.ownerId,
        titleAr: "طلب حجز جديد",
        titleEn: "New booking quote",
        messageAr: `تم إنشاء عرض سعر لعقارك`,
        messageEn: `A booking quote was created for your property`,
        link: `/dashboard/owner?booking=${bookingId}`,
      });

      const city = isCitiesMysql()
        ? await findCityByIdMysql(property.cityId)
        : await City.findById(property.cityId).lean();
      const row = await findBookingByIdMysql(bookingId);
      res.status(201).json({
        booking: serializeBooking({
          ...bookingToApi(row!),
          property: {
            ...propertyToApi(property),
            city: city ? (isCitiesMysql() ? cityRowToApi(city as any) : city) : undefined,
          },
        }),
        _db: "mysql",
      });
      return;
    }

    const property = await Property.findOne({
      _id: body.propertyId,
      status: "PUBLISHED",
      deletedAt: null,
    }).lean();
    if (!property) throw new AppError(404, "Property not available");
    if (body.guests > property.maxGuests) throw new AppError(400, "Too many guests");

    // Allow the same customer to create a fresh quote (previous abandoned quotes expire).
    await clearCustomerPendingQuotes(req.user!.id, property._id);

    const overlap = await findBlockingBooking({
      propertyId: property._id,
      checkIn,
      checkOut,
      excludeCustomerId: req.user!.id,
    });
    if (overlap) {
      throw new AppError(409, "Selected dates unavailable", {
        code: "DATES_UNAVAILABLE",
        reason: (overlap as { reason?: string }).reason || "BOOKED",
      });
    }

    const exchange = await getActiveExchangeRate("TND", "LYD");
    const subtotalTnd = Math.round(toNumber(property.basePriceTnd) * nights * 100) / 100;

    const couponResult = await resolveCouponDiscount({
      code: body.couponCode,
      nights,
      subtotalTnd,
      userId: req.user!.id,
    });

    let pointsRedeemed = 0;
    let pointsDiscountTnd = 0;
    if (body.redeemPoints && body.redeemPoints > 0) {
      const pts = await pointsToDiscountTnd({
        userId: req.user!.id,
        pointsToRedeem: body.redeemPoints,
        subtotalTnd: Math.max(0, subtotalTnd - couponResult.discountTnd),
      });
      pointsRedeemed = pts.pointsRedeemed;
      pointsDiscountTnd = pts.discountTnd;
    }

    const discountTnd =
      Math.round((couponResult.discountTnd + pointsDiscountTnd) * 100) / 100;

    const quote = calcQuoteTnd({
      basePriceTnd: toNumber(property.basePriceTnd),
      nights,
      cleaningFeeTnd: toNumber(property.cleaningFeeTnd),
      platformFeeRateTnd: Number(env.PLATFORM_FEE_RATE_TND),
      taxesRateTnd: Number(env.TAXES_RATE_TND),
      discountTnd,
    });
    const totalLyd = convertTndToLyd(quote.totalTnd, toNumber(exchange.rate));
    const lockedAt = new Date();
    const expiresAt = new Date(Date.now() + 15 * 60 * 1000);

    const booking = await Booking.create({
      propertyId: property._id,
      customerId: req.user!.id,
      ownerId: property.ownerId,
      checkIn,
      checkOut,
      guests: body.guests,
      nights,
      status: "PENDING_PAYMENT",
      exchangeRateRate: exchange.rate,
      exchangeRateLockedAt: lockedAt,
      exchangeRateExpiresAt: expiresAt,
      subtotalTnd: quote.subtotalTnd,
      cleaningFeeTnd: quote.cleaningFeeTnd,
      platformFeeTnd: quote.platformFeeTnd,
      taxesTnd: quote.taxesTnd,
      discountTnd: quote.discountTnd,
      totalTnd: quote.totalTnd,
      totalLyd,
      ownerPayoutTnd: quote.ownerPayoutTnd,
      couponCode: couponResult.code,
      pointsRedeemed,
      pointsDiscountTnd,
      payment: {
        _id: createId(),
        provider: getPaymentGatewayProviderId(),
        status: "PENDING",
        amount: totalLyd,
        currency: "LYD",
        metadata: { note: `Pending via ${getPaymentGatewayProviderId()}` },
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    });

    if (isFinancialDualWriteEnabled()) {
      await syncBookingToMysql(booking as any);
    }

    if (couponResult.coupon && couponResult.code && couponResult.discountTnd > 0) {
      // Redemption recorded on successful payment.
    }

    await notifyUser({
      userId: property.ownerId,
      titleAr: "طلب حجز جديد",
      titleEn: "New booking quote",
      messageAr: `تم إنشاء عرض سعر لعقارك`,
      messageEn: `A booking quote was created for your property`,
      link: `/dashboard/owner?booking=${booking._id}`,
    });

    const city = await City.findById(property.cityId).lean();
    res.status(201).json({
      booking: serializeBooking({
        ...booking.toObject(),
        property: { ...property, city },
      }),
    });
  }),
);

bookingsRouter.post(
  "/:id/refresh",
  requireAuth,
  requireRoles("CUSTOMER", "ADMIN"),
  requireActiveAccount(),
  asyncHandler(async (req, res) => {
    if (isBookingsMysql()) {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row || row.customerId !== req.user!.id) throw new AppError(404, "Not found");
      if (row.status !== "PENDING_PAYMENT") throw new AppError(409, "Cannot refresh");

      if (row.exchangeRateExpiresAt.getTime() > Date.now()) {
        const [hydrated] = await hydrateBookings([bookingToApi(row)]);
        return res.json({
          booking: serializeBooking(hydrated),
          note: "Quote not expired",
          _db: "mysql",
        });
      }

      const exchange = await getActiveExchangeRate("TND", "LYD");
      const totalLyd = convertTndToLyd(toNumber(row.totalTnd), toNumber(exchange.rate));
      const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
      const lockedAt = new Date();
      const payment = row.payment
        ? {
            ...(row.payment as Record<string, unknown>),
            amount: totalLyd,
            updatedAt: new Date(),
          }
        : row.payment;

      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: row.status,
        exchangeFromCurrency: row.exchangeFromCurrency,
        exchangeToCurrency: row.exchangeToCurrency,
        exchangeRateRate: toNumber(exchange.rate),
        exchangeRateLockedAt: lockedAt,
        exchangeRateExpiresAt: expiresAt,
        subtotalTnd: row.subtotalTnd,
        cleaningFeeTnd: row.cleaningFeeTnd,
        platformFeeTnd: row.platformFeeTnd,
        taxesTnd: row.taxesTnd,
        discountTnd: row.discountTnd,
        totalTnd: row.totalTnd,
        totalLyd,
        ownerPayoutTnd: row.ownerPayoutTnd,
        couponCode: row.couponCode,
        walletPaidLyd: row.walletPaidLyd,
        pointsEarned: row.pointsEarned,
        pointsRedeemed: row.pointsRedeemed,
        pointsDiscountTnd: row.pointsDiscountTnd,
        refundLyd: row.refundLyd,
        refundPercent: row.refundPercent,
        cancelledBy: row.cancelledBy,
        cancelledAt: row.cancelledAt,
        payment: payment as Record<string, unknown> | null,
        invoice: row.invoice,
        createdAt: row.createdAt,
        updatedAt: new Date(),
      });

      const updated = await findBookingByIdMysql(row.id);
      const [hydrated] = await hydrateBookings([bookingToApi(updated!)]);
      return res.json({ booking: serializeBooking(hydrated), _db: "mysql" });
    }

    const booking = await Booking.findOne({
      _id: req.params.id,
      customerId: req.user!.id,
      deletedAt: null,
    });
    if (!booking) throw new AppError(404, "Not found");
    if (booking.status !== "PENDING_PAYMENT") throw new AppError(409, "Cannot refresh");

    if (booking.exchangeRateExpiresAt.getTime() > Date.now()) {
      const [hydrated] = await hydrateBookings([booking.toObject()]);
      return res.json({
        booking: serializeBooking(hydrated),
        note: "Quote not expired",
      });
    }

    const exchange = await getActiveExchangeRate("TND", "LYD");
    const totalLyd = convertTndToLyd(toNumber(booking.totalTnd), toNumber(exchange.rate));
    const expiresAt = new Date(Date.now() + 15 * 60 * 1000);

    booking.exchangeRateRate = exchange.rate;
    booking.exchangeRateLockedAt = new Date();
    booking.exchangeRateExpiresAt = expiresAt;
    booking.totalLyd = totalLyd;
    if (booking.payment) {
      booking.payment.amount = totalLyd;
      booking.payment.updatedAt = new Date();
    }
    await booking.save();

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    res.json({ booking: serializeBooking(hydrated) });
  }),
);

bookingsRouter.post(
  "/:id/pay",
  requireAuth,
  requireRoles("CUSTOMER", "ADMIN"),
  requireActiveAccount(),
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        useWalletLyd: z.coerce.number().min(0).optional(),
      })
      .parse(req.body ?? {});

    if (isBookingsMysql() && isPropertiesMysql()) {
      return withMysqlNamedLock(`booking:${req.params.id}`, async () => {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row || row.customerId !== req.user!.id) throw new AppError(404, "Not found");
      if (row.status !== "PENDING_PAYMENT") throw new AppError(409, "Booking not payable");
      if (row.exchangeRateExpiresAt.getTime() < Date.now()) {
        throw new AppError(409, "Quote expired — refresh first");
      }

      const property = await findPropertyByIdMysql(row.propertyId);
      if (!property) throw new AppError(404, "Property not found");

      const settings = await getCommerceSettings();
      const totalLyd = toNumber(row.totalLyd);
      let walletPaidLyd = 0;

      if (settings.walletEnabled && body.useWalletLyd && body.useWalletLyd > 0) {
        const { balanceLyd } = await getWalletBalance(req.user!.id);
        walletPaidLyd = Math.min(body.useWalletLyd, balanceLyd, totalLyd);
        walletPaidLyd = Math.round(walletPaidLyd * 100) / 100;
      }

      const gatewayPaidLyd = Math.round((totalLyd - walletPaidLyd) * 100) / 100;
      let gatewayProvider: string = walletPaidLyd >= totalLyd ? "WALLET" : getPaymentGatewayProviderId();
      let providerRef = walletPaidLyd >= totalLyd ? `wallet_${row.id}` : "";

      if (gatewayPaidLyd > 0) {
        const gateway = getPaymentGateway();
        const charged = await gateway.charge({
          bookingId: row.id,
          userId: req.user!.id,
          amountLyd: gatewayPaidLyd,
          currency: "LYD",
          metadata: { useWalletLyd: walletPaidLyd },
        });
        if (!charged.ok) {
          throw new AppError(502, charged.error, {
            code: "PAYMENT_GATEWAY_FAILED",
            provider: charged.provider,
          });
        }
        gatewayProvider = charged.provider;
        providerRef = charged.providerRef;
      }

      if (walletPaidLyd > 0) {
        await debitWallet({
          userId: req.user!.id,
          amountLyd: walletPaidLyd,
          type: "BOOKING_PAY",
          bookingId: row.id,
          note: "Booking payment from wallet",
        });
      }

      if (toNumber(row.pointsRedeemed) > 0) {
        await adjustLoyalty({
          userId: req.user!.id,
          delta: -toNumber(row.pointsRedeemed),
          type: "REDEEM",
          bookingId: row.id,
          note: "Redeemed on payment",
        });
      }

      // Record coupon redemption on MySQL (or dual-write legacy path).
      if (row.couponCode && (isMysqlActive() || isFinancialDualWriteEnabled())) {
        const couponResult = await resolveCouponDiscount({
          code: row.couponCode,
          nights: row.nights,
          subtotalTnd: toNumber(row.subtotalTnd),
          userId: req.user!.id,
        });
        if (couponResult.coupon && couponResult.code) {
          await recordCouponRedemption({
            couponId: String(couponResult.coupon.id || couponResult.coupon._id),
            code: couponResult.code,
            userId: req.user!.id,
            bookingId: row.id,
            discountTnd:
              toNumber(row.pointsDiscountTnd) > 0
                ? Math.max(0, toNumber(row.discountTnd) - toNumber(row.pointsDiscountTnd))
                : toNumber(row.discountTnd),
          });
        }
      }

      const earned = await earnPointsForPayment(req.user!.id, totalLyd, row.id);
      const nextStatus = property.instantBooking ? "CONFIRMED" : "WAITING_OWNER";
      const payment = {
        ...(row.payment || {}),
        status: "PAID",
        provider: walletPaidLyd >= totalLyd ? "WALLET" : gatewayProvider,
        providerRef,
        paidAt: new Date(),
        updatedAt: new Date(),
        amount: totalLyd,
        metadata: {
          ...((row.payment?.metadata as Record<string, unknown>) || {}),
          walletPaidLyd,
          gatewayPaidLyd,
          demoPaidLyd: gatewayPaidLyd,
        },
      };

      const [customerDoc] = await findUsersByIdsMysql([row.customerId]);
      const [ownerDoc] = await findUsersByIdsMysql([row.ownerId]);
      const cityDoc = isCitiesMysql()
        ? await findCityByIdMysql(property.cityId)
        : await City.findById(property.cityId).lean();

      const bookingForInvoice = {
        ...bookingToApi(row),
        status: nextStatus,
        walletPaidLyd,
        pointsEarned: earned.points,
        payment,
      };
      const invoice = await buildInvoiceSnapshot({
        booking: bookingForInvoice as any,
        bookingStatus: nextStatus,
        customer: customerDoc
          ? {
              fullName: customerDoc.fullName,
              email: customerDoc.email,
              phone: customerDoc.phone ?? null,
            }
          : null,
        owner: ownerDoc ? { fullName: ownerDoc.fullName } : null,
        property: {
          titleAr: property.titleAr,
          titleEn: property.titleEn,
          address: property.address,
          city: cityDoc
            ? {
                nameAr: (cityDoc as any).nameAr ?? (cityDoc as any).name_ar,
                nameEn: (cityDoc as any).nameEn ?? (cityDoc as any).name_en,
              }
            : null,
        },
      });

      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,
        pointsEarned: earned.points,
        pointsRedeemed: row.pointsRedeemed,
        pointsDiscountTnd: row.pointsDiscountTnd,
        payment,
        invoice: invoice as any,
        createdAt: row.createdAt,
        updatedAt: new Date(),
      });

      await postBookingPaymentLedger({
        _id: row.id,
        customerId: row.customerId,
        totalLyd: toNumber(row.totalLyd),
        totalTnd: toNumber(row.totalTnd),
        platformFeeTnd: toNumber(row.platformFeeTnd),
        ownerPayoutTnd: toNumber(row.ownerPayoutTnd),
        exchangeRateRate: toNumber(row.exchangeRateRate),
      }).catch((err) => console.error("[ledger] booking payment post failed", err));

      await notifyUser({
        userId: row.ownerId,
        titleAr: nextStatus === "CONFIRMED" ? "حجز مؤكد" : "بانتظار موافقتك",
        titleEn: nextStatus === "CONFIRMED" ? "Booking confirmed" : "Awaiting your approval",
        messageAr: "تم دفع الحجز بنجاح",
        messageEn: "Booking payment completed successfully",
        link: `/dashboard/owner?booking=${row.id}`,
      });

      const updated = await findBookingByIdMysql(row.id);
      const [hydrated] = await hydrateBookings([bookingToApi(updated!)]);
      const serialized = serializeBooking(hydrated);

      const emailResult = await sendBookingInvoiceEmail({
        bookingId: row.id,
        invoice,
        bookingStatus: nextStatus,
        customer: customerDoc
          ? { email: customerDoc.email, phone: customerDoc.phone ?? null }
          : null,
      });

      return res.json({
        booking: serialized,
        invoiceEmailSent: emailResult.sent,
        invoiceEmailTo: customerDoc?.email || null,
        _db: "mysql",
      });
      });
    }

    const booking = await Booking.findOne({
      _id: req.params.id,
      customerId: req.user!.id,
      deletedAt: null,
    });
    if (!booking) throw new AppError(404, "Not found");
    if (booking.status !== "PENDING_PAYMENT") throw new AppError(409, "Booking not payable");
    if (booking.exchangeRateExpiresAt.getTime() < Date.now()) {
      throw new AppError(409, "Quote expired — refresh first");
    }

    const property = await Property.findById(booking.propertyId).lean();
    if (!property) throw new AppError(404, "Property not found");

    const settings = await getCommerceSettings();
    const totalLyd = toNumber(booking.totalLyd);
    let walletPaidLyd = 0;

    if (settings.walletEnabled && body.useWalletLyd && body.useWalletLyd > 0) {
      const { balanceLyd } = await getWalletBalance(req.user!.id);
      walletPaidLyd = Math.min(body.useWalletLyd, balanceLyd, totalLyd);
      walletPaidLyd = Math.round(walletPaidLyd * 100) / 100;
    }

    const gatewayPaidLyd = Math.round((totalLyd - walletPaidLyd) * 100) / 100;
    let gatewayProvider: string = walletPaidLyd >= totalLyd ? "WALLET" : getPaymentGatewayProviderId();
    let providerRef = walletPaidLyd >= totalLyd ? `wallet_${booking._id}` : "";

    if (gatewayPaidLyd > 0) {
      const gateway = getPaymentGateway();
      const charged = await gateway.charge({
        bookingId: String(booking._id),
        userId: req.user!.id,
        amountLyd: gatewayPaidLyd,
        currency: "LYD",
        metadata: { useWalletLyd: walletPaidLyd },
      });
      if (!charged.ok) {
        throw new AppError(502, charged.error, {
          code: "PAYMENT_GATEWAY_FAILED",
          provider: charged.provider,
        });
      }
      gatewayProvider = charged.provider;
      providerRef = charged.providerRef;
    }

    if (walletPaidLyd > 0) {
      await debitWallet({
        userId: req.user!.id,
        amountLyd: walletPaidLyd,
        type: "BOOKING_PAY",
        bookingId: String(booking._id),
        note: "Booking payment from wallet",
      });
    }

    // Finalize loyalty redeem reserved on quote
    if (toNumber(booking.pointsRedeemed) > 0) {
      await adjustLoyalty({
        userId: req.user!.id,
        delta: -toNumber(booking.pointsRedeemed),
        type: "REDEEM",
        bookingId: String(booking._id),
        note: "Redeemed on payment",
      });
    }

    if (booking.couponCode) {
      const couponResult = await resolveCouponDiscount({
        code: booking.couponCode,
        nights: booking.nights,
        subtotalTnd: toNumber(booking.subtotalTnd),
        userId: req.user!.id,
      });
      if (couponResult.coupon && couponResult.code) {
        await recordCouponRedemption({
          couponId: String(couponResult.coupon.id || couponResult.coupon._id),
          code: couponResult.code,
          userId: req.user!.id,
          bookingId: String(booking._id),
          discountTnd: toNumber(booking.pointsDiscountTnd) > 0
            ? Math.max(0, toNumber(booking.discountTnd) - toNumber(booking.pointsDiscountTnd))
            : toNumber(booking.discountTnd),
        });
      }
    }

    const earned = await earnPointsForPayment(req.user!.id, totalLyd, String(booking._id));

    const nextStatus = property.instantBooking ? "CONFIRMED" : "WAITING_OWNER";

    booking.status = nextStatus;
    booking.walletPaidLyd = walletPaidLyd;
    booking.pointsEarned = earned.points;
    if (booking.payment) {
      booking.payment.status = "PAID";
      booking.payment.provider = walletPaidLyd >= totalLyd ? "WALLET" : gatewayProvider;
      booking.payment.providerRef = providerRef;
      booking.payment.paidAt = new Date();
      booking.payment.updatedAt = new Date();
      booking.payment.metadata = {
        ...(booking.payment.metadata || {}),
        walletPaidLyd,
        gatewayPaidLyd,
        demoPaidLyd: gatewayPaidLyd,
      };
      booking.payment.amount = totalLyd;
    }

    const [customerDoc, ownerDoc, cityDoc] = await Promise.all([
      User.findById(booking.customerId).select("fullName email phone").lean(),
      User.findById(booking.ownerId).select("fullName").lean(),
      City.findById(property.cityId).lean(),
    ]);

    const invoice = await buildInvoiceSnapshot({
      booking,
      bookingStatus: nextStatus,
      customer: customerDoc,
      owner: ownerDoc,
      property: {
        titleAr: property.titleAr,
        titleEn: property.titleEn,
        address: property.address,
        city: cityDoc ? { nameAr: cityDoc.nameAr, nameEn: cityDoc.nameEn } : null,
      },
    });
    booking.invoice = invoice as any;
    await booking.save();

    if (isFinancialDualWriteEnabled()) {
      await syncBookingToMysql(booking as any);
    }

    await postBookingPaymentLedger({
      _id: String(booking._id),
      customerId: booking.customerId,
      totalLyd: toNumber(booking.totalLyd),
      totalTnd: toNumber(booking.totalTnd),
      platformFeeTnd: toNumber(booking.platformFeeTnd),
      ownerPayoutTnd: toNumber(booking.ownerPayoutTnd),
      exchangeRateRate: toNumber(booking.exchangeRateRate),
    }).catch((err) => console.error("[ledger] booking payment post failed", err));

    await notifyUser({
      userId: booking.ownerId,
      titleAr: nextStatus === "CONFIRMED" ? "حجز مؤكد" : "بانتظار موافقتك",
      titleEn: nextStatus === "CONFIRMED" ? "Booking confirmed" : "Awaiting your approval",
      messageAr: "تم دفع الحجز بنجاح",
      messageEn: "Booking payment completed successfully",
      link: `/dashboard/owner?booking=${booking._id}`,
    });

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    const serialized = serializeBooking(hydrated);

    const emailResult = await sendBookingInvoiceEmail({
      bookingId: String(booking._id),
      invoice,
      bookingStatus: nextStatus,
      customer: customerDoc,
    });

    res.json({
      booking: serialized,
      invoiceEmailSent: emailResult.sent,
      invoiceEmailTo: customerDoc?.email || null,
    });
  }),
);

bookingsRouter.get(
  "/:id/refund-preview",
  requireAuth,
  asyncHandler(async (req, res) => {
    const preview = await previewCancelRefund(req.params.id, req.user!.id);
    res.json(preview);
  }),
);

bookingsRouter.post(
  "/:id/cancel",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isBookingsMysql() && !isFinancialDualWriteEnabled()) {
      return withMysqlNamedLock(`booking:${req.params.id}`, async () => {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Not found");
      const allowed =
        isStaffRole(req.user!.role) ||
        row.customerId === req.user!.id ||
        row.ownerId === req.user!.id;
      if (!allowed) throw new AppError(403, "Forbidden");
      if (["CANCELLED", "COMPLETED", "REJECTED"].includes(row.status)) {
        throw new AppError(409, "Cannot cancel");
      }

      const actorRole = isStaffRole(req.user!.role)
        ? "ADMIN"
        : row.customerId === req.user!.id
          ? "CUSTOMER"
          : "OWNER";

      const preview = await previewCancelRefund(row.id, row.customerId);
      let refundLyd = 0;
      let refundPercent = 0;
      const payment = { ...(row.payment || {}) } as Record<string, unknown>;

      if (preview.paid && preview.refundLyd > 0) {
        refundLyd = preview.refundLyd;
        refundPercent = preview.refundPercent;
        await creditWallet({
          userId: row.customerId,
          amountLyd: refundLyd,
          type: "REFUND",
          bookingId: row.id,
          note: "Booking cancellation refund",
        });
        payment.status = refundPercent >= 100 ? "REFUNDED" : "PARTIALLY_REFUNDED";
        payment.updatedAt = new Date();
      } else if (payment.status === "PENDING") {
        payment.status = "CANCELLED";
        payment.updatedAt = new Date();
      }

      const cancelledAt = 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: "CANCELLED",
        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,
        refundPercent,
        cancelledBy: actorRole,
        cancelledAt,
        payment,
        invoice: row.invoice,
        createdAt: row.createdAt,
        updatedAt: cancelledAt,
      });

      const refund = { refundLyd, refundPercent };
      await notifyUser({
        userId: row.customerId === req.user!.id ? row.ownerId : row.customerId,
        titleAr: "تم إلغاء الحجز",
        titleEn: "Booking cancelled",
        messageAr:
          refund.refundLyd > 0
            ? `تم الإلغاء واسترداد ${refund.refundLyd} د.ل إلى المحفظة`
            : "تم إلغاء أحد الحجوزات",
        messageEn:
          refund.refundLyd > 0
            ? `Cancelled — ${refund.refundLyd} LYD credited to wallet`
            : "A booking was cancelled",
        link: `/dashboard/customer?booking=${row.id}`,
      });

      const updated = await findBookingByIdMysql(row.id);
      const [hydrated] = await hydrateBookings([bookingToApi(updated!)]);
      return res.json({ booking: serializeBooking(hydrated), refund, _db: "mysql" });
      });
    }

    const booking = await Booking.findOne({ _id: req.params.id, deletedAt: null });
    if (!booking) throw new AppError(404, "Not found");
    const allowed =
      isStaffRole(req.user!.role) ||
      booking.customerId === req.user!.id ||
      booking.ownerId === req.user!.id;
    if (!allowed) throw new AppError(403, "Forbidden");
    if (["CANCELLED", "COMPLETED", "REJECTED"].includes(booking.status)) {
      throw new AppError(409, "Cannot cancel");
    }

    const actorRole = isStaffRole(req.user!.role)
      ? "ADMIN"
      : booking.customerId === req.user!.id
        ? "CUSTOMER"
        : "OWNER";
    booking.status = "CANCELLED";
    booking.cancelledBy = actorRole;
    booking.cancelledAt = new Date();
    const refund = await applyRefundToWallet({ booking, reason: "CANCEL" });

    if (isFinancialDualWriteEnabled()) {
      await syncBookingToMysql(booking as any);
    }

    await notifyUser({
      userId: booking.customerId === req.user!.id ? booking.ownerId : booking.customerId,
      titleAr: "تم إلغاء الحجز",
      titleEn: "Booking cancelled",
      messageAr:
        refund.refundLyd > 0
          ? `تم الإلغاء واسترداد ${refund.refundLyd} د.ل إلى المحفظة`
          : "تم إلغاء أحد الحجوزات",
      messageEn:
        refund.refundLyd > 0
          ? `Cancelled — ${refund.refundLyd} LYD credited to wallet`
          : "A booking was cancelled",
      link: `/dashboard/customer?booking=${booking._id}`,
    });

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    res.json({ booking: serializeBooking(hydrated), refund });
  }),
);

bookingsRouter.post(
  "/:id/accept",
  requireAuth,
  requireRoles("OWNER", "ADMIN"),
  asyncHandler(async (req, res) => {
    if (isBookingsMysql() && !isFinancialDualWriteEnabled()) {
      return withMysqlNamedLock(`booking:${req.params.id}`, async () => {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Not found");
      if (!isStaffRole(req.user!.role) && row.ownerId !== req.user!.id) {
        throw new AppError(403, "Forbidden");
      }
      if (row.status !== "WAITING_OWNER") throw new AppError(409, "Not awaiting owner");
      await upsertBookingMysql({
        ...row,
        status: "CONFIRMED",
        updatedAt: new Date(),
      });
      await notifyUser({
        userId: row.customerId,
        titleAr: "تم قبول حجزك",
        titleEn: "Booking accepted",
        messageAr: "وافق المالك على حجزك",
        messageEn: "The owner accepted your booking",
        link: `/dashboard/customer?booking=${row.id}`,
      });
      const updated = await findBookingByIdMysql(row.id);
      const [hydrated] = await hydrateBookings([bookingToApi(updated!)]);
      return res.json({ booking: serializeBooking(hydrated), _db: "mysql" });
      });
    }

    const booking = await Booking.findOne({ _id: req.params.id, deletedAt: null });
    if (!booking) throw new AppError(404, "Not found");
    if (!isStaffRole(req.user!.role) && booking.ownerId !== req.user!.id) {
      throw new AppError(403, "Forbidden");
    }
    if (booking.status !== "WAITING_OWNER") throw new AppError(409, "Not awaiting owner");

    booking.status = "CONFIRMED";
    await booking.save();

    if (isFinancialDualWriteEnabled()) {
      await syncBookingToMysql(booking as any);
    }

    await notifyUser({
      userId: booking.customerId,
      titleAr: "تم قبول حجزك",
      titleEn: "Booking accepted",
      messageAr: "وافق المالك على حجزك",
      messageEn: "The owner accepted your booking",
      link: `/dashboard/customer?booking=${booking._id}`,
    });

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    res.json({ booking: serializeBooking(hydrated) });
  }),
);

bookingsRouter.post(
  "/:id/reject",
  requireAuth,
  requireRoles("OWNER", "ADMIN"),
  asyncHandler(async (req, res) => {
    if (isBookingsMysql() && !isFinancialDualWriteEnabled()) {
      return withMysqlNamedLock(`booking:${req.params.id}`, async () => {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Not found");
      if (!isStaffRole(req.user!.role) && row.ownerId !== req.user!.id) {
        throw new AppError(403, "Forbidden");
      }
      if (row.status !== "WAITING_OWNER") throw new AppError(409, "Not awaiting owner");

      const settings = await getCommerceSettings();
      const refundPercent = settings.ownerRejectRefundPercent;
      const refundLyd =
        row.payment?.status === "PAID"
          ? Math.round(((toNumber(row.totalLyd) * refundPercent) / 100) * 100) / 100
          : 0;
      const payment = { ...(row.payment || {}) } as Record<string, unknown>;
      if (refundLyd > 0) {
        await creditWallet({
          userId: row.customerId,
          amountLyd: refundLyd,
          type: "REFUND",
          bookingId: row.id,
          note: "Owner rejection refund",
        });
        payment.status = refundPercent >= 100 ? "REFUNDED" : "PARTIALLY_REFUNDED";
        payment.updatedAt = new Date();
      }
      const cancelledAt = new Date();
      await upsertBookingMysql({
        ...row,
        status: "REJECTED",
        cancelledBy: isStaffRole(req.user!.role) ? "ADMIN" : "OWNER",
        cancelledAt,
        refundLyd,
        refundPercent: refundLyd > 0 ? refundPercent : 0,
        payment,
        updatedAt: cancelledAt,
      });
      const refund = { refundLyd, refundPercent: refundLyd > 0 ? refundPercent : 0 };
      await notifyUser({
        userId: row.customerId,
        titleAr: "تم رفض الحجز",
        titleEn: "Booking rejected",
        messageAr:
          refund.refundLyd > 0
            ? `رُفض الحجز واستُرد ${refund.refundLyd} د.ل إلى محفظتك`
            : "رفض المالك طلب الحجز",
        messageEn:
          refund.refundLyd > 0
            ? `Rejected — ${refund.refundLyd} LYD credited to your wallet`
            : "The owner rejected your booking",
        link: `/dashboard/customer?booking=${row.id}`,
      });
      const updated = await findBookingByIdMysql(row.id);
      const [hydrated] = await hydrateBookings([bookingToApi(updated!)]);
      return res.json({ booking: serializeBooking(hydrated), refund, _db: "mysql" });
      });
    }

    const booking = await Booking.findOne({ _id: req.params.id, deletedAt: null });
    if (!booking) throw new AppError(404, "Not found");
    if (!isStaffRole(req.user!.role) && booking.ownerId !== req.user!.id) {
      throw new AppError(403, "Forbidden");
    }
    if (booking.status !== "WAITING_OWNER") throw new AppError(409, "Not awaiting owner");

    booking.status = "REJECTED";
    booking.cancelledBy = isStaffRole(req.user!.role) ? "ADMIN" : "OWNER";
    booking.cancelledAt = new Date();
    const refund = await applyRefundToWallet({ booking, reason: "OWNER_REJECT" });

    if (isFinancialDualWriteEnabled()) {
      await syncBookingToMysql(booking as any);
    }

    await notifyUser({
      userId: booking.customerId,
      titleAr: "تم رفض الحجز",
      titleEn: "Booking rejected",
      messageAr:
        refund.refundLyd > 0
          ? `رُفض الحجز واستُرد ${refund.refundLyd} د.ل إلى محفظتك`
          : "رفض المالك طلب الحجز",
      messageEn:
        refund.refundLyd > 0
          ? `Rejected — ${refund.refundLyd} LYD credited to your wallet`
          : "The owner rejected your booking",
      link: `/dashboard/customer?booking=${booking._id}`,
    });

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    res.json({ booking: serializeBooking(hydrated), refund });
  }),
);

function paymentAllowsInvoice(booking: {
  payment?: { status?: string; paidAt?: unknown } | null;
  paymentStatus?: string | null;
  invoice?: { invoiceNumber?: string } | null;
}) {
  const payStatus = String(booking.payment?.status || booking.paymentStatus || "");
  const paidAt = booking.payment?.paidAt;
  return (
    Boolean(paidAt) ||
    payStatus === "PAID" ||
    payStatus === "PARTIALLY_REFUNDED" ||
    payStatus === "REFUNDED" ||
    Boolean(booking.invoice?.invoiceNumber)
  );
}

async function buildMysqlInvoiceSnapshotForRow(row: NonNullable<Awaited<ReturnType<typeof findBookingByIdMysql>>>) {
  const booking = bookingToApi(row);
  const property = await findPropertyByIdMysql(row.propertyId);
  const users = await findUsersByIdsMysql([row.customerId, row.ownerId]);
  const customerDoc = users.find((u) => u.id === row.customerId) || null;
  const ownerDoc = users.find((u) => u.id === row.ownerId) || null;
  let cityDoc: { nameAr?: string; nameEn?: string; name_ar?: string; name_en?: string } | null = null;
  if (property) {
    if (isCitiesMysql()) {
      cityDoc = (await findCityByIdMysql(property.cityId)) as any;
    } else {
      cityDoc = (await City.findById(property.cityId).lean()) as any;
    }
  }
  const snapshot = await buildInvoiceSnapshot({
    booking: booking as any,
    bookingStatus: row.status,
    customer: customerDoc
      ? {
          fullName: customerDoc.fullName,
          email: customerDoc.email,
          phone: customerDoc.phone ?? null,
        }
      : null,
    owner: ownerDoc ? { fullName: ownerDoc.fullName } : null,
    property: property
      ? {
          titleAr: property.titleAr,
          titleEn: property.titleEn,
          address: property.address,
          city: cityDoc
            ? {
                nameAr: cityDoc.nameAr ?? cityDoc.name_ar,
                nameEn: cityDoc.nameEn ?? cityDoc.name_en,
              }
            : null,
        }
      : null,
  });
  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: row.status,
    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: row.refundLyd,
    refundPercent: row.refundPercent,
    cancelledBy: row.cancelledBy,
    cancelledAt: row.cancelledAt,
    payment: row.payment,
    invoice: snapshot as any,
    createdAt: row.createdAt,
    updatedAt: new Date(),
  });
  return { booking: { ...booking, invoice: snapshot }, snapshot, customerDoc, ownerDoc };
}

bookingsRouter.get(
  "/:id/invoice",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isBookingsMysql()) {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Not found");
      const allowed =
        isStaffRole(req.user!.role) ||
        row.customerId === req.user!.id ||
        row.ownerId === req.user!.id;
      if (!allowed) throw new AppError(403, "Forbidden");

      let booking = bookingToApi(row);
      if (!booking.invoice && paymentAllowsInvoice({ ...booking, paymentStatus: row.paymentStatus })) {
        const built = await buildMysqlInvoiceSnapshotForRow(row);
        booking = built.booking;
      }
      if (!booking.invoice) throw new AppError(404, "Invoice not found");

      const [hydrated] = await hydrateBookings([booking]);
      const serialized = serializeBooking(hydrated);
      return res.json({
        invoice: {
          ...serialized.invoice,
          booking: serialized,
        },
        _db: "mysql",
      });
    }

    const booking = await Booking.findOne({ _id: req.params.id, deletedAt: null });
    if (!booking) throw new AppError(404, "Not found");
    const allowed =
      isStaffRole(req.user!.role) ||
      booking.customerId === req.user!.id ||
      booking.ownerId === req.user!.id;
    if (!allowed) throw new AppError(403, "Forbidden");

    // Backfill snapshot for older paid bookings that only had a number
    if (!booking.invoice && booking.payment?.status === "PAID") {
      const [customerDoc, ownerDoc, property] = await Promise.all([
        User.findById(booking.customerId).select("fullName email phone").lean(),
        User.findById(booking.ownerId).select("fullName").lean(),
        Property.findById(booking.propertyId).lean(),
      ]);
      const cityDoc = property ? await City.findById(property.cityId).lean() : null;
      const snapshot = await buildInvoiceSnapshot({
        booking,
        bookingStatus: booking.status,
        customer: customerDoc,
        owner: ownerDoc,
        property: property
          ? {
              titleAr: property.titleAr,
              titleEn: property.titleEn,
              address: property.address,
              city: cityDoc ? { nameAr: cityDoc.nameAr, nameEn: cityDoc.nameEn } : null,
            }
          : null,
      });
      booking.invoice = snapshot as any;
      await booking.save();
    }

    if (!booking.invoice) throw new AppError(404, "Invoice not found");

    const [hydrated] = await hydrateBookings([booking.toObject()]);
    const serialized = serializeBooking(hydrated);

    res.json({
      invoice: {
        ...serialized.invoice,
        booking: serialized,
      },
    });
  }),
);

bookingsRouter.post(
  "/:id/invoice/email",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isBookingsMysql()) {
      const row = await findBookingByIdMysql(req.params.id);
      if (!row) throw new AppError(404, "Not found");
      const allowed =
        isStaffRole(req.user!.role) ||
        row.customerId === req.user!.id ||
        row.ownerId === req.user!.id;
      if (!allowed) throw new AppError(403, "Forbidden");

      console.log(
        `[invoice-email] request origin=${req.headers.origin || "-"} ua=${String(req.headers["user-agent"] || "").slice(0, 80)} booking=${row.id}`,
      );

      let booking = bookingToApi(row);
      if (!paymentAllowsInvoice({ ...booking, paymentStatus: row.paymentStatus })) {
        throw new AppError(409, "Invoice email is only available after payment");
      }

      let customerDoc: Awaited<ReturnType<typeof findUsersByIdsMysql>>[number] | null =
        (await findUsersByIdsMysql([row.customerId]))[0] || null;
      if (!booking.invoice) {
        const built = await buildMysqlInvoiceSnapshotForRow(row);
        booking = built.booking;
        customerDoc = built.customerDoc;
      }
      if (!customerDoc?.email) throw new AppError(400, "Customer email missing");
      if (!booking.invoice) throw new AppError(404, "Invoice not found");

      const inv = booking.invoice as any;
      const emailResult = await sendBookingInvoiceEmail({
        bookingId: row.id,
        invoice: {
          guestName: inv.guestName,
          invoiceNumber: inv.invoiceNumber,
          issuedAt: inv.issuedAt,
          propertyTitleAr: inv.propertyTitleAr,
          propertyTitleEn: inv.propertyTitleEn,
          propertyAddress: inv.propertyAddress,
          cityNameAr: inv.cityNameAr,
          cityNameEn: inv.cityNameEn,
          hostName: inv.hostName,
          checkIn: inv.checkIn,
          checkOut: inv.checkOut,
          nights: inv.nights,
          guests: inv.guests,
          subtotalTnd: toNumber(inv.subtotalTnd),
          cleaningFeeTnd: toNumber(inv.cleaningFeeTnd),
          platformFeeTnd: toNumber(inv.platformFeeTnd),
          taxesTnd: toNumber(inv.taxesTnd),
          totalTnd: toNumber(inv.totalTnd),
          totalLyd: toNumber(inv.totalLyd),
          exchangeRateRate: toNumber(inv.exchangeRateRate),
          paymentProvider: inv.paymentProvider,
        },
        bookingStatus: row.status,
        customer: customerDoc,
      });

      if (!emailResult.sent && emailResult.reason === "error") {
        throw new AppError(502, "Failed to send invoice email");
      }

      return res.json({
        message: "Invoice email sent",
        invoiceEmailSent: emailResult.sent,
        invoiceEmailTo: customerDoc.email,
        _db: "mysql",
      });
    }

    const booking = await Booking.findOne({ _id: req.params.id, deletedAt: null });
    if (!booking) throw new AppError(404, "Not found");
    const allowed =
      isStaffRole(req.user!.role) ||
      booking.customerId === req.user!.id ||
      booking.ownerId === req.user!.id;
    if (!allowed) throw new AppError(403, "Forbidden");

    console.log(
      `[invoice-email] request origin=${req.headers.origin || "-"} ua=${String(req.headers["user-agent"] || "").slice(0, 80)} booking=${booking._id}`,
    );

    // Allow after any successful payment, including later partial/full refunds.
    // (UI "مدفوع" used paidAt / invoice snapshot; live status may be PARTIALLY_REFUNDED.)
    if (!paymentAllowsInvoice(booking)) {
      throw new AppError(409, "Invoice email is only available after payment");
    }

    const [customerDoc, ownerDoc, property] = await Promise.all([
      User.findById(booking.customerId).select("fullName email phone").lean(),
      User.findById(booking.ownerId).select("fullName").lean(),
      Property.findById(booking.propertyId).lean(),
    ]);
    if (!customerDoc?.email) throw new AppError(400, "Customer email missing");

    if (!booking.invoice) {
      const cityDoc = property ? await City.findById(property.cityId).lean() : null;
      const snapshot = await buildInvoiceSnapshot({
        booking,
        bookingStatus: booking.status,
        customer: customerDoc,
        owner: ownerDoc,
        property: property
          ? {
              titleAr: property.titleAr,
              titleEn: property.titleEn,
              address: property.address,
              city: cityDoc ? { nameAr: cityDoc.nameAr, nameEn: cityDoc.nameEn } : null,
            }
          : null,
      });
      booking.invoice = snapshot as any;
      await booking.save();
    }

    const inv = booking.invoice as any;
    const emailResult = await sendBookingInvoiceEmail({
      bookingId: String(booking._id),
      invoice: {
        guestName: inv.guestName,
        invoiceNumber: inv.invoiceNumber,
        issuedAt: inv.issuedAt,
        propertyTitleAr: inv.propertyTitleAr,
        propertyTitleEn: inv.propertyTitleEn,
        propertyAddress: inv.propertyAddress,
        cityNameAr: inv.cityNameAr,
        cityNameEn: inv.cityNameEn,
        hostName: inv.hostName,
        checkIn: inv.checkIn,
        checkOut: inv.checkOut,
        nights: inv.nights,
        guests: inv.guests,
        subtotalTnd: toNumber(inv.subtotalTnd),
        cleaningFeeTnd: toNumber(inv.cleaningFeeTnd),
        platformFeeTnd: toNumber(inv.platformFeeTnd),
        taxesTnd: toNumber(inv.taxesTnd),
        totalTnd: toNumber(inv.totalTnd),
        totalLyd: toNumber(inv.totalLyd),
        exchangeRateRate: toNumber(inv.exchangeRateRate),
        paymentProvider: inv.paymentProvider,
      },
      bookingStatus: booking.status,
      customer: customerDoc,
    });

    if (!emailResult.sent && emailResult.reason === "error") {
      throw new AppError(502, "Failed to send invoice email");
    }

    res.json({
      message: "Invoice email sent",
      invoiceEmailSent: emailResult.sent,
      invoiceEmailTo: customerDoc.email,
    });
  }),
);
