// Shared capacity-checked recovery for bookings that don't currently hold a seat.
//
// Under the seat-holding rule (lib/capacity.ts) a seat is held by paid/checked-in
// tickets and by 'pending_approval' payments. Promoting a booking INTO one of those
// states — user clicking "I've paid", an admin approving/reactivating a payment —
// must atomically re-check that the event still has room, exactly like the original
// booking-creation flow in routes/tickets.ts. Demoting back to bare 'pending'
// (e.g. reopening a failed payment) claims no seat and skips the check.
//
// Callers choose which ticket statuses are eligible to be flipped via
// `options.fromTicketStatuses` (default ['on_hold']); tickets not in that list are
// left untouched. Tickets that already hold a seat cost no new capacity.
// `options.skipCapacityCheck` lets an admin knowingly approve over capacity —
// the UI warns first (routes/payments.ts /approve with allowOverCapacity).

import { eq, and, inArray } from 'drizzle-orm';
import { db, dbGet, tickets, payments, events, isSqlite } from '../db/index.js';
import { getNow, calculateAvailableSeats, isEventSoldOut } from './utils.js';
import { seatHolderCountQuery, unseatedTicketCountQuery } from './capacity.js';

export class HoldCapacityError extends Error {
  constructor(public available: number) {
    super('EVENT_FULL');
  }
}

interface ReserveOptions {
  paidByAdminId?: string;
  extraPaymentFields?: Record<string, any>;
  /** Ticket statuses eligible to be flipped to targetTicketStatus. Default: ['on_hold']. */
  fromTicketStatuses?: Array<'on_hold' | 'cancelled' | 'pending'>;
  /** Admin override: reserve even when it puts the event over capacity. */
  skipCapacityCheck?: boolean;
}

/**
 * Re-reserve seats for a group of released tickets (e.g. all tickets sharing a
 * bookingId), atomically re-checking capacity before flipping their status.
 * Only tickets whose current status is in `fromTicketStatuses` are flipped.
 * Capacity is asserted against the number of those tickets that don't already
 * hold a seat, so re-reserving tickets that are already seated is a no-op.
 * Throws HoldCapacityError if the event no longer has room (unless the target
 * state holds no seat, or skipCapacityCheck is set).
 */
export async function reserveOnHoldBooking(
  eventId: string,
  ticketIds: string[],
  targetTicketStatus: 'pending' | 'confirmed',
  targetPaymentStatus: 'pending_approval' | 'paid' | 'pending',
  options: ReserveOptions = {}
): Promise<void> {
  if (ticketIds.length === 0) return;

  const fromTicketStatuses = options.fromTicketStatuses ?? ['on_hold'];
  // Bare 'pending' payments hold no seat, so moving a booking back to 'pending'
  // consumes no capacity and needs no check.
  const targetHoldsSeat = targetPaymentStatus !== 'pending' || targetTicketStatus === 'confirmed';
  const checkCapacity = targetHoldsSeat && !options.skipCapacityCheck;

  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, eventId))
  );
  if (!event) {
    throw new Error('Event not found');
  }

  const now = getNow();
  const paymentUpdate: Record<string, any> = {
    status: targetPaymentStatus,
    updatedAt: now,
    ...options.extraPaymentFields,
  };
  if (targetPaymentStatus === 'paid') {
    paymentUpdate.paidAt = now;
    if (options.paidByAdminId) paymentUpdate.paidByAdminId = options.paidByAdminId;
  }

  // Keep the ticket-level payment flag in sync when the payment settles
  const ticketUpdate: Record<string, any> = { status: targetTicketStatus };
  if (targetPaymentStatus === 'paid') {
    ticketUpdate.paymentStatus = 'paid';
  }

  // `needed` is how many of these tickets don't currently hold a seat and so must
  // be found new capacity; tickets already in a seat-holding state cost nothing.
  const assertCapacity = (reserved: number, needed: number) => {
    if (needed <= 0) return;
    if (isEventSoldOut(event.capacity, reserved)) {
      throw new HoldCapacityError(0);
    }
    const seatsLeft = calculateAvailableSeats(event.capacity, reserved);
    if (needed > seatsLeft) {
      throw new HoldCapacityError(seatsLeft);
    }
  };

  if (isSqlite()) {
    (db as any).transaction((tx: any) => {
      if (checkCapacity) {
        const countRow = seatHolderCountQuery(tx, eventId).get();
        const neededRow = unseatedTicketCountQuery(tx, ticketIds).get();
        assertCapacity(Number(countRow?.count || 0), Number(neededRow?.count || 0));
      }

      tx.update(tickets)
        .set(ticketUpdate)
        .where(and(
          inArray((tickets as any).id, ticketIds),
          inArray((tickets as any).status, fromTicketStatuses)
        ))
        .run();

      tx.update(payments)
        .set(paymentUpdate)
        .where(inArray((payments as any).ticketId, ticketIds))
        .run();
    });
  } else {
    await (db as any).transaction(async (tx: any) => {
      if (checkCapacity) {
        const countRow = await dbGet<any>(seatHolderCountQuery(tx, eventId));
        const neededRow = await dbGet<any>(unseatedTicketCountQuery(tx, ticketIds));
        assertCapacity(Number(countRow?.count || 0), Number(neededRow?.count || 0));
      }

      await tx.update(tickets)
        .set(ticketUpdate)
        .where(and(
          inArray((tickets as any).id, ticketIds),
          inArray((tickets as any).status, fromTicketStatuses)
        ));

      await tx.update(payments)
        .set(paymentUpdate)
        .where(inArray((payments as any).ticketId, ticketIds));
    });
  }
}
