import { NextResponse } from 'next/server'
import bcrypt from 'bcryptjs'
import { z } from 'zod'

import { prisma } from '@/lib/db'
import { env } from '@/lib/env'
import { AUDIT_ACTIONS, clientIp, writeAudit } from '@/lib/audit'
import { apiRoute } from '@/lib/api/handler'
import { ApiError, ERROR_CODES } from '@/lib/api/errors'
import {
  generateToken,
  setCsrfCookie,
  setSessionCookie,
  signSession,
} from '@/lib/auth/session'
import { LOCALE_COOKIE } from '@/lib/i18n/config'

const loginSchema = z.object({
  email: z.string().trim().toLowerCase().email().max(200),
  password: z.string().min(1).max(200),
})

/**
 * Sign-in.
 *
 * Two properties worth calling out:
 *
 *  1. A wrong password and an unknown address produce the identical response, and the
 *     hash comparison runs either way against a dummy hash. Skipping the comparison for
 *     unknown users would make account existence measurable by response time.
 *
 *  2. Repeated failures lock the account for a configured window. The counter lives on
 *     the user row rather than in memory so it survives a restart.
 */
const DUMMY_HASH = '$2b$11$C2s7yWJ4dWzM3G3RhQ4uEuKQd3Nq3nR8bZ0zGRlkKcbYyQO5iZlz2'

export const POST = apiRoute(
  {
    public: true,
    bodySchema: loginSchema,
    limit: env.rateLimitLoginMax,
  },
  async ({ body, request }) => {
    const user = await prisma.user.findUnique({
      where: { email: body.email },
      include: { roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } } },
    })

    if (user?.lockedUntil && user.lockedUntil.getTime() > Date.now()) {
      await writeAudit({
        action: AUDIT_ACTIONS.loginFailed,
        entity: 'user',
        entityId: user.id,
        userEmail: body.email,
        status: 'denied',
        detail: 'Account locked',
        request,
      })
      throw new ApiError(ERROR_CODES.accountLocked)
    }

    const passwordMatches = await bcrypt.compare(body.password, user?.passwordHash ?? DUMMY_HASH)

    if (!user || !passwordMatches || !user.isActive) {
      if (user) {
        const failures = user.failedLoginCount + 1
        const shouldLock = failures >= env.maxFailedLogins
        await prisma.user.update({
          where: { id: user.id },
          data: {
            failedLoginCount: shouldLock ? 0 : failures,
            lockedUntil: shouldLock ? new Date(Date.now() + env.lockoutMinutes * 60_000) : null,
          },
        })
      }
      await writeAudit({
        action: AUDIT_ACTIONS.loginFailed,
        entity: 'user',
        entityId: user?.id ?? null,
        userEmail: body.email,
        status: 'denied',
        detail: user ? (user.isActive ? 'Bad password' : 'Inactive account') : 'Unknown address',
        request,
      })
      throw new ApiError(ERROR_CODES.invalidCredentials)
    }

    const permissions = new Set<string>()
    for (const link of user.roles) {
      for (const grant of link.role.permissions) permissions.add(grant.permission.key)
    }

    const tokenId = generateToken()
    const { token, expiresAt } = await signSession({
      sub: user.id,
      jti: tokenId,
      email: user.email,
      name: user.name,
      nameAr: user.nameAr,
      locale: user.locale === 'en' ? 'en' : 'ar',
      roles: user.roles.map((link) => link.role.key),
      permissions: [...permissions],
    })

    await prisma.$transaction([
      prisma.session.create({
        data: {
          userId: user.id,
          tokenId,
          ip: clientIp(request),
          userAgent: request.headers.get('user-agent')?.slice(0, 300) ?? null,
          expiresAt,
        },
      }),
      prisma.user.update({
        where: { id: user.id },
        data: { failedLoginCount: 0, lockedUntil: null, lastLoginAt: new Date() },
      }),
    ])

    await setSessionCookie(token, expiresAt)
    await setCsrfCookie(generateToken(), expiresAt)

    await writeAudit({
      action: AUDIT_ACTIONS.loginSucceeded,
      entity: 'session',
      entityId: tokenId,
      userEmail: user.email,
      user: {
        id: user.id,
        email: user.email,
        roles: user.roles.map((link) => link.role.key),
      } as never,
      request,
      detail: `Roles: ${user.roles.map((link) => link.role.key).join(', ')}`,
    })

    const response = NextResponse.json({
      ok: true,
      data: {
        id: user.id,
        email: user.email,
        name: user.name,
        nameAr: user.nameAr,
        locale: user.locale,
        roles: user.roles.map((link) => link.role.key),
      },
    })

    // Follow the account's stored language preference straight away, so the operator
    // lands in the right language and direction without a second navigation.
    response.cookies.set(LOCALE_COOKIE, user.locale === 'en' ? 'en' : 'ar', {
      httpOnly: false,
      sameSite: 'lax',
      secure: env.isProduction,
      path: '/',
      maxAge: 60 * 60 * 24 * 365,
    })

    return response
  },
)
