import { Collection } from "@discordjs/collection";
import { Role, User } from "../../generated/prisma/client";
import { SessionTokensObject } from "./SessionController";
import { sessions } from "../managers/sessions";
import BodyError from "../Errors/BodyError";
import { prisma } from "../constants";
import { events } from "../modules/globalEvents";
import { RoleController } from "./RoleController";
import { roles } from "../managers/roles";
import { signPermissions } from "../modules/permission-utils/signPermissions";
import { DecodedPermissionsBlock } from "../types/PermissionTypes";
import jwt from "jsonwebtoken";
import { permissionsPrivateKey } from "../constants";

export default class UserController {
  public id: string;
  public firstName: string | null;
  public lastName: string | null;
  public login: string;
  public email: string;
  public image: string | null;
  public cwIdentifier: string | null;
  public cwMemberId: number | null;

  private _roles: Collection<string, Role>;
  private _permissions: string | null;

  /** Cached result of fetchRoles() — populated on first hasPermission call. */
  private _resolvedRoleControllers: Collection<string, RoleController> | null =
    null;

  /** Per-permission result cache — avoids repeated JWT verification + DB lookups. */
  private _permissionCache: Map<string, boolean> = new Map();

  public createdAt: Date;
  public updatedAt: Date;

  public get name(): string | null {
    const full = [this.firstName, this.lastName]
      .filter(Boolean)
      .join(" ")
      .trim();
    return full.length > 0 ? full : null;
  }

  private _splitName(name: string): {
    firstName: string | null;
    lastName: string | null;
  } {
    const trimmed = name.trim();
    if (!trimmed) return { firstName: null, lastName: null };

    const parts = trimmed.split(/\s+/);
    const firstName = parts.shift() ?? null;
    const lastName = parts.length > 0 ? parts.join(" ") : null;

    return { firstName, lastName };
  }

  constructor(userdata: User & { roles: Role[] }) {
    this.id = userdata.id;
    this.firstName = userdata.firstName ?? null;
    this.lastName = userdata.lastName ?? null;
    this.login = userdata.login;
    this.email = userdata.email;
    this.image = userdata.image;
    this.cwIdentifier = userdata.cwIdentifier ?? null;
    this.cwMemberId = userdata.cwMemberId ?? null;
    this.updatedAt = userdata.updatedAt;
    this.createdAt = userdata.createdAt;
    this._permissions = userdata.permissions ?? null;

    this._roles = (() => {
      let collection = new Collection<string, Role>();
      userdata.roles.map((v: any) => collection.set(v.id, v));

      return collection;
    })();
  }

  /**
   * Update the internal values
   *
   * This is an internal method used to update all the internal values when we query the database.  This way
   * everything stays upto date even when we pass around the user controller.
   *
   * @param userdata - User object from Prisma
   */
  private _updateInternalValues(userdata: User) {
    this.id = userdata.id;
    this.firstName = userdata.firstName ?? null;
    this.lastName = userdata.lastName ?? null;
    this.login = userdata.login;
    this.email = userdata.email;
    this.image = userdata.image;
    this.cwIdentifier = userdata.cwIdentifier ?? null;
    this.cwMemberId = userdata.cwMemberId ?? null;
    this.updatedAt = userdata.updatedAt;
    this.createdAt = userdata.createdAt;
  }

  /**
   * Create Session
   *
   * This will create a session in the database that is linked to the user and will then create a pair of access and refresh
   * tokens to provide to the user such that they can authorized their api requests.
   *
   * @returns {Promise<SessionTokensObject>} - Object with an access token and a refresh token.
   */
  public async createSession(): Promise<SessionTokensObject> {
    return sessions.create({ user: this });
  }

  /**
   * Update the user
   *
   * Take in a partial of the user data and validate it then updated it if it passes validation and return
   * the updated `UserController` object.
   *
   * @param data - A partial of the user data
   * @returns {Promise<UserController>} - The updated user controller
   */
  public async update(
    data: Partial<Pick<User, "firstName" | "lastName" | "image">> & {
      name?: string;
    }
  ) {
    const updateData: Partial<Pick<User, "firstName" | "lastName" | "image">> =
      {};

    if (data.image !== undefined) updateData.image = data.image;
    if (data.name !== undefined) {
      const parsed = this._splitName(data.name);
      updateData.firstName = parsed.firstName;
      updateData.lastName = parsed.lastName;
    }
    if (data.firstName !== undefined) updateData.firstName = data.firstName;
    if (data.lastName !== undefined) updateData.lastName = data.lastName;

    if (Object.keys(updateData).length == 0)
      throw new BodyError("Body cannot be empty.");

    const updatedUser = await prisma.user.update({
      where: { id: this.id },
      data: updateData,
    });

    this._updateInternalValues(updatedUser);
    events.emit("user:updated", { user: this, updatedValues: updateData });

    return this;
  }

  /**
   * Set Roles
   *
   * Replace the user's roles with the provided array of role identifiers (id or moniker).
   * Validates that each role exists before assigning.
   *
   * @param roleIdentifiers - Array of role ids or monikers to assign
   * @returns {Promise<UserController>} - The updated user controller
   */
  public async setRoles(roleIdentifiers: string[]): Promise<UserController> {
    const resolvedRoles = await Promise.all(
      roleIdentifiers.map((identifier) => roles.fetch(identifier))
    );

    const updatedUser = await prisma.user.update({
      where: { id: this.id },
      data: {
        roles: {
          set: resolvedRoles.map((r) => ({ id: r.id })),
        },
      },
      include: { roles: true },
    });

    this._updateInternalValues(updatedUser);
    this._roles = new Collection<string, Role>();
    updatedUser.roles.map((v: any) => this._roles.set(v.id, v));
    this.clearPermissionCache();

    for (const role of resolvedRoles) {
      events.emit("user:role:assigned", { user: this, role });
    }

    return this;
  }

