/**
 * Configurable JWT auth middleware — RS256 JWKS verification with HS256 fallback.
 *
 * Factory function creates extractAuth() and requireRole() bound to the
 * provided config, eliminating the need for each product backend to maintain
 * its own copy.
 */
import { jwtVerify, createRemoteJWKSet } from 'jose';
import { UnauthorizedError, ForbiddenError } from '@bytelyst/errors';
import type { AuthPayload, FastifyAuthOptions } from './types.js';

export function createAuthMiddleware(opts: FastifyAuthOptions) {
  // Lazy-init JWKS client (cached, auto-refreshed by jose)
  let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
  let cachedJwksUrl: string | undefined;

  function resolveJwksUrl(): string | undefined {
    return typeof opts.jwksUrl === 'function' ? opts.jwksUrl() : opts.jwksUrl;
  }

  function resolveJwtSecret(): string {
    return typeof opts.jwtSecret === 'function' ? opts.jwtSecret() : opts.jwtSecret;
  }

  function getJWKS(): ReturnType<typeof createRemoteJWKSet> | null {
    const url = resolveJwksUrl();
    if (!url) return null;
    if (jwks && cachedJwksUrl === url) return jwks;
    jwks = createRemoteJWKSet(new URL(url));
    cachedJwksUrl = url;
    return jwks;
  }

  function getHmacSecret(): Uint8Array {
    return new TextEncoder().encode(resolveJwtSecret());
  }

  /**
   * Extract and verify auth payload from an Authorization header.
   * Tries RS256 via JWKS first, falls back to HS256.
   */
  async function extractAuth(req: { headers: { authorization?: string } }): Promise<AuthPayload> {
    const auth = req.headers.authorization;
    if (!auth?.startsWith('Bearer ')) {
      throw new UnauthorizedError();
    }
    const token = auth.slice(7);

    // Try RS256 via JWKS first
    const remoteJWKS = getJWKS();
    if (remoteJWKS) {
      try {
        const { payload } = await jwtVerify(token, remoteJWKS);
        const p = payload as unknown as AuthPayload;
        if (p.type !== 'access') throw new Error('Not an access token');
        return p;
      } catch {
        // Fall through to HS256
      }
    }

    // Fall back to HS256 (existing behavior)
    try {
      const { payload } = await jwtVerify(token, getHmacSecret());
      const p = payload as unknown as AuthPayload;
      if (p.type !== 'access') throw new Error('Not an access token');
      return p;
    } catch {
      throw new UnauthorizedError('Invalid or expired token');
    }
  }

  /**
   * Require specific roles. Extracts auth first, then checks role.
   */
  async function requireRole(
    req: { headers: { authorization?: string } },
    ...roles: string[]
  ): Promise<AuthPayload> {
    const payload = await extractAuth(req);
    if (roles.length > 0 && (!payload.role || !roles.includes(payload.role))) {
      throw new ForbiddenError('Insufficient permissions');
    }
    return payload;
  }

  return { extractAuth, requireRole };
}
