<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Payment;

use CpCreation\VitiCore\Behavior\HasPayment;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\Invoice;
use CpCreation\VitiCore\Cart\Model\Payment\StripeModel;
use CpCreation\VitiCore\Cart\Model\PaymentMethod;
use CpCreation\VitiCore\Cart\Repository\PaymentMethodRepository;
use Stripe\Customer;
use Stripe\Checkout\Session;
use Stripe\Event;
use Stripe\Exception\SignatureVerificationException;
use Stripe\PaymentIntent as StripePaymentIntent;
use Stripe\Refund;
use Stripe\StripeClient;
use Stripe\Webhook;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\SerializerInterface;

final class StripePayment
{
    public const METHOD = 'stripe';
    public const CURRENCY = 'eur';

    /** @var array<string, mixed> */
    private array $paths;

    private ?StripeModel $config = null;

    private ?StripeClient $client = null;

    private ?string $bootedSecretKey = null;

    public function __construct(
        private readonly PaymentMethodRepository $paymentMethodRepository,
        private readonly SerializerInterface $serializer,
        private readonly RouterInterface $router,
        array $paths,
    ) {
        $this->paths = $paths;
    }

    public function createCheckoutSession(HasPayment $payment): Session
    {
        $this->boot($payment->getPaymentMethod());
        $amount = $payment->getTotal();

        if (null === $amount || $amount <= 0) {
            throw new \RuntimeException('Le montant Stripe est invalide.');
        }

        $doneRoute = $this->paths['stripe']['done'] ?? null;
        $canceledRoute = $this->paths['stripe']['canceled'] ?? null;

        if (!\is_string($doneRoute) || '' === $doneRoute) {
            throw new \RuntimeException('Route paths[stripe][done] manquante.');
        }

        if (!\is_string($canceledRoute) || '' === $canceledRoute) {
            throw new \RuntimeException('Route paths[stripe][canceled] manquante.');
        }

        $returnParameters = $this->buildReturnParameters($payment);
        $returnParameters['session_id'] = '{CHECKOUT_SESSION_ID}';
        $successUrl = $this->router->generate($doneRoute, $returnParameters, UrlGeneratorInterface::ABSOLUTE_URL);
        $successUrl = str_replace(rawurlencode('{CHECKOUT_SESSION_ID}'), '{CHECKOUT_SESSION_ID}', $successUrl);
        $cancelUrl = $this->router->generate($canceledRoute, $returnParameters, UrlGeneratorInterface::ABSOLUTE_URL);

        $paymentReference = $this->buildPaymentReference($payment);
        $metadata = $this->buildStripeMetadata($payment, $paymentReference);

        $customerId = $this->resolveCheckoutCustomerId($payment, $metadata);

        $sessionParams = [
            'mode' => 'payment',
            'client_reference_id' => $paymentReference,
            'success_url' => $successUrl,
            'cancel_url' => $cancelUrl,
            'billing_address_collection' => 'auto',
            'line_items' => [[
                'quantity' => 1,
                'price_data' => [
                    'currency' => self::CURRENCY,
                    'unit_amount' => $amount,
                    'product_data' => [
                        'name' => $this->buildCheckoutLabel($payment),
                    ],
                ],
            ]],
            'metadata' => $metadata,
            'payment_intent_data' => [
                'description' => sprintf('Paiement %s', $paymentReference),
                'metadata' => $metadata,
                'receipt_email' => (string) $payment->getEmail(),
            ],
        ];

        if (null !== $customerId) {
            $sessionParams['customer'] = $customerId;
            $sessionParams['customer_update'] = [
                'address' => 'auto',
                'name' => 'auto',
                'shipping' => 'auto',
            ];
        } else {
            $sessionParams['customer_email'] = (string) $payment->getEmail();
        }

        return $this->client()->checkout->sessions->create($sessionParams);
    }

    public function retrieveCheckoutSession(string $sessionId, ?PaymentMethod $paymentMethod = null): Session
    {
        $this->boot($paymentMethod);

        return $this->client()->checkout->sessions->retrieve($sessionId, [
            'expand' => [
                'customer',
                'payment_intent',
                'payment_intent.latest_charge',
                'payment_intent.latest_charge.balance_transaction',
            ],
        ]);
    }

