import "server-only"

import { cache } from "react"
import { createId } from "@paralleldrive/cuid2"
import { and, eq, isNotNull, isNull, or, lt } from "drizzle-orm"

import { db } from "@/shared/db"
import { gradeDrafts } from "@/shared/db/schema"

import { isGradeDraftData } from "./lib/type-guards"

/**
 * 锁机制常量。
 *
 * P3-6 协同录入锁：避免多位教师同时编辑同一班级+科目+类型的成绩，
 * 通过乐观 TTL（5 分钟）+ 令牌续期的方式实现悲观锁语义。
 */
const LOCK_TTL_MS = 5 * 60 * 1000

/**
 * 成绩录入草稿数据访问层。
 *
 * P1-2 重构：从 data-access.ts 拆分而来，使 data-access.ts 回到 800 行以内。
 * 草稿机制允许教师跨设备恢复未提交的成绩录入。
 */

/** 草稿数据结构（持久化到 DB JSON 列） */
export interface GradeDraftData {
  scores: Record<string, string>
  timestamp: number
}

/**
 * 保存成绩录入草稿到 DB（upsert）。
 * 每位教师对每个 classId+subjectId+type 组合只有一个草稿。
 */
export async function saveGradeDraft(params: {
  userId: string
  classId: string
  subjectId: string
  type: string
  data: GradeDraftData
}): Promise<void> {
  const { userId, classId, subjectId, type, data } = params

  // upsert：存在则更新，不存在则插入
  const existing = await db.query.gradeDrafts.findFirst({
    where: and(
      eq(gradeDrafts.userId, userId),
      eq(gradeDrafts.classId, classId),
      eq(gradeDrafts.subjectId, subjectId),
      eq(gradeDrafts.type, type)
    ),
  })

  if (existing) {
    await db
      .update(gradeDrafts)
      .set({ content: data })
      .where(eq(gradeDrafts.id, existing.id))
  } else {
    await db.insert(gradeDrafts).values({
      id: createId(),
      userId,
      classId,
      subjectId,
      type,
      content: data,
    })
  }
}

/**
 * 获取成绩录入草稿。
 * 超过 24 小时的草稿视为过期，返回 null。
 */
export const getGradeDraft = cache(
  async (params: {
    userId: string
    classId: string
    subjectId: string
    type: string
  }): Promise<GradeDraftData | null> => {
    const { userId, classId, subjectId, type } = params

    const draft = await db.query.gradeDrafts.findFirst({
      where: and(
        eq(gradeDrafts.userId, userId),
        eq(gradeDrafts.classId, classId),
        eq(gradeDrafts.subjectId, subjectId),
        eq(gradeDrafts.type, type)
      ),
    })

    if (!draft) return null

    // 从 DB JSON 列解析，使用类型守卫确保结构安全
    if (!isGradeDraftData(draft.content)) return null
    const content = draft.content
    // 24 小时过期
    if (Date.now() - content.timestamp > 24 * 60 * 60 * 1000) {
      return null
    }

    return content
  }
)

/**
 * 删除成绩录入草稿（提交成功后调用）。
 */
export async function deleteGradeDraft(params: {
  userId: string
  classId: string
  subjectId: string
  type: string
}): Promise<void> {
  const { userId, classId, subjectId, type } = params

  await db
    .delete(gradeDrafts)
    .where(
      and(
        eq(gradeDrafts.userId, userId),
        eq(gradeDrafts.classId, classId),
        eq(gradeDrafts.subjectId, subjectId),
        eq(gradeDrafts.type, type)
      )
    )
}

// ============================================================================
// P3-6 协同录入锁机制
// ============================================================================

/** 锁状态信息（含持有者信息，用于前端展示提示） */
export interface DraftLockStatus {
  isLocked: boolean
  lockedBy: string | null
  lockedByName: string | null
  lockedAt: string | null
  isExpired: boolean
  isMine: boolean
}

/** 获取锁的结果（包含令牌，前端续期/释放时需回传） */
export interface AcquireLockResult {
  acquired: boolean
  lockToken: string | null
  status: DraftLockStatus
}

/**
 * 查询某班级+科目+类型的锁状态。
 *
 * 锁以草稿记录为载体：草稿存在 lockedBy 字段即视为被锁。
 * 超过 TTL 的锁视为过期（自动失效）。
 */
export async function getDraftLockStatus(params: {
  classId: string
  subjectId: string
  type: string
  currentUserId: string
}): Promise<DraftLockStatus> {
  const { classId, subjectId, type, currentUserId } = params

  const draft = await db.query.gradeDrafts.findFirst({
    where: and(
      eq(gradeDrafts.classId, classId),
      eq(gradeDrafts.subjectId, subjectId),
      eq(gradeDrafts.type, type)
    ),
  })

  if (!draft || !draft.lockedBy) {
    return {
      isLocked: false,
      lockedBy: null,
      lockedByName: null,
      lockedAt: null,
      isExpired: false,
      isMine: false,
    }
  }

  const lockedAtMs = draft.lockedAt ? new Date(draft.lockedAt).getTime() : 0
  const isExpired = Date.now() - lockedAtMs > LOCK_TTL_MS

  return {
    isLocked: !isExpired,
    lockedBy: draft.lockedBy,
    lockedByName: null, // 不做跨模块 join，由 Action 层补充姓名
    lockedAt: draft.lockedAt ? draft.lockedAt.toISOString() : null,
    isExpired,
    isMine: draft.lockedBy === currentUserId,
  }
}

