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

const DEFAULT_SCALE = 2;

export interface IQuantityOptions extends INullableValueObjectOptions {}

export interface IQuantityProps {
  amount: NullOr<number | string>;
  scale?: number;
}

interface IQuantity {
  amount: NullOr<number>;
  scale: number;
}

export interface QuantityObject {
  amount: number | null;
  scale: number;
}

const defaultQuantityProps = {
  amount: 0,
  scale: DEFAULT_SCALE,
};

export class Quantity extends NullableValueObject<IQuantity> {
  public static readonly DEFAULT_SCALE = DEFAULT_SCALE;
  public static readonly MIN_SCALE = 0;
  public static readonly MAX_SCALE = 2;

  private readonly _isNull: boolean;
  private readonly _options: IQuantityOptions;

  protected static validate(
    value: NullOr<number | string>,
    scale: NullOr<number>,
    options: IQuantityOptions = {}
  ) {
    const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED;

    const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;

    const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
      options.label ? options.label : "amount"
    );

    const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
      options.label ? options.label : "amount"
    );

    const ruleScale = Joi.number()
      .min(Quantity.MIN_SCALE)
      .max(Quantity.MAX_SCALE)
      .label(options.label ? options.label : "scale");

    const validationResults = new ResultCollection([
      RuleValidator.validate<NullOr<number>>(
        Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
        value
      ),
      RuleValidator.validate<NullOr<number>>(
        Joi.alternatives(
          RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
          ruleScale
        ),
        scale
      ),
    ]);

    if (validationResults.hasSomeFaultyResult()) {
      return validationResults.getFirstFaultyResult();
    }

    return Result.ok();
  }

  public static create(
    props: IQuantityProps = defaultQuantityProps,
    options: IQuantityOptions = {}
  ) {
    if (props === null) {
      throw new Error(`InvalidParams: props params is missing`);
    }

    const { amount = defaultQuantityProps.amount, scale = defaultQuantityProps.scale } = props;

    const _options = {
      label: "quantity",
      ...options,
    };

    const validationResult = Quantity.validate(amount, scale, _options);

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

    let _amount: NullOr<number> = Quantity._sanitize(amount);

    const _props = {
      amount: _amount === null ? 0 : _amount,
      scale,
    };

    return Result.ok<Quantity>(new Quantity(_props, _amount === null, options));
  }

  public static createFromFormattedValue(
    value: NullOr<number | string>,
    _options: IQuantityOptions = {}
  ) {
    if (value === null || value === "") {
      return Quantity.create({
        amount: null,
        scale: Quantity.DEFAULT_SCALE,
      });
    }

    const valueStr = String(value);
    const [integerPart, decimalPart] = valueStr.split(",");

    let _amount = integerPart;
    let _scale = 2;

    if (decimalPart === undefined) {
      // 99
      _scale = 0;
    } else {
      if (decimalPart === "") {
        // 99,
        _amount = integerPart + decimalPart.padEnd(1, "0");
        _scale = 1;
      }
      if (decimalPart.length === 1) {
        // 99,1
        _amount = integerPart + decimalPart.padEnd(1, "0");
        _scale = 1;
      } else {
        if (decimalPart.length === 2) {
          // 99,12
          _amount = integerPart + decimalPart.padEnd(2, "0");
          _scale = 2;
        }
      }
    }

    return Quantity.create(
      {
        amount: _amount,
        scale: _scale,
      },
      _options
    );
  }

  private static _sanitize(value: NullOr<number | string>): NullOr<number> {
    let _value: NullOr<number> = null;

    if (typeof value === "string") {
      _value = parseInt(value, 10);
    } else {
      _value = value;
    }

    return _value;
  }

  private static _toString(value: NullOr<number>, scale: number): string {
    if (value === null) {
      return "";
    }

    const factor = Math.pow(10, scale);
    const amount = Number(value) / factor;
    return amount.toFixed(scale);
  }

  constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) {
    super(quantity);
    this._isNull = Object.freeze(isNull);
    this._options = Object.freeze(options);
  }

  get amount(): NullOr<number> {
    return this.isNull() ? null : Number(this.props?.amount);
  }

  get scale(): number {
    return Number(this.props?.scale);
  }

  public getAmount(): NullOr<number> {
    return this.isNull() ? null : Number(this.props?.amount);
  }

  public getScale(): number {
    return this.isNull() ? 0 : Number(this.props?.scale);
  }

  public isEmpty = (): boolean => {
    return this.isNull();
  };

  public isNull = (): boolean => {
    return this._isNull;
  };

  public toString(): string {
    return Quantity._toString(this.amount, this.scale);
  }

  public toNumber(): number {
    return this.isNull() ? 0 : Number(this.toString());
  }

  public toFormat(): string {
    return this._isNull
      ? ""
      : Intl.NumberFormat("es-ES", {
          maximumFractionDigits: 2,
        }).format(this.toNumber());
  }

  public toPrimitive(): NullOr<number> {
    if (this.scale !== Quantity.DEFAULT_SCALE) {
      return this.convertScale(Quantity.DEFAULT_SCALE).toPrimitive();
    } else {
      return this.amount;
    }
  }

  public toPrimitives() {
    return this.toObject();
  }

  public toObject(): QuantityObject {
    return {
      amount: this.amount,
      scale: this.scale,
    };
  }

  public convertScale(newScale: number): Quantity {
    if (newScale < Quantity.MIN_SCALE || newScale > Quantity.MAX_SCALE) {
      throw new Error(`Scale out of range: ${newScale}`);
    }

    if (this.isNull()) {
      return new Quantity({ amount: null, scale: newScale }, true, this._options);
    }

    const oldFactor = Math.pow(10, this.scale);
    const value = Number(this.amount) / oldFactor;

    const newFactor = Math.pow(10, newScale);
    const newValue = Math.round(value * newFactor);

    return new Quantity({ amount: newValue, scale: newScale }, false, this._options);
  }

  public hasSameScale(quantity: Quantity) {
    return this.scale === quantity.scale;
  }

  public increment(anotherQuantity?: Quantity) {
    if (this.isNull()) {
      return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
    }

    if (!anotherQuantity) {
      return Quantity.create(
        {
          amount: Number(this.amount) + 1,
          scale: this.scale,
        },
        this._options
      );
    }

    if (!this.hasSameScale(anotherQuantity)) {
      return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas."));
    }

    return Quantity.create(
      {
        amount: Number(this.amount) + Number(anotherQuantity.amount),
        scale: this.scale,
      },
      this._options
    );
  }

  public decrement(anotherQuantity?: Quantity) {
    if (this.isNull()) {
      return anotherQuantity ? Quantity.create(anotherQuantity.toObject()) : Quantity.create();
    }

    if (!anotherQuantity) {
      return Quantity.create(
        {
          amount: Number(this.amount) - 1,
          scale: this.scale,
        },
        this._options
      );
    }

    if (!this.hasSameScale(anotherQuantity)) {
      return Result.fail(Error("No se pueden restar cantidades con diferentes escalas."));
    }

    return Quantity.create(
      {
        amount: Number(this.amount) - Number(anotherQuantity.amount),
        scale: this.scale,
      },
      this._options
    );
  }
}
