"use client"

import { useState, useRef, useEffect, useCallback, useMemo } from "react"
import { useFormStatus } from "react-dom"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { Search, TrendingUp, Trophy, AlertCircle, Download, Info, X } from "lucide-react"

import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/shared/components/ui/table"
import { cn } from "@/shared/lib/utils"
import { safeActionCall } from "@/shared/lib/action-utils"

import { batchCreateGradeRecordsAction, undoBatchCreateGradeRecordsAction, saveGradeDraftAction, getGradeDraftAction, deleteGradeDraftAction } from "../actions"
import type { SelectOption, GradeRecordType, GradeRecordSemester } from "../types"
import { isGradeType, isSemester } from "../lib/type-guards"

type Student = { id: string; name: string; email: string }

interface DraftData {
  scores: Record<string, string>
  timestamp: number
}

interface UndoData {
  ids: string[]
  timestamp: number
}

/** 类型守卫：验证 localStorage 草稿数据结构 */
function isDraftData(v: unknown): v is DraftData {
  if (typeof v !== "object" || v === null) return false
  // 从 unknown 转换为 Record 以访问属性（已通过对象检查）
  const obj = v as Record<string, unknown>
  if (typeof obj.timestamp !== "number") return false
  if (typeof obj.scores !== "object" || obj.scores === null) return false
  // 从 unknown 转换：已通过对象检查
  const scores = obj.scores as Record<string, unknown>
  return Object.values(scores).every((val) => typeof val === "string")
}

/** 类型守卫：验证 sessionStorage 撤销数据结构 */
function isUndoData(v: unknown): v is UndoData {
  if (typeof v !== "object" || v === null) return false
  // 从 unknown 转换为 Record 以访问属性（已通过对象检查）
  const obj = v as Record<string, unknown>
  if (typeof obj.timestamp !== "number") return false
  if (!Array.isArray(obj.ids)) return false
  return obj.ids.every((id) => typeof id === "string")
}

const MAX_SCORE = 100
const DRAFT_KEY_PREFIX = "grade-draft"

/** 将创建的记录 ID 序列化为可撤销的 sessionStorage 数据(提取到组件外部避免 purity lint) */
function serializeUndoData(ids: string[]): string {
  return JSON.stringify({ ids, timestamp: Date.now() })
}

function SubmitButton() {
  const t = useTranslations("grades")
  const { pending } = useFormStatus()
  return (
    <Button type="submit" disabled={pending}>
      {pending ? t("batch.saving") : t("batch.saveAllGrades")}
    </Button>
  )
}

