import { PassThrough } from 'stream';
import csv from 'csv-parser';
import db from '@/db/models';
import UsersDBApi from '@/db/api/users';
import config from '@/shared/config';
import ValidationError from '@/shared/errors/validation';
import AuthService from '@/services/auth';
import type { CurrentUser } from '@/db/api/types';

type CreateData = Parameters<typeof UsersDBApi.create>[0]['data'];
type UpdateData = Parameters<typeof UsersDBApi.update>[1];
type ListFilter = Parameters<typeof UsersDBApi.findAll>[0];
type BulkRow = Parameters<typeof UsersDBApi.bulkImport>[0][number];

/** Parses an uploaded CSV buffer into bulk-import rows. */
function parseCsvRows(fileBuffer: Buffer): Promise<BulkRow[]> {
  const bufferStream = new PassThrough();
  const results: BulkRow[] = [];
  bufferStream.end(Buffer.from(fileBuffer));

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

class UsersService {
  static async create(
    data: CreateData,
    currentUser?: CurrentUser,
    sendInvitationEmails = true,
    host?: string,
  ) {
    const transaction = await db.sequelize.transaction();
    const globalAccess = currentUser?.app_role?.globalAccess ?? false;

    const email = data.email;
    const emailsToInvite: string[] = [];

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

        await UsersDBApi.create({ data }, globalAccess, {
          currentUser,
          transaction,
        });
        emailsToInvite.push(email);
      } else {
        throw new ValidationError('iam.errors.emailRequired');
      }
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }

    if (emailsToInvite.length) {
      if (!sendInvitationEmails) return;

      AuthService.sendPasswordResetEmail(emailsToInvite[0], 'invitation', host);
    }
  }

  static async bulkImport(
    fileBuffer: Buffer,
    currentUser?: CurrentUser,
    sendInvitationEmails = true,
    host?: string,
  ) {
    const rows = await parseCsvRows(fileBuffer);
    const emailsToInvite: string[] = [];
    const transaction = await db.sequelize.transaction();

    try {
      const hasAllEmails = rows.every((row) => row.email);

      if (!hasAllEmails) {
        throw new ValidationError('importer.errors.userEmailMissing');
      }

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

      for (const row of rows) {
        if (row.email) emailsToInvite.push(row.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: UpdateData, id: string, currentUser?: CurrentUser) {
    const transaction = await db.sequelize.transaction();
    const globalAccess = currentUser?.app_role?.globalAccess ?? false;

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

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

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

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

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

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

      if (
        currentUser?.app_role?.name !== config.roles.admin &&
        currentUser?.app_role?.name !== config.roles.super_admin
      ) {
        throw new ValidationError('errors.forbidden.message');
      }

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

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

  static async deleteByIds(ids: string[], currentUser?: CurrentUser) {
    const transaction = await db.sequelize.transaction();
    try {
      await UsersDBApi.deleteByIds(ids, { currentUser, transaction });
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }

  static list(
    filter: ListFilter,
    globalAccess: boolean,
    currentUser?: CurrentUser,
  ) {
    return UsersDBApi.findAll(filter, globalAccess, { currentUser });
  }

  static count(
    filter: ListFilter,
    globalAccess: boolean,
    currentUser?: CurrentUser,
  ) {
    return UsersDBApi.findAll(filter, globalAccess, {
      countOnly: true,
      currentUser,
    });
  }

  static autocomplete(
    query: string | undefined,
    limit: number | undefined,
    offset: number | undefined,
    globalAccess: boolean,
    organizationId?: string,
  ) {
    return UsersDBApi.findAllAutocomplete(
      query,
      limit,
      offset,
      globalAccess,
      organizationId,
    );
  }

  static findById(id: string) {
    return UsersDBApi.findBy({ id });
  }
}

export default UsersService;
