// Auto-reject unconfirmed payments once their event is over.
//
// After an event ends, any booking whose payment was never confirmed
// (still 'pending', 'pending_approval', or 'on_hold') can no longer be honored.
// This job silently fails those payments and cancels their tickets so they stop
// lingering as "pending" forever. It deliberately sends NO email — unlike the
// admin reject route, this is a housekeeping sweep and users are not notified.
//
// An event is considered over when COALESCE(end_datetime, start_datetime) is in
// the past. Updates are guarded by the current status so re-running is a no-op.

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

// Payment statuses that represent an unconfirmed booking.
const UNCONFIRMED_PAYMENT_STATUSES = ['pending', 'pending_approval', 'on_hold'];
// Ticket statuses that are still "live" (not already confirmed/checked-in/cancelled).
const ACTIVE_TICKET_STATUSES = ['pending', 'on_hold'];

/**
 * Fail unconfirmed payments (and cancel their tickets) for events that have
 * already ended. Returns the number of payments rejected.
 */
export async function rejectUnconfirmedPaymentsForEndedEvents(): Promise<number> {
  // Pull candidate rows first, then decide "ended" in JS so the comparison works
  // identically for SQLite (ISO text) and Postgres (timestamp) datetime columns.
  const rows = await dbAll<{
    paymentId: string;
    ticketId: string;
    endDatetime: string | Date | null;
    startDatetime: string | Date | null;
  }>(
    (db as any)
      .select({
        paymentId: (payments as any).id,
        ticketId: (tickets as any).id,
        endDatetime: (events as any).endDatetime,
        startDatetime: (events as any).startDatetime,
      })
      .from(payments)
      .innerJoin(tickets, eq((payments as any).ticketId, (tickets as any).id))
      .innerJoin(events, eq((tickets as any).eventId, (events as any).id))
      .where(and(
        inArray((payments as any).status, UNCONFIRMED_PAYMENT_STATUSES),
        inArray((tickets as any).status, ACTIVE_TICKET_STATUSES),
      ))
  );

  const nowMs = Date.now();
  const ended = rows.filter((r) => {
    const ref = r.endDatetime || r.startDatetime;
    if (!ref) return false;
    return new Date(ref as any).getTime() < nowMs;
  });

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

  const paymentIds = Array.from(new Set(ended.map((r) => r.paymentId)));
  const ticketIds = Array.from(new Set(ended.map((r) => r.ticketId).filter((id): id is string => !!id)));
  const now = getNow();

  // Fail the payments. The status guard keeps this idempotent and avoids
  // clobbering anything that changed since we read the candidates.
  await (db as any)
    .update(payments)
    .set({ status: 'failed', adminNote: 'Auto-rejected: event ended', updatedAt: now })
    .where(and(
      inArray((payments as any).id, paymentIds),
      inArray((payments as any).status, UNCONFIRMED_PAYMENT_STATUSES),
    ));

  // Cancel the associated tickets, freeing any seats they still hold.
  if (ticketIds.length > 0) {
    await (db as any)
      .update(tickets)
      .set({ status: 'cancelled' })
      .where(and(
        inArray((tickets as any).id, ticketIds),
        inArray((tickets as any).status, ACTIVE_TICKET_STATUSES),
      ));
  }

  console.log(
    `[EventEndSweep] Auto-rejected ${paymentIds.length} unconfirmed payment(s) for ended event(s); ` +
    `cancelled ${ticketIds.length} ticket(s).`
  );
  return paymentIds.length;
}

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

/**
 * Start a periodic sweep that auto-rejects unconfirmed payments for ended
 * events. Each run is guarded by a distributed lock so that, across multiple
 * replicas, only one instance does the work per interval.
 */
export function startEventEndSweep(): void {
  const intervalMs = parseInt(process.env.EVENT_END_SWEEP_INTERVAL_MS || '900000', 10); // 15 min

  const run = () => {
    getLock()
      .withLock('sweep-ended-event-payments', Math.min(intervalMs, 60_000), () =>
        rejectUnconfirmedPaymentsForEndedEvents()
      )
      .catch((err) =>
        console.error('[EventEndSweep] Run failed:', err?.message || err)
      );
  };

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

export function stopEventEndSweep(): void {
  if (sweepTimer) {
    clearInterval(sweepTimer);
    sweepTimer = null;
  }
}
