import "server-only"

import { createId } from "@paralleldrive/cuid2"
import { and, asc, eq, inArray, sql, type SQL } from "drizzle-orm"
import { getTranslations } from "next-intl/server"

import { db } from "@/shared/db"
import {
  courseSelections,
  electiveCourses,
} from "@/shared/db/schema"
import { BusinessError } from "@/shared/lib/action-utils"
import { sendNotification } from "@/modules/notifications"

import { getElectiveCreditLimit, getCapacityNotifyThreshold } from "./data-access-settings"
import { getStudentGradeId } from "./data-access-selections"
import type { CourseSelectionStatus } from "./types"

/**
 * 选课模块业务错误码（与 i18n key `errors.*` 对应）。
 * 由 actions 层根据 code 通过 getTranslations 翻译为用户可见文案。
 */
export type ElectiveErrorCode =
  | "courseNotFound"
  | "selectionNotOpen"
  | "selectionNotStarted"
  | "selectionEnded"
  | "alreadySelected"
  | "scheduleConflict"
  | "creditExceeded"
  | "noActiveSelection"
  | "dropDeadlinePassed"

/**
 * 选课模块业务错误（带 i18n code 与参数）。
 * actions 层捕获后用 getTranslations(`elective.errors.${code}`) 翻译。
 */
export class ElectiveBusinessError extends BusinessError {
  constructor(
    public readonly code: ElectiveErrorCode,
    public readonly params?: Record<string, string | number>
  ) {
    super(`elective.errors.${code}`, code)
    this.name = "ElectiveBusinessError"
  }
}

/**
 * 构建 lotteryRank 的 CASE SQL 表达式（纯函数，便于测试 SQL 片段结构）。
 */
export function buildLotteryRankCase(ids: string[], startRank: number): SQL {
  const branches = ids.map(
    (id, idx) => sql`WHEN ${id} THEN ${startRank + idx}`
  )
  return sql`CASE ${courseSelections.id} ${sql.join(branches, sql` `)} END`
}

/**
 * 星期字符串归一化映射（纯函数，便于测试）。
 * 支持中英文全称与缩写，统一映射为 1-7 数字字符串。
 */
const DAY_NORMALIZE_MAP: Readonly<Record<string, string>> = Object.freeze({
  // 中文
  "周一": "1", "周二": "2", "周三": "3", "周四": "4",
  "周五": "5", "周六": "6", "周日": "7", "周天": "7",
  "星期一": "1", "星期二": "2", "星期三": "3", "星期四": "4",
  "星期五": "5", "星期六": "6", "星期日": "7", "星期天": "7",
  // 英文全称
  monday: "1", tuesday: "2", wednesday: "3", thursday: "4",
  friday: "5", saturday: "6", sunday: "7",
  // 英文缩写
  mon: "1", tue: "2", wed: "3", thu: "4",
  fri: "5", sat: "6", sun: "7",
})

export function normalizeDay(day: string): string {
  return DAY_NORMALIZE_MAP[day.toLowerCase()] ?? day
}

/**
 * 解析课程 schedule 字段为可比较的时间段数组（纯函数，便于测试）。
 *
 * 支持多时段（以逗号或分号分隔），例如：
 * - "周一 14:00-15:30"
 * - "Mon 14:00-15:30, Wed 16:00-17:30"
 * - "周一 14:00-15:30；周三 16:00-17:30"
 *
 * 返回空数组表示无法解析（不参与冲突检测）。
 */
export function parseSchedule(
  schedule: string | null
): Array<{ day: string; start: string; end: string }> {
  if (!schedule || schedule.length === 0) return []

  // 按逗号、分号、中文分号拆分多个时段
  const segments = schedule.split(/[,，;；]/).map((s) => s.trim()).filter(Boolean)
  const result: Array<{ day: string; start: string; end: string }> = []

  // 支持中英文星期与英文全称/缩写
  const dayPattern = "周[一二三四五六日天]|星期[一二三四五六日天]|[Mm]on(?:day)?|[Tt]ue(?:sday)?|[Ww]ed(?:nesday)?|[Tt]hu(?:rsday)?|[Ff]ri(?:day)?|[Ss]at(?:urday)?|[Ss]un(?:day)?"
  const timePattern = "(\\d{1,2}):(\\d{2})"
  const re = new RegExp(
    `^(${dayPattern})\\s+${timePattern}\\s*[-~～至到]\\s*${timePattern}$`,
    "i"
  )

  for (const seg of segments) {
    const match = seg.match(re)
    if (!match) continue
    const [, day, startH, startM, endH, endM] = match
    if (!day || !startH || !startM || !endH || !endM) continue
    result.push({
      day,
      start: `${startH.padStart(2, "0")}:${startM}`,
      end: `${endH.padStart(2, "0")}:${endM}`,
    })
  }
  return result
}

