import { Types } from 'mongoose';
import { NewCreatedPermission } from '@/modules/permissions/permissions.interface';
import {
  COMPANY_SETTINGS_PERMISSIONS,
  COMPANY_SETTINGS_SEED_PERMISSIONS,
  COMPANY_SUPPORT_PERMISSIONS,
} from '@/modules/permissions/permissions.constant';
import { CONSTANTS } from '@/shared/constants';
import { Permissions } from '@/modules/permissions/permissions.model';
import { Role } from '@/modules/roles/roles.model';
import { PermissionAction } from '@/shared/constants/enum.constant';
import { ApiError } from '@/shared/utils/errors';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';
import responseCodes from '@/shared/utils/responseCode/responseCode';

const { PermissionResponseCodes } = responseCodes;

/**
 * Normalizes a given access string or string array to a standardized string array.
 * If the given access is the string 'Full Access', it is converted to an array
 * containing all possible permission actions. If the given access is the string
 * 'No Access', it is converted to an array containing the string 'No Access'.
 * If the given access is an array, it is returned as is. If the given access is
 * a string that is not 'Full Access' or 'No Access', it is converted to an array
 * containing the string.
 * @param access - The access string or string array to normalize.
 * @returns The normalized access string array.
 */
const normalizeAccess = (access: string | string[]): string[] => {
  if (access === CONSTANTS.FULL_ACCESS) return [...PermissionAction];
  if (access === CONSTANTS.NO_ACCESS) return [CONSTANTS.NO_ACCESS];
  return Array.isArray(access) ? access : [];
};

/**
 * Appends default support permissions to the given payload if they are not already present.
 * Default support permissions are permissions that are required for the proper functioning of the system.
 * These permissions are typically created by the system and should not be modified or deleted.
 * @param payload - The payload to append the default permissions to.
 * @returns The payload with the default permissions appended.
 */
export const appendDefaultPermissions = (
  payload: NewCreatedPermission,
): NewCreatedPermission => {
  let finalPayload = [...payload];
  const payloadGroups = payload.map((p) => p.group);

  const missingSupportPermissions = COMPANY_SUPPORT_PERMISSIONS.filter(
    (supportPerm) => !payloadGroups.includes(supportPerm.group),
  );
  finalPayload = [...finalPayload, ...missingSupportPermissions];

  return finalPayload;
};

/**
 * Adds missing seeding permissions to the given payload.
 *
 * This function ensures that the payload includes all necessary seeding
 * permissions by adding any missing settings and support permissions.
 * It first identifies the existing permission groups in the payload,
 * then adds any missing settings permissions from the predefined
 * `COMPANY_SETTINGS_PERMISSIONS`. Finally, it adds any additional
 * support permissions from `COMPANY_SUPPORT_PERMISSIONS` that are
 * not already present.
 *
 * @param payload - The payload of permissions to augment.
 * @returns The updated payload with the missing seeding permissions added.
 */

export const addSeedingPermissions = (
  payload: NewCreatedPermission,
): NewCreatedPermission => {
  const existingGroups = new Set(payload.map((p) => p.group));

  const missingSettings = COMPANY_SETTINGS_PERMISSIONS.filter(
    (perm) => !existingGroups.has(perm.group),
  );

  const updatedGroups = new Set([
    ...existingGroups,
    ...missingSettings.map((p) => p.group),
  ]);
  const missingSupport = COMPANY_SUPPORT_PERMISSIONS.filter(
    (perm) => !updatedGroups.has(perm.group),
  );

  return [...payload, ...missingSettings, ...missingSupport];
};

/**
 * Remove existing settings permissions from the company.
 *
 * This function takes a company ID, role ID, and an array of existing permission
 * IDs. It then finds all settings permissions that are not in the given array
 * and removes them from the role and the permissions collection.
 *
 * @param companyId - The ID of the company to remove permissions from.
 * @param roleId - The ID of the role to remove permissions from.
 * @param currentPermissionIds - An array of existing permission IDs that should
 * not be removed.
 */
const removeExistingSettingsPermissions = async (
  companyId: Types.ObjectId,
  roleId: Types.ObjectId,
  currentPermissionIds: Types.ObjectId[],
): Promise<void> => {
  const settingsGroups = COMPANY_SETTINGS_PERMISSIONS.map((p) => p.group);

  const settingsPermissionIds = await Permissions.find({
    company: companyId,
    group: { $in: settingsGroups },
    _id: { $in: currentPermissionIds },
  }).select('_id');

  const idsToRemove = settingsPermissionIds.map((p) => p._id);

  if (idsToRemove.length === 0) return;

  await Promise.all([
    Role.findByIdAndUpdate(roleId, {
      $pull: { permissions: { $in: idsToRemove } },
    }),
    Permissions.deleteMany({ _id: { $in: idsToRemove } }),
  ]);
};

/**
 * Filter out settings groups from the given payload.
 *
 * This function takes a payload of permissions and returns a new payload that
 * only includes permissions that are not in the settings groups.
 *
 * @param payload - The payload of permissions to filter.
 * @returns A new payload with only the non-settings permissions.
 */
