import "server-only";

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

import { db } from "@/shared/db"
import {
  classes,
  classEnrollments,
  classSubjectTeachers,
  subjects,
  roles,
  users,
  usersToRoles,
} from "@/shared/db/schema"
import { ROLE_NAMES } from "@/shared/types/permissions"
import { DEFAULT_CLASS_SUBJECTS } from "./types"
import type {
  ClassSubject,
  CreateTeacherClassInput,
  TeacherOption,
  TeacherClass,
  UpdateTeacherClassInput,
} from "./types"
import {
  compareClassLike,
  getAccessibleClassIdsForTeacher,
  getClassSubjects,
  getSessionTeacherId,
  getTeacherIdForMutations,
} from "./data-access"
import { getClassHomeworkInsights } from "./data-access-stats"
import { getClassSchedule } from "./data-access-schedule"
import {
  generateUniqueInvitationCode,
  isDuplicateInvitationCodeError,
} from "./data-access-invitations"

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 getTeacherClasses = cache(async (params?: { teacherId?: string }): Promise<TeacherClass[]> => {
  const teacherId = params?.teacherId ?? (await getSessionTeacherId())
  if (!teacherId) return []

  const rows = await (async () => {
    try {
      const allIds = await getAccessibleClassIdsForTeacher(teacherId)

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

      return await db
        .select({
          id: classes.id,
          schoolName: classes.schoolName,
          name: classes.name,
          grade: classes.grade,
          homeroom: classes.homeroom,
          room: classes.room,
          invitationCode: classes.invitationCode,
          studentCount: sql<number>`COALESCE(SUM(CASE WHEN ${classEnrollments.status} = 'active' THEN 1 ELSE 0 END), 0)`,
        })
        .from(classes)
        .leftJoin(classEnrollments, eq(classEnrollments.classId, classes.id))
        .where(inArray(classes.id, allIds))
        .groupBy(classes.id, classes.schoolName, classes.name, classes.grade, classes.homeroom, classes.room, classes.invitationCode)
        .orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
    } catch (error) {
      console.error("getTeacherClasses query failed:", error)
      throw new Error("Failed to load teacher classes")
    }
  })()

  const list = rows.map((r) => ({
    id: r.id,
    schoolName: r.schoolName,
    name: r.name,
    grade: r.grade,
    homeroom: r.homeroom,
    room: r.room,
    invitationCode: r.invitationCode ?? null,
    studentCount: Number(r.studentCount ?? 0),
  }))

  list.sort(compareClassLike)

  // Fetch recent assignments for trends and schedule
  const listWithTrends = await Promise.all(
    list.map(async (c) => {
      const [insights, schedule] = await Promise.all([
        getClassHomeworkInsights({ classId: c.id, teacherId, limit: 7 }),
        getClassSchedule({ classId: c.id, teacherId }),
      ])

      const recentAssignments = insights
        ? insights.assignments.map((a) => ({
            id: a.assignmentId,
            title: a.title,
            status: a.status,
            subject: a.subject,
            isActive: a.isActive,
            isOverdue: a.isOverdue,
            dueAt: a.dueAt ? new Date(a.dueAt) : null,
            submittedCount: a.submittedCount,
            targetCount: a.targetCount,
            avgScore: a.scoreStats.avg,
            medianScore: a.scoreStats.median,
          }))
        : []
      return { ...c, recentAssignments, schedule }
    })
  )

  return listWithTrends
})

export const getTeacherOptions = cache(async (): Promise<TeacherOption[]> => {
  const rows = await db
    .select({ id: users.id, name: users.name, email: users.email })
    .from(users)
    .innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
    .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
    .where(eq(roles.name, ROLE_NAMES.TEACHER))
    .orderBy(asc(users.createdAt))

  return rows.map((r) => ({
    id: r.id,
    name: r.name ?? "Unnamed",
    email: r.email,
  }))
})

