import { db } from "@/shared/db"
import { exams, examQuestions, examSubmissions, submissionAnswers } from "@/shared/db/schema"
import { count, eq, desc, like, and, or, inArray } from "drizzle-orm"
import { cache } from "react"
import { createId } from "@paralleldrive/cuid2"
import { createQuestionWithRelations } from "@/modules/questions/data-access"
import { getClassGradeIdsByClassIds } from "@/modules/classes/data-access"
import { getSubjectNameById, getGradeNameById, getSubjectOptions, getGradeOptions } from "@/modules/school/data-access"

import type { Exam, ExamDifficulty, ExamStatus } from "./types"
import type { AiGeneratedQuestion, AiGeneratedStructureNode } from "./ai-pipeline"
import type { DataScope } from "@/shared/types/permissions"

export type GetExamsParams = {
  q?: string
  status?: string
  difficulty?: string
  page?: number
  pageSize?: number
}

const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null

const parseExamMeta = (description: string | null): Record<string, unknown> => {
  if (!description) return {}
  try {
    const parsed: unknown = JSON.parse(description)
    return isRecord(parsed) ? parsed : {}
  } catch {
    return {}
  }
}

const getString = (obj: Record<string, unknown>, key: string): string | undefined => {
  const v = obj[key]
  return typeof v === "string" ? v : undefined
}

const getNumber = (obj: Record<string, unknown>, key: string): number | undefined => {
  const v = obj[key]
  return typeof v === "number" ? v : undefined
}

const getStringArray = (obj: Record<string, unknown>, key: string): string[] | undefined => {
  const v = obj[key]
  if (!Array.isArray(v)) return undefined
  const items = v.filter((x): x is string => typeof x === "string")
  return items.length === v.length ? items : undefined
}

const isExamStatus = (v: unknown): v is ExamStatus =>
  v === "draft" || v === "published" || v === "archived"

const toExamStatus = (v: string | null | undefined): ExamStatus =>
  isExamStatus(v) ? v : "draft"

const toExamDifficulty = (n: number | undefined): ExamDifficulty => {
  if (n === 1 || n === 2 || n === 3 || n === 4 || n === 5) return n
  return 1
}


export const getExams = cache(async (params: GetExamsParams & { scope: DataScope }) => {
  const conditions = []

  if (params.q) {
    const search = `%${params.q}%`
    conditions.push(or(like(exams.title, search), like(exams.description, search)))
  }

  if (params.status && params.status !== "all") {
    conditions.push(eq(exams.status, params.status))
  }

  // Data scope filtering
  if (params.scope.type === "owned") {
    conditions.push(eq(exams.creatorId, params.scope.userId))
  }
  if (params.scope.type === "class_taught" && params.scope.classIds.length > 0) {
    // Teacher can see exams for grades their classes belong to
    const classGradeMap = await getClassGradeIdsByClassIds(params.scope.classIds)
    const gradeIds = Array.from(new Set(classGradeMap.values()))
    if (gradeIds.length > 0) {
      conditions.push(inArray(exams.gradeId, gradeIds))
    }
  }
  if (params.scope.type === "grade_managed" && params.scope.gradeIds.length > 0) {
    conditions.push(inArray(exams.gradeId, params.scope.gradeIds))
  }
  // "all" type: no filtering
  // "class_members": student sees published exams for their grade (would need student's gradeId)

  // Note: Difficulty is stored in JSON description field in current schema,
  // so we might need to filter in memory or adjust schema. 
  // For now, let's fetch and filter in memory if difficulty is needed, 
  // or just ignore strict DB filtering for JSON fields to keep it simple.

  const data = await db.query.exams.findMany({
    where: conditions.length ? and(...conditions) : undefined,
    orderBy: [desc(exams.createdAt)],
    with: {
      subject: true,
      gradeEntity: true,
    }
  })

  // Transform and Filter (especially for JSON fields)
  let result: Exam[] = data.map((exam) => {
    const meta = parseExamMeta(exam.description || null)

    return {
      id: exam.id,
      title: exam.title,
      status: toExamStatus(exam.status),
      subject: exam.subject?.name ?? getString(meta, "subject") ?? "General",
      grade: exam.gradeEntity?.name ?? getString(meta, "grade") ?? "General",
      difficulty: toExamDifficulty(getNumber(meta, "difficulty")),
      totalScore: getNumber(meta, "totalScore") || 100,
      durationMin: getNumber(meta, "durationMin") || 60,
      questionCount: getNumber(meta, "questionCount") || 0,
      scheduledAt: exam.startTime?.toISOString(),
      createdAt: exam.createdAt.toISOString(),
      updatedAt: exam.updatedAt?.toISOString(),
      tags: getStringArray(meta, "tags") || [],
    }
  })

  if (params.difficulty && params.difficulty !== "all") {
    const d = parseInt(params.difficulty)
    result = result.filter((e) => e.difficulty === d)
  }

  return result
})

