import {
  prisma,
  sessionDuration,
  accessTokenPrivateKey,
  refreshTokenPrivateKey,
} from "../constants";
import UserController from "../controllers/UserController";
import {
  SessionController,
  DecodedSession,
  SessionPayloadObject,
  SessionTokensObject,
} from "../controllers/SessionController";
import jwt from "jsonwebtoken";
import SessionError from "../Errors/SessionError";
import SessionTokenError from "../Errors/SessionTokenError";
import ExpiredAccessTokenError from "../Errors/ExpiredAccessTokenError";
import ExpiredRefreshTokenError from "../Errors/ExpiredRefreshTokenError";
import { events } from "../modules/globalEvents";

export interface SessionCreationData {
  user: UserController;
}

export const sessions = {
  /**
   * Create a session
   *
   * This will create a session instance in the databse that will be linked to both the session token
   * and the refresh token.
   *
   * @param data - The params needed to create the session tokens
   * @returns {Promise<SessionTokensObject>} Session Token and the Refresh Token
   */
  async create(data: SessionCreationData): Promise<SessionTokensObject> {
    const session = await prisma.session.create({
      data: {
        expires: new Date(Date.now() + sessionDuration),
        userId: data.user.id,
      },
    });

    const controller = new SessionController(session);

    // Trigger Global Event
    events.emit("session:created", {
      user: data.user,
      session: controller,
    });

    let tokens: SessionTokensObject = await controller.generateTokens();

    return tokens;
  },

  /**
   * Fetch a session
   *
   * This method is designed to be as versitile as possible, if you are looking for a session, use this method with
   * your choice of `accessToken`, `refreshToken`, `id`, or `sessionKey`.  The identifier value is a partial type.
   *
   * @param identifier - An object allowing you to put either session token, sessionKey, or id in to fetch the desired session.
   * @returns {Promise<SessionController>} The controller for the desired session.
   */
  async fetch(
    identifier: Partial<{
      refreshToken: string;
      accessToken: string;
      id: string;
      sessionKey: string;
    }>
  ) {
    if (identifier.refreshToken || identifier.accessToken) {
      const decodedJWT = identifier.refreshToken
        ? ((await new Promise((res, rej) =>
            jwt.verify(
              identifier.refreshToken!,
              refreshTokenPrivateKey,
              {
                algorithms: ["RS256"],
              },
              async (err, decode) => {
                if (
                  err &&
                  (err.name == "TokenExpiredError" ||
                    err.message == "invalid signature")
                ) {
                  let sessionDat = await prisma.session.findFirst({
                    where: {
                      sessionKey: (
                        jwt.decode(identifier.refreshToken!) as DecodedSession
                      ).sessionKey,
                    },
                  });

                  if (!sessionDat)
                    return rej(new SessionError("Invalid session."));
                  let session = new SessionController(sessionDat);

                  await session.terminate();
                  if (err.message == "invalid signature")
                    return rej(new SessionError("Invalid session."));

                  return rej(new ExpiredRefreshTokenError("It epired."));
                }
                if (err) return rej(err);

                return res(decode as DecodedSession);
              }
            )
          )) as DecodedSession)
        : ((await new Promise((res, rej) =>
            jwt.verify(
              identifier.accessToken!,
              accessTokenPrivateKey,
              {
                algorithms: ["RS256"],
              },
              (err, decode) => {
                if (err && err.name == "TokenExpiredError")
                  return rej(new ExpiredAccessTokenError());
                if (err) return rej(err);
                return res(decode as DecodedSession);
              }
            )
          )) as DecodedSession);

      const sessionData = await prisma.session.findFirst({
        where: { sessionKey: decodedJWT.sessionKey },
      });

      if (!sessionData) throw new SessionError("Invalid Session");
      if (identifier.accessToken && decodedJWT.exp > Date.now())
        throw new ExpiredAccessTokenError();

      if (identifier.refreshToken && decodedJWT.exp > Date.now()) {
        let sess = new SessionController(sessionData);
        await sess.terminate();
        throw new SessionError("Invalid Session...", "Expired Refresh Token");
      }

      return new SessionController(sessionData);
    }

    const sessionData = await prisma.session.findFirst({
      where: { OR: [{ sessionKey: identifier.sessionKey, id: identifier.id }] },
    });

    if (!sessionData) throw new SessionError("Invalid Session");

    return new SessionController(sessionData);
  },
};

/**
 * @TODO As a consequence of the above, need to setup pgBoss for cron and event loop.
 */