export const getTeacherTeachingSubjects = cache(async (): Promise<ClassSubject[]> => {
  const teacherId = await getSessionTeacherId()
  if (!teacherId) return []

  const rows = await db
    .select({ subject: subjects.name })
    .from(classSubjectTeachers)
    .innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
    .where(eq(classSubjectTeachers.teacherId, teacherId))
    .groupBy(subjects.name)
    .orderBy(asc(subjects.name))

  return rows
    .map((r) => toClassSubject(r.subject))
    .filter((s): s is ClassSubject => s !== null)
})

export async function createTeacherClass(data: CreateTeacherClassInput): Promise<string> {
  const teacherId = await getTeacherIdForMutations()
  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

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

  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 ensureClassInvitationCode(classId: string): Promise<string> {
  const teacherId = await getTeacherIdForMutations()
  const id = classId.trim()
  if (!id) throw new Error("Missing class id")

  const [owned] = await db
    .select({ id: classes.id, invitationCode: classes.invitationCode })
    .from(classes)
    .where(and(eq(classes.id, id), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) throw new Error("Class not found")

  const existing = owned.invitationCode
  if (typeof existing === "string" && /^\d{6}$/.test(existing)) return existing

  for (let attempt = 0; attempt < 40; attempt += 1) {
    const code = await generateUniqueInvitationCode()
    try {
      await db.update(classes).set({ invitationCode: code }).where(eq(classes.id, id))
      return code
    } catch (err) {
      if (isDuplicateInvitationCodeError(err)) continue
      throw err
    }
  }

  throw new Error("Failed to generate invitation code")
}

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

  const [owned] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(and(eq(classes.id, id), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) throw new Error("Class not found")

  for (let attempt = 0; attempt < 40; attempt += 1) {
    const code = await generateUniqueInvitationCode()
    try {
      await db.update(classes).set({ invitationCode: code }).where(eq(classes.id, id))
      return code
    } catch (err) {
      if (isDuplicateInvitationCodeError(err)) continue
      throw err
    }
  }

  throw new Error("Failed to generate invitation code")
}

export async function enrollStudentByInvitationCode(studentId: string, invitationCode: string): Promise<string> {
  const sid = studentId.trim()
  const code = invitationCode.trim()
  if (!sid) throw new Error("Missing student id")
  if (!code) throw new Error("Invalid invitation code")

  // v3：优先走新邀请码体系（validateInvitationCode 内部含 fallback 到旧 classes.invitationCode）
  const { validateInvitationCode, consumeInvitationCode } = await import("./data-access-invitations")
  const result = await validateInvitationCode(code)
  if (!result.valid || !result.classId) {
    throw new Error("Invalid invitation code")
  }

  await db
    .insert(classEnrollments)
    .values({ classId: result.classId, studentId: sid, status: "active" })
    .onDuplicateKeyUpdate({ set: { status: "active" } })

  // 消耗新表邀请码（旧表无计数，跳过）
  if (result.codeId) {
    await consumeInvitationCode(code)
  }

  return result.classId
}

export async function enrollTeacherByInvitationCode(
  teacherId: string,
  invitationCode: string,
  subject: string | null
): Promise<string> {
  const tid = teacherId.trim()
  const code = invitationCode.trim()
  if (!tid) throw new Error("Missing teacher id")
  if (!code) throw new Error("Invalid invitation code")

  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, tid), eq(roles.name, ROLE_NAMES.TEACHER)))
    .limit(1)

  if (!teacher) throw new Error("Teacher not found")

  // v3：优先走新邀请码体系（validateInvitationCode 内部含 fallback 到旧 classes.invitationCode）
  const { validateInvitationCode, consumeInvitationCode } = await import("./data-access-invitations")
  const result = await validateInvitationCode(code)
  if (!result.valid || !result.classId) {
    throw new Error("Invalid invitation code")
  }

  const [cls] = await db
    .select({ id: classes.id, teacherId: classes.teacherId })
    .from(classes)
    .where(eq(classes.id, result.classId))
    .limit(1)

  if (!cls) throw new Error("Invalid invitation code")
  if (cls.teacherId === tid) return cls.id

  const subjectValue = typeof subject === "string" ? subject.trim() : ""
  const [existing] = await db
    .select({ id: classSubjectTeachers.classId })
    .from(classSubjectTeachers)
    .where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.teacherId, tid)))
    .limit(1)

  if (existing && !subjectValue) return cls.id
  if (subjectValue) {
    const [subRow] = await db.select({ id: subjects.id }).from(subjects).where(eq(subjects.name, subjectValue)).limit(1)
    if (!subRow) throw new Error("Subject not found")
    const sid = subRow.id

    const [mapping] = await db
      .select({ teacherId: classSubjectTeachers.teacherId })
      .from(classSubjectTeachers)
      .where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid)))
      .limit(1)

    if (mapping?.teacherId && mapping.teacherId !== tid) throw new Error("Subject already assigned")
    if (mapping?.teacherId === tid) return cls.id
    if (!mapping) {
      await db
        .insert(classSubjectTeachers)
        .values({ classId: cls.id, subjectId: sid, teacherId: null })
        .onDuplicateKeyUpdate({ set: { teacherId: sql`${classSubjectTeachers.teacherId}` } })
    }

    const [existingSubject] = await db
      .select({ id: classSubjectTeachers.classId })
      .from(classSubjectTeachers)
      .where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), eq(classSubjectTeachers.teacherId, tid)))
      .limit(1)

    if (existingSubject) return cls.id

    await db
      .update(classSubjectTeachers)
      .set({ teacherId: tid })
      .where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), isNull(classSubjectTeachers.teacherId)))

    const [assigned] = await db
      .select({ id: classSubjectTeachers.classId })
      .from(classSubjectTeachers)
      .where(and(eq(classSubjectTeachers.classId, cls.id), eq(classSubjectTeachers.subjectId, sid), eq(classSubjectTeachers.teacherId, tid)))
      .limit(1)

    if (!assigned) throw new Error("Subject already assigned")
  } else {
    const subjectRows = await db
      .select({ id: classSubjectTeachers.subjectId, name: subjects.name })
      .from(classSubjectTeachers)
      .innerJoin(subjects, eq(subjects.id, classSubjectTeachers.subjectId))
      .where(and(eq(classSubjectTeachers.classId, cls.id), isNull(classSubjectTeachers.teacherId)))

    const preferred = DEFAULT_CLASS_SUBJECTS.find((s) => subjectRows.some((r) => r.name === s))
    if (!preferred) throw new Error("Class already has assigned teachers")
    const subjectRow = subjectRows.find((r) => r.name === preferred)
    if (!subjectRow) throw new Error("Subject not found")
    const sid = subjectRow.id

    await db
      .update(classSubjectTeachers)
      .set({ teacherId: tid })
      .where(
        and(
          eq(classSubjectTeachers.classId, cls.id),
          eq(classSubjectTeachers.subjectId, sid),
          isNull(classSubjectTeachers.teacherId)
        )
      )

    const [assigned] = await db
      .select({ id: classSubjectTeachers.classId })
      .from(classSubjectTeachers)
      .where(
        and(
          eq(classSubjectTeachers.classId, cls.id),
          eq(classSubjectTeachers.subjectId, sid),
          eq(classSubjectTeachers.teacherId, tid)
        )
      )
      .limit(1)

    if (!assigned) throw new Error("Class already has assigned teachers")
  }

  // 消耗新表邀请码（旧表无计数，跳过）
  if (result.codeId) {
    await consumeInvitationCode(code)
  }

  return cls.id
}

