import { User } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { SessionTokensObject } from "../controllers/SessionController";
import UserController from "../controllers/UserController";
import { fetchMicrosoftUser } from "../modules/fetchMicrosoftUser";
import { findCwIdentifierByEmail } from "../modules/cw-utils/members/fetchAllMembers";
import { events } from "../modules/globalEvents";
import { sessions } from "./sessions";
import * as msal from "@azure/msal-node";

export const users = {
  /**
   * Authenticate The User
   *
   * If the user has already been registered, this function will supply them with a session id, otherwise
   * this method will create an account, then create a session id.
   *
   * @summary It creates a user if one doesn't exist and will supply a session id
   *
   * @async
   * @param authRequest - The code supplied in the callback url of the Microsoft oAuth transaction
   */
  async authenticate(
    authRequest: msal.AuthenticationResult
  ): Promise<SessionTokensObject> {
    let id = authRequest.uniqueId as string;

    let user =
      (await this.fetchUser({ userId: id })) ??
      (await this.createUser(authRequest.accessToken));

    const tokens = await sessions.create({ user });
    events.emit("user:authenticated", { user, tokens });

    return tokens;
  },

  /**
   * Check to see if the user exists
   *
   * @param partial - A partial object that you can feed any value from the database into.
   * @returns {Promise<boolean>} Does the user exist?
   */
  async userExists(partial: Partial<User>): Promise<boolean> {
    const match = await prisma.user.findFirst({ where: partial });
    return !!match;
  },

  /**
   * Fetch a user
   *
   * This method takes in a unique identifier for the user you are trying to fetch, and returns
   * the controller for the user if the user exists.  If the user doesn't exist, or no identifier
   * was provided, this will reutrn `null`.
   *
   * @param identifier - A partial identifier for a user (e.g. id, email, userID, etc.)
   * @returns {Promise<UserController>} The controller for the user
   */
  async fetchUser(
    identifier: Partial<{
      id: string;
      email: string;
      login: string;
      userId: string;
    }>
  ) {
    if (Object.keys(identifier).length == 0) return null;
    const userData = await prisma.user.findFirst({
      where: {
        //@ts-ignore
        OR: Object.keys(identifier).map((v) => ({ [v]: identifier[v] })),
      },
      include: { roles: true },
    });

    if (!userData) return null;

    return new UserController(userData);
  },

  /**
   * Create a new user
   *
   * This method will poll Microsoft and get all the information on the user to then create the
   * record in our database. On top of that it also pushes it into the user cache.
   *
   * @param token - The Microsoft token provided by the auth method
   * @returns {Promise<UserController>} The new user controller for the user
   */
  async createUser(token: string): Promise<UserController> {
    const msData = await fetchMicrosoftUser(token);
    const resolvedEmail = (msData.mail ?? msData.userPrincipalName ?? "")
      .trim()
      .toLowerCase();

    if (!resolvedEmail) {
      throw new Error("Microsoft account did not include an email address.");
    }

    const resolvedLogin = (msData.userPrincipalName ?? resolvedEmail)
      .trim()
      .toLowerCase();

    // Attempt to resolve the user's ConnectWise identifier by email
    const cwIdentifier = await findCwIdentifierByEmail(resolvedEmail).catch(() => null);

    const existingUser = await prisma.user.findFirst({
      where: { email: resolvedEmail },
      select: { id: true },
    });

    const userData = {
      userId: msData.id,
      firstName: msData.givenName ?? null,
      lastName: msData.surname ?? null,
      login: resolvedLogin,
      cwIdentifier,
      token,
    };

    const newUser = existingUser
      ? await prisma.user.update({
          where: { email: resolvedEmail },
          data: userData,
          include: { roles: true },
        })
      : await prisma.user.create({
          data: { email: resolvedEmail, ...userData },
          include: { roles: true },
        });

    const controller = new UserController(newUser);
    if (!existingUser) {
      events.emit("user:created", controller);
    }

    return controller;
  },

  /**
   * Fetch all users
   *
   * Returns an array of UserController instances for every user in the database.
   *
   * @returns {Promise<UserController[]>} Array of user controllers
   */
  async fetchAllUsers(): Promise<UserController[]> {
    const allUsers = await prisma.user.findMany({
      include: { roles: true },
    });

    return allUsers.map((u) => new UserController(u));
  },

  /**
   * Delete a user
   *
   * Removes a user record from the database by their id.
   *
   * @param userId - The id of the user to delete
   */
  async deleteUser(userId: string): Promise<void> {
    await prisma.user.delete({ where: { id: userId } });
    events.emit("user:deleted", { id: userId });
  },
};
