import { Router, Response, NextFunction } from 'express'
import multer from 'multer'
import * as XLSX from 'xlsx'
import bcrypt from 'bcryptjs'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { requireAdmin } from '../middleware/rbac'
import { encrypt, decrypt, sha256 } from '../lib/crypto'
import prisma from '../lib/prisma'
import { extractBirthDateFromIdCard, extractGenderFromIdCard } from '../services/retirement.service'
import { calcBatchEntry } from '../services/payroll.service'
import { createEvidence } from '../services/evidence.service'

// RFC 5987 编码中文文件名
function contentDisposition(filename: string): string {
  const encoded = encodeURIComponent(filename)
  return `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`
}

const router = Router()
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } })

// 证件号码格式校验（18位正则 + 校验位算法）
function validateIdCard(idCard: string): { valid: boolean; upgraded?: string; error?: string } {
  if (!idCard) return { valid: true }
  const s = idCard.trim()
  // 15位证件号码升级为18位
  if (/^\d{15}$/.test(s)) {
    const upgraded = upgrade15To18(s)
    return { valid: true, upgraded }
  }
  if (!/^\d{17}[\dXx]$/.test(s)) {
    return { valid: false, error: '证件号码格式错误（应为18位）' }
  }
  // 校验位算法
  const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
  const sum = s.substring(0, 17).split('').reduce((acc, ch, i) => acc + parseInt(ch) * weights[i], 0)
  const expected = checkCodes[sum % 11]
  if (s.charAt(17).toUpperCase() !== expected) {
    return { valid: false, error: '证件号码校验位错误' }
  }
  return { valid: true }
}

function upgrade15To18(s15: string): string {
  const born = '19' + s15.substring(6, 12)
  const body = s15.substring(0, 6) + born + s15.substring(12)
  const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
  const sum = body.split('').reduce((acc, ch, i) => acc + parseInt(ch) * weights[i], 0)
  return body + checkCodes[sum % 11]
}

// 社保基数范围校验
const SOCIAL_INS_LIMITS: Record<string, { min: number; max: number }> = {
  '北京': { min: 6326, max: 33891 },
  '上海': { min: 7310, max: 36549 },
  '广州': { min: 5284, max: 27501 },
  '深圳': { min: 3523, max: 27501 },
  '杭州': { min: 4812, max: 24060 },
}
function validateSocialBase(base: number, city?: string): { valid: boolean; warning?: string } {
  if (!city || !SOCIAL_INS_LIMITS[city]) return { valid: true }
  const limits = SOCIAL_INS_LIMITS[city]
  if (base < limits.min) return { valid: true, warning: `基数${base}低于${city}下限${limits.min}` }
  if (base > limits.max) return { valid: true, warning: `基数${base}高于${city}上限${limits.max}` }
  return { valid: true }
}

function dateToMonth(d: Date): string {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}

function parseDate(v: any): Date | null {
  if (!v) return null
  if (v instanceof Date) return v
  if (typeof v === 'number') {
    const d = XLSX.SSF.parse_date_code(v)
    if (d) return new Date(d.y, d.m - 1, d.d)
  }
  const s = String(v).trim()
  if (/^\d{4}-\d{2}-\d{2}/.test(s)) return new Date(s)
  if (/^\d{4}\/\d{2}\/\d{2}/.test(s)) return new Date(s.replace(/\//g, '-'))
  return null
}

function val(v: any): string {
  if (v == null) return ''
  return String(v).trim()
}

function num(v: any): number {
  const n = Number(v)
  return isNaN(n) ? 0 : n
}

// 兼容带 * 后缀的列名和旧列名
function getField(row: any, ...keys: string[]): any {
  for (const k of keys) {
    if (row[k] != null && String(row[k]).trim() !== '') return row[k]
    // 尝试带 * 后缀
    if (row[k + '*'] != null && String(row[k + '*']).trim() !== '') return row[k + '*']
  }
  // 模糊匹配：遍历 row 的 key，去除 * 后比较
  for (const rk of Object.keys(row)) {
    const normalized = rk.replace(/\*.*$/, '').replace(/（.*$/, '')
    for (const k of keys) {
      if (normalized === k && row[rk] != null && String(row[rk]).trim() !== '') return row[rk]
    }
  }
  return undefined
}

// ========== 导入预览（不写入数据库） ==========

router.post('/excel/preview', authMiddleware, requireAdmin, upload.single('file'), async (req: AuthRequest, res: Response, next) => {
  try {
    if (!req.file) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请上传文件' } })

    const wb = XLSX.read(req.file.buffer, { type: 'buffer', cellDates: true })
    const preview: any = { employees: [], contracts: [], overtime: [], disciplinary: [], attendance: [], errors: [] as any[] }

    const empSheet = wb.Sheets['员工信息']
    if (empSheet) {
      const rows = XLSX.utils.sheet_to_json(empSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), department: val(getField(r, '部门')) || '未分配', hireDate: getField(r, '入职日期'), salary: num(getField(r, '月工资')), phone: val(getField(r, '手机号')), idCard: val(getField(r, '证件号码')), city: val(getField(r, '参保城市')) || null, status: 'normal', errors: [] as string[], warnings: [] as string[] }
        if (!row.name) { row.status = 'error'; row.errors.push('姓名为空') }
        const hireDate = parseDate(getField(r, '入职日期'))
        if (!hireDate) { row.status = 'error'; row.errors.push('入职日期格式错误') }
        if (row.salary === 0) { row.status = 'error'; row.errors.push('月工资为空') }
        if (row.idCard) {
          const idCheck = validateIdCard(row.idCard)
          if (!idCheck.valid) { row.status = row.status === 'normal' ? 'warning' : row.status; row.warnings.push(idCheck.error!) }
          if (idCheck.upgraded) { row.idCard = idCheck.upgraded; row.warnings.push('15位身份证已升级为18位') }
        }
        if (row.status === 'error') preview.errors.push({ sheet: '员工信息', row: i + 2, name: row.name, errors: row.errors })
        preview.employees.push(row)
      }
    }

    const contractSheet = wb.Sheets['劳动合同']
    if (contractSheet) {
      const rows = XLSX.utils.sheet_to_json(contractSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), contractType: val(getField(r, '合同类型')), startDate: getField(r, '合同开始日期'), endDate: getField(r, '合同结束日期'), status: 'normal', errors: [] as string[] }
        if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
        const sd = parseDate(getField(r, '合同开始日期'))
        if (!sd) { row.status = 'error'; row.errors.push('开始日期格式错误') }
        if (row.status === 'error') preview.errors.push({ sheet: '劳动合同', row: i + 2, name: row.name, errors: row.errors })
        preview.contracts.push(row)
      }
    }

    const otSheet = wb.Sheets['加班记录']
    if (otSheet) {
      const rows = XLSX.utils.sheet_to_json(otSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        const otType = val(getField(r, '加班类型')) || '工作日加班'
        const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), hours: num(getField(r, '加班时长')), otType, status: 'normal', errors: [] as string[] }
        if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
        const dt = parseDate(getField(r, '日期'))
        if (!dt) { row.status = 'error'; row.errors.push('日期格式错误') }
        if (row.status === 'error') preview.errors.push({ sheet: '加班记录', row: i + 2, name: row.name, errors: row.errors })
        preview.overtime.push(row)
      }
    }

    const discSheet = wb.Sheets['违纪记录']
    if (discSheet) {
      const rows = XLSX.utils.sheet_to_json(discSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), violationType: val(getField(r, '违纪类型')), description: val(getField(r, '描述')), status: 'normal', errors: [] as string[] }
        if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
        if (row.status === 'error') preview.errors.push({ sheet: '违纪记录', row: i + 2, name: row.name, errors: row.errors })
        preview.disciplinary.push(row)
      }
    }

    const attSheet = wb.Sheets['考勤记录']
    if (attSheet) {
      const rows = XLSX.utils.sheet_to_json(attSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        const row: any = { rowNo: i + 2, name: val(getField(r, '姓名')), idCard: val(getField(r, '证件号码')), date: getField(r, '日期'), attStatus: val(getField(r, '考勤状态')), status: 'normal', errors: [] as string[] }
        if (!row.name && !row.idCard) { row.status = 'error'; row.errors.push('姓名和证件号码都为空') }
        const dt = parseDate(getField(r, '日期'))
        if (!dt) { row.status = 'error'; row.errors.push('日期格式错误') }
        if (row.status === 'error') preview.errors.push({ sheet: '考勤记录', row: i + 2, name: row.name, errors: row.errors })
        preview.attendance.push(row)
      }
    }

    const summary = {
      totalRows: preview.employees.length + preview.contracts.length + preview.overtime.length + preview.disciplinary.length + preview.attendance.length,
      normalRows: 0,
      warningRows: 0,
      errorRows: preview.errors.length,
      sheets: Object.keys(wb.Sheets).filter(s => !s.startsWith('!')),
    }
    summary.normalRows = summary.totalRows - summary.errorRows
    preview.summary = summary

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