export function BatchGradeEntry({
  classes,
  subjects,
  students,
  defaultClassId,
  defaultSubjectId,
}: {
  classes: SelectOption[]
  subjects: SelectOption[]
  students: Student[]
  defaultClassId?: string
  defaultSubjectId?: string
}) {
  const t = useTranslations("grades")
  const router = useRouter()
  const initialDraftKey = `${DRAFT_KEY_PREFIX}-${defaultClassId ?? classes[0]?.id ?? ""}-${defaultSubjectId ?? subjects[0]?.id ?? ""}-exam`
  const [classId, setClassId] = useState(defaultClassId ?? classes[0]?.id ?? "")
  const [subjectId, setSubjectId] = useState(defaultSubjectId ?? subjects[0]?.id ?? "")
  const [type, setType] = useState<GradeRecordType>("exam")
  const [semester, setSemester] = useState<GradeRecordSemester>("1")
  const [scores, setScores] = useState<Record<string, string>>(() => {
    // 惰性初始化：从 localStorage 恢复草稿（避免 useEffect 中 setState 导致级联渲染）
    // P3 修复：添加 typeof window !== "undefined" 检查，防止 SSR 报错
    if (typeof window === "undefined") return {}
    try {
      const raw = localStorage.getItem(initialDraftKey)
      if (raw) {
        const parsed: unknown = JSON.parse(raw)
        if (
          isDraftData(parsed) &&
          Date.now() - parsed.timestamp < 2 * 60 * 60 * 1000 &&
          Object.keys(parsed.scores).length > 0
        ) {
          return parsed.scores
        }
      }
    } catch {
      // 解析失败，忽略
    }
    return {}
  })
  const [draftRestored] = useState(() => {
    // 检查是否恢复了草稿（用于显示 toast）
    // P3 修复：添加 typeof window !== "undefined" 检查，防止 SSR 报错
    if (typeof window === "undefined") return false
    try {
      const raw = localStorage.getItem(initialDraftKey)
      if (raw) {
        const parsed: unknown = JSON.parse(raw)
        if (isDraftData(parsed)) {
          return (
            Date.now() - parsed.timestamp < 2 * 60 * 60 * 1000 &&
            Object.keys(parsed.scores).length > 0
          )
        }
      }
    } catch {
      // 解析失败，忽略
    }
    return false
  })
  const [searchQuery, setSearchQuery] = useState("")
  const [isSubmitting, setIsSubmitting] = useState(false)
  const inputRefs = useRef<Record<string, HTMLInputElement | null>>({})

  const draftKey = `${DRAFT_KEY_PREFIX}-${classId}-${subjectId}-${type}`

  // P3-12 修复：使用 ref 保存最新 scores，避免草稿保存 useEffect 依赖 scores 导致每次按键触发重建定时器
  const scoresRef = useRef(scores)
  useEffect(() => {
    scoresRef.current = scores
  }, [scores])

  // 草稿恢复提示（仅在首次挂载时显示一次）
  useEffect(() => {
    if (draftRestored) {
      toast.info(t("batch.restored"))
    }
  }, [draftRestored, t])

  // v3-P2-10: 服务端草稿恢复（localStorage 无草稿时，尝试从服务端恢复）
  useEffect(() => {
    if (draftRestored) return // 本地已恢复，跳过
    if (typeof window === "undefined") return

    let cancelled = false
    void (async () => {
      const result = await safeActionCall(() =>
        getGradeDraftAction({ classId, subjectId, type })
      )
      if (cancelled || !result || !result.success || !result.data) return

      const draft = result.data
      // 24 小时过期检查（服务端已检查，这里双重校验）
      if (Date.now() - draft.timestamp > 24 * 60 * 60 * 1000) return
      if (Object.keys(draft.scores).length === 0) return

      setScores(draft.scores)
      toast.info(t("batch.restoredFromServer"))
    })()

    return () => { cancelled = true }
  }, [classId, subjectId, type, draftRestored, t])

  const handleScoreChange = useCallback((studentId: string, value: string) => {
    if (value === "" || /^\d*\.?\d{0,2}$/.test(value)) {
      setScores((prev) => ({ ...prev, [studentId]: value }))
    }
  }, [])

  const validateScore = useCallback((value: string): boolean => {
    if (value === "") return true
    const num = Number(value)
    return !isNaN(num) && num >= 0 && num <= MAX_SCORE
  }, [])

  const restoreDraft = useCallback((key: string): boolean => {
    // P3 修复：添加 typeof window !== "undefined" 检查，防止 SSR 报错
    if (typeof window === "undefined") return false
    try {
      const raw = localStorage.getItem(key)
      if (raw) {
        const parsed: unknown = JSON.parse(raw)
        if (
          isDraftData(parsed) &&
          Date.now() - parsed.timestamp < 2 * 60 * 60 * 1000 &&
          Object.keys(parsed.scores).length > 0
        ) {
          setScores(parsed.scores)
          return true
        }
      }
    } catch {
      // 解析失败，忽略
    }
    return false
  }, [])

  const handleClassChange = (newClassId: string) => {
    const hasUnsaved = Object.keys(scores).length > 0
    if (hasUnsaved && newClassId !== classId) {
      if (!window.confirm(t("batch.confirmSwitchClass"))) {
        return
      }
    }
    setClassId(newClassId)
    setScores({})
    // 切换班级后尝试恢复该班级的草稿
    const newDraftKey = `${DRAFT_KEY_PREFIX}-${newClassId}-${subjectId}-${type}`
    if (restoreDraft(newDraftKey)) {
      toast.info(t("batch.restored"))
    }
    const newUrl = newClassId ? `/teacher/grades/entry?classId=${encodeURIComponent(newClassId)}` : "/teacher/grades/entry"
    router.push(newUrl)
  }

  const filteredStudents = useMemo(
    () => students.filter((s) => !searchQuery || s.name.toLowerCase().includes(searchQuery.toLowerCase())),
    [students, searchQuery]
  )

  /**
   * Excel 粘贴处理器：从剪贴板解析一列分数，按当前学生列表顺序填充。
   * 支持格式：
   * - 单列分数（每行一个数字）
   * - 多列（取第一列或第一个可解析为数字的列）
   * - Tab/逗号/空格分隔
   */
  const handlePaste = useCallback((e: React.ClipboardEvent<HTMLInputElement>, startIndex: number) => {
    const text = e.clipboardData.getData("text")
    if (!text) return

    // 按行分割（兼容 \r\n 和 \n）
    const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
    if (lines.length === 0) return

    // 尝试从每行提取数字（支持单列和多列格式）
    const values: string[] = []
    for (const line of lines) {
      const trimmed = line.trim()
      // 尝试 Tab/逗号分隔，取第一个可解析为数字的部分
      const parts = trimmed.split(/[\t,]/).map((p) => p.trim())
      let found: string | null = null
      for (const part of parts) {
        if (part !== "" && /^\d*\.?\d+$/.test(part)) {
          found = part
          break
        }
      }
      // 如果多列没匹配到，尝试整行作为数字
      if (found === null && /^\d*\.?\d+$/.test(trimmed)) {
        found = trimmed
      }
      if (found !== null) values.push(found)
    }

    if (values.length === 0) {
      toast.error(t("batch.pasteNoMatch"))
      e.preventDefault()
      return
    }

    // 按当前过滤后的学生列表顺序填充
    const targetStudents = filteredStudents.slice(startIndex)
    const newScores: Record<string, string> = {}
    let appliedCount = 0
    for (let i = 0; i < Math.min(values.length, targetStudents.length); i += 1) {
      const student = targetStudents[i]
      if (!student) break
      const val = values[i]
      // 校验分数格式（与 handleScoreChange 一致）
      if (/^\d*\.?\d{0,2}$/.test(val)) {
        newScores[student.id] = val
        appliedCount += 1
      }
    }

    if (appliedCount > 0) {
      setScores((prev) => ({ ...prev, ...newScores }))
      toast.success(t("batch.pasteApplied", { count: appliedCount }))
      e.preventDefault()
    }
  }, [filteredStudents, t])

  const stats = useMemo(() => {
    const validScores = students
      .map((s) => scores[s.id])
      .filter((v): v is string => v !== undefined && v !== "" && validateScore(v))
      .map(Number)
    const entered = validScores.length
    if (entered === 0) return { entered: 0, average: 0, max: 0, min: 0, total: students.length }
    const sum = validScores.reduce((acc, v) => acc + v, 0)
    return {
      entered,
      average: Math.round((sum / entered) * 100) / 100,
      max: Math.max(...validScores),
      min: Math.min(...validScores),
      total: students.length,
    }
  }, [scores, students, validateScore])

  // v3-P3-1: 下载成绩录入模板（CSV，含学生姓名列表，支持 Excel 粘贴）
  const handleDownloadTemplate = useCallback(() => {
    const headers = [t("batch.templateStudentName"), t("batch.templateScore"), t("batch.templateRemark")]
    const rows = students.map((s) => [s.name, "", ""])
    const csv = [headers, ...rows]
      .map((r) => r.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(","))
      .join("\n")
    // 添加 BOM 以支持 Excel 正确识别 UTF-8
    const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" })
    const url = URL.createObjectURL(blob)
    const a = document.createElement("a")
    a.href = url
    a.download = t("batch.templateFilename")
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  }, [students, t])

  // v4-P3-2: 新手引导提示框，使用 localStorage 记住用户是否已关闭
  const [guideVisible, setGuideVisible] = useState<boolean>(() => {
    if (typeof window === "undefined") return true
    try {
      return localStorage.getItem("grade-entry-guide-dismissed") !== "true"
    } catch {
      return true
    }
  })

  const handleDismissGuide = useCallback(() => {
    setGuideVisible(false)
    try {
      localStorage.setItem("grade-entry-guide-dismissed", "true")
    } catch {
      // localStorage 不可用时静默失败
    }
  }, [])

  const hasInvalidScores = Object.values(scores).some((v) => v !== "" && v !== undefined && !validateScore(v))

  // v3-P2-10: 草稿保存到 localStorage（30秒间隔）+ 服务端同步（60秒间隔）
  // P3-12 修复：使用 scoresRef 读取最新 scores，定时器不再依赖 scores，避免每次按键重建定时器
  useEffect(() => {
    // P3 修复：添加 typeof window !== "undefined" 检查，防止 SSR 报错
    if (typeof window === "undefined") return

    // 本地草稿：30 秒间隔，快速恢复
    const localInterval = setInterval(() => {
      const currentScores = scoresRef.current
      if (Object.keys(currentScores).length > 0) {
        try {
          localStorage.setItem(draftKey, JSON.stringify({ scores: currentScores, timestamp: Date.now() }))
        } catch {
          // localStorage 可能已满或不可用，静默失败
        }
      }
    }, 30000)

    // 服务端草稿：60 秒间隔，跨设备同步
    const serverInterval = setInterval(() => {
      const currentScores = scoresRef.current
      if (Object.keys(currentScores).length > 0) {
        void safeActionCall(() =>
          saveGradeDraftAction({
            classId,
            subjectId,
            type,
            scores: currentScores,
          })
        )
      }
    }, 60000)

    return () => {
      clearInterval(localInterval)
      clearInterval(serverInterval)
    }
  }, [draftKey, classId, subjectId, type])

  // 清除草稿（本地 + 服务端）
  const clearDraft = useCallback(() => {
    // P3 修复：添加 typeof window !== "undefined" 检查，防止 SSR 报错
    if (typeof window === "undefined") return
    try {
      localStorage.removeItem(draftKey)
    } catch {
      // 忽略
    }
    // v3-P2-10: 同步清除服务端草稿
    void safeActionCall(() =>
      deleteGradeDraftAction({ classId, subjectId, type })
    )
  }, [draftKey, classId, subjectId, type])

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, studentId: string) => {
    if (e.key === "Enter") {
      e.preventDefault()
      const currentIndex = filteredStudents.findIndex((s) => s.id === studentId)
      const nextStudent = filteredStudents[currentIndex + 1]
      if (nextStudent && inputRefs.current[nextStudent.id]) {
        inputRefs.current[nextStudent.id]?.focus()
        inputRefs.current[nextStudent.id]?.select()
      }
    }
  }

  const handleSubmit = async (formData: FormData) => {
    if (!classId || !subjectId) {
      toast.error(t("batch.selectClassAndSubject"))
      return
    }

    if (hasInvalidScores) {
      toast.error(t("batch.invalidScoresError"))
      return
    }

    // P3 修复：区分"未输入"和"输入 0"
    // 只有当 scores[studentId] 有值且非空字符串时才视为已输入
    // 这样输入 0 会被正确提交，而未输入会被跳过
    const records = students
      .map((s) => {
        const raw = scores[s.id]
        if (raw === undefined || raw === "") return null
        return {
          studentId: s.id,
          score: Number(raw),
        }
      })
      .filter((r): r is { studentId: string; score: number } => r !== null)

    if (records.length === 0) {
      toast.error(t("batch.enterAtLeastOne"))
      return
    }

    setIsSubmitting(true)
    formData.set("classId", classId)
    formData.set("subjectId", subjectId)
    formData.set("type", type)
    formData.set("semester", semester)
    formData.set("recordsJson", JSON.stringify(records))

    // P3 修复：使用 safeActionCall 包装，确保异常时也能重置 loading 状态
    const result = await safeActionCall(
      () => batchCreateGradeRecordsAction(null, formData),
      {
        onError: () => toast.error(t("error.saveFailed")),
        onFinally: () => setIsSubmitting(false),
      }
    )
    if (result?.success) {
      clearDraft()
      // v3-P2-3：保存创建的记录 ID 到 sessionStorage，供撤销使用
      const createdIds = result.data ?? []
      if (createdIds.length > 0) {
        try {
          sessionStorage.setItem(
            "lastBatchGradeRecordIds",
            serializeUndoData(createdIds)
          )
        } catch {
          // sessionStorage 不可用时静默失败（不影响主流程）
        }
        // 显示带撤销按钮的 toast，10 秒内可撤销
        toast.success(result.message, {
          duration: 10000,
          action: {
            label: t("batch.undo"),
            onClick: () => handleUndo(),
          },
        })
      } else {
        toast.success(result.message)
      }
      router.push("/teacher/grades")
      router.refresh()
    } else if (result) {
      toast.error(result.message || t("error.saveFailed"))
    }
  }

  /**
   * v3-P2-3: 撤销最近一次批量录入。
   * 从 sessionStorage 读取上次创建的记录 ID 列表，调用 undo action 删除。
   */
  const handleUndo = useCallback(async () => {
    if (typeof window === "undefined") return
    let ids: string[] = []
    try {
      const raw = sessionStorage.getItem("lastBatchGradeRecordIds")
      if (!raw) {
        toast.error(t("batch.undoNoRecord"))
        return
      }
      const parsed: unknown = JSON.parse(raw)
      if (!isUndoData(parsed)) {
        toast.error(t("batch.undoNoRecord"))
        return
      }
      // 仅允许 5 分钟内的撤销
      if (Date.now() - parsed.timestamp > 5 * 60 * 1000) {
        sessionStorage.removeItem("lastBatchGradeRecordIds")
        toast.error(t("batch.undoExpired"))
        return
      }
      ids = parsed.ids
    } catch {
      toast.error(t("batch.undoNoRecord"))
      return
    }

    if (ids.length === 0) {
      toast.error(t("batch.undoNoRecord"))
      return
    }

    const result = await safeActionCall(
      () => undoBatchCreateGradeRecordsAction(ids),
      {
        onError: () => toast.error(t("batch.undoFailed")),
      }
    )
    if (result?.success) {
      try {
        sessionStorage.removeItem("lastBatchGradeRecordIds")
      } catch {
        // 忽略
      }
      toast.success(result.message)
      router.refresh()
    } else if (result) {
      toast.error(result.message || t("batch.undoFailed"))
    }
  }, [router, t])

  return (
    <Card className="relative">
      {isSubmitting && (
        <div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/60 backdrop-blur-sm">
          <div className="flex items-center gap-2 text-sm font-medium">
            <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
            {t("batch.savingGrades")}
          </div>
        </div>
      )}
      <CardHeader>
        <CardTitle>{t("batch.title")}</CardTitle>
        <p className="text-sm text-muted-foreground">
          {t("batch.fullScoreHint", { max: MAX_SCORE })}
        </p>
      </CardHeader>
      <CardContent>
        {/* v4-P3-2: 新手引导提示框 */}
        {guideVisible && (
          <div
            role="region"
            aria-label={t("batch.guide.title")}
            className="mb-6 rounded-md border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40"
          >
            <div className="flex items-start gap-3">
              <Info className="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" aria-hidden="true" />
              <div className="flex-1 space-y-2">
                <h3 className="text-sm font-semibold text-blue-900 dark:text-blue-100">
                  {t("batch.guide.title")}
                </h3>
                <ol className="list-decimal space-y-1 pl-4 text-sm text-blue-800 dark:text-blue-200">
                  <li>{t("batch.guide.step1")}</li>
                  <li>{t("batch.guide.step2")}</li>
                  <li>{t("batch.guide.step3")}</li>
                  <li>{t("batch.guide.step4")}</li>
                </ol>
              </div>
              <Button
                type="button"
                variant="ghost"
                size="icon"
                className="h-7 w-7 shrink-0 text-blue-600 hover:bg-blue-100 hover:text-blue-700 dark:text-blue-400 dark:hover:bg-blue-900"
                onClick={handleDismissGuide}
                aria-label={t("batch.guide.dismissAriaLabel")}
              >
                <X className="h-4 w-4" />
              </Button>
            </div>
          </div>
        )}
        <form action={handleSubmit} className="space-y-6">
          <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
            <div className="grid gap-2">
              <Label htmlFor="batch-class">{t("filters.class")}</Label>
              <Select value={classId} onValueChange={handleClassChange}>
                <SelectTrigger id="batch-class">
                  <SelectValue placeholder={t("form.selectClass")} />
                </SelectTrigger>
                <SelectContent>
                  {classes.map((c) => (
                    <SelectItem key={c.id} value={c.id}>
                      {c.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>

            <div className="grid gap-2">
              <Label htmlFor="batch-subject">{t("filters.subject")}</Label>
              <Select value={subjectId} onValueChange={setSubjectId}>
                <SelectTrigger id="batch-subject">
                  <SelectValue placeholder={t("form.selectSubject")} />
                </SelectTrigger>
                <SelectContent>
                  {subjects.map((s) => (
                    <SelectItem key={s.id} value={s.id}>
                      {s.name}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>

            <div className="grid gap-2 md:col-span-2">
              <Label htmlFor="title">{t("batch.examQuizTitle")}</Label>
              <Input id="title" name="title" placeholder={t("form.titlePlaceholder")} required />
            </div>

            <div className="grid gap-2">
              <Label htmlFor="fullScore">{t("form.fullScore")}</Label>
              <Input id="fullScore" name="fullScore" type="number" step="0.01" min="1" defaultValue={String(MAX_SCORE)} />
            </div>

            <div className="grid gap-2">
              <Label htmlFor="batch-type">{t("filters.type")}</Label>
              <Select value={type} onValueChange={(v) => { if (isGradeType(v)) setType(v) }}>
                <SelectTrigger id="batch-type">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="exam">{t("type.exam")}</SelectItem>
                  <SelectItem value="quiz">{t("type.quiz")}</SelectItem>
                  <SelectItem value="homework">{t("type.homework")}</SelectItem>
                  <SelectItem value="other">{t("type.other")}</SelectItem>
                </SelectContent>
              </Select>
            </div>

            <div className="grid gap-2">
              <Label htmlFor="batch-semester">{t("filters.semester")}</Label>
              <Select value={semester} onValueChange={(v) => { if (isSemester(v)) setSemester(v) }}>
                <SelectTrigger id="batch-semester">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="1">{t("semester.s1")}</SelectItem>
                  <SelectItem value="2">{t("semester.s2")}</SelectItem>
                </SelectContent>
              </Select>
            </div>
          </div>

          {students.length === 0 ? (
            <p className="text-sm text-muted-foreground">{t("batch.noStudentsInClass")}</p>
          ) : (
            <>
              {/* 实时统计栏 */}
              <div className="flex flex-col gap-3 rounded-md border bg-muted/30 p-3 sm:flex-row sm:items-center sm:justify-between">
                <div className="flex flex-wrap items-center gap-4 text-sm">
                  <span className="inline-flex items-center gap-1.5">
                    <span className="text-muted-foreground">{t("batch.entered")}</span>
                    <span className="font-semibold tabular-nums">{stats.entered}</span>
                    <span className="text-muted-foreground">/ {stats.total}</span>
                  </span>
                  {stats.entered > 0 && (
                    <>
                      <span className="inline-flex items-center gap-1.5">
                        <TrendingUp className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
                        <span className="text-muted-foreground">{t("batch.average")}</span>
                        <span className="font-semibold tabular-nums">{stats.average}</span>
                      </span>
                      <span className="inline-flex items-center gap-1.5">
                        <Trophy className="h-3.5 w-3.5 text-amber-500" aria-hidden="true" />
                        <span className="text-muted-foreground">{t("batch.max")}</span>
                        <span className="font-semibold tabular-nums">{stats.max}</span>
                      </span>
                      <span className="inline-flex items-center gap-1.5">
                        <span className="text-muted-foreground">{t("batch.min")}</span>
                        <span className="font-semibold tabular-nums">{stats.min}</span>
                      </span>
                    </>
                  )}
                  <span className="text-xs text-muted-foreground">{t("batch.pasteHint")}</span>
                </div>
                <div className="flex items-center gap-2">
                  {hasInvalidScores && (
                    <span className="inline-flex items-center gap-1 text-xs text-destructive">
                      <AlertCircle className="h-3.5 w-3.5" aria-hidden="true" />
                      {t("batch.invalidScoresBadge")}
                    </span>
                  )}
                  {/* v3-P3-1: 下载成绩录入模板 */}
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    className="h-8"
                    onClick={handleDownloadTemplate}
                    aria-label={t("batch.templateAriaLabel")}
                    disabled={students.length === 0}
                  >
                    <Download className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />
                    {t("batch.downloadTemplate")}
                  </Button>
                  <div className="relative">
                    <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
                    <Input
                      type="search"
                      placeholder={t("batch.searchStudent")}
                      value={searchQuery}
                      onChange={(e) => setSearchQuery(e.target.value)}
                      className="h-8 w-40 pl-8 text-sm"
                    />
                  </div>
                </div>
              </div>
              <div className="rounded-md border">
                <Table>
                  <caption className="sr-only">{t("batch.caption")}</caption>
                  <TableHeader>
                    <TableRow>
                      <TableHead className="w-12">#</TableHead>
                      <TableHead>{t("list.columns.student")}</TableHead>
                      <TableHead className="hidden md:table-cell">{t("batch.emailColumn")}</TableHead>
                      <TableHead className="w-32">{t("batch.score")}</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {filteredStudents.map((s, idx) => {
                      const scoreValue = scores[s.id] ?? ""
                      const isInvalid = scoreValue !== "" && !validateScore(scoreValue)
                      return (
                        <TableRow key={s.id}>
                          <TableCell className="text-muted-foreground tabular-nums">{idx + 1}</TableCell>
                          <TableCell className="font-medium">{s.name}</TableCell>
                          <TableCell className="hidden text-muted-foreground md:table-cell">{s.email}</TableCell>
                          <TableCell>
                            <Input
                              ref={(el) => { inputRefs.current[s.id] = el }}
                              type="number"
                              step="0.01"
                              min="0"
                              max={MAX_SCORE}
                              placeholder="0"
                              value={scoreValue}
                              onChange={(e) => handleScoreChange(s.id, e.target.value)}
                              onKeyDown={(e) => handleKeyDown(e, s.id)}
                              onPaste={(e) => handlePaste(e, idx)}
                              className={cn("h-8", isInvalid && "border-destructive focus-visible:ring-destructive")}
                              aria-invalid={isInvalid}
                            />
                          </TableCell>
                        </TableRow>
                      )
                    })}
                  </TableBody>
                </Table>
              </div>
            </>
          )}

          <CardFooter className="justify-end gap-2 px-0">
            <Button type="button" variant="outline" onClick={() => router.back()}>
              {t("batch.cancel")}
            </Button>
            <SubmitButton />
          </CardFooter>
        </form>
      </CardContent>
    </Card>
  )
}
