import "server-only"

import type {
  AutoScheduleParams,
  AutoScheduleResult,
  GeneratedSchedule,
  ScheduleConflict,
  SchedulingRule,
  TimeSlot,
} from "./types"

/**
 * Convert "HH:MM" to minutes since midnight for easy comparison.
 */
const toMinutes = (t: string): number => {
  const [h, m] = t.split(":").map(Number)
  return (h ?? 0) * 60 + (m ?? 0)
}

/**
 * Check if two time ranges overlap on the same weekday.
 */
const isOverlap = (
  a: { weekday: number; startTime: string; endTime: string },
  b: { weekday: number; startTime: string; endTime: string }
): boolean => {
  if (a.weekday !== b.weekday) return false
  const aStart = toMinutes(a.startTime)
  const aEnd = toMinutes(a.endTime)
  const bStart = toMinutes(b.startTime)
  const bEnd = toMinutes(b.endTime)
  return aStart < bEnd && bStart < aEnd
}

/**
 * Check if a candidate slot falls within the lunch break.
 */
const isWithinLunchBreak = (slot: TimeSlot, rules: SchedulingRule): boolean => {
  const start = toMinutes(slot.startTime)
  const end = toMinutes(slot.endTime)
  const lunchStart = toMinutes(rules.lunchBreakStart)
  const lunchEnd = toMinutes(rules.lunchBreakEnd)
  return start < lunchEnd && lunchStart < end
}

/**
 * Check if a candidate slot is outside the allowed daily window.
 */
const isOutsideDailyWindow = (slot: TimeSlot, rules: SchedulingRule): boolean => {
  const start = toMinutes(slot.startTime)
  const end = toMinutes(slot.endTime)
  return start < toMinutes(rules.morningStart) || end > toMinutes(rules.afternoonEnd)
}

/**
 * Count existing schedules for a given weekday.
 */
const countByWeekday = (schedules: GeneratedSchedule[], weekday: number): number =>
  schedules.filter((s) => s.weekday === weekday).length

/**
 * Find an optimal time slot for a single course session.
 * Greedy: pick the first slot that satisfies all constraints.
 */
export function findOptimalSlot(params: {
  candidateSlots: TimeSlot[]
  existingSchedules: GeneratedSchedule[]
  rules: SchedulingRule
  teacherId: string | null
  classroomId: string | null
  usedSlots: Set<string>
}): TimeSlot | null {
  const {
    candidateSlots,
    existingSchedules,
    rules,
    teacherId,
    classroomId,
    usedSlots,
  } = params

  for (const slot of candidateSlots) {
    if (isWithinLunchBreak(slot, rules)) continue
    if (isOutsideDailyWindow(slot, rules)) continue

    const slotKey = `${slot.weekday}-${slot.startTime}-${slot.endTime}`
    if (usedSlots.has(slotKey)) continue

    // Class overlap check (same class can't have two courses at the same time)
    const classConflict = existingSchedules.some((s) => isOverlap(s, slot))
    if (classConflict) continue

    // Teacher conflict (teacher can't teach two classes at the same time)
    if (teacherId) {
      const teacherConflict = existingSchedules.some(
        (s) => s.teacherId === teacherId && isOverlap(s, slot)
      )
      if (teacherConflict) continue
    }

    // Classroom conflict (classroom can't be used by two classes at the same time)
    if (classroomId) {
      const roomConflict = existingSchedules.some(
        (s) => s.location === classroomId && isOverlap(s, slot)
      )
      if (roomConflict) continue
    }

    // Daily max hours check
    const dailyCount = countByWeekday(existingSchedules, slot.weekday)
    if (dailyCount >= rules.maxDailyHours) continue

    // Avoid back-to-back if rule is set
    if (rules.avoidBackToBack) {
      const hasBackToBack = existingSchedules.some((s) => {
        if (s.weekday !== slot.weekday) return false
        return (
          toMinutes(s.endTime) === toMinutes(slot.startTime) ||
          toMinutes(s.startTime) === toMinutes(slot.endTime)
        )
      })
      if (hasBackToBack) continue
    }

    return slot
  }

  return null
}

/**
 * Validate a generated schedule against the rules.
 * Returns a list of conflicts/violations found.
 */
