// Path in project: app/api/admin/schedules/route.ts
import { NextRequest, NextResponse } from "next/server";
import type { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import { getAdminUser } from "@/lib/auth";
import { getDatesInRange, parseDateUTC, fmt12 } from "@/lib/timeslot-utils";
import { sendMail } from "@/lib/mailer";
import { slotAssignedEmailTemplate } from "@/lib/email-templates";

interface PatternInput { weekday: number; start_time: string; end_time: string; }
interface DateSlotInput { date: string; start_time: string; end_time: string; }
interface CreateScheduleBody {
  student_id: number;
  start_date: string;
  end_date: string;
  // Recurring weekday flow (existing behavior)
  patterns?: PatternInput[];
  // Specific-date flow: one or more explicit {date, start_time, end_time} entries,
  // used by the "Specific date(s)" mode (single date or a date-wise range).
  date_slots?: DateSlotInput[];
}

// Tracks exactly which date+time combos actually got booked inside the
// transaction, since upsertBooking silently skips dates that are already
// booked, have no generated slot, etc. The confirmation email should only
// reflect what really got booked, not the full requested range.
interface BookedEntry {
  dateLabel: string;
  startTime: string;
  endTime: string;
}

// GET /api/admin/schedules or /api/admin/schedules?student_id=5
export async function GET(req: NextRequest) {
  const admin = await getAdminUser();
  if (!admin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const studentId = req.nextUrl.searchParams.get("student_id");

  const schedules = await prisma.student_schedules.findMany({
    where: studentId ? { student_id: Number(studentId) } : undefined,
    include: {
      student_schedule_patterns: true,
      students: true,
      schedule_bookings: {
        where: { status: "active" },
        // time_slot_id, id, booking_date etc. are plain columns on schedule_bookings
        // and come back automatically alongside the nested time_slots relation.
        include: { time_slots: { select: { start_time: true, end_time: true } } },
        orderBy: { booking_date: "asc" },
      },
    },
    orderBy: { created_at: "desc" },
  });

  return NextResponse.json({ data: schedules });
}

// Creates (or reactivates) a booking for a single time slot, instead of a blind
// `create`. Three distinct things are being checked here and they must NOT be
// conflated:
//   1) Does this student already have an active booking on this exact date
//      (from ANY schedule)? -> skip silently. This is what lets a broader
//      weekday selection fill in only the un-covered dates of a range instead
//      of erroring, or double-booking a day the student is already in.
//   2) Is the slot itself full? -> booked_count >= capacity (this is what
//      allows many different students, up to `capacity`, to share a slot).
//   3) Does THIS student's schedule already have a (cancelled) row on this
//      exact slot+date? -> scoped by schedule_id, reactivate instead of insert.
// The unique constraint on schedule_bookings must be
// @@unique([time_slot_id, booking_date, schedule_id]) for #3 to be valid.
// Returns true if a booking was actually created/reactivated, false if skipped.
async function upsertBooking(
  tx: Prisma.TransactionClient,
  studentId: number,
  scheduleId: number,
  slotId: number,
  bookingDate: Date,
  dateLabel: string,
  timeLabel: string
): Promise<boolean> {
  const slot = await tx.time_slots.findUnique({ where: { id: slotId } });
  if (!slot) return false; // no generated slot for that date/time — skip silently, same as before

  const alreadyBookedThisDate = await tx.schedule_bookings.findFirst({
    where: {
      booking_date: bookingDate,
      status: "active",
      student_schedules: { student_id: studentId },
    },
  });
  if (alreadyBookedThisDate) return false; // keep the existing assignment for this date, skip

  if (slot.booked_count >= slot.capacity) {
    throw new Error(`${dateLabel} at ${timeLabel} is already full`);
  }

  const existingForThisSchedule = await tx.schedule_bookings.findUnique({
    where: {
      time_slot_id_booking_date_schedule_id: {
        time_slot_id: slot.id,
        booking_date: bookingDate,
        schedule_id: scheduleId,
      },
    },
  });

  if (existingForThisSchedule) {
    if (existingForThisSchedule.status === "active") {
      throw new Error(`${dateLabel} at ${timeLabel} is already booked for this student`);
    }
    // Reactivate this student's previously-cancelled row instead of inserting a duplicate.
    await tx.schedule_bookings.update({
      where: { id: existingForThisSchedule.id },
      data: { status: "active" },
    });
  } else {
    await tx.schedule_bookings.create({
      data: { schedule_id: scheduleId, time_slot_id: slot.id, booking_date: bookingDate },
    });
  }

  await tx.time_slots.update({
    where: { id: slot.id },
    data: { booked_count: { increment: 1 } },
  });

  return true;
}

export async function POST(req: NextRequest) {
  const admin = await getAdminUser();
  if (!admin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const body: CreateScheduleBody = await req.json();
  const { student_id, start_date, end_date, patterns, date_slots } = body;

  if (!student_id || !start_date || !end_date) {
    return NextResponse.json({ error: "student_id, start_date, end_date are required" }, { status: 400 });
  }
  if ((!patterns || !patterns.length) && (!date_slots || !date_slots.length)) {
    return NextResponse.json({ error: "Provide either patterns or date_slots" }, { status: 400 });
  }

  const bookedEntries: BookedEntry[] = [];

  try {
    const schedule = await prisma.$transaction(
      async (tx) => {
        // For date_slots mode there's no explicit "patterns" list from the client —
        // derive one (deduped by weekday+time) purely so the schedule/pattern records
        // stay useful for display in the "Assigned slots" summary.
        const effectivePatterns: PatternInput[] =
          date_slots && date_slots.length
            ? Array.from(
              new Map(
                date_slots.map((ds) => {
                  const weekday = parseDateUTC(ds.date).getUTCDay();
                  const key = `${weekday}-${ds.start_time}-${ds.end_time}`;
                  return [key, { weekday, start_time: ds.start_time, end_time: ds.end_time }];
                })
              ).values()
            )
            : (patterns as PatternInput[]);

        const created = await tx.student_schedules.create({
          data: {
            student_id,
            start_date: parseDateUTC(start_date),
            end_date: parseDateUTC(end_date),
            student_schedule_patterns: { create: effectivePatterns },
          },
        });

        if (date_slots && date_slots.length) {
          // Specific-date flow: book exactly the dates/times the admin picked, date-wise.
          for (const ds of date_slots) {
            const date = parseDateUTC(ds.date);
            const slot = await tx.time_slots.findUnique({
              where: {
                slot_date_start_time_end_time: {
                  slot_date: date,
                  start_time: ds.start_time,
                  end_time: ds.end_time,
                },
              },
            });
            if (!slot) continue;
            const booked = await upsertBooking(tx, student_id, created.id, slot.id, date, date.toDateString(), ds.start_time);
            if (booked) {
              bookedEntries.push({
                dateLabel: ds.date,
                startTime: fmt12(ds.start_time),
                endTime: fmt12(ds.end_time),
              });
            }
          }
        } else {
          // Recurring weekday flow: walk every date in the range, book whichever
          // dates match a chosen weekday pattern. Gaps are skipped automatically.
          const dates = getDatesInRange(start_date, end_date);
          for (const date of dates) {
            const pattern = (patterns as PatternInput[]).find((p) => p.weekday === date.getUTCDay());
            if (!pattern) continue;

            const slot = await tx.time_slots.findUnique({
              where: {
                slot_date_start_time_end_time: {
                  slot_date: date,
                  start_time: pattern.start_time,
                  end_time: pattern.end_time,
                },
              },
            });
            if (!slot) continue;
            const booked = await upsertBooking(tx, student_id, created.id, slot.id, date, date.toDateString(), pattern.start_time);
            if (booked) {
              bookedEntries.push({
                dateLabel: date.toISOString().slice(0, 10),
                startTime: fmt12(pattern.start_time),
                endTime: fmt12(pattern.end_time),
              });
            }
          }
        }

        return created;
      },
      {
        maxWait: 10000,
        timeout: 30000,
      });

    // Send confirmation email listing only the dates that actually got booked.
    // Failure to send must never fail the schedule-creation request itself.
    if (bookedEntries.length) {
      try {
        const student = await prisma.students.findUnique({ where: { id: student_id } });
        if (student) {
          bookedEntries.sort((a, b) => a.dateLabel.localeCompare(b.dateLabel));
          await sendMail({
            to: student.email,
            subject: "Your RKDA class schedule has been updated",
            html: slotAssignedEmailTemplate(
              student.name,
              bookedEntries,
              `${process.env.NEXT_PUBLIC_APP_URL}/student/schedule`
            ),
          });
        }
      } catch (mailErr) {
        console.error("Slot Assignment Email Error:", mailErr);
      }
    }

    return NextResponse.json({ data: schedule }, { status: 201 });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Failed to create schedule";
    return NextResponse.json({ error: message }, { status: 409 });
  }
}