import type { CommandContext } from "@tailor-platform/erp-kit/core";
import { logger } from "@tailor-platform/function-logger";
import type { AuthInvoker, MachineUserName, TailorAnyDBType } from "@tailor-platform/sdk";
import { createExecutor, recordCreatedTrigger } from "@tailor-platform/sdk";
import type { DB, Transaction } from "../generated/kysely-tailordb";
import { deliverPlanned } from "../internal/drainDeliveries";
import { planPersistedEvent, type PersistedEventRow } from "../internal/planEvent";
import type { DispatchAdapters } from "../lib/dispatchAdapters";
import { permissions } from "../lib/permissions.generated";

type ExecutorAuthInvoker = AuthInvoker<string> | MachineUserName;

/**
 * Primary trigger for delivery: a CDC `recordCreatedTrigger` on NotificationEvent.
 *
 * Each newly inserted PENDING event fires this executor, which re-reads the row
 * `forUpdate`, guards on `status = PENDING` (at-least-once CDC safety), and runs
 * the plan engine via the shared `planPersistedEvent` helper inside the
 * transaction, stamping the event DISPATCHED / NO_DELIVERY. After the plan
 * commits, the outbox rows it produced are delivered OUTSIDE the transaction so
 * a slow or failing provider can neither hold the row locks nor roll the plan
 * back; a crash before delivery leaves the rows for the redrain cron.
 *
 * App injects `notificationEventType` (the registered db type the trigger binds
 * to), `getDB` (the shared Kysely namespace), and the composed `adapters` (the
 * same instances dispatchNotification uses).
 */
export interface DispatchNotificationEventsParams {
  /** The registered NotificationEvent db type the CDC trigger binds to. */
  notificationEventType: TailorAnyDBType;
  /** Returns the app-side Kysely instance for the shared DB. */
  getDB: () => DB;
  /** Composed channel adapters (same instances used by dispatchNotification). */
  adapters: DispatchAdapters;
  /** Machine user identity used to invoke the executor. */
  authInvoker?: ExecutorAuthInvoker;
}

export function dispatchNotificationEvents(params: DispatchNotificationEventsParams) {
  return createExecutor({
    name: "dispatch-notification-events",
    description:
      "Dispatches a NotificationEvent the moment it is created (CDC), invoking dispatchNotification and stamping the event DISPATCHED/NO_DELIVERY",
    // oxlint-disable-next-line typescript/no-unsafe-type-assertion
    trigger: recordCreatedTrigger({ type: params.notificationEventType as never }),
    operation: {
      kind: "function",
      authInvoker: params.authInvoker,
      // The CDC payload carries the new row; only the id is needed since the
      // executor re-reads the row forUpdate to take the PENDING-guard lock.
      body: async (args: { newRecord: { id: string } }) => {
        const eventId = args.newRecord.id;
        const db = params.getDB();
        const now = new Date();
        const ctx: CommandContext = {
          actorId: "notification-dispatch-runner",
          permissions: [permissions.ingest.logNotificationEvent],
        };

        try {
          const outcome = await db.transaction().execute(async (trx: Transaction) => {
            const event = (await trx
              .selectFrom("NotificationEvent")
              .selectAll()
              .where("id", "=", eventId)
              .forUpdate()
              .executeTakeFirst()) as PersistedEventRow | undefined;

            // Already planned (at-least-once re-fire) or vanished: no-op.
            if (!event || event.status !== "PENDING") return undefined;

            return planPersistedEvent(trx, event, params.adapters, ctx, now);
          });
          if (outcome) {
            // The plan committed: deliver its outbox rows outside the
            // transaction. Per-row failures leave QUEUED/PENDING rows for the
            // redrain cron rather than reverting the plan.
            await deliverPlanned(db, outcome, params.adapters, now);
            // Surface why nothing was delivered (emitter contract bug, empty
            // audience, all recipients muted) instead of silently stamping it.
            if (outcome.status === "NO_DELIVERY") {
              logger.warn(
                `[dispatch-notification-events] event ${eventId} stamped NO_DELIVERY (reason: ${outcome.reason ?? "no deliverable recipients or destinations"})`,
              );
            }
          }
        } catch (error) {
          logger.error(`[dispatch-notification-events] failed to dispatch event ${eventId}`, error);
          throw error;
        }
      },
    },
  });
}
