<?php
/**
 * 2026 Paysera
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * @author Paysera <plugins@paysera.com>
 * @copyright 2026 Paysera
 * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
 * International Registered Trademark & Property of Paysera
 */

declare(strict_types=1);

namespace Paysera\PrestashopPayments\Handler;

use Paysera\CheckoutSdk\Entity\PaymentCallback;
use Paysera\CheckoutSdk\Entity\PaymentCallback\Payment;
use Paysera\CheckoutSdk\Entity\PaymentStatus;
use Paysera\CheckoutSdk\Exception\IntegrationException;
use Paysera\CheckoutSdk\SdkFacade;
use Paysera\PrestashopPayments\Dto\PayseraPayment;
use Paysera\PrestashopPayments\Enum\OrderCallbackStatus;
use Paysera\PrestashopPayments\Exception\CallbackVerificationException;
use Paysera\PrestashopPayments\Exception\OrderNotFoundException;
use Paysera\PrestashopPayments\Factory\MessageFactory;
use Paysera\PrestashopPayments\Repository\OrderRepository;
use Paysera\PrestashopPayments\Repository\PaymentStatusTransitionRepository;
use Paysera\PrestashopPayments\Repository\PayseraPaymentRepository;
use Paysera\PrestashopPayments\Service\CallbackValidator;
use Paysera\PrestashopPayments\Service\OrderStatusManager;
use Paysera\PrestashopPayments\Service\PaymentMethodTitleResolver;
use Paysera\PrestashopPayments\Service\PaymentStatusMapper;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;

if (!defined('_PS_VERSION_')) {
    exit;
}

class PaymentCallbackHandler
{
    public function __construct(
        private SdkFacade $sdkFacade,
        private CallbackValidator $callbackValidator,
        private OrderStatusManager $orderStatusManager,
        private PaymentStatusMapper $statusMapper,
        private MessageFactory $messageFactory,
        private LoggerInterface $logger,
        private OrderRepository $orderRepository,
        private PayseraPaymentRepository $payseraPaymentRepository,
        private PaymentStatusTransitionRepository $statusTransitionRepository,
        private PaymentMethodTitleResolver $paymentMethodTitleResolver
    ) {
    }

    public function processCallback(ServerRequestInterface $request): void
    {
        $this->logger->debug('Processing payment callback');

        $callback = $this->parseCallback($request);

        $data = $this->extractCallbackData($callback);

        $order = $this->findOrderByMerchantOrderId($data['merchant_order_id']);

        $initiationData = $this->payseraPaymentRepository->findByOrderId((int) $order->id);

        $this->callbackValidator->validateAgainstInitiationData($callback, $initiationData);

        $payment = $this->extractLatestPayment($callback);
        $status = $this->resolveCallbackStatus($payment, $data['status']);

        if ($this->statusMapper->isSuccessfulStatus($status)) {
            $this->callbackValidator->validatePaymentCompletion($callback);

            $this->callbackValidator->validateAmountAndCurrency($callback, $order);
        }

        $statusChanged = $this->persistCallbackData($order, $payment, $status);

        $this->updateOrderStatus($order, $status);

        if ($statusChanged && $this->statusMapper->isSuccessfulStatus($status)) {
            $this->addPaymentConfirmedMessage($order, $data['paysera_order_id']);
        }

        $this->logger->info('Payment callback processed successfully', [
            'order_id' => $data['paysera_order_id'],
            'status' => $status,
        ]);
    }

    private function resolveCallbackStatus(?Payment $payment, string $orderStatus): string
    {
        if ($payment !== null) {
            return strtolower($payment->getPaymentInfo()->getStatus());
        }

        return $this->mapOrderStatusToPaymentStatus($orderStatus);
    }

    private function mapOrderStatusToPaymentStatus(string $orderStatus): string
    {
        switch ($orderStatus) {
            case OrderCallbackStatus::PAID:
                return PaymentStatus::SETTLED;
            case OrderCallbackStatus::CANCELED:
            case OrderCallbackStatus::CLOSED:
                return PaymentStatus::CANCELLED;
            default:
                return PaymentStatus::INITIATED;
        }
    }

    private function persistCallbackData(\Order $order, ?Payment $payment, string $status): bool
    {
        $orderId = (int) $order->id;

        if ($payment !== null) {
            $this->payseraPaymentRepository->save($this->buildRowFromPayment($orderId, $payment, $status));
        } else {
            $this->payseraPaymentRepository->save(new PayseraPayment($orderId, status: $status));
        }

        return $this->statusTransitionRepository->append($orderId, $status);
    }

