import { Result } from "@repo/rdx-utils";
import { z } from "zod";
import { ValueObject } from "./value-object";

const DEFAULT_SCALE = 2;

export interface IQuantityProps {
  amount: number;
  scale: number;
}

interface IQuantity {
  amount: number;
  scale: number;

  getValue(): IQuantityProps;
  toNumber(): number;
  toString(): string;

  isZero(): boolean;
  isPositive(): boolean;
  isNegative(): boolean;

  increment(anotherQuantity?: Quantity): Result<Quantity, Error>;
  decrement(anotherQuantity?: Quantity): Result<Quantity, Error>;
  hasSameScale(otherQuantity: Quantity): boolean;
  convertScale(newScale: number): Result<Quantity, Error>;
}

export class Quantity extends ValueObject<IQuantityProps> implements IQuantity {
  protected static validate(values: IQuantityProps) {
    const schema = z.object({
      amount: z.number().int(),
      scale: z.number().int().min(Quantity.MIN_SCALE).max(Quantity.MAX_SCALE),
    });

    return schema.safeParse(values);
  }

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

  static create(props: IQuantityProps) {
    const checkProps = Quantity.validate(props);

    if (!checkProps.success) {
      return Result.fail(new Error(checkProps.error.errors[0].message));
    }
    return Result.ok(new Quantity({ ...(checkProps.data as IQuantityProps) }));
  }

  get amount(): number {
    return this.props.amount;
  }

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

  getValue(): IQuantityProps {
    return this.props;
  }

  toPrimitive() {
    return this.getValue();
  }

  toNumber(): number {
    return this.amount / 10 ** this.scale; // ** => Math.pow
  }

  toString(): string {
    return this.toNumber().toFixed(this.scale);
  }

  isZero(): boolean {
    return this.amount === 0;
  }

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

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

  increment(anotherQuantity?: Quantity): Result<Quantity, Error> {
    if (!anotherQuantity) {
      return Quantity.create({
        amount: Number(this.amount) + 1,
        scale: this.scale,
      });
    }

    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,
    });
  }

  decrement(anotherQuantity?: Quantity): Result<Quantity, Error> {
    if (!anotherQuantity) {
      return Quantity.create({
        amount: Number(this.amount) - 1,
        scale: this.scale,
      });
    }

    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,
    });
  }

  hasSameScale(otherQuantity: Quantity): boolean {
    return this.scale === otherQuantity.scale;
  }

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

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

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

    return Quantity.create({ amount: newValue, scale: newScale });
  }
}