export const getExamById = cache(async (id: string, scope?: DataScope) => {
  const exam = await db.query.exams.findFirst({
    where: eq(exams.id, id),
    with: {
      subject: true,
      gradeEntity: true,
      questions: {
        orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
        with: {
            question: true
        }
      }
    }
  })

  if (!exam) return null

  // Data scope verification for single-item fetch
  if (scope && scope.type !== "all") {
    if (scope.type === "owned" && exam.creatorId !== scope.userId) {
      return null
    }
    if (scope.type === "grade_managed" && scope.gradeIds.length > 0 && !scope.gradeIds.includes(exam.gradeId ?? "")) {
      return null
    }
    if (scope.type === "class_taught" && scope.classIds.length > 0) {
      const classGradeMap = await getClassGradeIdsByClassIds(scope.classIds)
      const gradeIds = Array.from(new Set(classGradeMap.values()))
      if (gradeIds.length > 0 && !gradeIds.includes(exam.gradeId ?? "")) {
        return null
      }
    }
  }

  const meta = parseExamMeta(exam.description || null)

  return {
    id: exam.id,
    title: exam.title,
    status: toExamStatus(exam.status),
    subject: exam.subject?.name ?? getString(meta, "subject") ?? "General",
    grade: exam.gradeEntity?.name ?? getString(meta, "grade") ?? "General",
    difficulty: toExamDifficulty(getNumber(meta, "difficulty")),
    totalScore: getNumber(meta, "totalScore") || 100,
    durationMin: getNumber(meta, "durationMin") || 60,
    scheduledAt: exam.startTime?.toISOString(),
    createdAt: exam.createdAt.toISOString(),
    updatedAt: exam.updatedAt?.toISOString(),
    tags: getStringArray(meta, "tags") || [],
    structure: exam.structure as unknown,
    questions: exam.questions.map((eqRel) => ({
      id: eqRel.questionId,
      score: eqRel.score ?? 0,
      order: eqRel.order ?? 0,
    })),
  }
})

export const omitScheduledAtFromDescription = (description: string | null): string => {
  if (!description) return "{}"
  try {
    const parsed: unknown = JSON.parse(description)
    if (isRecord(parsed)) {
      const rest = { ...parsed }
      delete rest.scheduledAt
      return JSON.stringify(rest)
    }
    return description
  } catch {
    return description || "{}"
  }
}

export const resolveSubjectGradeNames = async (input: {
  subjectId?: string
  gradeId?: string
}): Promise<{ subjectName: string | null; gradeName: string | null }> => {
  const [subjectName, gradeName] = await Promise.all([
    input.subjectId ? getSubjectNameById(input.subjectId) : Promise.resolve(null),
    input.gradeId ? getGradeNameById(input.gradeId) : Promise.resolve(null),
  ])
  return {
    subjectName,
    gradeName,
  }
}

