import "server-only"

import { cache } from "react"
import { createId } from "@paralleldrive/cuid2"
import { and, count, desc, eq, sql } from "drizzle-orm"

import { db } from "@/shared/db"
import { gradeRecords } from "@/shared/db/schema"
import {
  getActiveStudentIdsByClassId,
  getClassExists,
  getClassNameById,
  getClassNamesByIds,
} from "@/modules/classes/data-access"
import { getSubjectOptions } from "@/modules/school/data-access"
import { getUserNamesByIds } from "@/modules/users/data-access"
import type { DataScope } from "@/shared/types/permissions"

import { buildScopeClassFilter, toNumber } from "./lib/grade-utils"
import { computeAverageScore, computeGradeStats } from "./stats-service"
import type {
  ClassGradeStats,
  ClassRankingItem,
  GradeQueryParams,
  GradeRecord,
  GradeRecordListItem,
  GradeStats,
  StudentGradeSummary,
} from "./types"
import type {
  BatchCreateGradeRecordInput,
  CreateGradeRecordInput,
  UpdateGradeRecordInput,
} from "./schema"

const serializeRecord = (r: typeof gradeRecords.$inferSelect): GradeRecord => ({
  id: r.id,
  studentId: r.studentId,
  classId: r.classId,
  subjectId: r.subjectId,
  examId: r.examId ?? null,
  academicYearId: r.academicYearId ?? null,
  title: r.title,
  score: String(r.score),
  fullScore: String(r.fullScore),
  type: r.type,
  semester: r.semester,
  recordedBy: r.recordedBy,
  remark: r.remark ?? null,
  createdAt: r.createdAt.toISOString(),
  updatedAt: r.updatedAt.toISOString(),
})

export const getGradeRecords = cache(
  async (
    params: GradeQueryParams & { scope: DataScope; currentUserId?: string }
  ): Promise<GradeRecordListItem[]> => {
    const conditions = []

    const scopeFilter = buildScopeClassFilter(params.scope)
    if (scopeFilter) conditions.push(scopeFilter)

    if (params.scope.type === "class_members" && params.currentUserId) {
      conditions.push(eq(gradeRecords.studentId, params.currentUserId))
    }

    if (params.classId) conditions.push(eq(gradeRecords.classId, params.classId))
    if (params.subjectId) conditions.push(eq(gradeRecords.subjectId, params.subjectId))
    if (params.studentId) conditions.push(eq(gradeRecords.studentId, params.studentId))
    if (params.type) conditions.push(eq(gradeRecords.type, params.type))
    if (params.semester) conditions.push(eq(gradeRecords.semester, params.semester))
    if (params.examId) conditions.push(eq(gradeRecords.examId, params.examId))

    const rows = await db
      .select({
        record: gradeRecords,
      })
      .from(gradeRecords)
      .where(conditions.length > 0 ? and(...conditions) : undefined)
      .orderBy(desc(gradeRecords.createdAt))

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

    // Batch fetch display names via cross-module interfaces
    const studentIds = Array.from(new Set(rows.map((r) => r.record.studentId)))
    const classIds = Array.from(new Set(rows.map((r) => r.record.classId).filter((v): v is string => typeof v === "string" && v.length > 0)))
    const recorderIds = Array.from(new Set(rows.map((r) => r.record.recordedBy)))

    const [studentNameMap, classNameMap, subjectOptions, recorderNameMap] = await Promise.all([
      getUserNamesByIds(studentIds),
      getClassNamesByIds(classIds),
      getSubjectOptions(),
      getUserNamesByIds(recorderIds),
    ])

    const subjectNameById = new Map<string, string>()
    for (const s of subjectOptions) subjectNameById.set(s.id, s.name)

    return rows.map((r) => ({
      id: r.record.id,
      studentId: r.record.studentId,
      studentName: studentNameMap.get(r.record.studentId)?.name ?? "Unknown",
      classId: r.record.classId,
      className: r.record.classId ? classNameMap.get(r.record.classId) ?? "Unknown" : "Unknown",
      subjectId: r.record.subjectId,
      subjectName: r.record.subjectId ? subjectNameById.get(r.record.subjectId) ?? "Unknown" : "Unknown",
      examId: r.record.examId ?? null,
      title: r.record.title,
      score: toNumber(r.record.score),
      fullScore: toNumber(r.record.fullScore),
      type: r.record.type,
      semester: r.record.semester,
      recordedBy: r.record.recordedBy,
      recorderName: recorderNameMap.get(r.record.recordedBy)?.name ?? "Unknown",
      remark: r.record.remark ?? null,
      createdAt: r.record.createdAt.toISOString(),
    }))
  }
)

