import { Router, Response, NextFunction } from 'express'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { z } from 'zod'
import { decrypt } from '../lib/crypto'
import {
  getTemplate,
  calcBatchEntry,
  getPayrollRiskWarnings,
  generatePayslipFromBatches,
  prePayrollCheck,
} from '../services/payroll.service'
import { isInProbation } from '../services/contract.service'
import { getBonusByMonthAndEmployeeIds } from '../services/commission-bonus.service'

function safeDecryptStr(encrypted: string | null): string | null {
  if (!encrypted) return null
  try {
    if (!encrypted.includes(':')) return encrypted
    return decrypt(encrypted)
  } catch { return null }
}

/**
 * 查询上月递延扣款数据（上月已归档批次中该员工的递延金额）
 * 用于本月创建批次时补扣上月未扣完的社保/公积金/最低工资补齐
 */
async function getPrevDeferred(orgId: string, employeeId: string, currentMonth: string): Promise<{ socialEmp: number; housingEmp: number; minWage: number }> {
  // 计算上月份
  const [y, m] = currentMonth.split('-').map(Number)
  const prevDate = new Date(y, m - 2, 1)
  const prevMonth = `${prevDate.getFullYear()}-${String(prevDate.getMonth() + 1).padStart(2, '0')}`

  // 查上月已归档批次中该员工的递延记录
  const prevEntries = await prisma.batchEntry.findMany({
    where: {
      orgId,
      employeeId,
      batch: { month: prevMonth, status: 'ARCHIVED' },
    },
    select: {
      deferredSocialEmp: true,
      deferredHousingEmp: true,
      deferredMinWage: true,
    },
  })

  // 汇总上月所有批次的递延金额
  const result = {
    socialEmp: prevEntries.reduce((s, e) => s + (e.deferredSocialEmp || 0), 0),
    housingEmp: prevEntries.reduce((s, e) => s + (e.deferredHousingEmp || 0), 0),
    minWage: prevEntries.reduce((s, e) => s + (e.deferredMinWage || 0), 0),
  }

  return result
}

/**
 * 重算单个 BatchEntry 并更新到数据库
 * 用于批量操作（获取绩效/奖金/扣款等）后重算 totalPay/tax/netPay
 */
async function recalcEntry(orgId: string, batchId: string, batchMonth: string, batchType: string, entryId: string, employeeId: string) {
  const entry = await prisma.batchEntry.findUnique({ where: { id: entryId } })
  if (!entry) return

  const inputs = {
    baseSalary: entry.baseSalary,
    performanceSalary: entry.performanceSalary || undefined,
    overtimePay: entry.overtimePay,
    allowance: entry.allowance,
    deduction: entry.deduction,
    bonus: entry.bonus,
    positionSalary: entry.positionSalary || undefined,
    senioritySalary: entry.senioritySalary || undefined,
    transportAllowance: entry.transportAllowance || undefined,
    mealAllowance: entry.mealAllowance || undefined,
    housingAllowance: entry.housingAllowance || undefined,
    communicationAllowance: entry.communicationAllowance || undefined,
    otherDeduction: entry.otherDeduction || undefined,
  }

  const prevDeferred = await getPrevDeferred(orgId, employeeId, batchMonth)
  const calcResult = await calcBatchEntry(orgId, employeeId, batchMonth, inputs, batchType as any, { prevDeferred }, (entry.extraItems as Record<string, number>) || null)

  const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, extraItems: _ei, ...entryData } = calcResult
  await prisma.batchEntry.update({
    where: { id: entryId },
    data: { ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined },
  })
}

/**
 * 重算批次汇总
 */
async function recalcBatchTotals(batchId: string) {
  const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
  const totals = allEntries.reduce((acc, e) => ({
    totalPay: acc.totalPay + e.totalPay,
    totalNetPay: acc.totalNetPay + e.netPay,
    totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
    totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
    totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
    totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
    totalTax: acc.totalTax + e.tax,
  }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })

  await prisma.payrollBatch.update({
    where: { id: batchId },
    data: {
      totalPay: Math.round(totals.totalPay * 100) / 100,
      totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
      totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
      totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
      totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
      totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
      totalTax: Math.round(totals.totalTax * 100) / 100,
    },
  })
}

/**
 * 检查同月其他批次中哪些员工已有非零值
 * 返回有重复的员工ID集合
 */
async function checkOtherBatches(orgId: string, currentBatchId: string, month: string, field: string): Promise<Set<string>> {
  const otherEntries = await prisma.batchEntry.findMany({
    where: {
      orgId,
      batch: { month, id: { not: currentBatchId } },
    },
    select: { employeeId: true, [field]: true },
  })
  const duplicateIds = new Set<string>()
  for (const e of otherEntries) {
    if ((e as any)[field] > 0) duplicateIds.add(e.employeeId)
  }
  return duplicateIds
}


