/**
 * Cron: Group Hold Refresh — capture or release before 7-day Stripe expiry.
 *
 * Stripe manual-capture holds expire 7 days after creation. This cron runs
 * daily and processes reservations whose holdExpiresAt is within 24 hours:
 *
 *   - THRESHOLD_MET group deal → captureHold (deal succeeded, charge participants)
 *   - All other states          → releaseHold + emit group_didnt_fill outbox event
 *
 * Batch size capped at 100 per invocation (Workers CPU limit).
 */

import { createDbService } from '@/server/services/db.js';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import { getPaymentProvider } from '../payments/get-provider.js';
import * as groupReservationQueries from '../db/queries/group-reservations.js';
import * as groupDealQueries from '../db/queries/group-deals.js';
import { insertOutboxRow } from '../db/queries/outbox.js';
import { enqueueOutbox } from '../queues/outbox-producer.js';
import type { CronEnv } from './deal-expiry.js';

const BATCH_SIZE = 100;
/** Process holds expiring within this many hours from now. */
const EXPIRY_WINDOW_HOURS = 24;

export const runGroupHoldRefresh = withSentry(
  async function runGroupHoldRefresh(env: CronEnv): Promise<void> {
    const db = env.db ?? createDbService({ DATABASE_URL: env.DATABASE_URL });
    const threshold = new Date(Date.now() + EXPIRY_WINDOW_HOURS * 60 * 60 * 1000);

    const expiring = await groupReservationQueries.findHoldsExpiringBefore(db, threshold);

    const batch = expiring.slice(0, BATCH_SIZE);
    for (const reservation of batch) {
      if (!reservation.providerAuthorizationId) {
        // No hold on file — mark released so it doesn't re-surface.
        await groupReservationQueries.updateStatus(db, reservation.id, 'RELEASED');
        continue;
      }

      // Look up group deal to decide capture vs release.
      const groupDeal = await groupDealQueries.findById(db, reservation.groupDealId);

      if (groupDeal?.groupState === 'THRESHOLD_MET') {
        // Deal reached threshold — capture before hold expires.
        const prePurchaseId = crypto.randomUUID();
        const totalAgorot = Math.round(parseFloat(reservation.totalAmount) * 100);

        try {
          const captureResult = await (
            await getPaymentProvider(env)
          ).captureHold({
            reservationId: reservation.id,
            purchaseId: prePurchaseId,
            providerHoldId: reservation.providerAuthorizationId,
            totalAgorot,
          });

          if (captureResult.ok) {
            await groupReservationQueries.setCaptured(db, reservation.id, {
              providerTransactionId: captureResult.providerPaymentId,
              orderLineId: prePurchaseId,
            });
          } else {
            captureCaught(
              new Error(`captureHold failed: ${captureResult.code} — ${captureResult.message}`),
              {
                scope: 'server.cron.group-hold-refresh',
                severity: 'error',
                extra: {
                  reservationId: reservation.id,
                  groupDealId: reservation.groupDealId,
                  code: captureResult.code,
                },
              },
            );
          }
        } catch (err) {
          captureCaught(err, {
            scope: 'server.cron.group-hold-refresh',
            severity: 'error',
            extra: { reservationId: reservation.id, groupDealId: reservation.groupDealId },
          });
        }
      } else {
        // Deal did not reach threshold (or group deal not found) — release hold.
        try {
          await (
            await getPaymentProvider(env)
          ).releaseHold({
            reservationId: reservation.id,
            providerHoldId: reservation.providerAuthorizationId,
          });
        } catch (err) {
          captureCaught(err, {
            scope: 'server.cron.group-hold-refresh',
            severity: 'warning',
            extra: { reservationId: reservation.id, step: 'releaseHold' },
          });
          // Continue to mark released in DB even if Stripe release failed
          // (idempotent — hold will expire naturally).
        }

        await groupReservationQueries.updateStatus(db, reservation.id, 'RELEASED', {
          providerAuthorizationId: null,
          holdExpiresAt: null,
        });

        // Emit group_didnt_fill outbox event for downstream notifications (refund email, stats).
        try {
          const { id: outboxId } = await insertOutboxRow(db, {
            aggregateType: 'group_reservation',
            aggregateId: reservation.id,
            eventType: 'group_didnt_fill',
            payload: {
              reservationId: reservation.id,
              groupDealId: reservation.groupDealId,
              userId: reservation.userId ?? null,
              guestEmail: reservation.guestEmail ?? null,
            },
          });
          await enqueueOutbox(outboxId);
        } catch (err) {
          captureCaught(err, {
            scope: 'server.cron.group-hold-refresh',
            severity: 'warning',
            extra: { reservationId: reservation.id, step: 'outbox-group_didnt_fill' },
          });
        }
      }
    }
  },
  { name: 'cron.group-hold-refresh', kind: 'cron' },
);