    public function retrievePaymentIntent(string $paymentIntentId, ?PaymentMethod $paymentMethod = null): StripePaymentIntent
    {
        $this->boot($paymentMethod);

        /** @var StripePaymentIntent $paymentIntent */
        $paymentIntent = $this->client()->paymentIntents->retrieve($paymentIntentId, [
            'expand' => [
                'latest_charge',
                'latest_charge.balance_transaction',
            ],
        ]);

        return $paymentIntent;
    }

    public function isSuccessfulPaymentIntent(string $paymentIntentId, ?PaymentMethod $paymentMethod = null): bool
    {
        $this->boot($paymentMethod);

        $paymentIntent = $this->client()->paymentIntents->retrieve($paymentIntentId, []);
        $status = (string) ($paymentIntent->status ?? '');

        return 'succeeded' === $status;
    }

    /**
     * @return array<string, mixed>
     */
    public function normalizeCheckoutSession(Session $session): array
    {
        $json = json_encode($session, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
        $payload = false !== $json ? json_decode($json, true) : null;
        if (!\is_array($payload)) {
            $payload = [];
        }

        $metadata = \is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];
        $paymentIntent = \is_array($payload['payment_intent'] ?? null) ? $payload['payment_intent'] : null;
        $lastPaymentError = \is_array($paymentIntent['last_payment_error'] ?? null)
            ? $paymentIntent['last_payment_error']
            : null;
        $latestCharge = \is_array($paymentIntent['latest_charge'] ?? null) ? $paymentIntent['latest_charge'] : null;
        $balanceTransaction = \is_array($latestCharge['balance_transaction'] ?? null)
            ? $latestCharge['balance_transaction']
            : null;
        $customer = $payload['customer'] ?? null;
        $customerId = \is_array($customer)
            ? ($customer['id'] ?? null)
            : $customer;

        return [
            'event_id' => null,
            'event_type' => 'checkout.session.completed',
            'object' => $payload,
            'session' => $payload,
            'checkout_session_id' => $payload['id'] ?? null,
            'client_reference_id' => $payload['client_reference_id'] ?? null,
            'customer_id' => \is_string($customerId) ? $customerId : null,
            'payment_intent' => $paymentIntent['id'] ?? ($payload['payment_intent'] ?? null),
            'payment_intent_status' => $paymentIntent['status'] ?? null,
            'last_payment_error' => $lastPaymentError,
            'payment_status' => $payload['payment_status'] ?? null,
            'status' => $payload['status'] ?? null,
            'metadata' => $metadata,
            'payment_reference' => $metadata['payment_reference'] ?? ($payload['client_reference_id'] ?? null),
            'transaction_reference' => $metadata['transaction_id'] ?? ($metadata['payment_reference'] ?? ($payload['client_reference_id'] ?? null)),
            'order_reference' => $metadata['order_reference'] ?? ($metadata['payment_reference'] ?? ($metadata['transaction_id'] ?? ($payload['client_reference_id'] ?? null))),
            'charge_id' => $latestCharge['id'] ?? null,
            'transaction_fee_amount' => $this->normalizeStripeAmount($balanceTransaction['fee'] ?? null),
            'transaction_fee_currency' => $balanceTransaction['currency'] ?? null,
            'balance_transaction_id' => $balanceTransaction['id'] ?? null,
            'refund_id' => null,
            'refund_status' => null,
            'refund_amount' => null,
            'refund_failure_reason' => null,
            'raw' => [
                'data' => [
                    'object' => $payload,
                ],
            ],
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function normalizePaymentIntent(StripePaymentIntent $paymentIntent): array
    {
        $json = json_encode($paymentIntent, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
        $payload = false !== $json ? json_decode($json, true) : null;
        if (!\is_array($payload)) {
            $payload = [];
        }

        $latestCharge = \is_array($payload['latest_charge'] ?? null) ? $payload['latest_charge'] : null;
        $balanceTransaction = \is_array($latestCharge['balance_transaction'] ?? null)
            ? $latestCharge['balance_transaction']
            : null;
        $metadata = \is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];
        $lastPaymentError = \is_array($payload['last_payment_error'] ?? null) ? $payload['last_payment_error'] : null;

        return [
            'event_id' => null,
            'event_type' => 'payment_intent.retrieved',
            'payment_intent' => $payload['id'] ?? null,
            'payment_intent_status' => $payload['status'] ?? null,
            'last_payment_error' => $lastPaymentError,
            'metadata' => $metadata,
            'payment_reference' => $metadata['payment_reference'] ?? null,
            'transaction_reference' => $metadata['transaction_id'] ?? ($metadata['payment_reference'] ?? null),
            'order_reference' => $metadata['order_reference'] ?? ($metadata['payment_reference'] ?? ($metadata['transaction_id'] ?? null)),
            'charge_id' => $latestCharge['id'] ?? null,
            'transaction_fee_amount' => $this->normalizeStripeAmount($balanceTransaction['fee'] ?? null),
            'transaction_fee_currency' => $balanceTransaction['currency'] ?? null,
            'balance_transaction_id' => $balanceTransaction['id'] ?? null,
            'raw_payment_intent' => $payload,
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    public function isSuccessfulCheckoutSessionPayload(array $payload): bool
    {
        $status = $payload['status'] ?? null;
        $paymentStatus = $payload['payment_status'] ?? null;

        return 'complete' === $status && \in_array($paymentStatus, ['paid', 'no_payment_required'], true);
    }

    /**
     * @throws SignatureVerificationException
     */
    public function constructWebhookEvent(string $payload, ?string $signature, ?PaymentMethod $paymentMethod = null): Event
    {
        $this->boot($paymentMethod);

        $webhookSecret = $this->config?->getWebhookSecret();
        if (null === $webhookSecret || '' === $webhookSecret) {
            throw new \RuntimeException('Le secret de webhook Stripe est manquant.');
        }

        if (null === $signature || '' === $signature) {
            throw new \UnexpectedValueException('La signature Stripe est manquante.');
        }

        return Webhook::constructEvent($payload, $signature, $webhookSecret);
    }

    public function isDirectCreditRefundEnabled(?PaymentMethod $paymentMethod): bool
    {
        if (!$paymentMethod instanceof PaymentMethod || self::METHOD !== $paymentMethod->getMethod()) {
            return false;
        }

        try {
            $config = $this->resolveConfig($paymentMethod);
        } catch (\Throwable) {
            return false;
        }

        return $config->isRefundCreditNoteDirectly();
    }

    /**
     * @param array<string, mixed> $creditDetails
     *
     * @return array<string, mixed>
     */
    public function refundCreditNote(Cart $cart, Invoice $invoice, array $creditDetails): array
    {
        $paymentMethod = $cart->getPaymentMethod();
        if (!$paymentMethod instanceof PaymentMethod || self::METHOD !== $paymentMethod->getMethod()) {
            return [];
        }

        $this->boot($paymentMethod);

        if (!$this->config?->isRefundCreditNoteDirectly()) {
            return [
                'attempted' => false,
                'status' => 'skipped',
                'message' => 'Le remboursement direct par carte est desactive dans la configuration Stripe.',
            ];
        }

        $amount = max(0, (int) ($creditDetails['totals']['total_ttc'] ?? 0));
        if ($amount <= 0) {
            return [
                'attempted' => false,
                'status' => 'skipped',
                'message' => 'Le montant de l\'avoir est nul.',
            ];
        }

        $paymentIntentId = $this->extractPaymentIntentId($cart->getCartCheckout()?->getResponse());
        if (null === $paymentIntentId) {
            return [
                'attempted' => true,
                'status' => 'failed',
                'message' => 'Aucun payment_intent Stripe n\'a ete retrouve sur la commande.',
                'amount_ttc' => $amount,
            ];
        }

        try {
            /** @var Refund $refund */
            $refund = $this->client()->refunds->create([
                'payment_intent' => $paymentIntentId,
                'amount' => $amount,
                'metadata' => [
                    'order_reference' => (string) $cart->getToken(),
                    'credit_number' => null !== $invoice->getNumberCredit() ? (string) $invoice->getNumberCredit() : '',
                    'invoice_year' => null !== $invoice->getYear() ? (string) $invoice->getYear() : '',
                ],
            ]);

            return [
                'attempted' => true,
                'status' => (string) ($refund->status ?? 'pending'),
                'refund_id' => $refund->id,
                'payment_intent' => $paymentIntentId,
                'amount_ttc' => $amount,
                'created_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
            ];
        } catch (\Throwable $exception) {
            return [
                'attempted' => true,
                'status' => 'failed',
                'payment_intent' => $paymentIntentId,
                'amount_ttc' => $amount,
                'message' => $exception->getMessage(),
                'created_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
            ];
        }
    }

    /**
     * @param array<string, mixed>|null $response
     */
    public function extractPaymentIntentId(?array $response): ?string
    {
        if (!\is_array($response)) {
            return null;
        }

        $candidates = [
            $response['payment_intent'] ?? null,
            $response['object']['payment_intent']['id'] ?? null,
            $response['raw']['data']['object']['payment_intent'] ?? null,
            $response['session']['payment_intent'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            if (\is_string($candidate) && '' !== $candidate) {
                return $candidate;
            }
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    public function normalizeWebhookEvent(Event $event): array
    {
        $json = json_encode($event, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
        $payload = false !== $json ? json_decode($json, true) : null;
        if (!\is_array($payload)) {
            $payload = [];
        }

        $object = \is_array($payload['data']['object'] ?? null) ? $payload['data']['object'] : [];
        $eventType = \is_string($payload['type'] ?? null) ? $payload['type'] : null;
        $metadata = \is_array($object['metadata'] ?? null) ? $object['metadata'] : [];
        $latestRefund = [];

        if (
            'charge.refunded' === $eventType
            && \is_array($object['refunds']['data'] ?? null)
            && [] !== $object['refunds']['data']
        ) {
            $latestRefund = end($object['refunds']['data']);
            $latestRefund = \is_array($latestRefund) ? $latestRefund : [];

            if (\is_array($latestRefund['metadata'] ?? null) && [] !== $latestRefund['metadata']) {
                $metadata = $latestRefund['metadata'];
            }
        }

        if ('refund.updated' === $eventType || 'refund.failed' === $eventType) {
            $latestRefund = $object;
        }

        return [
            'event_id' => $payload['id'] ?? null,
            'event_type' => $eventType,
            'object' => $object,
            'session' => $object,
            'checkout_session_id' => $this->normalizeCheckoutSessionIdentifier($object['id'] ?? null),
            'client_reference_id' => $object['client_reference_id'] ?? null,
            'customer_id' => $object['customer'] ?? null,
            'payment_intent' => $latestRefund['payment_intent'] ?? $object['payment_intent'] ?? null,
            'payment_status' => $object['payment_status'] ?? null,
            'status' => $object['status'] ?? null,
            'metadata' => $metadata,
            'payment_reference' => $metadata['payment_reference'] ?? ($object['client_reference_id'] ?? null),
            'transaction_reference' => $metadata['transaction_id'] ?? ($metadata['payment_reference'] ?? ($object['client_reference_id'] ?? null)),
            'order_reference' => $metadata['order_reference'] ?? ($metadata['payment_reference'] ?? ($metadata['transaction_id'] ?? ($object['client_reference_id'] ?? null))),
            'transaction_fee_amount' => $this->extractTransactionFeeAmountFromWebhookObject($object),
            'transaction_fee_currency' => $this->extractTransactionFeeCurrencyFromWebhookObject($object),
            'balance_transaction_id' => $this->extractBalanceTransactionIdFromWebhookObject($object),
            'refund_id' => $latestRefund['id'] ?? null,
            'refund_status' => $latestRefund['status'] ?? null,
            'refund_amount' => $latestRefund['amount'] ?? null,
            'refund_failure_reason' => $latestRefund['failure_reason'] ?? null,
            'charge_id' => $latestRefund['charge'] ?? ($object['id'] ?? null),
            'raw' => $payload,
        ];
    }

    private function client(): StripeClient
    {
        if (!$this->client instanceof StripeClient) {
            throw new \RuntimeException('Le client Stripe n\'est pas initialise.');
        }

        return $this->client;
    }

    private function boot(?PaymentMethod $paymentMethod = null): void
    {
        $config = $this->resolveConfig($paymentMethod);
        $secretKey = $config->getSecretKey();

        if (null === $secretKey || '' === $secretKey) {
            throw new \RuntimeException('La cle secrete Stripe est manquante.');
        }

        if ($this->client instanceof StripeClient && $this->bootedSecretKey === $secretKey) {
            $this->config = $config;

            return;
        }

        $this->config = $config;
        $this->client = new StripeClient($secretKey);
        $this->bootedSecretKey = $secretKey;
    }

    private function resolveConfig(?PaymentMethod $paymentMethod = null): StripeModel
    {
        if ($paymentMethod instanceof PaymentMethod && self::METHOD === $paymentMethod->getMethod() && [] !== $paymentMethod->getConfiguration()) {
            /** @var StripeModel $config */
            $config = $this->serializer->denormalize($paymentMethod->getConfiguration(), StripeModel::class);

            return $config;
        }

        $configuredMethod = $this->paymentMethodRepository->findOneBy(['method' => self::METHOD]);
        if (!$configuredMethod instanceof PaymentMethod || [] === $configuredMethod->getConfiguration()) {
            throw new \RuntimeException('Payment method "stripe" non configure.');
        }

        /** @var StripeModel $config */
        $config = $this->serializer->denormalize($configuredMethod->getConfiguration(), StripeModel::class);

        return $config;
    }

    private function buildCheckoutLabel(HasPayment $payment): string
    {
        if ($payment instanceof Cart) {
            return sprintf('Commande %s', (string) $payment->getToken());
        }

        return sprintf('Paiement %s', (string) $payment->getToken());
    }

    /**
     * @return array<string, string>
     */
    private function buildReturnParameters(HasPayment $payment): array
    {
        if ($payment instanceof Cart) {
            return ['cartId' => $payment->getId()->toString()];
        }

        return [];
    }

    private function buildPaymentReference(HasPayment $payment): string
    {
        $rawReference = null;

        if ($payment instanceof Cart) {
            $rawReference = sprintf('CMD-%s', (string) $payment->getToken());
        } elseif ($payment instanceof \CpCreation\VitiCore\Payment\Model\Payment) {
            $rawReference = $payment->getReference() ?: $payment->getSlug() ?: $payment->getToken();
            $rawReference = sprintf('PAY-%s', (string) $rawReference);
        } else {
            $rawReference = (string) $payment->getToken();
        }

        $normalizedReference = preg_replace('/[^A-Za-z0-9._-]+/', '-', $rawReference) ?? $rawReference;
        $normalizedReference = trim($normalizedReference, '-_.');

        if ('' === $normalizedReference) {
            $normalizedReference = 'PAYMENT';
        }

        return mb_substr($normalizedReference, 0, 180);
    }

    /**
     * @return array<string, string>
     */
    private function buildStripeMetadata(HasPayment $payment, string $paymentReference): array
    {
        $metadata = [
            'transaction_id' => (string) $payment->getToken(),
            'payment_reference' => $paymentReference,
            'order_reference' => (string) $payment->getToken(),
            'payment_method' => self::METHOD,
            'payment_type' => $payment instanceof Cart ? 'cart' : 'payment',
        ];

        if ($payment instanceof Cart) {
            $metadata['cart_id'] = $payment->getId()->toString();
        }

        if ($payment instanceof \CpCreation\VitiCore\Payment\Model\Payment) {
            $metadata['payment_id'] = $payment->getId()->toString();
            $metadata['payment_slug'] = (string) $payment->getSlug();
            $metadata['payment_reference_source'] = (string) ($payment->getReference() ?: $payment->getSlug() ?: $payment->getToken());
            $metadata['order_reference'] = (string) ($payment->getReference() ?: $paymentReference);
        }

        return $metadata;
    }

    private function normalizeStripeAmount(mixed $value): ?int
    {
        return \is_int($value) ? $value : null;
    }

    private function extractTransactionFeeAmountFromWebhookObject(array $object): ?int
    {
        $balanceTransaction = $this->extractBalanceTransactionFromWebhookObject($object);

        return $this->normalizeStripeAmount($balanceTransaction['fee'] ?? null);
    }

    private function extractTransactionFeeCurrencyFromWebhookObject(array $object): ?string
    {
        $balanceTransaction = $this->extractBalanceTransactionFromWebhookObject($object);
        $currency = $balanceTransaction['currency'] ?? null;

        return \is_string($currency) && '' !== $currency ? $currency : null;
    }

    private function extractBalanceTransactionIdFromWebhookObject(array $object): ?string
    {
        $balanceTransaction = $this->extractBalanceTransactionFromWebhookObject($object);
        $balanceTransactionId = $balanceTransaction['id'] ?? null;

        return \is_string($balanceTransactionId) && '' !== $balanceTransactionId ? $balanceTransactionId : null;
    }

    /**
     * @return array<string, mixed>
     */
    private function extractBalanceTransactionFromWebhookObject(array $object): array
    {
        $latestCharge = \is_array($object['latest_charge'] ?? null) ? $object['latest_charge'] : null;

        if (\is_array($latestCharge['balance_transaction'] ?? null)) {
            return $latestCharge['balance_transaction'];
        }

        if (\is_array($object['balance_transaction'] ?? null)) {
            return $object['balance_transaction'];
        }

        return [];
    }

    private function normalizeCheckoutSessionIdentifier(mixed $value): ?string
    {
        return \is_string($value) && str_starts_with($value, 'cs_') ? $value : null;
    }

    /**
     * @param array<string, string> $metadata
     */
    private function resolveCheckoutCustomerId(HasPayment $payment, array $metadata): ?string
    {
        $email = mb_trim((string) $payment->getEmail());
        if ('' === $email) {
            return null;
        }

        $customerData = $this->buildCustomerData($payment, $metadata);
        $existingCustomer = $this->findCustomerByEmail($email);

        if ($existingCustomer instanceof Customer) {
            /** @var Customer $updatedCustomer */
            $updatedCustomer = $this->client()->customers->update($existingCustomer->id, $customerData);

            return $updatedCustomer->id;
        }

        /** @var Customer $customer */
        $customer = $this->client()->customers->create($customerData);

        return $customer->id;
    }

    private function findCustomerByEmail(string $email): ?Customer
    {
        $customers = $this->client()->customers->all([
            'email' => $email,
            'limit' => 1,
        ]);

        $customer = $customers->data[0] ?? null;

        return $customer instanceof Customer ? $customer : null;
    }

    /**
     * @param array<string, string> $metadata
     *
     * @return array<string, mixed>
     */
    private function buildCustomerData(HasPayment $payment, array $metadata): array
    {
        $customerData = [
            'email' => mb_trim((string) $payment->getEmail()),
            'name' => $this->buildCustomerName($payment),
            'phone' => $this->normalizeOptionalString($payment->getPhone()),
            'metadata' => $metadata,
        ];

        $address = array_filter([
            'line1' => $this->normalizeOptionalString($payment->getStreet()),
            'postal_code' => $this->normalizeOptionalString($payment->getPostalCode()),
            'city' => $this->normalizeOptionalString($payment->getCity()),
            'country' => $this->normalizeOptionalString($payment->getCountry()),
        ], static fn (?string $value): bool => null !== $value && '' !== $value);

        if ([] !== $address) {
            $customerData['address'] = $address;
        }

        return array_filter(
            $customerData,
            static fn (mixed $value): bool => null !== $value && [] !== $value
        );
    }

    private function buildCustomerName(HasPayment $payment): ?string
    {
        $company = $this->normalizeOptionalString($payment->getCompany());
        if (null !== $company) {
            return $company;
        }

        $firstname = $this->normalizeOptionalString($payment->getFirstname());
        $lastname = $this->normalizeOptionalString($payment->getLastname());
        $fullName = mb_trim(implode(' ', array_filter([$firstname, $lastname])));

        return '' !== $fullName ? $fullName : null;
    }

    private function normalizeOptionalString(?string $value): ?string
    {
        if (null === $value) {
            return null;
        }

        $normalized = mb_trim($value);

        return '' !== $normalized ? $normalized : null;
    }
}
