<?php

/*
 * This file is part of the VitiCore 2.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

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

use CpCreation\VitiCore\Behavior\HasPayment;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\Payment\Etransactions;
use CpCreation\VitiCore\Cart\Repository\PaymentMethodRepository;
use League\ISO3166\ISO3166;
use megastruktur\PhoneCountryCodes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\SerializerInterface;

final class EtransactionsPayment
{
    public const METHOD = 'e-transactions';
    public const TEST_URL = 'https://recette-tpeweb.e-transactions.fr/php/';
    public const PROD_URL = 'https://tpeweb.e-transactions.fr/php/';
    public const DEVISE = 978;
    private const DEFAULT_PUBLIC_KEY_PATH = __DIR__.'/../Resources/keys/paybox/pubkey.pem';

    public const PBX_RETOUR = 'Mt:M;Ref:R;Auto:A;Appel:T;Abo:B;Erreur:E;Trans:S;Pays:Y;Heure:Q;3DSecureVersion:v;Authentification:c,3DSecure:G;Carte4:j;Ip:I;Signature:K';

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

    private ?Etransactions $config = null;

    public function __construct(
        private readonly PaymentMethodRepository $paymentMethodRepository,
        private readonly SerializerInterface $serializer,
        private readonly RouterInterface $router,
        array $paths,
    ) {
        // Comme dans ton code, tu utilises $this->paths['etransactions']['done'] etc.
        $this->paths = $paths;
    }

    private function boot(): void
    {
        if (null !== $this->config) {
            return;
        }

        $paymentMethod = $this->paymentMethodRepository->findOneBy(['method' => self::METHOD]);
        if (!$paymentMethod) {
            throw new \RuntimeException('Payment method "e-transactions" introuvable.');
        }

        /** @var Etransactions $config */
        $config = $this->serializer->denormalize($paymentMethod->getConfiguration(), Etransactions::class);
        $this->config = $config;
    }

    public function canVerifyReturnSignature(): bool
    {
        $this->boot();

        return null !== $this->resolvePublicKey();
    }

    public function validReturn(Request $request): bool
    {
        $this->boot();

        $publicKey = $this->resolvePublicKey();
        if (null === $publicKey) {
            return false;
        }

        $payload = $this->rawReturnPayload($request);
        if ('' === $payload) {
            return false;
        }

        $signedMessage = $this->detachSignature($payload);
        if (null === $signedMessage) {
            return false;
        }

        [$message, $signature] = $signedMessage;
        $binarySignature = base64_decode(urldecode($signature), true);
        if (false === $binarySignature) {
            return false;
        }

        $key = openssl_pkey_get_public($publicKey);
        if (false === $key) {
            return false;
        }

        return 1 === openssl_verify($message, $binarySignature, $key, \OPENSSL_ALGO_SHA1);
    }

    private function rawReturnPayload(Request $request): string
    {
        $content = $request->getContent();
        if ('' !== $content) {
            return $content;
        }

        return $request->server->get('QUERY_STRING', '');
    }

    /**
     * @return array{0: string, 1: string}|null
     */
    private function detachSignature(string $payload): ?array
    {
        if (!preg_match('/(?:^|&)Signature=/', $payload, $matches, \PREG_OFFSET_CAPTURE)) {
            return null;
        }

        $offset = $matches[0][1];
        $separatorLength = str_starts_with($matches[0][0], '&') ? 1 : 0;
        $message = substr($payload, 0, $offset);
        $signature = substr($payload, $offset + \strlen($matches[0][0]));
        $trailingSeparator = strpos($signature, '&');

        if (false !== $trailingSeparator) {
            $signature = substr($signature, 0, $trailingSeparator);
        }

        if (1 === $separatorLength) {
            $message = rtrim($message, '&');
        }

        if ('' === $message || '' === $signature) {
            return null;
        }

        return [$message, $signature];
    }

    private function resolvePublicKey(): ?string
    {
        $publicKey = $this->config?->getPublicKey();

        if (null === $publicKey || '' === trim($publicKey)) {
            $content = file_get_contents(self::DEFAULT_PUBLIC_KEY_PATH);

            return false !== $content ? $content : null;
        }

        $publicKey = trim($publicKey);
        if (is_file($publicKey)) {
            $content = file_get_contents($publicKey);

            return false !== $content ? $content : null;
        }

        return $publicKey;
    }

    /**
     * @return array<string, mixed>
     */
    public function createData(HasPayment $payment): array
    {
        $this->boot();

        $returnParameters = $this->buildReturnParameters($payment);

        $url = $this->config->isProduction() ? self::PROD_URL : self::TEST_URL;

        // Routes (absolues)
        $responseAtRoute = $this->paths['etransactions']['response_at'] ?? null;
        $doneRoute = $this->paths['etransactions']['done'] ?? null;
        $refusedRoute = $this->paths['etransactions']['refused'] ?? null;
        $canceledRoute = $this->paths['etransactions']['canceled'] ?? null;

        if (!\is_string($responseAtRoute) || '' === $responseAtRoute) {
            throw new \RuntimeException('Route paths[etransactions][response_at] manquante.');
        }
        if (!\is_string($doneRoute) || '' === $doneRoute) {
            throw new \RuntimeException('Route paths[etransactions][done] manquante.');
        }
        if (!\is_string($refusedRoute) || '' === $refusedRoute) {
            throw new \RuntimeException('Route paths[etransactions][refused] manquante.');
        }
        if (!\is_string($canceledRoute) || '' === $canceledRoute) {
            throw new \RuntimeException('Route paths[etransactions][canceled] manquante.');
        }

        $pbxCmd = mb_strtoupper((string) $payment->getToken());

        $pbxRepondreA = $this->router->generate($responseAtRoute, [], UrlGeneratorInterface::ABSOLUTE_URL);
        $pbxEffectue = $this->router->generate($doneRoute, $returnParameters, UrlGeneratorInterface::ABSOLUTE_URL);
        $pbxRefuse = $this->router->generate($refusedRoute, $returnParameters, UrlGeneratorInterface::ABSOLUTE_URL);
        $pbxAnnule = $this->router->generate($canceledRoute, $returnParameters, UrlGeneratorInterface::ABSOLUTE_URL);

        // Shopping cart XML
        $qty = (int) $payment->getQuantity();
        $pbxShoppingCart = \sprintf(
            '<?xml version="1.0" encoding="utf-8"?><shoppingcart><total><totalQuantity>%d</totalQuantity></total></shoppingcart>',
            $qty
        );

        // Country numeric + phone code
        $iso = new ISO3166();
        $countryAlpha2 = (string) $payment->getCountry();
        $countryValues = $iso->alpha2($countryAlpha2); // ['alpha2','alpha3','numeric','name']
        $phoneCodes = new PhoneCountryCodes();
        $phoneCodeList = $phoneCodes->getCodesList();
        $countryPhoneCode = $phoneCodeList[$countryValues['alpha2']] ?? '';

        // Billing XML
        $firstName = (string) $payment->getFirstname();
        $lastName = (string) $payment->getLastname();
        $address1 = mb_substr((string) $payment->getStreet(), 0, 50);
        $zip = (string) $payment->getPostalCode();
        $city = (string) $payment->getCity();
        $mobile = (string) $payment->getPhone();
        $countryNum = (string) $countryValues['numeric'];

        $pbxBilling = '<?xml version="1.0" encoding="utf-8"?>'
            .'<Billing><Address>'
            .'<FirstName>'.$firstName.'</FirstName>'
            .'<LastName>'.$lastName.'</LastName>'
            .'<Address1>'.$address1.'</Address1>'
            .'<ZipCode>'.$zip.'</ZipCode>'
            .'<City>'.$city.'</City>'
            .'<CountryCode>'.$countryNum.'</CountryCode>'
            .'<CountryCodeMobilePhone>'.$countryPhoneCode.'</CountryCodeMobilePhone>'
            .'<MobilePhone>'.$mobile.'</MobilePhone>'
            .'</Address></Billing>';

        // Data PBX
        $data = [
            'URL' => $url,
            'PBX_SITE' => $this->config->getSite(),
            'PBX_RANG' => $this->config->getRank(),
            'PBX_IDENTIFIANT' => $this->config->getLogin(),
            'PBX_TOTAL' => $payment->getTotal(),
            'PBX_DEVISE' => self::DEVISE,
            'PBX_CMD' => $pbxCmd,
            'PBX_PORTEUR' => $payment->getEmail(),
            'PBX_REPONDRE_A' => $pbxRepondreA,
            'PBX_EFFECTUE' => $pbxEffectue,
            'PBX_REFUSE' => $pbxRefuse,
            'PBX_ANNULE' => $pbxAnnule,
            'PBX_RETOUR' => self::PBX_RETOUR,
            'PBX_HASH' => $this->config->getHmacAlgo(),
            'PBX_TIME' => (new \DateTimeImmutable())->format(\DATE_ATOM), // ISO 8601
            'PBX_SHOPPINGCART' => $pbxShoppingCart,
            'PBX_BILLING' => $pbxBilling,
        ];

        // Message HMAC dans le même ordre que tes concat
        $msg =
            'PBX_SITE='.$data['PBX_SITE']
            .'&PBX_RANG='.$data['PBX_RANG']
            .'&PBX_IDENTIFIANT='.$data['PBX_IDENTIFIANT']
            .'&PBX_TOTAL='.$data['PBX_TOTAL']
            .'&PBX_DEVISE='.$data['PBX_DEVISE']
            .'&PBX_CMD='.$data['PBX_CMD']
            .'&PBX_PORTEUR='.$data['PBX_PORTEUR']
            .'&PBX_REPONDRE_A='.$data['PBX_REPONDRE_A']
            .'&PBX_EFFECTUE='.$data['PBX_EFFECTUE']
            .'&PBX_REFUSE='.$data['PBX_REFUSE']
            .'&PBX_ANNULE='.$data['PBX_ANNULE']
            .'&PBX_RETOUR='.$data['PBX_RETOUR']
            .'&PBX_HASH='.$data['PBX_HASH']
            .'&PBX_TIME='.$data['PBX_TIME']
            .'&PBX_SHOPPINGCART='.$data['PBX_SHOPPINGCART']
            .'&PBX_BILLING='.$data['PBX_BILLING'];

        $algo = (string) $data['PBX_HASH']; // ex: "SHA512"
        $key = pack('H*', (string) $this->config->getHmacKey());

        $data['PBX_HMAC'] = mb_strtoupper(hash_hmac($algo, $msg, $key));

        return $data;
    }

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

        return [];
    }
}
