import * as Boom from "@hapi/boom";
import * as Hoek from "@hapi/hoek";
import * as Hapi from "@hapi/hapi";
import { promisify } from "node:util";
import jwt from "jsonwebtoken";
import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa";
import type { IAppConfig } from "../../config";

const CF_JWT_HEADER_NAME = "cf-access-jwt-assertion";
const CF_JWT_ALGOS = ["RS256"];

type VerifyFn = (token: string) => Promise<void>;

const verifyToken = (settings) => {
  const { audience, issuer } = settings;
  const client = jwksClient({
    jwksUri: `${issuer}/cdn-cgi/access/certs`,
  });

  return async (token: string) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const getKey = (header: any, callback: any) => {
      client.getSigningKey(header.kid, (err, key) => {
        if (err)
          throw Boom.serverUnavailable(
            "failed to fetch cloudflare access jwks"
          );
        callback(undefined, key?.getPublicKey());
      });
    };

    const opts = {
      algorithms: CF_JWT_ALGOS,
      audience,
      issuer,
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (promisify(jwt.verify) as any)(token, getKey, opts);
  };
};

const handleCfJwt =
  (verify: VerifyFn) =>
  async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
    const token = request.headers[CF_JWT_HEADER_NAME];
    if (token) {
      try {
        await verify(token);
      } catch (error) {
        console.error(error);
        return Boom.unauthorized("invalid cloudflare access token");
      }
    }

    return h.continue;
  };

const defaultOpts = {
  issuer: undefined,
  audience: undefined,
  strategyName: "clouflareaccess",
  validate: undefined,
};

const cfJwtRegister = async (
  server: Hapi.Server,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  options: any
): Promise<void> => {
  server.dependency(["hapi-auth-jwt2"]);
  const settings = Hoek.applyToDefaults(defaultOpts, options);
  const verify = verifyToken(settings);

  const { validate, strategyName, audience, issuer } = settings;
  server.ext("onPreAuth", handleCfJwt(verify));

  if (!strategyName) {
    throw new Error("Missing strategyName for cloudflare-jwt hapi plugin!");
  }

  server.auth.strategy(strategyName, "jwt", {
    key: hapiJwt2KeyAsync({
      jwksUri: `${issuer}/cdn-cgi/access/certs`,
    }),
    cookieKey: false,
    urlKey: false,
    headerKey: CF_JWT_HEADER_NAME,
    validate,
    verifyOptions: {
      audience,
      issuer,
      algorithms: ["RS256"],
    },
  });
};

export const registerCloudflareAccessJwt = async (
  server: Hapi.Server,
  config: IAppConfig
): Promise<void> => {
  const { audience, domain } = config.cfaccess;
  // only enable this plugin if cloudflare access config is configured
  if (audience && domain) {
    server.log(["auth"], "cloudflare access authorization enabled");
    await server.register({
      plugin: {
        name: "cloudflare-jwt",
        version: "0.0.1",
        register: cfJwtRegister,
      },
      options: {
        issuer: `https://${domain}`,
        audience,
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        validate(decoded: any, _request: any) {
          const { email, name } = decoded;
          return {
            isValid: true,
            credentials: { user: { email, name } },
          };
        },
      },
    });
  }
};
