import { errors, symbols } from '@adonisjs/auth'
import type { AuthClientResponse } from '@adonisjs/auth/types'
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import jwtService from '#services/jwt_service'

export class JwtGuard {
  readonly driverName = 'jwt' as const

  isAuthenticated = false
  authenticationAttempted = false
  user?: User;

  declare [symbols.GUARD_KNOWN_EVENTS]: {}

  readonly #ctx: HttpContext

  constructor(ctx: HttpContext) {
    this.#ctx = ctx
  }

  getUserOrFail(): User {
    if (!this.user) {
      throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
        guardDriverName: this.driverName,
      })
    }
    return this.user
  }

  async authenticate(): Promise<User> {
    if (this.authenticationAttempted) {
      return this.getUserOrFail()
    }

    this.authenticationAttempted = true

    const token = this.#extractBearerToken()
    if (!token) {
      throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
        guardDriverName: this.driverName,
      })
    }

    const payload = await jwtService.verify(token).catch(() => {
      throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
        guardDriverName: this.driverName,
      })
    })
    const userId = payload.sub as string

    const user = await User.find(userId)
    if (!user || user.deletedAt) {
      throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
        guardDriverName: this.driverName,
      })
    }

    // Reject tokens issued before the last password change
    const pwdChangedAt = payload['pwd_changed_at'] as number | undefined
    if (pwdChangedAt && payload.iat && pwdChangedAt > payload.iat) {
      throw new errors.E_UNAUTHORIZED_ACCESS('Unauthorized access', {
        guardDriverName: this.driverName,
      })
    }

    this.user = user
    this.isAuthenticated = true
    return user
  }

  async check(): Promise<boolean> {
    try {
      await this.authenticate()
      return true
    } catch {
      return false
    }
  }

  async authenticateAsClient(): Promise<AuthClientResponse> {
    throw new Error('JwtGuard does not support authenticateAsClient')
  }

  #extractBearerToken(): string | null {
    const header = this.#ctx.request.header('Authorization')
    if (!header?.startsWith('Bearer ')) return null
    return header.slice(7)
  }
}

export function jwtGuard() {
  return (ctx: HttpContext) => new JwtGuard(ctx)
}
