import { Router, type IRouter } from "express";
import {
  db,
  bookingsTable,
  profilesTable,
  locationsTable,
  servicesTable,
  orderProofsTable,
} from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { requireAuth } from "../middlewares/auth";
import {
  CreateBookingBody,
  GetBookingParams,
  CancelBookingParams,
  UpdateBookingStatusParams,
  UpdateBookingStatusBody,
  UploadBookingProofParams,
  UploadBookingProofBody,
} from "@workspace/api-zod";
import {
  adjustBalance,
  getPlatformUserId,
  splitCommission,
  InsufficientFundsError,
} from "../lib/wallet";

const router: IRouter = Router();

const DURATION_PRICE_FIELD: Record<string, "price1h" | "price2h" | "priceFullDay" | "priceNight"> = {
  "1h": "price1h",
  "2h": "price2h",
  full_day: "priceFullDay",
  night: "priceNight",
};
const CUSTOMER_LOCATION_FEE = 500;

type BookingStatus =
  | "pending"
  | "accepted"
  | "paid"
  | "proof_uploaded"
  | "completed"
  | "rejected"
  | "cancelled"
  | "refunded";

const TRANSITIONS: Record<BookingStatus, BookingStatus[]> = {
  pending: ["accepted", "rejected", "cancelled"],
  accepted: ["paid", "cancelled"],
  paid: ["proof_uploaded", "refunded"],
  proof_uploaded: ["completed", "paid", "refunded"],
  completed: [],
  rejected: [],
  cancelled: [],
  refunded: [],
};

// States in which the customer's funds have already been captured into escrow
// (the platform admin wallet). Cancelling out of these requires a refund.
const CAPTURED: BookingStatus[] = ["paid", "proof_uploaded"];

type BookingRow = typeof bookingsTable.$inferSelect;

function serializeBooking(
  b: BookingRow,
  extra: {
    profileName?: string | null;
    locationName?: string | null;
    serviceName?: string | null;
    customerName?: string | null;
  } = {},
) {
  return {
    id: b.id,
    userId: b.userId,
    profileId: b.profileId,
    locationId: b.locationId,
    serviceId: b.serviceId,
    profileName: extra.profileName ?? null,
    locationName: extra.locationName ?? null,
    serviceName: extra.serviceName ?? null,
    customerName: extra.customerName ?? null,
    duration: b.duration,
    locationType: b.locationType,
    totalPrice: b.totalPrice,
    status: b.status,
    notes: b.notes,
    scheduledAt: b.scheduledAt,
    createdAt: b.createdAt,
  };
}

const bookingWithNames = async (userId: number) => {
  const bookings = await db.select({
    booking: bookingsTable,
    profileName: profilesTable.name,
    locationName: locationsTable.name,
    serviceName: servicesTable.name,
  }).from(bookingsTable)
    .leftJoin(profilesTable, eq(bookingsTable.profileId, profilesTable.id))
    .leftJoin(locationsTable, eq(bookingsTable.locationId, locationsTable.id))
    .leftJoin(servicesTable, eq(bookingsTable.serviceId, servicesTable.id))
    .where(eq(bookingsTable.userId, userId))
    .orderBy(bookingsTable.createdAt);

  return bookings.map(({ booking, profileName, locationName, serviceName }) =>
    serializeBooking(booking, { profileName, locationName, serviceName }),
  );
};

router.get("/bookings", requireAuth, async (req, res): Promise<void> => {
  const bookings = await bookingWithNames(req.user!.id);
  res.json(bookings);
});

