const express = require('express')
const bcrypt = require('bcryptjs')
const crypto = require('crypto')
const { z } = require('zod')
const { nanoid } = require('nanoid')
const multer = require('multer')
const {
  countUsers,
  findUserByEmail,
  findUserByNickname,
  findUserById,
  createUser,
  touchUserLastLoginAt,
  updateUserPassword,
  verifyUserEmail,
  createEmailVerificationToken,
  findEmailVerificationTokenByHash,
  consumeEmailVerificationToken,
  createPasswordResetToken,
  findPasswordResetTokenByHash,
  consumePasswordResetToken,
  updateUserProfile,
  findPrimaryAdminUser,
} = require('../db')
const { requireAuth } = require('../middleware/auth')
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
const { isMailerConfigured, sendEmailVerificationMail, sendPasswordResetMail } = require('../lib/mailer')
const { isReservedNickname } = require('../lib/user-validation')

const router = express.Router()
const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000

const signupSchema = z.object({
  email: z.string().email(),
  nickname: z.string().trim().min(2).max(40),
  password: z.string().min(6),
})

const verifyEmailSchema = z.object({
  token: z.string().min(16).max(256),
})

const resendVerificationSchema = z.object({
  email: z.string().email(),
})

const requestPasswordResetSchema = z.object({
  email: z.string().email(),
})

const confirmPasswordResetSchema = z.object({
  token: z.string().min(16).max(256),
  password: z.string().min(6),
})

const changePasswordSchema = z.object({
  currentPassword: z.string().min(6),
  nextPassword: z.string().min(6),
})

const profileSchema = z.object({
  nickname: z.string().trim().min(1).max(40),
  removeAvatar: z.union([z.string(), z.undefined()]).optional(),
})

function establishSession(req, user) {
  return new Promise((resolve, reject) => {
    req.session.regenerate((regenerateError) => {
      if (regenerateError) return reject(regenerateError)
      req.session.userId = user.id
      req.session.isAdmin = !!user.isAdmin
      req.session.save((saveError) => {
        if (saveError) return reject(saveError)
        resolve()
      })
    })
  })
}

async function serializeUser(user) {
  if (!user) return null
  const primaryAdmin = await findPrimaryAdminUser()
  const isPrimaryAdmin = !!user.isAdmin && primaryAdmin?.id === user.id

  return {
    id: user.id,
    email: user.email,
    nickname: user.nickname || '',
    isAdmin: !!user.isAdmin,
    isPrimaryAdmin,
    isOperator: !!user.isAdmin && !isPrimaryAdmin,
    role: isPrimaryAdmin ? 'owner' : user.isAdmin ? 'operator' : 'user',
    avatarSrc: user.avatarSrc || '',
    emailVerified: user.emailVerified !== false,
    createdAt: user.createdAt,
  }
}

function createRawToken() {
  return crypto.randomBytes(32).toString('hex')
}

function hashToken(token) {
  return crypto.createHash('sha256').update(String(token || '')).digest('hex')
}

function resolveAppOrigin(req) {
  const envOrigin = String(process.env.APP_ORIGIN || process.env.PUBLIC_APP_ORIGIN || '').trim()
  if (envOrigin) return envOrigin.replace(/\/+$/, '')

  const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim()
  const protocol = forwardedProto || req.protocol || 'http'
  const host = req.get('host')
  return host ? `${protocol}://${host}` : ''
}

async function issueEmailVerificationMail(req, user) {
  if (!isMailerConfigured()) {
    const error = new Error('mail_not_configured')
    error.code = 'mail_not_configured'
    throw error
  }

  const rawToken = createRawToken()
  await createEmailVerificationToken({
    id: nanoid(),
    userId: user.id,
    tokenHash: hashToken(rawToken),
    expiresAt: Date.now() + EMAIL_VERIFICATION_TTL_MS,
  })

  const appOrigin = resolveAppOrigin(req)
  const verificationUrl = `${appOrigin}/login?verifyToken=${encodeURIComponent(rawToken)}`
  await sendEmailVerificationMail({
    to: user.email,
    nickname: user.nickname,
    verificationUrl,
  })
}

