import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IRepositoryManager, Password } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
  DomainError,
  Email,
  ICreateUser_Request_DTO,
  IDomainError,
  Language,
  Name,
  Result,
  UniqueID,
  ensureEmailIsValid,
  ensureIdIsValid,
  ensureNameIsValid,
} from "@shared/contexts";
import { IUserRepository, User, UserRole } from "../domain";

export type CreateUserResponseOrError =
  | Result<never, IUseCaseError> // Misc errors (value objects)
  | Result<User, never>; // Success!

export class CreateUserUseCase
  implements IUseCase<ICreateUser_Request_DTO, Promise<CreateUserResponseOrError>>
{
  private _adapter: ISequelizeAdapter;
  private _repositoryManager: IRepositoryManager;

  constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
    this._adapter = props.adapter;
    this._repositoryManager = props.repositoryManager;
  }

  async execute(request: ICreateUser_Request_DTO) {
    const { id, name, email } = request;

    // Validaciones de datos
    const idOrError = ensureIdIsValid(id);
    if (idOrError.isFailure) {
      const message = idOrError.error.message; //`User ID ${userDTO.id} is not valid`;
      return Result.fail(
        UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }])
      );
    }

    const nameOrError = ensureNameIsValid(name);
    if (nameOrError.isFailure) {
      const message = nameOrError.error.message; //`User ID ${userDTO.id} is not valid`;
      return Result.fail(
        UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "name" }])
      );
    }

    const emailOrError = ensureEmailIsValid(email);
    if (emailOrError.isFailure) {
      const message = emailOrError.error.message;
      return Result.fail(
        UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "email" }])
      );
    }

    // Comprobar que no existe un usuario previo con esos datos
    const userRepository = this._getUserRepository();

    const idExists = await userRepository().existsUserWithId(idOrError.object);
    if (idExists) {
      const message = `Another user with same ID exists`;
      return Result.fail(
        UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
          path: "id",
        })
      );
    }

    const nameExists = await userRepository().existsUserWithName(nameOrError.object);
    if (nameExists) {
      const message = `Another user with same name exists`;
      return Result.fail(
        UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
          path: "name",
        })
      );
    }

    const emailExists = await userRepository().existsUserWithEmail(emailOrError.object);

    if (emailExists) {
      const message = `Another user with same email exists`;
      return Result.fail(
        UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
          path: "email",
        })
      );
    }

    // Crear user
    const userOrError = this._tryCreateUserInstance(request, idOrError.object);

    if (userOrError.isFailure) {
      const { error: domainError } = userOrError;
      let errorCode = "";
      let message = "";

      switch (domainError.code) {
        case User.ERROR_USER_WITHOUT_NAME:
          errorCode = UseCaseError.INVALID_INPUT_DATA;
          message = "El usuario debe tener un nombre.";
          break;

        case DomainError.INVALID_INPUT_DATA:
          errorCode = UseCaseError.INVALID_INPUT_DATA;
          message = "El usuario tiene algún dato erróneo.";
          break;

        default:
          errorCode = UseCaseError.UNEXCEPTED_ERROR;
          message = domainError.message;
          break;
      }

      return Result.fail(UseCaseError.create(errorCode, message, domainError));
    }

    return this._saveUser(userOrError.object);
  }

  private async _saveUser(user: User) {
    // Guardar el contacto
    const transaction = this._adapter.startTransaction();
    const userRepository = this._getUserRepository();
    let userRepo: IUserRepository;

    try {
      await transaction.complete(async (t) => {
        userRepo = userRepository({ transaction: t });
        await userRepo.create(user);
      });

      return Result.ok<User>(user);
    } catch (error: unknown) {
      const _error = error as IInfrastructureError;
      return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
    }
  }

  private _tryCreateUserInstance(
    userDTO: ICreateUser_Request_DTO,
    userId: UniqueID
  ): Result<User, IDomainError> {
    const nameOrError = Name.create(userDTO.name);
    if (nameOrError.isFailure) {
      return Result.fail(nameOrError.error);
    }

    const emailOrError = Email.create(userDTO.email);
    if (emailOrError.isFailure) {
      return Result.fail(emailOrError.error);
    }

    const passwordOrError = Password.createFromPlainTextPassword(userDTO.password);
    if (passwordOrError.isFailure) {
      return Result.fail(passwordOrError.error);
    }

    return User.create(
      {
        name: nameOrError.object,
        email: emailOrError.object,
        password: passwordOrError.object,
        roles: [UserRole.ROLE_USER],
        language: Language.createDefaultCode().object,
      },
      userId
    );
  }

  private _getUserRepository() {
    return this._repositoryManager.getRepository<IUserRepository>("User");
  }
}
