import { db } from "@/shared/db"
import { exams, examQuestions } from "@/shared/db/schema"
import { 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 { escapeLikePattern } from "@/shared/lib/action-utils"
import { isRecord } from "@/shared/lib/type-guards"

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

// 跨模块接口已拆分至 data-access-cross-module.ts，文末 re-export 保持向后兼容

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

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

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

export 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"

export 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 = `%${escapeLikePattern(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))
    } else {
      // P0 fix: empty grade set must NOT bypass filtering (would expose all exams)
      conditions.push(eq(exams.id, "__none__"))
    }
  } else if (params.scope.type === "class_taught") {
    // P0 fix: class_taught scope with no classIds must return nothing
    conditions.push(eq(exams.id, "__none__"))
  }
  if (params.scope.type === "grade_managed" && params.scope.gradeIds.length > 0) {
    conditions.push(inArray(exams.gradeId, params.scope.gradeIds))
  } else if (params.scope.type === "grade_managed") {
    // P0 fix: grade_managed scope with no gradeIds must return nothing
    conditions.push(eq(exams.id, "__none__"))
  }
  // "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",
      subjectId: exam.subjectId ?? null,
      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, 10)
    if (!Number.isNaN(d)) {
      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") {
      // P0 fix: empty gradeIds must NOT bypass filtering (would leak exam details)
      if (scope.gradeIds.length === 0) return null
      if (!scope.gradeIds.includes(exam.gradeId ?? "")) {
        return null
      }
    }
    if (scope.type === "class_taught") {
      // P0 fix: empty classIds must NOT bypass filtering (would leak exam details)
      if (scope.classIds.length === 0) return null
      const classGradeMap = await getClassGradeIdsByClassIds(scope.classIds)
      const gradeIds = Array.from(new Set(classGradeMap.values()))
      if (gradeIds.length === 0) return null
      if (!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",
    subjectId: exam.subjectId ?? null,
    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,
    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)
      }
      if (node.type === "section" && 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,
        }))
      )
    }
  })
}

/**
 * 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))
  }
}

/**
 * 从富文本编辑器更新已有试卷（事务）：更新 exam 标题与 structure，并重建 exam_questions 关联。
 * P0-4 修复：将原 actions.ts 中直接操作 DB 的事务逻辑下沉到 data-access 层。
 */
export const updateExamWithRichEditorData = async (
  examId: string,
  data: {
    title: string
    structure: unknown
    orderedQuestions: Array<{ id: string; score: number }>
  }
): Promise<void> => {
  await db.transaction(async (tx) => {
    await tx.update(exams).set({
      title: data.title,
      structure: data.structure,
    }).where(eq(exams.id, examId))

    await tx.delete(examQuestions).where(eq(examQuestions.examId, examId))
    if (data.orderedQuestions.length > 0) {
      await tx.insert(examQuestions).values(
        data.orderedQuestions.map((q, idx) => ({
          examId,
          questionId: q.id,
          score: q.score,
          order: idx,
        }))
      )
    }
  })
}

/**
 * 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 }))
}

// ---------------------------------------------------------------------------
// Re-export 跨模块接口（保持向后兼容，调用方仍可从 data-access 导入）
// ---------------------------------------------------------------------------

export {
  getExamsDashboardStats,
  getExamIdsByGradeIds,
  getExamWithQuestionsForHomework,
  getExamSubjectIdMap,
  getExamTitleById,
  getExamForProctoringCrossModule,
  getExamSubmissionForProctoringCrossModule,
  getExamSubmissionWithAnswers,
  getExamSubmissionsForExam,
  addExamQuestions,
  getExamsByGradeId,
  getExamsForGradeEntry,
  getExamForGradeEntry,
} from "./data-access-cross-module"

export type {
  ExamsDashboardStats,
  ExamWithQuestionsForHomework,
  ExamForProctoring,
  ExamSubmissionWithAnswers,
  ExamSubmissionForProctoringSummary,
} from "./data-access-cross-module"
