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

declare(strict_types=1);

 namespace BookiFlex\Extensions\Payment\Model;

use BookiFlex\Core\Contract\DeferrerEvent\DeferredEventSupportInterface;
use BookiFlex\Core\Contract\DeferrerEvent\Trait\HasDeferredEvents;
use BookiFlex\Core\Contract\Payment\PaymentStatus;
use BookiFlex\Core\Model\Customer\Customer;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Util\DateTimeUtils;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;
use InvalidArgumentException;
use Payum\Core\Model\CreditCardInterface;
use Payum\Core\Model\PaymentInterface as PayumPaymentInterface;
use BookiFlex\Core\Contract\Payment\PaymentInterface;
use Traversable;

defined( 'ABSPATH' ) || exit;

class Payment implements PayumPaymentInterface, PaymentInterface, DeferredEventSupportInterface
{
    use Timestamps;
    use HasDeferredEvents;
    use Identifiable;

    private int $totalAmount;
    private string $currencyCode;
    private string $gatewayName;
    private Customer $client;
    private PaymentStatus $status;
    /** @var PaymentAllocation[] */
    private array $paymentAllocations        = [];
    private string $number                   = '';
    private string $description              = '';
    private array $details                   = [];
    private ?CreditCardInterface $creditCard = null;

    public function __construct()
    {
        $this->status    = new PaymentStatus(PaymentStatus::WAITING);
        $this->createdAt = DateTimeUtils::nowUtc();
    }

    public function getNumber(): string
    {
        return $this->number;
    }

    public function setNumber(string $number): void
    {
        $this->number = $number;
    }

    public function getClient(): Customer
    {
        return $this->client;
    }

    public function getClientEmail(): string
    {
        return $this->client->getEmail();
    }

    public function getClientId(): string
    {
        return (string) $this->client->getId();
    }

    /**
     * @throws MathException
     */
    public function setAmount(Money $amount): void
    {
        $this->totalAmount  = $amount->getMinorAmount()->toInt();
        $this->currencyCode = $amount->getCurrency()->getCurrencyCode();
    }

    /**
     * @throws UnknownCurrencyException
     * @throws NumberFormatException
     * @throws RoundingNecessaryException
     */
    public function getAmount(): Money
    {
        return Money::ofMinor($this->totalAmount, $this->currencyCode);
    }

    public function getTotalAmount(): int
    {
        return $this->totalAmount;
    }

    public function getCurrencyCode(): string
    {
        return $this->currencyCode;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function setDescription(string $description): void
    {
        $this->description = $description;
    }

    public function getDetails(): array
    {
        return $this->details;
    }

    /**
     * @param array|Traversable $details
     */
    public function setDetails($details): void
    {
        if ($details instanceof Traversable) {
            $details = iterator_to_array($details);
        }

        $this->details = $details;
    }

    /**
     * @return CreditCardInterface|null
     */
    public function getCreditCard(): ?CreditCardInterface
    {
        return $this->creditCard;
    }

    public function setCreditCard(?CreditCardInterface $creditCard): void
    {
        $this->creditCard = $creditCard;
    }

    /**
     * @throws MoneyMismatchException
     * @throws UnknownCurrencyException
     * @throws RoundingNecessaryException
     * @throws MathException
     * @throws NumberFormatException
     */
    public function allocatePayment(PaymentAllocation $allocation): void
    {
        if ($allocation->getPayment() !== $this) {
            throw new InvalidArgumentException('Allocation does not belong to this payment');
        }

        if ($allocation->getAmount()->isGreaterThan($this->getNotAllocatedAmount())) {
            throw new InvalidArgumentException('Amount is greater than not allocated amount');
        }

        $this->paymentAllocations[] = $allocation;
    }

    public function getPaymentAllocations(): array
    {
        return $this->paymentAllocations;
    }

    public function removePaymentAllocation(PaymentAllocation $allocation): void
    {
        foreach ($this->paymentAllocations as $key => $paymentAllocation) {
            if ($paymentAllocation === $allocation) {
                unset($this->paymentAllocations[$key]);
                return;
            }
        }
    }

    public function isAllocatedOn(Reservation $reservation): bool
    {
        foreach ($this->paymentAllocations as $paymentAllocation) {
            if ($paymentAllocation->getReservation()->getId() === $reservation->getId()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Calculate the amount that has not been allocated to reservations.
     *
     * @return Money
     * @throws MathException
     * @throws NumberFormatException
     * @throws RoundingNecessaryException
     * @throws UnknownCurrencyException
     * @throws MoneyMismatchException
     */
    public function getNotAllocatedAmount(): Money
    {
        $allocatedAmount = Money::zero($this->currencyCode);
        foreach ($this->paymentAllocations as $allocation) {
            $allocatedAmount = $allocatedAmount->plus($allocation->getAmount());
        }
        return $this->getAmount()->minus($allocatedAmount);
    }

    public function getGatewayName(): string
    {
        return $this->gatewayName;
    }

    public function setGatewayName(string $gatewayName): void
    {
        $this->gatewayName = $gatewayName;
    }

    public function isPaid(): bool
    {
        return $this->status->isEquals(PaymentStatus::PAID);
    }

    public function isWaiting(): bool
    {
        return $this->status->isEquals(PaymentStatus::WAITING);
    }

    public function isAuthorized(): bool
    {
        return $this->status->isEquals(PaymentStatus::AUTHORIZED);
    }

    public function isExpired(): bool
    {
        return $this->status->isEquals(PaymentStatus::EXPIRED);
    }

    public function getStatus(): PaymentStatus
    {
        return $this->status;
    }

    public function markWaiting(): void
    {
        $this->setStatus(PaymentStatus::WAITING);
    }

    public function markAuthorized(): void
    {
        $this->setStatus(PaymentStatus::AUTHORIZED);
    }

    public function markCancelled(): void
    {
        $this->setStatus(PaymentStatus::CANCELLED);
    }

    public function markExpired(): void
    {
        $this->setStatus(PaymentStatus::EXPIRED);
    }

    public function markPaid(): void
    {
        $this->setStatus(PaymentStatus::PAID);
    }

    private function setStatus(string $status): void
    {
        try {
            $this->status->transitionTo($status);
        } catch (\Exception $e) {
            unset($e);
        }
    }

    public function setClient(?Customer $client): void
    {
        $this->client = $client;
    }
}