router.post("/bookings", requireAuth, async (req, res): Promise<void> => {
  const parsed = CreateBookingBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const { profileId, locationId, serviceId, duration, locationType, notes, scheduledAt } = parsed.data;

  if (!profileId || !serviceId || !duration || !locationType || !scheduledAt) {
    res.status(400).json({ error: "profileId, serviceId, duration, locationType, and scheduledAt are required" });
    return;
  }
  if (!DURATION_PRICE_FIELD[duration]) {
    res.status(400).json({ error: "Invalid duration" });
    return;
  }
  if (locationType !== "in_studio" && locationType !== "customer_location") {
    res.status(400).json({ error: "Invalid locationType" });
    return;
  }

  const [service] = await db.select().from(servicesTable).where(eq(servicesTable.id, serviceId));
  if (!service) {
    res.status(400).json({ error: "Service not found" });
    return;
  }
  if (!service.isActive) {
    res.status(400).json({ error: "Service is not available for booking" });
    return;
  }

  const [profile] = await db.select().from(profilesTable).where(eq(profilesTable.id, profileId));
  if (!profile) {
    res.status(400).json({ error: "Provider not found" });
    return;
  }
  if (profile.approvalStatus !== "approved" || !profile.isActive) {
    res.status(400).json({ error: "Provider is not available for booking" });
    return;
  }
  const [location] = locationId ? await db.select().from(locationsTable).where(eq(locationsTable.id, locationId)) : [null];
  if (locationId && !location) {
    res.status(400).json({ error: "Location not found" });
    return;
  }

  const basePrice = profile[DURATION_PRICE_FIELD[duration]];
  if (basePrice == null) {
    res.status(400).json({ error: "This provider has not set a price for the selected duration" });
    return;
  }
  let totalPrice = basePrice;
  if (locationType === "customer_location") totalPrice += CUSTOMER_LOCATION_FEE;

  // Orders are created as a free request — no charge is taken up front. The
  // customer is only debited once the model accepts and the payment is captured
  // into escrow.
  const [booking] = await db.insert(bookingsTable).values({
    userId: req.user!.id,
    profileId: profileId ?? undefined,
    locationId: locationId ?? undefined,
    serviceId: serviceId ?? undefined,
    duration: duration ?? undefined,
    locationType: locationType ?? undefined,
    totalPrice: totalPrice ?? undefined,
    notes: notes ?? undefined,
    scheduledAt: scheduledAt ? new Date(scheduledAt) : undefined,
    status: "pending",
  }).returning();

  res.status(201).json(serializeBooking(booking, {
    profileName: profile?.name ?? null,
    locationName: location?.name ?? null,
    serviceName: service?.name ?? null,
  }));
});

router.get("/bookings/:id", requireAuth, async (req, res): Promise<void> => {
  const params = GetBookingParams.safeParse({ id: req.params.id });
  if (!params.success) {
    res.status(400).json({ error: "Invalid ID" });
    return;
  }
  const [row] = await db.select({
    booking: bookingsTable,
    profileName: profilesTable.name,
    locationName: locationsTable.name,
    serviceName: servicesTable.name,
  }).from(bookingsTable)
    .leftJoin(profilesTable, eq(bookingsTable.profileId, profilesTable.id))
    .leftJoin(locationsTable, eq(bookingsTable.locationId, locationsTable.id))
    .leftJoin(servicesTable, eq(bookingsTable.serviceId, servicesTable.id))
    .where(and(eq(bookingsTable.id, params.data.id), eq(bookingsTable.userId, req.user!.id)));
  if (!row) {
    res.status(404).json({ error: "Booking not found" });
    return;
  }
  const { booking, profileName, locationName, serviceName } = row;
  res.json(serializeBooking(booking, { profileName, locationName, serviceName }));
});

/**
 * Apply the wallet/ledger side effects of a booking status transition and flip
 * the status atomically. Returns the updated row, or null if the guarded update
 * matched nothing (already transitioned by a concurrent request).
 *
 * Money model — escrow is physically held in the platform admin wallet:
 *  - paid (capture):  customer −amount (order_payment), admin +amount (escrow_in)
 *  - completed (settle): admin −amount (escrow_out), model +90% (earning),
 *                        admin +10% (commission)  ⇒ net admin change = +10%
 *  - refunded / captured→cancelled (refund): admin −amount (escrow_out),
 *                        customer +amount (refund)
 */
