import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"
import {
  Table,
  Button,
  Space,
  message,
  Modal,
  Form,
  Input,
  Tag,
  Pagination,
  Dropdown,
  Checkbox,
  Select,
} from "antd"
import type { ColumnsType } from "antd/es/table"
import { UploadOutlined, MoreOutlined, PlusOutlined } from "@ant-design/icons"
import { useTranslation } from "react-i18next"
import { TagEditorDialog } from "./TagEditorDialog"
import { getAvatarFromExtraJson, setAvatarInExtraJson } from "../utils/studentAvatar"
import { useResponsive, useIsMobile, useIsTablet } from "../hooks/useResponsive"

const createXlsxWorker = () => {
  return new Worker(new URL("../workers/xlsxWorker.ts", import.meta.url), {
    type: "module",
  })
}

interface student {
  id: number
  name: string
  group_name?: string | null
  score: number
  tags?: string[]
  tagIds?: number[]
  extra_json?: string | null
  avatarUrl?: string | null
}

interface BanYouClassroom {
  classId: string
  classNickName: string
  invitationCode?: string | null
  masterName?: string | null
  studentsNum?: number | null
  praiseCount?: number | null
  classAvatarPath?: string | null
  classAvatarDataUrl?: string | null
  isOwn?: boolean | null
}

interface BanYouMedal {
  key?: string
  uid?: string
  name: string
  type?: number
  medalType?: number
  value?: number
}

interface BanYouStudentItem {
  studentId: string
  studentName: string
}

interface BanYouTeamItem {
  teamId: string
  teamName: string
  students: BanYouStudentItem[]
}

interface BanYouTeamPlanOption {
  teamPlanId: number
  name?: string
}

interface BanYouClassroomDetail {
  medals: BanYouMedal[]
  students: BanYouStudentItem[]
  teams: BanYouTeamItem[]
  ungroupedStudents: BanYouStudentItem[]
  teamPlanIdUsed?: number
  teamPlans?: BanYouTeamPlanOption[]
  teamPlanSource?: string
}

