const fs = require('fs/promises')
const path = require('path')
const express = require('express')
const multer = require('multer')
const bcrypt = require('bcryptjs')
const { z } = require('zod')
const { nanoid } = require('nanoid')
const {
  findUserById,
  findUserByEmail,
  findUserByNickname,
  findTopicById,
  findTopicBySlug,
  findTopicItemById,
  listTopicItems,
  findImageAssetById,
  createTopic,
  listTopics,
  updateTopicMeta,
  updateTopicThumbnail,
  createTopicItem,
  updateTopicItemLabel,
  updateTopicItemMeta,
  updateTopicItemDisplayOrder,
  countTierListsUsingTopicItem,
  updateCustomItemLabel,
  updateCustomItemMeta,
  updateImageAssetLabel,
  deleteTopicItem,
  deleteTopic,
  deleteTierList,
  updateTopicDisplayOrder,
  listCustomItems,
  findCustomItemById,
  findUnusedCustomItems,
  findCustomItemsByIds,
  deleteCustomItems,
  markCustomItemReplaced,
  listUsers,
  findPrimaryAdminUser,
  listAdminTierLists,
  summarizeAdminTierLists,
  findTierListById,
  updateAdminTierListMeta,
  updateTierListFeaturedStatus,
  listAdminTemplateRequests,
  findTemplateRequestById,
  updateTemplateRequestStatus,
  updateTemplateRequestTargetTopic,
  adminUpdateUser,
  adminUpdateUserPassword,
  adminDeleteUser,
  listUnusedImageAssets,
  deleteImageAssets,
  getImageAssetStats,
  listRecentImageOptimizationJobs,
  clearImageOptimizationJobs,
  cleanupMissingUploadReferences,
  listReferencedUploadSources,
  replaceUploadSourceReferences,
  updateCustomItemDisplayReferences,
  clearCustomItemReplacement,
} = require('../db')
const { requireAdmin } = require('../middleware/auth')
const { createMemoryUpload, writeOptimizedImage, getImageOptimizationQueueState } = require('../lib/image-storage')
const router = express.Router()

function getTemplateIdFromParams(req) {
  return req.params.templateId || ''
}

function buildUploadFilename(file) {
  const ext = path.extname(file.originalname || '').toLowerCase()
  const safeExt = ext && /^[.a-z0-9]+$/.test(ext) ? ext : ''
  return `${Date.now()}-${nanoid()}${safeExt}`
}

function buildItemLabelFromFilename(file) {
  const originalName = file?.originalname || ''
  const base = path.basename(originalName, path.extname(originalName))
  const normalized = base
    .replace(/[_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 60)

  return normalized || 'item'
}

function buildItemLabelFromSrc(src) {
  const raw = typeof src === 'string' ? src : ''
  const base = path.basename(raw.split('?')[0] || '', path.extname(raw.split('?')[0] || ''))
  const normalized = base
    .replace(/[_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 60)
  return normalized || 'item'
}

const upload = createMemoryUpload(multer, { fileSize: 20 * 1024 * 1024, maxCount: 100 })
const avatarUpload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })

