import { Prisma, type PrismaClient } from '../generated/prisma/client.js';
import type { EventInput, SessionInput } from '../shared/contracts.js';
import { HttpError, required } from './errors.js';
import { sessionInclude } from './serialize.js';

type Tx = Prisma.TransactionClient;
export function eventBounds(startDate: string, endDate: string) {
  return { start: new Date(`${startDate}T00:00:00+07:00`), end: new Date(new Date(`${endDate}T00:00:00+07:00`).getTime() + 86400000) };
}
async function lockEvents(tx: Tx, ids: number[]) {
  const sorted = [...new Set(ids)].sort((a, b) => a - b);
  if (sorted.length) await tx.$queryRaw(Prisma.sql`SELECT id FROM events WHERE id IN (${Prisma.join(sorted)}) ORDER BY id FOR UPDATE`);
}
async function lockRooms(tx: Tx, ids: number[]) {
  const sorted = [...new Set(ids)].sort((a, b) => a - b);
  if (sorted.length) await tx.$queryRaw(Prisma.sql`SELECT id FROM rooms WHERE id IN (${Prisma.join(sorted)}) ORDER BY id FOR UPDATE`);
}
async function removeTopics(tx: Tx, sessionId: number) {
  await tx.topicSpeaker.deleteMany({ where: { topic: { sessionId } } });
  await tx.topic.deleteMany({ where: { sessionId } });
}
export async function saveSession(db: PrismaClient, input: SessionInput, id?: number) {
  return db.$transaction(async tx => {
    const before = id ? required(await tx.session.findUnique({ where: { id }, include: { eventRoom: true } })) : null;
    const target = required(await tx.eventRoom.findUnique({ where: { id: input.eventRoomId } }));
    await lockEvents(tx, [target.eventId, ...(before ? [before.eventRoom.eventId] : [])]);
    await lockRooms(tx, [target.roomId, ...(before ? [before.eventRoom.roomId] : [])]);
    const current = id ? required(await tx.session.findUnique({ where: { id } })) : null;
    if (current && current.eventRoomId !== before?.eventRoomId) throw new HttpError(409, 'Sesi baru dipindahkan pengguna lain. Muat ulang dan coba lagi.');
    const assignment = required(await tx.eventRoom.findUnique({ where: { id: input.eventRoomId }, include: { event: true } }));
    const startsAt = new Date(input.startsAt), endsAt = new Date(input.endsAt);
    const bounds = eventBounds(assignment.event.startDate.toISOString().slice(0, 10), assignment.event.endDate.toISOString().slice(0, 10));
    if (startsAt < bounds.start || endsAt > bounds.end) throw new HttpError(422, 'Waktu sesi harus berada dalam tanggal acara (WIB).');
    const conflict = await tx.session.findFirst({ where: { ...(id ? { id: { not: id } } : {}), eventRoom: { roomId: assignment.roomId }, startsAt: { lt: endsAt }, endsAt: { gt: startsAt } } });
    if (conflict) throw new HttpError(409, `Ruangan sudah digunakan oleh sesi “${conflict.title}” pada waktu tersebut.`);
    const data = { eventRoomId: assignment.id, title: input.title, startsAt, endsAt };
    const session = id ? await tx.session.update({ where: { id }, data }) : await tx.session.create({ data });
    if (id) await removeTopics(tx, id);
    for (const [index, topic] of input.topics.entries()) {
      await tx.topic.create({ data: { sessionId: session.id, title: topic.title, position: index + 1, topicSpeakers: { create: topic.speakerIds.map((speakerId, i) => ({ speakerId, position: i + 1 })) } } });
    }
    return tx.session.findUniqueOrThrow({ where: { id: session.id }, include: sessionInclude });
  }, { timeout: 15_000, isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
export async function deleteSession(db: PrismaClient, id: number) {
  await db.$transaction(async tx => {
    const before = required(await tx.session.findUnique({ where: { id }, include: { eventRoom: true } }));
    await lockEvents(tx, [before.eventRoom.eventId]);
    await lockRooms(tx, [before.eventRoom.roomId]);
    const current = required(await tx.session.findUnique({ where: { id } }));
    if (current.eventRoomId !== before.eventRoomId) throw new HttpError(409, 'Sesi baru dipindahkan. Muat ulang dan coba lagi.');
    await removeTopics(tx, id);
    await tx.session.delete({ where: { id } });
  }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
export async function saveEvent(db: PrismaClient, input: EventInput, id?: number) {
  return db.$transaction(async tx => {
    const data = { name: input.name, startDate: new Date(`${input.startDate}T00:00:00Z`), endDate: new Date(`${input.endDate}T00:00:00Z`) };
    if (!id) return tx.event.create({ data });
    await lockEvents(tx, [id]);
    required(await tx.event.findUnique({ where: { id } }));
    const assignments = await tx.eventRoom.findMany({ where: { eventId: id } });
    await lockRooms(tx, assignments.map(a => a.roomId));
    const bounds = eventBounds(input.startDate, input.endDate);
    const outside = await tx.session.findFirst({ where: { eventRoom: { eventId: id }, OR: [{ startsAt: { lt: bounds.start } }, { endsAt: { gt: bounds.end } }] } });
    if (outside) throw new HttpError(409, 'Tanggal baru akan menempatkan sesi di luar acara. Ubah jadwal sesi terlebih dahulu.');
    return tx.event.update({ where: { id }, data });
  }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
export async function createAssignment(db: PrismaClient, data: { eventId: number; roomId: number; category?: string | null }) {
  return db.$transaction(async tx => {
    await lockEvents(tx, [data.eventId]);
    await lockRooms(tx, [data.roomId]);
    return tx.eventRoom.create({ data, include: { event: true, room: true } });
  }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
export async function deleteAssignment(db: PrismaClient, id: number) {
  await db.$transaction(async tx => {
    const assignment = required(await tx.eventRoom.findUnique({ where: { id } }));
    await lockEvents(tx, [assignment.eventId]);
    await lockRooms(tx, [assignment.roomId]);
    await tx.eventRoom.delete({ where: { id } });
  }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