async function applyTransition(
  booking: BookingRow,
  target: BookingStatus,
  modelUserId: number | null,
  platformUserId: number | null,
) {
  const amount = booking.totalPrice ?? 0;
  const from = booking.status as BookingStatus;

  return db.transaction(async (tx) => {
    const [row] = await tx
      .update(bookingsTable)
      .set({ status: target })
      .where(and(eq(bookingsTable.id, booking.id), eq(bookingsTable.status, from)))
      .returning();
    if (!row) return null;

    if (target === "paid") {
      // Capture the customer's funds into escrow.
      if (amount > 0) {
        if (!platformUserId) throw new Error("No platform account configured for escrow");
        await adjustBalance({ userId: booking.userId, amount: -amount, type: "order_payment", bookingId: booking.id, description: `Payment for order #${booking.id}`, tx });
        await adjustBalance({ userId: platformUserId, amount, type: "escrow_in", bookingId: booking.id, description: `Escrow hold for order #${booking.id}`, tx });
      }
    } else if (target === "completed") {
      // Settle escrow: release the hold, pay the model 90%, keep 10% commission.
      const { commission, modelAmount } = splitCommission(amount);
      if (amount > 0) {
        if (!platformUserId) throw new Error("No platform account configured for escrow");
        await adjustBalance({ userId: platformUserId, amount: -amount, type: "escrow_out", bookingId: booking.id, description: `Escrow release for order #${booking.id}`, tx });
        if (modelUserId && modelAmount > 0) {
          await adjustBalance({ userId: modelUserId, amount: modelAmount, type: "earning", bookingId: booking.id, description: `Earnings from order #${booking.id}`, tx });
        }
        if (commission > 0) {
          await adjustBalance({ userId: platformUserId, amount: commission, type: "commission", bookingId: booking.id, description: `Commission from order #${booking.id}`, tx });
        }
      }
    } else if (target === "refunded" || (target === "cancelled" && CAPTURED.includes(from))) {
      // Release captured funds from escrow back to the customer.
      if (amount > 0) {
        if (!platformUserId) throw new Error("No platform account configured for escrow");
        await adjustBalance({ userId: platformUserId, amount: -amount, type: "escrow_out", bookingId: booking.id, description: `Escrow release (refund) for order #${booking.id}`, tx });
        await adjustBalance({ userId: booking.userId, amount, type: "refund", bookingId: booking.id, description: `Refund for order #${booking.id}`, tx });
      }
    }
    return row;
  });
}

async function resolveModelUserId(profileId: number | null): Promise<number | null> {
  if (!profileId) return null;
  const [profile] = await db.select().from(profilesTable).where(eq(profilesTable.id, profileId));
  return profile?.userId ?? null;
}

router.patch("/bookings/:id/status", requireAuth, async (req, res): Promise<void> => {
  const params = UpdateBookingStatusParams.safeParse({ id: req.params.id });
  if (!params.success) {
    res.status(400).json({ error: "Invalid ID" });
    return;
  }
  const parsed = UpdateBookingStatusBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const target = parsed.data.status as BookingStatus;

  const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, params.data.id));
  if (!booking) {
    res.status(404).json({ error: "Booking not found" });
    return;
  }

  const role = req.user!.role;
  const isAdmin = role === "admin";
  const isOwner = booking.userId === req.user!.id;
  const modelUserId = await resolveModelUserId(booking.profileId);
  const isProvider = modelUserId != null && modelUserId === req.user!.id;

  // Authorize the requested target by actor role.
  const allowedActors: Record<BookingStatus, ("owner" | "provider" | "admin")[]> = {
    accepted: ["provider", "admin"],
    rejected: ["provider", "admin"],
    paid: ["owner", "admin"],
    // proof_uploaded is reachable only via POST /bookings/:id/proof (which records the proof photo)
    proof_uploaded: [],
    // completed is reachable only via admin proof review (PATCH /admin/proofs/:id)
    completed: [],
    cancelled: ["owner", "provider", "admin"],
    refunded: ["admin"],
    pending: [],
  };
  const actorOk =
    (allowedActors[target]?.includes("admin") && isAdmin) ||
    (allowedActors[target]?.includes("owner") && isOwner) ||
    (allowedActors[target]?.includes("provider") && isProvider);
  if (!actorOk) {
    res.status(403).json({ error: "You are not allowed to perform this action" });
    return;
  }

  const current = booking.status as BookingStatus;
  if (!TRANSITIONS[current]?.includes(target)) {
    res.status(409).json({ error: `Cannot change a ${current} order to ${target}` });
    return;
  }

  const platformUserId = await getPlatformUserId();
  let updated;
  try {
    updated = await applyTransition(booking, target, modelUserId, platformUserId);
  } catch (err) {
    if (err instanceof InsufficientFundsError) {
      if (target === "paid") {
        res.status(400).json({ error: "Insufficient wallet balance. Please add funds to your wallet." });
      } else {
        res.status(409).json({ error: "Cannot complete this action due to insufficient escrow funds." });
      }
      return;
    }
    throw err;
  }
  if (!updated) {
    res.status(409).json({ error: "This order was already updated. Please refresh." });
    return;
  }

  // Best-effort capture on acceptance: if the customer already has the funds,
  // pull them into escrow immediately (accepted → paid). If they're short, the
  // order rests at "accepted" and the customer can pay manually.
  if (target === "accepted") {
    try {
      const captured = await applyTransition(updated, "paid", modelUserId, platformUserId);
      if (captured) updated = captured;
    } catch (err) {
      if (!(err instanceof InsufficientFundsError)) throw err;
    }
  }

  res.json(serializeBooking(updated));
});