/**
 * 检测两组时间段是否存在冲突（纯函数，便于测试）。
 * 仅当 day 相同（归一化后）且时间区间重叠时判定为冲突。
 */
export function isScheduleConflict(
  a: { day: string; start: string; end: string },
  b: { day: string; start: string; end: string }
): boolean {
  if (normalizeDay(a.day) !== normalizeDay(b.day)) return false
  return a.start < b.end && b.start < a.end
}

/**
 * 检测学生选课时间冲突（P2 建议：选课时间冲突检测）。
 * 查询学生已选/已录取的课程，与新课程 schedule 比对。
 */
async function checkScheduleConflict(
  tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
  studentId: string,
  newCourseId: string
): Promise<boolean> {
  const [newCourse] = await tx
    .select({ schedule: electiveCourses.schedule })
    .from(electiveCourses)
    .where(eq(electiveCourses.id, newCourseId))
    .limit(1)
  const newSlots = parseSchedule(newCourse?.schedule ?? null)
  if (newSlots.length === 0) return false

  const existingCourses = await tx
    .select({
      schedule: electiveCourses.schedule,
    })
    .from(courseSelections)
    .innerJoin(electiveCourses, eq(electiveCourses.id, courseSelections.courseId))
    .where(
      and(
        eq(courseSelections.studentId, studentId),
        inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
      )
    )

  for (const row of existingCourses) {
    const existingSlots = parseSchedule(row.schedule)
    for (const newSlot of newSlots) {
      for (const existingSlot of existingSlots) {
        if (isScheduleConflict(newSlot, existingSlot)) {
          return true
        }
      }
    }
  }
  return false
}

/**
 * 检测学生学分是否超限（P2-4 重构：使用 system_settings 配置化上限）。
 * 查询学生已选课程的学分总和，加上新课程学分后是否超过上限。
 *
 * 上限来源（按优先级）：
 * 1. `creditLimit:grade:<studentGradeId>`（按年级配置）
 * 2. `creditLimit:default`（全局配置）
 * 3. 默认值 10
 */
async function checkCreditLimit(
  tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
  studentId: string,
  newCourseId: string,
  studentGradeId: string | null
): Promise<{ exceeded: boolean; current: number; max: number }> {
  const [newCourse] = await tx
    .select({ credit: electiveCourses.credit })
    .from(electiveCourses)
    .where(eq(electiveCourses.id, newCourseId))
    .limit(1)
  const newCredit = Number(newCourse?.credit ?? 0)

  const existing = await tx
    .select({
      credit: electiveCourses.credit,
    })
    .from(courseSelections)
    .innerJoin(electiveCourses, eq(electiveCourses.id, courseSelections.courseId))
    .where(
      and(
        eq(courseSelections.studentId, studentId),
        inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
      )
    )

  const currentCredit = existing.reduce((sum, row) => sum + Number(row.credit ?? 0), 0)
  const total = currentCredit + newCredit
  // 配置化上限：按年级或全局，默认 10
  const max = await getElectiveCreditLimit(studentGradeId)
  return {
    exceeded: total > max,
    current: total,
    max,
  }
}

