import "server-only"

import { and, asc, desc, eq, inArray, isNull, or, type SQL } from "drizzle-orm"
import { createId } from "@paralleldrive/cuid2"

import { db } from "@/shared/db"
import {
  classes,
  classSchedule,
  classSubjectTeachers,
  classrooms,
  scheduleChanges,
  schedulingRules,
  subjects,
  users,
} from "@/shared/db/schema"

import type {
  ScheduleChangeListItem,
  ScheduleChangeQueryParams,
  ScheduleConflict,
  SchedulingRule,
} from "./types"
import type { SchedulingRuleInput, ScheduleChangeInput } from "./schema"

const serializeDate = (d: Date | string | null): string | null =>
  d ? new Date(d).toISOString().slice(0, 10) : null

const mapRule = (r: typeof schedulingRules.$inferSelect): SchedulingRule => ({
  id: r.id,
  classId: r.classId ?? null,
  maxDailyHours: r.maxDailyHours ?? 8,
  maxContinuousHours: r.maxContinuousHours ?? 2,
  lunchBreakStart: r.lunchBreakStart ?? "12:00",
  lunchBreakEnd: r.lunchBreakEnd ?? "13:00",
  morningStart: r.morningStart ?? "08:00",
  afternoonEnd: r.afternoonEnd ?? "17:00",
  avoidBackToBack: r.avoidBackToBack ?? false,
  balancedSubjects: r.balancedSubjects ?? true,
  createdAt: r.createdAt.toISOString(),
  updatedAt: r.updatedAt.toISOString(),
})

export async function getSchedulingRules(classId?: string): Promise<SchedulingRule[]> {
  const conditions: SQL[] = []
  if (classId) {
    const cond = or(eq(schedulingRules.classId, classId), isNull(schedulingRules.classId))
    if (cond) conditions.push(cond)
  }

  const rows = await db
    .select()
    .from(schedulingRules)
    .where(conditions.length > 0 ? and(...conditions) : undefined)
    .orderBy(desc(schedulingRules.classId), desc(schedulingRules.updatedAt))

  return rows.map(mapRule)
}

export async function upsertSchedulingRules(data: SchedulingRuleInput): Promise<string> {
  const [existing] = await db
    .select()
    .from(schedulingRules)
    .where(eq(schedulingRules.classId, data.classId))
    .limit(1)

  if (existing) {
    await db
      .update(schedulingRules)
      .set({
        maxDailyHours: data.maxDailyHours ?? 8,
        maxContinuousHours: data.maxContinuousHours ?? 2,
        lunchBreakStart: data.lunchBreakStart ?? "12:00",
        lunchBreakEnd: data.lunchBreakEnd ?? "13:00",
        morningStart: data.morningStart ?? "08:00",
        afternoonEnd: data.afternoonEnd ?? "17:00",
        avoidBackToBack: data.avoidBackToBack ?? false,
        balancedSubjects: data.balancedSubjects ?? true,
        updatedAt: new Date(),
      })
      .where(eq(schedulingRules.id, existing.id))
    return existing.id
  }

  const id = createId()
  await db.insert(schedulingRules).values({
    id,
    classId: data.classId,
    maxDailyHours: data.maxDailyHours ?? 8,
    maxContinuousHours: data.maxContinuousHours ?? 2,
    lunchBreakStart: data.lunchBreakStart ?? "12:00",
    lunchBreakEnd: data.lunchBreakEnd ?? "13:00",
    morningStart: data.morningStart ?? "08:00",
    afternoonEnd: data.afternoonEnd ?? "17:00",
    avoidBackToBack: data.avoidBackToBack ?? false,
    balancedSubjects: data.balancedSubjects ?? true,
  })
  return id
}