export function validateSchedule(
  schedule: GeneratedSchedule[],
  rules: SchedulingRule
): ScheduleConflict[] {
  const conflicts: ScheduleConflict[] = []

  // Pairwise overlap check (class/teacher/classroom)
  for (let i = 0; i < schedule.length; i += 1) {
    for (let j = i + 1; j < schedule.length; j += 1) {
      const a = schedule[i]
      const b = schedule[j]
      if (!a || !b) continue
      if (!isOverlap(a, b)) continue

      if (a.teacherId && a.teacherId === b.teacherId) {
        conflicts.push({
          type: "teacher_overlap",
          description: `Teacher ${a.teacherId} has overlapping sessions on day ${a.weekday}`,
          scheduleIds: [a.classId, b.classId],
        })
      }
      if (a.location && a.location === b.location) {
        conflicts.push({
          type: "classroom_overlap",
          description: `Classroom ${a.location} double-booked on day ${a.weekday}`,
          scheduleIds: [a.classId, b.classId],
        })
      }
      if (a.classId === b.classId) {
        conflicts.push({
          type: "class_overlap",
          description: `Class ${a.classId} has overlapping sessions on day ${a.weekday}`,
          scheduleIds: [a.classId, b.classId],
        })
      }
    }
  }

  // Daily max hours check
  const byClassDay = new Map<string, number>()
  for (const s of schedule) {
    const key = `${s.classId}-${s.weekday}`
    byClassDay.set(key, (byClassDay.get(key) ?? 0) + 1)
  }
  for (const [key, count] of byClassDay.entries()) {
    if (count > rules.maxDailyHours) {
      conflicts.push({
        type: "rule_violation",
        description: `${key} exceeds max daily hours (${count} > ${rules.maxDailyHours})`,
        scheduleIds: [],
      })
    }
  }

  return conflicts
}

/**
 * Core auto-scheduling algorithm: greedy + backtracking (limited).
 * - Sorts subjects by weekly hours (desc) to schedule heavy subjects first.
 * - For each subject, tries to allocate `weeklyHours` sessions.
 * - Picks the first valid slot for each session.
 */
export function autoSchedule(params: AutoScheduleParams): AutoScheduleResult {
  const { classId, rules, subjects, classrooms, timeSlots } = params

  const sortedSubjects = [...subjects].sort((a, b) => b.weeklyHours - a.weeklyHours)
  const scheduled: GeneratedSchedule[] = []
  const conflicts: ScheduleConflict[] = []
  const usedSlots = new Set<string>()

  const classroomList = classrooms

  for (const subject of sortedSubjects) {
    let placed = 0
    let attempts = 0
    const maxAttempts = timeSlots.length * 4

    while (placed < subject.weeklyHours && attempts < maxAttempts) {
      attempts += 1

      const slot = findOptimalSlot({
        candidateSlots: timeSlots,
        existingSchedules: scheduled,
        rules,
        teacherId: subject.teacherId,
        classroomId: classroomList[0]?.id ?? null,
        usedSlots,
      })

      if (!slot) {
        conflicts.push({
          type: "rule_violation",
          description: `Cannot find a slot for "${subject.subjectName}" (session ${placed + 1}/${subject.weeklyHours})`,
          scheduleIds: [],
        })
        break
      }

      const slotKey = `${slot.weekday}-${slot.startTime}-${slot.endTime}`
      usedSlots.add(slotKey)

      scheduled.push({
        classId,
        weekday: slot.weekday,
        startTime: slot.startTime,
        endTime: slot.endTime,
        course: subject.subjectName,
        location: classroomList[0]?.name ?? null,
        teacherId: subject.teacherId ?? null,
        subjectId: subject.subjectId,
      })

      placed += 1
    }
  }

  // Final validation pass
  const validationConflicts = validateSchedule(scheduled, rules)
  conflicts.push(...validationConflicts)

  return {
    success: conflicts.length === 0,
    scheduledCount: scheduled.length,
    conflictCount: conflicts.length,
    conflicts,
    schedules: scheduled,
  }
}

/**
 * Build default time slots for a week (Mon-Fri, 4 morning + 4 afternoon sessions).
 * Each session is 50 minutes with a 10-minute break.
 */
export function buildDefaultTimeSlots(
  morningStart: string,
  afternoonEnd: string,
  lunchStart: string,
  lunchEnd: string
): Array<{ weekday: number; startTime: string; endTime: string }> {
  const slots: Array<{ weekday: number; startTime: string; endTime: string }> = []
  const toMin = (t: string) => {
    const [h, m] = t.split(":").map(Number)
    return (h ?? 0) * 60 + (m ?? 0)
  }
  const fromMin = (m: number) => {
    const h = Math.floor(m / 60)
    const mm = m % 60
    return `${String(h).padStart(2, "0")}:${String(mm).padStart(2, "0")}`
  }

  const startM = toMin(morningStart)
  const lunchStartM = toMin(lunchStart)
  const lunchEndM = toMin(lunchEnd)
  const endM = toMin(afternoonEnd)

  let cur = startM
  const morningSlots: Array<{ startTime: string; endTime: string }> = []
  for (let i = 0; i < 4 && cur + 50 <= lunchStartM; i += 1) {
    morningSlots.push({ startTime: fromMin(cur), endTime: fromMin(cur + 50) })
    cur += 60
  }

  cur = lunchEndM
  const afternoonSlots: Array<{ startTime: string; endTime: string }> = []
  for (let i = 0; i < 4 && cur + 50 <= endM; i += 1) {
    afternoonSlots.push({ startTime: fromMin(cur), endTime: fromMin(cur + 50) })
    cur += 60
  }

  for (let weekday = 1; weekday <= 5; weekday += 1) {
    for (const s of morningSlots) slots.push({ weekday, ...s })
    for (const s of afternoonSlots) slots.push({ weekday, ...s })
  }

  return slots
}