const filterOutSettingsGroups = (
  payload: NewCreatedPermission,
): NewCreatedPermission => {
  const settingsGroups = new Set(
    COMPANY_SETTINGS_PERMISSIONS.map((p) => p.group),
  );
  return payload.filter((p) => !settingsGroups.has(p.group));
};

/**
 * Add missing settings permissions to the payload.
 *
 * This function takes a payload of permissions and adds any missing settings
 * permissions (i.e. permissions that are not in the payload but should be).
 *
 * @param payload - The payload of permissions to check.
 * @returns The payload with the missing settings permissions added.
 */
const addMissingSettingsPermissions = (
  payload: NewCreatedPermission,
): NewCreatedPermission => {
  const existingGroups = new Set(payload.map((p) => p.group));

  const missingSettings = COMPANY_SETTINGS_PERMISSIONS.filter(
    (perm) => !existingGroups.has(perm.group),
  );

  return [...payload, ...missingSettings];
};

/**
 * Handle non-seeding scenario (i.e. role update) by removing existing settings
 * permissions if all settings groups have NO_ACCESS, or add missing settings
 * permissions if any settings group has FULL_ACCESS or PARTIAL_ACCESS.
 *
 * @param payload - The payload to process, which is an array of permission
 * groups. Each group has a group name, group access, and an array of
 * permissions.
 * @param companyId - The ID of the company to which these permissions belong.
 * @param roleId - The ID of the role to which these permissions belong.
 * @param currentPermissionIds - The existing permission IDs that need to be
 * updated or deleted.
 *
 * @returns An array of processed permission groups.
 */
export const handleNonSeedingPermissions = async (
  payload: NewCreatedPermission,
  companyId: Types.ObjectId,
  roleId: Types.ObjectId,
  currentPermissionIds: Types.ObjectId[],
): Promise<NewCreatedPermission> => {
  let finalPayload = appendDefaultPermissions(payload);

  const settingsGroups = new Set(
    COMPANY_SETTINGS_SEED_PERMISSIONS.map((p) => p.group),
  );
  const payloadGroups = new Set(finalPayload.map((p) => p.group));

  const hasCompanySettingsGroup = [...payloadGroups].some((group) =>
    settingsGroups.has(group),
  );

  if (!hasCompanySettingsGroup) return finalPayload;

  const allHaveNoAccess = [...settingsGroups].every((group) => {
    const groupInPayload = finalPayload.find((p) => p.group === group);
    return groupInPayload?.groupAccess === CONSTANTS.NO_ACCESS;
  });

  if (allHaveNoAccess) {
    await removeExistingSettingsPermissions(
      companyId,
      roleId,
      currentPermissionIds,
    );
    finalPayload = filterOutSettingsGroups(finalPayload);
  } else {
    finalPayload = addMissingSettingsPermissions(finalPayload);
  }

  return finalPayload;
};

/**
 * Process permissions in the given payload. This involves updating existing
 * permissions and creating new ones.
 *
 * @param payload - The payload to process, which is an array of permission
 * groups. Each group has a group name, group access, and an array of
 * permissions.
 * @param currentPermissionIds - The existing permission IDs that need to be
 * updated or deleted.
 * @param companyId - The ID of the company to which these permissions belong.
 *
 * @returns An array of IDs of the processed permissions.
 */
export const processPermissions = async (
  payload: NewCreatedPermission,
  currentPermissionIds: Types.ObjectId[],
  companyId: Types.ObjectId,
): Promise<Types.ObjectId[]> => {
  if (payload.length === 0) return [];

  const groups = payload.map((p) => p.group);

  // Single query to get all existing permissions for these groups
  const existingPermissions = await Permissions.find({
    _id: { $in: currentPermissionIds },
    group: { $in: groups },
    company: companyId,
  });

  const existingPermissionMap = new Map(
    existingPermissions.map((perm) => [perm.group, perm]),
  );

  const permissionPromises = payload.map(async (groupPermission) => {
    const { group, groupAccess, permissions, createdBy, updatedBy } =
      groupPermission;

    const normalizedPermissions = permissions.map((p) => ({
      ...p,
      access:
        groupAccess === CONSTANTS.FULL_ACCESS
          ? [...PermissionAction]
          : normalizeAccess(p.access),
    }));

    const existingPermission = existingPermissionMap.get(group);

    let permission;
    if (existingPermission)
      permission = await Permissions.findByIdAndUpdate(
        existingPermission._id,
        {
          $set: {
            groupAccess,
            permissions: normalizedPermissions,
            updatedBy,
          },
        },
        { new: true },
      );
    else
      permission = await Permissions.create({
        group,
        groupAccess,
        permissions: normalizedPermissions,
        createdBy,
        updatedBy,
        company: companyId,
      });

    if (!permission)
      throw new ApiError(
        defaultStatus.OK,
        `Failed to process permission group: ${group}`,
        true,
        '',
        PermissionResponseCodes.PERMISSION_ERROR,
      );

    return permission._id;
  });

  return Promise.all(permissionPromises);
};
