import type { UserDoc } from "@/corePlugins/raclette__core/backend/datatypes/user/user.schema"
import type { Static } from "@sinclair/typebox"
import configService from "@/core/config/configService"
import type { FastifyRequest, FastifyReply, FastifyInstance } from "fastify"
import { Type } from "@sinclair/typebox"
import { toPublicUser } from "@/corePlugins/raclette__core/backend/datatypes/user/user.schema"
import { randomIdGenerator } from "@/helpers"
import {
  ForbiddenError,
  NotFoundError,
  UnauthorizedError,
  UserError,
} from "@/utils/errors"
import { CACHE_CONFIG } from "@m/cache/providers/valkey/valkeyConfig"
import { callDataTypeOperation } from "@c/contracting/contractRegistrar"

const { LOGIN_ATTEMPTS, KEY_PREFIXES } = CACHE_CONFIG

const LoginBodySchema = Type.Object({
  email: Type.String(),
  password: Type.String(),
  deviceId: Type.Optional(Type.String()),
})

const ResponseSchema = Type.Object({
  token: Type.String(),
  decoded: Type.Any(),
  user: Type.Any(),
  requestId: Type.String(),
})

type LoginBody = Static<typeof LoginBodySchema>

const randomId = randomIdGenerator(16)

export default (fastify: FastifyInstance) => {
  const trackLoginAttempt = async (key: string): Promise<void> => {
    const attemptsKey = `${KEY_PREFIXES.LOGIN_ATTEMPTS}${key}`

    const multi = fastify.cache.multi()

    multi.incr(attemptsKey)
    multi.expire(attemptsKey, LOGIN_ATTEMPTS.ATTEMPT_WINDOW)

    const results = await multi.exec()
    const attempts = results?.[0]?.[1] as number

    if (attempts >= LOGIN_ATTEMPTS.MAX_ATTEMPTS) {
      const lockKey = `${KEY_PREFIXES.LOGIN_LOCK}${key}`

      await fastify.cache.setex(lockKey, LOGIN_ATTEMPTS.LOCKOUT_DURATION, "1")
    }
  }

  const canAttemptLogin = async (key: string): Promise<boolean> => {
    const lockKey = `${KEY_PREFIXES.LOGIN_LOCK}${key}`
    const attemptsKey = `${KEY_PREFIXES.LOGIN_ATTEMPTS}${key}`

    const [isLocked, attempts] = await Promise.all([
      fastify.cache.exists(lockKey),
      fastify.cache.get(attemptsKey),
    ])

    if (isLocked) {
      fastify.log.info(`Login blocked for ${key}: Account is locked`)

      return false
    }

    const attemptCount = attempts ? parseInt(attempts) : 0
    const canAttempt = attemptCount < LOGIN_ATTEMPTS.MAX_ATTEMPTS

    if (!canAttempt) {
      fastify.log.info(
        `Login blocked for ${key}: Too many attempts (${attemptCount})`,
      )
    }

    return canAttempt
  }

  const resetLoginAttempts = async (key: string): Promise<void> => {
    const lockKey = `${KEY_PREFIXES.LOGIN_LOCK}${key}`
    const attemptsKey = `${KEY_PREFIXES.LOGIN_ATTEMPTS}${key}`

    await Promise.all([
      fastify.cache.del(attemptsKey),
      fastify.cache.del(lockKey),
    ])
  }

  const readUser = async (
    email: string,
    password: string,
  ): Promise<UserDoc> => {
    const user = await callDataTypeOperation(
      "raclette__core",
      "user",
      "readByMail",
      email,
    )

    if (!user) {
      throw new NotFoundError("User not found")
    }

    if (user.blocked) {
      throw new ForbiddenError("User is blocked")
    }

    if (!user.email_verified) {
      throw new UnauthorizedError("Email not verified")
    }

    if (!user.active) {
      throw new ForbiddenError("User account is inactive")
    }

    const isCorrectPassword = await user.checkPassword(password)

    if (!isCorrectPassword) {
      throw new UnauthorizedError("Invalid credentials")
    }

    return user
  }

  const checkDatabase = async () => {
    const userCount = await callDataTypeOperation(
      "raclette__core",
      "user",
      "readCount",
    )

    return userCount > 0
  }

  const handler = async (
    request: FastifyRequest<{
      Body: LoginBody
    }>,
    reply: FastifyReply,
  ) => {
    const { email, password, deviceId } = request.body
    const clientKey = deviceId || request.ip || email.toLowerCase()

    if (!(await checkDatabase())) {
      // user database is empty
      // this is a critical information for the frontend
      const replyObj = {
        statusCode: 500,
        code: "RCLT_DB_NO_USERS",
        error: "NoUsersInDatabase",
        message: "No Users in Database",
      }

      return reply.code(replyObj.statusCode).send(replyObj)
    }

    try {
      if (!(await canAttemptLogin(clientKey))) {
        const remainingLockTime = await fastify.cache.ttl(
          `${KEY_PREFIXES.LOGIN_LOCK}${clientKey}`,
        )

        return reply.code(429).send({
          error: "Too many login attempts. Please try again later.",
          lockTimeRemaining: remainingLockTime,
        })
      }

      console.warn("TRACK LOGIN ATTEMPT")
      await trackLoginAttempt(clientKey)

      const user = await readUser(email, password)

      // Reset login attempts on successful login
      await resetLoginAttempts(clientKey)

      // Create token with additional security measures
      const tokenPayload = {
        _id: user._id.split("|").pop(),
        tokenId: randomId,
        deviceId: deviceId || request.ip,
        iat: Math.floor(Date.now() / 1000),
      }
      // Set the expiry time/TTL for the auth token to the configured value
      const expiryDays = configService.getAuthTokenExpiryDays()
      const token = fastify.jwt.sign(tokenPayload, {
        expiresIn: `${expiryDays}d`,
      })
      const decoded = fastify.jwt.decode(token)

      fastify.log.info(`Successful login for user ${user._id}`)

      return {
        token,
        decoded,
        user: toPublicUser(user),
        requestId: request.id,
      }
    } catch (error: any) {
      fastify.log.error(`Login failed: ${error.message}`)

      if (error instanceof UserError) {
        return reply.code(error.statusCode).send({ error: error.message })
      }

      return reply.code(500).send({ error: "Authentication failed" })
    }
  }

  return {
    handler,
    preValidation: (
      request: FastifyRequest<{
        Body: LoginBody
      }>,
      _reply: FastifyReply,
      done: (error?: Error) => void,
    ) => {
      if (request.body?.email) {
        request.body.email = request.body.email.toLowerCase().trim()
      }
      done()
    },
    schema: {
      summary: "Authenticate user and receive access token",
      description: `Secure login endpoint with rate limiting and brute force protection.
        Implements Cache-based attempt tracking and temporary lockouts.
        Max attempts: ${LOGIN_ATTEMPTS.MAX_ATTEMPTS}
        Lockout duration: ${LOGIN_ATTEMPTS.LOCKOUT_DURATION} seconds
        Attempt window: ${LOGIN_ATTEMPTS.ATTEMPT_WINDOW} seconds`,
      tags: ["core/user"],
      body: LoginBodySchema,
      response: {
        200: ResponseSchema,
        429: Type.Object({
          error: Type.String(),
          lockTimeRemaining: Type.Number(),
        }),
      },
    },
  }
}