export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
  const UNGROUPED_KEY = "__ungrouped__"
  const { t } = useTranslation()
  const [data, setData] = useState<student[]>([])
  const [loading, setLoading] = useState(false)
  const [currentPage, setCurrentPage] = useState(1)
  const [pageSize, setPageSize] = useState<number>(50)
  const [visible, setVisible] = useState(false)
  const [textImportVisible, setTextImportVisible] = useState(false)
  const [xlsxVisible, setXlsxVisible] = useState(false)
  const [banYouVisible, setBanYouVisible] = useState(false)
  const [tagEditVisible, setTagEditVisible] = useState(false)
  const [editingStudent, setEditingStudent] = useState<student | null>(null)
  const [groupEditVisible, setGroupEditVisible] = useState(false)
  const [groupEditStudent, setGroupEditStudent] = useState<student | null>(null)
  const [groupSaving, setGroupSaving] = useState(false)
  const [groupBoardVisible, setGroupBoardVisible] = useState(false)
  const [groupBoardSaving, setGroupBoardSaving] = useState(false)
  const [groupBoard, setGroupBoard] = useState<Record<string, student[]>>({})
  const [groupBoardOrder, setGroupBoardOrder] = useState<string[]>([])
  const [groupBoardNewGroupName, setGroupBoardNewGroupName] = useState("")
  const [pointerDraggingStudentId, setPointerDraggingStudentId] = useState<number | null>(null)
  const [pointerTargetGroup, setPointerTargetGroup] = useState<string | null>(null)
  const [pointerDragStudentName, setPointerDragStudentName] = useState("")
  const [pointerDraggingGroupKey, setPointerDraggingGroupKey] = useState<string | null>(null)
  const [pointerTargetGroupOrderKey, setPointerTargetGroupOrderKey] = useState<string | null>(null)
  const [pointerDragGroupName, setPointerDragGroupName] = useState("")
  const pointerDragGhostRef = useRef<HTMLDivElement | null>(null)
  const pointerDragPositionRef = useRef<{ x: number; y: number } | null>(null)
  const pointerDragRafRef = useRef<number | null>(null)
  const draggingStudentIdRef = useRef<number | null>(null)
  const pointerDragSourceGroupRef = useRef<string | null>(null)
  const pointerDragTargetGroupRef = useRef<string | null>(null)
  const pointerDragGroupGhostRef = useRef<HTMLDivElement | null>(null)
  const pointerDragGroupPositionRef = useRef<{ x: number; y: number } | null>(null)
  const pointerDragGroupRafRef = useRef<number | null>(null)
  const pointerDragGroupSourceRef = useRef<string | null>(null)
  const pointerDragGroupTargetRef = useRef<string | null>(null)
  const pointerDragGroupInsertAfterRef = useRef(false)
  const [avatarVisible, setAvatarVisible] = useState(false)
  const [avatarSaving, setAvatarSaving] = useState(false)
  const [avatarStudent, setAvatarStudent] = useState<student | null>(null)
  const [avatarValue, setAvatarValue] = useState<string | null>(null)
  const avatarInputRef = useRef<HTMLInputElement | null>(null)
  const [xlsxLoading, setXlsxLoading] = useState(false)
  const [textImportLoading, setTextImportLoading] = useState(false)
  const [xlsxFileName, setXlsxFileName] = useState("")
  const [xlsxAoa, setXlsxAoa] = useState<any[][]>([])
  const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null)
  const [textImportValue, setTextImportValue] = useState("")
  const [banYouCookie, setBanYouCookie] = useState("")
  const [banYouLoading, setBanYouLoading] = useState(false)
  const [banYouDetailLoading, setBanYouDetailLoading] = useState(false)
  const [banYouImportLoading, setBanYouImportLoading] = useState(false)
  const [banYouClassrooms, setBanYouClassrooms] = useState<BanYouClassroom[]>([])
  const [banYouDetailVisible, setBanYouDetailVisible] = useState(false)
  const [banYouSelectedClass, setBanYouSelectedClass] = useState<BanYouClassroom | null>(null)
  const [banYouDetail, setBanYouDetail] = useState<BanYouClassroomDetail | null>(null)
  const [banYouCheckedMedals, setBanYouCheckedMedals] = useState<string[]>([])
  const [banYouCheckedStudents, setBanYouCheckedStudents] = useState<string[]>([])
  const [banYouCheckedTeams, setBanYouCheckedTeams] = useState<string[]>([])
  const [banYouTeamPlanOptions, setBanYouTeamPlanOptions] = useState<BanYouTeamPlanOption[]>([])
  const [banYouSelectedTeamPlanId, setBanYouSelectedTeamPlanId] = useState<number | undefined>(
    undefined
  )
  const xlsxInputRef = useRef<HTMLInputElement | null>(null)
  const xlsxWorkerRef = useRef<Worker | null>(null)
  const [form] = Form.useForm()
  const [groupForm] = Form.useForm()
  const [messageApi, contextHolder] = message.useMessage()
  const addFormGroupName = Form.useWatch("group_name", form)
  const editFormGroupName = Form.useWatch("group_name", groupForm)
  const isMobile = useIsMobile()
  const isTablet = useIsTablet()
  const breakpoint = useResponsive()

  useEffect(() => {
    xlsxWorkerRef.current = createXlsxWorker()
    return () => {
      xlsxWorkerRef.current?.terminate()
      if (pointerDragRafRef.current != null) {
        cancelAnimationFrame(pointerDragRafRef.current)
        pointerDragRafRef.current = null
      }
      if (pointerDragGroupRafRef.current != null) {
        cancelAnimationFrame(pointerDragGroupRafRef.current)
        pointerDragGroupRafRef.current = null
      }
    }
  }, [])

  const emitDataUpdated = (category: "students" | "all") => {
    window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category } }))
  }

  const fetchStudents = useCallback(async () => {
    if (!(window as any).api) return
    setLoading(true)
    try {
      const res = await (window as any).api.queryStudents({})
      if (res.success && res.data) {
        try {
          const students = await Promise.all(
            (res.data as any[]).map(async (s) => {
              let tagIds: number[] = []
              let tags: string[] = []

              try {
                const tagsRes = await (window as any).api.tagsGetByStudent(s.id)
                if (tagsRes.success && tagsRes.data) {
                  tagIds = tagsRes.data.map((t: any) => t.id)
                  tags = tagsRes.data.map((t: any) => t.name)
                }
              } catch (e) {
                console.warn("Failed to fetch tags for student:", s.id, e)
              }

              return {
                id: s.id,
                name: s.name,
                group_name: s.group_name ?? null,
                score: s.score,
                extra_json: s.extra_json ?? null,
                avatarUrl: getAvatarFromExtraJson(s.extra_json),
                tags,
                tagIds,
              }
            })
          )
          console.debug("Fetched students:", students)
          setData(students)
        } catch (e) {
          console.warn("Failed to parse students response, falling back:", e)
          setData(res.data)
        }
      }
    } catch (e) {
      console.error("Failed to fetch students:", e)
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    fetchStudents()
    const onDataUpdated = (e: any) => {
      const category = e?.detail?.category
      if (category === "students" || category === "all") fetchStudents()
    }
    window.addEventListener("ss:data-updated", onDataUpdated as any)
    return () => window.removeEventListener("ss:data-updated", onDataUpdated as any)
  }, [fetchStudents])

  const handleAdd = async () => {
    if (!(window as any).api) return
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    try {
      const values = await form.validateFields()
      if (!values.name) {
        messageApi.warning(t("students.nameRequired"))
        return
      }

      const name = values.name.trim()
      const groupName =
        typeof values.group_name === "string" && values.group_name.trim()
          ? values.group_name.trim()
          : undefined
      if (data.some((s) => s.name === name)) {
        messageApi.warning(t("students.nameExists"))
        return
      }

      const res = await (window as any).api.createStudent({
        ...values,
        name,
        group_name: groupName,
      })
      if (res.success) {
        messageApi.success(t("students.addSuccess"))
        setVisible(false)
        form.resetFields()
        fetchStudents()
        emitDataUpdated("students")
      } else {
        messageApi.error(res.message || t("students.addFailed"))
      }
    } catch (err) {
      try {
        const api = (window as any).api
        api?.writeLog?.({
          level: "error",
          message: "renderer:validate error",
          meta:
            err instanceof Error
              ? { message: err.message, stack: err.stack }
              : { err: String(err) },
        })
      } catch {
        return
      }
    }
  }

  const handleDelete = async (id: number) => {
    if (!(window as any).api) return
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    const res = await (window as any).api.deleteStudent(id)
    if (res.success) {
      messageApi.success(t("students.deleteSuccess"))
      fetchStudents()
      emitDataUpdated("students")
    } else {
      messageApi.error(res.message || t("students.deleteFailed"))
    }
  }

  const handleOpenTagEditor = (student: student) => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    setEditingStudent(student)
    setTagEditVisible(true)
  }

  const handleOpenAvatarEditor = (student: student) => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    setAvatarStudent(student)
    setAvatarValue(student.avatarUrl || null)
    setAvatarVisible(true)
  }

  const handleOpenGroupEditor = (student: student) => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    setGroupEditStudent(student)
    groupForm.setFieldsValue({ group_name: student.group_name || "" })
    setGroupEditVisible(true)
  }

  const handleSaveGroup = async () => {
    if (!(window as any).api || !groupEditStudent) return

    try {
      const values = await groupForm.validateFields()
      const groupName =
        typeof values.group_name === "string" && values.group_name.trim()
          ? values.group_name.trim()
          : ""
      setGroupSaving(true)
      const res = await (window as any).api.updateStudent(groupEditStudent.id, {
        group_name: groupName,
      })
      if (res?.success) {
        messageApi.success(t("students.groupSaveSuccess"))
        setGroupEditVisible(false)
        setGroupEditStudent(null)
        groupForm.resetFields()
        fetchStudents()
        emitDataUpdated("students")
      } else {
        messageApi.error(res?.message || t("students.groupSaveFailed"))
      }
    } catch {
      return
    } finally {
      setGroupSaving(false)
    }
  }

  const normalizeGroupBoardOrder = useCallback(
    (order: string[]) => {
      const withoutUngrouped = order.filter((key) => key !== UNGROUPED_KEY)
      return [...withoutUngrouped, UNGROUPED_KEY]
    },
    [UNGROUPED_KEY]
  )

  const openGroupBoardEditor = () => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }

    const groups = new Set<string>()
    data.forEach((student) => {
      const normalized = student.group_name?.trim()
      if (normalized) groups.add(normalized)
    })
    const sortedGroups = Array.from(groups).sort((a, b) => a.localeCompare(b, "zh-CN"))
    const order = normalizeGroupBoardOrder([...sortedGroups, UNGROUPED_KEY])
    const board: Record<string, student[]> = {}
    order.forEach((key) => {
      board[key] = []
    })
    data.forEach((student) => {
      const key = student.group_name?.trim() || UNGROUPED_KEY
      if (!board[key]) {
        board[key] = []
        order.push(key)
      }
      board[key].push(student)
    })
    order.forEach((key) => {
      board[key].sort((a, b) => a.name.localeCompare(b.name, "zh-CN"))
    })
    console.debug("[GroupBoard] open editor", {
      groups: order.map((group) => (group === UNGROUPED_KEY ? "ungrouped" : group)),
      studentCount: data.length,
    })
    setGroupBoardOrder(normalizeGroupBoardOrder(order))
    setGroupBoard(board)
    setGroupBoardNewGroupName("")
    setGroupBoardVisible(true)
  }

  const handleCreateGroupInBoard = () => {
    const name = groupBoardNewGroupName.trim()
    if (!name) {
      messageApi.warning(t("students.groupBoardGroupNameRequired"))
      return
    }
    if (name === UNGROUPED_KEY) {
      messageApi.warning(t("students.groupBoardGroupNameInvalid"))
      return
    }
    if (groupBoardOrder.includes(name)) {
      messageApi.warning(t("students.groupBoardGroupExists"))
      return
    }

    setGroupBoard((prev) => ({ ...prev, [name]: [] }))
    setGroupBoardOrder((prev) => {
      const withoutUngrouped = prev.filter((key) => key !== UNGROUPED_KEY)
      return normalizeGroupBoardOrder([...withoutUngrouped, name, UNGROUPED_KEY])
    })
    setGroupBoardNewGroupName("")
  }

  const moveStudentToGroup = (studentId: number, targetGroup: string) => {
    setGroupBoard((prev) => {
      let movingStudent: student | null = null
      let sourceGroup = ""
      const next: Record<string, student[]> = {}
      Object.entries(prev).forEach(([group, students]) => {
        next[group] = students.filter((student) => {
          if (student.id === studentId) {
            movingStudent = student
            sourceGroup = group
            return false
          }
          return true
        })
      })

      if (!movingStudent) return prev
      const movedStudent = movingStudent as student
      if (sourceGroup === targetGroup) return prev
      if (!next[targetGroup]) next[targetGroup] = []
      next[targetGroup] = [...next[targetGroup], movedStudent]
      console.debug("[GroupBoard] move student", {
        studentId,
        studentName: movedStudent.name,
        from: sourceGroup === UNGROUPED_KEY ? "ungrouped" : sourceGroup,
        to: targetGroup === UNGROUPED_KEY ? "ungrouped" : targetGroup,
      })
      return next
    })
  }

  const handleSaveGroupBoard = async () => {
    if (!(window as any).api) return

    const groupByStudentId = new Map<number, string>()
    groupBoardOrder.forEach((groupKey) => {
      const students = groupBoard[groupKey] || []
      students.forEach((student) => {
        groupByStudentId.set(student.id, groupKey === UNGROUPED_KEY ? "" : groupKey)
      })
    })

    const changedStudents = data.filter((student) => {
      const originalGroup = student.group_name?.trim() || ""
      const nextGroup = groupByStudentId.get(student.id) ?? ""
      return originalGroup !== nextGroup
    })

    if (changedStudents.length === 0) {
      console.debug("[GroupBoard] save skipped, no changes")
      messageApi.info(t("students.groupBoardNoChanges"))
      setGroupBoardVisible(false)
      return
    }

    setGroupBoardSaving(true)
    try {
      const results = await Promise.allSettled(
        changedStudents.map((student) => {
          const nextGroup = groupByStudentId.get(student.id) ?? ""
          return (window as any).api.updateStudent(student.id, { group_name: nextGroup })
        })
      )
      const failedCount = results.filter(
        (result) => result.status === "rejected" || !result.value?.success
      ).length
      console.debug("[GroupBoard] save result", {
        changedCount: changedStudents.length,
        failedCount,
      })

      if (failedCount > 0) {
        messageApi.error(t("students.groupBoardSaveFailed", { failed: failedCount }))
      } else {
        messageApi.success(t("students.groupBoardSaveSuccess", { count: changedStudents.length }))
        setGroupBoardVisible(false)
      }
      fetchStudents()
      emitDataUpdated("students")
    } finally {
      setGroupBoardSaving(false)
    }
  }

  const beginPointerDrag = (
    e: React.PointerEvent<HTMLDivElement>,
    studentId: number,
    studentName: string,
    sourceGroup: string
  ) => {
    if (e.button !== 0) return
    e.preventDefault()
    draggingStudentIdRef.current = studentId
    pointerDragSourceGroupRef.current = sourceGroup
    pointerDragTargetGroupRef.current = null
    setPointerDraggingStudentId(studentId)
    setPointerDragStudentName(studentName)
    pointerDragPositionRef.current = { x: e.clientX, y: e.clientY }
    requestAnimationFrame(() => {
      if (!pointerDragPositionRef.current) return
      const ghost = pointerDragGhostRef.current
      if (!ghost) return
      ghost.style.transform = `translate3d(${pointerDragPositionRef.current.x + 14}px, ${
        pointerDragPositionRef.current.y + 14
      }px, 0)`
    })
    setPointerTargetGroup(null)
    e.currentTarget.setPointerCapture(e.pointerId)
    console.debug("[GroupBoard] pointer drag start", {
      studentId,
      studentName,
      sourceGroup: sourceGroup === UNGROUPED_KEY ? "ungrouped" : sourceGroup,
    })
  }

  const schedulePointerDragPositionUpdate = (clientX: number, clientY: number) => {
    pointerDragPositionRef.current = { x: clientX, y: clientY }
    if (pointerDragRafRef.current != null) return
    pointerDragRafRef.current = requestAnimationFrame(() => {
      pointerDragRafRef.current = null
      if (!pointerDragPositionRef.current) return
      const ghost = pointerDragGhostRef.current
      if (!ghost) return
      ghost.style.transform = `translate3d(${pointerDragPositionRef.current.x + 14}px, ${
        pointerDragPositionRef.current.y + 14
      }px, 0)`
    })
  }

  const trackPointerTarget = (clientX: number, clientY: number) => {
    if (draggingStudentIdRef.current == null) return
    schedulePointerDragPositionUpdate(clientX, clientY)
    const element = document.elementFromPoint(clientX, clientY) as HTMLElement | null
    const dropZone = element?.closest("[data-group-drop]") as HTMLElement | null
    const targetGroup = dropZone?.dataset.groupDrop ?? null
    if (pointerDragTargetGroupRef.current !== targetGroup) {
      pointerDragTargetGroupRef.current = targetGroup
      setPointerTargetGroup(targetGroup)
      console.debug("[GroupBoard] pointer target", {
        targetGroup: targetGroup === UNGROUPED_KEY ? "ungrouped" : targetGroup,
      })
    }
  }

  const finishPointerDrag = () => {
    const studentId = draggingStudentIdRef.current
    const sourceGroup = pointerDragSourceGroupRef.current
    const targetGroup = pointerDragTargetGroupRef.current
    console.debug("[GroupBoard] pointer drag end", {
      studentId,
      sourceGroup: sourceGroup === UNGROUPED_KEY ? "ungrouped" : sourceGroup,
      targetGroup: targetGroup === UNGROUPED_KEY ? "ungrouped" : targetGroup,
    })
    if (studentId != null && targetGroup && targetGroup !== sourceGroup) {
      moveStudentToGroup(studentId, targetGroup)
    }
    draggingStudentIdRef.current = null
    pointerDragSourceGroupRef.current = null
    pointerDragTargetGroupRef.current = null
    pointerDragPositionRef.current = null
    if (pointerDragRafRef.current != null) {
      cancelAnimationFrame(pointerDragRafRef.current)
      pointerDragRafRef.current = null
    }
    setPointerDraggingStudentId(null)
    setPointerDragStudentName("")
    setPointerTargetGroup(null)
    const ghost = pointerDragGhostRef.current
    if (ghost) ghost.style.transform = "translate3d(-9999px, -9999px, 0)"
  }

  const resetGroupOrderDragState = () => {
    pointerDragGroupSourceRef.current = null
    pointerDragGroupTargetRef.current = null
    pointerDragGroupPositionRef.current = null
    pointerDragGroupInsertAfterRef.current = false
    if (pointerDragGroupRafRef.current != null) {
      cancelAnimationFrame(pointerDragGroupRafRef.current)
      pointerDragGroupRafRef.current = null
    }
    setPointerDraggingGroupKey(null)
    setPointerTargetGroupOrderKey(null)
    setPointerDragGroupName("")
    const ghost = pointerDragGroupGhostRef.current
    if (ghost) ghost.style.transform = "translate3d(-9999px, -9999px, 0)"
  }

  const scheduleGroupPointerPositionUpdate = (clientX: number, clientY: number) => {
    pointerDragGroupPositionRef.current = { x: clientX, y: clientY }
    if (pointerDragGroupRafRef.current != null) return
    pointerDragGroupRafRef.current = requestAnimationFrame(() => {
      pointerDragGroupRafRef.current = null
      if (!pointerDragGroupPositionRef.current) return
      const ghost = pointerDragGroupGhostRef.current
      if (!ghost) return
      ghost.style.transform = `translate3d(${pointerDragGroupPositionRef.current.x + 14}px, ${
        pointerDragGroupPositionRef.current.y + 14
      }px, 0)`
    })
  }

  const beginGroupOrderDrag = (
    e: React.PointerEvent<HTMLDivElement>,
    groupKey: string,
    groupLabel: string
  ) => {
    if (e.button !== 0 || pointerDraggingStudentId != null || groupKey === UNGROUPED_KEY) return
    e.preventDefault()
    pointerDragGroupSourceRef.current = groupKey
    pointerDragGroupTargetRef.current = null
    pointerDragGroupInsertAfterRef.current = false
    setPointerDraggingGroupKey(groupKey)
    setPointerTargetGroupOrderKey(null)
    setPointerDragGroupName(groupLabel)
    scheduleGroupPointerPositionUpdate(e.clientX, e.clientY)
    e.currentTarget.setPointerCapture(e.pointerId)
  }

  const trackGroupOrderTarget = (clientX: number, clientY: number) => {
    if (!pointerDragGroupSourceRef.current) return
    scheduleGroupPointerPositionUpdate(clientX, clientY)
    const element = document.elementFromPoint(clientX, clientY) as HTMLElement | null
    const targetElement = element?.closest("[data-group-column]") as HTMLElement | null
    const targetGroup = targetElement?.dataset.groupColumn ?? null
    if (targetElement) {
      const rect = targetElement.getBoundingClientRect()
      pointerDragGroupInsertAfterRef.current = clientX >= rect.left + rect.width / 2
    } else {
      pointerDragGroupInsertAfterRef.current = false
    }
    if (pointerDragGroupTargetRef.current !== targetGroup) {
      pointerDragGroupTargetRef.current = targetGroup
      setPointerTargetGroupOrderKey(targetGroup)
    }
  }

  const finishGroupOrderDrag = () => {
    const source = pointerDragGroupSourceRef.current
    const target = pointerDragGroupTargetRef.current
    const insertAfter = pointerDragGroupInsertAfterRef.current
    if (source && target && source !== target) {
      setGroupBoardOrder((prev) => {
        const next = prev.filter((key) => key !== source)
        const targetIndex = next.indexOf(target)
        if (targetIndex < 0) return prev
        const insertIndex = Math.max(0, Math.min(next.length, targetIndex + (insertAfter ? 1 : 0)))
        next.splice(insertIndex, 0, source)
        return normalizeGroupBoardOrder(next)
      })
    }
    resetGroupOrderDragState()
  }

  const readFileAsDataUrl = (file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onload = () => {
        const result = reader.result
        if (typeof result === "string") resolve(result)
        else reject(new Error("invalid-result"))
      }
      reader.onerror = () => reject(reader.error || new Error("read-failed"))
      reader.readAsDataURL(file)
    })
  }

  const handleAvatarFileChange = async (file?: File) => {
    if (!file) return
    if (!file.type.startsWith("image/")) {
      messageApi.error(t("students.avatarInvalidFile"))
      return
    }
    const maxSize = 2 * 1024 * 1024
    if (file.size > maxSize) {
      messageApi.error(t("students.avatarTooLarge"))
      return
    }
    try {
      const dataUrl = await readFileAsDataUrl(file)
      setAvatarValue(dataUrl)
    } catch {
      messageApi.error(t("students.avatarReadFailed"))
    }
  }

  const handleSaveAvatar = async () => {
    if (!(window as any).api || !avatarStudent) return

    setAvatarSaving(true)
    try {
      const payload = setAvatarInExtraJson(avatarStudent.extra_json, avatarValue)
      const res = await (window as any).api.updateStudent(avatarStudent.id, {
        extra_json: payload,
      })
      if (res?.success) {
        messageApi.success(t("students.avatarSaveSuccess"))
        setAvatarVisible(false)
        setAvatarStudent(null)
        setAvatarValue(null)
        fetchStudents()
        emitDataUpdated("students")
      } else {
        messageApi.error(res?.message || t("students.avatarSaveFailed"))
      }
    } catch {
      messageApi.error(t("students.avatarSaveFailed"))
    } finally {
      setAvatarSaving(false)
    }
  }

  const handleSaveTags = async (tagIds: number[]) => {
    if (!editingStudent || !(window as any).api) return

    try {
      const res = await (window as any).api.tagsUpdateStudentTags(editingStudent.id, tagIds)
      if (res && res.success) {
        messageApi.success("标签保存成功")
        setTagEditVisible(false)
        setEditingStudent(null)
        fetchStudents()
        emitDataUpdated("students")
      } else {
        const errorMsg = res?.message || t("students.tagSaveFailed")
        messageApi.error(errorMsg)
      }
    } catch (error) {
      const errorMsg = error instanceof Error ? error.message : String(error)
      messageApi.error(`${t("students.tagSaveFailed")}: ${errorMsg}`)
    }
  }

  const excelColName = (idx: number) => {
    let n = idx + 1
    let s = ""
    while (n > 0) {
      const mod = (n - 1) % 26
      s = String.fromCharCode(65 + mod) + s
      n = Math.floor((n - 1) / 26)
    }
    return s
  }

  const parseXlsxFile = async (file: File) => {
    if (!xlsxWorkerRef.current) {
      messageApi.error(t("students.workerNotReady"))
      return
    }

    setXlsxLoading(true)
    try {
      const buf = await file.arrayBuffer()

      xlsxWorkerRef.current.postMessage({
        type: "parseXlsx",
        data: { buffer: buf },
      })

      const handleMessage = (event: MessageEvent) => {
        if (event.data.type === "success") {
          setXlsxFileName(file.name)
          setXlsxAoa(event.data.data)
          setXlsxSelectedCol(null)
          setXlsxVisible(true)
          setTextImportVisible(false)
          setXlsxLoading(false)
        } else if (event.data.type === "error") {
          messageApi.error(event.data.error || t("students.parseXlsxFailed"))
          setXlsxLoading(false)
        }
        xlsxWorkerRef.current?.removeEventListener("message", handleMessage)
      }

      xlsxWorkerRef.current.addEventListener("message", handleMessage)
    } catch (e: any) {
      messageApi.error(e?.message || t("students.parseXlsxFailed"))
      setXlsxLoading(false)
    }
  }

  const xlsxMaxCols = useMemo(() => {
    let max = 0
    for (const row of xlsxAoa) {
      if (Array.isArray(row)) max = Math.max(max, row.length)
    }
    return max
  }, [xlsxAoa])

  const xlsxPreviewRows = useMemo(() => {
    const limit = 50
    const rows = xlsxAoa.slice(0, limit)
    return rows.map((row, idx) => {
      const record: any = { __row: idx + 1 }
      for (let c = 0; c < xlsxMaxCols; c++) {
        record[`c${c}`] = row?.[c] ?? ""
      }
      return record
    })
  }, [xlsxAoa, xlsxMaxCols])

  const xlsxPreviewColumns = useMemo(() => {
    const cols: any[] = [
      {
        title: "#",
        dataIndex: "__row",
        key: "__row",
        width: 60,
        align: "center" as const,
        fixed: "left" as const,
      },
    ]
    for (let c = 0; c < xlsxMaxCols; c++) {
      const selected = xlsxSelectedCol === c
      cols.push({
        title: (
          <span
            style={{
              cursor: "pointer",
              fontWeight: selected ? 700 : 500,
              color: selected ? "var(--ant-color-primary, #1890ff)" : undefined,
            }}
            onClick={() => setXlsxSelectedCol(c)}
          >
            {excelColName(c)}
          </span>
        ),
        dataIndex: `c${c}`,
        key: `c${c}`,
        width: 120,
      })
    }
    return cols
  }, [xlsxMaxCols, xlsxSelectedCol])

  const extractUniqueNames = (rawNames: string[]) => {
    const out: string[] = []
    const seen = new Set<string>()
    const banned = new Set([t("students.name").toLowerCase(), "name", t("students.name")])
    for (const raw of rawNames) {
      const name = String(raw ?? "").trim()
      if (!name) continue
      if (banned.has(name.toLowerCase()) || banned.has(name)) continue
      if (seen.has(name)) continue
      seen.add(name)
      out.push(name)
    }
    return out
  }

  const extractNamesFromAoa = (aoa: any[][], colIdx: number) => {
    const names = aoa.map((row) => String(row?.[colIdx] ?? ""))
    return extractUniqueNames(names)
  }

  const extractNamesFromText = (text: string) => {
    const lines = text.split(/\r?\n/)
    return extractUniqueNames(lines)
  }

  const importNames = async (names: string[], options?: { toast?: boolean }) => {
    const toast = options?.toast ?? true
    if (!(window as any).api) return { success: false, inserted: 0, skipped: 0 }
    const res = await (window as any).api.importStudentsFromXlsx({ names })
    if (!res?.success) {
      if (toast) messageApi.error(res?.message || t("students.importFailed"))
      return { success: false, inserted: 0, skipped: 0 }
    }
    const inserted = Number(res?.data?.inserted ?? 0)
    const skipped = Number(res?.data?.skipped ?? 0)
    if (toast) messageApi.success(t("students.importComplete", { inserted, skipped }))
    fetchStudents()
    emitDataUpdated("students")
    return { success: true, inserted, skipped }
  }

  const handleConfirmXlsxImport = async () => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    if (xlsxSelectedCol == null) {
      messageApi.warning(t("students.selectNameColFirst"))
      return
    }

    const names = extractNamesFromAoa(xlsxAoa, xlsxSelectedCol)
    if (!names.length) {
      messageApi.error(t("students.noNamesFound"))
      return
    }

    setXlsxLoading(true)
    try {
      const result = await importNames(names)
      if (!result.success) return
      setXlsxVisible(false)
      setXlsxAoa([])
      setXlsxFileName("")
      setXlsxSelectedCol(null)
    } finally {
      setXlsxLoading(false)
    }
  }

  const handleImportTextNames = async () => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    const names = extractNamesFromText(textImportValue)
    if (!names.length) {
      messageApi.warning(t("students.noNamesFound"))
      return
    }

    setTextImportLoading(true)
    try {
      const result = await importNames(names)
      if (!result.success) return
      setTextImportValue("")
      setTextImportVisible(false)
    } finally {
      setTextImportLoading(false)
    }
  }

  const handleOpenTextImport = () => {
    setTextImportVisible(true)
  }

  const handleOpenXlsxImport = () => {
    xlsxInputRef.current?.click()
  }

  const handleOpenBanYouImport = () => {
    setBanYouVisible(true)
  }

  const handleFetchBanYouClassrooms = async () => {
    if (!canEdit) {
      messageApi.error(t("common.readOnly"))
      return
    }
    const cookie = banYouCookie.trim()
    if (!cookie) {
      messageApi.warning(t("students.banyouCookieRequired"))
      return
    }

    setBanYouLoading(true)
    try {
      console.debug("[BanYou] fetch classrooms start", {
        cookieLength: cookie.length,
        containsUid: cookie.includes("uid="),
        containsAccessToken: cookie.includes("accessToken="),
      })
      const res = await (window as any).api.fetchBanYouClassrooms({ cookie })
      if (!res?.success || !res?.data) {
        console.error("[BanYou] fetch classrooms failed", res)
        messageApi.error(res?.message || t("students.banyouFetchFailed"))
        return
      }
      const classrooms = Array.isArray(res.data.classrooms) ? res.data.classrooms : []
      console.debug("[BanYou] fetch classrooms success", {
        classrooms: classrooms.length,
        administrativeGroups: Array.isArray(res.data.administrativeGroups)
          ? res.data.administrativeGroups.length
          : 0,
      })
      setBanYouClassrooms(classrooms)
      messageApi.success(t("students.banyouFetchSuccess", { count: classrooms.length }))
    } catch (e: any) {
      console.error("[BanYou] fetch classrooms exception", e)
      messageApi.error(e?.message || t("students.banyouFetchFailed"))
    } finally {
      setBanYouLoading(false)
    }
  }

  const medalKey = (item: BanYouMedal, idx: number) => item.key || item.uid || `${item.name}-${idx}`

  const fetchBanYouClassDetail = async (classroom: BanYouClassroom, teamPlanId?: number) => {
    const cookie = banYouCookie.trim()
    if (!cookie) {
      messageApi.warning(t("students.banyouCookieRequired"))
      return
    }
    setBanYouSelectedClass(classroom)
    setBanYouDetailVisible(true)
    setBanYouDetailLoading(true)
    try {
      const parsedTeamPlanId = typeof teamPlanId === "number" ? teamPlanId : undefined
      const params: any = {
        cookie,
        classId: classroom.classId,
      }
      if (Number.isFinite(parsedTeamPlanId)) params.teamPlanId = parsedTeamPlanId
      const res = await (window as any).api.fetchBanYouClassroomDetail(params)
      if (!res?.success || !res?.data) {
        messageApi.error(res?.message || t("students.banyouFetchDetailFailed"))
        setBanYouDetail(null)
        return
      }
      const detail = res.data as BanYouClassroomDetail
      setBanYouDetail(detail)
      const options = Array.isArray(detail.teamPlans) ? detail.teamPlans : []
      setBanYouTeamPlanOptions(options)
      const used = Number(detail.teamPlanIdUsed)
      if (Number.isFinite(used) && used > 0) {
        setBanYouSelectedTeamPlanId(used)
      } else if (options.length > 0) {
        setBanYouSelectedTeamPlanId(Number(options[0].teamPlanId))
      } else {
        setBanYouSelectedTeamPlanId(undefined)
      }
      setBanYouCheckedStudents((detail.students || []).map((s) => s.studentId))
      setBanYouCheckedMedals((detail.medals || []).map((m, idx) => medalKey(m, idx)))
      setBanYouCheckedTeams((detail.teams || []).map((team) => team.teamId))
    } catch (e: any) {
      messageApi.error(e?.message || t("students.banyouFetchDetailFailed"))
      setBanYouDetail(null)
    } finally {
      setBanYouDetailLoading(false)
    }
  }

  const handleOpenBanYouClassDetail = async (classroom: BanYouClassroom) => {
    await fetchBanYouClassDetail(classroom)
  }

  const handleReloadBanYouGroupByPlan = async () => {
    if (!banYouSelectedClass) return
    if (!banYouSelectedTeamPlanId) {
      messageApi.warning(t("students.banyouSelectTeamPlanFirst"))
      return
    }
    await fetchBanYouClassDetail(banYouSelectedClass, banYouSelectedTeamPlanId)
  }

  const handleImportBanYouSelected = async () => {
    if (!banYouDetail) return
    if (!(window as any).api) return

    const selectedStudents = (banYouDetail.students || []).filter((s) =>
      banYouCheckedStudents.includes(s.studentId)
    )
    const selectedMedals = (banYouDetail.medals || []).filter((m, idx) =>
      banYouCheckedMedals.includes(medalKey(m, idx))
    )
    const selectedTeams = (banYouDetail.teams || []).filter((g) =>
      banYouCheckedTeams.includes(g.teamId)
    )

    if (!selectedStudents.length && !selectedMedals.length && !selectedTeams.length) {
      messageApi.warning(t("students.banyouNothingSelected"))
      return
    }

    setBanYouImportLoading(true)
    try {
      let studentInserted = 0
      let studentSkipped = 0
      if (selectedStudents.length) {
        const names = selectedStudents.map((s) => s.studentName)
        const res = await importNames(names, { toast: false })
        if (!res.success) {
          messageApi.error(t("students.importFailed"))
          return
        }
        studentInserted = res.inserted
        studentSkipped = res.skipped
      }

      let reasonInserted = 0
      let reasonSkipped = 0
      if (selectedMedals.length) {
        const reasonCategory = "班优导入"
        const currentReasons = await (window as any).api.queryReasons()
        const existing = new Set<string>()
        if (currentReasons?.success && Array.isArray(currentReasons?.data)) {
          for (const r of currentReasons.data) {
            existing.add(`${r.category}::${r.content}::${Number(r.delta)}`)
          }
        }
        for (const medal of selectedMedals) {
          const content = String(medal.name || "").trim()
          if (!content) {
            reasonSkipped += 1
            continue
          }
          const medalType = Number(medal.medalType ?? medal.type ?? 1)
          const value = Math.abs(Number(medal.value ?? 1)) || 1
          const delta = medalType < 0 ? -value : value
          const key = `${reasonCategory}::${content}::${delta}`
          if (existing.has(key)) {
            reasonSkipped += 1
            continue
          }
          const createRes = await (window as any).api.createReason({
            content,
            category: reasonCategory,
            delta,
          })
          if (createRes?.success) {
            reasonInserted += 1
            existing.add(key)
          } else {
            reasonSkipped += 1
          }
        }
      }

      let groupUpdated = 0
      let groupSkipped = 0
      if (selectedTeams.length) {
        const mapping = new Map<string, string>()
        for (const team of selectedTeams) {
          for (const student of team.students || []) {
            const name = String(student.studentName || "").trim()
            if (name && !mapping.has(name)) mapping.set(name, team.teamName || "")
          }
        }
        if (mapping.size > 0) {
          const localStudentsRes = await (window as any).api.queryStudents({})
          const localMap = new Map<string, number>()
          if (localStudentsRes?.success && Array.isArray(localStudentsRes?.data)) {
            for (const s of localStudentsRes.data) {
              localMap.set(String(s.name), Number(s.id))
            }
          }
          for (const [name, groupName] of mapping.entries()) {
            const id = localMap.get(name)
            if (!id) {
              groupSkipped += 1
              continue
            }
            const updateRes = await (window as any).api.updateStudent(id, { group_name: groupName })
            if (updateRes?.success) groupUpdated += 1
            else groupSkipped += 1
          }
        }
      }

      emitDataUpdated("all")
      fetchStudents()
      messageApi.success(
        t("students.banyouImportSummary", {
          studentsInserted: studentInserted,
          studentsSkipped: studentSkipped,
          reasonsInserted: reasonInserted,
          reasonsSkipped: reasonSkipped,
          groupsUpdated: groupUpdated,
          groupsSkipped: groupSkipped,
        })
      )
    } finally {
      setBanYouImportLoading(false)
    }
  }

  const banYouCreatedClasses = useMemo(
    () => banYouClassrooms.filter((item) => item.isOwn !== false),
    [banYouClassrooms]
  )
  const banYouJoinedClasses = useMemo(
    () => banYouClassrooms.filter((item) => item.isOwn === false),
    [banYouClassrooms]
  )

  const columns: ColumnsType<student> = useMemo(() => {
    const baseColumns: ColumnsType<student> = [
      {
        title: t("students.avatar"),
        key: "avatar",
        width: isMobile ? 60 : 88,
        align: "center",
        render: (_, row) =>
          row.avatarUrl ? (
            <img
              src={row.avatarUrl}
              alt={row.name}
              style={{
                width: isMobile ? 24 : 32,
                height: isMobile ? 24 : 32,
                borderRadius: "50%",
                objectFit: "cover",
                border: "1px solid var(--ss-border-color)",
              }}
            />
          ) : (
            <div
              style={{
                width: isMobile ? 24 : 32,
                height: isMobile ? 24 : 32,
                borderRadius: "50%",
                margin: "0 auto",
                backgroundColor: "var(--ss-bg-color)",
                border: "1px dashed var(--ss-border-color)",
                color: "var(--ss-text-secondary)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                fontSize: isMobile ? 10 : 12,
              }}
            >
              -
            </div>
          ),
      },
      {
        title: t("students.name"),
        dataIndex: "name",
        key: "name",
        width: isMobile ? 80 : 100,
        ellipsis: true,
      },
      {
        title: t("students.group"),
        dataIndex: "group_name",
        key: "group_name",
        width: isMobile ? 90 : 120,
        ellipsis: true,
        render: (groupName?: string | null) => groupName?.trim() || t("students.noGroup"),
      },
      {
        title: t("students.currentScore"),
        dataIndex: "score",
        key: "score",
        width: isMobile ? 100 : 160,
        align: "center",
        render: (score: number) => (
          <span
            style={{
              fontWeight: "bold",
              color:
                score > 0
                  ? "var(--ant-color-success, #52c41a)"
                  : score < 0
                    ? "var(--ant-color-error, #ff4d4f)"
                    : "inherit",
              fontSize: isMobile ? 12 : 14,
            }}
          >
            {score > 0 ? `+${score}` : score}
          </span>
        ),
      },
      {
        title: t("students.tags"),
        dataIndex: "tags",
        key: "tags",
        width: isMobile ? 120 : 200,
        render: (tags: string[] = []) => (
          <Space size={isMobile ? 2 : 4}>
            {tags.length === 0 ? (
              <span style={{ color: "var(--ss-text-secondary)", fontSize: isMobile ? 10 : 12 }}>
                {t("students.noTags")}
              </span>
            ) : (
              tags.slice(0, isMobile ? 2 : 3).map((tag) => (
                <Tag key={tag} color="blue" style={{ fontSize: isMobile ? 10 : 12, margin: 0 }}>
                  {tag}
                </Tag>
              ))
            )}
            {tags.length > (isMobile ? 2 : 3) && (
              <Tag style={{ fontSize: isMobile ? 10 : 12, margin: 0 }}>
                +{tags.length - (isMobile ? 2 : 3)}
              </Tag>
            )}
          </Space>
        ),
      },
      {
        title: t("common.operation"),
        key: "operation",
        width: isMobile ? 96 : 110,
        fixed: isMobile ? "right" : undefined,
        render: (_, row) => (
          <Dropdown
            trigger={["click"]}
            menu={{
              items: [
                { key: "editTags", label: t("students.editTags") },
                { key: "editGroup", label: t("students.editGroup") },
                { key: "editAvatar", label: t("students.editAvatar") },
                { key: "delete", danger: true, label: t("common.delete") },
              ],
              onClick: ({ key }) => {
                if (key === "editTags") handleOpenTagEditor(row)
                else if (key === "editGroup") handleOpenGroupEditor(row)
                else if (key === "editAvatar") handleOpenAvatarEditor(row)
                else if (key === "delete") handleDelete(row.id)
              },
            }}
          >
            <Button
              size={isMobile ? "small" : "middle"}
              disabled={!canEdit}
              icon={<MoreOutlined />}
            >
              {t("common.operation")}
            </Button>
          </Dropdown>
        ),
      },
    ]

    if (isMobile) {
      return baseColumns.filter((col) => col.key !== "tags")
    }

    return baseColumns
  }, [t, canEdit, isMobile, isTablet, breakpoint])

  const existingGroupNames = useMemo(() => {
    const groups = new Set<string>()
    data.forEach((student) => {
      const normalized = student.group_name?.trim()
      if (normalized) groups.add(normalized)
    })
    return Array.from(groups).sort((a, b) => a.localeCompare(b, "zh-CN"))
  }, [data])

  const normalizedAddFormGroupName =
    typeof addFormGroupName === "string" && addFormGroupName.trim() ? addFormGroupName.trim() : ""
  const normalizedEditFormGroupName =
    typeof editFormGroupName === "string" && editFormGroupName.trim()
      ? editFormGroupName.trim()
      : ""

  const importMenu = {
    items: [
      { key: "text", label: t("students.importByText") },
      { key: "xlsx", label: t("students.importByXlsx") },
      { key: "banyou", label: t("students.importByBanyou") },
    ],
    onClick: ({ key }: { key: string }) => {
      if (!canEdit) {
        messageApi.error(t("common.readOnly"))
        return
      }
      if (key === "text") handleOpenTextImport()
      else if (key === "xlsx") handleOpenXlsxImport()
      else if (key === "banyou") handleOpenBanYouImport()
    },
  }

  const paginatedData = data.slice((currentPage - 1) * pageSize, currentPage * pageSize)

  return (
    <div style={{ padding: "24px" }}>
      {contextHolder}
      <div style={{ marginBottom: "16px", display: "flex", justifyContent: "space-between" }}>
        <h2 style={{ margin: 0, color: "var(--ss-text-main)" }}>{t("students.title")}</h2>
        <Space>
          <Button disabled={!canEdit} onClick={openGroupBoardEditor}>
            {t("students.groupBoardEdit")}
          </Button>
          <Dropdown trigger={["click"]} menu={importMenu} disabled={!canEdit}>
            <Button disabled={!canEdit}>{t("students.importList")}</Button>
          </Dropdown>
          <Button type="primary" disabled={!canEdit} onClick={() => setVisible(true)}>
            {t("students.addStudent")}
          </Button>
        </Space>
      </div>

      <Table
        dataSource={paginatedData}
        columns={columns}
        rowKey="id"
        loading={loading}
        pagination={false}
        style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}
      />
      <div style={{ marginTop: 16, textAlign: "right" }}>
        <Pagination
          current={currentPage}
          pageSize={pageSize}
          total={data.length}
          onChange={(page, size) => {
            setCurrentPage(page)
            setPageSize(size)
          }}
          showSizeChanger
          showTotal={(total) => t("common.total", { count: total })}
        />
      </div>

      <Modal
        title={t("students.addTitle")}
        open={visible}
        onOk={handleAdd}
        onCancel={() => setVisible(false)}
        okText={t("students.addConfirm")}
        cancelText={t("common.cancel")}
        destroyOnHidden
      >
        <Form form={form} layout="vertical">
          <Form.Item
            label={t("students.name")}
            name="name"
            rules={[{ required: true, message: t("students.nameRequired") }]}
          >
            <Input placeholder={t("students.namePlaceholder")} />
          </Form.Item>
          <Form.Item label={t("students.group")} name="group_name">
            <Input placeholder={t("students.groupPlaceholder")} />
          </Form.Item>
          <div style={{ marginTop: 8 }}>
            <div style={{ color: "var(--ss-text-secondary)", marginBottom: 8, fontSize: 12 }}>
              {t("students.existingGroups")}
            </div>
            {existingGroupNames.length > 0 ? (
              <Space wrap size={[8, 8]}>
                {existingGroupNames.map((group) => {
                  const active = normalizedAddFormGroupName === group
                  return (
                    <Tag
                      key={`add-group-${group}`}
                      color={active ? "processing" : undefined}
                      style={{ cursor: "pointer", userSelect: "none", marginInlineEnd: 0 }}
                      onClick={() => form.setFieldValue("group_name", group)}
                    >
                      {group}
                    </Tag>
                  )
                })}
              </Space>
            ) : (
              <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
                {t("students.noExistingGroups")}
              </div>
            )}
          </div>
        </Form>
      </Modal>

      <Modal
        title={t("students.editGroupTitle", { name: groupEditStudent?.name || "" })}
        open={groupEditVisible}
        onCancel={() => {
          setGroupEditVisible(false)
          setGroupEditStudent(null)
          groupForm.resetFields()
        }}
        onOk={handleSaveGroup}
        okText={t("common.save")}
        cancelText={t("common.cancel")}
        okButtonProps={{ loading: groupSaving, disabled: !groupEditStudent }}
        destroyOnHidden
      >
        <Form form={groupForm} layout="vertical">
          <Form.Item label={t("students.group")} name="group_name">
            <Input placeholder={t("students.groupPlaceholder")} />
          </Form.Item>
          <div style={{ marginTop: 8 }}>
            <div style={{ color: "var(--ss-text-secondary)", marginBottom: 8, fontSize: 12 }}>
              {t("students.existingGroups")}
            </div>
            {existingGroupNames.length > 0 ? (
              <Space wrap size={[8, 8]}>
                {existingGroupNames.map((group) => {
                  const active = normalizedEditFormGroupName === group
                  return (
                    <Tag
                      key={`edit-group-${group}`}
                      color={active ? "processing" : undefined}
                      style={{ cursor: "pointer", userSelect: "none", marginInlineEnd: 0 }}
                      onClick={() => groupForm.setFieldValue("group_name", group)}
                    >
                      {group}
                    </Tag>
                  )
                })}
              </Space>
            ) : (
              <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
                {t("students.noExistingGroups")}
              </div>
            )}
          </div>
        </Form>
      </Modal>

      <Modal
        title={t("students.groupBoardTitle")}
        open={groupBoardVisible}
        centered
        onCancel={() => {
          setGroupBoardVisible(false)
          setGroupBoardNewGroupName("")
          draggingStudentIdRef.current = null
          pointerDragSourceGroupRef.current = null
          pointerDragTargetGroupRef.current = null
          pointerDragPositionRef.current = null
          if (pointerDragRafRef.current != null) {
            cancelAnimationFrame(pointerDragRafRef.current)
            pointerDragRafRef.current = null
          }
          const ghost = pointerDragGhostRef.current
          if (ghost) ghost.style.transform = "translate3d(-9999px, -9999px, 0)"
          setPointerDraggingStudentId(null)
          setPointerDragStudentName("")
          setPointerTargetGroup(null)
          resetGroupOrderDragState()
        }}
        onOk={handleSaveGroupBoard}
        okText={t("common.save")}
        cancelText={t("common.cancel")}
        okButtonProps={{ loading: groupBoardSaving }}
        width="90%"
        wrapClassName="ss-group-board-modal"
        styles={{ body: { maxHeight: "calc(100vh - 220px)", overflow: "hidden" } }}
        destroyOnHidden
      >
        <div style={{ color: "var(--ss-text-secondary)", marginBottom: 12, fontSize: 12 }}>
          {t("students.groupBoardHint")}
        </div>
        <Space style={{ marginBottom: 12, width: "100%" }}>
          <Input
            value={groupBoardNewGroupName}
            onChange={(e) => setGroupBoardNewGroupName(e.target.value)}
            onPressEnter={handleCreateGroupInBoard}
            placeholder={t("students.groupBoardNewGroupPlaceholder")}
            maxLength={32}
          />
          <Button icon={<PlusOutlined />} onClick={handleCreateGroupInBoard}>
            {t("students.groupBoardAddGroup")}
          </Button>
        </Space>
        <div
          style={{
            display: "flex",
            gap: 12,
            overflowX: "auto",
            paddingBottom: 4,
            userSelect:
              pointerDraggingStudentId != null || pointerDraggingGroupKey != null ? "none" : "auto",
            WebkitUserSelect:
              pointerDraggingStudentId != null || pointerDraggingGroupKey != null ? "none" : "auto",
          }}
        >
          {groupBoardOrder.map((groupKey) => {
            const studentsInGroup = groupBoard[groupKey] || []
            const groupLabel = groupKey === UNGROUPED_KEY ? t("students.noGroup") : groupKey
            const isDraggingGroup = pointerDraggingGroupKey === groupKey
            const isGroupOrderTarget = pointerTargetGroupOrderKey === groupKey
            return (
              <div
                key={groupKey}
                data-group-column={groupKey}
                data-group-drop={groupKey}
                style={{
                  minWidth: isMobile ? 180 : 220,
                  maxWidth: isMobile ? 220 : 260,
                  border: isGroupOrderTarget
                    ? "1px solid var(--ant-color-primary, #1677ff)"
                    : "1px solid var(--ss-border-color)",
                  borderRadius: 10,
                  backgroundColor:
                    pointerTargetGroup === groupKey ? "var(--ss-bg-color)" : "var(--ss-card-bg)",
                  padding: 10,
                  display: "flex",
                  flexDirection: "column",
                  gap: 8,
                  transition: "background-color 120ms ease, border-color 120ms ease",
                  opacity: isDraggingGroup ? 0.55 : 1,
                }}
              >
                <div
                  onPointerDown={(e) => beginGroupOrderDrag(e, groupKey, groupLabel)}
                  onPointerMove={(e) => trackGroupOrderTarget(e.clientX, e.clientY)}
                  onPointerUp={finishGroupOrderDrag}
                  onPointerCancel={finishGroupOrderDrag}
                  onLostPointerCapture={finishGroupOrderDrag}
                  style={{
                    fontWeight: 600,
                    color: "var(--ss-text-main)",
                    borderBottom: "1px dashed var(--ss-border-color)",
                    paddingBottom: 8,
                    cursor:
                      groupKey === UNGROUPED_KEY || pointerDraggingStudentId != null
                        ? "default"
                        : "grab",
                    userSelect: "none",
                    WebkitUserSelect: "none",
                    touchAction: "none",
                  }}
                >
                  {groupLabel} ({studentsInGroup.length})
                </div>
                <div
                  style={{
                    display: "flex",
                    flexDirection: "column",
                    gap: 8,
                    minHeight: 160,
                    maxHeight: "44vh",
                    overflowY: "auto",
                    overflowX: "hidden",
                    paddingRight: 2,
                  }}
                >
                  {studentsInGroup.length > 0 ? (
                    studentsInGroup.map((student) => (
                      <div
                        key={`${groupKey}-${student.id}`}
                        onPointerDown={(e) =>
                          beginPointerDrag(e, student.id, student.name, groupKey)
                        }
                        onPointerMove={(e) => trackPointerTarget(e.clientX, e.clientY)}
                        onPointerUp={finishPointerDrag}
                        onPointerCancel={finishPointerDrag}
                        onLostPointerCapture={finishPointerDrag}
                        style={{
                          border: "1px solid var(--ss-border-color)",
                          borderRadius: 8,
                          backgroundColor:
                            pointerDraggingStudentId === student.id
                              ? "var(--ss-bg-color)"
                              : "var(--ss-card-bg)",
                          opacity: pointerDraggingStudentId === student.id ? 0.45 : 1,
                          padding: "8px 10px",
                          cursor: pointerDraggingStudentId === student.id ? "grabbing" : "grab",
                          userSelect: "none",
                          WebkitUserSelect: "none",
                          touchAction: "none",
                        }}
                      >
                        {student.name}
                      </div>
                    ))
                  ) : (
                    <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
                      {t("students.groupBoardEmpty")}
                    </div>
                  )}
                </div>
              </div>
            )
          })}
        </div>
        {pointerDraggingStudentId != null && (
          <div
            ref={pointerDragGhostRef}
            style={{
              position: "fixed",
              left: 0,
              top: 0,
              pointerEvents: "none",
              zIndex: 2100,
              border: "1px solid var(--ss-border-color)",
              borderRadius: 8,
              backgroundColor: "var(--ss-card-bg)",
              color: "var(--ss-text-main)",
              padding: "8px 10px",
              boxShadow: "0 8px 24px rgba(0, 0, 0, 0.16)",
              fontSize: 13,
              maxWidth: 220,
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis",
              transform: "translate3d(-9999px, -9999px, 0)",
              willChange: "transform",
            }}
          >
            {pointerDragStudentName}
          </div>
        )}
        {pointerDraggingGroupKey != null && (
          <div
            ref={pointerDragGroupGhostRef}
            style={{
              position: "fixed",
              left: 0,
              top: 0,
              pointerEvents: "none",
              zIndex: 2101,
              border: "1px solid var(--ant-color-primary, #1677ff)",
              borderRadius: 8,
              backgroundColor: "var(--ss-card-bg)",
              color: "var(--ss-text-main)",
              padding: "8px 10px",
              boxShadow: "0 8px 24px rgba(0, 0, 0, 0.16)",
              fontSize: 13,
              maxWidth: 240,
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis",
              transform: "translate3d(-9999px, -9999px, 0)",
              willChange: "transform",
            }}
          >
            {pointerDragGroupName}
          </div>
        )}
      </Modal>

      <Modal
        title={t("students.importByText")}
        open={textImportVisible}
        onCancel={() => setTextImportVisible(false)}
        footer={null}
        destroyOnHidden
      >
        <Space orientation="vertical" style={{ width: "100%" }}>
          <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
            {t("students.importTextHint")}
          </div>
          <Input.TextArea
            value={textImportValue}
            onChange={(e) => setTextImportValue(e.target.value)}
            rows={8}
            placeholder={t("students.importTextPlaceholder")}
            disabled={!canEdit || textImportLoading}
          />
          <Button
            type="primary"
            loading={textImportLoading}
            disabled={!canEdit}
            onClick={handleImportTextNames}
          >
            {t("students.importConfirm")}
          </Button>
        </Space>
      </Modal>

      <Modal
        title={t("students.importByBanyou")}
        open={banYouVisible}
        onCancel={() => setBanYouVisible(false)}
        footer={null}
        width={900}
        destroyOnHidden
      >
        <Space orientation="vertical" style={{ width: "100%" }} size={12}>
          <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
            {t("students.banyouCookieHint")}
          </div>
          <Input.TextArea
            value={banYouCookie}
            onChange={(e) => setBanYouCookie(e.target.value)}
            rows={4}
            placeholder={t("students.banyouCookiePlaceholder")}
            disabled={banYouLoading}
          />
          <Button type="primary" loading={banYouLoading} onClick={handleFetchBanYouClassrooms}>
            {t("students.banyouFetch")}
          </Button>

          <div style={{ marginTop: 8 }}>
            <h3 style={{ margin: "0 0 12px", color: "var(--ss-text-main)" }}>
              {t("students.banyouCreatedClasses")}
            </h3>
            {banYouCreatedClasses.length > 0 ? (
              <div
                style={{
                  display: "grid",
                  gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(320px, 1fr))",
                  gap: 12,
                }}
              >
                {banYouCreatedClasses.map((item) => (
                  <div
                    key={`created-${item.classId}`}
                    onClick={() => handleOpenBanYouClassDetail(item)}
                    style={{
                      border: "1px solid var(--ss-border-color)",
                      borderRadius: 12,
                      backgroundColor: "var(--ss-card-bg)",
                      overflow: "hidden",
                      cursor: "pointer",
                    }}
                  >
                    <div
                      style={{
                        padding: "12px 14px",
                        borderBottom: "1px solid var(--ss-border-color)",
                        fontWeight: 700,
                        color: "var(--ss-text-main)",
                        fontSize: 20,
                      }}
                    >
                      {item.classNickName} {item.invitationCode ? `(${item.invitationCode})` : ""}
                    </div>
                    <div style={{ padding: 14, display: "flex", gap: 12 }}>
                      {item.classAvatarDataUrl ? (
                        <img
                          src={item.classAvatarDataUrl}
                          alt={item.classNickName}
                          style={{ width: 72, height: 72, borderRadius: "50%", objectFit: "cover" }}
                        />
                      ) : (
                        <div
                          style={{
                            width: 72,
                            height: 72,
                            borderRadius: "50%",
                            backgroundColor: "var(--ss-bg-color)",
                            border: "1px dashed var(--ss-border-color)",
                          }}
                        />
                      )}
                      <div style={{ flex: 1, color: "var(--ss-text-main)" }}>
                        <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 6 }}>
                          {t("students.banyouClassTeacher")}
                          {item.masterName || "-"}
                        </div>
                        <div style={{ color: "var(--ss-text-secondary)", fontSize: 14 }}>
                          {t("students.banyouStudentCount")}
                          {Number(item.studentsNum ?? 0)}
                        </div>
                      </div>
                      <div
                        style={{
                          minWidth: 48,
                          display: "flex",
                          alignItems: "flex-end",
                          justifyContent: "center",
                          color: "var(--ss-text-main)",
                          fontSize: 42,
                          fontWeight: 700,
                          lineHeight: 1,
                        }}
                      >
                        {Number(item.praiseCount ?? 0)}
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <div style={{ color: "var(--ss-text-secondary)", fontSize: 13 }}>
                {t("students.banyouNoClasses")}
              </div>
            )}
          </div>

          <div style={{ marginTop: 8 }}>
            <h3 style={{ margin: "0 0 12px", color: "var(--ss-text-main)" }}>
              {t("students.banyouJoinedClasses")}
            </h3>
            {banYouJoinedClasses.length > 0 ? (
              <div
                style={{
                  display: "grid",
                  gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(320px, 1fr))",
                  gap: 12,
                }}
              >
                {banYouJoinedClasses.map((item) => (
                  <div
                    key={`joined-${item.classId}`}
                    onClick={() => handleOpenBanYouClassDetail(item)}
                    style={{
                      border: "1px solid var(--ss-border-color)",
                      borderRadius: 12,
                      backgroundColor: "var(--ss-card-bg)",
                      padding: 14,
                      color: "var(--ss-text-main)",
                      cursor: "pointer",
                    }}
                  >
                    <div style={{ fontWeight: 700, fontSize: 18, marginBottom: 8 }}>
                      {item.classNickName} {item.invitationCode ? `(${item.invitationCode})` : ""}
                    </div>
                    <div style={{ color: "var(--ss-text-secondary)", fontSize: 14 }}>
                      {t("students.banyouClassTeacher")}
                      {item.masterName || "-"}
                    </div>
                    <div style={{ color: "var(--ss-text-secondary)", fontSize: 14 }}>
                      {t("students.banyouStudentCount")}
                      {Number(item.studentsNum ?? 0)}
                    </div>
                    <div style={{ color: "var(--ss-text-secondary)", fontSize: 14 }}>
                      {t("students.banyouPraiseCount")}
                      {Number(item.praiseCount ?? 0)}
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <div style={{ color: "var(--ss-text-secondary)", fontSize: 13 }}>
                {t("students.banyouNoClasses")}
              </div>
            )}
          </div>
        </Space>
      </Modal>

      <Modal
        title={
          banYouSelectedClass
            ? `${t("students.banyouClassDetail")} - ${banYouSelectedClass.classNickName}`
            : t("students.banyouClassDetail")
        }
        open={banYouDetailVisible}
        onCancel={() => {
          setBanYouDetailVisible(false)
          setBanYouDetail(null)
          setBanYouSelectedClass(null)
          setBanYouTeamPlanOptions([])
          setBanYouSelectedTeamPlanId(undefined)
        }}
        width={980}
        destroyOnHidden
        onOk={handleImportBanYouSelected}
        okText={t("students.banyouImportSelected")}
        okButtonProps={{ loading: banYouImportLoading, disabled: !banYouDetail }}
        cancelText={t("common.cancel")}
      >
        {banYouDetailLoading ? (
          <div
            style={{ padding: "24px 0", textAlign: "center", color: "var(--ss-text-secondary)" }}
          >
            {t("common.loading")}
          </div>
        ) : !banYouDetail ? (
          <div style={{ color: "var(--ss-text-secondary)" }}>{t("students.banyouDetailEmpty")}</div>
        ) : (
          <div style={{ display: "flex", flexDirection: "column", gap: 16, maxHeight: "70vh" }}>
            {banYouTeamPlanOptions.length > 0 && (
              <div>
                <div style={{ fontWeight: 700, marginBottom: 8 }}>
                  {t("students.banyouTeamPlanSelector")}
                </div>
                <Space>
                  <Select
                    style={{ minWidth: 320 }}
                    value={banYouSelectedTeamPlanId}
                    onChange={(v) => setBanYouSelectedTeamPlanId(Number(v))}
                    options={banYouTeamPlanOptions.map((item) => ({
                      value: item.teamPlanId,
                      label: item.name?.trim()
                        ? `${item.name} (${item.teamPlanId})`
                        : String(item.teamPlanId),
                    }))}
                  />
                  <Button loading={banYouDetailLoading} onClick={handleReloadBanYouGroupByPlan}>
                    {t("students.banyouLoadTeamPlan")}
                  </Button>
                </Space>
                {banYouDetail.teamPlanSource ? (
                  <div style={{ marginTop: 6, color: "var(--ss-text-secondary)", fontSize: 12 }}>
                    {t("students.banyouTeamPlanSource")}
                    {banYouDetail.teamPlanSource}
                  </div>
                ) : null}
              </div>
            )}

            <div>
              <div style={{ fontWeight: 700, marginBottom: 8 }}>
                {t("students.banyouReasonList")}
              </div>
              <div
                style={{
                  border: "1px solid var(--ss-border-color)",
                  borderRadius: 8,
                  padding: 10,
                  maxHeight: 180,
                  overflowY: "auto",
                }}
              >
                <Checkbox.Group
                  value={banYouCheckedMedals}
                  onChange={(vals) => setBanYouCheckedMedals(vals as string[])}
                  style={{ width: "100%" }}
                >
                  <Space direction="vertical" style={{ width: "100%" }}>
                    {banYouDetail.medals.map((item, idx) => {
                      const key = medalKey(item, idx)
                      const type = Number(item.medalType ?? item.type ?? 1)
                      const value = Math.abs(Number(item.value ?? 1)) || 1
                      const delta = type < 0 ? -value : value
                      return (
                        <Checkbox key={key} value={key}>
                          {item.name} ({delta > 0 ? `+${delta}` : delta})
                        </Checkbox>
                      )
                    })}
                  </Space>
                </Checkbox.Group>
              </div>
            </div>

            <div>
              <div style={{ fontWeight: 700, marginBottom: 8 }}>
                {t("students.banyouStudentList")}
              </div>
              <div
                style={{
                  border: "1px solid var(--ss-border-color)",
                  borderRadius: 8,
                  padding: 10,
                  maxHeight: 220,
                  overflowY: "auto",
                }}
              >
                <Checkbox.Group
                  value={banYouCheckedStudents}
                  onChange={(vals) => setBanYouCheckedStudents(vals as string[])}
                  style={{ width: "100%" }}
                >
                  <Space direction="vertical" style={{ width: "100%" }}>
                    {banYouDetail.students.map((item) => (
                      <Checkbox key={item.studentId} value={item.studentId}>
                        {item.studentName}
                      </Checkbox>
                    ))}
                  </Space>
                </Checkbox.Group>
              </div>
            </div>

            <div>
              <div style={{ fontWeight: 700, marginBottom: 8 }}>{t("students.banyouTeamList")}</div>
              <div
                style={{
                  border: "1px solid var(--ss-border-color)",
                  borderRadius: 8,
                  padding: 10,
                  maxHeight: 220,
                  overflowY: "auto",
                }}
              >
                <Checkbox.Group
                  value={banYouCheckedTeams}
                  onChange={(vals) => setBanYouCheckedTeams(vals as string[])}
                  style={{ width: "100%" }}
                >
                  <Space direction="vertical" style={{ width: "100%" }}>
                    {banYouDetail.teams.length > 0 ? (
                      banYouDetail.teams.map((team) => (
                        <Checkbox key={team.teamId} value={team.teamId}>
                          {team.teamName} ({(team.students || []).length})
                        </Checkbox>
                      ))
                    ) : (
                      <span style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
                        {t("students.banyouNoTeams")}
                      </span>
                    )}
                  </Space>
                </Checkbox.Group>
              </div>
            </div>
          </div>
        )}
      </Modal>

      <input
        ref={xlsxInputRef}
        type="file"
        accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        style={{ display: "none" }}
        onChange={(e) => {
          const file = e.target.files?.[0]
          if (file) parseXlsxFile(file)
          if (xlsxInputRef.current) xlsxInputRef.current.value = ""
        }}
      />

      <Modal
        title={t("students.xlsxPreview")}
        open={xlsxVisible}
        onCancel={() => setXlsxVisible(false)}
        onOk={handleConfirmXlsxImport}
        okText={t("students.importConfirm")}
        okButtonProps={{ loading: xlsxLoading, disabled: xlsxSelectedCol == null }}
        width="80%"
        destroyOnHidden
      >
        <div style={{ marginBottom: "12px", color: "var(--ss-text-secondary)", fontSize: "12px" }}>
          <div>
            {t("students.file")}
            {xlsxFileName || "-"}
          </div>
          <div>
            {t("students.selectNameCol")}
            {xlsxSelectedCol == null ? "-" : excelColName(xlsxSelectedCol)}
          </div>
          <div>{t("students.previewRows")}</div>
        </div>
        <Table
          dataSource={xlsxPreviewRows}
          columns={xlsxPreviewColumns}
          rowKey="__row"
          bordered
          scroll={{ y: 420 }}
          style={{ backgroundColor: "var(--ss-card-bg)", color: "var(--ss-text-main)" }}
          pagination={false}
        />
      </Modal>

      <TagEditorDialog
        visible={tagEditVisible}
        onClose={() => {
          setTagEditVisible(false)
          setEditingStudent(null)
        }}
        onConfirm={handleSaveTags}
        initialTagIds={editingStudent?.tagIds || []}
        title={t("students.editTagTitle", { name: editingStudent?.name || "" })}
      />

      <Modal
        title={t("students.editAvatarTitle", { name: avatarStudent?.name || "" })}
        open={avatarVisible}
        onCancel={() => {
          setAvatarVisible(false)
          setAvatarStudent(null)
          setAvatarValue(null)
        }}
        onOk={handleSaveAvatar}
        okButtonProps={{ loading: avatarSaving, disabled: !avatarStudent }}
        okText={t("common.save")}
        cancelText={t("common.cancel")}
        destroyOnHidden
      >
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <div style={{ display: "flex", justifyContent: "center", padding: "8px 0" }}>
            {avatarValue ? (
              <img
                src={avatarValue}
                alt={avatarStudent?.name || "avatar"}
                style={{
                  width: 96,
                  height: 96,
                  borderRadius: "50%",
                  objectFit: "cover",
                  border: "1px solid var(--ss-border-color)",
                }}
              />
            ) : (
              <div
                style={{
                  width: 96,
                  height: 96,
                  borderRadius: "50%",
                  border: "1px dashed var(--ss-border-color)",
                  backgroundColor: "var(--ss-bg-color)",
                  color: "var(--ss-text-secondary)",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                }}
              >
                {t("students.noAvatar")}
              </div>
            )}
          </div>
          <div style={{ display: "flex", gap: 8 }}>
            <Button
              icon={<UploadOutlined />}
              onClick={() => avatarInputRef.current?.click()}
              disabled={!canEdit}
            >
              {t("students.avatarUpload")}
            </Button>
            <Button onClick={() => setAvatarValue(null)} disabled={!canEdit}>
              {t("students.avatarClear")}
            </Button>
          </div>
          <input
            ref={avatarInputRef}
            type="file"
            accept="image/*"
            style={{ display: "none" }}
            onChange={(e) => {
              const file = e.target.files?.[0]
              handleAvatarFileChange(file)
              if (avatarInputRef.current) avatarInputRef.current.value = ""
            }}
          />
          <div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
            {t("students.avatarTip")}
          </div>
        </div>
      </Modal>
    </div>
  )
}