export async function getScheduleChanges(
  params: ScheduleChangeQueryParams
): Promise<ScheduleChangeListItem[]> {
  const conditions: SQL[] = []
  if (params.classId) conditions.push(eq(scheduleChanges.classId, params.classId))
  if (params.status) conditions.push(eq(scheduleChanges.status, params.status))
  if (params.requesterId) conditions.push(eq(scheduleChanges.requestedBy, params.requesterId))

  const rows = await db
    .select({
      change: scheduleChanges,
      className: classes.name,
      originalTeacherName: users.name,
    })
    .from(scheduleChanges)
    .innerJoin(classes, eq(classes.id, scheduleChanges.classId))
    .leftJoin(users, eq(users.id, scheduleChanges.originalTeacherId))
    .where(conditions.length > 0 ? and(...conditions) : undefined)
    .orderBy(desc(scheduleChanges.createdAt))

  // Resolve substitute teacher & approver names separately to avoid join ambiguity
  const userIds = Array.from(
    new Set(
      rows.flatMap((r) => [
        r.change.substituteTeacherId,
        r.change.approvedBy,
        r.change.requestedBy,
      ].filter((v): v is string => typeof v === "string" && v.length > 0))
    )
  )

  const userMap = new Map<string, string>()
  if (userIds.length > 0) {
    const userRows = await db
      .select({ id: users.id, name: users.name })
      .from(users)
      .where(inArray(users.id, userIds))
    for (const u of userRows) userMap.set(u.id, u.name ?? "Unknown")
  }

  return rows.map((r) => ({
    id: r.change.id,
    originalScheduleId: r.change.originalScheduleId ?? null,
    classId: r.change.classId,
    originalTeacherId: r.change.originalTeacherId ?? null,
    substituteTeacherId: r.change.substituteTeacherId ?? null,
    originalDate: serializeDate(r.change.originalDate),
    newDate: serializeDate(r.change.newDate),
    newStartTime: r.change.newStartTime ?? null,
    newEndTime: r.change.newEndTime ?? null,
    reason: r.change.reason ?? null,
    status: r.change.status,
    requestedBy: r.change.requestedBy,
    approvedBy: r.change.approvedBy ?? null,
    createdAt: r.change.createdAt.toISOString(),
    updatedAt: r.change.updatedAt.toISOString(),
    className: r.className,
    originalTeacherName: r.originalTeacherName ?? null,
    substituteTeacherName: r.change.substituteTeacherId
      ? userMap.get(r.change.substituteTeacherId) ?? null
      : null,
    requesterName: userMap.get(r.change.requestedBy) ?? "Unknown",
    approverName: r.change.approvedBy ? userMap.get(r.change.approvedBy) ?? null : null,
  }))
}

export async function createScheduleChange(
  data: ScheduleChangeInput,
  requestedBy: string
): Promise<string> {
  const id = createId()
  await db.insert(scheduleChanges).values({
    id,
    originalScheduleId: data.originalScheduleId ?? null,
    classId: data.classId,
    originalTeacherId: data.originalTeacherId ?? null,
    substituteTeacherId: data.substituteTeacherId ?? null,
    originalDate: data.originalDate ? new Date(data.originalDate) : null,
    newDate: data.newDate ? new Date(data.newDate) : null,
    newStartTime: data.newStartTime ?? null,
    newEndTime: data.newEndTime ?? null,
    reason: data.reason,
    status: "pending",
    requestedBy,
  })
  return id
}

export async function updateScheduleChangeStatus(
  id: string,
  status: "approved" | "rejected" | "completed",
  approverId: string
): Promise<void> {
  await db
    .update(scheduleChanges)
    .set({ status, approvedBy: approverId, updatedAt: new Date() })
    .where(eq(scheduleChanges.id, id))
}

export async function getClassConflicts(classId: string): Promise<ScheduleConflict[]> {
  const rows = await db
    .select({
      id: classSchedule.id,
      weekday: classSchedule.weekday,
      startTime: classSchedule.startTime,
      endTime: classSchedule.endTime,
      course: classSchedule.course,
    })
    .from(classSchedule)
    .where(eq(classSchedule.classId, classId))
    .orderBy(asc(classSchedule.weekday), asc(classSchedule.startTime))

  const conflicts: ScheduleConflict[] = []
  for (let i = 0; i < rows.length; i += 1) {
    for (let j = i + 1; j < rows.length; j += 1) {
      const a = rows[i]
      const b = rows[j]
      if (!a || !b) continue
      if (a.weekday !== b.weekday) continue
      // Time overlap: a.start < b.end && b.start < a.end
      if (a.startTime < b.endTime && b.startTime < a.endTime) {
        conflicts.push({
          type: "class_overlap",
          description: `Time overlap on day ${a.weekday}: "${a.course}" (${a.startTime}-${a.endTime}) vs "${b.course}" (${b.startTime}-${b.endTime})`,
          scheduleIds: [a.id, b.id],
        })
      }
    }
  }
  return conflicts
}

// --- Helpers for scheduling pages ---

/** Lightweight class info for scheduling selectors */
export type SchedulingClassOption = {
  id: string
  name: string
  grade: string
}

/** Lightweight teacher info for scheduling selectors */
export type SchedulingTeacherOption = {
  id: string
  name: string | null
  email: string
}

/** Lightweight classroom info for scheduling selectors */
export type SchedulingClassroomOption = {
  id: string
  name: string
  building: string | null
}

/** Class subject with assigned teacher for scheduling */
export type SchedulingClassSubject = {
  subjectId: string
  subjectName: string
  teacherId: string | null
}

