import { Result } from "@repo/rdx-utils";
import DineroFactory, { Currency, Dinero } from "dinero.js";
import { Percentage } from "./percentage";
import { Quantity } from "./quantity";
import { ValueObject } from "./value-object";

const DEFAULT_SCALE = 2;
const DEFAULT_CURRENCY_CODE = "EUR";

type CurrencyData = Currency;

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

export interface MoneyValueProps {
  value: number;
  scale?: number;
  currency_code?: string;
}

export interface IMoneyValue {
  value: number;
  scale: number;
  currencyCode: string;

  getProps(): MoneyValueProps;
  convertScale(newScale: number): MoneyValue;
  add(addend: MoneyValue): MoneyValue;
  subtract(subtrahend: MoneyValue): MoneyValue;
  multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue;
  divide(divisor: number, roundingMode?: RoundingMode): MoneyValue;
  percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue;
  equalsTo(comparator: MoneyValue): boolean;
  greaterThan(comparator: MoneyValue): boolean;
  lessThan(comparator: MoneyValue): boolean;
  isZero(): boolean;
  isPositive(): boolean;
  isNegative(): boolean;
  hasSameCurrency(comparator: MoneyValue): boolean;
  hasSameAmount(comparator: MoneyValue): boolean;
  format(locale: string): string;
}

export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyValue {
  private readonly dinero: Dinero;

  static DEFAULT_SCALE = DEFAULT_SCALE;
  static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE;

  static create({ value, currency_code, scale }: MoneyValueProps) {
    const props = {
      value: Number(value),
      scale: scale ?? MoneyValue.DEFAULT_SCALE,
      currency_code: currency_code ?? MoneyValue.DEFAULT_CURRENCY_CODE,
    };
    return Result.ok(new MoneyValue(props));
  }

  constructor(props: MoneyValueProps) {
    super(props);
    const { value: amount, scale, currency_code } = props;
    this.dinero = Object.freeze(
      DineroFactory({
        amount,
        precision: scale || MoneyValue.DEFAULT_SCALE,
        currency: (currency_code as Currency) || MoneyValue.DEFAULT_CURRENCY_CODE,
      })
    ); // 🔒 Garantiza inmutabilidad
  }

  get value(): number {
    return this.dinero.getAmount();
  }

  get currencyCode(): string {
    return this.dinero.getCurrency().toString();
  }

  get scale(): number {
    return this.dinero.getPrecision();
  }

  getProps(): MoneyValueProps {
    return this.props;
  }

  get formattedValue(): number {
    return this.dinero.getAmount() / 10 ** this.dinero.getPrecision(); // ** => Math.pow
  }

  /** Reconstruye el VO desde la cadena persistida */
  static fromPersistence(value: string): MoneyValue {
    const [currencyCode, amountStr, scaleStr] = value.split(":");
    const amount = Number.parseInt(amountStr, 10);
    const scale = Number.parseInt(scaleStr, 10);
    const currency = currencyCode;
    return new MoneyValue({ value: amount, scale, currency_code: currency });
  }

  toPrimitive() {
    return {
      value: this.value,
      scale: this.scale,
      currency_code: this.currencyCode,
    };
  }

  convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue {
    const _newDinero = this.dinero.convertPrecision(newScale, roundingMode);
    return new MoneyValue({
      value: _newDinero.getAmount(),
      scale: _newDinero.getPrecision(),
      currency_code: _newDinero.getCurrency(),
    });
  }

  add(addend: MoneyValue): MoneyValue {
    return new MoneyValue({
      value: this.dinero.add(addend.dinero).getAmount(),
      scale: this.scale,
      currency_code: this.currencyCode,
    });
  }

  subtract(subtrahend: MoneyValue): MoneyValue {
    return new MoneyValue({
      value: this.dinero.subtract(subtrahend.dinero).getAmount(),
      scale: this.scale,
      currency_code: this.currencyCode,
    });
  }

  multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
    const _multiplier = typeof multiplier === "number" ? multiplier : multiplier.toNumber();

    const _newDinero = this.dinero.multiply(_multiplier, roundingMode);
    return new MoneyValue({
      value: _newDinero.getAmount(),
      scale: _newDinero.getPrecision(),
      currency_code: _newDinero.getCurrency(),
    });
  }

  divide(divisor: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
    const _divisor = typeof divisor === "number" ? divisor : divisor.toNumber();

    const _newDinero = this.dinero.divide(_divisor, roundingMode);
    return new MoneyValue({
      value: _newDinero.getAmount(),
      scale: _newDinero.getPrecision(),
      currency_code: _newDinero.getCurrency(),
    });
  }

  percentage(percentage: number | Percentage, roundingMode?: RoundingMode): MoneyValue {
    const _percentage = typeof percentage === "number" ? percentage : percentage.toNumber();

    const _newDinero = this.dinero.percentage(_percentage, roundingMode);
    return new MoneyValue({
      value: _newDinero.getAmount(),
      scale: _newDinero.getPrecision(),
      currency_code: _newDinero.getCurrency(),
    });
  }

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

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

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

  isZero(): boolean {
    return this.dinero.isZero();
  }

  isPositive(): boolean {
    return this.value > 0;
  }

  isNegative(): boolean {
    return this.value < 0;
  }

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

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

  hasSameScale(comparator: MoneyValue): boolean {
    return this.dinero.getPrecision() === comparator.dinero.getPrecision();
  }

  /**
   * Devuelve una cadena con el importe formateado.
   * Ejemplo: 123456 -> 1.234,56 €
   * @param locale Código de idioma y país (ej. "es-ES")
   * @returns Importe formateado
   */
  format(locale: string): string {
    const currencyCode = this.currencyCode;
    const scale = this.scale;

    return new Intl.NumberFormat(locale, {
      style: "currency",
      currency: currencyCode,
      minimumFractionDigits: scale,
      maximumFractionDigits: scale,
      useGrouping: true,
    }).format(this.formattedValue);
  }
}
