import Joi from "joi";
import { isNull } from "lodash";
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 IPercentageOptions extends INullableValueObjectOptions {}

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

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

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

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

export class Percentage extends NullableValueObject<IPercentage> {
  public static readonly DEFAULT_SCALE = DEFAULT_SCALE;
  public static readonly MIN_VALUE = 0;
  public static readonly MAX_VALUE = 100;

  public static readonly MIN_SCALE = 0;
  public static readonly MAX_SCALE = 2;

  private readonly _isNull: boolean;
  private readonly _options: IPercentageOptions;

  protected static validate(
    value: NullOr<number | string>,
    scale: NullOr<number>,
    options: IPercentageOptions
  ) {
    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(Percentage.MIN_SCALE)
      .max(Percentage.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();
    }

    // Convert the value to a number if it's a string
    let numericValue = typeof value === "string" ? parseInt(value, 10) : Number(value);

    // Check if scale is null, and set to default if so
    let numericScale = isNull(scale) ? Percentage.DEFAULT_SCALE : Number(scale);

    // Calculate the adjusted value
    const adjustedValue = numericValue / Math.pow(10, numericScale);

    // Check if the adjusted value is within the specified range
    if (adjustedValue < Percentage.MIN_VALUE || adjustedValue > Percentage.MAX_VALUE) {
      return Result.fail(new Error(`Value with scale is out of range: ${adjustedValue}`));
    }

    return Result.ok();
  }

  public static create(
    props: IPercentageProps = defaultPercentageProps,
    options: IPercentageOptions = {}
  ) {
    if (props === null) {
      throw new Error(`InvalidParams: props params is missing`);
    }

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

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

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

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

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

    const _props = {
      amount: isNull(_amount) ? 0 : _amount,
      scale,
    };

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

  public static createFromFormattedValue(
    value: NullOr<number | string>,
    _options: IPercentageOptions = {}
  ) {
    if (value === null || value === "") {
      return Percentage.create({
        amount: null,
        scale: Percentage.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 Percentage.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);
  }

  private static _isWithinRange(value: NullOr<number>, scale: number): boolean {
    const _value = Number(Percentage._toString(value, scale));
    return _value >= Percentage.MIN_VALUE && _value <= Percentage.MAX_VALUE;
  }

  constructor(percentage: IPercentage, isNull: boolean, options: IPercentageOptions) {
    super(percentage);
    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 Percentage._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 !== Percentage.DEFAULT_SCALE) {
      return this.convertScale(Percentage.DEFAULT_SCALE).toPrimitive();
    } else {
      return this.amount;
    }
  }

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

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

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

    if (this.isNull()) {
      return new Percentage({ 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);

    if (!Percentage._isWithinRange(newValue, newScale)) {
      throw new Error(`Value out of range after conversion: ${newValue} ${newScale}`);
    }

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

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

  public isWithinRange(): boolean {
    return Percentage._isWithinRange(this.amount, this.scale);
  }
}