function contentDisposition(filename: string): string {
  const encoded = encodeURIComponent(filename)
  return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`
}

const router = Router()
router.use(authMiddleware)

// ========== 薪酬模版 ==========

// 获取薪酬模版
router.get('/template', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const items = await getTemplate(req.user!.orgId)
    res.json({ success: true, data: items })
  } catch (err) {
    next(err)
  }
})

// 更新薪酬模版项
const updateTemplateItemSchema = z.object({
  name: z.string().min(1).optional(),
  formula: z.string().nullable().optional(),
  calcType: z.enum(['FORMULA', 'SYSTEM']).optional(),
  dependencies: z.string().optional(),
  dataSource: z.string().nullable().optional(),
  order: z.number().int().optional(),
  isEditable: z.boolean().optional(),
})

router.put('/template/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const data = updateTemplateItemSchema.parse(req.body)
    const item = await prisma.payslipItem.findFirst({
      where: { id: req.params.id, orgId: req.user!.orgId },
    })
    if (!item) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模版项不存在' } })

    const updateData: any = {}
    if (data.name !== undefined && !item.isDefault) updateData.name = data.name
    if (data.formula !== undefined) updateData.formula = data.formula
    if (data.order !== undefined) updateData.order = data.order
    if (data.isEditable !== undefined) updateData.isEditable = data.isEditable
    if (data.calcType !== undefined) updateData.calcType = data.calcType
    if (data.dependencies !== undefined) updateData.dependencies = data.dependencies
    if (data.dataSource !== undefined) updateData.dataSource = data.dataSource

    const updated = await prisma.payslipItem.update({ where: { id: req.params.id }, data: updateData })
    res.json({ success: true, data: updated })
  } catch (err) {
    next(err)
  }
})

// 新增薪酬模版项
const createTemplateItemSchema = z.object({
  name: z.string().min(1),
  code: z.string().min(1),
  type: z.enum(['INPUT', 'CALCULATED']),
  formula: z.string().nullable().optional(),
  calcType: z.enum(['FORMULA', 'SYSTEM']).default('FORMULA'),
  dependencies: z.string().default('[]'),
  dataSource: z.string().nullable().default(null),
  order: z.number().int().default(99),
  isEditable: z.boolean().default(true),
})

router.post('/template', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const data = createTemplateItemSchema.parse(req.body)
    const item = await prisma.payslipItem.create({
      data: { ...data, orgId: req.user!.orgId, isDefault: false },
    })
    res.json({ success: true, data: item })
  } catch (err) {
    next(err)
  }
})

// 删除薪酬模版项（仅非预置项）
router.delete('/template/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const item = await prisma.payslipItem.findFirst({
      where: { id: req.params.id, orgId: req.user!.orgId },
    })
    if (!item) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模版项不存在' } })
    if (item.isDefault) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '预置项不可删除' } })

    await prisma.payslipItem.delete({ where: { id: req.params.id } })
    res.json({ success: true })
  } catch (err) {
    next(err)
  }
})

// ========== 发薪批次 ==========

// 检查本月是否已发薪
router.get('/batches/check', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { month } = req.query
    if (!month) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })

    const archivedBatches = await prisma.payrollBatch.count({
      where: { orgId: req.user!.orgId, month: String(month), status: 'ARCHIVED' },
    })
    const draftBatches = await prisma.payrollBatch.count({
      where: { orgId: req.user!.orgId, month: String(month), status: 'DRAFT' },
    })
    const publishedPayslips = await prisma.payslip.count({
      where: { orgId: req.user!.orgId, month: String(month), status: 'PUBLISHED' },
    })

    res.json({
      success: true,
      data: {
        hasArchivedBatch: archivedBatches > 0,
        archivedCount: archivedBatches,
        draftCount: draftBatches,
        payslipsPublished: publishedPayslips > 0,
      },
    })
  } catch (err) {
    next(err)
  }
})

// 获取可复制的归档批次列表
router.get('/batches/archived/list', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const orgId = req.user!.orgId
    const batches = await prisma.payrollBatch.findMany({
      where: { orgId, status: 'ARCHIVED' },
      orderBy: [{ month: 'desc' }, { batchNo: 'desc' }],
      select: { id: true, name: true, month: true, type: true, employeeCount: true, totalPay: true, totalNetPay: true },
      take: 20,
    })
    res.json({ success: true, data: batches })
  } catch (err) {
    next(err)
  }
})

// 获取批次列表
router.get('/batches', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { month, monthFrom, monthTo, status, type, dateFrom, dateTo } = req.query
    const batches = await prisma.payrollBatch.findMany({
      where: {
        orgId: req.user!.orgId,
        ...(month ? { month: String(month) } : {}),
        ...(monthFrom ? { month: { gte: String(monthFrom) } } : {}),
        ...(monthTo ? { month: { lte: String(monthTo) } } : {}),
        ...(status ? { status: String(status) as any } : {}),
        ...(type ? { type: String(type) as any } : {}),
        ...(dateFrom ? { createdAt: { gte: new Date(String(dateFrom)) } } : {}),
        ...(dateTo ? { createdAt: { lte: new Date(String(dateTo) + 'T23:59:59') } } : {}),
      },
      orderBy: [{ createdAt: 'desc' }, { month: 'desc' }, { batchNo: 'asc' }],
    })
    res.json({ success: true, data: batches })
  } catch (err) {
    next(err)
  }
})

// 获取批次详情
router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const rawBatch = await prisma.payrollBatch.findFirst({
      where: { id: req.params.id, orgId: req.user!.orgId },
      include: {
        entries: {
          include: {
            employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } },
          },
          orderBy: { employee: { name: 'asc' } },
        },
      },
    })
    if (!rawBatch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })

    // 查询批次月份的绩效记录，附加到 entry 上
    const perfRecords = await prisma.performanceRecord.findMany({
      where: { orgId: req.user!.orgId, period: rawBatch.month, periodType: 'MONTHLY' },
      select: { employeeId: true, grade: true, score: true, coefficient: true },
    })
    // 等级默认系数：优先从组织缺省配置取，回退到硬编码
    const orgForCoeff = await prisma.organization.findUnique({ where: { id: req.user!.orgId }, select: { defaultGradeCoefficients: true } })
    const DEFAULT_GRADE_COEFFICIENTS: Record<string, number> = (orgForCoeff?.defaultGradeCoefficients as Record<string, number>) || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
    const perfMap = new Map<string, { grade: string; coefficient: number }>()
    for (const r of perfRecords) {
      if (!perfMap.has(r.employeeId)) {
        // 优先用记录中保存的 coefficient，回退到等级默认值
        const coeff = r.coefficient != null ? r.coefficient : (DEFAULT_GRADE_COEFFICIENTS[r.grade] || 1.0)
        perfMap.set(r.employeeId, { grade: r.grade, coefficient: coeff })
      }
    }

    const batch = {
      ...rawBatch,
      entries: rawBatch.entries.map((e: any) => {
        const perf = perfMap.get(e.employeeId)
        return {
          ...e,
          employee: e.employee ? { ...e.employee, idCardNumber: safeDecryptStr(e.employee.idCardNumber) } : e.employee,
          perfGrade: perf?.grade || null,
          perfCoefficient: perf?.coefficient || null,
        }
      }),
    }
    res.json({ success: true, data: batch })
  } catch (err) {
    next(err)
  }
})

// 重命名批次
router.put('/batches/:id/name', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { name } = req.body
    if (!name || typeof name !== 'string' || name.trim().length === 0) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '批次名称不能为空' } })
    }
    const batch = await prisma.payrollBatch.findFirst({
      where: { id: req.params.id, orgId: req.user!.orgId },
    })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已归档批次不可重命名' } })
    }
    const updated = await prisma.payrollBatch.update({
      where: { id: req.params.id },
      data: { name: name.trim() },
    })
    res.json({ success: true, data: { id: updated.id, name: updated.name } })
  } catch (err) {
    next(err)
  }
})

// 创建批次
const createBatchSchema = z.object({
  month: z.string().regex(/^\d{4}-\d{2}$/),
  payMonth: z.string().regex(/^\d{4}-\d{2}$/).optional(),
  type: z.enum(['REGULAR', 'TERMINATION', 'BONUS', 'SEVERANCE']).default('REGULAR'),
  mode: z.enum(['copy_last', 'blank_employees', 'blank_all', 'copy_batch', 'custom']).default('copy_last'),
  sourceBatchId: z.string().optional(),
  employeeIds: z.array(z.string()).optional(),
  name: z.string().optional(),
  remark: z.string().optional(),
})

router.post('/batches', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { month, payMonth, type, mode, sourceBatchId, employeeIds, name, remark } = createBatchSchema.parse(req.body)
    const orgId = req.user!.orgId

    // 查询当月最大批次号，避免删除后 count 不准导致唯一键冲突
    const lastBatch = await prisma.payrollBatch.findFirst({
      where: { orgId, month },
      orderBy: { batchNo: 'desc' },
      select: { batchNo: true },
    })
    const batchNo = (lastBatch?.batchNo || 0) + 1

    // 获取在职员工 + 本月离职员工
    const monthStart = new Date(`${month}-01`)
    const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 0, 23, 59, 59)

    // 获取上月发薪数据
    const prevMonth = new Date(monthStart.getFullYear(), monthStart.getMonth() - 1, 1)
    const prevMonthStr = `${prevMonth.getFullYear()}-${String(prevMonth.getMonth() + 1).padStart(2, '0')}`

    const batchName = name || `${month} 第${batchNo}批 ${type === 'BONUS' ? '奖金' : type === 'TERMINATION' ? '离职结算' : type === 'SEVERANCE' ? '补偿金' : '发薪'}`

    // 根据模式确定员工列表和数据来源
    let employees: any[] = []
    let sourceEntries: any[] | null = null

    if (mode === 'blank_all') {
      // 全空白：不拉入员工
      employees = []
    } else if (mode === 'custom' && employeeIds && employeeIds.length > 0) {
      // 自定义选择：仅包含指定员工
      employees = await prisma.employee.findMany({
        where: { id: { in: employeeIds }, orgId },
        include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
      })
    } else if (mode === 'copy_batch' && sourceBatchId) {
      // 复制指定批次：从源批次复制条目
      const sourceBatch = await prisma.payrollBatch.findFirst({
        where: { id: sourceBatchId, orgId, status: 'ARCHIVED' },
        include: { entries: true },
      })
      if (!sourceBatch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '源批次不存在或未归档' } })
      sourceEntries = sourceBatch.entries
      // 提取员工 ID，后续按此创建条目
      const employeeIds = sourceEntries.map(e => e.employeeId)
      employees = await prisma.employee.findMany({
        where: { id: { in: employeeIds }, orgId },
        include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
      })
    } else {
      // copy_last 或 blank_employees：拉入员工
      if (type === 'TERMINATION' || type === 'SEVERANCE') {
        const terminations = await prisma.terminationRecord.findMany({
          where: { orgId, terminationDate: { gte: monthStart, lte: monthEnd } },
          include: { employee: { include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } } },
        })
        // SEVERANCE 批次：仅包含已审批通过（APPROVED/EXECUTING/COMPLETED）且有补偿金的离职记录
        if (type === 'SEVERANCE') {
          const eligibleTerms = terminations.filter(t =>
            (t.status === 'APPROVED' || t.status === 'EXECUTING' || t.status === 'COMPLETED') &&
            (t.compensation > 0 || (t.compensationBreakdown as any)?.total > 0)
          )
          employees = eligibleTerms.map(t => t.employee)
          // 缓存 terminationRecord 以便后续 entry 创建时读取补偿金
          ;(req as any)._severanceTerms = new Map(eligibleTerms.map(t => [t.employeeId, t]))
        } else {
          employees = terminations.map(t => t.employee)
        }
      } else {
        // 预入职判定：用当前日期而非批次月末，避免预入职人员被拉入
        const today = new Date()
        today.setHours(0, 0, 0, 0)
        const todayEnd = new Date(today)
        todayEnd.setDate(todayEnd.getDate() + 1)
        employees = await prisma.employee.findMany({
          where: {
            orgId,
            OR: [
              // 在职且已入职（hireDate < 明天，排除预入职）
              { status: 'ACTIVE', hireDate: { lt: todayEnd } },
              // 本月离职的员工（离职当月仍需结算）
              { status: 'RESIGNED', updatedAt: { gte: monthStart, lte: monthEnd } },
            ],
          },
          include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
        })
      }
    }

    // 创建批次
    const batch = await prisma.payrollBatch.create({
      data: {
        orgId,
        month,
        payMonth: payMonth || null,
        batchNo,
        name: batchName,
        type,
        remark,
        createdBy: req.user!.id,
        employeeCount: employees.length,
      },
    })

    // 创建批次条目
    const entries: any[] = []
    const failedEmployees: { employeeId: string; name: string; error: string }[] = []
    for (const emp of employees) {
      let baseSalary = 0
      let performanceSalary = 0
      let overtimePay = 0
      let allowance = 0
      let deduction = 0
      let bonus = 0

      // 解密员工工资结构
      let empBaseSalary = 0
      let empPerformanceSalary = 0
      if (emp.baseSalary) {
        try { empBaseSalary = Number(decrypt(emp.baseSalary)) || 0 } catch { empBaseSalary = Number(emp.baseSalary) || 0 }
      }
      if (emp.performanceSalary) {
        try { empPerformanceSalary = Number(decrypt(emp.performanceSalary)) || 0 } catch { empPerformanceSalary = Number(emp.performanceSalary) || 0 }
      }

      if (mode === 'copy_batch' && sourceEntries) {
        // 复制指定批次：从源条目复制数据
        const srcEntry = sourceEntries.find(e => e.employeeId === emp.id)
        if (srcEntry) {
          baseSalary = srcEntry.baseSalary
          performanceSalary = srcEntry.performanceSalary || 0
          overtimePay = srcEntry.overtimePay
          allowance = srcEntry.allowance
          deduction = srcEntry.deduction
          bonus = srcEntry.bonus
        }
      } else if (mode === 'copy_last') {
        // 复制上月：从上月工资条复制
        const prevPayslip = await prisma.payslip.findUnique({
          where: { employeeId_month: { employeeId: emp.id, month: prevMonthStr } },
        })
        const overtime = await prisma.overtimeRecord.findUnique({
          where: { employeeId_month: { employeeId: emp.id, month } },
        })

        // 按批次月份判定是否仍在试用期（试用期结束日 = 合同开始日 + 试用期月数）
        // 试用期且 probationSalary > 0 → 用试用期工资；否则用转正工资
        const batchMonthEnd = new Date(`${month}-28T23:59:59`) // 月末近似
        const latestContract = emp.contracts?.[0]
        const inProbation = isInProbation(latestContract, batchMonthEnd)
        if (inProbation && latestContract.probationSalary > 0) {
          baseSalary = latestContract.probationSalary
          performanceSalary = 0 // 试用期一般不发绩效
        } else if (prevPayslip) {
          // 非试用期：优先用上月工资条的基本工资（保持薪资连续性）
          baseSalary = prevPayslip.baseSalary
          // 绩效工资不自动带出，需手动点"获取绩效工资"按钮
        } else if (empBaseSalary > 0 || empPerformanceSalary > 0) {
          // 新数据：有工资结构
          baseSalary = empBaseSalary
          // 绩效工资不自动带出，需手动点"获取绩效工资"按钮
        } else if (emp.monthlySalary) {
          // 旧数据兼容：回退到 monthlySalary
          try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
        }
        overtimePay = overtime?.totalPay || 0
        allowance = prevPayslip?.allowance || 0
        deduction = prevPayslip?.deduction || 0
      } else if (mode === 'blank_employees') {
        // 空白员工模式：拉入员工但所有金额默认0，需手动填写
      } else if (mode === 'custom') {
        // 自定义模式：取员工工资结构（非试用期）
        const batchMonthEnd = new Date(`${month}-28T23:59:59`)
        const latestContract = emp.contracts?.[0]
        const inProbation = isInProbation(latestContract, batchMonthEnd)
        if (inProbation && latestContract?.probationSalary > 0) {
          baseSalary = latestContract.probationSalary
          performanceSalary = 0
        } else if (empBaseSalary > 0 || empPerformanceSalary > 0) {
          baseSalary = empBaseSalary
          // 绩效工资不自动带出，需手动点"获取绩效工资"按钮
        } else if (emp.monthlySalary) {
          // custom 模式旧数据兼容
          try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
        }
      }
      // blank_all: 所有金额默认 0

      // 统一试用期判定（仅 copy_last / copy_batch / custom 模式适用，SEVERANCE 除外）
      // blank_employees / blank_all 模式下所有金额应为 0，不覆盖试用期工资
      if (type !== 'SEVERANCE' && type !== 'TERMINATION' && mode !== 'blank_employees' && mode !== 'blank_all') {
        const batchMonthEnd = new Date(`${month}-28T23:59:59`)
        const latestContract = emp.contracts?.[0]
        if (isInProbation(latestContract, batchMonthEnd) && latestContract?.probationSalary > 0) {
          baseSalary = latestContract.probationSalary
          performanceSalary = 0
        }
      }

      // SEVERANCE 批次：从离职记录读取补偿金作为应发金额，不走工资/社保/个税计算
      let severanceAmount = 0
      let severanceBreakdown: any = null
      if (type === 'SEVERANCE') {
        const severanceTerms: Map<string, any> = (req as any)._severanceTerms || new Map()
        const termRecord = severanceTerms.get(emp.id)
        if (termRecord) {
          severanceBreakdown = termRecord.compensationBreakdown
          // 优先取 compensationBreakdown.total（含手动调整），否则取 compensation
          severanceAmount = (severanceBreakdown as any)?.total || termRecord.compensation || 0
          baseSalary = severanceAmount
        }
      }

      // calcBatchEntry 内部会按员工检查当月已归档批次是否已扣社保，已扣则跳过
      // SEVERANCE 批次：补偿金不走社保/个税计算，直接作为应发和实发金额
      let calcResult: any
      if (type === 'SEVERANCE' && severanceAmount > 0) {
        calcResult = { socialEmp: 0, socialOrg: 0, housingEmp: 0, housingOrg: 0, tax: 0, totalPay: severanceAmount, netPay: severanceAmount, minWage: 0, minWageApplied: 0, deferredSocialEmp: 0, deferredHousingEmp: 0, deferredMinWage: 0, prevDeferredSocialEmp: 0, prevDeferredHousingEmp: 0, prevDeferredMinWage: 0 }
      } else {
        try {
          // 查询上月递延数据（上月已归档批次中该员工的递延金额）
          const prevDeferred = await getPrevDeferred(orgId, emp.id, month)
          calcResult = await calcBatchEntry(orgId, emp.id, month, { baseSalary, performanceSalary, overtimePay, allowance, deduction, bonus }, type, { prevDeferred })
        } catch (calcErr: any) {
          // 单个员工计算失败不阻塞整个批次，记录错误并使用零值
          failedEmployees.push({ employeeId: emp.id, name: emp.name, error: calcErr?.message || '计算失败' })
          calcResult = { socialEmp: 0, socialOrg: 0, housingEmp: 0, housingOrg: 0, tax: 0, totalPay: baseSalary + overtimePay + allowance + bonus - deduction, netPay: baseSalary + overtimePay + allowance + bonus - deduction, minWage: 0, minWageApplied: 0, deferredSocialEmp: 0, deferredHousingEmp: 0, deferredMinWage: 0, prevDeferredSocialEmp: 0, prevDeferredHousingEmp: 0, prevDeferredMinWage: 0 }
        }
      }

      // 风险提示
      const riskWarnings = await getPayrollRiskWarnings(orgId, emp.id)

      const entry = await prisma.batchEntry.create({
        data: {
          batchId: batch.id,
          orgId,
          employeeId: emp.id,
          baseSalary,
          performanceSalary,
          overtimePay,
          allowance,
          deduction,
          bonus,
          socialEmp: calcResult.socialEmp,
          socialOrg: calcResult.socialOrg,
          housingEmp: calcResult.housingEmp,
          housingOrg: calcResult.housingOrg,
          supplementaryHousingEmp: calcResult.supplementaryHousingEmp || 0,
          supplementaryHousingOrg: calcResult.supplementaryHousingOrg || 0,
          tax: calcResult.tax,
          totalPay: calcResult.totalPay,
          netPay: calcResult.netPay,
          // 最低工资保护 + 递延扣款
          minWage: calcResult.minWage || 0,
          minWageApplied: calcResult.minWageApplied || 0,
          deferredSocialEmp: calcResult.deferredSocialEmp || 0,
          deferredHousingEmp: calcResult.deferredHousingEmp || 0,
          deferredMinWage: calcResult.deferredMinWage || 0,
          prevDeferredSocialEmp: calcResult.prevDeferredSocialEmp || 0,
          prevDeferredHousingEmp: calcResult.prevDeferredHousingEmp || 0,
          prevDeferredMinWage: calcResult.prevDeferredMinWage || 0,
          riskWarnings,
          extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined,
        },
      })
      entries.push(entry)
    }

    // 更新批次汇总
    const totals = entries.reduce((acc, e) => ({
      totalPay: acc.totalPay + e.totalPay,
      totalNetPay: acc.totalNetPay + e.netPay,
      totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
      totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
      totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
      totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
      totalTax: acc.totalTax + e.tax,
    }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })

    const updatedBatch = await prisma.payrollBatch.update({
      where: { id: batch.id },
      data: {
        totalPay: Math.round(totals.totalPay * 100) / 100,
        totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
        totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
        totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
        totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
        totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
        totalTax: Math.round(totals.totalTax * 100) / 100,
      },
      include: { entries: { include: { employee: { select: { id: true, name: true, department: true, status: true } } } } },
    })

    res.json({ success: true, data: updatedBatch, failedEmployees: failedEmployees.length > 0 ? failedEmployees : undefined })
  } catch (err) {
    next(err)
  }
})

// 编辑批次条目（计算依据项 + 社保公积金手动覆盖 + 自定义模板项）
// 预置字段通过固定 schema 校验，自定义字段通过模板 isEditable 动态校验
const updateEntrySchema = z.object({
  baseSalary: z.number().optional(),
  performanceSalary: z.number().optional(),
  overtimePay: z.number().optional(),
  allowance: z.number().optional(),
  deduction: z.number().optional(),
  bonus: z.number().optional(),
  socialEmp: z.number().optional(),
  socialOrg: z.number().optional(),
  housingEmp: z.number().optional(),
  housingOrg: z.number().optional(),
  positionSalary: z.number().optional(),
  senioritySalary: z.number().optional(),
  transportAllowance: z.number().optional(),
  mealAllowance: z.number().optional(),
  housingAllowance: z.number().optional(),
  communicationAllowance: z.number().optional(),
  otherDeduction: z.number().optional(),
  // 自定义字段通过 extraItems 传递
  extraItems: z.record(z.string(), z.number()).optional(),
}).passthrough()

router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId, employeeId } = req.params
    const data = updateEntrySchema.parse(req.body)
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已归档批次不可编辑' } })

    const entry = await prisma.batchEntry.findUnique({
      where: { batchId_employeeId: { batchId, employeeId } },
    })
    if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } })

    // 获取模板，确定哪些自定义字段可编辑
    const templateItems = await getTemplate(orgId)
    const presetCodes = new Set(['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'socialOrg', 'housingEmp', 'housingOrg', 'positionSalary', 'senioritySalary', 'transportAllowance', 'mealAllowance', 'housingAllowance', 'communicationAllowance', 'otherDeduction'])
    const customEditableCodes = new Set(templateItems.filter(i => i.isEditable && !presetCodes.has(i.code)).map(i => i.code))

    // 合并 extraItems：已有值 + 新传入的可编辑自定义字段
    const existingExtraItems = (entry.extraItems as Record<string, number>) || {}
    const newExtraItems = { ...existingExtraItems }
    if (data.extraItems) {
      for (const [code, val] of Object.entries(data.extraItems)) {
        if (customEditableCodes.has(code)) {
          newExtraItems[code] = val
        }
      }
    }

    // 合并输入项（包含细化薪资字段，避免编辑时丢失岗位工资/绩效工资等）
    const inputs = {
      baseSalary: data.baseSalary ?? entry.baseSalary,
      overtimePay: data.overtimePay ?? entry.overtimePay,
      allowance: data.allowance ?? entry.allowance,
      deduction: data.deduction ?? entry.deduction,
      bonus: data.bonus ?? entry.bonus,
      positionSalary: data.positionSalary ?? entry.positionSalary ?? undefined,
      performanceSalary: data.performanceSalary ?? entry.performanceSalary ?? undefined,
      senioritySalary: data.senioritySalary ?? entry.senioritySalary ?? undefined,
      transportAllowance: data.transportAllowance ?? (entry.transportAllowance || undefined),
      mealAllowance: data.mealAllowance ?? (entry.mealAllowance || undefined),
      housingAllowance: data.housingAllowance ?? (entry.housingAllowance || undefined),
      communicationAllowance: data.communicationAllowance ?? (entry.communicationAllowance || undefined),
      otherDeduction: data.otherDeduction ?? (entry.otherDeduction || undefined),
    }

    // 构建社保覆盖参数（如果请求中包含社保字段）
    const overrideSocial: any = {}
    if (data.socialEmp !== undefined) overrideSocial.socialEmp = data.socialEmp
    if (data.socialOrg !== undefined) overrideSocial.socialOrg = data.socialOrg
    if (data.housingEmp !== undefined) overrideSocial.housingEmp = data.housingEmp
    if (data.housingOrg !== undefined) overrideSocial.housingOrg = data.housingOrg

    // 查询上月递延数据（与创建批次时一致）
    const prevDeferred = await getPrevDeferred(orgId, employeeId, batch.month)

    // calcBatchEntry 内部会按员工检查当月已归档批次是否已扣社保，手动覆盖优先
    const options: any = { prevDeferred }
    if (Object.keys(overrideSocial).length > 0) options.overrideSocial = overrideSocial

    // 重新计算（传入 extraItems 供自定义 INPUT 项取值）
    const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type, options, newExtraItems)

    const { systemSocialEmp, systemSocialOrg, systemHousingEmp, systemHousingOrg, systemSupplementaryHousingEmp, systemSupplementaryHousingOrg, taxBreakdown, extraItems: calcExtraItems, ...entryData } = calcResult
    // 合并计算结果中的 extraItems（包含自定义计算项的值）
    const finalExtraItems = { ...newExtraItems, ...(calcExtraItems || {}) }
    const updated = await prisma.batchEntry.update({
      where: { id: entry.id },
      data: { ...inputs, ...entryData, extraItems: Object.keys(finalExtraItems).length > 0 ? finalExtraItems : undefined },
    })

    // 更新批次汇总
    const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
    const totals = allEntries.reduce((acc, e) => ({
      totalPay: acc.totalPay + (e.id === entry.id ? calcResult.totalPay : e.totalPay),
      totalNetPay: acc.totalNetPay + (e.id === entry.id ? calcResult.netPay : e.netPay),
      totalSocialOrg: acc.totalSocialOrg + (e.id === entry.id ? calcResult.socialOrg : e.socialOrg),
      totalSocialEmp: acc.totalSocialEmp + (e.id === entry.id ? calcResult.socialEmp : e.socialEmp),
      totalHousingOrg: acc.totalHousingOrg + (e.id === entry.id ? calcResult.housingOrg : e.housingOrg),
      totalHousingEmp: acc.totalHousingEmp + (e.id === entry.id ? calcResult.housingEmp : e.housingEmp),
      totalTax: acc.totalTax + (e.id === entry.id ? calcResult.tax : e.tax),
    }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })

    await prisma.payrollBatch.update({
      where: { id: batchId },
      data: {
        totalPay: Math.round(totals.totalPay * 100) / 100,
        totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
        totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
        totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
        totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
        totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
        totalTax: Math.round(totals.totalTax * 100) / 100,
      },
    })

    res.json({ success: true, data: updated, taxBreakdown: calcResult.taxBreakdown })
  } catch (err) {
    next(err)
  }
})

// 获取提成奖金：按批次月份从 CommissionBonus 表拉取，填充到 entries.bonus
router.post('/batches/:batchId/fetch-bonus', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const force = (req.body as any)?.force === true

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })

    // 获取批次所有条目
    const entries = await prisma.batchEntry.findMany({
      where: { batchId },
      select: { id: true, employeeId: true, bonus: true },
    })

    if (entries.length === 0) {
      return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
    }

    // 按批次月份查询提成奖金
    const bonusMap = await getBonusByMonthAndEmployeeIds(orgId, batch.month, entries.map((e) => e.employeeId))

    // 检查跨批次重复（非 force 模式）
    const toFillEmployeeIds = new Set(entries.filter(e => bonusMap.has(e.employeeId)).map(e => e.employeeId))
    if (!force && toFillEmployeeIds.size > 0) {
      const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'bonus')
      const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
      if (overlap.length > 0) {
        const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
        return res.json({
          success: true,
          data: {
            needConfirm: true,
            duplicateCount: overlap.length,
            duplicateNames: overlapNames.map(e => e.name),
            message: `${overlap.length} 人在其他批次中已有提成奖金（${overlapNames.map(e => e.name).join('、')}），确认是否再次获取？`,
          },
        })
      }
    }

    let filled = 0
    let totalAmount = 0
    for (const entry of entries) {
      const bonus = bonusMap.get(entry.employeeId)
      if (bonus) {
        await prisma.batchEntry.update({
          where: { id: entry.id },
          data: { bonus: bonus.amount },
        })
        await recalcEntry(orgId, batchId, batch.month, batch.type, entry.id, entry.employeeId)
        filled++
        totalAmount += bonus.amount
      }
    }

    // 重算批次汇总
    await recalcBatchTotals(batchId)

    res.json({
      success: true,
      data: {
        filled,
        totalAmount: Math.round(totalAmount * 100) / 100,
        message: filled > 0 ? `已填充 ${filled} 人提成奖金，合计 ¥${Math.round(totalAmount * 100) / 100}` : `${batch.month} 无提成奖金数据`,
      },
    })
  } catch (err) {
    next(err)
  }
})

// 获取绩效工资：按批次月份从 PerformanceRecord 查考核等级，结合 Employee.performanceSalary 计算绩效工资
// 绩效系数：A=1.2, B=1.0, C=0.8, D=0.6；无绩效记录则系数=1.0
router.post('/batches/:batchId/fetch-performance', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const force = (req.body as any)?.force === true

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })

    // 获取批次所有条目
    const entries = await prisma.batchEntry.findMany({
      where: { batchId },
      select: { id: true, employeeId: true, performanceSalary: true },
    })

    if (entries.length === 0) {
      return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
    }

    // 查询批次月份的绩效记录（按 period = YYYY-MM 匹配）
    const performanceRecords = await prisma.performanceRecord.findMany({
      where: { orgId, period: batch.month, periodType: 'MONTHLY' },
      select: { employeeId: true, grade: true, score: true, result: true, coefficient: true, templateId: true },
    })
    // 查询关联的绩效模板（用于回退取 gradeRules 中的 coefficient）
    const templateIds = [...new Set(performanceRecords.map(r => r.templateId).filter(Boolean))] as string[]
    const templates = templateIds.length > 0 ? await prisma.performanceTemplate.findMany({
      where: { id: { in: templateIds } },
      select: { id: true, gradeRules: true },
    }) : []
    const templateCoeffMap = new Map<string, Record<string, number>>()
    for (const t of templates) {
      const coeffs: Record<string, number> = {}
      if (t.gradeRules && Array.isArray(t.gradeRules)) {
        for (const rule of t.gradeRules as any[]) {
          if (rule.grade && rule.coefficient != null) coeffs[rule.grade] = Number(rule.coefficient)
        }
      }
      templateCoeffMap.set(t.id, coeffs)
    }
    const perfMap = new Map<string, { grade: string; score: number; coefficient: number | null }>()
    for (const r of performanceRecords) {
      // 同一员工同月可能有多条记录，取最新一条（按创建时间倒序已由 DB 保证）
      if (!perfMap.has(r.employeeId)) {
        perfMap.set(r.employeeId, { grade: r.grade, score: r.score, coefficient: r.coefficient })
      }
    }

    // 查询员工绩效工资基数
    const employees = await prisma.employee.findMany({
      where: { id: { in: entries.map(e => e.employeeId) } },
      select: { id: true, performanceSalary: true },
    })
    const empPerfSalaryMap = new Map<string, number>()
    for (const emp of employees) {
      if (emp.performanceSalary) {
        try { empPerfSalaryMap.set(emp.id, Number(decrypt(emp.performanceSalary)) || 0) } catch {}
      }
    }

    // 检查跨批次重复（非 force 模式）
    const toFillEmployeeIds = new Set(entries.filter(e => (empPerfSalaryMap.get(e.employeeId) || 0) > 0).map(e => e.employeeId))
    if (!force && toFillEmployeeIds.size > 0) {
      const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'performanceSalary')
      const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
      if (overlap.length > 0) {
        const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
        return res.json({
          success: true,
          data: {
            needConfirm: true,
            duplicateCount: overlap.length,
            duplicateNames: overlapNames.map(e => e.name),
            message: `${overlap.length} 人在其他批次中已有绩效工资（${overlapNames.map(e => e.name).join('、')}），确认是否再次获取？`,
          },
        })
      }
    }

    // 绩效系数：优先用记录中保存的 coefficient，回退到组织缺省配置，再回退到硬编码
    const orgForCoeff = await prisma.organization.findUnique({ where: { id: orgId }, select: { defaultGradeCoefficients: true } })
    const DEFAULT_GRADE_COEFFICIENTS: Record<string, number> = (orgForCoeff?.defaultGradeCoefficients as Record<string, number>) || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }

    let filled = 0
    let totalAmount = 0
    const details: { employeeId: string; grade: string | null; coefficient: number; performanceSalary: number }[] = []
    for (const entry of entries) {
      const basePerfSalary = empPerfSalaryMap.get(entry.employeeId) || 0
      if (basePerfSalary <= 0) continue // 绩效工资基数为0则跳过

      const perfRecord = perfMap.get(entry.employeeId)
      const grade = perfRecord?.grade || null
      // 系数优先级：记录中保存的 coefficient > 模板 gradeRules 中的 coefficient > 系统默认值
      let coefficient = 1.0
      if (perfRecord) {
        if (perfRecord.coefficient != null) {
          coefficient = perfRecord.coefficient
        } else {
          // 查绩效记录关联的模板
          const perfRecWithTemplate = performanceRecords.find(r => r.employeeId === entry.employeeId && r.templateId)
          const templateCoeffs = perfRecWithTemplate?.templateId ? templateCoeffMap.get(perfRecWithTemplate.templateId) : null
          coefficient = (templateCoeffs && perfRecord.grade in templateCoeffs) ? templateCoeffs[perfRecord.grade] : (DEFAULT_GRADE_COEFFICIENTS[perfRecord.grade] || 1.0)
        }
      }
      const calculatedPerfSalary = Math.round(basePerfSalary * coefficient * 100) / 100

      await prisma.batchEntry.update({
        where: { id: entry.id },
        data: { performanceSalary: calculatedPerfSalary },
      })
      await recalcEntry(orgId, batchId, batch.month, batch.type, entry.id, entry.employeeId)
      details.push({ employeeId: entry.employeeId, grade, coefficient, performanceSalary: calculatedPerfSalary })
      filled++
      totalAmount += calculatedPerfSalary
    }

    // 重算批次汇总
    await recalcBatchTotals(batchId)

    res.json({
      success: true,
      data: {
        filled,
        totalAmount: Math.round(totalAmount * 100) / 100,
        details,
        message: filled > 0
          ? `已填充 ${filled} 人绩效工资，合计 ¥${Math.round(totalAmount * 100) / 100}${performanceRecords.length > 0 ? `（${performanceRecords.length} 人有考核记录）` : '（无考核记录，按系数1.0计算）'}`
          : '无绩效工资数据（员工未设置绩效工资基数）',
      },
    })
  } catch (err) {
    next(err)
  }
})

// 获取违纪扣款：按批次月份从 DisciplinaryRecord 查 action=DEDUCTION 的记录，汇总扣款金额填充到 entries.deduction
router.post('/batches/:batchId/fetch-disciplinary', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const force = (req.body as any)?.force === true

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })

    // 获取批次所有条目
    const entries = await prisma.batchEntry.findMany({
      where: { batchId },
      select: { id: true, employeeId: true, deduction: true },
    })

    if (entries.length === 0) {
      return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
    }

    // 查询批次月份内的违纪扣款记录（按 violationDate 落在批次月份内）
    const monthStart = new Date(batch.month + '-01T00:00:00.000Z')
    const monthEnd = new Date(monthStart)
    monthEnd.setUTCMonth(monthEnd.getUTCMonth() + 1)

    const disciplinaryRecords = await prisma.disciplinaryRecord.findMany({
      where: {
        orgId,
        employeeId: { in: entries.map(e => e.employeeId) },
        action: 'DEDUCTION',
        deductionAmount: { gt: 0 },
        violationDate: { gte: monthStart, lt: monthEnd },
      },
      select: { employeeId: true, deductionAmount: true },
    })
    console.log(`[fetch-disciplinary] batch.month=${batch.month} monthStart=${monthStart.toISOString()} monthEnd=${monthEnd.toISOString()} entries=${entries.length} records=${disciplinaryRecords.length}`)

    // 按员工汇总扣款金额
    const deductionMap = new Map<string, number>()
    for (const r of disciplinaryRecords) {
      deductionMap.set(r.employeeId, (deductionMap.get(r.employeeId) || 0) + r.deductionAmount)
    }

    // 检查跨批次重复（非 force 模式）
    const toFillEmployeeIds = new Set([...deductionMap.keys()])
    if (!force && toFillEmployeeIds.size > 0) {
      const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'deduction')
      const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
      if (overlap.length > 0) {
        const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
        return res.json({
          success: true,
          data: {
            needConfirm: true,
            duplicateCount: overlap.length,
            duplicateNames: overlapNames.map(e => e.name),
            message: `${overlap.length} 人在其他批次中已有扣款（${overlapNames.map(e => e.name).join('、')}），确认是否再次获取？`,
          },
        })
      }
    }

    let filled = 0
    let totalAmount = 0
    for (const entry of entries) {
      const deduction = deductionMap.get(entry.employeeId)
      if (deduction && deduction > 0) {
        await prisma.batchEntry.update({
          where: { id: entry.id },
          data: { deduction: Math.round(deduction * 100) / 100 },
        })
        await recalcEntry(orgId, batchId, batch.month, batch.type, entry.id, entry.employeeId)
        filled++
        totalAmount += deduction
      }
    }

    // 重算批次汇总
    await recalcBatchTotals(batchId)

    res.json({
      success: true,
      data: {
        filled,
        totalAmount: Math.round(totalAmount * 100) / 100,
        message: filled > 0
          ? `已填充 ${filled} 人违纪扣款，合计 ¥${Math.round(totalAmount * 100) / 100}`
          : `${batch.month} 无违纪扣款数据`,
      },
    })
  } catch (err) {
    next(err)
  }
})

// 获取考勤扣款：按批次月份从 AttendanceConfirmation 查 deductionAmount > 0 的记录，填充到 entries.deduction
router.post('/batches/:batchId/fetch-attendance-deduction', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const force = (req.body as any)?.force === true

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '已归档批次不可操作' } })

    // 获取批次所有条目
    const entries = await prisma.batchEntry.findMany({
      where: { batchId },
      select: { id: true, employeeId: true, deduction: true },
    })

    if (entries.length === 0) {
      return res.json({ success: true, data: { filled: 0, totalAmount: 0, message: '批次无员工条目' } })
    }

    // 查询批次月份的考勤确认记录
    const confirmations = await prisma.attendanceConfirmation.findMany({
      where: {
        orgId,
        employeeId: { in: entries.map(e => e.employeeId) },
        month: batch.month,
        deductionAmount: { gt: 0 },
      },
      select: { employeeId: true, deductionAmount: true },
    })

    const deductionMap = new Map<string, number>()
    for (const c of confirmations) {
      deductionMap.set(c.employeeId, c.deductionAmount)
    }

    // 检查跨批次重复（非 force 模式）
    const toFillEmployeeIds = new Set([...deductionMap.keys()])
    if (!force && toFillEmployeeIds.size > 0) {
      const dupIds = await checkOtherBatches(orgId, batchId, batch.month, 'deduction')
      const overlap = [...toFillEmployeeIds].filter(id => dupIds.has(id))
      if (overlap.length > 0) {
        const overlapNames = await prisma.employee.findMany({ where: { id: { in: overlap } }, select: { name: true } })
        return res.json({
          success: true,
          data: {
            needConfirm: true,
            duplicateCount: overlap.length,
            duplicateNames: overlapNames.map(e => e.name),
            message: `${overlap.length} 人在其他批次中已有扣款（${overlapNames.map(e => e.name).join('、')}），确认是否再次获取？`,
          },
        })
      }
    }

    let filled = 0
    let totalAmount = 0
    for (const entry of entries) {
      const deduction = deductionMap.get(entry.employeeId)
      if (deduction && deduction > 0) {
        await prisma.batchEntry.update({
          where: { id: entry.id },
          data: { deduction: Math.round(deduction * 100) / 100 },
        })
        await recalcEntry(orgId, batchId, batch.month, batch.type, entry.id, entry.employeeId)
        filled++
        totalAmount += deduction
      }
    }

    // 重算批次汇总
    await recalcBatchTotals(batchId)

    res.json({
      success: true,
      data: {
        filled,
        totalAmount: Math.round(totalAmount * 100) / 100,
        message: filled > 0
          ? `已填充 ${filled} 人考勤扣款，合计 ¥${Math.round(totalAmount * 100) / 100}`
          : `${batch.month} 无考勤扣款数据（需先在考勤确认中填写扣款金额）`,
      },
    })
  } catch (err) {
    next(err)
  }
})

// 获取条目个税计算明细
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId, employeeId } = req.params
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })

    const entry = await prisma.batchEntry.findUnique({
      where: { batchId_employeeId: { batchId, employeeId } },
    })
    if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } })

    const inputs = {
      baseSalary: entry.baseSalary,
      overtimePay: entry.overtimePay,
      allowance: entry.allowance,
      deduction: entry.deduction,
      bonus: entry.bonus,
    }
    const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type)
    const taxBreakdown = calcResult.taxBreakdown || {}
    // 加入社保公积金系统计算值 vs 实际值对比
    taxBreakdown.systemSocialEmp = calcResult.systemSocialEmp
    taxBreakdown.systemSocialOrg = calcResult.systemSocialOrg
    taxBreakdown.systemHousingEmp = calcResult.systemHousingEmp
    taxBreakdown.systemHousingOrg = calcResult.systemHousingOrg
    taxBreakdown.systemSupplementaryHousingEmp = calcResult.systemSupplementaryHousingEmp
    taxBreakdown.systemSupplementaryHousingOrg = calcResult.systemSupplementaryHousingOrg
    taxBreakdown.actualSocialEmp = entry.socialEmp
    taxBreakdown.actualSocialOrg = entry.socialOrg
    taxBreakdown.actualHousingEmp = entry.housingEmp
    taxBreakdown.actualHousingOrg = entry.housingOrg
    taxBreakdown.actualSupplementaryHousingEmp = entry.supplementaryHousingEmp || 0
    taxBreakdown.actualSupplementaryHousingOrg = entry.supplementaryHousingOrg || 0
    res.json({ success: true, data: taxBreakdown })
  } catch (err) {
    next(err)
  }
})

// 批次增加人员
router.post('/batches/:batchId/employees', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const { employeeIds } = req.body as { employeeIds: string[] }
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已归档批次不可编辑' } })

    const results: any[] = []
    for (const employeeId of employeeIds) {
      // 检查是否已在批次中
      const existing = await prisma.batchEntry.findUnique({
        where: { batchId_employeeId: { batchId, employeeId } },
      })
      if (existing) continue

      const emp = await prisma.employee.findFirst({
        where: { id: employeeId, orgId },
        include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
      })
      if (!emp) continue

      let baseSalary = 0
      // 按批次月份判定是否仍在试用期
      const batchMonthEnd = new Date(`${batch.month}-28T23:59:59`)
      const latestContract = emp.contracts?.[0]
      if (isInProbation(latestContract, batchMonthEnd) && latestContract.probationSalary > 0) {
        baseSalary = latestContract.probationSalary
      } else if (emp.monthlySalary) {
        try { baseSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { baseSalary = Number(emp.monthlySalary) || 0 }
      }

      const overtime = await prisma.overtimeRecord.findUnique({
        where: { employeeId_month: { employeeId, month: batch.month } },
      })
      const overtimePay = overtime?.totalPay || 0

      // calcBatchEntry 内部会按员工检查当月已归档批次是否已扣社保，已扣则跳过
      const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, { baseSalary, overtimePay, allowance: 0, deduction: 0, bonus: 0 }, batch.type)
      const riskWarnings = await getPayrollRiskWarnings(orgId, employeeId)

      const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult
      const entry = await prisma.batchEntry.create({
        data: {
          batchId, orgId, employeeId,
          baseSalary, overtimePay, allowance: 0, deduction: 0, bonus: 0,
          ...entryData, riskWarnings,
          extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined,
        },
      })
      results.push(entry)
    }

    // 更新批次人数和汇总
    const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
    const totals = allEntries.reduce((acc, e) => ({
      totalPay: acc.totalPay + e.totalPay,
      totalNetPay: acc.totalNetPay + e.netPay,
      totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
      totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
      totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
      totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
      totalTax: acc.totalTax + e.tax,
    }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
    await prisma.payrollBatch.update({
      where: { id: batchId },
      data: {
        employeeCount: allEntries.length,
        totalPay: Math.round(totals.totalPay * 100) / 100,
        totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
        totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
        totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
        totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
        totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
        totalTax: Math.round(totals.totalTax * 100) / 100,
      },
    })

    res.json({ success: true, data: { added: results.length } })
  } catch (err) {
    next(err)
  }
})

// 批次移除人员
router.delete('/batches/:batchId/employees/:employeeId', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId, employeeId } = req.params
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已归档批次不可编辑' } })

    await prisma.batchEntry.deleteMany({ where: { batchId, employeeId } })

    // 更新批次人数和汇总
    const allEntries = await prisma.batchEntry.findMany({ where: { batchId } })
    const totals = allEntries.reduce((acc, e) => ({
      totalPay: acc.totalPay + e.totalPay,
      totalNetPay: acc.totalNetPay + e.netPay,
      totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
      totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
      totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
      totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
      totalTax: acc.totalTax + e.tax,
    }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })
    await prisma.payrollBatch.update({
      where: { id: batchId },
      data: {
        employeeCount: allEntries.length,
        totalPay: Math.round(totals.totalPay * 100) / 100,
        totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
        totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
        totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
        totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
        totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
        totalTax: Math.round(totals.totalTax * 100) / 100,
      },
    })

    res.json({ success: true })
  } catch (err) {
    next(err)
  }
})

// 删除批次（仅限草稿状态）
router.delete('/batches/:batchId', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已归档批次不可删除' } })

    await prisma.batchEntry.deleteMany({ where: { batchId } })
    await prisma.payrollBatch.delete({ where: { id: batchId } })

    res.json({ success: true })
  } catch (err) {
    next(err)
  }
})

// 归档批次（归档前重算所有条目，确保累计个税/社保包含先前已归档批次的数据）
router.post('/batches/:batchId/archive', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status === 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '批次已归档' } })

    // 1. 重算本批次所有条目（此时 calcBatchEntry 会包含所有已归档批次的累计数据）
    const entries = await prisma.batchEntry.findMany({ where: { batchId } })
    const recalcErrors: string[] = []
    for (const entry of entries) {
      try {
        const inputs = {
          baseSalary: entry.baseSalary,
          overtimePay: entry.overtimePay,
          allowance: entry.allowance,
          deduction: entry.deduction,
          bonus: entry.bonus,
          positionSalary: entry.positionSalary || undefined,
          performanceSalary: entry.performanceSalary || undefined,
          senioritySalary: entry.senioritySalary || undefined,
          transportAllowance: entry.transportAllowance || undefined,
          mealAllowance: entry.mealAllowance || undefined,
          housingAllowance: entry.housingAllowance || undefined,
          communicationAllowance: entry.communicationAllowance || undefined,
          otherDeduction: entry.otherDeduction || undefined,
        }
        // 社保如被手动覆盖，保留覆盖值
        // 社保如被手动覆盖，保留覆盖值（通过比较系统值和实际值判断是否覆盖过）
        const overrideSocial: any = {}
        // 归档重算时不强制覆盖社保，让系统重新计算（除非之前有手动覆盖）
        // 这里简化处理：不传 overrideSocial，让系统重算
        const prevDeferred = await getPrevDeferred(orgId, entry.employeeId, batch.month)
        const options: any = { prevDeferred }

        const calcResult = await calcBatchEntry(orgId, entry.employeeId, batch.month, inputs, batch.type, options, (entry.extraItems as Record<string, number>) || null)
        const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, systemSupplementaryHousingEmp: _sshe, systemSupplementaryHousingOrg: _ssho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult
        await prisma.batchEntry.update({
          where: { id: entry.id },
          data: { ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined },
        })
      } catch (e: any) {
        recalcErrors.push(`${entry.employeeId}: ${e?.message || '重算失败'}`)
      }
    }

    // 2. 更新批次汇总
    const recalcedEntries = await prisma.batchEntry.findMany({ where: { batchId } })
    const totals = recalcedEntries.reduce((acc, e) => ({
      totalPay: acc.totalPay + e.totalPay,
      totalNetPay: acc.totalNetPay + e.netPay,
      totalSocialOrg: acc.totalSocialOrg + e.socialOrg,
      totalSocialEmp: acc.totalSocialEmp + e.socialEmp,
      totalHousingOrg: acc.totalHousingOrg + e.housingOrg,
      totalHousingEmp: acc.totalHousingEmp + e.housingEmp,
      totalTax: acc.totalTax + e.tax,
    }), { totalPay: 0, totalNetPay: 0, totalSocialOrg: 0, totalSocialEmp: 0, totalHousingOrg: 0, totalHousingEmp: 0, totalTax: 0 })

    // 3. 标记为已归档
    await prisma.payrollBatch.update({
      where: { id: batchId },
      data: {
        status: 'ARCHIVED',
        archivedAt: new Date(),
        totalPay: Math.round(totals.totalPay * 100) / 100,
        totalNetPay: Math.round(totals.totalNetPay * 100) / 100,
        totalSocialOrg: Math.round(totals.totalSocialOrg * 100) / 100,
        totalSocialEmp: Math.round(totals.totalSocialEmp * 100) / 100,
        totalHousingOrg: Math.round(totals.totalHousingOrg * 100) / 100,
        totalHousingEmp: Math.round(totals.totalHousingEmp * 100) / 100,
        totalTax: Math.round(totals.totalTax * 100) / 100,
      },
    })

    res.json({ success: true, data: { archived: true, recalculated: entries.length, errors: recalcErrors.length > 0 ? recalcErrors : undefined } })
  } catch (err) {
    next(err)
  }
})

// 取消归档（只能取消最后一个归档批次，依次取消）
router.post('/batches/:batchId/unarchive', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId

    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status !== 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '仅已归档批次可取消归档' } })

    // 检查是否是最后一个归档批次（按 batchNo 倒序）
    const lastArchived = await prisma.payrollBatch.findFirst({
      where: { orgId, month: batch.month, status: 'ARCHIVED' },
      orderBy: { batchNo: 'desc' },
      select: { id: true },
    })
    if (lastArchived?.id !== batchId) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '只能依次取消归档，请先取消最后归档的批次' } })
    }

    // 取消归档
    await prisma.payrollBatch.update({
      where: { id: batchId },
      data: { status: 'DRAFT', archivedAt: null },
    })

    // 如果当月已无归档批次，删除对应工资条（取消个税累计）
    const remainingArchived = await prisma.payrollBatch.count({
      where: { orgId, month: batch.month, status: 'ARCHIVED' },
    })
    if (remainingArchived === 0) {
      // 查找该月所有批次的员工，删除其工资条
      const entries = await prisma.batchEntry.findMany({
        where: { orgId, batch: { month: batch.month } },
        select: { employeeId: true },
        distinct: ['employeeId'],
      })
      if (entries.length > 0) {
        await prisma.payslip.deleteMany({
          where: {
            orgId,
            employeeId: { in: entries.map(e => e.employeeId) },
            month: batch.month,
          },
        })
      }
    } else {
      // 还有归档批次，重新生成工资条（基于剩余归档批次汇总）
      await generatePayslipFromBatches(orgId, batch.month)
    }

    res.json({ success: true, data: { unarchived: true } })
  } catch (err) {
    next(err)
  }
})

// 从已归档批次汇总生成工资条
router.post('/payslips/generate', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { month } = req.body
    const orgId = req.user!.orgId

    if (!month || !/^\d{4}-\d{2}$/.test(month)) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请提供有效的月份（YYYY-MM）' } })
    }

    // 检查是否有已归档批次
    const archivedBatches = await prisma.payrollBatch.count({
      where: { orgId, month, status: 'ARCHIVED' },
    })
    if (archivedBatches === 0) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '当月无已归档批次，无法生成工资条' } })
    }

    const result = await generatePayslipFromBatches(orgId, month)

    // 自动标记"生成工资条"待办为已完成
    await prisma.riskItem.updateMany({
      where: { orgId, status: 'PENDING', type: 'SALARY', title: { startsWith: `${month}月 生成工资条` } },
      data: { status: 'RESOLVED', resolvedAt: new Date(), resolvedBy: req.user!.id },
    })

    res.json({ success: true, data: { generated: result.generated } })
  } catch (err) {
    next(err)
  }
})

// 银行代发文件导出（接口预留）
router.get('/batches/:batchId/export', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const { format = 'csv' } = req.query

    const batch = await prisma.payrollBatch.findFirst({
      where: { id: batchId, orgId },
      include: {
        entries: {
          include: { employee: { select: { name: true, bankAccount: true, bankName: true } } },
        },
      },
    })
    if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    if (batch.status !== 'ARCHIVED') return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '仅归档批次可导出' } })

    if (format === 'csv') {
      const header = '姓名,银行账号,开户行,实发金额\n'
      const rows = batch.entries.map(e => `${e.employee.name},${e.employee.bankAccount || ''},${e.employee.bankName || ''},${e.netPay}`).join('\n')
      res.setHeader('Content-Type', 'text/csv; charset=utf-8')
      res.setHeader('Content-Disposition', contentDisposition(`银行代发文件-${batch.month}-批次${batch.batchNo}.csv`))
      return res.send('\ufeff' + header + rows)
    }

    res.json({ success: true, data: batch })
  } catch (err) {
    next(err)
  }
})

// 算薪前 AI 校验
router.get('/batches/:batchId/pre-check', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const result = await prePayrollCheck(req.user!.orgId, req.params.batchId)
    res.json({ success: true, data: result })
  } catch (err: any) {
    if (err?.code === 'NOT_FOUND') {
      return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
    }
    next(err)
  }
})

// ========== 工资条发布 ==========

// 发布工资条（将批次内所有工资条标记为 PUBLISHED）
router.post('/batches/:batchId/publish', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const orgId = req.user!.orgId
    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) {
      return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    }
    // 查找该批次关联的所有工资条（通过 BatchEntry 关联的 employeeId + month）
    const entries = await prisma.batchEntry.findMany({
      where: { batchId, orgId },
      select: { employeeId: true },
    })
    const employeeIds = entries.map(e => e.employeeId)
    if (employeeIds.length === 0) {
      return res.status(400).json({ success: false, error: { code: 'EMPTY', message: '批次内无员工' } })
    }
    // 更新对应月份的工资条
    const result = await prisma.payslip.updateMany({
      where: { orgId, employeeId: { in: employeeIds }, month: batch.month },
      data: { publishStatus: 'PUBLISHED', publishedAt: new Date() },
    })
    res.json({ success: true, data: { published: result.count, month: batch.month } })
  } catch (err) {
    next(err)
  }
})

// 定时发送工资条
router.post('/batches/:batchId/schedule', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const { batchId } = req.params
    const { scheduledAt } = req.body
    if (!scheduledAt) {
      return res.status(400).json({ success: false, error: { code: 'MISSING_DATE', message: '请选择发送时间' } })
    }
    const orgId = req.user!.orgId
    const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
    if (!batch) {
      return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
    }
    const entries = await prisma.batchEntry.findMany({
      where: { batchId, orgId },
      select: { employeeId: true },
    })
    const employeeIds = entries.map(e => e.employeeId)
    if (employeeIds.length === 0) {
      return res.status(400).json({ success: false, error: { code: 'EMPTY', message: '批次内无员工' } })
    }
    const result = await prisma.payslip.updateMany({
      where: { orgId, employeeId: { in: employeeIds }, month: batch.month },
      data: { publishStatus: 'SCHEDULED', scheduledAt: new Date(scheduledAt) },
    })
    res.json({ success: true, data: { scheduled: result.count, scheduledAt } })
  } catch (err) {
    next(err)
  }
})

// 定时发送记录
router.get('/schedule-records', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const records = await prisma.payslip.findMany({
      where: { orgId: req.user!.orgId, publishStatus: 'SCHEDULED' },
      include: { employee: { select: { name: true, department: true } } },
      orderBy: { scheduledAt: 'asc' },
    })
    res.json({ success: true, data: records })
  } catch (err) {
    next(err)
  }
})

// 取消定时发送
router.post('/schedule/:id/cancel', async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const payslip = await prisma.payslip.findFirst({
      where: { id: req.params.id, orgId: req.user!.orgId, publishStatus: 'SCHEDULED' },
    })
    if (!payslip) {
      return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '定时发送记录不存在' } })
    }
    const updated = await prisma.payslip.update({
      where: { id: payslip.id },
      data: { publishStatus: 'UNPUBLISHED', scheduledAt: null },
    })
    res.json({ success: true, data: updated })
  } catch (err) {
    next(err)
  }
})

export default router
