import { Collection } from "@discordjs/collection";
import GenericError from "../Errors/GenericError";
import RoleError from "../Errors/RoleError";
import { prisma } from "../constants";
import { RoleController } from "../controllers/RoleController";
import { signPermissions } from "../modules/permission-utils/signPermissions";
import cuid from "cuid";
import UserController from "../controllers/UserController";
import InsufficientPermission from "../Errors/InsufficientPermission";
import { z } from "zod";

export const roles = {
  /**
   * Create a role
   *
   * @param data - Data required to make a role
   * @returns {RoleController} - The new role
   */
  async create(data: {
    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, "Cannot have a blank permission node."))
        .optional(),
    });

    data = await schema.parseAsync(data);

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

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

    const id = cuid();
    const newRole = await prisma.role.create({
      data: {
        id,
        title: data.title,
        moniker: data.moniker,
        permissions: signPermissions({
          issuer: "roles",
          subject: id,
          permissions: data.permissions ?? [],
        }),
      },
      include: {
        users: {
          include: {
            roles: true,
          },
        },
      },
    });

    const controller = new RoleController(newRole);

    return controller;
  },

  /**
   * Fetch a role
   *
   * Fetch a role using either it's id or it's moniker.
   *
   * @param identifier - Role `id` or `moniker`
   * @param identifier - Options for fetching a role.
   * @returns {RoleController} - Role Controller
   */
  async fetch(identifier: string, opt?: { requestingUser?: UserController }) {
    const roleData = await prisma.role.findFirst({
      where: { OR: [{ id: identifier }, { moniker: identifier }] },
      include: {
        users: {
          include: {
            roles: true,
          },
        },
      },
    });

    if (!roleData)
      throw new GenericError({
        name: "UnknownRole",
        message: "Unknown role...",
        status: 404,
      });

    if (
      opt?.requestingUser &&
      !(await opt.requestingUser.hasPermission(
        this._buildPermissionNode(roleData.id, "read"),
      ))
    )
      throw new InsufficientPermission(
        "You do not have permission to access this role.",
      );
    const controller = new RoleController(roleData);

    return controller;
  },

  /**
   * Fetch all roles
   *
   * This will give you all of the roles and their respective controllers
   *
   * @param opt - Options for fetching all roles.
   * @returns {Collection<string, RoleController>} A collection of all the roles and their id's
   */
  async fetchAllRoles(opt?: { requestingUser?: UserController }) {
    let collection = new Collection<string, RoleController>();
    const roles = await prisma.role.findMany({
      include: { users: { include: { roles: true } } },
    });

    roles.map((v: any) => collection.set(v.id, new RoleController(v)));

    if (opt?.requestingUser) {
      const permittedRoles = await Promise.all(
        collection.map(async (v) =>
          (await opt.requestingUser?.hasPermission(
            this._buildPermissionNode(v.id, "read"),
          ))
            ? v.id
            : null,
        ),
      );
      collection = collection.filter((v) =>
        permittedRoles.filter((x) => x !== null).includes(v.id),
      );
    }

    return collection;
  },

  /**
   * Build Permissino Node
   *
   * Build a role centric permission node with a role id and permission scope.
   *
   * **FORMAT**: `roles.{id}.{scope}
   *
   * @param id - Role ID
   * @param scope - Scope of the permission node
   * @returns {string} Constructed Permission Node
   */
  _buildPermissionNode(id: string, scope: "read" | "write") {
    return ["roles", id, scope].join(".");
  },
};

/**
 * @TODO automatic string transformation for monikers to be all lower case with no spaces and only underscores.
 *
 * @TODO create and inheritance and ordering system @see https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
 *
 * @TODO go through and make sure all the zod schemas have mins, maxes, etc.
 *
 * @TODO Limit those who can give out the `*` permission to those with the `*` permission.
 *        - Maybe also limit the use of `*` all together?
 */
