import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
import bcrypt from 'bcryptjs'
import type { NextFunction, Request, Response } from 'express'
import { config, cookieSecure } from '../config.js'
import { prisma } from '../db.js'

const cookieName = 'stacklogic_admin_session'
const attempts = new Map<string, { count: number; resetAt: number }>()

declare global {
  namespace Express {
    interface Request {
      admin?: {
        id: string
        email: string
        name: string | null
        csrfToken: string
      }
    }
  }
}

function hashToken(token: string) {
  return createHash('sha256').update(`${config.ADMIN_SESSION_SECRET}:${token}`).digest('hex')
}

function randomToken(bytes = 32) {
  return randomBytes(bytes).toString('base64url')
}

export function getClientIp(request: Request) {
  return request.ip || request.socket.remoteAddress || 'unknown'
}

export function checkLoginRateLimit(request: Request) {
  const key = getClientIp(request)
  const now = Date.now()
  const entry = attempts.get(key)
  if (!entry || entry.resetAt <= now) {
    attempts.set(key, { count: 1, resetAt: now + config.LOGIN_RATE_LIMIT_WINDOW_MS })
    return true
  }

  entry.count += 1
  return entry.count <= config.LOGIN_RATE_LIMIT_MAX
}

export function clearLoginRateLimit(request: Request) {
  attempts.delete(getClientIp(request))
}

export async function createAdminSession(response: Response, adminId: string) {
  const token = randomToken()
  const csrfToken = randomToken(24)
  const expiresAt = new Date(Date.now() + config.ADMIN_SESSION_DAYS * 24 * 60 * 60 * 1000)

  await prisma.adminSession.create({
    data: {
      tokenHash: hashToken(token),
      csrfToken,
      adminId,
      expiresAt,
    },
  })

  response.cookie(cookieName, token, {
    httpOnly: true,
    sameSite: 'lax',
    secure: cookieSecure,
    expires: expiresAt,
    path: '/admin',
  })
}

export async function destroyAdminSession(request: Request, response: Response) {
  const token = request.cookies?.[cookieName]
  if (token) {
    await prisma.adminSession.deleteMany({ where: { tokenHash: hashToken(token) } })
  }
  response.clearCookie(cookieName, { path: '/admin' })
}

export async function loadAdminSession(request: Request, _response: Response, next: NextFunction) {
  const token = request.cookies?.[cookieName]
  if (!token) {
    next()
    return
  }

  const session = await prisma.adminSession.findUnique({
    where: { tokenHash: hashToken(token) },
    include: { admin: true },
  })

  if (!session || session.expiresAt < new Date()) {
    next()
    return
  }

  request.admin = {
    id: session.admin.id,
    email: session.admin.email,
    name: session.admin.name,
    csrfToken: session.csrfToken,
  }
  await prisma.adminSession.update({
    where: { id: session.id },
    data: { lastSeenAt: new Date() },
  })
  next()
}

export function requireAdmin(request: Request, response: Response, next: NextFunction) {
  if (!request.admin) {
    response.redirect('/admin/login')
    return
  }
  next()
}

export function requireCsrf(request: Request, response: Response, next: NextFunction) {
  if (!request.admin) {
    response.status(401).send('Not authenticated')
    return
  }

  const submitted = String(request.body?._csrf ?? '')
  const expected = request.admin.csrfToken
  const submittedBuffer = Buffer.from(submitted)
  const expectedBuffer = Buffer.from(expected)

  if (submittedBuffer.length !== expectedBuffer.length || !timingSafeEqual(submittedBuffer, expectedBuffer)) {
    response.status(403).send('Invalid CSRF token')
    return
  }

  next()
}

export async function verifyPassword(email: string, password: string) {
  const admin = await prisma.adminUser.findUnique({ where: { email: email.toLowerCase() } })
  if (!admin) return null
  const valid = await bcrypt.compare(password, admin.passwordHash)
  return valid ? admin : null
}
