<?php

declare(strict_types=1);

namespace App\Framework\Core\ValueObjects;

use InvalidArgumentException;

/**
 * Represents a monetary amount with currency
 * Stores amounts in minor units (cents, pence, etc.) to avoid floating point issues
 * Immutable value object for safe money calculations
 */
final readonly class Money
{
    public function __construct(
        public int $amount,  // Amount in minor units (cents, pence, etc.)
        public Currency $currency
    ) {
    }

    // Factory Methods
    public static function zero(Currency $currency): self
    {
        return new self(0, $currency);
    }

    public static function fromMajorUnits(float $amount, Currency $currency): self
    {
        $minorUnits = (int) round($amount * $currency->getMinorUnit());

        return new self($minorUnits, $currency);
    }

    public static function EUR(int $cents): self
    {
        return new self($cents, Currency::EUR);
    }

    public static function USD(int $cents): self
    {
        return new self($cents, Currency::USD);
    }

    public static function GBP(int $pence): self
    {
        return new self($pence, Currency::GBP);
    }

    public static function CHF(int $rappen): self
    {
        return new self($rappen, Currency::CHF);
    }

    // Arithmetic Operations
    public function add(self $other): self
    {
        $this->assertSameCurrency($other);

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

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

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

    public function multiply(float $multiplier): self
    {
        $newAmount = (int) round($this->amount * $multiplier);

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

    public function divide(float $divisor): self
    {
        if ($divisor == 0) {
            throw new InvalidArgumentException('Division by zero');
        }
        $newAmount = (int) round($this->amount / $divisor);

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

    public function negate(): self
    {
        return new self(-$this->amount, $this->currency);
    }

    public function absolute(): self
    {
        return new self(abs($this->amount), $this->currency);
    }

    // Percentage Operations
    public function percentage(Percentage $percentage): self
    {
        $newAmount = (int) round($this->amount * ($percentage->toDecimal()));

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

    public function addPercentage(Percentage $percentage): self
    {
        $addition = $this->percentage($percentage);

        return $this->add($addition);
    }

    public function subtractPercentage(Percentage $percentage): self
    {
        $subtraction = $this->percentage($percentage);

        return $this->subtract($subtraction);
    }

    // Allocation (splitting money)
    public function allocate(int ...$ratios): array
    {
        if (empty($ratios)) {
            throw new InvalidArgumentException('At least one ratio must be provided');
        }

        $total = array_sum($ratios);
        if ($total === 0) {
            throw new InvalidArgumentException('Total ratio must be greater than zero');
        }

        $remainder = $this->amount;
        $results = [];

        foreach ($ratios as $ratio) {
            $share = (int) floor($this->amount * $ratio / $total);
            $results[] = new self($share, $this->currency);
            $remainder -= $share;
        }

        // Add remainder to first allocation
        if ($remainder > 0) {
            $results[0] = new self($results[0]->amount + $remainder, $this->currency);
        }

        return $results;
    }

    public function split(int $parts): array
    {
        if ($parts <= 0) {
            throw new InvalidArgumentException('Parts must be greater than zero');
        }

        $ratios = array_fill(0, $parts, 1);

        return $this->allocate(...$ratios);
    }

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

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

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

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

        return $this->amount >= $other->amount;
    }

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

        return $this->amount < $other->amount;
    }

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

        return $this->amount <= $other->amount;
    }

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

    public function isPositive(): bool
    {
        return $this->amount > 0;
    }

    public function isNegative(): bool
    {
        return $this->amount < 0;
    }

    // Conversion
    public function toMajorUnits(): float
    {
        return $this->amount / $this->currency->getMinorUnit();
    }

    // Formatting
    public function format(): string
    {
        return $this->currency->formatAmount($this->amount);
    }

    public function formatWithCode(): string
    {
        $decimals = $this->currency->getDecimals();
        $amount = $this->toMajorUnits();
        $formatted = number_format($amount, $decimals, '.', ',');

        return $formatted . ' ' . $this->currency->value;
    }

    // Serialization
    public function toString(): string
    {
        return $this->formatWithCode();
    }

    public function __toString(): string
    {
        return $this->format();
    }

    public function toArray(): array
    {
        return [
            'amount' => $this->amount,
            'currency' => $this->currency->value,
            'formatted' => $this->format(),
        ];
    }

    public static function fromArray(array $data): self
    {
        $currency = Currency::from($data['currency']);

        return new self($data['amount'], $currency);
    }

    // Helper Methods
    private function assertSameCurrency(self $other): void
    {
        if ($this->currency->isNot($other->currency)) {
            throw new InvalidArgumentException(
                "Cannot perform operation with different currencies: {$this->currency->value} and {$other->currency->value}"
            );
        }
    }

    // Static helpers with variadic parameters
    public static function sum(self ...$monies): ?self
    {
        if (empty($monies)) {
            return null;
        }

        $first = array_shift($monies);

        return array_reduce($monies, fn (self $carry, self $money) => $carry->add($money), $first);
    }

    public static function average(self ...$monies): ?self
    {
        if (empty($monies)) {
            return null;
        }

        $sum = self::sum(...$monies);

        return $sum?->divide(count($monies));
    }

    public static function min(self ...$monies): ?self
    {
        if (empty($monies)) {
            return null;
        }

        $min = $monies[0];
        foreach ($monies as $money) {
            if ($money->isLessThan($min)) {
                $min = $money;
            }
        }

        return $min;
    }

    public static function max(self ...$monies): ?self
    {
        if (empty($monies)) {
            return null;
        }

        $max = $monies[0];
        foreach ($monies as $money) {
            if ($money->isGreaterThan($max)) {
                $max = $money;
            }
        }

        return $max;
    }
}
