<?php

namespace App\Support\Money;

use InvalidArgumentException;

/**
 * An amount of money: an integer number of minor units plus its currency (§112).
 *
 * Integers because floating point cannot represent 0.10 exactly, and money that
 * drifts by a paisa per operation is money someone eventually has to explain.
 * Currency travels with the amount so two currencies can never be added by
 * accident.
 */
final readonly class Money
{
    private function __construct(
        public int $minorUnits,
        public string $currency,
    ) {
        if ($minorUnits < 0) {
            throw new InvalidArgumentException('Money cannot be negative; model a refund as its own record.');
        }
    }

    public static function of(int $minorUnits, string $currency): self
    {
        return new self($minorUnits, strtoupper($currency));
    }

    public static function zero(string $currency): self
    {
        return new self(0, strtoupper($currency));
    }

    public function plus(self $other): self
    {
        $this->assertSameCurrency($other);

        return new self($this->minorUnits + $other->minorUnits, $this->currency);
    }

    public function minus(self $other): self
    {
        $this->assertSameCurrency($other);

        if ($other->minorUnits > $this->minorUnits) {
            throw new InvalidArgumentException('Subtracting more than the amount held.');
        }

        return new self($this->minorUnits - $other->minorUnits, $this->currency);
    }

    public function times(int $multiplier): self
    {
        return new self($this->minorUnits * $multiplier, $this->currency);
    }

    public function isZero(): bool
    {
        return $this->minorUnits === 0;
    }

    public function isGreaterThan(self $other): bool
    {
        $this->assertSameCurrency($other);

        return $this->minorUnits > $other->minorUnits;
    }

    public function equals(self $other): bool
    {
        return $this->currency === $other->currency && $this->minorUnits === $other->minorUnits;
    }

    /** For display only — never feed this back into a calculation. */
    public function toDecimalString(int $scale = 2): string
    {
        $divisor = 10 ** $scale;

        return number_format($this->minorUnits / $divisor, $scale, '.', '');
    }

    public function format(): string
    {
        return $this->toDecimalString().' '.$this->currency;
    }

    private function assertSameCurrency(self $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException(
                "Cannot combine {$this->currency} with {$other->currency}."
            );
        }
    }
}
