import User from '#models/user'
import RefreshToken from '#models/refresh_token'
import { loginValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'
import jwtService from '#services/jwt_service'
import app from '@adonisjs/core/services/app'
import env from '#start/env'
import hash from '@adonisjs/core/services/hash'
import { compare as bcryptCompare } from 'bcryptjs'
import { createHash, randomBytes } from 'node:crypto'
import { DateTime } from 'luxon'

export default class AccessTokensController {
  async store({ request, response, serialize }: HttpContext) {
    const { email, password } = await request.validateUsing(loginValidator)

    const user = await resolveCredentials(email, password)

    if (!user) {
      return response.unauthorized({ message: 'Invalid credentials.' })
    }
    if (user.status === 'banned') {
      return response.forbidden({ message: 'This account has been banned.' })
    }
    if (user.status === 'pending') {
      return response.forbidden({ message: 'Please verify your email before signing in.' })
    }
    await user.load('userPref')
    await user.load('roles')
    const { accessToken, refreshToken } = await issueTokenPair(user)
    setRefreshTokenCookie(response, refreshToken)

    return serialize({
      user: UserTransformer.transform(user),
      accessToken,
      ...(isNativeClient(request) ? { refreshToken } : {}),
    })
  }

  async destroy({ request, response }: HttpContext) {
    const rawToken: string | undefined = request.cookie('vtrap_rt') ?? request.input('refreshToken')
    if (rawToken) {
      const hash = createHash('sha256').update(rawToken).digest('hex')
      await RefreshToken.query()
        .where('token_hash', hash)
        .whereNull('revoked_at')
        .update({ revoked_at: DateTime.now().toUTC().toFormat('yyyy-MM-dd HH:mm:ss') })
    }
    clearRefreshTokenCookie(response)
    return { message: 'Logged out successfully' }
  }
}

export function isNativeClient(request: HttpContext['request']): boolean {
  return request.header('X-Client-Platform')?.toLowerCase() === 'native'
}

export async function issueTokenPair(user: User) {
  const accessToken = await jwtService.sign({
    sub: user.id,
    email: user.email,
    username: user.username,
    pwd_changed_at: user.passwordChangedAt?.toUnixInteger() ?? 0,
    roles: user.roles.map((role) => role.name),
  })

  const rawToken = randomBytes(40).toString('hex')
  const tokenHash = createHash('sha256').update(rawToken).digest('hex')

  await RefreshToken.create({
    userId: user.id,
    tokenHash,
    expiresAt: DateTime.now().plus({ days: 30 }),
  })

  return { accessToken, refreshToken: rawToken }
}

function refreshCookieOptions() {
  const domain = env.get('COOKIE_DOMAIN')
  return {
    httpOnly: true,
    secure: app.inProduction,
    sameSite: 'lax' as const,
    ...(domain ? { domain } : {}),
    maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
  }
}

export function setRefreshTokenCookie(response: HttpContext['response'], rawToken: string) {
  response.cookie('vtrap_rt', rawToken, refreshCookieOptions())
}

export function clearRefreshTokenCookie(response: HttpContext['response']) {
  const domain = env.get('COOKIE_DOMAIN')
  response.clearCookie('vtrap_rt', {
    ...(domain ? { domain } : {}),
  })
}

/**
 * Resolves login credentials supporting both current (scrypt) and legacy (bcrypt) hashes.
 *
 * - New users:          password column holds a scrypt hash → verified with hash.verify()
 * - Legacy unmigrated:  password column holds a bcrypt hash → verified with bcryptCompare(),
 *                       then immediately re-hashed to scrypt
 * - Legacy migrated:    password column is null, legacy_password holds bcrypt hash →
 *                       same bcrypt path, then re-hashed to scrypt
 *
 * Always performs a hash operation even when the user is not found to prevent
 * timing-based email enumeration.
 *
 * Returns the User on success, null on failure.
 */
async function resolveCredentials(email: string, password: string): Promise<User | null> {
  // Scrypt dummy hash used for timing-safe rejection when user is not found
  const DUMMY_HASH =
    '$scrypt$n=16384,r=8,p=1$AAAAAAAAAAAAAAAAAAAAAA$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

  const user = await User.query().where('email', email).whereNull('deleted_at').first()

  if (!user) {
    await hash.verify(DUMMY_HASH, password)
    return null
  }

  // --- Path 1: current scrypt hash ---
  if (user.password && !user.password.startsWith('$2')) {
    return (await hash.verify(user.password, password)) ? user : null
  }

  // --- Path 2: legacy bcrypt (unmigrated — still in password column) ---
  if (user.password?.startsWith('$2')) {
    const valid = await bcryptCompare(password, user.password)
    if (!valid) return null
    user.password = password // beforeSave hook re-hashes to scrypt
    await user.save()
    return user
  }

  // --- Path 3: legacy bcrypt (migrated to legacy_password column) ---
  if (user.legacyPassword) {
    const valid = await bcryptCompare(password, user.legacyPassword)
    if (!valid) return null
    user.password = password // beforeSave hook re-hashes to scrypt
    user.legacyPassword = null
    await user.save()
    return user
  }

  // No usable password hash found
  await hash.verify(DUMMY_HASH, password)
  return null
}