export const buildExamDescription = (input: {
  subject: string
  grade: string
  difficulty: number
  totalScore: number
  durationMin: number
  scheduledAt?: string
  questionCount?: number
}) => JSON.stringify({
  subject: input.subject,
  grade: input.grade,
  difficulty: input.difficulty,
  totalScore: input.totalScore,
  durationMin: input.durationMin,
  scheduledAt: input.scheduledAt,
  questionCount: input.questionCount,
})

export type ExamModeConfig = {
  examMode: "homework" | "timed" | "proctored"
  durationMinutes: number | null
  shuffleQuestions: boolean
  allowLateStart: boolean
  lateStartGraceMinutes: number
  antiCheatEnabled: boolean
}

export const persistExamDraft = async (input: {
  examId: string
  title: string
  creatorId: string
  subjectId: string
  gradeId: string
  scheduledAt?: string
  description: string
  examModeConfig?: ExamModeConfig
}) => {
  await db.insert(exams).values({
    id: input.examId,
    title: input.title,
    description: input.description,
    creatorId: input.creatorId,
    subjectId: input.subjectId,
    gradeId: input.gradeId,
    startTime: input.scheduledAt ? new Date(input.scheduledAt) : null,
    status: "draft",
    examMode: input.examModeConfig?.examMode ?? "homework",
    durationMinutes: input.examModeConfig?.durationMinutes ?? null,
    shuffleQuestions: input.examModeConfig?.shuffleQuestions ?? false,
    allowLateStart: input.examModeConfig?.allowLateStart ?? false,
    lateStartGraceMinutes: input.examModeConfig?.lateStartGraceMinutes ?? 0,
    antiCheatEnabled: input.examModeConfig?.antiCheatEnabled ?? false,
  })
}

const buildOrderedQuestionsFromStructure = (
  structure: AiGeneratedStructureNode[],
  generated: AiGeneratedQuestion[]
): Array<{ id: string; score: number }> => {
  const questionById = new Map(generated.map((q) => [q.id, q] as const))
  const orderedQuestions: Array<{ id: string; score: number }> = []
  const collectOrder = (nodes: AiGeneratedStructureNode[]) => {
    for (const node of nodes) {
      if (node.type === "question" && typeof node.questionId === "string" && node.questionId) {
        const score = typeof node.score === "number" ? node.score : questionById.get(node.questionId)?.score ?? 0
        orderedQuestions.push({ id: node.questionId, score })
        continue
      }
      if (node.type === "group" && Array.isArray(node.children) && node.children.length > 0) {
        collectOrder(node.children)
      }
    }
  }
  collectOrder(structure)
  if (orderedQuestions.length === 0) {
    return generated.map((q) => ({ id: q.id, score: q.score ?? 0 }))
  }
  return orderedQuestions
}

export const persistAiGeneratedExamDraft = async (input: {
  examId: string
  title: string
  creatorId: string
  subjectId: string
  gradeId: string
  scheduledAt?: string
  description: string
  structure: AiGeneratedStructureNode[]
  generated: AiGeneratedQuestion[]
  examModeConfig?: ExamModeConfig
}): Promise<void> => {
  const orderedQuestions = buildOrderedQuestionsFromStructure(input.structure, input.generated)

  // P0-1 fix: create questions via questions module data-access instead of direct table insert.
  // createQuestionWithRelations generates new IDs, so we remap structure references accordingly.
  const questionIdMapping = new Map<string, string>()
  for (const q of input.generated) {
    const newQuestionId = await createQuestionWithRelations(
      {
        content: q.content,
        type: q.type,
        difficulty: q.difficulty,
      },
      input.creatorId
    )
    questionIdMapping.set(q.id, newQuestionId)
  }

  const remappedOrderedQuestions = orderedQuestions
    .map((q) => {
      const mappedId = questionIdMapping.get(q.id)
      return mappedId ? { id: mappedId, score: q.score } : null
    })
    .filter((q): q is { id: string; score: number } => q !== null)

  await db.transaction(async (tx) => {
    await tx.insert(exams).values({
      id: input.examId,
      title: input.title,
      description: input.description,
      creatorId: input.creatorId,
      subjectId: input.subjectId,
      gradeId: input.gradeId,
      startTime: input.scheduledAt ? new Date(input.scheduledAt) : null,
      status: "draft",
      structure: input.structure,
      examMode: input.examModeConfig?.examMode ?? "homework",
      durationMinutes: input.examModeConfig?.durationMinutes ?? null,
      shuffleQuestions: input.examModeConfig?.shuffleQuestions ?? false,
      allowLateStart: input.examModeConfig?.allowLateStart ?? false,
      lateStartGraceMinutes: input.examModeConfig?.lateStartGraceMinutes ?? 0,
      antiCheatEnabled: input.examModeConfig?.antiCheatEnabled ?? false,
    })

    if (remappedOrderedQuestions.length > 0) {
      await tx.insert(examQuestions).values(
        remappedOrderedQuestions.map((q, idx) => ({
          examId: input.examId,
          questionId: q.id,
          score: q.score ?? 0,
          order: idx,
        }))
      )
    }
  })
}