router.post("/bookings/:id/proof", requireAuth, async (req, res): Promise<void> => {
  const params = UploadBookingProofParams.safeParse({ id: req.params.id });
  if (!params.success) {
    res.status(400).json({ error: "Invalid ID" });
    return;
  }
  const parsed = UploadBookingProofBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const { photoUrl } = parsed.data;

  const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, params.data.id));
  if (!booking) {
    res.status(404).json({ error: "Booking not found" });
    return;
  }

  const isAdmin = req.user!.role === "admin";
  const modelUserId = await resolveModelUserId(booking.profileId);
  const isProvider = modelUserId != null && modelUserId === req.user!.id;
  if (!isProvider && !isAdmin) {
    res.status(403).json({ error: "You are not allowed to perform this action" });
    return;
  }

  if (booking.status !== "paid") {
    res.status(409).json({ error: "Proof can only be uploaded for a paid order" });
    return;
  }

  const result = await db.transaction(async (tx) => {
    const [row] = await tx
      .update(bookingsTable)
      .set({ status: "proof_uploaded" })
      .where(and(eq(bookingsTable.id, booking.id), eq(bookingsTable.status, "paid")))
      .returning();
    if (!row) return null;
    await tx.insert(orderProofsTable).values({
      bookingId: booking.id,
      userId: req.user!.id,
      photoUrl,
      status: "pending",
    });
    return row;
  });
  if (!result) {
    res.status(409).json({ error: "This order was already updated. Please refresh." });
    return;
  }

  res.json(serializeBooking(result));
});

router.delete("/bookings/:id", requireAuth, async (req, res): Promise<void> => {
  const params = CancelBookingParams.safeParse({ id: req.params.id });
  if (!params.success) {
    res.status(400).json({ error: "Invalid ID" });
    return;
  }
  const [booking] = await db.select().from(bookingsTable).where(
    and(eq(bookingsTable.id, params.data.id), eq(bookingsTable.userId, req.user!.id))
  );
  if (!booking) {
    res.status(404).json({ error: "Booking not found" });
    return;
  }
  const current = booking.status as BookingStatus;
  if (!TRANSITIONS[current]?.includes("cancelled")) {
    res.status(409).json({ error: `A ${current} order cannot be cancelled` });
    return;
  }
  const platformUserId = await getPlatformUserId();
  let result;
  try {
    result = await applyTransition(booking, "cancelled", null, platformUserId);
  } catch (err) {
    if (err instanceof InsufficientFundsError) {
      res.status(409).json({ error: "Cannot cancel this order due to insufficient escrow funds." });
      return;
    }
    throw err;
  }
  if (!result) {
    res.status(409).json({ error: "This order was already updated. Please refresh." });
    return;
  }
  res.sendStatus(204);
});

export default router;