// ========== 错误日志导出 ==========

router.post('/excel/error-log', authMiddleware, async (req: AuthRequest, res: Response, next) => {
  try {
    const { errors } = req.body as { errors: any[] }
    if (!errors || !errors.length) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无错误数据' } })
    }
    const data = errors.map(e => ({
      'Sheet': e.sheet || '',
      '行号': e.row || '',
      '员工姓名': e.name || '',
      '错误类型': Array.isArray(e.errors) ? e.errors.join('; ') : (e.error || ''),
    }))
    const ws = XLSX.utils.json_to_sheet(data)
    const wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, '错误日志')
    const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
    res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
    res.setHeader('Content-Disposition', contentDisposition('导入错误日志.xlsx'))
    res.send(buf)
  } catch (err) {
    next(err)
  }
})

router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async (req: AuthRequest, res: Response, next) => {
  try {
    if (!req.file) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请上传文件' } })
    const orgId = req.user!.orgId
    const userId = req.user!.id

    const wb = XLSX.read(req.file.buffer, { type: 'buffer', cellDates: true })
    const result: any = { employees: 0, contracts: 0, overtime: 0, disciplinary: 0, attendance: 0, skipped: 0, duplicates: 0, errors: [] as string[], details: [] as any[] }

    const empSheet = wb.Sheets['员工信息']
    if (empSheet) {
      const rows = XLSX.utils.sheet_to_json(empSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const name = val(getField(r, '姓名'))
          if (!name) { result.skipped++; result.errors.push(`员工第${i + 2}行：姓名为空，跳过`); result.details.push({ sheet: '员工信息', row: i + 2, name: '', status: 'skipped', message: '姓名为空' }); continue }
          const dept = val(getField(r, '部门')) || '未分配'
          const hireDate = parseDate(getField(r, '入职日期'))
          if (!hireDate) { result.skipped++; result.errors.push(`员工第${i + 2}行：入职日期格式错误`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '入职日期格式错误' }); continue }
          const salary = String(num(getField(r, '月工资')))
          if (salary === '0') { result.skipped++; result.errors.push(`员工第${i + 2}行：月工资为空`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '月工资为空' }); continue }

          let idCard = val(getField(r, '证件号码'))
          if (!idCard) { result.skipped++; result.errors.push(`员工第${i + 2}行：证件号码为空，跳过`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '证件号码为空' }); continue }
          if (idCard) {
            const idCheck = validateIdCard(idCard)
            if (!idCheck.valid) { result.skipped++; result.errors.push(`员工第${i + 2}行：${idCheck.error}`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: idCheck.error }); continue }
            if (idCheck.upgraded) idCard = idCheck.upgraded
          }

          // 同企业内身份证查重
          if (idCard) {
            const idCardExists = await prisma.employee.findFirst({
              where: { orgId, idCardHash: sha256(idCard) },
              select: { id: true, name: true },
            })
            if (idCardExists) {
              result.skipped++
              result.errors.push(`员工第${i + 2}行：证件号码已存在（${idCardExists.name}），跳过`)
              result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '证件号码已存在' })
              continue
            }
          }
          // 同企业内手机号查重
          const importPhone = val(getField(r, '手机号'))
          if (importPhone) {
            const phoneExists = await prisma.employee.findFirst({
              where: { orgId, phone: importPhone },
              select: { id: true, name: true },
            })
            if (phoneExists) {
              result.skipped++
              result.errors.push(`员工第${i + 2}行：手机号已存在（${phoneExists.name}），跳过`)
              result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '手机号已存在' })
              continue
            }
          }

          // 社保基数：值为 0 或「无」表示不参保
          const socialInsBaseVal = num(getField(r, '社保基数'))
          const socialInsOptOut = val(getField(r, '社保基数')) === '无' || val(getField(r, '社保基数')) === '不缴'
          const socialInsBase = socialInsOptOut ? 0 : (socialInsBaseVal || num(salary))

          // 公积金基数：值为 0 或「无」表示不缴纳
          const housingFundBaseVal = num(getField(r, '公积金基数'))
          const housingFundOptOut = val(getField(r, '公积金基数')) === '无' || val(getField(r, '公积金基数')) === '不缴'
          const housingFundBase = housingFundOptOut ? 0 : (housingFundBaseVal || num(salary))

          // 默认密码：手机号后6位（无手机号则 123456）
          const importPhoneVal = val(getField(r, '手机号')) || null
          const defaultPwd = importPhoneVal ? importPhoneVal.slice(-6) : '123456'
          const importPasswordHash = await bcrypt.hash(defaultPwd, 10)

          const emp = await prisma.employee.create({
            data: {
              orgId, name, department: dept, hireDate,
              monthlySalary: encrypt(salary),
              gender: val(getField(r, '性别')) || (idCard ? extractGenderFromIdCard(idCard) : null),
              femaleWorkerType: (val(getField(r, '女性岗位类型')) === '工人' || val(getField(r, '女性岗位类型')) === 'WORKER') ? 'WORKER'
                : (val(getField(r, '女性岗位类型')) === '干部' || val(getField(r, '女性岗位类型')) === 'CADRE') ? 'CADRE' : null,
              phone: importPhoneVal,
              passwordHash: importPasswordHash,
              idCardNumber: idCard ? encrypt(idCard) : null,
              idCardHash: idCard ? sha256(idCard) : null,
              birthDate: idCard ? extractBirthDateFromIdCard(idCard) : null,
              emergencyContact: val(getField(r, '紧急联系人')) || null,
              emergencyPhone: val(getField(r, '紧急联系电话')) || null,
              address: val(getField(r, '住址')) || null,
              bankName: val(getField(r, '开户行')) || null,
              bankAccount: val(getField(r, '银行账号')) ? encrypt(val(getField(r, '银行账号'))) : null,
              socialInsBase,
              housingFundBase,
              specialDeduction: num(getField(r, '专项附加扣除')) || 0,
              city: val(getField(r, '参保城市')) || null,
              isPregnant: val(getField(r, '孕期')) === '是',
              isInMedicalPeriod: val(getField(r, '医疗期')) === '是',
              isWorkInjured: val(getField(r, '工伤')) === '是',
              socialInsStartMonth: socialInsOptOut ? null : dateToMonth(hireDate),
              housingFundStartMonth: housingFundOptOut ? null : dateToMonth(hireDate),
              createdBy: userId,
            },
          })

          // 社保公积金记录不在导入时创建，由 HR 在社保模块办理增员后创建
          await prisma.salaryChangeRecord.create({ data: { orgId, employeeId: emp.id, oldSalary: 0, newSalary: num(salary), effectiveDate: hireDate, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })
          await prisma.employeeDepartmentRecord.create({ data: { orgId, employeeId: emp.id, oldDepartment: '', newDepartment: dept, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })

          await createEvidence({
            orgId, category: 'ONBOARD', refId: emp.id, employeeId: emp.id,
            events: [{ action: '员工入职登记（批量导入）', timestamp: new Date().toISOString(), ip: req.ip || '', userAgent: req.get('User-Agent') || '' }],
            createdBy: userId,
          })

          result.employees++
          result.details.push({ sheet: '员工信息', row: i + 2, name, employeeId: emp.id, status: 'success', message: '导入成功' })
        } catch (e: any) {
          const msg = e?.message || ''
          if (msg.includes('Unique constraint')) {
            result.duplicates++
            result.errors.push(`员工第${i + 2}行：该员工已存在（证件号码重复），跳过`)
            result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'duplicate', message: '证件号码重复' })
          } else if (msg.includes('invalid') || msg.includes('validation')) {
            result.skipped++
            result.errors.push(`员工第${i + 2}行：数据格式不正确，请检查各项填写`)
            result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'error', message: '数据格式不正确' })
          } else {
            result.skipped++
            result.errors.push(`员工第${i + 2}行：导入失败 — ${msg || '未知错误'}`)
            result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'error', message: msg || '未知错误' })
          }
        }
      }
    }

    const contractSheet = wb.Sheets['劳动合同']
    if (contractSheet) {
      const rows = XLSX.utils.sheet_to_json(contractSheet)
      const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
      const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e.id]))
      const empByName = new Map(employees.map(e => [e.name, e.id]))
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const idCard = val(getField(r, '证件号码'))
          const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
          if (!empId) { result.errors.push(`合同第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const startDate = parseDate(getField(r, '合同开始日期'))
          if (!startDate) { result.errors.push(`合同第${i + 2}行：开始日期格式错误`); continue }
          const typeMap: any = { '固定期限': 'FIXED', '无固定期限': 'UNFIXED', '未签': 'UNSIGNED' }
          const contractType = typeMap[val(getField(r, '合同类型'))] || 'FIXED'
          if (contractType !== 'UNSIGNED') {
            await prisma.laborContract.create({
              data: {
                orgId, employeeId: empId,
                signDate: parseDate(getField(r, '签订日期')) || null,
                startDate,
                endDate: parseDate(getField(r, '合同结束日期')) || null,
                contractType,
                signMethod: val(getField(r, '签订方式')) === '电子' ? 'ELECTRONIC' : 'PAPER',
                contractYears: num(getField(r, '合同年限')) || 3,
                probationMonths: num(getField(r, '试用期月数')) || 0,
                probationSalary: num(getField(r, '试用期工资')) || 0,
                createdBy: userId,
              },
            })
            result.contracts++
          }
          await createEvidence({
            orgId, category: 'CONTRACT_SIGN', refId: undefined, employeeId: empId,
            events: [{ action: '合同签订（批量导入）', timestamp: new Date().toISOString(), ip: req.ip || '', userAgent: req.get('User-Agent') || '' }],
            createdBy: userId,
          })
        } catch (e: any) {
          const msg = e?.message || ''
          if (msg.includes('Unique constraint')) result.errors.push(`合同第${i + 2}行：该员工合同已存在，跳过`)
          else result.errors.push(`合同第${i + 2}行：导入失败 — ${msg || '未知错误'}`)
        }
      }
    }

    const otSheet = wb.Sheets['加班记录']
    if (otSheet) {
      const rows = XLSX.utils.sheet_to_json(otSheet)
      const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
      const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e.id]))
      const empByName = new Map(employees.map(e => [e.name, e.id]))
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const idCard = val(getField(r, '证件号码'))
          const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
          if (!empId) { result.errors.push(`加班第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const date = parseDate(getField(r, '日期'))
          if (!date) { result.errors.push(`加班第${i + 2}行：日期格式错误`); continue }
          const month = dateToMonth(date)
          const otType = val(getField(r, '加班类型')) || '工作日加班'
          const hours = num(getField(r, '加班时长'))
          const weekdayHours = num(getField(r, '工作日加班时长')) || (otType.includes('工作日') ? hours : 0)
          const weekendHours = num(getField(r, '休息日加班时长')) || (otType.includes('休息日') ? hours : 0)
          const holidayHours = num(getField(r, '法定节假日加班时长')) || (otType.includes('法定') ? hours : 0)
          await prisma.overtimeRecord.create({ data: { orgId, employeeId: empId, month, weekdayHours, weekendHours, holidayHours } })
          result.overtime++
        } catch (e: any) {
          const msg = e?.message || ''
          if (msg.includes('Unique constraint')) result.errors.push(`加班第${i + 2}行：该员工此月份已有加班记录，同一员工同一月只能导入一条，请合并后重试`)
          else result.errors.push(`加班第${i + 2}行：导入失败 — ${msg || '未知错误'}`)
        }
      }
    }

    const discSheet = wb.Sheets['违纪记录']
    if (discSheet) {
      const rows = XLSX.utils.sheet_to_json(discSheet)
      const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
      const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e.id]))
      const empByName = new Map(employees.map(e => [e.name, e.id]))
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const idCard = val(getField(r, '证件号码'))
          const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
          if (!empId) { result.errors.push(`违纪第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const date = parseDate(getField(r, '日期'))
          if (!date) { result.errors.push(`违纪第${i + 2}行：日期格式错误`); continue }
          const typeMap: any = { '迟到': 'LATE', '旷工': 'ABSENT', '不服从': 'INSUBORDINATION', '违纪': 'MISCONDUCT', '违规': 'VIOLATE_POLICY', '其他': 'OTHER' }
          const sevMap: any = { '警告': 'WARNING', '严重': 'SERIOUS', '重度': 'SEVERE' }
          const actMap: any = { '口头警告': 'ORAL_WARNING', '书面警告': 'WRITTEN_WARNING', '扣款': 'DEDUCTION', '降级': 'DEMOTION', '辞退': 'TERMINATION' }
          await prisma.disciplinaryRecord.create({ data: { orgId, employeeId: empId, violationDate: date, violationType: typeMap[val(getField(r, '违纪类型'))] || 'OTHER', description: val(getField(r, '描述')), severity: sevMap[val(getField(r, '严重程度'))] || 'WARNING', action: actMap[val(getField(r, '处罚'))] || 'ORAL_WARNING', createdBy: userId } })

          await createEvidence({
            orgId, category: 'DISCIPLINARY', refId: undefined, employeeId: empId,
            events: [{ action: `违纪记录创建（${val(getField(r, '违纪类型')) || '其他'}）`, timestamp: new Date().toISOString(), ip: req.ip || '', userAgent: req.get('User-Agent') || '' }],
            createdBy: userId,
          })

          result.disciplinary++
        } catch (e: any) {
          const msg = e?.message || ''
          result.errors.push(`违纪第${i + 2}行：导入失败 — ${msg || '未知错误'}`)
        }
      }
    }

    const attSheet = wb.Sheets['考勤记录']
    if (attSheet) {
      const rows = XLSX.utils.sheet_to_json(attSheet)
      const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
      const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e.id]))
      const empByName = new Map(employees.map(e => [e.name, e.id]))
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const idCard = val(getField(r, '证件号码'))
          const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
          if (!empId) { result.errors.push(`考勤第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const date = parseDate(getField(r, '日期'))
          if (!date) { result.errors.push(`考勤第${i + 2}行：日期格式错误`); continue }
          const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }
          await prisma.attendanceRecord.create({ data: { orgId, employeeId: empId, date, status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId } })

          await createEvidence({
            orgId, category: 'ATTENDANCE', refId: undefined, employeeId: empId,
            events: [{ action: `考勤记录导入（${val(getField(r, '考勤状态')) || '正常'}）`, timestamp: new Date().toISOString(), ip: req.ip || '', userAgent: req.get('User-Agent') || '' }],
            createdBy: userId,
          })

          result.attendance++
        } catch (e: any) {
          const msg = e?.message || ''
          if (msg.includes('Unique constraint')) result.errors.push(`考勤第${i + 2}行：该员工此日期已有考勤记录，跳过`)
          else result.errors.push(`考勤第${i + 2}行：导入失败 — ${msg || '未知错误'}`)
        }
      }
    }

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

