<?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\Service;

use BookiFlex\Core\Contract\Payment\PaymentInterface;
use BookiFlex\Core\Exception\NotFoundException;
use BookiFlex\Core\Exception\RuntimeException;
use BookiFlex\Extensions\Payment\Contract\PaymentInitiatorInterface;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Repository\ReservationRepository;
use BookiFlex\Extensions\Payment\Model\Payment;
use BookiFlex\Extensions\Payment\Provider\PaymentAllocationProvider;
use BookiFlex\Extensions\Payment\Service\Processor\PaymentProcessorResolver;
use Payum\Core\Payum;

defined( 'ABSPATH' ) || exit;

/**
 * Orchestrates payment initiation: given a reservation/payment, decides which
 * gateway and intent (capture/authorize/cancel) apply, then delegates URL
 * creation to the processor resolved for that gateway.
 */
class PaymentInitiator implements PaymentInitiatorInterface
{
    public function __construct(
        private readonly ReservationRepository $reservationRepository,
        private readonly PaymentAllocationProvider $paymentAllocationProvider,
        private readonly Payum $payum,
        private readonly PaymentProcessorResolver $processorResolver,
    ) {
    }

    /**
     * Create capture token for reservation by customer.
     *
     * Reservation should be in WAITING_PAYMENT status and with valid time for payment.
     * Payment for this reservation should be WAITING status.
     *
     * @param string $sid reservation sid
     * @param array $exceptGateways don't create a URL for these gateways
     * @return string redirect URL
     * @throws RuntimeException|NotFoundException
     */
    public function createPublicCaptureUrl(string $sid, array $exceptGateways = ['offline']): string
    {
        $reservation = $this->getReservation($sid);
        if (!$reservation->isWaitingPayment()) {
            throw new RuntimeException('The time for payment of the reservation has expired.');
        }

        $paymentType        = $reservation->getPaymentType();
        $paymentAllocations = $this->paymentAllocationProvider->getPaymentAllocations([$reservation]);

        foreach ($paymentAllocations as $paymentAllocation) {
            $payment = $paymentAllocation->getPayment();

            if (in_array($paymentType->getGatewayName(), $exceptGateways)) {
                continue;
            }

            if (!$payment->isWaiting() || !$paymentType->getGatewayName()) {
                continue;
            }

            if ($paymentType->usePaymentAuthorization()) {
                return $this->createAuthorizeUrl($payment, $paymentType->getGatewayName());
            }

            return $this->createCaptureUrl($payment, $paymentType->getGatewayName());
        }

        throw new RuntimeException('No invoices found for payment.');
    }

    /**
     * Create capture token for reservation by manager.
     *
     * If payment has a WAITING status will be captured by 'offline' payment gateway.
     * If payment has a AUTHORIZED status will be captured by origin gateway.
     *
     * @param string $sid reservation sid
     * @param int $paymentId payment id
     * @return string redirect URL
     * @throws RuntimeException
     * @throws NotFoundException
     */
    public function createPrivateCaptureUrl(string $sid, int $paymentId): string
    {
        $payment = $this->getPayment($paymentId);
        $reservation = $this->getReservation($sid);

        if (!$payment->isAllocatedOn($reservation)) {
            throw new RuntimeException('The reservation is not allocated on payment.', 400, null, ['errorCode' => 'token_not_exist']);
        }

        if ($payment->isWaiting()) {
            return $this->createCaptureUrl($payment, 'offline');
        }

        if ($payment->isAuthorized()) {
            return $this->createCaptureUrl($payment, $reservation->getPaymentType()->getGatewayName());
        }

        throw new RuntimeException('For capture payment, payment should be in WAITING or AUTHORIZED state.', 400, null, ['errorCode' => 'token_not_exist']);
    }

    /**
     * @return string redirect URL
     * @throws RuntimeException|NotFoundException
     */
    public function createPrivateCancelUrl(string $sid, int $paymentId): string
    {
        $payment = $this->getPayment($paymentId);
        $reservation = $this->getReservation($sid);

        if (!$payment->isAllocatedOn($reservation)) {
            throw new RuntimeException('The reservation is not allocated on payment.', 400, null, ['errorCode' => 'token_not_exist']);
        }

        if ($payment->isWaiting() || $payment->isAuthorized()) {
            return $this->createCancelUrl($payment, $reservation->getPaymentType()->getGatewayName());
        }

        throw new RuntimeException('For cancel payment, payment should be in WAITING or AUTHORIZED state.');
    }

    private function createAuthorizeUrl(PaymentInterface $payment, string $gatewayName): string
    {
        return $this->processorResolver->resolve($gatewayName)->createAuthorizeUrl($payment, $gatewayName);
    }

    private function createCaptureUrl(PaymentInterface $payment, string $gatewayName): string
    {
        return $this->processorResolver->resolve($gatewayName)->createCaptureUrl($payment, $gatewayName);
    }

    private function createCancelUrl(PaymentInterface $payment, string $gatewayName): string
    {
        return $this->processorResolver->resolve($gatewayName)->createCancelUrl($payment, $gatewayName);
    }

    /**
     * @throws NotFoundException
     */
    private function getPayment(int $paymentId): Payment
    {
        $storage = $this->payum->getStorage(Payment::class);

        /** @var Payment|null $payment */
        $payment = $storage->find($paymentId);
        if ($payment === null) {
            throw new NotFoundException('Payment is not found');
        }

        return $payment;
    }

    /**
     * @throws NotFoundException
     */
    private function getReservation(string $sid): Reservation
    {
        /** @var Reservation|null $reservation */
        $reservation = $this->reservationRepository->findOne(['sid' => $sid]);
        if (null === $reservation) {
            throw new NotFoundException('Reservation is not found.');
        }

        return $reservation;
    }
}
