import fp from 'fastify-plugin';
import * as jose from 'jose';
import config from '../config/configuration.js';
import type { AuthUser } from '../common/types.js';
import { UnauthorizedError } from '../common/errors.js';
import type { UsersRepository } from '../modules/users/users.repository.js';

let jwks: jose.JWTVerifyGetKey | undefined;

function getJwks(): jose.JWTVerifyGetKey {
  if (!jwks) {
    // Use internal URL (config.keycloak.url) to reach JWKS endpoint within Docker network
    jwks = jose.createRemoteJWKSet(
      new URL(
        `${config.keycloak.url}/realms/${config.keycloak.realm}/protocol/openid-connect/certs`,
      ),
    );
  }
  return jwks;
}

export default fp(
  async (fastify) => {
    // Decorate request with user and householdId (null defaults)
    fastify.decorateRequest('user', null as unknown as AuthUser);
    fastify.decorateRequest('householdId', '');

    fastify.addHook('onRequest', async (request, _reply) => {
      // Skip auth for routes marked as public via route config
      const routeConfig = request.routeOptions.config as unknown as
        | Record<string, unknown>
        | undefined;
      if (routeConfig?.['public'] === true) {
        return;
      }

      const authHeader = request.headers.authorization;
      if (!authHeader?.startsWith('Bearer ')) {
        throw new UnauthorizedError('Missing or invalid Authorization header');
      }

      const token = authHeader.slice(7);
      // Use public issuer URL (config.keycloak.issuerUrl) to validate the iss claim in the token.
      // Tokens have iss set to the public URL (KC_HOSTNAME_URL), not the internal Docker hostname.
      const expectedIssuer = `${config.keycloak.issuerUrl}/realms/${config.keycloak.realm}`;

      try {
        const { payload } = await jose.jwtVerify(token, getJwks(), {
          issuer: expectedIssuer,
          audience: config.keycloak.clientId,
        });

        const keycloakId = payload.sub ?? '';
        const email = (payload['email'] as string) ?? '';
        const displayName = (payload['preferred_username'] as string) ?? '';
        const roles = (payload['realm_access'] as Record<string, string[]>)?.['roles'] ?? [];

        // Look up user from DB to get fresh householdIds (application state
        // belongs in the database, not baked into the JWT).
        const usersRepository = fastify.diContainer.resolve<UsersRepository>('usersRepository');
        let dbUser = await usersRepository.findByKeycloakId(keycloakId);
        if (!dbUser) {
          dbUser = await usersRepository.upsertFromToken(keycloakId, email, displayName);
        }

        const user: AuthUser = {
          keycloakId,
          email,
          displayName,
          roles,
          householdIds: dbUser?.householdIds ?? [],
        };

        request.user = user;
      } catch (err) {
        // Only map jose-specific errors (bad signature, expired, wrong issuer, etc.) to 401.
        // Other errors (e.g. JWKS network failure) propagate as 500 so callers aren't misled.
        if (err instanceof jose.errors.JOSEError) {
          throw new UnauthorizedError('Invalid or expired token');
        }
        throw err;
      }
    });
  },
  { name: 'auth-plugin' },
);