export async function runLottery(courseId: string): Promise<{
  enrolled: number
  waitlist: number
}> {
  const [courseRows, selections] = await Promise.all([
    db
      .select()
      .from(electiveCourses)
      .where(eq(electiveCourses.id, courseId))
      .limit(1),
    db
      .select()
      .from(courseSelections)
      .where(
        and(
          eq(courseSelections.courseId, courseId),
          eq(courseSelections.status, "selected")
        )
      )
      .orderBy(asc(courseSelections.priority), asc(courseSelections.selectedAt)),
  ])
  const course = courseRows[0]
  if (!course) throw new ElectiveBusinessError("courseNotFound")

  if (selections.length === 0) {
    return { enrolled: 0, waitlist: 0 }
  }

  // Fisher-Yates shuffle: 无偏均匀随机排列，避免 sort(() => Math.random() - 0.5) 的分布偏差
  const shuffled = [...selections]
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
  }
  const capacity = course.capacity
  const now = new Date()

  const enrolledIds: string[] = []
  const waitlistIds: string[] = []
  for (let i = 0; i < shuffled.length; i++) {
    if (i < capacity) {
      enrolledIds.push(shuffled[i].id)
    } else {
      waitlistIds.push(shuffled[i].id)
    }
  }

  const enrolledCount = enrolledIds.length
  const waitlistCount = waitlistIds.length

  // P1-12 改进：抽签后不强制 close 课程，保留 status="open" 以便管理员重抽。
  // 管理员可手动通过 closeSelection 关闭选课。
  await db.transaction(async (tx) => {
    if (enrolledIds.length > 0) {
      await tx
        .update(courseSelections)
        .set({
          status: "enrolled",
          lotteryRank: buildLotteryRankCase(enrolledIds, 1),
          enrolledAt: now,
          updatedAt: now,
        })
        .where(inArray(courseSelections.id, enrolledIds))
    }
    if (waitlistIds.length > 0) {
      await tx
        .update(courseSelections)
        .set({
          status: "waitlist",
          lotteryRank: buildLotteryRankCase(waitlistIds, capacity + 1),
          updatedAt: now,
        })
        .where(inArray(courseSelections.id, waitlistIds))
    }
    // 仅更新 enrolledCount，不自动关闭课程
    await tx
      .update(electiveCourses)
      .set({ enrolledCount, updatedAt: now })
      .where(eq(electiveCourses.id, courseId))
  })

  return { enrolled: enrolledCount, waitlist: waitlistCount }
}

export async function selectCourse(
  courseId: string,
  studentId: string,
  priority?: number
): Promise<{ status: CourseSelectionStatus }> {
  // P2-4：先查询学生年级 ID（用于按年级配置的学分上限）
  const studentGradeId = await getStudentGradeId(studentId)

  return db.transaction(async (tx) => {
    // 锁定课程行，防止 FCFS 模式下并发超卖
    const [course] = await tx
      .select()
      .from(electiveCourses)
      .where(eq(electiveCourses.id, courseId))
      .for("update")
      .limit(1)
    if (!course) throw new ElectiveBusinessError("courseNotFound")
    if (course.status !== "open") throw new ElectiveBusinessError("selectionNotOpen")

    const now = new Date()
    if (course.selectionStartAt && now < course.selectionStartAt) {
      throw new ElectiveBusinessError("selectionNotStarted")
    }
    if (course.selectionEndAt && now > course.selectionEndAt) {
      throw new ElectiveBusinessError("selectionEnded")
    }

    const [existing] = await tx
      .select()
      .from(courseSelections)
      .where(
        and(
          eq(courseSelections.courseId, courseId),
          eq(courseSelections.studentId, studentId),
          inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
        )
      )
      .limit(1)
    if (existing) throw new ElectiveBusinessError("alreadySelected")

    // P2 建议：选课时间冲突检测
    const hasConflict = await checkScheduleConflict(tx, studentId, courseId)
    if (hasConflict) {
      throw new ElectiveBusinessError("scheduleConflict")
    }

    // P2-4：学分上限校验（使用按年级配置的上限）
    const creditCheck = await checkCreditLimit(tx, studentId, courseId, studentGradeId)
    if (creditCheck.exceeded) {
      throw new ElectiveBusinessError("creditExceeded", {
        current: creditCheck.current,
        max: creditCheck.max,
      })
    }

    const id = createId()
    let status: CourseSelectionStatus = "selected"
    let enrolledAt: Date | null = null

    if (course.selectionMode === "fcfs" && course.enrolledCount < course.capacity) {
      status = "enrolled"
      enrolledAt = now
      const newEnrolledCount = course.enrolledCount + 1
      await tx
        .update(electiveCourses)
        .set({
          enrolledCount: newEnrolledCount,
          updatedAt: now,
        })
        .where(eq(electiveCourses.id, courseId))

      // P2-4：容量阈值通知（fire-and-forget，不阻塞事务）
      // 仅在跨过阈值时触发（避免每次选课都通知）
      void notifyCapacityThresholdIfNeeded(course, newEnrolledCount)
    } else if (course.selectionMode === "fcfs") {
      status = "waitlist"
    }

    await tx.insert(courseSelections).values({
      id,
      courseId,
      studentId,
      status,
      priority: priority ?? 1,
      selectedAt: now,
      enrolledAt,
    })

    return { status }
  })
}

