<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Payment;

use Cart;
use Configuration;
use Currency;
use Order;
use SimplyIn\Checkout\PaymentMethodDataBuilder;
use SimplyIn\Session\SimplyInSessionInterface;
use SimplyInOrder;

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

final class PaymentCreationService
{
    public function __construct(
        private readonly SimplyInSessionInterface $session,
        private readonly PaymentApiInterface $api,
        private readonly PaymentRecorderInterface $recorder,
        private readonly OrderStateUpdaterInterface $orderStateUpdater,
        private readonly PaymentMethodDataBuilder $paymentMethodDataBuilder,
    ) {
    }

    public function createPayment(
        Cart $cart,
        Order $order,
        string $customerFirstName,
        string $customerLastName,
        string $guestEmail,
        string $authenticatedEmail,
        bool $isGuest,
        bool $accountCreated,
        string $returnUrl,
        string $webhookUrl,
    ): PaymentCreationResult {
        $authToken = (string) ($this->session->get('simplyin_auth_token') ?: '');
        $paymentMethodId = $this->session->get('simplyin_payment_id');
        $paymentType = (string) ($this->session->get('simplyin_payment_type') ?: '');

        $paymentId = $this->recorder->newPaymentId($order);
        $email = $authToken !== '' ? $authenticatedEmail : $guestEmail;
        $paymentMethodData = $this->paymentMethodDataBuilder->build($paymentType, $accountCreated);

        // The backend's order _id is the canonical matching key for post-create ops. Persisted at
        // order creation in simplyin_order.simplyin_id (same source the status channel uses,
        // OrderStatusSync). Legacy orders have no stored _id -> omit orderId and let the backend
        // fall back to shopOrderNumber. Never send an empty orderId.
        $simplyInOrder = SimplyInOrder::getByReference((string) $order->reference);
        $simplyinOrderId = (string) ($simplyInOrder->simplyin_id ?? '');

        $body = [
            'paymentId' => $paymentId,
            'paymentMethodId' => $paymentMethodId,
            'title' => (string) $order->reference,
            'amount' => (float) $order->getOrdersTotalPaid(),
            'currency' => Currency::getIsoCodeById((int) $order->id_currency),
            'customerPaymentData' => [
                'email' => $email,
                'firstName' => $customerFirstName,
                'lastName' => $customerLastName,
                'phone' => '',
            ],
            'paymentMethodData' => $paymentMethodData,
            'returnUrl' => $returnUrl,
            'webhookUrl' => $webhookUrl,
        ];

        if ($simplyinOrderId !== '') {
            $body['orderId'] = $simplyinOrderId;
        }

        $response = $this->api->createPayment($body, $authToken);

        $status = $this->resolveStatus($response);

        $this->recorder->record(
            $cart,
            $paymentType,
            $status,
            $paymentId,
            (string) ($this->session->get('simplyin_payment_provider') ?: ''),
            (string) ($this->session->get('simplyin_card_provider') ?: ''),
            (string) ($this->session->get('simplyin_payment_image') ?: ''),
            (string) ($this->session->get('simplyin_payment_logo') ?: ''),
        );

        if ($status === 'FAILED') {
            $this->orderStateUpdater->update(
                $order,
                (int) Configuration::get('SIMPLYIN_PAYMENT_STATUS_CANCELED'),
                'markAsRejected',
            );
        }

        if ($isGuest) {
            $this->session->clear();
        }
        $this->session->clearCustomer();

        $externalRedirect = null;
        if (($paymentType === 'card' || $paymentType === 'add-card')
            && is_object($response)
            && isset($response->redirectUrl)
            && is_string($response->redirectUrl)
        ) {
            if ($this->isSafeRedirectUrl($response->redirectUrl)) {
                $externalRedirect = $response->redirectUrl;
            } else {
                // Rejected URL degrades to the regular order-confirmation redirect: the order
                // stays pending instead of the browser being sent to an unvetted destination.
                \PrestaShopLogger::addLog(
                    'Simplyin: rejected external redirect URL from payment API (not an absolute https URL)',
                    3,
                    null,
                    'Order',
                    (int) $order->id,
                );
            }
        }

        return new PaymentCreationResult($status, $externalRedirect);
    }

    private function isSafeRedirectUrl(string $url): bool
    {
        // Control characters and whitespace survive parse_url() but have no place in a redirect
        // target (header-injection vector) — reject outright.
        if (preg_match('/[\x00-\x20\x7f]/', $url) === 1) {
            return false;
        }

        $parts = parse_url($url);

        return is_array($parts)
            && ($parts['scheme'] ?? '') === 'https'
            && !empty($parts['host'])
            && !isset($parts['user'])
            && !isset($parts['pass']);
    }

    private function resolveStatus(?object $response): string
    {
        if ($response === null) {
            return 'FAILED';
        }
        if (isset($response->code) && $response->code === 'FORBIDDEN') {
            return 'FAILED';
        }
        if (isset($response->status) && is_string($response->status)) {
            return $response->status;
        }

        return 'FAILED';
    }
}