  /**
   * Set Permissions
   *
   * Replace the user's direct permissions with the provided array of permission strings.
   * Signs the permissions with the user issuer before storing.
   *
   * @param permissions - Array of permission node strings to assign
   * @returns {Promise<UserController>} - The updated user controller
   */
  public async setPermissions(permissions: string[]): Promise<UserController> {
    const signed = signPermissions({
      issuer: "user",
      subject: this.id,
      permissions,
    });

    const updatedUser = await prisma.user.update({
      where: { id: this.id },
      data: { permissions: signed },
    });

    this._updateInternalValues(updatedUser);

    return this;
  }

  /**
   * Read Permissions
   *
   * Verifies and decodes the user's direct permissions JWT and returns the array of
   * permission node strings. Returns an empty array if the user has no direct permissions.
   *
   * @returns {string[]} The user's direct permission nodes
   */
  public readPermissions(): string[] {
    if (!this._permissions) return [];

    const decoded = jwt.verify(this._permissions, permissionsPrivateKey, {
      algorithms: ["RS256"],
      issuer: "user",
      subject: this.id,
    }) as DecodedPermissionsBlock;

    return decoded.permissions;
  }

  /**
   * Read Role Permissions
   *
   * Verifies and decodes a role permissions JWT and returns the permission nodes.
   * Returns an empty array if verification fails.
   *
   * @param role - Role record containing the signed permissions token
   * @returns {string[]} The role permission nodes
   */
  private _readRolePermissions(role: Role): string[] {
    try {
      const decoded = jwt.verify(role.permissions, permissionsPrivateKey, {
        algorithms: ["RS256"],
        issuer: "roles",
        subject: role.id,
      }) as DecodedPermissionsBlock;

      return decoded.permissions;
    } catch {
      return [];
    }
  }

  /**
   * Read All Permissions
   *
   * Aggregates the user's direct permissions and all permissions from their assigned roles
   * into a single deduplicated array.
   *
   * @returns {Promise<string[]>} Combined array of all permission nodes
   */
  public async readAllPermissions(): Promise<string[]> {
    const directPermissions = this.readPermissions();
    const rolePermissions = this._roles
      .map((role) => this._readRolePermissions(role))
      .flatMap((permissions) => permissions);

    return [...new Set([...directPermissions, ...rolePermissions])];
  }

  /**
   * Fetch Roles
   *
   * This method will fetch all of the roles that a user belongs to and will return each of their controllers in a collection
   * of role id's and RoleControllers.
   *
   * @returns {Promise<Collection<string, RoleController>>} A collection of all the roles a user has
   */
  public async fetchRoles(): Promise<Collection<string, RoleController>> {
    const collection = new Collection<string, RoleController>();

    await Promise.all(
      this._roles.map(async (v) =>
        collection.set(v.id, await roles.fetch(v.id))
      )
    );

    return collection;
  }

  /**
   * Check Permission
   *
   * Check if this user has this specific permission.  This method will not only check explicit permissions defined in
   * the database under users and roles, but will also generate implicit permissions for resources that the user has
   * access to but doesn't specifically have defined under any given permissions object.
   *
   * @param permission - The permission to check for
   * @returns {boolean} Does this user have the specified permission
   */
  public async hasPermission(permission: string) {
    // Fast path: return cached result if we already resolved this permission
    const cached = this._permissionCache.get(permission);
    if (cached !== undefined) return cached;

    // Resolve role controllers once and cache them for the lifetime of this
    // controller instance (i.e. the current request).
    if (!this._resolvedRoleControllers) {
      this._resolvedRoleControllers = await this.fetchRoles();
    }

    const result = this._resolvedRoleControllers
      .map((v) => v.checkPermission(permission))
      .includes(true);

    this._permissionCache.set(permission, result);
    return result;
  }

  /**
   * Clear Permission Cache
   *
   * Invalidates the in-memory permission cache so the next
   * `hasPermission` call re-fetches roles from the database.
   * Call this after role or permission mutations on the user.
   */
  public clearPermissionCache() {
    this._resolvedRoleControllers = null;
    this._permissionCache.clear();
  }

  /**
   * To JSON
   *
   * Create an object that can be safely returned to the user of an api request such that when you
   * need to return data to the end user, you don't accidently return data that could be harmful
   * if leaked.
   *
   * Options:
   * - Safe return is to return only data that is considered "safe", and not detrimental to pass around
   *
   * @param opts - Options to change the output
   * @returns - An object that is JSON friendly
   */
  public toJson(opts?: { safeReturn: boolean }) {
    return {
      id: this.id,
      name: this.name,
      firstName: this.firstName,
      lastName: this.lastName,
      roles: opts?.safeReturn
        ? undefined
        : this._roles.size > 0
        ? this._roles.map((v) => v.moniker)
        : undefined,
      permissions: opts?.safeReturn
        ? undefined
        : (() => {
            const directPermissions = this.readPermissions();
            const rolePermissions = this._roles
              .map((role) => this._readRolePermissions(role))
              .flatMap((permissions) => permissions);

            return [...new Set([...directPermissions, ...rolePermissions])];
          })(),
      login: opts?.safeReturn ? undefined : this.login,
      email: opts?.safeReturn ? undefined : this.email,
      cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier,
      cwMemberId: this.cwMemberId,
      image: this.image,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
    };
  }
}