/**
 * 容量阈值通知（P2-4 新增）。
 *
 * 触发条件：FCFS 模式下，录取后 `enrolledCount >= capacity * threshold`。
 * 阈值来自 system_settings（`capacityNotifyThreshold`，默认 0.9）。
 *
 * 通知接收者：课程创建者/教师（teacherId）。
 * 通知为 fire-and-forget，失败不影响选课流程。
 *
 * 防重复策略：仅当 `enrolledCount === Math.ceil(capacity * threshold)` 时触发，
 * 即只在跨过阈值的瞬间触发一次。
 */
async function notifyCapacityThresholdIfNeeded(
  course: { teacherId: string; id: string; name: string; capacity: number },
  newEnrolledCount: number
): Promise<void> {
  try {
    const threshold = await getCapacityNotifyThreshold()
    const triggerPoint = Math.ceil(course.capacity * threshold)
    // 仅在跨过阈值瞬间触发（避免每次选课都通知）
    if (newEnrolledCount !== triggerPoint) return

    // P2-4：通知文案使用 i18n 翻译键
    const t = await getTranslations("elective")
    const title = t("notifications.capacityWarningTitle", { courseName: course.name })
    const content = t("notifications.capacityWarningContent", {
      courseName: course.name,
      enrolled: newEnrolledCount,
      capacity: course.capacity,
      percent: Math.round(threshold * 100),
    })

    await sendNotification({
      userId: course.teacherId,
      title,
      content,
      type: "warning",
      actionUrl: `/admin/elective/${course.id}`,
      metadata: {
        courseId: course.id,
        enrolledCount: newEnrolledCount,
        capacity: course.capacity,
        threshold,
      },
    })
  } catch {
    // fire-and-forget：通知失败不影响选课事务
  }
}

export async function dropCourse(
  courseId: string,
  studentId: string,
  dropReason?: string
): Promise<void> {
  await db.transaction(async (tx) => {
    const [existing] = await tx
      .select()
      .from(courseSelections)
      .where(
        and(
          eq(courseSelections.courseId, courseId),
          eq(courseSelections.studentId, studentId),
          inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
        )
      )
      .limit(1)
    if (!existing) throw new ElectiveBusinessError("noActiveSelection")

    // 锁定课程行，确保 enrolledCount 更新与候补递补的原子性
    const [course] = await tx
      .select()
      .from(electiveCourses)
      .where(eq(electiveCourses.id, courseId))
      .for("update")
      .limit(1)

    const now = new Date()
    // P2-4：退课截止时间校验
    if (course?.dropDeadline && now > course.dropDeadline) {
      throw new ElectiveBusinessError("dropDeadlinePassed")
    }

    await tx
      .update(courseSelections)
      .set({
        status: "dropped",
        droppedAt: now,
        // P2-4：记录退课理由（trim 后存入，空字符串转为 null）
        dropReason: dropReason && dropReason.trim().length > 0 ? dropReason.trim() : null,
        updatedAt: now,
      })
      .where(eq(courseSelections.id, existing.id))

    if (existing.status === "enrolled" && course && course.selectionMode === "fcfs") {
      const newEnrolledCount = Math.max(0, course.enrolledCount - 1)
      await tx
        .update(electiveCourses)
        .set({ enrolledCount: newEnrolledCount, updatedAt: now })
        .where(eq(electiveCourses.id, courseId))

      const [nextWait] = await tx
        .select()
        .from(courseSelections)
        .where(
          and(
            eq(courseSelections.courseId, courseId),
            eq(courseSelections.status, "waitlist")
          )
        )
        .orderBy(asc(courseSelections.priority), asc(courseSelections.selectedAt))
        .limit(1)
      if (nextWait) {
        await tx
          .update(courseSelections)
          .set({
            status: "enrolled",
            enrolledAt: now,
            updatedAt: now,
          })
          .where(eq(courseSelections.id, nextWait.id))
        await tx
          .update(electiveCourses)
          .set({ enrolledCount: newEnrolledCount + 1, updatedAt: now })
          .where(eq(electiveCourses.id, courseId))
      }
    }
  })
}