export async function getAdminClassesForScheduling(): Promise<SchedulingClassOption[]> {
  return await db
    .select({ id: classes.id, name: classes.name, grade: classes.grade })
    .from(classes)
    .orderBy(classes.grade, classes.name)
}

export async function getTeachersForScheduling(): Promise<SchedulingTeacherOption[]> {
  return await db
    .select({ id: users.id, name: users.name, email: users.email })
    .from(users)
    .innerJoin(classSubjectTeachers, eq(classSubjectTeachers.teacherId, users.id))
    .groupBy(users.id, users.name, users.email)
    .orderBy(users.name)
}

export async function getClassroomsForScheduling(): Promise<SchedulingClassroomOption[]> {
  return await db
    .select({ id: classrooms.id, name: classrooms.name, building: classrooms.building })
    .from(classrooms)
    .orderBy(classrooms.name)
}

export async function getClassSubjectsForScheduling(
  classId: string
): Promise<SchedulingClassSubject[]> {
  return await db
    .select({
      subjectId: subjects.id,
      subjectName: subjects.name,
      teacherId: classSubjectTeachers.teacherId,
    })
    .from(classSubjectTeachers)
    .innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
    .where(eq(classSubjectTeachers.classId, classId))
}

// ---------------------------------------------------------------------------
// Unified classSchedule write entry points
// ---------------------------------------------------------------------------
// All classSchedule writes MUST go through these functions to ensure
// consistent conflict detection and data integrity.
// See: docs/architecture/audit/01_decoupling_roadmap.md P0-6
// ---------------------------------------------------------------------------

/** Input for a single schedule item insert */
export interface ScheduleItemInput {
  classId: string
  weekday: number
  startTime: string
  endTime: string
  course: string
  location?: string | null
}

/**
 * Insert a single classSchedule row.
 * Returns the generated id.
 */
export async function insertClassScheduleItem(
  item: ScheduleItemInput
): Promise<string> {
  const id = createId()
  await db.insert(classSchedule).values({
    id,
    classId: item.classId,
    weekday: item.weekday,
    startTime: item.startTime,
    endTime: item.endTime,
    course: item.course,
    location: item.location ?? null,
  })
  return id
}

/**
 * Update a classSchedule row by id.
 * Only the provided fields are updated.
 */
export async function updateClassScheduleItemById(
  scheduleId: string,
  data: Partial<Omit<ScheduleItemInput, "classId">> & { classId?: string }
): Promise<void> {
  const update: Partial<typeof classSchedule.$inferSelect> = {}
  if (data.classId !== undefined) update.classId = data.classId
  if (data.weekday !== undefined) update.weekday = data.weekday
  if (data.startTime !== undefined) update.startTime = data.startTime
  if (data.endTime !== undefined) update.endTime = data.endTime
  if (data.course !== undefined) update.course = data.course
  if (data.location !== undefined) update.location = data.location ?? null

  if (Object.keys(update).length === 0) return

  await db
    .update(classSchedule)
    .set(update)
    .where(eq(classSchedule.id, scheduleId))
}

/**
 * Delete a classSchedule row by id.
 */
export async function deleteClassScheduleItemById(scheduleId: string): Promise<void> {
  await db.delete(classSchedule).where(eq(classSchedule.id, scheduleId))
}

/**
 * Replace all schedule items for a class in a single transaction.
 * Deletes existing items then inserts the new ones atomically.
 *
 * This is the single entry point for batch schedule replacement
 * (used by auto-scheduling and admin bulk operations).
 */
export async function replaceClassSchedule(
  classId: string,
  items: ScheduleItemInput[]
): Promise<void> {
  await db.transaction(async (tx) => {
    await tx.delete(classSchedule).where(eq(classSchedule.classId, classId))
    if (items.length === 0) return
    const rows = items.map((s) => ({
      id: createId(),
      classId,
      weekday: s.weekday,
      startTime: s.startTime,
      endTime: s.endTime,
      course: s.course,
      location: s.location ?? null,
    }))
    await tx.insert(classSchedule).values(rows)
  })
}

// ---------------------------------------------------------------------------
// Schedule grid view entries for admin scheduling pages
// ---------------------------------------------------------------------------

/** Lightweight schedule entry for the admin schedule grid view */
export type ScheduleEntry = {
  id: string
  dayOfWeek: number
  period: number
  subject: string
  teacherName: string
  className: string
  room: string | null
}

/**
 * Get schedule entries for the admin schedule grid view.
 * Returns a flattened list of schedule items keyed by day/period.
 *
 * Note: simplified implementation returns an empty array; a real
 * implementation should join classSchedule with classes/users to
 * populate teacherName/className/subject/room.
 */
export async function getScheduleEntriesForAdmin(): Promise<ScheduleEntry[]> {
  return []
}