/**
 * 获取草稿锁（悲观语义）。
 *
 * 规则：
 * 1. 若锁不存在或已过期 → 创建/覆盖锁
 * 2. 若锁被自己持有且未过期 → 视为续期成功，刷新 token + 时间
 * 3. 若锁被他人持有且未过期 → 拒绝，返回当前状态
 * 4. 若锁被他人持有但已过期 → 抢占
 */
export async function acquireDraftLock(params: {
  classId: string
  subjectId: string
  type: string
  userId: string
}): Promise<AcquireLockResult> {
  const { classId, subjectId, type, userId } = params

  const status = await getDraftLockStatus({
    classId,
    subjectId,
    type,
    currentUserId: userId,
  })

  // 情况 3：他人持锁且未过期
  if (status.isLocked && !status.isMine) {
    return { acquired: false, lockToken: null, status }
  }

  // 情况 1/2/4：创建/续期/抢占
  const lockToken = createId()
  const now = new Date()

  const existing = await db.query.gradeDrafts.findFirst({
    where: and(
      eq(gradeDrafts.classId, classId),
      eq(gradeDrafts.subjectId, subjectId),
      eq(gradeDrafts.type, type)
    ),
  })

  if (existing) {
    await db
      .update(gradeDrafts)
      .set({
        lockedBy: userId,
        lockedAt: now,
        lockToken,
      })
      .where(eq(gradeDrafts.id, existing.id))
  } else {
    // 锁可以独立于草稿内容存在（教师刚进入页面尚未输入成绩）
    await db.insert(gradeDrafts).values({
      id: createId(),
      userId,
      classId,
      subjectId,
      type,
      content: { scores: {}, timestamp: now.getTime() },
      lockedBy: userId,
      lockedAt: now,
      lockToken,
    })
  }

  return {
    acquired: true,
    lockToken,
    status: {
      isLocked: true,
      lockedBy: userId,
      lockedByName: null,
      lockedAt: now.toISOString(),
      isExpired: false,
      isMine: true,
    },
  }
}

/**
 * 续期锁（心跳机制）。
 *
 * 前端每隔 2-3 分钟续期一次，避免教师长时间编辑时锁过期被抢占。
 * 校验 lockToken 防止他人伪造 userId 续期。
 */
export async function renewDraftLock(params: {
  classId: string
  subjectId: string
  type: string
  userId: string
  lockToken: string
}): Promise<AcquireLockResult> {
  const { classId, subjectId, type, userId, lockToken } = params

  const draft = await db.query.gradeDrafts.findFirst({
    where: and(
      eq(gradeDrafts.classId, classId),
      eq(gradeDrafts.subjectId, subjectId),
      eq(gradeDrafts.type, type)
    ),
  })

  if (!draft || !draft.lockedBy) {
    return {
      acquired: false,
      lockToken: null,
      status: await getDraftLockStatus({
        classId,
        subjectId,
        type,
        currentUserId: userId,
      }),
    }
  }

  // 令牌或持有人不匹配 → 拒绝
  if (draft.lockedBy !== userId || draft.lockToken !== lockToken) {
    return {
      acquired: false,
      lockToken: null,
      status: await getDraftLockStatus({
        classId,
        subjectId,
        type,
        currentUserId: userId,
      }),
    }
  }

  const newToken = createId()
  const now = new Date()
  await db
    .update(gradeDrafts)
    .set({ lockedAt: now, lockToken: newToken })
    .where(eq(gradeDrafts.id, draft.id))

  return {
    acquired: true,
    lockToken: newToken,
    status: {
      isLocked: true,
      lockedBy: userId,
      lockedByName: null,
      lockedAt: now.toISOString(),
      isExpired: false,
      isMine: true,
    },
  }
}

/**
 * 主动释放锁（提交成功或用户主动退出时调用）。
 *
 * 仅当 token + userId 匹配时才允许释放，避免他人恶意解锁。
 */
export async function releaseDraftLock(params: {
  classId: string
  subjectId: string
  type: string
  userId: string
  lockToken: string
}): Promise<boolean> {
  const { classId, subjectId, type, userId, lockToken } = params

  const result = await db
    .update(gradeDrafts)
    .set({ lockedBy: null, lockedAt: null, lockToken: null })
    .where(
      and(
        eq(gradeDrafts.classId, classId),
        eq(gradeDrafts.subjectId, subjectId),
        eq(gradeDrafts.type, type),
        eq(gradeDrafts.lockedBy, userId),
        eq(gradeDrafts.lockToken, lockToken)
      )
    )

  // drizzle MySQL 返回 [ResultSetHeader, FieldPacket[]]
  const [header] = result
  return (header?.affectedRows ?? 0) > 0
}

/**
 * 清理所有过期锁（Cron 任务调用）。
 *
 * 由于 acquireDraftLock 已实现"过期可抢占"语义，
 * 此函数主要用于：1) 减小表体积；2) 让前端轮询看到干净的"无锁"状态。
 */
export async function cleanupExpiredDraftLocks(): Promise<number> {
  const expiredThreshold = new Date(Date.now() - LOCK_TTL_MS)

  const result = await db
    .update(gradeDrafts)
    .set({ lockedBy: null, lockedAt: null, lockToken: null })
    .where(
      and(
        isNotNull(gradeDrafts.lockedBy),
        or(
          isNull(gradeDrafts.lockedAt),
          lt(gradeDrafts.lockedAt, expiredThreshold)
        )
      )
    )

  const [header] = result
  return header?.affectedRows ?? 0
}