function normalizeRouteTags(tags) {
  const values = Array.isArray(tags) ? tags : typeof tags === 'string' ? tags.split(',') : []
  return Array.from(
    new Set(
      values
        .map((tag) => String(tag || '').trim().replace(/^#/, '').slice(0, 40))
        .filter(Boolean)
    )
  ).slice(0, 30)
}

function decorateAdminUser(user, primaryAdmin) {
  if (!user) return null
  const isPrimaryAdmin = !!user.isAdmin && primaryAdmin?.id === user.id
  return {
    ...user,
    isPrimaryAdmin,
    isOperator: !!user.isAdmin && !isPrimaryAdmin,
    role: isPrimaryAdmin ? 'owner' : user.isAdmin ? 'operator' : 'user',
  }
}

async function getAdminUserContext(targetUserId, actingUserId) {
  const [targetUser, actingUser, primaryAdmin] = await Promise.all([
    findUserById(targetUserId),
    findUserById(actingUserId),
    findPrimaryAdminUser(),
  ])
  return { targetUser, actingUser, primaryAdmin }
}

function canManageAdminRole(actingUser, primaryAdmin) {
  return !!actingUser?.isAdmin && primaryAdmin?.id === actingUser.id
}

router.post('/templates', requireAdmin, async (req, res) => {
  const schema = z.object({
    slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
    name: z.string().min(1).max(60),
    tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
    isPublic: z.boolean().optional().default(false),
    thumbnailSrc: z.string().max(255).optional().default(''),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
  const exists = await findTopicBySlug(parsed.data.slug)
  if (exists) return res.status(409).json({ error: 'topic_slug_taken' })
  let template
  try {
    template = await createTopic({
      slug: parsed.data.slug,
      name: parsed.data.name,
      tags: normalizeRouteTags(parsed.data.tags),
      isPublic: parsed.data.isPublic,
    })
  } catch (error) {
    if (error?.code === 'TOPIC_SLUG_INVALID') return res.status(400).json({ error: 'topic_slug_invalid' })
    if (error?.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'topic_slug_taken' })
    throw error
  }
  if (parsed.data.thumbnailSrc) {
    const copiedThumb = await copyUploadIntoTopicAsset(parsed.data.thumbnailSrc)
    await updateTopicThumbnail(template.id, copiedThumb)
  }
  const savedTemplate = await findTopicById(template.id)
  res.json({ template: savedTemplate })
})

router.patch('/templates/:templateId', requireAdmin, async (req, res) => {
  const schema = z.object({
    slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
    name: z.string().trim().min(1).max(60).optional(),
    tags: z.array(z.string().trim().min(1).max(40)).max(30).optional(),
    isPublic: z.boolean().optional(),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const templateId = getTemplateIdFromParams(req)
  const template = await findTopicById(templateId)
  if (!template) return res.status(404).json({ error: 'not_found' })

  try {
    const updated =
      typeof parsed.data.name === 'string' || typeof parsed.data.slug === 'string' || typeof parsed.data.isPublic === 'boolean'
        ? await updateTopicMeta(template.id, {
          slug: parsed.data.slug || template.slug,
          name: parsed.data.name || template.name,
          tags: typeof parsed.data.tags !== 'undefined' ? normalizeRouteTags(parsed.data.tags) : template.tags || [],
          isPublic: typeof parsed.data.isPublic === 'boolean' ? parsed.data.isPublic : template.isPublic,
        })
        : await findTopicById(template.id)
    return res.json({ template: updated })
  } catch (error) {
    if (error?.code === 'TOPIC_SLUG_INVALID') return res.status(400).json({ error: 'topic_slug_invalid' })
    if (error?.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'topic_slug_taken' })
    throw error
  }
})

router.patch('/templates/display-order', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicIds: z.array(z.string().min(1)).max(50),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const templates = await listTopics('', { includePrivate: true })
  const validTopicIds = new Set(templates.map((template) => template.id))
  const filteredIds = parsed.data.topicIds.filter((topicId) => validTopicIds.has(topicId))
  const updatedTemplates = await updateTopicDisplayOrder(filteredIds)
  res.json({ templates: updatedTemplates })
})

router.patch('/templates/:templateId/items/display-order', requireAdmin, async (req, res) => {
  const schema = z.object({
    itemIds: z.array(z.string().min(1)).min(1),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const templateId = getTemplateIdFromParams(req)
  const template = await findTopicById(templateId)
  if (!template) return res.status(404).json({ error: 'not_found' })

  const items = await updateTopicItemDisplayOrder(template.id, parsed.data.itemIds)
  res.json({ items })
})

router.post('/templates/:templateId/thumbnail', requireAdmin, upload.single('thumbnail'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'file_required' })
  const templateId = getTemplateIdFromParams(req)
  const template = await findTopicById(templateId)
  if (!template) return res.status(404).json({ error: 'not_found' })

  const optimized = await writeOptimizedImage({
    file: req.file,
    directory: 'topics',
    width: 1280,
    height: 1280,
    fit: 'inside',
    quality: 84,
  })

  const updated = await updateTopicThumbnail(templateId, optimized.src)
  res.json({ template: updated })
})

router.post('/templates/:templateId/images', requireAdmin, upload.array('images', 100), async (req, res) => {
  const files = Array.isArray(req.files) ? req.files : []
  if (!files.length) return res.status(400).json({ error: 'file_required' })
  const templateId = getTemplateIdFromParams(req)
  const template = await findTopicById(templateId)
  if (!template) return res.status(404).json({ error: 'not_found' })

  const labelsRaw = req.body?.labels
  const labels = Array.isArray(labelsRaw) ? labelsRaw : labelsRaw ? [labelsRaw] : []
  const normalizedLabels = labels.map((label) => (typeof label === 'string' ? label.trim().slice(0, 60) : ''))
  if (normalizedLabels.some((label) => label.length > 60)) return res.status(400).json({ error: 'bad_request' })
  const totalBytes = files.reduce((sum, file) => sum + Number(file?.size || 0), 0)

  console.info('[admin] template image upload start', {
    templateId: template.id,
    fileCount: files.length,
    totalBytes,
    fileNames: files.map((file) => file.originalname),
  })

  try {
    const items = await Promise.all(
      files.map(async (file, index) => {
        const optimized = await writeOptimizedImage({
          file,
          directory: 'topics',
          width: 512,
          height: 512,
          fit: 'inside',
          quality: 84,
        })

        return createTopicItem({
          id: nanoid(),
          topicId: template.id,
          src: optimized.src,
          label: normalizedLabels[index] || buildItemLabelFromFilename(file),
        })
      })
    )

    console.info('[admin] template image upload success', {
      templateId: template.id,
      fileCount: items.length,
      totalBytes,
    })

    res.json({ item: items[0], items })
  } catch (error) {
    console.error('[admin] template image upload failed', {
      templateId: template.id,
      fileCount: files.length,
      totalBytes,
      message: error?.message || 'upload_failed',
      code: error?.code || '',
      stack: error?.stack || '',
    })
    res.status(500).json({
      error: 'template_image_upload_failed',
      detail: error?.message || 'upload_failed',
    })
  }
})

router.delete('/templates/:templateId/items/:itemId', requireAdmin, async (req, res) => {
  const template = await findTopicById(getTemplateIdFromParams(req))
  if (!template) return res.status(404).json({ error: 'not_found' })
  await deleteTopicItem(req.params.itemId)
  res.json({ ok: true })
})

router.get('/templates/:templateId/items/:itemId/usage', requireAdmin, async (req, res) => {
  const template = await findTopicById(getTemplateIdFromParams(req))
  if (!template) return res.status(404).json({ error: 'not_found' })
  const item = await findTopicItemById(req.params.itemId)
  if (!item || item.topicId !== template.id) return res.status(404).json({ error: 'not_found' })
  const usage = await countTierListsUsingTopicItem(req.params.itemId)
  res.json({ usage })
})

router.patch('/templates/:templateId/items/:itemId', requireAdmin, async (req, res) => {
  const schema = z.object({
    label: z.string().trim().min(1).max(60),
    tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const template = await findTopicById(getTemplateIdFromParams(req))
  if (!template) return res.status(404).json({ error: 'not_found' })

  const updated = await updateTopicItemMeta(req.params.itemId, {
    label: parsed.data.label,
    tags: normalizeRouteTags(parsed.data.tags),
  })
  if (!updated || updated.topicId !== template.id) return res.status(404).json({ error: 'not_found' })
  res.json({ item: updated })
})

router.delete('/templates/:templateId', requireAdmin, async (req, res) => {
  const templateId = getTemplateIdFromParams(req)
  const template = await findTopicById(templateId)
  if (!template) return res.status(404).json({ error: 'not_found' })
  await deleteTopic(templateId)
  res.json({ ok: true })
})

router.patch('/custom-items/:itemId/label', requireAdmin, async (req, res) => {
  const schema = z.object({
    label: z.string().trim().min(1).max(60),
    tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
    sourceType: z.enum(['template', 'user', 'asset']).optional().default('user'),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const itemId = req.params.itemId
  if (itemId.startsWith('asset:')) {
    const updated = await updateImageAssetLabel(itemId.slice(6), parsed.data.label)
    if (!updated) return res.status(404).json({ error: 'not_found' })
    return res.json({ item: updated })
  }

  if (parsed.data.sourceType === 'template') {
    const updated = await updateTopicItemMeta(itemId, {
      label: parsed.data.label,
      tags: normalizeRouteTags(parsed.data.tags),
    })
    if (!updated) return res.status(404).json({ error: 'not_found' })
    return res.json({ item: updated })
  }

  const updated = await updateCustomItemMeta(itemId, {
    label: parsed.data.label,
    tags: normalizeRouteTags(parsed.data.tags),
  })
  if (!updated) return res.status(404).json({ error: 'not_found' })
  return res.json({ item: updated })
})

router.get('/custom-items', requireAdmin, async (req, res) => {
  const schema = z.object({
    q: z.string().trim().max(120).optional().default(''),
    page: z.coerce.number().int().min(1).optional().default(1),
    limit: z.coerce.number().int().min(1).max(200).optional().default(50),
    collapseShared: z
      .union([z.string(), z.boolean(), z.number()])
      .optional()
      .transform((value) => value === true || value === 1 || value === '1' || value === 'true'),
    filter: z
      .enum(['library', 'all', 'user', 'template', 'asset', 'thumbnail', 'avatar', 'unused', 'unused-user', 'replaced-user', 'unused-admin'])
      .optional()
      .default('library'),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const result = await listCustomItems({
    queryText: parsed.data.q,
    page: parsed.data.page,
    limit: parsed.data.limit,
    filterMode: parsed.data.filter,
    collapseShared: parsed.data.collapseShared,
  })
  res.json(result)
})

router.get('/tierlists', requireAdmin, async (req, res) => {
  const schema = z.object({
    q: z.string().trim().max(120).optional().default(''),
    topicId: z.string().trim().max(120).optional().default(''),
    sort: z.enum(['recent', 'created', 'favorites']).optional().default('recent'),
    minFavorites: z.coerce.number().int().min(0).max(1000000).optional().default(0),
    page: z.coerce.number().int().min(1).optional().default(1),
    limit: z.coerce.number().int().min(1).max(200).optional().default(50),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const result = await listAdminTierLists({
    queryText: parsed.data.q,
    topicId: parsed.data.topicId,
    sort: parsed.data.sort,
    minFavorites: parsed.data.minFavorites,
    page: parsed.data.page,
    limit: parsed.data.limit,
    currentUserId: req.session?.userId || '',
  })
  res.json(result)
})

router.get('/tierlists/stats', requireAdmin, async (req, res) => {
  const schema = z.object({
    q: z.string().trim().max(120).optional().default(''),
    topicId: z.string().trim().max(120).optional().default(''),
    minFavorites: z.coerce.number().int().min(0).max(1000000).optional().default(0),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const result = await summarizeAdminTierLists({
    queryText: parsed.data.q,
    topicId: parsed.data.topicId,
    minFavorites: parsed.data.minFavorites,
  })
  res.json(result)
})

router.patch('/tierlists/:tierListId/featured', requireAdmin, async (req, res) => {
  const schema = z.object({
    isFeatured: z.boolean(),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const tierList = await findTierListById(req.params.tierListId, req.session?.userId || '')
  if (!tierList) return res.status(404).json({ error: 'not_found' })
  if (parsed.data.isFeatured && !tierList.isPublic) return res.status(400).json({ error: 'public_tierlist_required' })

  const updated = await updateTierListFeaturedStatus({
    id: tierList.id,
    isFeatured: parsed.data.isFeatured,
    adminUserId: req.session.userId,
  })
  res.json({ tierList: updated })
})

router.get('/template-requests', requireAdmin, async (req, res) => {
  const requests = await listAdminTemplateRequests({ statuses: ['pending', 'reviewing'] })
  res.json({ requests })
})

router.get('/image-assets/orphans', requireAdmin, async (req, res) => {
  const schema = z.object({
    limit: z.coerce.number().int().min(1).max(500).optional().default(100),
    minAgeHours: z.coerce.number().min(0).max(24 * 365).optional().default(24),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const assets = await listUnusedImageAssets(parsed.data)
  res.json({ assets })
})

async function removeImageAssetFiles(assets) {
  await Promise.all(
    (assets || []).map(async (asset) => {
      if (!asset?.src || !asset.src.startsWith('/uploads/')) return
      const absolutePath = path.join(__dirname, '..', '..', asset.src.replace(/^\//, ''))
      try {
        await fs.unlink(absolutePath)
      } catch (error) {
        if (error?.code !== 'ENOENT') throw error
      }
    })
  )
}

router.post('/image-assets/cleanup', requireAdmin, async (req, res) => {
  const schema = z.object({
    limit: z.coerce.number().int().min(1).max(500).optional().default(100),
    minAgeHours: z.coerce.number().min(0).max(24 * 365).optional().default(24),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const assets = await listUnusedImageAssets(parsed.data)
  const deleted = await deleteImageAssets(assets.map((asset) => asset.id))
  await removeImageAssetFiles(deleted)
  res.json({ deletedCount: deleted.length, assets: deleted })
})

router.get('/image-assets/stats', requireAdmin, async (req, res) => {
  const schema = z.object({
    month: z.string().regex(/^\d{4}-\d{2}$/).optional(),
    limit: z.coerce.number().int().min(1).max(24).optional().default(12),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const filters = { month: parsed.data.month }
  const [stats, recentJobs] = await Promise.all([
    getImageAssetStats(filters),
    listRecentImageOptimizationJobs(parsed.data.limit, filters),
  ])
  res.json({
    stats,
    filters,
    queue: getImageOptimizationQueueState(),
    recentJobs,
  })
})

router.post('/image-assets/stats/reset', requireAdmin, async (req, res) => {
  const schema = z.object({
    month: z.string().regex(/^\d{4}-\d{2}$/).optional().nullable(),
  })
  const parsed = schema.safeParse(req.body || {})
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const deletedCount = await clearImageOptimizationJobs({ month: parsed.data.month || undefined })
  res.json({ deletedCount })
})

router.post('/image-assets/missing/cleanup', requireAdmin, async (req, res) => {
  const result = await cleanupMissingUploadReferences()
  res.json({ result })
})

async function removeUploadFiles(srcs) {
  await Promise.all(
    (srcs || []).map(async (src) => {
      if (!src || !src.startsWith('/uploads/')) return
      const absolutePath = path.join(__dirname, '..', '..', src.replace(/^\//, ''))
      try {
        await fs.unlink(absolutePath)
      } catch (e) {
        if (e?.code !== 'ENOENT') throw e
      }
    })
  )
}

async function removeCustomItemFiles(items) {
  await Promise.all(
    items.map(async (item) => {
      if (!item?.src || !item.src.startsWith('/uploads/custom/')) return
      const absolutePath = path.join(__dirname, '..', '..', item.src.replace(/^\//, ''))
      try {
        await fs.unlink(absolutePath)
      } catch (e) {
        if (e?.code !== 'ENOENT') throw e
      }
    })
  )
}

async function removeUnreferencedCustomItemFiles(items) {
  const referencedSrcs = new Set(await listReferencedUploadSources())
  const removableItems = (items || []).filter((item) => item?.src && !referencedSrcs.has(item.src))
  await removeCustomItemFiles(removableItems)
}

async function promoteLibraryItemToTemplateItem({ item, templateId }) {
  return createTopicItem({
    id: nanoid(),
    topicId: templateId,
    src: item.src || '',
    label: item.label,
    tags: item.tags || [],
  })
}

async function findLibraryItemForReplacement(itemId, sourceType = '') {
  const normalizedId = String(itemId || '').trim()
  const normalizedSourceType = String(sourceType || '').trim()
  if (!normalizedId) return null

  if (normalizedId.startsWith('asset:') || normalizedSourceType === 'asset') {
    const assetId = normalizedId.startsWith('asset:') ? normalizedId.slice(6) : normalizedId
    const asset = await findImageAssetById(assetId)
    if (!asset) return null
    return {
      id: `asset:${asset.id}`,
      sourceType: 'asset',
      src: asset.src || '',
      label: asset.labelOverride || buildItemLabelFromSrc(asset.src),
      tags: [],
    }
  }

  if (normalizedSourceType === 'template') {
    const item = await findTopicItemById(normalizedId)
    if (!item) return null
    return {
      id: item.id,
      sourceType: 'template',
      src: item.src || '',
      label: item.label || buildItemLabelFromSrc(item.src),
      tags: item.tags || [],
    }
  }

  const customItem = await findCustomItemById(normalizedId)
  if (customItem) {
    return {
      id: customItem.id,
      sourceType: 'user',
      src: customItem.src || '',
      label: customItem.label || buildItemLabelFromSrc(customItem.src),
      tags: customItem.tags || [],
    }
  }

  const templateItem = await findTopicItemById(normalizedId)
  if (templateItem) {
    return {
      id: templateItem.id,
      sourceType: 'template',
      src: templateItem.src || '',
      label: templateItem.label || buildItemLabelFromSrc(templateItem.src),
      tags: templateItem.tags || [],
    }
  }

  return null
}

async function copyUploadIntoTopicAsset(src) {
  if (typeof src !== 'string') return ''
  const raw = src.trim()
  if (!raw) return ''

  if (raw.startsWith('/uploads/')) {
    if (raw.startsWith('/uploads/assets/')) return raw
    return raw
  }

  try {
    const url = new URL(raw)
    if (url.pathname.startsWith('/uploads/')) {
      return url.pathname
    }
  } catch (error) {
    return raw
  }

  return raw
}

function uniqueTierListPoolItems(tierList) {
  const seen = new Set()
  return (tierList?.pool || []).filter((item) => {
    if (!item?.id || seen.has(item.id)) return false
    seen.add(item.id)
    return true
  })
}

async function promoteTierListItemsToTemplate({ tierList, templateId, itemIds = [] }) {
  const allowedIds = new Set((itemIds || []).filter(Boolean))
  const sourceItems = uniqueTierListPoolItems(tierList).filter((item) => item.origin === 'custom')
  const itemsToCopy = allowedIds.size ? sourceItems.filter((item) => allowedIds.has(item.id)) : sourceItems
  const createdItems = []

  for (const item of itemsToCopy) {
    const copiedSrc = await copyUploadIntoTopicAsset(item.src)
    createdItems.push(
      await createTopicItem({
        id: nanoid(),
        topicId: templateId,
        src: copiedSrc,
        label: item.label,
      })
    )
  }

  return createdItems
}

async function promoteSnapshotItemsToTemplate({ items, templateId }) {
  const existingItems = await listTopicItems(templateId)
  const existingSrcs = new Set(
    existingItems
      .map((item) => (typeof item?.src === 'string' ? item.src.trim() : ''))
      .filter(Boolean)
  )
  const createdItems = []

  for (const item of items || []) {
    const copiedSrc = await copyUploadIntoTopicAsset(item.src)
    if (!copiedSrc || existingSrcs.has(copiedSrc)) continue
    createdItems.push(
      await createTopicItem({
        id: nanoid(),
        topicId: templateId,
        src: copiedSrc,
        label: item.label,
      })
    )
    existingSrcs.add(copiedSrc)
  }

  return createdItems
}

function pickTemplateRequestItems(templateRequest, itemIds = [], itemLabels = {}, itemSrcs = []) {
  const requestedIds = new Set((itemIds || []).filter(Boolean))
  const requestedSrcs = new Set((itemSrcs || []).filter((src) => typeof src === 'string' && src.trim()).map((src) => src.trim()))
  const items = Array.isArray(templateRequest?.items) ? templateRequest.items : []
  const filtered =
    requestedIds.size || requestedSrcs.size
      ? items.filter((item) => (item?.id && requestedIds.has(item.id)) || (typeof item?.src === 'string' && requestedSrcs.has(item.src.trim())))
      : items
  return filtered
    .filter((item) => typeof item?.src === 'string' && item.src.trim())
    .map((item) => {
      const draftLabel =
        typeof itemLabels?.[item.id] === 'string' && itemLabels[item.id].trim()
          ? itemLabels[item.id].trim().slice(0, 60)
          : typeof item?.label === 'string' && item.label.trim()
            ? item.label.trim().slice(0, 60)
            : buildItemLabelFromSrc(item.src)

      return {
        ...item,
        src: item.src.trim(),
        label: draftLabel,
      }
    })
}

async function createTemplateFromTierList({ tierList, templateSlug, templateName }) {
  const template = await createTopic({ slug: templateSlug, name: templateName, isPublic: false })
  if (tierList.thumbnailSrc) {
    const copiedThumb = await copyUploadIntoTopicAsset(tierList.thumbnailSrc)
    await updateTopicThumbnail(template.id, copiedThumb)
  }

  const createdItems = []
  for (const item of uniqueTierListPoolItems(tierList)) {
    const copiedSrc = await copyUploadIntoTopicAsset(item.src)
    createdItems.push(
      await createTopicItem({
        id: nanoid(),
        topicId: template.id,
        src: copiedSrc,
        label: item.label,
      })
    )
  }

  return { template: await findTopicById(template.id), items: createdItems }
}

async function createTemplateFromRequest({ templateRequest, templateSlug, templateName }) {
  const template = await createTopic({ slug: templateSlug, name: templateName, isPublic: false })

  if (templateRequest.thumbnailSrc) {
    const copiedThumb = await copyUploadIntoTopicAsset(templateRequest.thumbnailSrc)
    await updateTopicThumbnail(template.id, copiedThumb)
  }

  const items = await promoteSnapshotItemsToTemplate({
    items: templateRequest.items || [],
    templateId: template.id,
  })

  return { template: await findTopicById(template.id), items }
}

router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
  const result = await listCustomItems({ page: 1, limit: 10000, filterMode: 'all' })
  const target = result.items.find((item) => item.id === req.params.itemId)
  if (!target) return res.status(404).json({ error: 'not_found' })
  if (target.sourceType === 'asset' || String(target.id || '').startsWith('asset:')) {
    const assetId = String(target.id).slice('asset:'.length)
    const asset = await findImageAssetById(assetId)
    if (!asset) return res.status(404).json({ error: 'not_found' })
    await deleteImageAssets([assetId])
    await removeUploadFiles([asset.src])
    return res.json({ ok: true, sourceType: 'asset' })
  }

  if (target.sourceType === 'template') {
    if (String(target.id || '').startsWith('asset:')) {
      const assetId = String(target.id).slice('asset:'.length)
      const asset = await findImageAssetById(assetId)
      if (!asset) return res.status(404).json({ error: 'not_found' })
      await deleteImageAssets([assetId])
      await removeUploadFiles([asset.src])
      return res.json({ ok: true, sourceType: 'template-asset' })
    }

    await deleteTopicItem(target.id)
    return res.json({ ok: true, sourceType: 'template' })
  }

  const canDeleteReplacedUserItem = target.sourceType === 'user' && !!target.replacedAt
  if (!target.canDelete) return res.status(409).json({ error: 'item_locked' })
  if (!canDeleteReplacedUserItem && target.linkedTemplates.length > 0) return res.status(409).json({ error: 'item_linked' })
  if (!canDeleteReplacedUserItem && target.usageCount > 0) return res.status(409).json({ error: 'item_in_use' })

  const items = await findCustomItemsByIds([target.id])
  await deleteCustomItems([target.id])
  await removeUnreferencedCustomItemFiles(items)
  res.json({ ok: true, sourceType: 'user' })
})

router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicId: z.string().min(1),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const template = await findTopicById(parsed.data.topicId)
  if (!template) return res.status(404).json({ error: 'topic_not_found' })

  const customItem = await findCustomItemById(req.params.itemId)
  const templateItem = customItem ? null : await findTopicItemById(req.params.itemId)
  const assetItemId = String(req.params.itemId || '')
  const imageAsset = !customItem && !templateItem && assetItemId.startsWith('asset:') ? await findImageAssetById(assetItemId.slice(6)) : null
  const sourceItem =
    customItem ||
    templateItem ||
    (imageAsset
      ? {
          src: imageAsset.src || '',
          label: imageAsset.labelOverride || path.basename(imageAsset.src || '', path.extname(imageAsset.src || '')) || 'item',
        }
      : null)
  if (!sourceItem) return res.status(404).json({ error: 'not_found' })

  const item = await promoteLibraryItemToTemplateItem({ item: sourceItem, templateId: template.id })
  res.json({ item })
})

router.post('/custom-items/:itemId/unlink-template', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicId: z.string().trim().min(1),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const template = await findTopicById(parsed.data.topicId)
  if (!template) return res.status(404).json({ error: 'topic_not_found' })

  const sourceItem = await findLibraryItemForReplacement(req.params.itemId)
  if (!sourceItem?.src) return res.status(404).json({ error: 'not_found' })

  const templateItems = await listTopicItems(template.id)
  const matchedItems = templateItems.filter((item) => item?.src === sourceItem.src)
  if (!matchedItems.length) return res.status(404).json({ error: 'linked_template_item_not_found' })

  await Promise.all(matchedItems.map((item) => deleteTopicItem(item.id)))
  res.json({ ok: true, deletedCount: matchedItems.length, topicId: template.id, src: sourceItem.src })
})

router.post('/custom-items/:itemId/replace', requireAdmin, async (req, res) => {
  const schema = z.object({
    targetItemId: z.string().trim().min(1),
    targetSourceType: z.enum(['template', 'user', 'asset']).optional().default('user'),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const sourceItem = await findLibraryItemForReplacement(req.params.itemId)
  if (!sourceItem?.src) return res.status(404).json({ error: 'source_not_found' })
  if (sourceItem.sourceType !== 'user') return res.status(400).json({ error: 'user_item_required' })

  const targetItem = await findLibraryItemForReplacement(parsed.data.targetItemId, parsed.data.targetSourceType)
  if (!targetItem?.src) return res.status(404).json({ error: 'target_not_found' })
  if (sourceItem.src === targetItem.src && (sourceItem.label || '') === (targetItem.label || '')) {
    return res.status(409).json({ error: 'same_target' })
  }

  const result = await replaceUploadSourceReferences({
    fromSrc: sourceItem.src,
    toSrc: targetItem.src,
    toLabel: targetItem.label || '',
    updateCustomItemsBySrc: false,
  })
  const displayResult = await updateCustomItemDisplayReferences({
    itemId: sourceItem.id,
    src: targetItem.src,
    label: targetItem.label || '',
  })
  await markCustomItemReplaced({
    itemId: sourceItem.id,
    replacedByItemId: targetItem.id || '',
    replacedBySrc: targetItem.src || '',
    replacedByLabel: targetItem.label || '',
  })

  res.json({
    ok: true,
    updatedRows: (result.updatedRows || 0) + (displayResult.updatedRows || 0),
    sourceItem,
    targetItem,
  })
})

router.post('/custom-items/:itemId/restore', requireAdmin, async (req, res) => {
  const sourceItem = await findCustomItemById(req.params.itemId)
  if (!sourceItem?.id) return res.status(404).json({ error: 'not_found' })
  if (!sourceItem.replacedAt) return res.status(409).json({ error: 'not_replaced' })

  const restored = await updateCustomItemDisplayReferences({
    itemId: sourceItem.id,
    src: sourceItem.src,
    label: sourceItem.label || '',
  })
  await clearCustomItemReplacement(sourceItem.id)

  res.json({
    ok: true,
    restoredRows: restored.updatedRows || 0,
    item: await findCustomItemById(sourceItem.id),
  })
})

router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicId: z.string().min(1),
    itemIds: z.array(z.string().min(1)).optional().default([]),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const template = await findTopicById(parsed.data.topicId)
  if (!template) return res.status(404).json({ error: 'topic_not_found' })

  const tierList = await findTierListById(req.params.tierListId)
  if (!tierList) return res.status(404).json({ error: 'not_found' })

  const items = await promoteTierListItemsToTemplate({
    tierList,
    templateId: template.id,
    itemIds: parsed.data.itemIds,
  })
  res.json({ items })
})

router.post('/tierlists/:tierListId/create-template', requireAdmin, async (req, res) => {
  const schema = z.object({
    slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
    name: z.string().trim().min(1).max(120),
    itemIds: z.array(z.string().min(1)).optional().default([]),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const exists = await findTopicBySlug(parsed.data.slug)
  if (exists) return res.status(409).json({ error: 'topic_slug_taken' })

  const tierList = await findTierListById(req.params.tierListId)
  if (!tierList) return res.status(404).json({ error: 'not_found' })

  try {
    const result = await createTemplateFromTierList({
      tierList: {
        ...tierList,
        pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
      },
      templateSlug: parsed.data.slug,
      templateName: parsed.data.name,
    })
    return res.json(result)
  } catch (error) {
    if (error?.code === 'TOPIC_SLUG_INVALID') return res.status(400).json({ error: 'topic_slug_invalid' })
    if (error?.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'topic_slug_taken' })
    throw error
  }
})

router.patch('/tierlists/:tierListId', requireAdmin, async (req, res) => {
  const schema = z.object({
    title: z.string().trim().min(1).max(120),
    description: z.string().max(500).optional().default(''),
    isPublic: z.boolean(),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const tierList = await findTierListById(req.params.tierListId)
  if (!tierList) return res.status(404).json({ error: 'not_found' })

  const updated = await updateAdminTierListMeta({
    id: tierList.id,
    title: parsed.data.title,
    description: parsed.data.description || '',
    isPublic: parsed.data.isPublic,
  })
  res.json({ tierList: updated })
})

router.delete('/tierlists/:tierListId', requireAdmin, async (req, res) => {
  const tierList = await findTierListById(req.params.tierListId)
  if (!tierList) return res.status(404).json({ error: 'not_found' })
  await deleteTierList(tierList.id)
  res.json({ ok: true })
})

router.post('/template-requests/:requestId/approve', requireAdmin, async (req, res) => {
  const templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (templateRequest.status !== 'pending') return res.status(409).json({ error: 'request_already_handled' })

  if (templateRequest.type === 'update') {
    const targetTopicId = templateRequest.targetTopicId || templateRequest.sourceTopicId
    const template = await findTopicById(targetTopicId)
    if (!template) return res.status(404).json({ error: 'topic_not_found' })

    const items = await promoteSnapshotItemsToTemplate({
      items: templateRequest.items || [],
      templateId: template.id,
    })
    const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
    return res.json({ request, items })
  }

  const schema = z.object({
    slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
    name: z.string().trim().min(1).max(120),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const exists = await findTopicBySlug(parsed.data.slug)
  if (exists) return res.status(409).json({ error: 'topic_slug_taken' })

  let result
  try {
    result = await createTemplateFromRequest({
      templateRequest,
      templateSlug: parsed.data.slug,
      templateName: parsed.data.name,
    })
  } catch (error) {
    if (error?.code === 'TOPIC_SLUG_INVALID') return res.status(400).json({ error: 'topic_slug_invalid' })
    if (error?.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'topic_slug_taken' })
    throw error
  }
  const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
  res.json({ request, ...result })
})

router.post('/template-requests/:requestId/review', requireAdmin, async (req, res) => {
  let templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (templateRequest.status === 'completed' || templateRequest.status === 'rejected' || templateRequest.status === 'approved') {
    return res.status(409).json({ error: 'request_already_handled' })
  }

  if (templateRequest.type === 'create' && templateRequest.targetTopicId && !templateRequest.targetTopicName) {
    templateRequest = await updateTemplateRequestTargetTopic({
      id: templateRequest.id,
      targetTopicId: '',
    })
  }

  if (templateRequest.status === 'reviewing') {
    return res.json({ request: templateRequest })
  }

  const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'reviewing' })
  res.json({ request })
})

router.post('/template-requests/:requestId/link-template', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicId: z.string().trim().min(1).max(120),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (templateRequest.type !== 'create') return res.status(409).json({ error: 'create_request_required' })
  if (!['pending', 'reviewing'].includes(templateRequest.status)) {
    return res.status(409).json({ error: 'request_already_handled' })
  }

  const template = await findTopicById(parsed.data.topicId)
  if (!template) return res.status(404).json({ error: 'topic_not_found' })

  const request = await updateTemplateRequestTargetTopic({
    id: templateRequest.id,
    targetTopicId: template.id,
  })
  res.json({ request })
})

router.post('/template-requests/:requestId/promote-items', requireAdmin, async (req, res) => {
  const schema = z.object({
    topicId: z.string().trim().min(1).max(120),
    itemIds: z.array(z.string().min(1)).optional().default([]),
    itemSrcs: z.array(z.string().min(1)).optional().default([]),
    itemLabels: z.record(z.string(), z.string().min(1).max(60)).optional().default({}),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (!['pending', 'reviewing'].includes(templateRequest.status)) {
    return res.status(409).json({ error: 'request_already_handled' })
  }

  const template = await findTopicById(parsed.data.topicId)
  if (!template) return res.status(404).json({ error: 'topic_not_found' })

  const promotableItems = pickTemplateRequestItems(templateRequest, parsed.data.itemIds, parsed.data.itemLabels, parsed.data.itemSrcs)
  if (!promotableItems.length) {
    return res.status(400).json({ error: 'no_items_selected' })
  }

  let items = []
  try {
    items = await promoteSnapshotItemsToTemplate({
      items: promotableItems,
      templateId: template.id,
    })
  } catch (error) {
    console.error('[admin] template request promote-items failed', {
      requestId: templateRequest.id,
      topicId: template.id,
      itemCount: promotableItems.length,
      message: error?.message || 'unknown_error',
      code: error?.code || '',
      stack: error?.stack || '',
    })
    return res.status(500).json({
      error: 'promote_items_failed',
      detail: error?.message || 'unknown_error',
      code: error?.code || '',
    })
  }

  const request =
    templateRequest.status === 'reviewing'
      ? templateRequest
      : await updateTemplateRequestStatus({ id: templateRequest.id, status: 'reviewing' })

  res.json({ request, items })
})

router.post('/template-requests/:requestId/complete', requireAdmin, async (req, res) => {
  const templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (templateRequest.status === 'completed') return res.json({ request: templateRequest })
  if (templateRequest.status === 'rejected' || templateRequest.status === 'approved') {
    return res.status(409).json({ error: 'request_already_handled' })
  }

  const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'completed' })
  res.json({ request })
})

router.post('/template-requests/:requestId/reject', requireAdmin, async (req, res) => {
  const templateRequest = await findTemplateRequestById(req.params.requestId)
  if (!templateRequest) return res.status(404).json({ error: 'not_found' })
  if (templateRequest.status !== 'pending') return res.status(409).json({ error: 'request_already_handled' })

  const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'rejected' })
  res.json({ request })
})

router.delete('/custom-items', requireAdmin, async (req, res) => {
  const schema = z.object({
    q: z.string().trim().max(120).optional().default(''),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const result = await listCustomItems({
    queryText: parsed.data.q,
    page: 1,
    limit: 10000,
    filterMode: 'unused',
  })
  const customItems = result.items.filter((item) => item?.sourceType === 'user')
  const assetItems = result.items.filter((item) => item?.sourceType === 'asset' || item?.isAssetLibraryItem)
  const customItemIds = customItems.map((item) => item.id)
  const assetIds = assetItems
    .map((item) => String(item.id || ''))
    .filter((id) => id.startsWith('asset:'))
    .map((id) => id.slice('asset:'.length))

  await deleteCustomItems(customItemIds)
  await removeUnreferencedCustomItemFiles(customItems)

  const deletedAssets = await deleteImageAssets(assetIds)
  await removeImageAssetFiles(deletedAssets)

  res.json({ ok: true, deletedCount: customItemIds.length + deletedAssets.length })
})

router.get('/users', requireAdmin, async (req, res) => {
  const schema = z.object({
    q: z.string().trim().max(120).optional().default(''),
    sort: z.enum(['recent', 'lastLogin', 'created', 'tierlists', 'followers', 'favorites']).optional().default('recent'),
    direction: z.enum(['asc', 'desc']).optional().default('desc'),
  })
  const parsed = schema.safeParse(req.query)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const [users, primaryAdmin] = await Promise.all([
    listUsers({ queryText: parsed.data.q, sort: parsed.data.sort, direction: parsed.data.direction }),
    findPrimaryAdminUser(),
  ])
  res.json({ users: users.map((user) => decorateAdminUser(user, primaryAdmin)) })
})

router.patch('/users/:userId', requireAdmin, async (req, res) => {
  const schema = z.object({
    email: z.string().email(),
    nickname: z.string().trim().max(40).default(''),
    isAdmin: z.boolean(),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const { targetUser, actingUser, primaryAdmin } = await getAdminUserContext(req.params.userId, req.session.userId)
  if (!targetUser) return res.status(404).json({ error: 'not_found' })

  const actingIsPrimaryAdmin = canManageAdminRole(actingUser, primaryAdmin)
  const targetIsPrimaryAdmin = primaryAdmin?.id === targetUser.id
  const roleChanged = parsed.data.isAdmin !== !!targetUser.isAdmin

  if (req.params.userId === req.session.userId && !parsed.data.isAdmin) {
    return res.status(400).json({ error: 'self_admin_required' })
  }
  if (targetIsPrimaryAdmin && !actingIsPrimaryAdmin) {
    return res.status(403).json({ error: 'primary_admin_protected' })
  }
  if (targetIsPrimaryAdmin && !parsed.data.isAdmin) {
    return res.status(400).json({ error: 'primary_admin_required' })
  }
  if (roleChanged && !actingIsPrimaryAdmin) {
    return res.status(403).json({ error: 'primary_admin_only' })
  }

  const duplicateEmail = await findUserByEmail(parsed.data.email)
  if (duplicateEmail && duplicateEmail.id !== targetUser.id) {
    return res.status(409).json({ error: 'email_taken' })
  }
  const duplicateNickname = await findUserByNickname(parsed.data.nickname, targetUser.id)
  if (duplicateNickname) {
    return res.status(409).json({ error: 'nickname_taken' })
  }

  try {
    const updated = await adminUpdateUser({
      id: targetUser.id,
      email: parsed.data.email,
      nickname: parsed.data.nickname,
      isAdmin: parsed.data.isAdmin,
    })
    res.json({ user: decorateAdminUser(updated, primaryAdmin) })
  } catch (e) {
    if (e && e.code === 'ER_DUP_ENTRY') {
      return res.status(409).json({ error: 'email_taken' })
    }
    throw e
  }
})

router.post('/users/:userId/avatar', requireAdmin, avatarUpload.single('avatar'), async (req, res) => {
  const schema = z.object({
    removeAvatar: z.union([z.literal('1'), z.undefined()]).optional(),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const { targetUser, actingUser, primaryAdmin } = await getAdminUserContext(req.params.userId, req.session.userId)
  if (!targetUser) return res.status(404).json({ error: 'not_found' })
  if (primaryAdmin?.id === targetUser.id && !canManageAdminRole(actingUser, primaryAdmin)) {
    return res.status(403).json({ error: 'primary_admin_protected' })
  }

  const optimized = req.file
    ? await writeOptimizedImage({
        file: req.file,
        directory: 'avatars',
        width: 512,
        height: 512,
        fit: 'cover',
        quality: 82,
      })
    : null
  const shouldRemoveAvatar = parsed.data.removeAvatar === '1'
  const nextAvatarSrc = shouldRemoveAvatar ? '' : optimized?.src || targetUser.avatarSrc || ''
  const updated = await adminUpdateUser({
    id: targetUser.id,
    email: targetUser.email,
    nickname: targetUser.nickname || '',
    isAdmin: !!targetUser.isAdmin,
    avatarSrc: nextAvatarSrc,
  })

  res.json({ user: decorateAdminUser(updated, primaryAdmin) })
})

router.delete('/users/:userId', requireAdmin, async (req, res) => {
  if (req.params.userId === req.session.userId) {
    return res.status(400).json({ error: 'cannot_delete_self' })
  }

  const { targetUser, actingUser, primaryAdmin } = await getAdminUserContext(req.params.userId, req.session.userId)
  if (!targetUser) return res.status(404).json({ error: 'not_found' })

  const actingIsPrimaryAdmin = canManageAdminRole(actingUser, primaryAdmin)
  const targetIsPrimaryAdmin = primaryAdmin?.id === targetUser.id
  if (targetIsPrimaryAdmin) {
    return res.status(400).json({ error: 'cannot_delete_primary_admin' })
  }
  if (targetUser.isAdmin && !actingIsPrimaryAdmin) {
    return res.status(403).json({ error: 'primary_admin_only' })
  }

  await adminDeleteUser(targetUser.id)
  res.json({ ok: true })
})

router.patch('/users/:userId/password', requireAdmin, async (req, res) => {
  const schema = z.object({
    password: z.string().min(6).max(120),
  })
  const parsed = schema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const { targetUser, actingUser, primaryAdmin } = await getAdminUserContext(req.params.userId, req.session.userId)
  if (!targetUser) return res.status(404).json({ error: 'not_found' })
  if (primaryAdmin?.id === targetUser.id && !canManageAdminRole(actingUser, primaryAdmin)) {
    return res.status(403).json({ error: 'primary_admin_protected' })
  }

  const passwordHash = await bcrypt.hash(parsed.data.password, 10)
  await adminUpdateUserPassword({ id: targetUser.id, passwordHash })
  res.json({ ok: true })
})

module.exports = router
