import DineroFactory, { Currency, Dinero } from "dinero.js";
import Joi from "joi";
import { isNull } from "lodash";
import { NullOr } from "../../../../utilities";
import { adjustPrecision } from "../../../../utilities/helpers";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator";
import { CurrencyData } from "./CurrencyData";
import { Result } from "./Result";
import { IValueObjectOptions, ValueObject } from "./ValueObject";

export interface IMoneyValueOptions extends IValueObjectOptions {
  locale: string;
}

export const defaultMoneyValueOptions: IMoneyValueOptions = {
  locale: "es-ES",
};

export interface MoneyValueObject {
  amount: number | null;
  scale: number;
  currency_code: string;
}

export type RoundingMode =
  | "HALF_ODD"
  | "HALF_EVEN"
  | "HALF_UP"
  | "HALF_DOWN"
  | "HALF_TOWARDS_ZERO"
  | "HALF_AWAY_FROM_ZERO"
  | "DOWN";

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

const defaultMoneyValueProps = {
  amount: null,
  currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
  scale: 2,
};

interface IMoneyValue {
  toPrimitive(): NullOr<number>;
  toPrimitives(): MoneyValueObject;
  isEmpty(): boolean;
  toString(): string;
  toJSON(): MoneyValueObject | {};
  isNull(): boolean;

  getAmount(): number;
  getCurrency(): CurrencyData;
  getLocale(): string;
  getScale(): number;
  convertScale(newScale: number, roundingMode?: RoundingMode): MoneyValue;

  add(addend: MoneyValue): MoneyValue;
  subtract(subtrahend: MoneyValue): MoneyValue;
  multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue;
  divide(divisor: number, roundingMode?: RoundingMode): MoneyValue;
  percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue;
  allocate(ratios: ReadonlyArray<number>): MoneyValue[];

  equalsTo(comparator: MoneyValue): boolean;
  lessThan(comparator: MoneyValue): boolean;
  lessThanOrEqual(comparator: MoneyValue): boolean;
  greaterThan(comparator: MoneyValue): boolean;
  greaterThanOrEqual(comparator: MoneyValue): boolean;
  isZero(): boolean;
  isPositive(): boolean;
  isNegative(): boolean;

  hasSameCurrency(comparator: MoneyValue): boolean;
  hasSameAmount(comparator: MoneyValue): boolean;
  toFormat(format?: string, roundingMode?: RoundingMode): string;
  toUnit(): number;
  toRoundedUnit(digits: number, roundingMode?: RoundingMode): number;
  toObject(): MoneyValueObject;
  toNumber(): number;
}

