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

// recordDeliveryEvent is invoked by channel adapters processing provider
// webhooks. The contract surface accepts the three webhook-driven event types
// that downstream providers report. The dispatcher does not consume webhooks
// — this is the "reserved port" for provider-webhook wiring.
export type RecordableEventType = "DELIVERED" | "BOUNCED" | "OPENED";

const ALLOWED_EVENT_TYPES: readonly RecordableEventType[] = ["DELIVERED", "BOUNCED", "OPENED"];

function isRecordableEventType(value: string): value is RecordableEventType {
  return (ALLOWED_EVENT_TYPES as readonly string[]).includes(value);
}

export interface RecordDeliveryEventInput {
  notificationId: string;
  eventType: string;
  occurredAt: string;
  errorClass?: string;
  errorDetail?: string;
}

function sanitizeErrorDetail(value: string): string {
  return value
    .replace(/(api[_-]?key|token|password|secret)=([^&\s]+)/gi, "$1=[REDACTED]")
    .slice(0, 2_000);
}

/**
 * Records a provider delivery event (DELIVERED/BOUNCED/OPENED) for a Notification: appends an audit row and advances deliveryStatus within its lifecycle. Adapter-permission authorized; append-only.
 */
export async function run(db: Transaction, input: RecordDeliveryEventInput, ctx: CommandContext) {
  void ctx;
  const { notificationId, eventType, occurredAt, errorClass, errorDetail } = input;

  if (!isRecordableEventType(eventType)) {
    return err(new InvalidEventTypeError(eventType));
  }
  if (!occurredAt) {
    return err(new MissingRequiredFieldError("occurredAt"));
  }
  if (eventType === "BOUNCED" && (!errorClass || !errorDetail)) {
    return err(new MissingRequiredFieldError("errorClass,errorDetail"));
  }

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

  if (!notification) {
    return err(new NotificationNotFoundError(notificationId));
  }

  const now = new Date();
  const occurredAtDate = new Date(occurredAt);

  const audit = await db
    .insertInto("NotificationDeliveryAudit")
    .values({
      id: crypto.randomUUID(),
      notificationId,
      eventType,
      occurredAt: occurredAtDate,
      occurredBy: "system",
      errorClass: errorClass ?? null,
      errorDetail: errorDetail ? sanitizeErrorDetail(errorDetail) : null,
      createdAt: now,
      updatedAt: now,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  // Notification.deliveryStatus transitions (per the ProviderBounce /
  // ProviderDelivered transitions in db/notification.lifecycle.generated.ts):
  //   DELIVERED advances SENT             -> DELIVERED
  //   BOUNCED   advances SENT | DELIVERED -> BOUNCED
  //   OPENED    is observability-only (no status change)
  // Out-of-lifecycle statuses (QUEUED, FAILED, BOUNCED) keep their status;
  // the audit row above is written regardless.
  if (eventType === "DELIVERED" && notification.deliveryStatus === "SENT") {
    await db
      .updateTable("Notification")
      .set({ deliveryStatus: "DELIVERED", updatedAt: now })
      .where("id", "=", notificationId)
      .execute();
  } else if (
    eventType === "BOUNCED" &&
    (notification.deliveryStatus === "SENT" || notification.deliveryStatus === "DELIVERED")
  ) {
    await db
      .updateTable("Notification")
      .set({ deliveryStatus: "BOUNCED", updatedAt: now })
      .where("id", "=", notificationId)
      .execute();
  }

  return ok({ audit });
}