export async function updateTeacherClass(classId: string, data: UpdateTeacherClassInput): Promise<void> {
  const teacherId = await getTeacherIdForMutations()

  const [owned] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) 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 (Object.keys(update).length === 0) return

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

export async function setClassSubjectTeachers(params: {
  classId: string
  assignments: Array<{ subject: ClassSubject; teacherId: string | null }>
}): Promise<void> {
  const classId = params.classId.trim()
  if (!classId) throw new Error("Missing class id")

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

  const teacherIds = params.assignments
    .map((a) => a.teacherId)
    .filter((v): v is string => typeof v === "string" && v.trim().length > 0)

  if (teacherIds.length > 0) {
    const rows = 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(roles.name, ROLE_NAMES.TEACHER), inArray(users.id, teacherIds)))
    if (rows.length !== new Set(teacherIds).size) throw new Error("Teacher not found")
  }

  const teacherBySubject = new Map<string, string | null>()
  const subjectNames = await getClassSubjects()
  for (const a of params.assignments) {
    if (!subjectNames.includes(a.subject)) continue
    teacherBySubject.set(a.subject, typeof a.teacherId === "string" && a.teacherId.trim().length > 0 ? a.teacherId.trim() : null)
  }

  // Map subject names to ids
  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)
  }

  const values = subjectNames.flatMap((name) => {
    const subjectId = idByName.get(name)
    if (!subjectId) return []
    return [{ classId, subjectId, teacherId: teacherBySubject.get(name) ?? null }]
  })

  await db
    .insert(classSubjectTeachers)
    .values(values)
    .onDuplicateKeyUpdate({ set: { teacherId: sql`VALUES(${classSubjectTeachers.teacherId})` } })
}

