<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\ValueObject;

use InvalidArgumentException;
use JsonSerializable;
use Brick\Money\Money;

defined( 'ABSPATH' ) || exit;

/**
 * Lightweight Value Object for working with monetary amounts
 * Stores amount in currency minor units (cents, pennies)
 */
final class MoneyAmount implements JsonSerializable
{
    private int $amount;

    /**
     * @param int $amount Amount in currency minor units (cents)
     */
    public function __construct(int $amount)
    {
        $this->amount = $amount;
    }

    /**
     * Create zero amount
     */
    public static function zero(): self
    {
        return new self(0);
    }

    /**
     * Create from amount in major units (euros, dollars)
     *
     * @param float $amount Amount in major units
     * @param int $scale Number of decimal places (default 2)
     */
    public static function fromDecimal(float $amount, int $scale = 2): self
    {
        $multiplier = 10 ** $scale;
        return new self((int)round($amount * $multiplier));
    }

    /**
     * Create from Brick\Money
     */
    public static function fromBrickMoney(Money $money): self
    {
        return new self($money->getMinorAmount()->toInt());
    }

    // ============================================
    // Basic arithmetic operations
    // ============================================

    /**
     * Addition
     */
    public function add(MoneyAmount $other): self
    {
        // Overflow check
        if ($this->amount > 0 && $other->amount > 0 &&
            $this->amount > PHP_INT_MAX - $other->amount) {
            throw new InvalidArgumentException('Integer overflow detected in addition');
        }

        if ($this->amount < 0 && $other->amount < 0 &&
            $this->amount < PHP_INT_MIN - $other->amount) {
            throw new InvalidArgumentException('Integer underflow detected in addition');
        }

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

    /**
     * Subtraction
     */
    public function subtract(MoneyAmount $other): self
    {
        // Overflow check
        if ($other->amount < 0 && $this->amount > PHP_INT_MAX + $other->amount) {
            throw new InvalidArgumentException('Integer overflow detected in subtraction');
        }

        if ($other->amount > 0 && $this->amount < PHP_INT_MIN + $other->amount) {
            throw new InvalidArgumentException('Integer underflow detected in subtraction');
        }

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

    /**
     * Multiplication by integer
     */
    public function multiply(int $multiplier): self
    {
        if ($multiplier === 0) {
            return new self(0);
        }

        // Overflow check
        if ($multiplier > 0) {
            if ($this->amount > 0 && $this->amount > PHP_INT_MAX / $multiplier) {
                throw new InvalidArgumentException('Integer overflow detected in multiplication');
            }
            if ($this->amount < 0 && $this->amount < PHP_INT_MIN / $multiplier) {
                throw new InvalidArgumentException('Integer underflow detected in multiplication');
            }
        } else {
            if ($this->amount > 0 && $this->amount > PHP_INT_MIN / $multiplier) {
                throw new InvalidArgumentException('Integer underflow detected in multiplication');
            }
            if ($this->amount < 0 && $this->amount < PHP_INT_MAX / $multiplier) {
                throw new InvalidArgumentException('Integer overflow detected in multiplication');
            }
        }

        return new self($this->amount * $multiplier);
    }

    /**
     * Division by integer
     *
     * @param int $divisor Divisor
     * @param int $roundingMode Rounding mode (PHP_ROUND_* constants)
     */
    public function divide(int $divisor, int $roundingMode = PHP_ROUND_HALF_UP): self
    {
        if ($divisor === 0) {
            throw new InvalidArgumentException('Division by zero');
        }

        if ($roundingMode < 1 || $roundingMode > 4) {
            throw new InvalidArgumentException('Invalid rounding mode. Must be one of PHP_ROUND_* constants (1-4)');
        }

        $result = $this->amount / $divisor;
        return new self((int)round($result, 0, $roundingMode));
    }

    // ============================================
    // Operations for discounts/markups
    // ============================================

    /**
     * Apply percentage change
     *
     * @param int $percentage Percentage (e.g., 10 for 10%)
     * @param bool $isDiscount true for discount, false for markup
     */
    public function applyPercentage(int $percentage, bool $isDiscount = false): self
    {
        if ($percentage < 0) {
            throw new InvalidArgumentException('Percentage cannot be negative');
        }

        if ($percentage === 0) {
            return new self($this->amount);
        }

        $adjustment = (int)round($this->amount * ($percentage / 100));

        if ($isDiscount) {
            $newAmount = $this->amount - $adjustment;
            // Prevent negative prices on discount
            return new self(max(0, $newAmount));
        } else {
            // Overflow check on markup
            if ($adjustment > 0 && $this->amount > PHP_INT_MAX - $adjustment) {
                throw new InvalidArgumentException('Integer overflow detected in percentage addition');
            }
            return new self($this->amount + $adjustment);
        }
    }

    /**
     * Apply fixed change
     *
     * @param int $adjustment Change amount in minor units
     * @param bool $isDiscount true for discount, false for markup
     */
    public function applyFixed(int $adjustment, bool $isDiscount = false): self
    {
        if ($adjustment < 0) {
            throw new InvalidArgumentException('Adjustment cannot be negative');
        }

        if ($adjustment === 0) {
            return new self($this->amount);
        }

        if ($isDiscount) {
            $newAmount = $this->amount - $adjustment;
            // Prevent negative prices on discount
            return new self(max(0, $newAmount));
        } else {
            // Overflow check
            if ($this->amount > PHP_INT_MAX - $adjustment) {
                throw new InvalidArgumentException('Integer overflow detected in fixed addition');
            }
            return new self($this->amount + $adjustment);
        }
    }

    // ============================================
    // Comparison operations
    // ============================================

    /**
     * Check if zero
     */
    public function isZero(): bool
    {
        return $this->amount === 0;
    }

    /**
     * Check if positive
     */
    public function isPositive(): bool
    {
        return $this->amount > 0;
    }

    /**
     * Check if negative
     */
    public function isNegative(): bool
    {
        return $this->amount < 0;
    }

    /**
     * Check for equality
     */
    public function equals(MoneyAmount $other): bool
    {
        return $this->amount === $other->amount;
    }

    /**
     * Greater than
     */
    public function isGreaterThan(MoneyAmount $other): bool
    {
        return $this->amount > $other->amount;
    }

    /**
     * Greater than or equal to
     */
    public function isGreaterThanOrEqual(MoneyAmount $other): bool
    {
        return $this->amount >= $other->amount;
    }

    /**
     * Less than
     */
    public function isLessThan(MoneyAmount $other): bool
    {
        return $this->amount < $other->amount;
    }

    /**
     * Less than or equal to
     */
    public function isLessThanOrEqual(MoneyAmount $other): bool
    {
        return $this->amount <= $other->amount;
    }

    // ============================================
    // Utilities
    // ============================================

    /**
     * Get amount in minor units
     */
    public function getAmount(): int
    {
        return $this->amount;
    }

    /**
     * Get amount in major units (euros, dollars)
     *
     * @param int $scale Number of decimal places
     */
    public function toDecimal(int $scale = 2): float
    {
        $divisor = 10 ** $scale;
        return $this->amount / $divisor;
    }

    /**
     * Get absolute value
     */
    public function absolute(): self
    {
        return new self(abs($this->amount));
    }

    /**
     * Get minimum of two values
     */
    public function min(MoneyAmount $other): self
    {
        return $this->amount <= $other->amount ? new self($this->amount) : new self($other->amount);
    }

    /**
     * Get maximum of two values
     */
    public function max(MoneyAmount $other): self
    {
        return $this->amount >= $other->amount ? new self($this->amount) : new self($other->amount);
    }

    /**
     * Invert sign (positive to negative and vice versa)
     */
    public function negate(): self
    {
        // Overflow check for PHP_INT_MIN
        if ($this->amount === PHP_INT_MIN) {
            throw new InvalidArgumentException('Cannot negate PHP_INT_MIN');
        }

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

    // ============================================
    // Conversion
    // ============================================

    /**
     * Convert to Brick\Money
     *
     * @param string $currencyCode Currency code (EUR, USD, etc.)
     */
    public function toBrickMoney(string $currencyCode): Money
    {
        return Money::ofMinor($this->amount, $currencyCode);
    }

    /**
     * Format for display
     *
     * @param int $scale Number of decimal places
     * @param string $thousandsSeparator Thousands separator
     * @param string $decimalSeparator Decimal separator
     */
    public function format(
        int $scale = 2,
        string $thousandsSeparator = ',',
        string $decimalSeparator = '.'
    ): string {
        $decimal = $this->toDecimal($scale);
        return number_format($decimal, $scale, $decimalSeparator, $thousandsSeparator);
    }

    /**
     * String representation (for debugging)
     */
    public function __toString(): string
    {
        return (string)$this->amount;
    }

    /**
     * JSON representation — serializes as the integer amount in minor units.
     */
    public function jsonSerialize(): int
    {
        return $this->amount;
    }

    // ============================================
    // Additional useful methods
    // ============================================

    /**
     * Create array with amount split into N parts
     * Useful for distributing amount across days or guests
     *
     * @param int $parts Number of parts
     * @return self[] Array of MoneyAmount objects
     */
    public function split(int $parts): array
    {
        if ($parts <= 0) {
            throw new InvalidArgumentException('Parts must be greater than zero');
        }

        if ($parts === 1) {
            return [new self($this->amount)];
        }

        $baseAmount = intdiv($this->amount, $parts);
        $remainder = $this->amount % $parts;

        $result = [];
        for ($i = 0; $i < $parts; $i++) {
            // Distribute remainder across first parts
            $amount = $baseAmount + ($i < $remainder ? 1 : 0);
            $result[] = new self($amount);
        }

        return $result;
    }

    /**
     * Create sum from array of MoneyAmount objects
     *
     * @param self[] $amounts
     */
    public static function sum(array $amounts): self
    {
        $total = 0;

        foreach ($amounts as $amount) {
            if (!$amount instanceof self) {
                throw new InvalidArgumentException('Array must contain only MoneyAmount objects');
            }

            // Overflow check
            if ($total > 0 && $amount->amount > 0 && $total > PHP_INT_MAX - $amount->amount) {
                throw new InvalidArgumentException('Integer overflow detected in sum');
            }
            if ($total < 0 && $amount->amount < 0 && $total < PHP_INT_MIN - $amount->amount) {
                throw new InvalidArgumentException('Integer underflow detected in sum');
            }

            $total += $amount->amount;
        }

        return new self($total);
    }

    /**
     * Check if amount is within range
     */
    public function isBetween(MoneyAmount $min, MoneyAmount $max, bool $inclusive = true): bool
    {
        if ($inclusive) {
            return $this->amount >= $min->amount && $this->amount <= $max->amount;
        }

        return $this->amount > $min->amount && $this->amount < $max->amount;
    }
}