export type ExamsDashboardStats = {
  examCount: number
}

export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<ExamsDashboardStats> => {
  const conditions = []

  if (scope && scope.type !== "all") {
    if (scope.type === "owned") {
      conditions.push(eq(exams.creatorId, scope.userId))
    }
    if (scope.type === "grade_managed" && scope.gradeIds.length > 0) {
      conditions.push(inArray(exams.gradeId, scope.gradeIds))
    }
    if (scope.type === "class_taught" && scope.classIds.length > 0) {
      const classGradeMap = await getClassGradeIdsByClassIds(scope.classIds)
      const gradeIds = Array.from(new Set(classGradeMap.values()))
      if (gradeIds.length > 0) {
        conditions.push(inArray(exams.gradeId, gradeIds))
      }
    }
  }

  const [row] = await db
    .select({ value: count() })
    .from(exams)
    .where(conditions.length ? and(...conditions) : undefined)

  return { examCount: Number(row?.value ?? 0) }
})

/**
 * Get exam creator ID for ownership check.
 * Returns null if exam not found.
 */
export const getExamCreatorId = async (examId: string): Promise<string | null> => {
  const exam = await db.query.exams.findFirst({
    where: eq(exams.id, examId),
    columns: { creatorId: true },
  })
  return exam?.creatorId ?? null
}

/**
 * Update an exam, optionally replacing its questions.
 * Preserves original behavior: questions replacement is not transactional with exam field update.
 */
export const updateExamWithQuestions = async (
  examId: string,
  data: {
    questions?: Array<{ id: string; score: number }>
    structure?: unknown
    status?: ExamStatus
  }
): Promise<void> => {
  if (data.questions) {
    await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
    if (data.questions.length > 0) {
      await db.insert(examQuestions).values(
        data.questions.map((q, idx) => ({
          examId,
          questionId: q.id,
          score: q.score ?? 0,
          order: idx,
        }))
      )
    }
  }

  const updateData: Partial<typeof exams.$inferInsert> = {}
  if (data.status) updateData.status = data.status
  if (data.structure !== undefined) updateData.structure = data.structure

  if (Object.keys(updateData).length > 0) {
    await db.update(exams).set(updateData).where(eq(exams.id, examId))
  }
}

/**
 * Delete an exam by ID.
 */
export const deleteExamById = async (examId: string): Promise<void> => {
  await db.delete(exams).where(eq(exams.id, examId))
}

/**
 * Duplicate an exam (including its questions) in a transaction.
 * Returns the new exam ID, or null if the source exam is not found.
 */