export async function deleteTeacherClass(classId: string): Promise<void> {
  const teacherId = await getTeacherIdForMutations()

  const [owned] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) throw new Error("Class not found")

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

export async function enrollStudentByEmail(classId: string, email: string): Promise<void> {
  const teacherId = await getTeacherIdForMutations()
  const normalized = email.trim().toLowerCase()
  if (!normalized) throw new Error("Student email is required")

  const [owned] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) throw new Error("Class not found")

  const [student] = await db
    .select({ id: users.id })
    .from(users)
    .where(eq(users.email, normalized))
    .limit(1)

  if (!student) throw new Error("Student not found")
  const [studentRole] = await db
    .select({ id: usersToRoles.userId })
    .from(usersToRoles)
    .innerJoin(roles, eq(usersToRoles.roleId, roles.id))
    .where(and(eq(usersToRoles.userId, student.id), eq(roles.name, ROLE_NAMES.STUDENT)))
    .limit(1)
  if (!studentRole) throw new Error("User is not a student")

  await db
    .insert(classEnrollments)
    .values({ classId, studentId: student.id, status: "active" })
    .onDuplicateKeyUpdate({ set: { status: "active" } })
}

export async function setStudentEnrollmentStatus(classId: string, studentId: string, status: "active" | "inactive"): Promise<void> {
  const teacherId = await getTeacherIdForMutations()

  const [owned] = await db
    .select({ id: classes.id })
    .from(classes)
    .where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
    .limit(1)

  if (!owned) throw new Error("Class not found")

  const [existing] = await db
    .select({ classId: classEnrollments.classId })
    .from(classEnrollments)
    .where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.studentId, studentId)))
    .limit(1)

  if (!existing) throw new Error("Enrollment not found")

  await db
    .update(classEnrollments)
    .set({ status })
    .where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.studentId, studentId)))
}

// ---------------------------------------------------------------------------
// DataScope resolver helpers (P1-5/P1-6 audit fix)
// These lightweight functions return only the IDs needed by the RBAC
// data-scope resolver, so shared/lib/auth-guard no longer queries classes/
// classEnrollments/classSubjectTeachers tables directly.
// ---------------------------------------------------------------------------

/**
 * Get a teacher's class IDs and subject IDs for DataScope resolution.
 * Merges homeroom classes (classes.teacherId) and subject-teacher assignments
 * (classSubjectTeachers.teacherId). Returns deduplicated arrays.
 */
export async function getTeacherScopeData(
  teacherId: string,
): Promise<{ classIds: string[]; subjectIds: string[] }> {
  const [homeroomRows, subjectRows] = await Promise.all([
    db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId)),
    db
      .select({ classId: classSubjectTeachers.classId, subjectId: classSubjectTeachers.subjectId })
      .from(classSubjectTeachers)
      .where(eq(classSubjectTeachers.teacherId, teacherId)),
  ])

  const classIdSet = new Set<string>(homeroomRows.map((r) => r.id))
  const subjectIdSet = new Set<string>()
  for (const row of subjectRows) {
    classIdSet.add(row.classId)
    if (row.subjectId !== null) subjectIdSet.add(row.subjectId)
  }

  return {
    classIds: Array.from(classIdSet),
    subjectIds: Array.from(subjectIdSet),
  }
}
