import { v4 as uuidv4 } from "uuid";

import Joi from "joi";
import { NullOr, UndefinedOr } from "../../../../utilities";
import { RuleValidator } from "../RuleValidator";
import { DomainError, handleDomainError } from "../errors";
import {
  INullableValueObjectOptions,
  NullableValueObject,
} from "./NullableValueObject";
import { Result } from "./Result";

export interface IUniqueIDOptions extends INullableValueObjectOptions {
  generateOnEmpty?: boolean;
}

export class UniqueID extends NullableValueObject<string> {
  protected static validate(
    value: UndefinedOr<string>,
    options: IUniqueIDOptions,
  ) {
    const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("");

    const ruleIsGuid = Joi.string()
      .guid({
        version: ["uuidv4"],
      })
      .label(options.label ? options.label : "id");

    const rules = Joi.alternatives(ruleIsEmpty, ruleIsGuid);

    return RuleValidator.validate<string>(rules, value);
  }

  private static sanitize(id: string): string {
    return id.trim();
  }

  public static create(value: NullOr<string>, options: IUniqueIDOptions = {}) {
    const _options: IUniqueIDOptions = {
      label: "id",
      generateOnEmpty: false,
      ...options,
    };

    if (!value && !_options.generateOnEmpty) {
      return Result.fail(
        handleDomainError(
          DomainError.INVALID_INPUT_DATA,
          "ID is null or empty",
        ),
      );
    }

    if (value) {
      const validationResult = UniqueID.validate(value, _options);

      if (validationResult.isFailure) {
        return Result.fail(
          handleDomainError(
            DomainError.INVALID_INPUT_DATA,
            validationResult.error.message,
            _options,
          ),
        );
      }

      return Result.ok<UniqueID>(
        new UniqueID(UniqueID.sanitize(validationResult.object)),
      );
    }

    if (_options.generateOnEmpty) {
      return UniqueID.generateNewID();
    }

    return Result.ok<UniqueID>(new UniqueID(null));
  }

  public static generateNewID(): Result<UniqueID, never> {
    return Result.ok<UniqueID>(new UniqueID(uuidv4()));
  }

  get value(): string {
    return String(this.props);
  }

  public toString(): string {
    return String(this.props);
  }

  public toPrimitive(): string {
    return this.toString();
  }
}

export class InvalidUniqueIDError extends Error {}
