import "server-only";

import { cache } from "react"
import { and, asc, eq, inArray, or, sql } from "drizzle-orm"
import { createId } from "@paralleldrive/cuid2"

import { db } from "@/shared/db"
import {
  classes,
  classEnrollments,
  classSubjectTeachers,
  grades,
  schools,
  subjects,
  roles,
  users,
  usersToRoles,
} from "@/shared/db/schema"
import { ROLE_NAMES } from "@/shared/types/permissions"
import { DEFAULT_CLASS_SUBJECTS } from "./types"
import type {
  AdminClassListItem,
  ClassSubject,
  ClassSubjectTeacherAssignment,
  CreateTeacherClassInput,
  TeacherOption,
  UpdateTeacherClassInput,
} from "./types"
import {
  compareClassLike,
  generateUniqueInvitationCode,
  getClassSubjects,
  isDuplicateInvitationCodeError,
} from "./data-access"

const isClassSubject = (v: unknown): v is ClassSubject =>
  typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v)

const toClassSubject = (v: string): ClassSubject | null =>
  isClassSubject(v) ? v : null

export const getAdminClasses = cache(async (): Promise<AdminClassListItem[]> => {
  const [rows, subjectRows] = await Promise.all([
    (async () => {
      try {
        return await db
          .select({
            id: classes.id,
            schoolName: classes.schoolName,
            schoolId: classes.schoolId,
            name: classes.name,
            grade: classes.grade,
            gradeId: classes.gradeId,
            homeroom: classes.homeroom,
            room: classes.room,
            invitationCode: classes.invitationCode,
            teacherId: users.id,
            teacherName: users.name,
            teacherEmail: users.email,
            studentCount: sql<number>`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`,
            createdAt: classes.createdAt,
            updatedAt: classes.updatedAt,
          })
          .from(classes)
          .innerJoin(users, eq(users.id, classes.teacherId))
          .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id))
          .groupBy(
            classes.id,
            classes.schoolName,
            classes.schoolId,
            classes.name,
            classes.grade,
            classes.gradeId,
            classes.homeroom,
            classes.room,
            classes.invitationCode,
            users.id,
            users.name,
            users.email,
            classes.createdAt,
            classes.updatedAt
          )
          .orderBy(
            asc(classes.schoolName),
            asc(classes.grade),
            asc(classes.name),
            asc(classes.homeroom),
            asc(classes.room)
          )
      } catch (error) {
        console.error("getAdminClasses primary query failed, falling back:", error)
        return await db
          .select({
            id: classes.id,
            schoolName: sql<string | null>`NULL`.as("schoolName"),
            schoolId: sql<string | null>`NULL`.as("schoolId"),
            name: classes.name,
            grade: classes.grade,
            gradeId: sql<string | null>`NULL`.as("gradeId"),
            homeroom: classes.homeroom,
            room: classes.room,
            invitationCode: sql<string | null>`NULL`.as("invitationCode"),
            teacherId: users.id,
            teacherName: users.name,
            teacherEmail: users.email,
            studentCount: sql<number>`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`,
            createdAt: classes.createdAt,
            updatedAt: classes.updatedAt,
          })
          .from(classes)
          .innerJoin(users, eq(users.id, classes.teacherId))
          .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id))
          .groupBy(
            classes.id,
            classes.name,
            classes.grade,
            classes.homeroom,
            classes.room,
            users.id,
            users.name,
            users.email,
            classes.createdAt,
            classes.updatedAt
          )
          .orderBy(asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
      }
    })(),
    db
      .select({
        classId: classSubjectTeachers.classId,
        subject: subjects.name,
        teacherId: users.id,
        teacherName: users.name,
        teacherEmail: users.email,
      })
      .from(classSubjectTeachers)
      .innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
      .leftJoin(users, eq(users.id, classSubjectTeachers.teacherId))
      .orderBy(asc(classSubjectTeachers.classId), asc(subjects.name)),
  ])

  const subjectsByClassId = new Map<string, Map<ClassSubject, TeacherOption | null>>()
  for (const r of subjectRows) {
    const subject = toClassSubject(r.subject)
    if (!subject) continue
    const teacher =
      typeof r.teacherId === "string" && r.teacherId.length > 0
        ? { id: r.teacherId, name: r.teacherName ?? "Unnamed", email: r.teacherEmail ?? "" }
        : null
    const bySubject = subjectsByClassId.get(r.classId) ?? new Map<ClassSubject, TeacherOption | null>()
    bySubject.set(subject, teacher)
    subjectsByClassId.set(r.classId, bySubject)
  }

  const list = rows.map((r) => {
    const bySubject = subjectsByClassId.get(r.id)
    const subjectTeachers: ClassSubjectTeacherAssignment[] = DEFAULT_CLASS_SUBJECTS.map((subject) => ({
      subject,
      teacher: bySubject?.get(subject) ?? null,
    }))

    return {
      id: r.id,
      schoolName: r.schoolName,
      schoolId: r.schoolId,
      name: r.name,
      grade: r.grade,
      gradeId: r.gradeId,
      homeroom: r.homeroom,
      room: r.room,
      invitationCode: r.invitationCode ?? null,
      teacher: {
        id: r.teacherId,
        name: r.teacherName ?? "Unnamed",
        email: r.teacherEmail,
      },
      subjectTeachers,
      studentCount: Number(r.studentCount ?? 0),
      createdAt: r.createdAt.toISOString(),
      updatedAt: r.updatedAt.toISOString(),
    }
  })

  list.sort(compareClassLike)
  return list
})

