import jwt from "jsonwebtoken";
import {
  prisma,
  refreshTokenDuration,
  accessTokenDuration,
  accessTokenPrivateKey,
  refreshTokenPrivateKey,
} from "../constants";
import SessionTokenError from "../Errors/SessionTokenError";
import { events } from   "../modules/globalEvents";
import UserController from "./UserController";
import { users } from "../managers/users";
import { Session } from "../../generated/prisma/client";

export interface SessionPayloadObject {
  userID: string;
  sessionKey: string;
}

export interface SessionTokensObject {
  accessToken: string;
  refreshToken: string;
}

export type DecodedSession = SessionPayloadObject & {
  iat: number;
  exp: number;
};

/**
 * Session Controller
 *
 * This class is for create a controller that can manage, generate and refresh tokens, self terminate and self delete
 * all sessions.  This also allows you to access all data about a session.
 */
export class SessionController {
  public readonly id: string;
  public readonly sessionKey: string;
  public readonly userId: string;
  public readonly expires: Date;
  public refreshedAt: Date | null;
  public invalidatedAt: Date | null;

  public terminated: boolean = false;

  private _refreshTokenGenrated: boolean;

  constructor(sessionData: Session) {
    this.id = sessionData.id;
    this.sessionKey = sessionData.sessionKey;
    this.userId = sessionData.userId;
    this.expires = sessionData.expires;
    this.refreshedAt = sessionData.refreshedAt;
    this.invalidatedAt = sessionData.invalidatedAt;

    this._refreshTokenGenrated = sessionData.refreshTokenGenerated;
  }

  /** @ignore */
  private _generateAccessToken() {
    const payload: SessionPayloadObject = {
      sessionKey: this.sessionKey,
      userID: this.userId,
    };

    return jwt.sign(payload, accessTokenPrivateKey, {
      algorithm: "RS256",
      expiresIn: accessTokenDuration,
    });
  }

  /** @ignore */
  private _generateRefreshToken() {
    const payload: SessionPayloadObject = {
      sessionKey: this.sessionKey,
      userID: this.userId,
    };

    return jwt.sign(payload, refreshTokenPrivateKey, {
      algorithm: "RS256",
      expiresIn: refreshTokenDuration,
    });
  }

  /**
   * Invalidate the Session
   *
   * The purpose for this function is if you wanted to be able to listen for somebody using an invalid session,
   * you just have to invalidate it with this function and go from there.
   *
   * @returns {Promise<void>} - nothing
   */
  public async invalidate() {
    const invalidationDate = new Date();

    if (this.invalidatedAt)
      throw new Error("Session has already been invalidated.");

    await prisma.session.update({
      data: { invalidatedAt: invalidationDate },
      where: { id: this.id },
    });
    this.invalidatedAt = invalidationDate;

    events.emit("session:invalidated", this);
    return;
  }

  /**
   * Terminate the session
   *
   * Terminating the session will immediately delete the session making it impossible for it to be referenced again.
   *
   * @returns {Promise<void>} - nothing
   */
  public async terminate() {
    await prisma.session.delete({ where: { id: this.id } });
    events.emit("session:terminated", this);
    this.terminated = true;
    return;
  }

  /**
   * Generate Tokens
   *
   * **NOTE**: This method can only be ran once per session.
   *
   * Running this function will allow you to generate an accessToken and a refreshToken.  Each token
   * will be generated with their own respective private keys.
   *
   *
   * @returns {Promise<SessionTokensObject>} An object containing the `accessToken` and `refreshToken`
   */
  public async generateTokens(): Promise<SessionTokensObject> {
    if (this._refreshTokenGenrated)
      throw new Error("Tokens have alredy been generated for this session.");

    const accessToken = this._generateAccessToken();
    const refreshToken = this._generateRefreshToken();

    const newRefreshDate = new Date();
    await prisma.session.update({
      data: { refreshTokenGenerated: true, refreshedAt: newRefreshDate },
      where: { id: this.id },
    });
    this._refreshTokenGenrated = true;
    this.refreshedAt = newRefreshDate;

    let tokens = { accessToken, refreshToken };

    events.emit("session:tokens_generated", { session: this, tokens });

    return tokens;
  }

  /**
   * Refresh the session
   *
   * Refreshing the session will generate a new accessToken for the user to authenticate their requests with.
   *
   * **NOTE**: Best practice when implementing token refreshing into the UI is that if for any reason this method
   * throws an error, imediately purge the existing tokens and have the user login again.  This way you don't hold
   * them up any longer than necessary.
   *
   * @param refreshToken - The refresh token provided at session generation
   * @returns {Promise<string>} The new access token.
   */
  public refresh(refreshToken: string): Promise<string> {
    return new Promise(async (res, rej) => {
      if (this.expires.getTime() <= Date.now()) {
        await this.terminate();
        throw new SessionTokenError("Session has Expired.");
      }
      jwt.verify(
        refreshToken,
        refreshTokenPrivateKey,
        {
          algorithms: ["RS256"],
        },
        async (err, decode) => {
          if (err) {
            if (
              err.name == "TokenExpiredError" ||
              err.message == "invalid signature"
            )
              this.terminate();
            rej(err);
          }
          const data: DecodedSession = decode as DecodedSession;

          if (
            data.sessionKey !== this.sessionKey ||
            data.userID !== this.userId
          )
            rej(
              new SessionTokenError(
                "Refresh token does not match this session."
              )
            );

          await prisma.session.update({
            data: { refreshedAt: new Date() },
            where: { id: this.id },
          });

          const newToken = this._generateAccessToken();
          events.emit("session:token_refresh", {
            session: this,
            tokens: { accessToken: newToken, refreshToken },
          });

          return res(newToken);
        }
      );
    });
  }

  /**
   * Fetch Session User
   *
   * Fetch the user controller of the user that created the session.
   *
   * @returns {Promise<UserController>} The user that created this session.
   */
  public async fetchUser(): Promise<UserController> {
    return (await users.fetchUser({ id: this.userId }))!;
  }
}