export const duplicateExam = async (
  sourceExamId: string,
  newCreatorId: string
): Promise<string | null> => {
  const source = await db.query.exams.findFirst({
    where: eq(exams.id, sourceExamId),
    with: {
      questions: {
        orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
      },
    },
  })

  if (!source) return null

  const newExamId = createId()

  await db.transaction(async (tx) => {
    await tx.insert(exams).values({
      id: newExamId,
      title: `${source.title} (Copy)`,
      description: omitScheduledAtFromDescription(source.description),
      creatorId: newCreatorId,
      startTime: null,
      endTime: null,
      status: "draft",
      structure: source.structure,
    })

    if (source.questions.length > 0) {
      await tx.insert(examQuestions).values(
        source.questions.map((q) => ({
          examId: newExamId,
          questionId: q.questionId,
          score: q.score ?? 0,
          order: q.order ?? 0,
        }))
      )
    }
  })

  return newExamId
}

/**
 * Get exam preview data (structure + questions).
 * Returns null if exam not found.
 */
export const getExamPreview = async (
  examId: string
): Promise<{ structure: unknown; questions: Array<{ id: string }> } | null> => {
  const exam = await db.query.exams.findFirst({
    where: eq(exams.id, examId),
    with: {
      questions: {
        orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
        with: {
          question: true,
        },
      },
    },
  })

  if (!exam) return null

  const questions = exam.questions.map((eqRel) => eqRel.question)
  return {
    structure: exam.structure,
    questions,
  }
}

/**
 * Get all subjects for exam forms.
 * Delegates to school module data-access to avoid direct DB queries on subjects table.
 */
export const getExamSubjects = async (): Promise<Array<{ id: string; name: string }>> => {
  const allSubjects = await getSubjectOptions()
  return allSubjects.map((s) => ({ id: s.id, name: s.name }))
}

/**
 * Get all grades for exam forms.
 * Delegates to school module data-access to avoid direct DB queries on grades table.
 */
export const getExamGrades = async (): Promise<Array<{ id: string; name: string }>> => {
  const allGrades = await getGradeOptions()
  return allGrades.map((g) => ({ id: g.id, name: g.name }))
}

// ---------------------------------------------------------------------------
// Cross-module query interfaces (供其他模块调用，避免直查 exams/examSubmissions 表)
// ---------------------------------------------------------------------------

/**
 * 获取指定年级 ID 列表对应的所有考试 ID。
 * 供 homework/grades 等模块跨模块调用使用。
 */
export const getExamIdsByGradeIds = async (gradeIds: string[]): Promise<string[]> => {
  if (gradeIds.length === 0) return []
  const rows = await db
    .select({ id: exams.id })
    .from(exams)
    .where(inArray(exams.gradeId, gradeIds))
  return rows.map((r) => r.id)
}

/**
 * 获取考试的基本信息（含题目列表），供 homework 模块创建作业时使用。
 * 返回的数据包含 examId、title、subjectId、structure 和题目列表。
 */
export type ExamWithQuestionsForHomework = {
  id: string
  title: string
  subjectId: string | null
  structure: unknown
  questions: Array<{ questionId: string; score: number | null; order: number | null }>
}

export const getExamWithQuestionsForHomework = async (
  examId: string
): Promise<ExamWithQuestionsForHomework | null> => {
  const exam = await db.query.exams.findFirst({
    where: eq(exams.id, examId),
    with: {
      questions: {
        orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
      },
    },
  })
  if (!exam) return null
  return {
    id: exam.id,
    title: exam.title,
    subjectId: exam.subjectId,
    structure: exam.structure,
    questions: exam.questions.map((q) => ({
      questionId: q.questionId,
      score: q.score ?? null,
      order: q.order ?? null,
    })),
  }
}

/**
 * 获取多个考试的 subjectId 映射（examId -> subjectId）。
 * 供 homework 模块查询作业对应科目时使用。
 */
export const getExamSubjectIdMap = async (examIds: string[]): Promise<Map<string, string | null>> => {
  if (examIds.length === 0) return new Map()
  const rows = await db
    .select({ id: exams.id, subjectId: exams.subjectId })
    .from(exams)
    .where(inArray(exams.id, examIds))
  const map = new Map<string, string | null>()
  for (const r of rows) map.set(r.id, r.subjectId)
  return map
}

