<?php

namespace App\Services;

use InvalidArgumentException;
use OverflowException;

class MoneyMath
{
    public function toMinor(string|int $amount): int
    {
        $value = trim((string) $amount);
        if (! preg_match('/^(\d+)(?:\.(\d{1,6}))?$/', $value, $m)) {
            throw new InvalidArgumentException("Invalid monetary amount: {$value}");
        }
        return $this->parsedMinor($m[1], $m[2] ?? '');
    }

    public function toSignedMinor(string|int $amount): int
    {
        $value = trim((string) $amount);
        if (! preg_match('/^([+-]?)(\d+)(?:\.(\d{1,6}))?$/', $value, $m)) {
            throw new InvalidArgumentException("Invalid signed monetary amount: {$value}");
        }
        $minor = $this->parsedMinor($m[2], $m[3] ?? '');
        return $m[1] === '-' ? -$minor : $minor;
    }

    public function fromMinor(int $minor): string
    {
        if ($minor < 0) throw new InvalidArgumentException('Negative money is not supported by this calculator.');
        return intdiv($minor, 100).'.'.str_pad((string) ($minor % 100), 2, '0', STR_PAD_LEFT);
    }

    public function fromSignedMinor(int $minor): string
    {
        if ($minor >= 0) return $this->fromMinor($minor);
        if ($minor === PHP_INT_MIN) throw new OverflowException('Signed monetary amount exceeds supported range.');
        return '-'.$this->fromMinor(abs($minor));
    }

    public function rateScaled(string|int $rate): int
    {
        $value = trim((string) $rate);
        if (! preg_match('/^(\d+)(?:\.(\d{1,6}))?$/', $value, $m)) throw new InvalidArgumentException("Invalid percentage rate: {$value}");
        $whole = (int) $m[1];
        if ($whole > 1000) throw new InvalidArgumentException('Percentage rate cannot exceed 1000%.');
        return $whole * 1_000_000 + (int) str_pad($m[2] ?? '', 6, '0');
    }

    public function percentageOf(int $minorAmount, string|int $rate, int $periodDivisor = 1): int
    {
        if ($periodDivisor < 1) throw new InvalidArgumentException('Period divisor must be at least 1.');
        return $this->mulDivRound($minorAmount, $this->rateScaled($rate), 100_000_000 * $periodDivisor);
    }

    /** @return list<int> */
    public function allocate(int $totalMinor, int $parts): array
    {
        if ($parts < 1) throw new InvalidArgumentException('Allocation requires at least one part.');
        if ($totalMinor < 0) throw new InvalidArgumentException('Cannot allocate a negative amount.');
        $base = intdiv($totalMinor, $parts); $remainder = $totalMinor % $parts; $result = [];
        for ($i = 0; $i < $parts; $i++) $result[] = $base + ($i < $remainder ? 1 : 0);
        return $result;
    }

    public function mulDivRound(int $a, int $b, int $divisor): int
    {
        if ($a < 0 || $b < 0 || $divisor <= 0) throw new InvalidArgumentException('mulDivRound expects non-negative values and a positive divisor.');
        if ($a === 0 || $b === 0) return 0;
        $g = $this->gcd($b, $divisor); $b = intdiv($b, $g); $divisor = intdiv($divisor, $g);
        $g = $this->gcd($a, $divisor); $a = intdiv($a, $g); $divisor = intdiv($divisor, $g);
        if ($a > intdiv(PHP_INT_MAX, $b)) throw new OverflowException('Financial calculation exceeds supported integer range.');
        $product = $a * $b; $quotient = intdiv($product, $divisor); $remainder = $product % $divisor;
        $threshold = intdiv($divisor, 2) + ($divisor % 2);
        return $quotient + ($remainder >= $threshold ? 1 : 0);
    }

    private function parsedMinor(string $wholeInput, string $fractionInput): int
    {
        $whole = ltrim($wholeInput, '0'); $whole = $whole === '' ? '0' : $whole;
        $limit = intdiv(PHP_INT_MAX - 100, 100);
        if (strlen($whole) > strlen((string) $limit) || (strlen($whole) === strlen((string) $limit) && strcmp($whole, (string) $limit) > 0)) {
            throw new OverflowException('Monetary amount exceeds supported range.');
        }
        $fraction = str_pad($fractionInput, 6, '0');
        $minor = ((int) $whole) * 100 + (int) substr($fraction, 0, 2);
        if ((int) $fraction[2] >= 5) $minor++;
        return $minor;
    }

    private function gcd(int $a, int $b): int
    {
        while ($b !== 0) { $tmp = $a % $b; $a = $b; $b = $tmp; }
        return max(1, $a);
    }
}
