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 BulkPreferenceEntry {
  categoryId: string;
  channelId: string;
  allowed: boolean;
}

export interface BulkUpdateNotificationPreferencesInput {
  userId: string;
  entries: BulkPreferenceEntry[];
}

/**
 * Atomically upserts a caller's NotificationPreference matrix in one transaction, validating each entry's category, channel, and opt-out eligibility before any write.
 */
export async function run(
  db: Transaction,
  input: BulkUpdateNotificationPreferencesInput,
  ctx: CommandContext,
) {
  const { userId, entries } = input;

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

  if (!entries || entries.length === 0) {
    return err(new MissingRequiredFieldError("entries"));
  }
  for (const [index, entry] of entries.entries()) {
    if (!entry.categoryId || !entry.channelId || typeof entry.allowed !== "boolean") {
      return err(new MissingRequiredFieldError(`entries[${index}]`));
    }
  }

  // Pre-flight: batch-load all unique categories and channels, then validate.
  const uniqueCategoryIds = new Set(entries.map((e) => e.categoryId));
  const uniqueChannelIds = new Set(entries.map((e) => e.channelId));

  const [categories, channels] = await Promise.all([
    db
      .selectFrom("NotificationCategory")
      .selectAll()
      .where("id", "in", Array.from(uniqueCategoryIds))
      .execute(),
    db
      .selectFrom("NotificationChannel")
      .selectAll()
      .where("id", "in", Array.from(uniqueChannelIds))
      .execute(),
  ]);

  const categoryById = new Map(categories.map((c) => [c.id, c]));
  const channelById = new Map(channels.map((ch) => [ch.id, ch]));

  for (const entry of entries) {
    const category = categoryById.get(entry.categoryId);
    if (!category) {
      return err(new CategoryNotFoundError(entry.categoryId));
    }

    const channel = channelById.get(entry.channelId);
    if (!channel) {
      return err(new ChannelNotFoundError(entry.channelId));
    }

    const wantsOptOut = !entry.allowed;
    if (!category.optOutAllowed && wantsOptOut) {
      return err(new CategoryNotOptOutAbleError(entry.categoryId));
    }
  }

  // All entries valid: upsert each row in the same transaction.
  const now = new Date();
  const preferences: unknown[] = [];

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

    if (existing) {
      const updated = await db
        .updateTable("NotificationPreference")
        .set({
          allowed: entry.allowed,
          updatedAt: now,
        })
        .where("id", "=", existing.id)
        .returningAll()
        .executeTakeFirstOrThrow();
      preferences.push(updated);
    } else {
      const inserted = await db
        .insertInto("NotificationPreference")
        .values({
          id: crypto.randomUUID(),
          userId,
          categoryId: entry.categoryId,
          channelId: entry.channelId,
          allowed: entry.allowed,
        })
        .returningAll()
        .executeTakeFirstOrThrow();
      preferences.push(inserted);
    }
  }

  return ok({ preferences });
}