/**
 * 获取考试标题。
 * 供 proctoring 等模块跨模块调用使用。
 */
export const getExamTitleById = async (examId: string): Promise<string | null> => {
  const [row] = await db
    .select({ title: exams.title })
    .from(exams)
    .where(eq(exams.id, examId))
    .limit(1)
  return row?.title ?? null
}

/**
 * 获取考试的基本信息（含监考模式相关字段），供 proctoring 模块使用。
 */
export type ExamForProctoring = {
  id: string
  title: string
  examMode: string | null
  durationMinutes: number | null
  shuffleQuestions: boolean | null
  allowLateStart: boolean | null
  lateStartGraceMinutes: number | null
  antiCheatEnabled: boolean | null
}

export const getExamForProctoringCrossModule = async (examId: string): Promise<ExamForProctoring | null> => {
  const exam = await db.query.exams.findFirst({
    where: eq(exams.id, examId),
  })
  if (!exam) return null
  return {
    id: exam.id,
    title: exam.title,
    examMode: exam.examMode,
    durationMinutes: exam.durationMinutes ?? null,
    shuffleQuestions: exam.shuffleQuestions ?? false,
    allowLateStart: exam.allowLateStart ?? false,
    lateStartGraceMinutes: exam.lateStartGraceMinutes ?? 0,
    antiCheatEnabled: exam.antiCheatEnabled ?? false,
  }
}

/**
 * 校验提交记录归属（监考事件上报前的安全校验）。
 * 供 proctoring 模块跨模块调用使用。
 */
export const getExamSubmissionForProctoringCrossModule = async (
  submissionId: string,
  studentId: string
): Promise<{ id: string; examId: string; studentId: string } | null> => {
  const submission = await db.query.examSubmissions.findFirst({
    where: and(
      eq(examSubmissions.id, submissionId),
      eq(examSubmissions.studentId, studentId),
    ),
    columns: {
      id: true,
      examId: true,
      studentId: true,
    },
  })
  return submission ?? null
}

/**
 * 获取考试提交记录及其答题数据，供 diagnostic 模块更新知识点掌握度使用。
 */
export type ExamSubmissionWithAnswers = {
  studentId: string
  answers: Array<{ questionId: string; score: number | null }>
}

export const getExamSubmissionWithAnswers = async (
  submissionId: string
): Promise<ExamSubmissionWithAnswers | null> => {
  const [submission] = await db
    .select({ studentId: examSubmissions.studentId })
    .from(examSubmissions)
    .where(eq(examSubmissions.id, submissionId))
    .limit(1)
  if (!submission) return null

  const answers = await db
    .select({
      questionId: submissionAnswers.questionId,
      score: submissionAnswers.score,
    })
    .from(submissionAnswers)
    .where(eq(submissionAnswers.submissionId, submissionId))

  return {
    studentId: submission.studentId,
    answers,
  }
}

/**
 * 获取一场考试的所有提交记录（含学生 ID 和状态），供 proctoring 模块使用。
 */
export type ExamSubmissionForProctoringSummary = {
  id: string
  studentId: string
  status: string | null
}

export const getExamSubmissionsForExam = async (
  examId: string
): Promise<ExamSubmissionForProctoringSummary[]> => {
  const rows = await db
    .select({
      id: examSubmissions.id,
      studentId: examSubmissions.studentId,
      status: examSubmissions.status,
    })
    .from(examSubmissions)
    .where(eq(examSubmissions.examId, examId))
  return rows
}

/**
 * 批量插入考试题目（跨模块写接口）。
 * 供 lesson-preparation 模块发布课案为作业时使用，避免直接操作 examQuestions 表。
 */
export const addExamQuestions = async (
  examId: string,
  items: Array<{ questionId: string; score: number; order: number }>
): Promise<void> => {
  if (items.length === 0) return
  await db.insert(examQuestions).values(
    items.map((it) => ({
      examId,
      questionId: it.questionId,
      score: it.score,
      order: it.order,
    }))
  )
}
