import UserController from "./UserController";
import { Collection } from "@discordjs/collection";
import jwt, { JsonWebTokenError } from "jsonwebtoken";
import { permissionsPrivateKey, prisma } from "../constants";
import PermissionsVerificationError from "../Errors/PermissionsVerificationError";
import { mergeArrays } from "../modules/tools/mergeArrays";
import { signPermissions } from "../modules/permission-utils/signPermissions";
import { DecodedPermissionsBlock } from "../types/PermissionTypes";
import { permissionValidator } from "../modules/permission-utils/permissionValidator";
import { z } from "zod";
import { roles } from "../managers/roles";
import GenericError from "../Errors/GenericError";
import RoleError from "../Errors/RoleError";
import { Role, User } from "../../generated/prisma/client";
import { events } from "../modules/globalEvents";

/**
 * Roles
 *
 * Roles are for adding onto a users permissions.  They are not for defining a default users permissions.
 *
 * Roles have two forms of identifiers, titles and monikers.  The title is something that can be capitalized and publicly displayed,
 * so if you make modifications, and you need to be able to see how they were able to do that without having explicit permission to
 * make the modifications, the title is what would be shown to you.  The moniker is the human readable identifier.  So in an api
 * response where you need to get the roles of a user, instead of being given a bunch of id's, you will be given a bunch of monikers
 * which are easy to read and easy to identify.
 */
export class RoleController {
  public readonly id: string;
  public title: string;
  public moniker: string; // e.g. admin, super_admin, moderator

  private _permissionsToken: string;
  private _users: (User & { roles: Role[] })[];

  /** Cached result of JWT verification — avoids repeated RSA verify calls. */
  private _cachedVerifiedPermissions: { permissions: string[] } | null = null;

  public readonly createdAt: Date;
  public updatedAt: Date;

  public deleted: boolean = false;

  constructor(roledata: Role & { users: (User & { roles: Role[] })[] }) {
    this.id = roledata.id;
    this.title = roledata.title;
    this.moniker = roledata.moniker;

    this._permissionsToken = roledata.permissions;
    this._users = roledata.users;

    this.createdAt = roledata.createdAt;
    this.updatedAt = roledata.updatedAt;
  }

  private _signPermissions = signPermissions;

  /**
   * Verify Permissions
   *
   * This method is vital to maintining the security of this system.  This method is here to ensure
   * that the permissions object is authentic and signed by our system.
   *
   * @param permissionsToken - Signed permissions JWT
   * @returns - Verified object with permissions in it.
   */
  private _verifyPermissions(permissionsToken: string) {
    // Return cached result if the token hasn't changed
    if (
      this._cachedVerifiedPermissions &&
      permissionsToken === this._permissionsToken
    ) {
      return this._cachedVerifiedPermissions;
    }

    let perms: DecodedPermissionsBlock;
    try {
      perms = jwt.verify(permissionsToken, permissionsPrivateKey, {
        algorithms: ["RS256"],
        issuer: "roles",
        subject: this.id,
      }) as DecodedPermissionsBlock;
    } catch (err) {
      events.emit("role:permissions:verification_error", {
        currentSigned: this._permissionsToken,
        attemptedVerification: permissionsToken,
        err: err as Error,
        role: this,
      });
      throw new PermissionsVerificationError(
        `Unable to verify permissions for role '${this.title}, it is recommended that you override and rewrite these permissions immediately.`,
        (err as Error).message
      );
    }

    const result = perms as { permissions: string[] };
    // Cache only if verifying the current token
    if (permissionsToken === this._permissionsToken) {
      this._cachedVerifiedPermissions = result;
    }
    return result;
  }

  /**
   * Get Users
   *
   * This will get all the users that have this role and return it as a collection dictionary where the key is
   * the `id` of the user and the value is the users `UserController`.
   *
   * @returns {Collection<string, UserController>} - A collection of all the users that are assigned to this role
   */
  public getUsers() {
    const collection = new Collection<string, UserController>();
    this._users.map((v) => collection.set(v.id, new UserController(v)));
    return collection;
  }

  /**
   * Check Permission
   *
   * Check to see if a role has a specified set of permissions.
   *
   * @param permission - The permission to check for
   * @returns {boolean} Does this role have the specified permission
   */
  public checkPermission(permission: string): boolean {
    const permissions = this._verifyPermissions(this._permissionsToken);
    return permissionValidator(permission, permissions.permissions);
  }

  /**
   * Set permissions
   *
   * This will remove all existing permissions and replate them with the permissions defined in the params
   * of this method.
   *
   * @param permissions - Array of all Permissions
   * @returns {void}
   */
  public async setPermissions(...permissions: string[]) {
    /*
    Make sure the current permissions are verified before updating them again.  Basically speaking if the permissions
    are tampered with in any way, this bricks the further modification of the permissions
    */
    const previous = this._verifyPermissions(this._permissionsToken);

    const newPermissionsToken = this._signPermissions({
      issuer: "roles",
      subject: this.id,
      permissions,
    });
    const newRaw = await prisma.role.update({
      where: { id: this.id },
      data: { permissions: newPermissionsToken },
    });

    events.emit("role:permissions:set", {
      current: permissions,
      currentSigned: newPermissionsToken,
      previous: previous.permissions,
      previousSigned: this._permissionsToken,
      role: this,
    });

    this._permissionsToken = newPermissionsToken;
    this.updatedAt = newRaw.updatedAt;
    return;
  }