export const getGradeManagedClasses = cache(async (userId: string): Promise<AdminClassListItem[]> => {
  const managedGradeIds = await db
    .select({ id: grades.id })
    .from(grades)
    .where(or(eq(grades.gradeHeadId, userId), eq(grades.teachingHeadId, userId)))

  if (managedGradeIds.length === 0) return []

  const gradeIds = managedGradeIds.map((g) => g.id)

  const [rows, subjectRows] = await Promise.all([
    (async () => {
      try {
        return await db
          .select({
            id: classes.id,
            schoolName: classes.schoolName,
            schoolId: classes.schoolId,
            name: classes.name,
            grade: classes.grade,
            gradeId: classes.gradeId,
            homeroom: classes.homeroom,
            room: classes.room,
            invitationCode: classes.invitationCode,
            teacherId: users.id,
            teacherName: users.name,
            teacherEmail: users.email,
            studentCount: sql<number>`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`,
            createdAt: classes.createdAt,
            updatedAt: classes.updatedAt,
          })
          .from(classes)
          .innerJoin(users, eq(users.id, classes.teacherId))
          .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id))
          .where(inArray(classes.gradeId, gradeIds))
          .groupBy(
            classes.id,
            classes.schoolName,
            classes.schoolId,
            classes.name,
            classes.grade,
            classes.gradeId,
            classes.homeroom,
            classes.room,
            classes.invitationCode,
            users.id,
            users.name,
            users.email,
            classes.createdAt,
            classes.updatedAt
          )
          .orderBy(
            asc(classes.schoolName),
            asc(classes.grade),
            asc(classes.name),
            asc(classes.homeroom),
            asc(classes.room)
          )
      } catch (error) {
        console.error("getGradeManagedClasses primary query failed:", error)
        return []
      }
    })(),
    db
      .select({
        classId: classSubjectTeachers.classId,
        subject: subjects.name,
        teacherId: users.id,
        teacherName: users.name,
        teacherEmail: users.email,
      })
      .from(classSubjectTeachers)
      .innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
      .innerJoin(classes, eq(classes.id, classSubjectTeachers.classId))
      .leftJoin(users, eq(users.id, classSubjectTeachers.teacherId))
      .where(inArray(classes.gradeId, gradeIds))
      .orderBy(asc(classSubjectTeachers.classId), asc(subjects.name)),
  ])

  const subjectsByClassId = new Map<string, Map<ClassSubject, TeacherOption | null>>()
  for (const r of subjectRows) {
    const subject = toClassSubject(r.subject)
    if (!subject) continue
    const teacher =
      typeof r.teacherId === "string" && r.teacherId.length > 0
        ? { id: r.teacherId, name: r.teacherName ?? "Unnamed", email: r.teacherEmail ?? "" }
        : null
    const bySubject = subjectsByClassId.get(r.classId) ?? new Map<ClassSubject, TeacherOption | null>()
    bySubject.set(subject, teacher)
    subjectsByClassId.set(r.classId, bySubject)
  }

  const list = rows.map((r) => {
    const bySubject = subjectsByClassId.get(r.id)
    const subjectTeachers: ClassSubjectTeacherAssignment[] = DEFAULT_CLASS_SUBJECTS.map((subject) => ({
      subject,
      teacher: bySubject?.get(subject) ?? null,
    }))

    return {
      id: r.id,
      schoolName: r.schoolName,
      schoolId: r.schoolId,
      name: r.name,
      grade: r.grade,
      gradeId: r.gradeId,
      homeroom: r.homeroom,
      room: r.room,
      invitationCode: r.invitationCode ?? null,
      teacher: {
        id: r.teacherId,
        name: r.teacherName ?? "Unnamed",
        email: r.teacherEmail,
      },
      subjectTeachers,
      studentCount: Number(r.studentCount ?? 0),
      createdAt: r.createdAt.toISOString(),
      updatedAt: r.updatedAt.toISOString(),
    }
  })

  list.sort(compareClassLike)
  return list
})