router.get('/template', authMiddleware, async (_req: AuthRequest, res: Response) => {
  const wb = XLSX.utils.book_new()

  const empData = [
    { '姓名*': '张三', '部门': '技术部', '性别（选填，留空自动识别）': '男', '手机号': '13800138000', '证件号码': '110101199001011234', '入职日期*': '2023-03-01', '月工资*': 10000, '社保基数': 10000, '公积金基数': 10000, '专项附加扣除': 1000, '参保城市': '北京', '紧急联系人': '李四', '紧急联系电话': '13900139000', '住址': '北京市朝阳区', '开户行': '工商银行', '银行账号': '6222021234567890', '孕期': '否', '医疗期': '否', '工伤': '否' },
  ]
  const empWs = XLSX.utils.json_to_sheet(empData, { header: ['姓名*', '部门', '性别（选填，留空自动识别）', '手机号', '证件号码', '入职日期*', '月工资*', '社保基数', '公积金基数', '专项附加扣除', '参保城市', '紧急联系人', '紧急联系电话', '住址', '开户行', '银行账号', '孕期', '医疗期', '工伤'] })
  // 设置示例行样式（灰色背景）
  empWs['!cols'] = [{ wch: 10 }, { wch: 12 }, { wch: 22 }, { wch: 13 }, { wch: 20 }, { wch: 12 }, { wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 12 }, { wch: 10 }, { wch: 10 }, { wch: 13 }, { wch: 18 }, { wch: 10 }, { wch: 18 }, { wch: 6 }, { wch: 6 }, { wch: 6 }]
  XLSX.utils.book_append_sheet(wb, empWs, '员工信息')

  const contractData = [
    { '姓名*': '张三', '证件号码': '110101199001011234', '合同类型': '固定期限', '签订日期': '2023-03-01', '合同开始日期*': '2023-03-01', '合同结束日期': '2026-03-01', '合同年限': 3, '签订方式': '纸质', '试用期月数': 3, '试用期工资': 8000 },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(contractData), '劳动合同')

  const otData = [
    { '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-15', '工作日加班时长': 2, '休息日加班时长': 0, '法定节假日加班时长': 0, '加班类型': '工作日加班', '加班时长': 2, '倍率': 1.5, '是否审批': '是' },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(otData), '加班记录')

  const discData = [
    { '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(discData), '违纪记录')

  const attData = [
    { '姓名*': '张三', '证件号码': '110101199001011234', '日期*': '2024-01-15', '考勤状态': '正常', '上班时间': '09:00', '下班时间': '18:00', '备注': '' },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(attData), '考勤记录')

  const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
  res.setHeader('Content-Disposition', contentDisposition('员工导入模板.xlsx'))
  res.send(buf)
})

// ========== 月度导入 ==========

router.post('/monthly', authMiddleware, requireAdmin, upload.single('file'), async (req: AuthRequest, res: Response, next) => {
  try {
    if (!req.file) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请上传文件' } })
    const orgId = req.user!.orgId
    const userId = req.user!.id
    const month = val(req.body.month) || dateToMonth(new Date())
    if (!/^\d{4}-\d{2}$/.test(month)) {
      return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '月份格式应为 YYYY-MM' } })
    }

    const wb = XLSX.read(req.file.buffer, { type: 'buffer', cellDates: true })
    const result: any = { month, attendance: 0, overtime: 0, discipline: 0, salaryChanges: 0, socialInsChanges: 0, housingFundChanges: 0, errors: [] as string[], strategies: { '考勤记录': '覆盖（同员工同日覆盖）', '加班记录': '累加（同员工同月累加）', '违纪记录': '追加（同员工同日可多条）', '薪资调整': '覆盖（关闭旧记录，新建新记录）', '社保变动': '覆盖（关闭旧记录，新建新记录）', '公积金变动': '覆盖（关闭旧记录，新建新记录）' } }

    const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, monthlySalary: true, department: true, idCardHash: true } })
    const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e]))
    const empByName = new Map(employees.map(e => [e.name, e]))

    // 获取加班费配置，用于自动计算 totalPay
    const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId } }) ?? { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 }
    function calcOvertimePay(monthlyWage: number, wdHours: number, weHours: number, hoHours: number) {
      const hourlyWage = (monthlyWage || 0) / otConfig.monthlyDays / otConfig.dailyHours
      const weekdayPay = hourlyWage * otConfig.weekdayRate * wdHours
      const weekendPay = hourlyWage * otConfig.weekendRate * weHours
      const holidayPay = hourlyWage * otConfig.holidayRate * hoHours
      return Math.round((weekdayPay + weekendPay + holidayPay) * 100) / 100
    }

    function findEmp(r: any) {
      const idCard = val(getField(r, '证件号码'))
      if (idCard) {
        const emp = empByHash.get(sha256(idCard))
        if (emp) return emp
      }
      return empByName.get(val(getField(r, '姓名')))
    }

    // 考勤记录 + 加班记录（支持合并Sheet"考勤与加班"或独立Sheet）
    const mergedSheet = wb.Sheets['考勤与加班']
    const attSheet = wb.Sheets['考勤记录']
    const otSheet = wb.Sheets['加班记录']
    const statusMap: any = { '正常': 'NORMAL', '迟到': 'LATE', '早退': 'EARLY_LEAVE', '缺勤': 'ABSENT', '请假': 'LEAVE', '出差': 'BUSINESS_TRIP' }

    if (mergedSheet) {
      // 合并Sheet：每行同时处理考勤和加班
      const rows = XLSX.utils.sheet_to_json(mergedSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const emp = findEmp(r)
          if (!emp) { result.errors.push(`第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const date = parseDate(getField(r, '日期'))
          if (!date) { result.errors.push(`第${i + 2}行：日期格式错误`); continue }

          // 考勤部分
          const attStatus = val(getField(r, '考勤状态'))
          if (attStatus || val(getField(r, '上班时间')) || val(getField(r, '下班时间'))) {
            await prisma.attendanceRecord.upsert({
              where: { employeeId_date: { employeeId: emp.id, date } },
              create: { orgId, employeeId: emp.id, date, status: statusMap[attStatus] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId },
              update: { status: statusMap[attStatus] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null },
            })
            result.attendance++
          }

          // 加班部分
          const wdHours = num(getField(r, '工作日加班时长'))
          const weHours = num(getField(r, '休息日加班时长'))
          const hoHours = num(getField(r, '法定节假日加班时长'))
          if (wdHours > 0 || weHours > 0 || hoHours > 0) {
            const otMonth = dateToMonth(date)
            let monthlyWage = 0
            try { monthlyWage = Number(decrypt(emp.monthlySalary)) || 0 } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
            const totalPay = calcOvertimePay(monthlyWage, wdHours, weHours, hoHours)
            await prisma.overtimeRecord.upsert({
              where: { employeeId_month: { employeeId: emp.id, month: otMonth } },
              create: { orgId, employeeId: emp.id, month: otMonth, weekdayHours: wdHours, weekendHours: weHours, holidayHours: hoHours, totalPay } as any,
              update: {
                weekdayHours: { increment: wdHours },
                weekendHours: { increment: weHours },
                holidayHours: { increment: hoHours },
                totalPay: { increment: totalPay },
              },
            })
            result.overtime++
          }
        } catch (e: any) { result.errors.push(`第${i + 2}行：${e?.message || '导入失败'}`) }
      }
    } else {
      // 向后兼容：独立Sheet
      if (attSheet) {
        const rows = XLSX.utils.sheet_to_json(attSheet)
        for (let i = 0; i < rows.length; i++) {
          const r = rows[i] as any
          try {
            const emp = findEmp(r)
            if (!emp) { result.errors.push(`考勤第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
            const date = parseDate(getField(r, '日期'))
            if (!date) { result.errors.push(`考勤第${i + 2}行：日期格式错误`); continue }
            await prisma.attendanceRecord.upsert({
              where: { employeeId_date: { employeeId: emp.id, date } },
              create: { orgId, employeeId: emp.id, date, status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null, createdBy: userId },
              update: { status: statusMap[val(getField(r, '考勤状态'))] || 'NORMAL', checkInTime: val(getField(r, '上班时间')) || null, checkOutTime: val(getField(r, '下班时间')) || null, remark: val(getField(r, '备注')) || null },
            })
            result.attendance++
          } catch (e: any) { result.errors.push(`考勤第${i + 2}行：${e?.message || '导入失败'}`) }
        }
      }

      if (otSheet) {
        const rows = XLSX.utils.sheet_to_json(otSheet)
        for (let i = 0; i < rows.length; i++) {
          const r = rows[i] as any
          try {
            const emp = findEmp(r)
            if (!emp) { result.errors.push(`加班第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
            const date = parseDate(getField(r, '日期'))
            if (!date) { result.errors.push(`加班第${i + 2}行：日期格式错误`); continue }
            const otMonth = dateToMonth(date)
            const hours = num(getField(r, '加班时长'))
            const otType = val(getField(r, '加班类型')) || '工作日加班'
            const wdHours = num(getField(r, '工作日加班时长')) || (otType.includes('工作日') ? hours : 0)
            const weHours = num(getField(r, '休息日加班时长')) || (otType.includes('休息日') ? hours : 0)
            const hoHours = num(getField(r, '法定节假日加班时长')) || (otType.includes('法定') ? hours : 0)
            let monthlyWage = 0
            try { monthlyWage = Number(decrypt(emp.monthlySalary)) || 0 } catch { monthlyWage = Number(emp.monthlySalary) || 0 }
            const totalPay = calcOvertimePay(monthlyWage, wdHours, weHours, hoHours)
            await prisma.overtimeRecord.upsert({
              where: { employeeId_month: { employeeId: emp.id, month: otMonth } },
              create: { orgId, employeeId: emp.id, month: otMonth, weekdayHours: wdHours, weekendHours: weHours, holidayHours: hoHours, totalPay } as any,
              update: {
                weekdayHours: { increment: wdHours },
                weekendHours: { increment: weHours },
                holidayHours: { increment: hoHours },
                totalPay: { increment: totalPay },
              },
            })
            result.overtime++
          } catch (e: any) { result.errors.push(`加班第${i + 2}行：${e?.message || '导入失败'}`) }
        }
      }
    }

    // 薪资调整
    const salarySheet = wb.Sheets['薪资调整']
    if (salarySheet) {
      const rows = XLSX.utils.sheet_to_json(salarySheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const emp = findEmp(r)
          if (!emp) { result.errors.push(`薪资第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const newSalary = num(getField(r, '调整后月薪'))
          if (newSalary <= 0) { result.errors.push(`薪资第${i + 2}行：调整后月薪无效`); continue }
          const effDate = parseDate(getField(r, '生效日期')) || new Date(month + '-01')
          const effMonth = dateToMonth(effDate)
          let oldSalary = 0
          try { oldSalary = Number(decrypt(emp.monthlySalary)) || 0 } catch { oldSalary = 0 }
          // 关闭之前有效记录
          await prisma.salaryChangeRecord.updateMany({ where: { employeeId: emp.id, endMonth: null }, data: { endMonth: effMonth } })
          await prisma.salaryChangeRecord.create({ data: { orgId, employeeId: emp.id, oldSalary, newSalary, effectiveDate: effDate, effectiveMonth: effMonth, endMonth: null, changeType: 'SALARY_CHANGE', reason: val(getField(r, '调薪原因')) || '月度导入', createdBy: userId } })
          await prisma.employee.update({ where: { id: emp.id }, data: { monthlySalary: encrypt(String(newSalary)) } })
          result.salaryChanges++
        } catch (e: any) { result.errors.push(`薪资第${i + 2}行：${e?.message || '导入失败'}`) }
      }
    }

    // 社保增减员
    const socialSheet = wb.Sheets['社保变动']
    if (socialSheet) {
      const rows = XLSX.utils.sheet_to_json(socialSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const emp = findEmp(r)
          if (!emp) { result.errors.push(`社保第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const changeType = val(getField(r, '变动类型'))
          const base = num(getField(r, '缴费基数'))
          const city = val(getField(r, '城市')) || '北京'
          if (changeType === '增员' || changeType === '调基') {
            const baseCheck = validateSocialBase(base, city)
            if (baseCheck.warning) result.errors.push(`社保第${i + 2}行警告：${baseCheck.warning}`)
            // 关闭之前有效记录
            await prisma.employeeSocialInsRecord.updateMany({ where: { employeeId: emp.id, endMonth: null }, data: { endMonth: month } })
            await prisma.employeeSocialInsRecord.create({ data: { orgId, employeeId: emp.id, startMonth: month, endMonth: null, base: base || 0, changeType: changeType === '增员' ? 'ONBOARDING' : 'ADJUST', createdBy: userId } })
            await prisma.employee.update({ where: { id: emp.id }, data: { socialInsBase: base || 0, socialInsStartMonth: month, socialInsEndMonth: null } })
          } else if (changeType === '减员') {
            await prisma.employeeSocialInsRecord.updateMany({ where: { employeeId: emp.id, endMonth: null }, data: { endMonth: month, changeType: 'TERMINATION' } })
            await prisma.employee.update({ where: { id: emp.id }, data: { socialInsEndMonth: month } })
          }
          result.socialInsChanges++
        } catch (e: any) { result.errors.push(`社保第${i + 2}行：${e?.message || '导入失败'}`) }
      }
    }

    // 公积金增减员
    const hfSheet = wb.Sheets['公积金变动']
    if (hfSheet) {
      const rows = XLSX.utils.sheet_to_json(hfSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const emp = findEmp(r)
          if (!emp) { result.errors.push(`公积金第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const changeType = val(getField(r, '变动类型'))
          const base = num(getField(r, '缴费基数'))
          if (changeType === '增员' || changeType === '调基') {
            await prisma.employeeHousingFundRecord.updateMany({ where: { employeeId: emp.id, endMonth: null }, data: { endMonth: month } })
            await prisma.employeeHousingFundRecord.create({ data: { orgId, employeeId: emp.id, startMonth: month, endMonth: null, base: base || 0, changeType: changeType === '增员' ? 'ONBOARDING' : 'ADJUST', createdBy: userId } })
            await prisma.employee.update({ where: { id: emp.id }, data: { housingFundBase: base || 0, housingFundStartMonth: month, housingFundEndMonth: null } })
          } else if (changeType === '减员') {
            await prisma.employeeHousingFundRecord.updateMany({ where: { employeeId: emp.id, endMonth: null }, data: { endMonth: month, changeType: 'TERMINATION' } })
            await prisma.employee.update({ where: { id: emp.id }, data: { housingFundEndMonth: month } })
          }
          result.housingFundChanges++
        } catch (e: any) { result.errors.push(`公积金第${i + 2}行：${e?.message || '导入失败'}`) }
      }
    }

    // 违纪记录
    const discSheet = wb.Sheets['违纪记录']
    if (discSheet) {
      const rows = XLSX.utils.sheet_to_json(discSheet)
      for (let i = 0; i < rows.length; i++) {
        const r = rows[i] as any
        try {
          const emp = findEmp(r)
          if (!emp) { result.errors.push(`违纪第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
          const date = parseDate(getField(r, '日期'))
          if (!date) { result.errors.push(`违纪第${i + 2}行：日期格式错误`); continue }
          const typeMap: any = { '迟到': 'LATE', '旷工': 'ABSENT', '不服从': 'INSUBORDINATION', '违纪': 'MISCONDUCT', '违规': 'VIOLATE_POLICY', '其他': 'OTHER' }
          const actMap: any = { '口头警告': 'ORAL_WARNING', '书面警告': 'WRITTEN_WARNING', '扣款': 'DEDUCTION', '降级': 'DEMOTION', '辞退': 'TERMINATION' }
          await prisma.disciplinaryRecord.create({
            data: { orgId, employeeId: emp.id, violationDate: date, violationType: typeMap[val(getField(r, '违纪类型'))] || 'OTHER', description: val(getField(r, '描述')) || '', action: actMap[val(getField(r, '处罚'))] || 'ORAL_WARNING', createdBy: userId },
          })
          result.discipline++
        } catch (e: any) { result.errors.push(`违纪第${i + 2}行：${e?.message || '导入失败'}`) }
      }
    }

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

router.get('/monthly-template', authMiddleware, async (_req: AuthRequest, res: Response) => {
  const wb = XLSX.utils.book_new()

  // 合并考勤+加班为一个Sheet，减少重复录入姓名证件号码
  const attOtData = [{
    '姓名': '张三',
    '证件号码': '110101199001011234',
    '日期': '2024-06-01',
    '考勤状态': '正常',
    '上班时间': '09:00',
    '下班时间': '18:00',
    '工作日加班时长': 0,
    '休息日加班时长': 0,
    '法定节假日加班时长': 0,
    '备注': '',
  }]
  const attOtWs = XLSX.utils.json_to_sheet(attOtData)
  attOtWs['!cols'] = [
    { wch: 10 }, { wch: 20 }, { wch: 12 }, { wch: 10 }, { wch: 8 }, { wch: 8 },
    { wch: 14 }, { wch: 14 }, { wch: 16 }, { wch: 12 },
  ]
  XLSX.utils.book_append_sheet(wb, attOtWs, '考勤与加班')

  const salaryData = [{ '姓名': '张三', '证件号码': '110101199001011234', '调整后月薪': 12000, '生效日期': '2024-06-01', '调薪原因': '年度调薪' }]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(salaryData), '薪资调整')

  const socialData = [{ '姓名': '张三', '证件号码': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(socialData), '社保变动')

  const hfData = [{ '姓名': '张三', '证件号码': '110101199001011234', '变动类型': '调基', '缴费基数': 12000 }]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(hfData), '公积金变动')

  const discData = [{ '姓名': '张三', '证件号码': '110101199001011234', '日期': '2024-06-10', '违纪类型': '警告', '描述': '迟到', '处罚': '口头警告' }]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(discData), '违纪记录')

  const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
  res.setHeader('Content-Disposition', contentDisposition('考勤月度导入模板.xlsx'))
  res.send(buf)
})

// 工资表导入 — 批量更新批次条目的薪酬输入项
router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    if (!req.file) return res.status(400).json({ success: false, message: '请上传文件' })
    const orgId = req.user!.orgId
    const batchId = req.body.batchId as string
    if (!batchId) return res.status(400).json({ success: false, message: '缺少批次ID' })

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

    const wb = XLSX.read(req.file.buffer, { type: 'buffer' })
    const sheet = wb.Sheets[wb.SheetNames[0]]
    if (!sheet) return res.status(400).json({ success: false, message: 'Excel 文件无有效 Sheet' })

    const rows = XLSX.utils.sheet_to_json(sheet)
    const result = { total: rows.length, updated: 0, errors: [] as string[] }

    // 构建员工查找索引
    const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
    const empByName = new Map(employees.map(e => [e.name, e.id]))
    const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash!, e.id]))

    // 查询现有批次条目
    const entries = await prisma.batchEntry.findMany({ where: { batchId }, select: { id: true, employeeId: true } })
    const entryByEmp = new Map(entries.map(e => [e.employeeId, e.id]))

    for (let i = 0; i < rows.length; i++) {
      const r = rows[i] as any
      try {
        const idCard = val(getField(r, '证件号码'))
        const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(val(getField(r, '姓名')))
        if (!empId) { result.errors.push(`第${i + 2}行：找不到员工「${val(getField(r, '姓名'))}」`); continue }
        const entryId = entryByEmp.get(empId)
        if (!entryId) { result.errors.push(`第${i + 2}行：员工「${val(getField(r, '姓名'))}」不在本批次中`); continue }

        const inputs = {
          baseSalary: num(getField(r, '基本工资')) || 0,
          overtimePay: num(getField(r, '加班费')) || 0,
          allowance: num(getField(r, '津贴')) || 0,
          deduction: num(getField(r, '扣款')) || 0,
          bonus: num(getField(r, '奖金')) || 0,
        }

        // 重新计算税费
        const entry = await prisma.batchEntry.findUnique({ where: { id: entryId } })
        const calcResult = await calcBatchEntry(orgId, empId, batch.month, inputs, batch.type, undefined, (entry?.extraItems as Record<string, number>) || null)
        const { systemSocialEmp: _sse, systemSocialOrg: _sso, systemHousingEmp: _she, systemHousingOrg: _sho, taxBreakdown: _tb, extraItems: _ei, ...entryData } = calcResult
        await prisma.batchEntry.update({ where: { id: entryId }, data: { ...inputs, ...entryData, extraItems: calcResult.extraItems && Object.keys(calcResult.extraItems).length > 0 ? calcResult.extraItems : undefined } })
        result.updated++
      } catch (e: any) {
        result.errors.push(`第${i + 2}行：${e?.message || '导入失败'}`)
      }
    }

    // 更新批次汇总
    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,
      },
    })

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

// 工资表导入模板下载
router.get('/payroll-template', authMiddleware, (_req: AuthRequest, res: Response) => {
  const wb = XLSX.utils.book_new()
  const data = [
    { '姓名*': '张三', '证件号码*': '110101199001011234', '基本工资': 10000, '加班费': 500, '津贴': 800, '扣款': 0, '奖金': 2000 },
    { '姓名*': '李四', '证件号码*': '110101199002021234', '基本工资': 12000, '加班费': 0, '津贴': 600, '扣款': 100, '奖金': 0 },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data), '工资表')
  const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
  res.setHeader('Content-Disposition', contentDisposition('工资表导入模板.xlsx'))
  res.send(buf)
})

// ========== 专项附加扣除批量导入 ==========

router.get('/special-deduction/template', authMiddleware, (_req: AuthRequest, res: Response) => {
  const wb = XLSX.utils.book_new()
  const data = [
    { '姓名*': '张三', '证件号码': '110101199001011234', '子女教育': 1000, '赡养老人': 2000, '住房': 1500, '继续教育': 0, '婴幼儿照护': 0, '备注': '' },
    { '姓名*': '李四', '证件号码': '110101199002021234', '子女教育': 0, '赡养老人': 1000, '住房': 0, '继续教育': 400, '婴幼儿照护': 1000, '备注': '继续教育证书' },
  ]
  XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(data), '专项附加扣除')
  const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })
  res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
  res.setHeader('Content-Disposition', contentDisposition('专项附加扣除导入模板.xlsx'))
  res.send(buf)
})

router.post('/special-deduction', authMiddleware, requireAdmin, upload.single('file'), async (req: AuthRequest, res: Response, next) => {
  try {
    if (!req.file) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请上传文件' } })
    const orgId = req.user!.orgId
    const userId = req.user!.id
    const month = (req.body.month as string) || new Date().toISOString().slice(0, 7)

    const wb = XLSX.read(req.file.buffer, { type: 'buffer' })
    const sheet = wb.Sheets['专项附加扣除']
    if (!sheet) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未找到「专项附加扣除」Sheet' } })

    const rows = XLSX.utils.sheet_to_json(sheet)
    const employees = await prisma.employee.findMany({ where: { orgId }, select: { id: true, name: true, idCardHash: true } })
    const empByHash = new Map(employees.filter(e => e.idCardHash).map(e => [e.idCardHash, e.id]))
    const empByName = new Map(employees.map(e => [e.name, e.id]))

    const result: any = { total: rows.length, updated: 0, skipped: 0, errors: [] as string[], details: [] as any[] }

    for (let i = 0; i < rows.length; i++) {
      const r = rows[i] as any
      try {
        const name = val(getField(r, '姓名'))
        const idCard = val(getField(r, '证件号码'))
        const empId = idCard ? empByHash.get(sha256(idCard)) : empByName.get(name)
        if (!empId) { result.skipped++; result.errors.push(`第${i + 2}行：找不到员工「${name}」`); result.details.push({ row: i + 2, name, status: 'skipped', message: '找不到员工' }); continue }

        const children = num(getField(r, '子女教育'))
        const elderly = num(getField(r, '赡养老人'))
        const housing = num(getField(r, '住房'))
        const education = num(getField(r, '继续教育'))
        const infant = num(getField(r, '婴幼儿照护'))
        const amount = children + elderly + housing + education + infant
        const remark = val(getField(r, '备注')) || null

        await prisma.specialDeductionRecord.upsert({
          where: { employeeId_month: { employeeId: empId, month } },
          create: { orgId, employeeId: empId, month, amount, children, elderly, housing, education, infant, remark, createdBy: userId },
          update: { amount, children, elderly, housing, education, infant, remark },
        })
        await prisma.employee.update({ where: { id: empId }, data: { specialDeduction: amount } })
        result.updated++
      } catch (e: any) {
        result.skipped++
        result.errors.push(`第${i + 2}行：${e?.message || '导入失败'}`)
        result.details.push({ row: i + 2, name: val(getField(r, '姓名')), status: 'error', message: e?.message || '导入失败' })
      }
    }

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

export default router