  /**
   * Add Permissions
   *
   * This will take the permissions provided in the params of the method and combine them into the pre-existing
   * permissions of this role.
   *
   * @param permissions - Array of added Permissions
   * @returns {void}
   */
  public async addPermissions(...permissions: string[]) {
    /*
      Make sure the current permissions are verified before updating them again.  Basically speaking if the permissions
      are tampered with in any way, this bricks the further modification of the permissions
      */
    const previous = this._verifyPermissions(this._permissionsToken);

    const newPermissionsToken = this._signPermissions({
      issuer: "roles",
      subject: this.id,
      permissions: mergeArrays(previous.permissions, permissions),
    });
    const newRaw = await prisma.role.update({
      where: { id: this.id },
      data: { permissions: newPermissionsToken },
    });

    events.emit("role:permissions:added", {
      previous: previous.permissions,
      previousSigned: this._permissionsToken,
      added: permissions,
      currentSigned: newPermissionsToken,
      role: this,
    });

    this._permissionsToken = newPermissionsToken;
    this.updatedAt = newRaw.updatedAt;
    return;
  }

  /**
   * Remove Permissions
   *
   * This will take the permissions provided in the params of the method and remove them from the exisitng
   * permissions object for this role.
   *
   * @param permissions - Array of removed Permissions
   * @returns {void}
   */
  public async removePermissions(...permissions: string[]) {
    /*
      Make sure the current permissions are verified before updating them again.  Basically speaking if the permissions
      are tampered with in any way, this bricks the further modification of the permissions
      */
    const previous = this._verifyPermissions(this._permissionsToken);

    const newPermissionsToken = this._signPermissions({
      issuer: "roles",
      subject: this.id,
      permissions: previous.permissions.filter((v) => !permissions.includes(v)),
    });
    const newRaw = await prisma.role.update({
      where: { id: this.id },
      data: { permissions: newPermissionsToken },
    });

    events.emit("role:permissions:removed", {
      previous: previous.permissions,
      previousSigned: this._permissionsToken,
      removed: permissions,
      currentSigned: newPermissionsToken,
      role: this,
    });

    this._permissionsToken = newPermissionsToken;
    this.updatedAt = newRaw.updatedAt;
    return;
  }

  /**
   * Get Current Permissions
   *
   * Get all of the permissions for this role.
   * @returns {string[]} - Existing permissions in an array
   */
  public getPermissions() {
    const permissions = this._verifyPermissions(this._permissionsToken!);
    return permissions;
  }

  public async update(
    data: Partial<{
      title: string;
      moniker: string;
      permissions: string[];
    }>
  ) {
    const schema = z
      .object({
        title: z.string().min(1, "Title cannot be empty."),
        moniker: z.string().min(1, "Moniker cannot be empty."),
        permissions: z.array(
          z.string().min(1, "Permission node cannot be empty")
        ),
      })
      .partial()
      .strict();

    data = schema.parse(data);

    if (data.moniker) {
      const checkMoniker = await prisma.role.findFirst({
        where: { moniker: data.moniker },
      });

      if (checkMoniker && checkMoniker.moniker !== this.moniker)
        throw new RoleError(
          "Moniker is already taken.",
          "Another role with this moniker already exists in the databse."
        );
    }

    const updatedRole = await prisma.role.update({
      where: { id: this.id },
      data: {
        ...data,
        permissions: undefined,
      },
    });

    if (data.permissions) await this.setPermissions(...data.permissions);

    events.emit("role:updated", { role: this, updateData: data });
    this.title = data.title ?? this.title;
    this.moniker = data.moniker ?? this.moniker;

    return this;
  }

  /**
   * Delete Role
   *
   * @returns {Promise<RoleController>} The Remains of a deleted role.
   */
  public async delete() {
    const deletedData = await prisma.role.delete({ where: { id: this.id } });

    this.deleted = true;
    events.emit("role:deleted", this);
    return this;
  }

  /**
   * To JSON
   *
   * Create a JSON object that can be used in things like API responses.
   *
   * @param opts - Optional values to include in the response.
   * @returns - JSON-Friendly object.
   */
  public toJson(opts?: { viewUsers?: boolean; viewPermissions?: boolean }) {
    let object = {
      id: this.id,
      title: this.title,
      moniker: this.moniker,
      permissions: opts?.viewPermissions
        ? this._verifyPermissions(this._permissionsToken).permissions
        : undefined,
      users: opts?.viewUsers
        ? this._users.map((v) => ({
            id: v.id,
            name:
              `${v.firstName ?? ""} ${v.lastName ?? ""}`.trim() ||
              v.login ||
              v.email,
            login: v.login,
            roles: v.roles.map((r: any) => r.id),
          }))
        : undefined,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };

    return object;
  }
}
