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

export interface UpdateNotificationPreferenceInput {
  userId: string;
  categoryId: string;
  channelId: string;
  allowed: boolean;
}

/**
 * Upserts a single caller NotificationPreference row after validating self-service access, category/channel existence, and opt-out eligibility.
 */
export async function run(
  db: Transaction,
  input: UpdateNotificationPreferenceInput,
  ctx: CommandContext,
) {
  const { userId, categoryId, channelId, allowed } = input;

  // Required-field validation precedes the authz/existence checks so a blank key
  // is reported as MISSING_REQUIRED_FIELD rather than leaking out as FORBIDDEN
  // (blank userId) or CATEGORY_NOT_FOUND / CHANNEL_NOT_FOUND (blank id).
  if (typeof userId !== "string" || userId.trim() === "") {
    return err(new MissingRequiredFieldError("userId"));
  }
  if (typeof categoryId !== "string" || categoryId.trim() === "") {
    return err(new MissingRequiredFieldError("categoryId"));
  }
  if (typeof channelId !== "string" || channelId.trim() === "") {
    return err(new MissingRequiredFieldError("channelId"));
  }
  if (typeof allowed !== "boolean") {
    return err(new MissingRequiredFieldError("allowed"));
  }

  if (ctx.actorId !== userId) {
    return err(new ForbiddenError(userId));
  }

  const category = await db
    .selectFrom("NotificationCategory")
    .selectAll()
    .where("id", "=", categoryId)
    .executeTakeFirst();

  if (!category) {
    return err(new CategoryNotFoundError(categoryId));
  }

  const channel = await db
    .selectFrom("NotificationChannel")
    .selectAll()
    .where("id", "=", channelId)
    .executeTakeFirst();

  if (!channel) {
    return err(new ChannelNotFoundError(channelId));
  }

  // Transactional invariant: opt-out forbidden on optOutAllowed=false categories.
  if (!category.optOutAllowed && !allowed) {
    return err(new CategoryNotOptOutAbleError(categoryId));
  }

  const existing = await db
    .selectFrom("NotificationPreference")
    .selectAll()
    .where("userId", "=", userId)
    .where("categoryId", "=", categoryId)
    .where("channelId", "=", channelId)
    .forUpdate()
    .executeTakeFirst();

  const now = new Date();

  if (existing) {
    const updated = await db
      .updateTable("NotificationPreference")
      .set({ allowed, updatedAt: now })
      .where("id", "=", existing.id)
      .returningAll()
      .executeTakeFirstOrThrow();

    return ok({ preference: updated });
  }

  const inserted = await db
    .insertInto("NotificationPreference")
    .values({
      id: crypto.randomUUID(),
      userId,
      categoryId,
      channelId,
      allowed,
    })
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ preference: inserted });
}
