import * as fs from "node:fs/promises";
import axios from "axios";
import provideConfig from "./provideConfig";
import { initLogger, logError } from "./logger";

// @ts-ignore
import reportErrorToRollbar from "./rollbar.mjs";
import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials";
const oraP = import("ora");
const openP = import("open");

export default async () => {
  await initLogger("login");
  const breadcrumbs: string[] = [];
  const ora = (await oraP).default;
  const authenticationSpinner = ora("Waiting for code verification...").start();

  try {
    const open = (await openP).default;
    const config = await provideConfig();
    breadcrumbs.push("provideConfig");

    await resolveFiles();
    breadcrumbs.push("resolveFiles");

    const deviceCodePayload = {
      client_id: config.authClientId,
      audience: config.audienceUrl,
      scope: "openid offline_access",
    };

    const deviceCodeResponse = await axios.post(
      `https://${config.authDomain}/oauth/device/code`,
      deviceCodePayload,
    );

    ora(
      "Confirm this code on your browser: " +
        deviceCodeResponse.data["user_code"],
    ).info();

    const tokenPayload = {
      grant_type: "urn:ietf:params:oauth:grant-type:device_code",
      device_code: deviceCodeResponse.data["device_code"],
      client_id: config.authClientId,
    };

    await open(deviceCodeResponse.data["verification_uri_complete"]);

    let isTerminated = false;
    const signalHandler = () => {
      isTerminated = true;
      authenticationSpinner.fail("Authentication process interrupted");
      process.exit(0);
    };
    process.on("SIGTERM", signalHandler);
    process.on("SIGINT", signalHandler);

    /**
     * This is a recommended way to poll, since it take some time for a user to enter a `user_code` in a browser.
     * deviceCodeResponse.data['interval'] is a recommended/calculated polling interval specified in seconds.
     */
    while (!isTerminated) {
      try {
        const tokenResponse = await axios.post(
          `https://${config.authDomain}/oauth/token`,
          tokenPayload,
        );
        await fs.writeFile(
          CREDENTIALS_FILE,
          JSON.stringify(tokenResponse.data),
        );
        authenticationSpinner.succeed(
          "You are successfully authenticated now!",
        );
        break;
      } catch (e: any) {
        if (e.response.data?.error !== "authorization_pending") {
          authenticationSpinner.fail(
            "Authentication failed. Please try again.",
          );
          process.exit(1);
        }

        await sleep(deviceCodeResponse.data["interval"] * 1000);
      }
    }

    // Clean up signal handlers
    process.off("SIGTERM", signalHandler);
    process.off("SIGINT", signalHandler);
  } catch (error: unknown) {
    authenticationSpinner.fail("Authentication failed. Please try again.");
    await logError({ command: "login", breadcrumbs, error });
    await reportErrorToRollbar(error);
    console.log(error);
    process.exit(1);
  }
};

const REFRESH_MARGIN_SECONDS = 30;

const isAccessTokenExpired = (accessToken: string): boolean => {
  try {
    const [, payloadBase64] = accessToken.split(".");
    if (!payloadBase64) return true;
    const payload = JSON.parse(
      Buffer.from(payloadBase64, "base64url").toString(),
    );
    const exp = payload.exp;
    if (typeof exp !== "number") return true;
    return Date.now() / 1000 + REFRESH_MARGIN_SECONDS >= exp;
  } catch {
    return true;
  }
};

export async function getToken() {
  try {
    const rawCredentials = await fs.readFile(CREDENTIALS_FILE, "utf-8");
    const credentials = JSON.parse(rawCredentials.toString());
    const accessToken: string = credentials?.access_token ?? "";

    if (!accessToken) return "";
    if (!isAccessTokenExpired(accessToken)) return accessToken;
    if (!credentials?.refresh_token) return accessToken;

    try {
      const config = await provideConfig();
      const refreshResponse = await axios.post(
        `https://${config.authDomain}/oauth/token`,
        {
          grant_type: "refresh_token",
          client_id: config.authClientId,
          refresh_token: credentials.refresh_token,
        },
      );
      const newCredentials = { ...credentials, ...refreshResponse.data };
      await fs.writeFile(CREDENTIALS_FILE, JSON.stringify(newCredentials));
      return newCredentials.access_token ?? "";
    } catch {
      try {
        const latestRawCredentials = await fs.readFile(CREDENTIALS_FILE, "utf-8");
        const latestCredentials = JSON.parse(latestRawCredentials);
        const latestAccessToken = latestCredentials?.access_token ?? "";

        if (latestAccessToken && !isAccessTokenExpired(latestAccessToken)) {
          return latestAccessToken;
        }
      } catch {}

      return accessToken;
    }
  } catch (_e) {
    return "";
  }
}

function sleep(ms: number) {
  return new Promise((res) => setTimeout(res, ms));
}

export async function resolveFiles() {
  try {
    await fs.access(CREDENTIALS_DIR);
  } catch (_e) {
    await fs.mkdir(CREDENTIALS_DIR);
  }

  try {
    await fs.access(CREDENTIALS_FILE);
  } catch (e) {
    await fs.writeFile(CREDENTIALS_FILE, "");
  }
}