async function issuePasswordResetMail(req, user) {
  if (!isMailerConfigured()) {
    const error = new Error('mail_not_configured')
    error.code = 'mail_not_configured'
    throw error
  }

  const rawToken = createRawToken()
  await createPasswordResetToken({
    id: nanoid(),
    userId: user.id,
    tokenHash: hashToken(rawToken),
    expiresAt: Date.now() + PASSWORD_RESET_TTL_MS,
  })

  const appOrigin = resolveAppOrigin(req)
  const resetUrl = `${appOrigin}/login?resetToken=${encodeURIComponent(rawToken)}`
  await sendPasswordResetMail({
    to: user.email,
    nickname: user.nickname,
    resetUrl,
  })
}

router.post('/signup', async (req, res) => {
  const parsed = signupSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const { email, nickname, password } = parsed.data
  const exists = await findUserByEmail(email)
  if (exists) return res.status(409).json({ error: 'email_taken' })
  if (isReservedNickname(nickname)) return res.status(400).json({ error: 'nickname_reserved' })
  const nicknameExists = await findUserByNickname(nickname)
  if (nicknameExists) return res.status(409).json({ error: 'nickname_taken' })

  const passwordHash = await bcrypt.hash(password, 10)
  const isAdmin = (await countUsers()) === 0
  if (!isAdmin && !isMailerConfigured()) {
    return res.status(503).json({ error: 'mail_not_configured' })
  }

  const user = await createUser({
    id: nanoid(),
    email,
    nickname,
    passwordHash,
    emailVerified: isAdmin,
    isAdmin,
  })

  if (!isAdmin) {
    try {
      await issueEmailVerificationMail(req, user)
      return res.json({
        user: null,
        verificationRequired: true,
        email: user.email,
      })
    } catch (err) {
      const statusCode = err?.code === 'mail_not_configured' ? 503 : 502
      return res.status(statusCode).json({ error: err?.code || 'mail_send_failed' })
    }
  }

  try {
    await establishSession(req, user)
    const touchedUser = await touchUserLastLoginAt(user.id)
    res.json({ user: await serializeUser(touchedUser || user), verificationRequired: false })
  } catch (err) {
    return res.status(500).json({ error: 'session_save_failed' })
  }
})

router.post('/login', async (req, res) => {
  const parsed = z.object({
    email: z.string().email(),
    password: z.string().min(6),
  }).safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const { email, password } = parsed.data
  const user = await findUserByEmail(email)
  if (!user) return res.status(401).json({ error: 'invalid_credentials' })

  const ok = await bcrypt.compare(password, user.passwordHash)
  if (!ok) return res.status(401).json({ error: 'invalid_credentials' })
  if (!user.emailVerified) return res.status(403).json({ error: 'email_unverified', email: user.email })

  try {
    await establishSession(req, user)
    const touchedUser = await touchUserLastLoginAt(user.id)
    res.json({ user: await serializeUser(touchedUser || user) })
  } catch (err) {
    return res.status(500).json({ error: 'session_save_failed' })
  }
})

router.post('/logout', async (req, res) => {
  if (!req.session) return res.json({ ok: true })
  req.session.destroy(() => res.json({ ok: true }))
})

router.get('/me', async (req, res) => {
  if (!req.session || !req.session.userId) return res.json({ user: null })
  const user = await touchUserLastLoginAt(req.session.userId)
  if (!user) return res.json({ user: null })
  res.json({ user: await serializeUser(user) })
})

router.get('/meta', async (req, res) => {
  res.json({ hasUsers: (await countUsers()) > 0 })
})

