import prisma from '$lib/server/database';

import {
  type WorkBookPlacement,
  type WorkBookPlacements,
  type WorkbooksWithPlacement,
  type PlacementInputs,
  type PlacementCreates,
} from '$features/workbooks/types/workbook_placement';

import { WorkBookType } from '$features/workbooks/types/workbook';

import {
  buildTaskMapFromCurriculumRows,
  buildCurriculumWorkbooksForInit,
  initializeCurriculumPlacements,
  initializeSolutionPlacements,
} from './initializers';

// --- Basic CRUD operations for placements ---

/**
 * Returns all CURRICULUM and SOLUTION workbooks with their placements, ordered by id.
 */
export async function getWorkbooksWithPlacements(): Promise<WorkbooksWithPlacement> {
  return prisma.workBook.findMany({
    where: { workBookType: { in: [WorkBookType.CURRICULUM, WorkBookType.SOLUTION] } },
    include: { placement: true },
    orderBy: { id: 'asc' },
  });
}

/**
 * Returns all placements for workbooks of the given type, ordered by priority.
 */
export async function getPlacementsByWorkBookType(
  workBookType: typeof WorkBookType.CURRICULUM | typeof WorkBookType.SOLUTION,
): Promise<WorkBookPlacements> {
  return prisma.workBookPlacement.findMany({
    where: { workBook: { workBookType } },
    orderBy: { priority: 'asc' },
  });
}

/**
 * Updates existing placements in a single transaction.
 * No-op when given an empty array.
 *
 * Intentional N+1: one UPDATE per row. Acceptable for an infrequent admin-only
 * operation; raw SQL bulk updates are not worth the type-safety trade-off here.
 */
export async function updateWorkBookPlacements(updatedPlacements: PlacementInputs): Promise<void> {
  if (updatedPlacements.length === 0) {
    return;
  }

  await prisma.$transaction(
    updatedPlacements.map((updatedPlacement) =>
      prisma.workBookPlacement.update({
        where: { id: updatedPlacement.id },
        data: {
          priority: updatedPlacement.priority,
          taskGrade: updatedPlacement.taskGrade ?? null,
          solutionCategory: updatedPlacement.solutionCategory ?? null,
        },
      }),
    ),
  );
}

/**
 * Queries all unplaced CURRICULUM and SOLUTION workbooks, computes their initial
 * placements, and writes them to the database in a single createMany call.
 * No-op when all workbooks are already placed.
 */
export async function createInitialPlacements(): Promise<void> {
  const { unplacedCurriculum, unplacedSolution } = await fetchUnplacedWorkbooks();

  if (unplacedCurriculum.length === 0 && unplacedSolution.length === 0) {
    return;
  }

  const tasksMapByIds = buildTaskMapFromCurriculumRows(unplacedCurriculum);
  const curriculumWorkbooksForInit = buildCurriculumWorkbooksForInit(unplacedCurriculum);

  const curriculumPlacements = initializeCurriculumPlacements(
    curriculumWorkbooksForInit,
    tasksMapByIds,
  );
  const solutionPlacements = initializeSolutionPlacements(unplacedSolution);

  await prisma.workBookPlacement.createMany({
    data: [...curriculumPlacements, ...solutionPlacements],
    skipDuplicates: true,
  });
}

async function fetchUnplacedWorkbooks() {
  const [unplacedCurriculum, unplacedSolution] = await Promise.all([
    prisma.workBook.findMany({
      where: { workBookType: WorkBookType.CURRICULUM, placement: null },
      include: {
        workBookTasks: {
          include: { task: { select: { task_id: true, grade: true } } },
        },
      },
      orderBy: { id: 'asc' },
    }),
    prisma.workBook.findMany({
      where: { workBookType: WorkBookType.SOLUTION, placement: null },
      orderBy: { id: 'asc' },
    }),
  ]);

  return { unplacedCurriculum, unplacedSolution };
}

/**
 * Validates that no update crosses CURRICULUM/SOLUTION boundary, then upserts.
 * Returns { error } on validation failure, null on success.
 */
export async function validateAndUpdatePlacements(
  updates: PlacementInputs,
): Promise<{ error: string } | null> {
  const validationError = await validatePlacements(updates);

  if (validationError) {
    return validationError;
  }

  await updateWorkBookPlacements(updates);

  return null;
}

async function validatePlacements(updates: PlacementInputs): Promise<{ error: string } | null> {
  const ids = updates.map((update) => update.id);
  const existingPlacements = await prisma.workBookPlacement.findMany({
    where: { id: { in: ids } },
    include: { workBook: { select: { workBookType: true } } },
  });

  const existingById = new Map(existingPlacements.map((placement) => [placement.id, placement]));

  for (const update of updates) {
    const existing = existingById.get(update.id);

    if (!existing) {
      return { error: `Not found placement id=${update.id}` };
    }

    const isCurriculumToSolution =
      existing.workBook.workBookType === WorkBookType.CURRICULUM &&
      update.solutionCategory !== null;
    const isSolutionToCurriculum =
      existing.workBook.workBookType === WorkBookType.SOLUTION && update.taskGrade !== null;

    if (isCurriculumToSolution || isSolutionToCurriculum) {
      return { error: 'Moving between CURRICULUM and SOLUTION is not allowed' };
    }
  }

  return null;
}

// --- Only used for seeding initial placements, not exposed to runtime code ---

/**
 * Persists an array of new placement records to the database.
 */
export async function createWorkBookPlacements(placements: PlacementCreates): Promise<void> {
  if (placements.length === 0) {
    return;
  }

  await prisma.workBookPlacement.createMany({ data: placements });
}

// Re-export for consumers that only need the placement type (e.g. +server.ts upsert).
export type { WorkBookPlacement };
