import { Op } from 'sequelize';

import { events } from '../libs/event';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { Event } from '../store/models/event';
import { WebhookAttempt } from '../store/models/webhook-attempt';
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
import { addWebhookJob } from './webhook';

type EventJob = {
  eventId: string;
};

export const handleEvent = async (job: EventJob) => {
  logger.info('Starting to handle event', job);

  const event = await Event.findByPk(job.eventId);
  if (!event) {
    logger.warn('Event not found', job);
    return;
  }

  if (!event.pending_webhooks) {
    logger.warn('Event already processed', job);
    return;
  }

  const webhooks = await WebhookEndpoint.findAll({ where: { status: 'enabled', livemode: event.livemode } });
  const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
  if (eventWebhooks.length === 0) {
    logger.info('no webhook endpoint for event', job);
    await event.update({ pending_webhooks: 0 });
    return;
  }

  // Decide which endpoints still need a first attempt. The previous logic
  // counted only SUCCESS attempts (2xx), so any permanent-failure endpoint
  // (e.g. wrong URL → 404) would forever match attemptCount===0 and get
  // re-pushed by the retry cron every minute, producing thousands of bogus
  // attempts. We now count ANY attempt: webhookQueue has its own retry
  // ladder (MAX_RETRY_COUNT=20) that owns recovery for transient failures,
  // so re-pushing from this handler after the first attempt is double-work.
  let stillPending = 0;
  for (const webhook of eventWebhooks) {
    // eslint-disable-next-line no-await-in-loop -- sequential per-webhook scheduling keeps D1 writes ordered
    const anyAttempt = await WebhookAttempt.count({
      where: { event_id: event.id, webhook_endpoint_id: webhook.id },
    });
    if (anyAttempt === 0) {
      stillPending += 1;
      logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
      // persist=true: write a D1 jobs row so CF Queue transport failures
      // (momentary unavailability, consumer skip) don't silently lose the
      // delivery — cron picks orphaned rows back up.
      // eslint-disable-next-line no-await-in-loop -- same reason as above
      await addWebhookJob(event.id, webhook.id, { persist: true });
    }
  }

  // pending_webhooks reflects "endpoints that have not yet been attempted at
  // all" — once all matched endpoints have any attempt row, the cron's
  // pending>0 scan stops finding this event and it falls out of rotation.
  await event.update({ pending_webhooks: stillPending });
  logger.info(`Finished handling event ${job.eventId} (stillPending=${stillPending})`);
};

export const eventQueue = createQueue<EventJob>({
  name: 'event',
  onJob: handleEvent,
  options: {
    concurrency: 5,
    maxRetries: 3,
  },
});

export const startEventQueue = async () => {
  const docs = await Event.findAll({
    where: {
      pending_webhooks: { [Op.gt]: 0 },
    },
    attributes: ['id'],
  });

  logger.info(`Found ${docs.length} events with pending webhooks`);

  docs.forEach(async (x) => {
    const exist = await eventQueue.get(x.id);
    if (!exist) {
      logger.info(`Pushing event ${x.id} to queue`);
      eventQueue.push({ id: x.id, job: { eventId: x.id }, persist: false });
    }
  });

  logger.info('Finished starting event queue');
};

eventQueue.on('failed', ({ id, job, error }) => {
  logger.error('event job failed', { id, job, error });
});

events.on('event.created', async (event) => {
  if ((globalThis as any).__CF_ENV__) {
    // CF Workers: execute inline to save 2 CF Queue ops per event.
    // eventQueue only dispatches webhooks — lightweight DB lookup + webhookQueue.push.
    // Webhook delivery still goes through webhookQueue with full retry guarantees.
    try {
      await handleEvent({ eventId: event.id });
    } catch (err: any) {
      logger.error('event inline handler failed', { eventId: event.id, error: err?.message });
    }
  } else {
    // Blocklet Server: use queue as before
    eventQueue.push({ id: event.id, job: { eventId: event.id }, persist: false });
  }
});