router.post('/email/verify', async (req, res) => {
  const parsed = verifyEmailSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const tokenRow = await findEmailVerificationTokenByHash(hashToken(parsed.data.token))
  if (!tokenRow || tokenRow.consumedAt || tokenRow.expiresAt < Date.now()) {
    return res.status(400).json({ error: 'invalid_or_expired_token' })
  }

  const user = await verifyUserEmail(tokenRow.userId)
  if (!user) return res.status(404).json({ error: 'not_found' })

  await consumeEmailVerificationToken(tokenRow.id)

  try {
    await establishSession(req, user)
    const touchedUser = await touchUserLastLoginAt(user.id)
    res.json({ user: await serializeUser(touchedUser || user) })
  } catch (err) {
    return res.status(500).json({ error: 'session_save_failed' })
  }
})

router.post('/email/resend', async (req, res) => {
  const parsed = resendVerificationSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const user = await findUserByEmail(parsed.data.email)
  if (!user || user.emailVerified) return res.json({ ok: true })

  try {
    await issueEmailVerificationMail(req, user)
    res.json({ ok: true })
  } catch (err) {
    const statusCode = err?.code === 'mail_not_configured' ? 503 : 502
    return res.status(statusCode).json({ error: err?.code || 'mail_send_failed' })
  }
})

router.post('/password-reset/request', async (req, res) => {
  const parsed = requestPasswordResetSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const user = await findUserByEmail(parsed.data.email)
  if (!user) return res.json({ ok: true })

  try {
    await issuePasswordResetMail(req, user)
    res.json({ ok: true })
  } catch (err) {
    const statusCode = err?.code === 'mail_not_configured' ? 503 : 502
    return res.status(statusCode).json({ error: err?.code || 'mail_send_failed' })
  }
})

router.post('/password-reset/confirm', async (req, res) => {
  const parsed = confirmPasswordResetSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const tokenRow = await findPasswordResetTokenByHash(hashToken(parsed.data.token))
  if (!tokenRow || tokenRow.consumedAt || tokenRow.expiresAt < Date.now()) {
    return res.status(400).json({ error: 'invalid_or_expired_token' })
  }

  const passwordHash = await bcrypt.hash(parsed.data.password, 10)
  const updatedUser = await updateUserPassword({ id: tokenRow.userId, passwordHash })
  if (!updatedUser) return res.status(404).json({ error: 'not_found' })

  const verifiedUser = updatedUser.emailVerified ? updatedUser : await verifyUserEmail(updatedUser.id)
  await consumePasswordResetToken(tokenRow.id)

  try {
    await establishSession(req, verifiedUser)
    const touchedUser = await touchUserLastLoginAt(verifiedUser.id)
    res.json({ user: await serializeUser(touchedUser || verifiedUser) })
  } catch (err) {
    return res.status(500).json({ error: 'session_save_failed' })
  }
})

router.post('/password', requireAuth, async (req, res) => {
  const parsed = changePasswordSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const user = await findUserById(req.session.userId)
  if (!user) return res.status(404).json({ error: 'not_found' })

  const authUser = await findUserByEmail(user.email)
  if (!authUser) return res.status(404).json({ error: 'not_found' })

  const passwordMatched = await bcrypt.compare(parsed.data.currentPassword, authUser.passwordHash)
  if (!passwordMatched) return res.status(401).json({ error: 'invalid_current_password' })

  const passwordHash = await bcrypt.hash(parsed.data.nextPassword, 10)
  const updated = await updateUserPassword({ id: authUser.id, passwordHash })
  res.json({ user: await serializeUser(updated) })
})

const upload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })

router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) => {
  const parsed = profileSchema.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'bad_request' })

  const user = await findUserById(req.session.userId)
  if (!user) return res.status(404).json({ error: 'not_found' })
  if (isReservedNickname(parsed.data.nickname)) return res.status(400).json({ error: 'nickname_reserved' })
  const nicknameExists = await findUserByNickname(parsed.data.nickname, user.id)
  if (nicknameExists) return res.status(409).json({ error: 'nickname_taken' })

  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 || user.avatarSrc || ''
  const updated = await updateUserProfile({
    id: user.id,
    nickname: parsed.data.nickname,
    avatarSrc: nextAvatarSrc,
  })

  res.json({ user: await serializeUser(updated) })
})

module.exports = router