    private function buildRowFromPayment(int $orderId, Payment $payment, string $status): PayseraPayment
    {
        $purchase = $payment->getPurchaseInfo();
        $payer = $payment->getPayerInfo();

        $methodKey = $payment->getPaymentInfo()->getMethod();
        if ($methodKey !== '') {
            $this->paymentMethodTitleResolver->resolve($methodKey);
        }

        return new PayseraPayment(
            $orderId,
            $payment->getId(),
            null,
            null,
            $purchase->getOriginalAmount(),
            $purchase->getOriginalCurrency(),
            $purchase->getPaymentAmount(),
            $purchase->getPaymentCurrency(),
            $payer->getPayerName(),
            $payer->getPayerEmail(),
            $methodKey !== '' ? $methodKey : null,
            $status
        );
    }

    private function extractLatestPayment(PaymentCallback $callback): ?Payment
    {
        $candidates = [];

        foreach ($callback->getOrder()->getPaymentLinkCollection() as $link) {
            foreach ($link->getPayments() as $payment) {
                $candidates[] = $payment;
            }
        }

        if ($candidates === []) {
            return null;
        }

        usort(
            $candidates,
            static fn (Payment $a, Payment $b): int => self::paymentOrderingTimestamp($b) <=> self::paymentOrderingTimestamp($a)
        );

        return $candidates[0];
    }

    private static function paymentOrderingTimestamp(Payment $payment): int
    {
        $timestamps = $payment->getPaymentInfo()->getTimestamps();

        return $timestamps->getUpdatedAt() ?? $timestamps->getCreatedAt() ?? 0;
    }

    private function parseCallback(ServerRequestInterface $request): PaymentCallback
    {
        try {
            return $this->sdkFacade
                ->getCallbacksFacade()
                ->processPaymentCallback($request)
            ;
        } catch (IntegrationException $exception) {
            $this->logger->error('Callback signature verification failed', [
                'exception' => $exception->getMessage(),
            ]);

            throw new CallbackVerificationException('Callback signature verification failed', 0, $exception);
        }
    }

    private function extractCallbackData(PaymentCallback $callback): array
    {
        $orderInfo = $callback->getOrder()->getOrderInfo();

        return [
            'paysera_order_id' => $callback->getOrder()->getId(),
            'merchant_order_id' => $orderInfo->getMerchantOrderId(),
            'amount' => $orderInfo->getAmount(),
            'amount_paid' => $orderInfo->getAmountPaid(),
            'currency' => $orderInfo->getCurrency(),
            'is_paid' => $orderInfo->isPaid(),
            'status' => strtolower($orderInfo->getStatus()),
        ];
    }

    private function findOrderByMerchantOrderId(string $merchantOrderId): \Order
    {
        $orderId = $this->extractOrderId($merchantOrderId);

        if ($orderId <= 0) {
            $this->logger->error('Invalid merchant order ID', [
                'merchant_order_id' => $merchantOrderId,
            ]);

            throw new OrderNotFoundException(sprintf('Invalid merchant order ID: %s', $merchantOrderId));
        }

        $order = $this->orderRepository->findById($orderId);

        if ($order === null) {
            $this->logger->error('Order not found', [
                'merchant_order_id' => $merchantOrderId,
                'order_id' => $orderId,
            ]);

            throw new OrderNotFoundException(sprintf('Order not found for merchant order ID: %s', $merchantOrderId));
        }

        return $order;
    }

    private function updateOrderStatus(\Order $order, string $payseraStatus): void
    {
        switch (true) {
            case $this->statusMapper->isSuccessfulStatus($payseraStatus):
                $this->orderStatusManager->setToConfirmed($order);
                break;
            case $this->statusMapper->isFailedStatus($payseraStatus):
                $this->orderStatusManager->setToFailed($order);
                break;
            case $this->statusMapper->isCancelledStatus($payseraStatus):
                $this->orderStatusManager->setToCancelled($order);
                break;
            case $this->statusMapper->isPendingStatus($payseraStatus):
                $this->orderStatusManager->setToAwaitingConfirmation($order);
                break;
            default:
                $this->logger->debug('Payment status does not map to an order state change', [
                    'order_id' => $order->id,
                    'status' => $payseraStatus,
                ]);
        }
    }

    private function extractOrderId(string $merchantOrderId): int
    {
        if (preg_match('/^(\d+)/', $merchantOrderId, $matches) === 1) {
            return (int) $matches[1];
        }

        return 0;
    }

    private function addPaymentConfirmedMessage(\Order $order, string $payseraOrderId): void
    {
        $message = $this->messageFactory->createPrivateOrderMessage(
            $order->id,
            sprintf('Paysera payment confirmed (Order ID: %s)', $payseraOrderId)
        );
        $message->add();

        $this->logger->debug('Payment confirmed message added', [
            'order_id' => $order->id,
            'paysera_order_id' => $payseraOrderId,
        ]);
    }
}
