const db = require('../db/models');
const UsersDBApi = require('../db/api/users');
const processFile = require('../middlewares/upload');
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
const config = require('../config');
const stream = require('stream');

const AuthService = require('./auth');

const BUSINESS_ASSIGNABLE_ROLE_NAMES = [
  config.roles.super_admin,
  config.roles.admin,
  config.roles.concierge,
  config.roles.customer,
];

const HIGH_TRUST_ROLE_NAMES = [
  config.roles.super_admin,
  config.roles.admin,
];

async function findRoleById(roleId, transaction) {
  if (!roleId) {
    return null;
  }

  return db.roles.findByPk(roleId, { transaction });
}

async function findDefaultRole(transaction) {
  return db.roles.findOne({
    where: { name: config.roles.user },
    transaction,
  });
}

function isAdminOrSuperAdmin(currentUser) {
  return [config.roles.admin, config.roles.super_admin].includes(currentUser?.app_role?.name);
}

function isHighTrustRole(roleName) {
  return Boolean(roleName) && HIGH_TRUST_ROLE_NAMES.includes(roleName);
}

async function assertManageableExistingUser(user, currentUser) {
  if (!user) {
    throw new ValidationError('iam.errors.userNotFound');
  }

  if (!isAdminOrSuperAdmin(currentUser)) {
    throw new ValidationError('errors.forbidden.message');
  }

  if (currentUser?.id === user.id) {
    throw new ValidationError('iam.errors.deletingHimself');
  }

  if (!currentUser?.app_role?.globalAccess && isHighTrustRole(user?.app_role?.name)) {
    throw new ValidationError('errors.forbidden.message');
  }
}

async function normalizeManagedUserPayload(data, currentUser, transaction, existingUser = null) {
  const payload = { ...data };
  const isSuperAdmin = Boolean(currentUser?.app_role?.globalAccess);
  const currentOrganizationId = currentUser?.organization?.id || currentUser?.organizationId || null;

  if (!payload.app_role) {
    payload.app_role = existingUser?.app_role?.id || null;
  }

  let selectedRole = await findRoleById(payload.app_role, transaction);

  if (!selectedRole && !existingUser) {
    selectedRole = await findDefaultRole(transaction);
    if (selectedRole) {
      payload.app_role = selectedRole.id;
    }
  }

  if (selectedRole && !BUSINESS_ASSIGNABLE_ROLE_NAMES.includes(selectedRole.name)) {
    throw new ValidationError('errors.forbidden.message');
  }

  if (existingUser?.app_role?.name && !isSuperAdmin && isHighTrustRole(existingUser.app_role.name)) {
    throw new ValidationError('errors.forbidden.message');
  }

  if (selectedRole?.name && !isSuperAdmin && isHighTrustRole(selectedRole.name)) {
    throw new ValidationError('errors.forbidden.message');
  }

  if (!isSuperAdmin) {
    payload.custom_permissions = existingUser?.custom_permissions?.map((permission) => permission.id) || [];
    payload.organizations = currentOrganizationId || existingUser?.organizations?.id || null;
  }

  if (!payload.organizations && existingUser?.organizations?.id) {
    payload.organizations = existingUser.organizations.id;
  }

  return payload;
}

module.exports = class UsersService {
  static async create(data, currentUser, sendInvitationEmails = true, host) {
    const transaction = await db.sequelize.transaction();
    const email = data.email;
    const emailsToInvite = [];

    try {
      if (!email) {
        throw new ValidationError('iam.errors.emailRequired');
      }

      const user = await UsersDBApi.findBy({ email }, { transaction });
      if (user) {
        throw new ValidationError('iam.errors.userAlreadyExists');
      }

      const normalizedPayload = await normalizeManagedUserPayload(data, currentUser, transaction);

      await UsersDBApi.create(
        { data: normalizedPayload },
        currentUser.app_role.globalAccess,
        {
          currentUser,
          transaction,
        },
      );

      emailsToInvite.push(email);
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }

    if (emailsToInvite.length && sendInvitationEmails) {
      AuthService.sendPasswordResetEmail(email, 'invitation', host);
    }
  }

  static async bulkImport(req, res, sendInvitationEmails = true, host) {
    const transaction = await db.sequelize.transaction();
    let emailsToInvite = [];

    try {
      await processFile(req, res);
      const bufferStream = new stream.PassThrough();
      const results = [];

      await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8'));

      await new Promise((resolve, reject) => {
        bufferStream
          .pipe(csv())
          .on('data', (data) => results.push(data))
          .on('end', resolve)
          .on('error', reject);
      });

      const hasAllEmails = results.every((result) => result.email);
      if (!hasAllEmails) {
        throw new ValidationError('importer.errors.userEmailMissing');
      }

      await UsersDBApi.bulkImport(results, {
        transaction,
        ignoreDuplicates: true,
        validate: true,
        currentUser: req.currentUser,
      });

      emailsToInvite = results.map((result) => result.email);
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }

    if (emailsToInvite.length && !sendInvitationEmails) {
      emailsToInvite.forEach((email) => {
        AuthService.sendPasswordResetEmail(email, 'invitation', host);
      });
    }
  }

  static async update(data, id, currentUser) {
    const transaction = await db.sequelize.transaction();

    try {
      const user = await UsersDBApi.findBy({ id }, { transaction });

      if (!user) {
        throw new ValidationError('iam.errors.userNotFound');
      }

      if (!currentUser?.app_role?.globalAccess) {
        if (currentUser?.id === id) {
          throw new ValidationError('errors.forbidden.message');
        }

        if (isHighTrustRole(user?.app_role?.name)) {
          throw new ValidationError('errors.forbidden.message');
        }
      }

      const normalizedPayload = await normalizeManagedUserPayload(data, currentUser, transaction, user);

      const updatedUser = await UsersDBApi.update(
        id,
        normalizedPayload,
        currentUser.app_role.globalAccess,
        {
          currentUser,
          transaction,
        },
      );

      await transaction.commit();
      return updatedUser;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async remove(id, currentUser) {
    const transaction = await db.sequelize.transaction();

    try {
      const user = await UsersDBApi.findBy({ id }, { transaction });

      await assertManageableExistingUser(user, currentUser);

      await UsersDBApi.remove(id, {
        currentUser,
        transaction,
      });

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static async deleteByIds(ids, currentUser) {
    const transaction = await db.sequelize.transaction();

    try {
      const normalizedIds = Array.from(new Set((Array.isArray(ids) ? ids : []).filter(Boolean)));

      if (!normalizedIds.length) {
        return [];
      }

      const users = await Promise.all(
        normalizedIds.map((id) => UsersDBApi.findBy({ id }, { transaction })),
      );

      for (const user of users) {
        await assertManageableExistingUser(user, currentUser);
      }

      const deletedUsers = await UsersDBApi.deleteByIds(normalizedIds, {
        currentUser,
        transaction,
      });

      await transaction.commit();
      return deletedUsers;
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
};
