// PERSONAL delivery worker. Given a QUEUED Notification outbox row, resolve its
// channel and recipient address, invoke the channel adapter OUTSIDE any
// transaction, then record the terminal status and audit trail in a short
// transaction. The adapter is handed the row id as an idempotency key so a
// redrain re-send of the same row collapses to a single provider-side delivery.

import type { DB, Transaction } from "../generated/kysely-tailordb";
import type { DispatchAdapters } from "../lib/dispatchAdapters";
import { loadChannelById } from "../lib/dispatchQueries";

interface NotificationDeliveryRow {
  id: string;
  recipientUserId: string;
  channelId: string;
  subject: string;
  body: string;
  htmlBody: string | null;
  deliveryStatus: string;
}

export interface DeliverNotificationOutcome {
  deliveryStatus: "SENT" | "DELIVERED" | "FAILED" | "SKIPPED";
  reason?: string;
}

type AuditEventType = "SENT" | "DELIVERED" | "FAILED";

async function writeAudit(
  db: Transaction,
  notificationId: string,
  eventType: AuditEventType,
  now: Date,
  errorClass: string | null = null,
  errorDetail: string | null = null,
) {
  await db
    .insertInto("NotificationDeliveryAudit")
    .values({
      id: crypto.randomUUID(),
      notificationId,
      eventType,
      occurredAt: now,
      occurredBy: "system",
      errorClass,
      errorDetail,
      createdAt: now,
      updatedAt: now,
    })
    .execute();
}

// Each transition is conditional on the expected prior status so a concurrent
// worker that already advanced the row is never overwritten.
async function advance(
  db: Transaction,
  notificationId: string,
  patch: Record<string, unknown>,
  now: Date,
  fromStatus: "QUEUED" | "SENT",
) {
  await db
    .updateTable("Notification")
    .set({ ...patch, updatedAt: now })
    .where("id", "=", notificationId)
    .where("deliveryStatus", "=", fromStatus)
    .execute();
}

async function recordFailure(
  db: DB,
  notificationId: string,
  now: Date,
  errorClass: string,
  errorDetail: string,
): Promise<DeliverNotificationOutcome> {
  await db.transaction().execute(async (trx) => {
    await advance(trx, notificationId, { deliveryStatus: "FAILED" }, now, "QUEUED");
    await writeAudit(trx, notificationId, "FAILED", now, errorClass, errorDetail);
  });
  return { deliveryStatus: "FAILED", reason: "CHANNEL_ADAPTER_FAILED" };
}

export async function deliverNotification(
  db: DB,
  notificationId: string,
  adapters: DispatchAdapters,
  now: Date,
): Promise<DeliverNotificationOutcome> {
  const row = (await db
    .selectFrom("Notification")
    .selectAll()
    .where("id", "=", notificationId)
    .executeTakeFirst()) as NotificationDeliveryRow | undefined;
  if (!row || row.deliveryStatus !== "QUEUED") return { deliveryStatus: "SKIPPED" };

  const channel = await loadChannelById(db, row.channelId);
  const channelKey = channel?.channelId;
  if (!channel || !channel.enabled || channel.kind === "DESTINATION" || !channelKey) {
    return recordFailure(db, row.id, now, "ChannelUnavailable", channelKey ?? row.channelId);
  }

  const profile = await adapters.resolveRecipient(db, row.recipientUserId);
  const recipientAddress = profile?.addresses[channelKey] ?? null;
  if (recipientAddress === null) {
    return recordFailure(db, row.id, now, "MissingRecipientAddress", channelKey);
  }

  if (channelKey === "IN_APP") {
    const sent = await adapters.inApp.send({
      notificationId: row.id,
      idempotencyKey: row.id,
      recipientAddress,
      subject: row.subject,
      body: row.body,
    });
    if (!sent.ok) {
      return recordFailure(db, row.id, now, sent.errorClass, sent.errorDetail);
    }
    await db.transaction().execute(async (trx) => {
      await advance(trx, row.id, { deliveryStatus: "SENT" }, now, "QUEUED");
      await writeAudit(trx, row.id, "SENT", now);
      await advance(trx, row.id, { deliveryStatus: "DELIVERED" }, now, "SENT");
      await writeAudit(trx, row.id, "DELIVERED", now);
    });
    return { deliveryStatus: "DELIVERED" };
  }

  if (channelKey === "EMAIL") {
    if (!adapters.email) {
      return recordFailure(db, row.id, now, "ChannelAdapterNotConfigured", "EMAIL");
    }
    const sent = await adapters.email.send({
      notificationId: row.id,
      idempotencyKey: row.id,
      recipientAddress,
      subject: row.subject,
      body: row.body,
      htmlBody: row.htmlBody,
    });
    if (!sent.ok) {
      return recordFailure(db, row.id, now, sent.errorClass, sent.errorDetail);
    }
    await db.transaction().execute(async (trx) => {
      await advance(
        trx,
        row.id,
        { deliveryStatus: "SENT", adapterMessageId: sent.messageId },
        now,
        "QUEUED",
      );
      await writeAudit(trx, row.id, "SENT", now);
    });
    return { deliveryStatus: "SENT" };
  }

  // A PERSONAL channel with no wired adapter can never succeed; fail it so the
  // redrain worker stops retrying.
  return recordFailure(db, row.id, now, "ChannelAdapterNotConfigured", channelKey);
}
