// Expire stale pending bookings.
//
// When a booking is started, its tickets are created with status 'pending' and
// a 'pending' payment. Pending tickets count toward an event's capacity, so an
// abandoned checkout would otherwise hold those seats forever. This job cancels
// pending tickets whose payment is still 'pending' (i.e. never paid and not
// awaiting admin approval) after a configurable TTL, freeing the seats.
//
// Two exclusions:
//   - Manual-verification providers (bank transfer / TPago / cash) are never
//     auto-failed; they are settled by an admin and instead follow the 72h on-hold
//     sweep. See holdSweep.ts and MANUAL_PAYMENT_PROVIDERS.
//   - Staleness is measured from `updatedAt`, not `createdAt`, so an admin action
//     (e.g. reopening a payment to 'pending' via /reopen) restarts the TTL rather
//     than being immediately re-failed on the next run.

import { and, eq, lt, inArray, notInArray } from 'drizzle-orm';
import { db, dbAll, tickets, payments } from '../db/index.js';
import { getNow, toDbDate } from './utils.js';
import { getLock } from './stores/lock.js';
import { MANUAL_PAYMENT_PROVIDERS } from './paymentProviders.js';

function getTtlMs(): number {
  const minutes = parseInt(process.env.PENDING_BOOKING_TTL_MINUTES || '30', 10);
  return (Number.isFinite(minutes) && minutes > 0 ? minutes : 30) * 60 * 1000;
}

/**
 * Cancel stale pending bookings. Returns the number of tickets cancelled.
 *
 * A booking is considered stale when its payment is still 'pending' (not
 * 'pending_approval', which means an admin is reviewing a manual transfer),
 * uses a non-manual provider, and has not been touched (updatedAt) for
 * PENDING_BOOKING_TTL_MINUTES.
 */
export async function cleanupStalePendingBookings(): Promise<number> {
  const cutoff = toDbDate(new Date(Date.now() - getTtlMs()));

  const stale = await dbAll<{ ticketId: string | null; paymentId: string }>(
    (db as any)
      .select({
        ticketId: (payments as any).ticketId,
        paymentId: (payments as any).id,
      })
      .from(payments)
      .where(and(
        eq((payments as any).status, 'pending'),
        notInArray((payments as any).provider, [...MANUAL_PAYMENT_PROVIDERS]),
        lt((payments as any).updatedAt, cutoff)
      ))
  );

  if (stale.length === 0) return 0;

  const ticketIds = stale.map((s) => s.ticketId).filter((id): id is string => !!id);
  const paymentIds = stale.map((s) => s.paymentId);
  const now = getNow();

  let cancelledTickets = 0;
  if (ticketIds.length > 0) {
    const result: any = await (db as any)
      .update(tickets)
      .set({ status: 'cancelled' })
      .where(and(
        inArray((tickets as any).id, ticketIds),
        eq((tickets as any).status, 'pending')
      ));
    cancelledTickets = result?.changes ?? result?.rowCount ?? ticketIds.length;
  }

  await (db as any)
    .update(payments)
    .set({ status: 'failed', updatedAt: now })
    .where(inArray((payments as any).id, paymentIds));

  console.log(
    `[BookingCleanup] Expired ${stale.length} stale pending payment(s); ` +
    `cancelled ${cancelledTickets} ticket(s).`
  );
  return cancelledTickets;
}

let cleanupTimer: ReturnType<typeof setInterval> | null = null;

/**
 * Start a periodic cleanup of stale pending bookings. Each run is guarded by a
 * distributed lock so that, across multiple replicas, only one instance does
 * the work per interval.
 */
export function startBookingCleanup(): void {
  const intervalMs = parseInt(process.env.PENDING_BOOKING_CLEANUP_INTERVAL_MS || '300000', 10); // 5 min

  const run = () => {
    getLock()
      .withLock('cleanup-pending-bookings', Math.min(intervalMs, 60_000), () =>
        cleanupStalePendingBookings()
      )
      .catch((err) =>
        console.error('[BookingCleanup] Run failed:', err?.message || err)
      );
  };

  // Run shortly after startup, then on the interval.
  setTimeout(run, 30_000).unref?.();
  cleanupTimer = setInterval(run, intervalMs);
  cleanupTimer.unref?.();
  console.log(`[BookingCleanup] Scheduled every ${Math.round(intervalMs / 1000)}s`);
}

export function stopBookingCleanup(): void {
  if (cleanupTimer) {
    clearInterval(cleanupTimer);
    cleanupTimer = null;
  }
}