export const getGradeRecordById = cache(async (id: string): Promise<GradeRecord | null> => {
  const [row] = await db.select().from(gradeRecords).where(eq(gradeRecords.id, id)).limit(1)
  return row ? serializeRecord(row) : null
})

export async function createGradeRecord(
  data: CreateGradeRecordInput,
  recordedBy: string
): Promise<string> {
  const id = createId()
  await db.insert(gradeRecords).values({
    id,
    studentId: data.studentId,
    classId: data.classId,
    subjectId: data.subjectId,
    examId: data.examId ?? null,
    academicYearId: data.academicYearId ?? null,
    title: data.title,
    score: String(data.score),
    fullScore: String(data.fullScore ?? 100),
    type: data.type ?? "exam",
    semester: data.semester ?? "1",
    recordedBy,
    remark: data.remark ?? null,
  })
  return id
}

export async function batchCreateGradeRecords(
  data: BatchCreateGradeRecordInput,
  recordedBy: string
): Promise<number> {
  const rows = data.records.map((r) => ({
    id: createId(),
    studentId: r.studentId,
    classId: data.classId,
    subjectId: data.subjectId,
    examId: data.examId ?? null,
    academicYearId: data.academicYearId ?? null,
    title: data.title,
    score: String(r.score),
    fullScore: String(data.fullScore ?? 100),
    type: data.type ?? "exam",
    semester: data.semester ?? "1",
    recordedBy,
    remark: r.remark ?? null,
  }))

  if (rows.length === 0) return 0
  await db.insert(gradeRecords).values(rows)
  return rows.length
}

