import pick from 'lodash/pick';
import type { LiteralUnion } from 'type-fest';

import type { EventType } from '../store/models';
import { Event } from '../store/models/event';
import { events } from './event';
import { context } from './context';

const API_VERSION = '2023-09-05';

/**
 * Invoke every registered listener for `eventName` and await any Promise
 * results. EventEmitter.emit() returns sync — listener async work would
 * otherwise run as detached microtasks and die when the CF Workers handler
 * unwinds. We want listeners (notably queues/event.ts's event.created
 * handler that drives webhook delivery) to complete inside createEvent's
 * Promise so waitUntil covers the whole chain.
 */
async function emitAndAwait(eventName: string, ...args: any[]): Promise<void> {
  const listeners = events.rawListeners(eventName);
  for (const listener of listeners) {
    try {
      const result = (listener as any)(...args);
      if (result && typeof result.then === 'function') {
        // eslint-disable-next-line no-await-in-loop -- sequential await keeps listener ordering deterministic for waitUntil chains
        await result;
      }
    } catch (err: any) {
      console.error('[audit emitAndAwait]', eventName, err?.message || err);
    }
  }
}

export function createEvent(
  scope: string,
  type: LiteralUnion<EventType, string>,
  model: any,
  options: any = {}
): Promise<void> {
  // Context-aware tracking for CF Workers:
  // - HTTP requests: use ctx.waitUntil (non-blocking, like Blocklet's fire-and-forget)
  // - Queue consumer/Cron: use __cfPendingJobs__ (blocking, listeners must complete)
  // - Blocklet Server: no tracking needed (long-lived process)
  const promise = doCreateEvent(scope, type, model, options);
  const isHttp = (globalThis as any).__cfHttpContext__;
  if (isHttp) {
    const waitUntil = (globalThis as any).__cfWaitUntil__;
    if (typeof waitUntil === 'function') {
      waitUntil(promise.catch((err: any) => console.error('[createEvent]', type, err?.message || err)));
    }
  } else {
    const pending = (globalThis as any).__cfPendingJobs__;
    if (Array.isArray(pending)) {
      pending.push(promise.catch((err: any) => console.error('[createEvent]', type, err?.message || err)));
    }
  }
  return promise;
}

async function doCreateEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any = {}) {
  const data: any = {
    object: model.dataValues,
  };
  if (type.endsWith('updated')) {
    data.previous_attributes = pick(model._previousDataValues, options.fields);
  }

  const event = await Event.create({
    type,
    api_version: API_VERSION,
    livemode: !!model.livemode,
    object_id: model.id,
    object_type: scope,
    data,
    request: {
      id: '',
      idempotency_key: '',
      requested_by: options.requestedBy || context.getRequestedBy() || 'system',
    },
    metadata: {},
    pending_webhooks: 99,
  });

  // Synchronously await listener chains so the HTTP handler's waitUntil
  // scope covers the full handleEvent → addWebhookJob → push pipeline.
  // EventEmitter.emit() returns sync and discards listener Promises — on
  // CF Workers that leaves the listener microtask racing against worker
  // termination, and `customer.subscription.started` / `.deleted` events
  // fired at the tail of HTTP handlers were observed to lose their first
  // delivery attempt because of it.
  await emitAndAwait('event.created', { id: event.id });
  await emitAndAwait(event.type, data.object, options);
}

export async function createStatusEvent(
  scope: string,
  prefix: string,
  config: Record<string, string>,
  model: any,
  options: any = {}
) {
  if (options.fields.includes('status') === false) {
    return;
  }

  const data: any = {
    object: model.dataValues,
    previous_attributes: pick(model._previousDataValues, options.fields),
  };

  if (!config[data.object.status]) {
    return;
  }

  const suffix = config[data.object.status];
  const event = await Event.create({
    type: [prefix, suffix].join('.'),
    api_version: API_VERSION,
    livemode: !!model.livemode,
    object_id: model.id,
    object_type: scope,
    data,
    request: {
      id: '',
      idempotency_key: '',
      requested_by: options.requestedBy || context.getRequestedBy() || 'system',
    },
    metadata: {},
    pending_webhooks: 99,
  });

  await emitAndAwait('event.created', { id: event.id });
  await emitAndAwait(event.type, data.object);
}

export async function createCustomEvent(
  scope: string,
  prefix: string,
  type: (current: any, previous: any) => string,
  model: any,
  options: any
) {
  const data: any = {
    object: model.dataValues,
    previous_attributes: pick(model._previousDataValues, options.fields),
  };

  const suffix = type(data.object, data.previous_attributes);
  if (!suffix) {
    return;
  }

  const event = await Event.create({
    type: [prefix, suffix].join('.'),
    api_version: API_VERSION,
    livemode: !!model.livemode,
    object_id: model.id,
    object_type: scope,
    data,
    request: {
      id: '',
      idempotency_key: '',
      requested_by: options.requestedBy || context.getRequestedBy() || 'system',
    },
    metadata: {},
    pending_webhooks: 99,
  });

  await emitAndAwait('event.created', { id: event.id });
  await emitAndAwait(event.type, data.object);
}

/**
 * 创建自定义事件，无需依赖模型对象
 */
export async function createFlexibleEvent(
  type: string,
  objectType: string,
  objectId: string,
  data: Record<string, any>,
  options: {
    livemode?: boolean;
    requestedBy?: string;
    metadata?: Record<string, any>;
  } = {}
) {
  const { livemode = false, requestedBy, metadata = {} } = options;

  const event = await Event.create({
    type,
    api_version: API_VERSION,
    livemode,
    object_id: objectId,
    object_type: objectType,
    data,
    request: {
      id: '',
      idempotency_key: '',
      requested_by: requestedBy || context.getRequestedBy() || 'system',
    },
    metadata,
    pending_webhooks: 99,
  });

  await emitAndAwait('event.created', { id: event.id });
  await emitAndAwait(type, data);
  return event;
}
