<?php

/**
 *  Solidshop  —  Shopping cart for Joomla!
 *  ─────────────────────────────────────────
 *
 *  @author     Solid Labs <contact@solidshop.app>
 *  @link       https://www.solidshop.app
 *  @copyright  © 2026 Solid Labs. All rights reserved.
 *  @license    GPL-3.0-or-later
 */

namespace Joomla\Plugin\Solidshoppayment\PayPal\Extension;

\defined('_JEXEC') or die;

use Joomla\CMS\Event\Plugin\AjaxEvent;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\WebAsset\WebAssetManagerInterface;
use Joomla\Component\Solidshop\Site\Event\PaymentInitiateEvent;
use Joomla\Component\Solidshop\Site\Event\PaymentMethodsRegisterEvent;
use Joomla\Component\Solidshop\Site\Helper\CheckoutHelper;
use Joomla\Component\Solidshop\Site\Helper\OrderHelper;
use Joomla\Component\Solidshop\Site\Plugin\PaymentPluginAbstract;
use Joomla\Event\SubscriberInterface;
use PaypalServerSdkLib\Authentication\ClientCredentialsAuthCredentialsBuilder;
use PaypalServerSdkLib\Environment;
use PaypalServerSdkLib\Models\Builders\AddressBuilder;
use PaypalServerSdkLib\Models\Builders\AmountWithBreakdownBuilder;
use PaypalServerSdkLib\Models\Builders\NameBuilder;
use PaypalServerSdkLib\Models\Builders\OrderRequestBuilder;
use PaypalServerSdkLib\Models\Builders\PayerBuilder;
use PaypalServerSdkLib\Models\Builders\PurchaseUnitRequestBuilder;
use PaypalServerSdkLib\Models\CheckoutPaymentIntent;
use PaypalServerSdkLib\PaypalServerSdkClient;
use PaypalServerSdkLib\PaypalServerSdkClientBuilder;
use Solidshop\Lib\Status\PaymentStatus;

final class PayPal extends PaymentPluginAbstract implements SubscriberInterface
{
    // PayPal's officially supported transaction currencies.
    // Reference: https://developer.paypal.com/api/rest/reference/currency-codes/
    public const array ALLOW_CURRENCIES = [
        'AUD',
        'BRL',
        'CAD',
        'CHF',
        'CNY',
        'CZK',
        'DKK',
        'EUR',
        'GBP',
        'HKD',
        'HUF',
        'ILS',
        'JPY',
        'MXN',
        'MYR',
        'NOK',
        'NZD',
        'PHP',
        'PLN',
        'SEK',
        'SGD',
        'THB',
        'TWD',
        'USD',
    ];

    // PayPal requires whole-number amounts (no decimals) for these currencies.
    private const array ZERO_DECIMAL_CURRENCIES = ['HUF', 'JPY', 'TWD'];

    public static function getSubscribedEvents(): array
    {
        return [
            'onAjaxPaypal'                    => 'handleOrder',
            PaymentMethodsRegisterEvent::NAME => 'register',
        ];
    }

    public function register(PaymentMethodsRegisterEvent $event): void
    {
        if (!\in_array(strtoupper($event->getCurrency()->getCode()), PayPal::ALLOW_CURRENCIES)) {
            $event->removePayment('paypal');

            return;
        }

        $document = $this->getApplication()
            ->getDocument();
        $params   = $event->getParams('paypal');
        $document->addScriptOPtions('plg_solidshoppayment_paypal', [
            'clientId' => $params->get(($params->get('test') ? 'test_' : '') . 'client_id'),
            'orderUrl' => Route::_('index.php?option=com_ajax&plugin=paypal&group=solidshoppayment&format=json', false),
        ]);

        /** @var WebAssetManagerInterface $wa */
        $document->getWebAssetManager()
            ->registerAndUseScript(
                'plg_solidshoppayment_paypal',
                'plg_solidshoppayment_paypal/paypal.min.js',
                ['relative' => true]
            );
    }

    public function handleOrder(AjaxEvent $event)
    {
        $input = $this->getApplication()->getInput();
        $step  = $input->get('step');

        if (!\in_array($step, ['createOrder', 'approveOrder'])) {
            throw new \RuntimeException('Invalid step: ' . $step);
        }

        $order = $this->getApplication()
            ->bootComponent('solidshop')
            ->getMVCFactory()
            ->createModel('Order', 'Administrator', ['ignore_request' => true])
            ->getItem($input->getUint('order_id', 0));

        if (!$order->id) {
            throw new \RuntimeException('Invalid order ID: ' . $order->id);
        }

        try {
            $event->setArgument('result', $this->{$step}($order));
        } catch (\Throwable $e) {
            throw new \RuntimeException('Invalid step: ' . $step . ', error: ' . $e->getMessage());
        }
    }

    public function getLogo(): ?string
    {
        return HTMLHelper::image('plg_solidshoppayment_paypal/logo.png', 'PayPal', [], true);
    }

    public function initiatePayment(PaymentInitiateEvent $event): void
    {
        $event->wait();
    }

