import chalk from "chalk";
import cookieParser from "cookie-parser";
import crypto from "crypto";
import "dotenv/config";
import express from "express";
import basicAuth from "express-basic-auth";
import { createJwtMiddleware } from "../../utils/backend/jwt_middleware";
import { getTokenFromQueryString } from "../../utils/backend/jwt_middleware/jwt_middleware";
import { JSONFileDatabase } from "../database/database";

/**
 * These are the hard-coded credentials for logging in to this template.
 */
const USERNAME = "username";
const PASSWORD = "password";

const COOKIE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes

const CANVA_BASE_URL = "https://www.canva.com";

interface Data {
  users: string[];
}

/**
 * For more information on Authentication, refer to our documentation: {@link https://www.canva.dev/docs/apps/authenticating-users/#types-of-authentication}.
 */
export const createAuthRouter = () => {
  const APP_ID = getAppId();

  /**
   * Set up a database with a "users" table. In this example code, the
   * database is simply a JSON file.
   */
  const db = new JSONFileDatabase<Data>({ users: [] });

  const router = express.Router();

  /**
   * The `cookieParser` middleware allows the routes to read and write cookies.
   *
   * By passing a value into the middleware, we enable the "signed cookies" feature of Express.js. The
   * value should be static and cryptographically generated. If it's dynamic (as shown below), cookies
   * won't persist between server restarts.
   *
   * TODO: Replace `crypto.randomUUID()` with a static value, loaded via an environment variable.
   */
  router.use(cookieParser(crypto.randomUUID()));

  /**
   * This endpoint is hit at the start of the authentication flow. It contains a state which must
   * be passed back to canva so that Canva can verify the response. It must also set a nonce in the
   * user's browser cookies and send the nonce back to Canva as a url parameter.
   *
   * If Canva can validate the state, it will then redirect back to the chosen redirect url.
   */
  router.get("/configuration/start", async (req, res) => {
    /**
     * Generate a unique nonce for each request. A nonce is a random, single-use value
     * that's impossible to guess or enumerate. We recommended using a Version 4 UUID that
     * is cryptographically secure, such as one generated by the `randomUUID` method.
     */
    const nonce = crypto.randomUUID();
    // Set the expiry time for the nonce. We recommend 5 minutes.
    const expiry = Date.now() + COOKIE_EXPIRY_MS;

    // Create a JSON string that contains the nonce and an expiry time
    const nonceWithExpiry = JSON.stringify([nonce, expiry]);

    // Set a cookie that contains the nonce and the expiry time
    res.cookie("nonce", nonceWithExpiry, {
      secure: true,
      httpOnly: true,
      maxAge: COOKIE_EXPIRY_MS,
      signed: true,
    });

    // Create the query parameters that Canva requires
    const params = new URLSearchParams({
      nonce,
      state: req?.query?.state?.toString() || "",
    });

    // Redirect to Canva with required parameters
    res.redirect(302, `${CANVA_BASE_URL}/apps/configure/link?${params}`);
  });

  /**
   * This endpoint renders a login page. Once the user logs in, they're
   * redirected back to Canva, which completes the authentication flow.
   */
  router.get(
    "/redirect-url",
    /**
     * Use a JSON Web Token (JWT) to verify incoming requests. The JWT is
     * extracted from the `canva_user_token` parameter.
     */
    createJwtMiddleware(APP_ID, getTokenFromQueryString),
    /**
     * Warning: For demonstration purposes, we're using basic authentication and
     * hard- coding a username and password. This is not a production-ready
     * solution!
     */
    basicAuth({
      users: { [USERNAME]: PASSWORD },
      challenge: true,
    }),
    async (req, res) => {
      const failureResponse = () => {
        const params = new URLSearchParams({
          success: "false",
          state: req.query.state?.toString() || "",
        });
        res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
      };

      // Get the nonce and expiry time stored in the cookie.
      const cookieNonceAndExpiry = req.signedCookies.nonce;

      // Get the nonce from the query parameter.
      const queryNonce = req.query.nonce?.toString();

      // After reading the cookie, clear it. This forces abandoned auth flows to be restarted.
      res.clearCookie("nonce");

      let cookieNonce = "";
      let expiry = 0;

      try {
        [cookieNonce, expiry] = JSON.parse(cookieNonceAndExpiry);
      } catch {
        // If the nonce can't be parsed, assume something has been compromised and exit.
        return failureResponse();
      }

      // If the nonces are empty, exit the authentication flow.
      if (
        isEmpty(cookieNonceAndExpiry) ||
        isEmpty(queryNonce) ||
        isEmpty(cookieNonce)
      ) {
        return failureResponse();
      }

      /**
       * Check that:
       *
       * - The nonce in the cookie and query parameter contain the same value
       * - The nonce has not expired
       *
       * **Note:** We could rely on the cookie expiry, but that is vulnerable to tampering
       * with the browser's time. This allows us to double-check based on server time.
       */
      if (expiry < Date.now() || cookieNonce !== queryNonce) {
        return failureResponse();
      }

      // Get the userId from JWT middleware
      const { userId } = req.canva;

      // Load the database
      const data = await db.read();

      // Add the user to the database
      if (!data.users.includes(userId)) {
        data.users.push(userId);
        await db.write(data);
      }

      // Create query parameters for redirecting back to Canva
      const params = new URLSearchParams({
        success: "true",
        state: req?.query?.state?.toString() || "",
      });

      // Redirect the user back to Canva
      res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
    },
  );

  /**
   * TODO: Add this middleware to all routes that will receive requests from
   * your app.
   */
  const jwtMiddleware = createJwtMiddleware(APP_ID);

  /**
   * This endpoint is called when a user disconnects an app from their account.
   * The app is expected to de-authenticate the user on its backend, so if the
   * user reconnects the app, they'll need to re-authenticate.
   *
   * Note: The name of the endpoint is *not* configurable.
   *
   * Note: This endpoint is called by Canva's backend directly and must be
   * exposed via a public URL. To test this endpoint, add a proxy URL, such as
   * one generated by nGrok, to the 'Add authentication' section in the
   * Developer Portal. Localhost addresses will not work to test this endpoint.
   */
  router.post("/configuration/delete", jwtMiddleware, async (req, res) => {
    // Get the userId from JWT middleware
    const { userId } = req.canva;

    // Load the database
    const data = await db.read();

    // Remove the user from the database
    await db.write({
      users: data.users.filter((user) => user !== userId),
    });

    // Confirm that the user was removed
    res.send({
      type: "SUCCESS",
    });
  });

  /**
   * All routes that start with /api will be protected by JWT authentication
   */
  router.use("/api", jwtMiddleware);

  /**
   * This endpoint checks if a user is authenticated.
   */
  router.post("/api/authentication/status", async (req, res) => {
    // Load the database
    const data = await db.read();

    // Check if the user is authenticated
    const isAuthenticated = data.users.includes(req.canva.userId);

    // Return the authentication status
    res.send({
      isAuthenticated,
    });
  });

  return router;
};

