import { cookies } from "next/headers";
import dayjs from "dayjs";
import type { NextAuthConfig } from "next-auth";

import { and, eq, inArray } from "@homarr/db";
import type { Database } from "@homarr/db";
import { groupMembers, groups, users } from "@homarr/db/schema";
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
import { logger } from "@homarr/log";

import { env } from "./env";
import { extractProfileName } from "./providers/oidc/oidc-provider";

export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
  return async ({ user, profile }) => {
    logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
    if (!user.id) throw new Error("User ID is missing");

    const dbUser = await db.query.users.findFirst({
      where: eq(users.id, user.id),
      columns: {
        name: true,
        colorScheme: true,
      },
    });

    if (!dbUser) throw new Error("User not found");

    const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
    // Groups from oidc provider are provided from the profile, it's not typed.
    if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
      logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
      await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
    }

    // In ldap-authroization we return the groups from ldap, it's not typed.
    if ("groups" in user && Array.isArray(user.groups)) {
      logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
      await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
    }
    await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);

    if (dbUser.name !== user.name) {
      await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
      logger.info(
        `Username for user of credentials provider has changed. user=${user.id} old=${dbUser.name} new=${user.name}`,
      );
    }

    if (profile) {
      const profileUsername = extractProfileName(profile);
      if (!profileUsername) {
        throw new Error(`OIDC provider did not return a name properties='${Object.keys(profile).join(",")}'`);
      }

      if (dbUser.name !== profileUsername) {
        await db.update(users).set({ name: profileUsername }).where(eq(users.id, user.id));
        logger.info(
          `Username for user of oidc provider has changed. user=${user.id} old='${dbUser.name}' new='${profileUsername}'`,
        );
      }
    }

    logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);

    // We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
    (await cookies()).set(colorSchemeCookieKey, dbUser.colorScheme, {
      path: "/",
      expires: dayjs().add(1, "year").toDate(),
    });
  };
};

const addUserToEveryoneGroupIfNotMemberAsync = async (db: Database, userId: string) => {
  const dbEveryoneGroup = await db.query.groups.findFirst({
    where: eq(groups.name, everyoneGroup),
    with: {
      members: {
        where: eq(groupMembers.userId, userId),
      },
    },
  });

  if (dbEveryoneGroup?.members.length === 0) {
    await db.insert(groupMembers).values({
      userId,
      groupId: dbEveryoneGroup.id,
    });
    logger.info(`Added user to everyone group. user=${userId}`);
  }
};

const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: string, externalGroups: string[]) => {
  const ignoredGroups = [everyoneGroup];
  const dbGroupMembers = await db.query.groupMembers.findMany({
    where: eq(groupMembers.userId, userId),
    with: {
      group: { columns: { name: true } },
    },
  });

  /**
   * The below groups are those groups the user is part of in the external system, but not in Homarr.
   * So he has to be added to those groups.
   */
  const missingExternalGroupsForUser = externalGroups.filter(
    (externalGroup) => !dbGroupMembers.some(({ group }) => group.name === externalGroup),
  );

  if (missingExternalGroupsForUser.length > 0) {
    logger.debug(
      `Homarr does not have the user in certain groups. user=${userId} count=${missingExternalGroupsForUser.length}`,
    );

    const groupIds = await db.query.groups.findMany({
      columns: {
        id: true,
      },
      where: inArray(groups.name, missingExternalGroupsForUser),
    });

    logger.debug(`Homarr has found groups in the database user is not in. user=${userId} count=${groupIds.length}`);

    if (groupIds.length > 0) {
      await db.insert(groupMembers).values(
        groupIds.map((group) => ({
          userId,
          groupId: group.id,
        })),
      );

      logger.info(`Added user to groups successfully. user=${userId} count=${groupIds.length}`);
    } else {
      logger.debug(`User is already in all groups of Homarr. user=${userId}`);
    }
  }

  /**
   * The below groups are those groups the user is part of in Homarr, but not in the external system and not ignored.
   * So he has to be removed from those groups.
   */
  const groupsUserIsNoLongerMemberOfExternally = dbGroupMembers.filter(
    ({ group }) => !externalGroups.concat(ignoredGroups).includes(group.name),
  );

  if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
    logger.debug(
      `Homarr has the user in certain groups that LDAP does not have. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
    );

    await db.delete(groupMembers).where(
      and(
        eq(groupMembers.userId, userId),
        inArray(
          groupMembers.groupId,
          groupsUserIsNoLongerMemberOfExternally.map(({ groupId }) => groupId),
        ),
      ),
    );

    logger.info(
      `Removed user from groups successfully. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
    );
  }
};
