import crypto from "crypto";
import { addMinutes, subMinutes } from "date-fns";
import type { Request } from "express";
import fetch from "fetch-with-proxy";
import {
  StateStoreStoreCallback,
  StateStoreVerifyCallback,
} from "passport-oauth2";
import { getCookieDomain, parseDomain } from "@shared/utils/domains";
import { AuthRedirectError, OAuthStateMismatchError } from "../errors";

export class StateStore {
  key = "state";

  store = (req: Request, callback: StateStoreStoreCallback) => {
    // token is a short lived one-time pad to prevent replay attacks
    // appDomain is the domain the user originated from when attempting auth
    // we expect it to be a team subdomain, custom domain, or apex domain
    const token = crypto.randomBytes(8).toString("hex");
    const appDomain = parseDomain(req.hostname);
    const state = buildState(appDomain.host, token);

    req.cookies.set(this.key, state, {
      httpOnly: false,
      expires: addMinutes(new Date(), 10),
      domain: getCookieDomain(req.hostname),
    });

    callback(null, token);
  };

  verify = (
    req: Request,
    providedToken: string,
    callback: StateStoreVerifyCallback
  ) => {
    const state = req.cookies.get(this.key);

    if (!state) {
      return callback(
        OAuthStateMismatchError("State not return in OAuth flow"),
        false,
        state
      );
    }

    const { host, token } = parseState(state);

    // Oauth callbacks are hard-coded to come to the apex domain, so we
    // redirect to the original app domain before attempting authentication.
    // If there is an error during auth, the user will end up on the same domain
    // that they started from.
    const appDomain = parseDomain(host);
    if (appDomain.host !== parseDomain(req.hostname).host) {
      const reqProtocol = req.protocol;
      const requestHost = req.get("host");
      const requestPath = req.originalUrl;
      const requestUrl = `${reqProtocol}://${requestHost}${requestPath}`;
      const url = new URL(requestUrl);

      url.host = appDomain.host;

      return callback(AuthRedirectError(``, url.toString()), false, token);
    }

    // Destroy the one-time pad token and ensure it matches
    req.cookies.set(this.key, "", {
      httpOnly: false,
      expires: subMinutes(new Date(), 1),
      domain: getCookieDomain(req.hostname),
    });

    if (!token || token !== providedToken) {
      return callback(OAuthStateMismatchError(), false, token);
    }

    // @ts-expect-error Type in library is wrong
    callback(null, true, state);
  };
}

export async function request(endpoint: string, accessToken: string) {
  const response = await fetch(endpoint, {
    method: "GET",
    credentials: "same-origin",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
  });
  return response.json();
}

function buildState(host: string, token: string) {
  return [host, token].join("|");
}

export function parseState(state: string) {
  const [host, token] = state.split("|");
  return { host, token };
}
