import { err, ok, type CommandContext } from "@tailor-platform/erp-kit/core";
import type { Transaction } from "../generated/kysely-tailordb";
import { NotDeliveredError, NotificationNotFoundError } from "../lib/errors.generated";

export interface MarkNotificationAsReadInput {
  notificationId: string;
}

/**
 * Adds READ (and SEEN) to a single caller-owned, deliverable Notification, stamping readAt and writing a READ audit row. Idempotent.
 */
export async function run(
  db: Transaction,
  input: MarkNotificationAsReadInput,
  ctx: CommandContext,
) {
  const { notificationId } = input;

  const notification = await db
    .selectFrom("Notification")
    .selectAll()
    .where("id", "=", notificationId)
    .forUpdate()
    .executeTakeFirst();

  // A notification owned by another user is reported exactly like a missing
  // one so callers cannot enumerate foreign notification ids.
  if (!notification || notification.recipientUserId !== ctx.actorId) {
    return err(new NotificationNotFoundError(notificationId));
  }

  if (notification.deliveryStatus !== "SENT" && notification.deliveryStatus !== "DELIVERED") {
    return err(new NotDeliveredError(notificationId));
  }

  // Idempotent: row already READ -> return unchanged, do not re-stamp readAt
  if (notification.engagementStatuses.includes("READ")) {
    return ok({ notification });
  }

  // Archived rows are terminal for engagement writes: return unchanged
  // (idempotent), mirroring the ARCHIVED skip in markAllNotificationsAsRead.
  if (notification.engagementStatuses.includes("ARCHIVED")) {
    return ok({ notification });
  }

  const now = new Date();
  const seenAlreadyPresent = notification.engagementStatuses.includes("SEEN");

  // Build the next set: monotonic add of SEEN (if absent) and READ
  const nextStatuses = [...notification.engagementStatuses];
  if (!seenAlreadyPresent) nextStatuses.push("SEEN");
  nextStatuses.push("READ");

  const updated = await db
    .updateTable("Notification")
    .set({
      engagementStatuses: nextStatuses,
      // Stamp seenAt only on first SEEN addition; preserve otherwise
      seenAt: seenAlreadyPresent ? notification.seenAt : now,
      readAt: now,
      updatedAt: now,
    })
    .where("id", "=", notificationId)
    .returningAll()
    .executeTakeFirstOrThrow();

  await db
    .insertInto("NotificationDeliveryAudit")
    .values({
      id: crypto.randomUUID(),
      notificationId,
      eventType: "READ",
      occurredAt: now,
      occurredBy: notification.recipientUserId,
      errorClass: null,
      errorDetail: null,
      createdAt: now,
      updatedAt: now,
    })
    .execute();

  return ok({ notification: updated });
}
