// Shared core: given a persisted PENDING NotificationEvent row, map it to the
// plan engine input, run `planNotificationInternal` (which only writes the
// outbox — Notification QUEUED + DestinationDeliveryLog PENDING rows), stamp the
// event's terminal status, and return the ids of the outbox rows that still need
// delivery. Used by both the CDC `dispatch-notification-events` executor
// (primary trigger) and the insurance re-drain cron, so the two share one
// mapping + status rule.

import { type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Transaction } from "../generated/kysely-tailordb";
import type { DispatchAdapters } from "../lib/dispatchAdapters";
import { parseNotificationEventPayload } from "../lib/events";
import { planNotificationInternal, type PlanNotificationInput } from "./planNotification";

export interface PersistedEventRow {
  id: string;
  eventType: string;
  sourceType: string;
  sourceId: string;
  actorUserId: string | null;
  payload: string;
  payloadHash: string;
  status: string;
}

export type PlanEventStatus = "DISPATCHED" | "NO_DELIVERY";

export interface PlanPersistedEventOutcome {
  status: PlanEventStatus;
  ok: boolean;
  /** Error code when planning failed (e.g. CATEGORY_NOT_FOUND). */
  reason?: string;
  /** QUEUED Notification ids to hand to the delivery worker. */
  notificationIds: string[];
  /** PENDING DestinationDeliveryLog ids to hand to the delivery worker. */
  destinationLogIds: string[];
}

/**
 * Plan one persisted event into outbox rows and stamp its terminal status.
 *
 * `idempotencyKey` is derived from the event's logical identity
 * `(eventType, sourceType, sourceId, payloadHash)` — not the event row id — so
 * plan dedup is stable across CDC re-fires and insurance re-drains AND converges
 * at the notification level even if duplicate event rows slip past the ingress
 * dedup (the same logical event maps to the same Notification rows). A planning
 * error (e.g. an `eventType` absent from the Event Catalog — an emitter contract
 * bug) is treated as `NO_DELIVERY` so the insurance cron does not re-plan it
 * forever; the error code is surfaced for diagnosis rather than silently
 * swallowed.
 */
export async function planPersistedEvent(
  db: Transaction,
  event: PersistedEventRow,
  adapters: DispatchAdapters,
  ctx: CommandContext,
  now: Date,
): Promise<PlanPersistedEventOutcome> {
  const payload = parseNotificationEventPayload(event.payload) ?? {};

  const input: PlanNotificationInput = {
    eventType: event.eventType,
    sourceType: event.sourceType,
    sourceId: event.sourceId,
    recipients: payload.recipients,
    actorUserId: event.actorUserId,
    notifySelf: payload.notifySelf,
    payloadVars: payload,
    idempotencyKey: `${event.eventType}:${event.sourceType}:${event.sourceId}:${event.payloadHash}`,
    locale: typeof payload.locale === "string" ? payload.locale : undefined,
  };

  const result = await planNotificationInternal(db, input, ctx, adapters);

  let status: PlanEventStatus;
  let reason: string | undefined;
  let notificationIds: string[] = [];
  let destinationLogIds: string[] = [];
  if (!result.ok) {
    status = "NO_DELIVERY";
    reason = result.error.code ?? "DISPATCH_FAILED";
  } else {
    const r = result.value;
    const produced = r.notifications.length + r.deduped.length + r.destinations.length > 0;
    status = produced ? "DISPATCHED" : "NO_DELIVERY";
    notificationIds = r.notifications.filter((n) => n.deliveryStatus === "QUEUED").map((n) => n.id);
    destinationLogIds = r.destinations.filter((d) => d.status === "PENDING").map((d) => d.logId);
  }

  await db
    .updateTable("NotificationEvent")
    .set({ status, dispatchedAt: now, updatedAt: now })
    .where("id", "=", event.id)
    .execute();

  return { status, ok: result.ok, reason, notificationIds, destinationLogIds };
}