export async function updateGradeRecord(
  id: string,
  data: UpdateGradeRecordInput
): Promise<void> {
  const update: Record<string, unknown> = { updatedAt: new Date() }
  if (data.title !== undefined) update.title = data.title
  if (data.score !== undefined) update.score = String(data.score)
  if (data.fullScore !== undefined) update.fullScore = String(data.fullScore)
  if (data.type !== undefined) update.type = data.type
  if (data.semester !== undefined) update.semester = data.semester
  if (data.remark !== undefined) update.remark = data.remark
  if (data.examId !== undefined) update.examId = data.examId

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

export async function deleteGradeRecord(id: string): Promise<void> {
  await db.delete(gradeRecords).where(eq(gradeRecords.id, id))
}

export const getClassGradeStats = cache(
  async (
    classId: string,
    subjectId?: string,
    examId?: string
  ): Promise<GradeStats | null> => {
    const conditions = [eq(gradeRecords.classId, classId)]
    if (subjectId) conditions.push(eq(gradeRecords.subjectId, subjectId))
    if (examId) conditions.push(eq(gradeRecords.examId, examId))

    const rows = await db
      .select({
        score: gradeRecords.score,
        fullScore: gradeRecords.fullScore,
      })
      .from(gradeRecords)
      .where(and(...conditions))

    return computeGradeStats(rows)
  }
)

export const getStudentGradeSummary = cache(
  async (studentId: string): Promise<StudentGradeSummary | null> => {
  const studentNameMap = await getUserNamesByIds([studentId])
  const studentName = studentNameMap.get(studentId)?.name ?? null
  if (!studentName && !studentNameMap.has(studentId)) return null

  const records = await db
    .select({
      record: gradeRecords,
    })
    .from(gradeRecords)
    .where(eq(gradeRecords.studentId, studentId))
    .orderBy(desc(gradeRecords.createdAt))

  if (records.length === 0) {
    return {
      studentId,
      studentName: studentName ?? "Unknown",
      records: [],
      averageScore: 0,
      rank: 0,
    }
  }

  // Batch fetch display names via cross-module interfaces
  const classIds = Array.from(new Set(records.map((r) => r.record.classId).filter((v): v is string => typeof v === "string" && v.length > 0)))

  const [classNameMap, subjectOptions] = await Promise.all([
    getClassNamesByIds(classIds),
    getSubjectOptions(),
  ])

  const subjectNameById = new Map<string, string>()
  for (const s of subjectOptions) subjectNameById.set(s.id, s.name)

  const listItems: GradeRecordListItem[] = records.map((r) => ({
    id: r.record.id,
    studentId: r.record.studentId,
    studentName: studentName ?? "Unknown",
    classId: r.record.classId,
    className: r.record.classId ? classNameMap.get(r.record.classId) ?? "Unknown" : "Unknown",
    subjectId: r.record.subjectId,
    subjectName: r.record.subjectId ? subjectNameById.get(r.record.subjectId) ?? "Unknown" : "Unknown",
    examId: r.record.examId ?? null,
    title: r.record.title,
    score: toNumber(r.record.score),
    fullScore: toNumber(r.record.fullScore),
    type: r.record.type,
    semester: r.record.semester,
    recordedBy: r.record.recordedBy,
    recorderName: "Unknown",
    remark: r.record.remark ?? null,
    createdAt: r.record.createdAt.toISOString(),
  }))

  const avg = computeAverageScore(listItems.map((i) => i.score))

  return {
    studentId,
    studentName: studentName ?? "Unknown",
    records: listItems,
    averageScore: avg,
    rank: 0,
  }
  }
)

export const getClassRanking = cache(
  async (
    classId: string,
    subjectId?: string,
    examId?: string
  ): Promise<ClassRankingItem[]> => {
    const conditions = [eq(gradeRecords.classId, classId)]
    if (subjectId) conditions.push(eq(gradeRecords.subjectId, subjectId))
    if (examId) conditions.push(eq(gradeRecords.examId, examId))

    const rows = await db
      .select({
        studentId: gradeRecords.studentId,
        avgScore: sql<number>`AVG(${gradeRecords.score})`,
        recordCount: count(gradeRecords.id),
      })
      .from(gradeRecords)
      .where(and(...conditions))
      .groupBy(gradeRecords.studentId)
      .orderBy(desc(sql`AVG(${gradeRecords.score})`))

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

    const studentIds = Array.from(new Set(rows.map((r) => r.studentId)))
    const studentNameMap = await getUserNamesByIds(studentIds)

    return rows.map((r, idx) => ({
      studentId: r.studentId,
      studentName: studentNameMap.get(r.studentId)?.name ?? "Unknown",
      averageScore: Math.round(toNumber(r.avgScore) * 100) / 100,
      rank: idx + 1,
      recordCount: toNumber(r.recordCount),
    }))
  }
)

export const getClassStudentsForEntry = cache(async (classId: string): Promise<
  Array<{ id: string; name: string; email: string }>
> => {
  const studentIds = await getActiveStudentIdsByClassId(classId)
  if (studentIds.length === 0) return []

  const studentNameMap = await getUserNamesByIds(studentIds)

  return studentIds
    .map((id) => {
      const info = studentNameMap.get(id)
      return {
        id,
        name: info?.name ?? "Unknown",
        email: info?.email ?? "",
      }
    })
    .sort((a, b) => a.name.localeCompare(b.name))
})

export const getClassGradeStatsWithMeta = cache(
  async (
    classId: string,
    subjectId?: string,
    examId?: string
  ): Promise<ClassGradeStats | null> => {
    const classExists = await getClassExists(classId)
    if (!classExists) return null

    const className = await getClassNameById(classId)
    const stats = await getClassGradeStats(classId, subjectId, examId)
    if (!stats) {
      return {
        classId,
        className: className ?? "Unknown",
        stats: {
          average: 0,
          median: 0,
          max: 0,
          min: 0,
          stdDev: 0,
          passRate: 0,
          excellentRate: 0,
          count: 0,
        },
        studentCount: 0,
      }
    }

    const activeStudentIds = await getActiveStudentIdsByClassId(classId)

    return {
      classId,
      className: className ?? "Unknown",
      stats,
      studentCount: activeStudentIds.length,
    }
  }
)