    private function createOrder(object $order)
    {
        $ordersController = $this->createPayPalSDKClient($order)
            ->getOrdersController();
        $mvcFactory       = $this->getApplication()
            ->bootComponent('solidshop')
            ->getMVCFactory();
        $currencyCode = strtoupper($order->currency->code);
        $body         = OrderRequestBuilder::init(
            CheckoutPaymentIntent::CAPTURE,
            [
                PurchaseUnitRequestBuilder::init(
                    AmountWithBreakdownBuilder::init(
                        $currencyCode,
                        $this->formatAmount((float) $order->grand_total, $currencyCode)
                    )->build()
                )->build(),
            ]
        );

        if (!empty($address = $order->payment_data['billing_address'] ?? [])) {
            $countryTable = $mvcFactory->createTable('Country', 'Administrator');
            $countryTable->load($address['country_id'] ?? 0);

            $stateTable = $mvcFactory->createTable('State', 'Administrator');
            $stateTable->load($address['state_id'] ?? 0);

            $body->payer(
                PayerBuilder::init()
                    ->emailAddress($order->customer_email)
                    ->name(
                        NameBuilder::init()
                            ->givenName($address['first_name'])
                            ->surname($address['last_name'] ?? '')
                            ->build()
                    )
                    ->address(
                        AddressBuilder::init($countryTable->code_2 ?? null)
                            ->addressLine1($address['address_1'] ?? null)
                            ->addressLine2($address['address_2'] ?? null)
                            ->adminArea1($stateTable->name ?? null)
                            ->adminArea2($address['city'] ?? null)
                            ->build()
                    )
                    ->build()
            )->build();
        }

        $response = $ordersController->createOrder(['body' => $body->build(), 'prefer' => 'return=minimal']);

        if ($response->isSuccess()) {
            $result = $response->getResult();
            $txnId  = $result->getId();

            $this->updateOrderPayment(
                $order->id,
                ['transaction_data' => $result->jsonSerialize()],
                txnId: $txnId,
            );

            return $txnId;
        }

        throw new \RuntimeException('Can\'t create the PayPal order: ' . $this->formatPayPalError($response));
    }

    private function formatAmount(float $amount, string $currencyCode): string
    {
        $decimals = \in_array(strtoupper($currencyCode), self::ZERO_DECIMAL_CURRENCIES, true) ? 0 : 2;

        return number_format($amount, $decimals, '.', '');
    }

    private function formatPayPalError($response): string
    {
        $status = $response->getStatusCode();
        $body   = $response->getBody();
        $data   = \is_string($body) ? json_decode($body, true) : $body;

        if (\is_array($data)) {
            $parts = [];

            if (!empty($data['details'][0]['issue'])) {
                $parts[] = $data['details'][0]['issue'];
            }

            if (!empty($data['details'][0]['description'])) {
                $parts[] = $data['details'][0]['description'];
            } elseif (!empty($data['message'])) {
                $parts[] = $data['message'];
            } elseif (!empty($data['name'])) {
                $parts[] = $data['name'];
            }

            if (!empty($data['debug_id'])) {
                $parts[] = 'debug_id=' . $data['debug_id'];
            }

            if (!empty($parts)) {
                return 'HTTP ' . $status . ' — ' . implode(' — ', $parts);
            }
        }

        return 'HTTP ' . $status . ' — ' . (\is_string($body) ? $body : 'no response body');
    }

    private function createPayPalSDKClient($order): PaypalServerSdkClient
    {
        require_once JPATH_PLUGINS . '/solidshoppayment/paypal/vendor/autoload.php';
        $params   = $order->store->params['payments']['paypal'] ?? null;
        $test     = $params['test'] ?? 1;
        $env      = $test ? Environment::SANDBOX : Environment::PRODUCTION;
        $prefix   = $test ? 'test_' : '';
        $clientId = $params[$prefix . 'client_id'] ?? '';
        $secretId = $params[$prefix . 'secret_id'] ?? '';

        return PaypalServerSdkClientBuilder::init()
            ->clientCredentialsAuthCredentials(ClientCredentialsAuthCredentialsBuilder::init($clientId, $secretId))
            ->environment($env)
            ->build();
    }

    private function approveOrder(object $order)
    {
        $paymentTxnId = $this->getApplication()->getInput()->getString('payment_txn_id');

        if ($order->payment_method_id !== 'paypal'
            || empty($order->payment_txn_id)
            || $order->payment_txn_id !== $paymentTxnId
        ) {
            throw new \RuntimeException('Invalid payment transaction ID: ' . $paymentTxnId);
        }

        $response = $this->createPayPalSDKClient($order)
            ->getOrdersController()
            ->captureOrder(['id' => $paymentTxnId]);

        if ($response->isSuccess()) {
            $this->updateOrderPayment(
                $order->id,
                ['transaction_data' => $response->getResult()->jsonSerialize()],
                PaymentStatus::PAID,
            );

            // Queue the confirmation email now that capture is confirmed.
            // Uses the queue (not direct-send) so it is consistent with all other payment methods.
            OrderHelper::enqueueEmail($order);

            CheckoutHelper::update(['finalData' => ['orderId' => $order->id]]);
        } else {
            throw new \RuntimeException('Can\'t approve the PayPal order: ' . $this->formatPayPalError($response));
        }
    }
}