/**
 * Checks if a given param is nullish or an empty string
 *
 * @param str The string to check
 * @returns true if the string is nullish or empty, false otherwise
 */
function isEmpty(str?: string): boolean {
  return str == null || str.length === 0;
}

/**
 * Retrieves the CANVA_APP_ID from the environment variables.
 * Throws an error if the CANVA_APP_ID environment variable is undefined or set to a default value.
 *
 * @returns {string} The Canva App ID
 * @throws {Error} If CANVA_APP_ID environment variable is undefined or set to a default value
 */
function getAppId(): string {
  // TODO: Set the CANVA_APP_ID environment variable in the project's .env file
  const appId = process.env.CANVA_APP_ID;

  if (!appId) {
    throw new Error(
      `The CANVA_APP_ID environment variable is undefined. Set the variable in the project's .env file.`,
    );
  }

  if (appId === "YOUR_APP_ID_HERE") {
    // eslint-disable-next-line no-console
    console.log(
      chalk.bgRedBright(
        "Default 'CANVA_APP_ID' environment variable detected.",
      ),
    );
    // eslint-disable-next-line no-console
    console.log(
      "Please update the 'CANVA_APP_ID' environment variable in your project's `.env` file " +
        `with the App ID obtained from the Canva Developer Portal: ${chalk.greenBright(
          "https://www.canva.com/developers/apps",
        )}\n`,
    );
  }

  return appId;
}