export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
  public static readonly DEFAULT_SCALE = defaultMoneyValueProps.scale;
  public static readonly DEFAULT_CURRENCY_CODE = defaultMoneyValueProps.currencyCode;

  private readonly _isNull: boolean;

  private readonly _options: IMoneyValueOptions;

  protected static validate(amount: NullOr<number | string>, options: IMoneyValueOptions) {
    const ruleNull = Joi.any()
      .optional() // <- undefined
      .valid(null); // <- null

    const ruleNumber = Joi.number().label(options.label ? options.label : "amount");

    const rules = Joi.alternatives(ruleNull, ruleNumber);

    return RuleValidator.validate<NullOr<number | string>>(rules, amount);
  }

  protected static getMonetaryValueInfo(amount: string): [string, number] {
    // Divide la cadena de entrada en dos partes: valor y precisión
    const [valuePart, scalePart] = amount.split(".");

    // Calcula la precisión utilizada
    const scale = scalePart ? scalePart.length : 0;

    // Elimina cualquier carácter no numérico de la parte del valor y concaténalo
    const sanitizedValue = (valuePart + scalePart).replace(/[^0-9]/g, "");

    return [sanitizedValue, scale];
  }

  public static create(
    props: IMoneyValueProps = defaultMoneyValueProps,
    options = defaultMoneyValueOptions
  ) {
    if (props === null) {
      throw new Error(`InvalidParams: props params is missing`);
    }

    const {
      amount = defaultMoneyValueProps.amount,
      currencyCode = defaultMoneyValueProps.currencyCode,
      scale = defaultMoneyValueProps.scale,
    } = props || {};

    const validationResult = MoneyValue.validate(amount, options);

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

    const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
    const _currency = CurrencyData.createFromCode(currencyCode).object.code;

    const prop = DineroFactory({
      amount: Number(_amount),
      currency: _currency as Currency,
      precision: scale,
    }).setLocale(options.locale);

    return Result.ok<MoneyValue>(new this(prop, isNull(_amount), options));
  }

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

    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;
        } else {
          if (decimalPart.length === 3) {
            // 99,123
            _amount = integerPart + decimalPart.padEnd(3, "0");
            _scale = 3;
          } else {
            if (decimalPart.length === 4) {
              // 99,1235
              _amount = integerPart + decimalPart.padEnd(4, "0");
              _scale = 4;
            }
          }
        }
      }
    }

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

  private static sanitize(amount: NullOr<number | string>): NullOr<number> {
    let _amount: NullOr<number> = null;

    if (typeof amount === "string") {
      _amount = parseFloat(amount);
    } else {
      _amount = amount;
    }

    return _amount;
  }

  protected static createFromDinero(dinero: Dinero) {
    return Result.ok<MoneyValue>(new MoneyValue(dinero, false, defaultMoneyValueOptions));
  }

  public static normalizeScale(objects: ReadonlyArray<MoneyValue>): MoneyValue[] {
    return DineroFactory.normalizePrecision(objects.map((object) => object.props)).map(
      (dinero) => MoneyValue.createFromDinero(dinero).object
    );
  }

  public static minimum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
    return MoneyValue.createFromDinero(DineroFactory.minimum(objects.map((object) => object.props)))
      .object;
  }

  public static maximum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
    return MoneyValue.createFromDinero(DineroFactory.maximum(objects.map((object) => object.props)))
      .object;
  }

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

    new Intl.NumberFormat(locales, {
      /*minimumSignificantDigits: scale,
      maximumSignificantDigits: scale,
      minimumFractionDigits: scale,*/
      useGrouping: true,
    }).format(value === null ? 0 : adjustPrecision({ amount: value, scale }));

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

  constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
    super(value);
    this._isNull = Object.freeze(isNull);
    this._options = Object.freeze(options);
  }

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

  public toString(): string {
    return MoneyValue._toString(
      this.isNull() ? null : this.getAmount(),
      this.getScale(),
      this._options.locale
    );
  }

  public toJSON() {
    return this._isNull ? {} : this.props?.toJSON();
  }

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

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

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

  public getAmount(): number {
    return this.props.getAmount();
  }

  public getScale(): number {
    return this.props.getPrecision();
  }

  public convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
    if (this._isNull) {
      return MoneyValue.create({
        amount: null,
        scale: newScale,
        currencyCode: this.getCurrency().code,
      }).object;
    } else {
      return MoneyValue.createFromDinero(this.props.convertPrecision(newScale, roundingMode))
        .object;
    }
  }

  public getCurrency(): CurrencyData {
    return CurrencyData.createFromCode(this.props.getCurrency()).object;
  }

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

  public add(addend: MoneyValue): MoneyValue {
    return MoneyValue.createFromDinero(this.props.add(addend.props)).object;
  }

  public subtract(subtrahend: MoneyValue): MoneyValue {
    return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props)).object;
  }

  public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue {
    return MoneyValue.createFromDinero(this.props.multiply(multiplier, roundingMode)).object;
  }

  public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue {
    return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode)).object;
  }

  public percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue {
    return MoneyValue.createFromDinero(this.props.percentage(percentage, roundingMode)).object;
  }

  public allocate(ratios: ReadonlyArray<number>): MoneyValue[] {
    return this.props.allocate(ratios).map((dinero) => MoneyValue.createFromDinero(dinero).object);
  }

  public equalsTo(comparator: MoneyValue): boolean {
    return this.props.equalsTo(comparator.props);
  }

  public lessThan(comparator: MoneyValue): boolean {
    return this.props.lessThan(comparator.props);
  }

  public lessThanOrEqual(comparator: MoneyValue): boolean {
    return this.props.lessThanOrEqual(comparator.props);
  }

  public greaterThan(comparator: MoneyValue): boolean {
    return this.props.greaterThan(comparator.props);
  }

  public greaterThanOrEqual(comparator: MoneyValue): boolean {
    return this.props.greaterThanOrEqual(comparator.props);
  }

  public isZero(): boolean {
    return this.props.isZero();
  }

  public isPositive(): boolean {
    return this.props.isPositive();
  }

  public isNegative(): boolean {
    return this.props.isNegative();
  }

  public hasSameCurrency(comparator: MoneyValue): boolean {
    return this.props.hasSameCurrency(comparator.props);
  }

  public hasSameAmount(comparator: MoneyValue): boolean {
    return this.props.hasSameAmount(comparator.props);
  }

  public toFormat(format?: string, roundingMode?: RoundingMode): string {
    return this._isNull ? "" : this.props.toFormat(format, roundingMode);
  }

  public toUnit(): number {
    return this.props.toUnit();
  }

  public toRoundedUnit(digits: number, roundingMode?: RoundingMode): number {
    return this.props.toRoundedUnit(digits, roundingMode);
  }

  public toObject(): MoneyValueObject {
    const obj = this.props.toObject();
    return {
      amount: this._isNull ? null : obj.amount,
      scale: obj.precision,
      currency_code: String(obj.currency),
    };
  }

  public toNumber(): number {
    return this.toUnit();
  }
}