export const getManagedGrades = cache(async (userId: string): Promise<{ id: string; name: string; schoolId: string; schoolName: string }[]> => {
  return await db
    .select({
      id: grades.id,
      name: grades.name,
      schoolId: grades.schoolId,
      schoolName: schools.name,
    })
    .from(grades)
    .innerJoin(schools, eq(schools.id, grades.schoolId))
    .where(or(eq(grades.gradeHeadId, userId), eq(grades.teachingHeadId, userId)))
    .orderBy(asc(schools.name), asc(grades.name))
})

export async function createAdminClass(data: CreateTeacherClassInput & { teacherId: string }): Promise<string> {
  const id = createId()

  const schoolName = data.schoolName?.trim() || null
  const schoolId = data.schoolId?.trim() || null
  const name = data.name.trim()
  const grade = data.grade.trim()
  const gradeId = data.gradeId?.trim() || null
  const homeroom = data.homeroom?.trim() || null
  const room = data.room?.trim() || null
  const teacherId = data.teacherId.trim()

  if (!name) throw new Error("Name is required")
  if (!grade) throw new Error("Grade is required")
  if (!teacherId) throw new Error("Teacher is required")

  const [teacher] = await db
    .select({ id: users.id })
    .from(users)
    .innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
    .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
    .where(and(eq(users.id, teacherId), eq(roles.name, ROLE_NAMES.TEACHER)))
    .limit(1)
  if (!teacher) throw new Error("Teacher not found")

  for (let attempt = 0; attempt < 20; attempt += 1) {
    const invitationCode = await generateUniqueInvitationCode()
    try {
      const subjectNames = await getClassSubjects()
      const subjectRows = await db
        .select({ id: subjects.id, name: subjects.name })
        .from(subjects)
        .where(inArray(subjects.name, subjectNames))
      const idByName = new Map<string, string>()
      for (const r of subjectRows) {
        idByName.set(r.name, r.id)
      }

      await db.transaction(async (tx) => {
        await tx.insert(classes).values({
          id,
          schoolName,
          schoolId,
          name,
          grade,
          gradeId,
          homeroom,
          room,
          invitationCode,
          teacherId,
        })

        const values = subjectNames.flatMap((name) => {
          const subjectId = idByName.get(name)
          if (!subjectId) return []
          return [{ classId: id, subjectId, teacherId: null }]
        })
        await tx.insert(classSubjectTeachers).values(values)
      })
      return id
    } catch (err) {
      if (isDuplicateInvitationCodeError(err)) continue
      throw err
    }
  }
  throw new Error("Failed to create class")
}

export async function updateAdminClass(
  classId: string,
  data: UpdateTeacherClassInput & { teacherId?: string }
): Promise<void> {
  const id = classId.trim()
  if (!id) throw new Error("Missing class id")

  const [existing] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(eq(classes.id, id))
    .limit(1)
  if (!existing) throw new Error("Class not found")

  const update: Partial<typeof classes.$inferSelect> = {}
  if (data.schoolName !== undefined) update.schoolName = data.schoolName?.trim() || null
  if (data.schoolId !== undefined) update.schoolId = data.schoolId?.trim() || null
  if (typeof data.name === "string") update.name = data.name.trim()
  if (typeof data.grade === "string") update.grade = data.grade.trim()
  if (data.gradeId !== undefined) update.gradeId = data.gradeId?.trim() || null
  if (data.homeroom !== undefined) update.homeroom = data.homeroom?.trim() || null
  if (data.room !== undefined) update.room = data.room?.trim() || null

  if (typeof data.teacherId === "string") {
    const nextTeacherId = data.teacherId.trim()
    if (!nextTeacherId) throw new Error("Teacher is required")

    const [teacher] = await db
      .select({ id: users.id })
      .from(users)
      .innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
      .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
      .where(and(eq(users.id, nextTeacherId), eq(roles.name, ROLE_NAMES.TEACHER)))
      .limit(1)
    if (!teacher) throw new Error("Teacher not found")

    update.teacherId = nextTeacherId
  }

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

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

export async function deleteAdminClass(classId: string): Promise<void> {
  const id = classId.trim()
  if (!id) throw new Error("Missing class id")

  const [existing] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(eq(classes.id, id))
    .limit(1)
  if (!existing) throw new Error("Class not found")

  await db.delete(classes).where(eq(classes.id, id))
}
