<?php

declare(strict_types=1);

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

use CpCreation\VitiCore\Behavior\HasPayment;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Utils\Mailer;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartCheckout;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Payment\Model\Payment;
use CpCreation\VitiCore\Payment\Repository\PaymentRepository;
use Psr\Log\LoggerInterface;
use Stripe\Checkout\Session as StripeCheckoutSession;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

final class StripePaymentStateSynchronizer
{
    public function __construct(
        private readonly StripePayment $stripePayment,
        private readonly CartRepository $cartRepository,
        private readonly PaymentRepository $paymentRepository,
        private readonly Mailer $mailer,
        private readonly RequestStack $requestStack,
        private readonly LoggerInterface $logger,
    ) {
    }

    public function persistCheckoutInitiation(HasPayment $payment, StripeCheckoutSession $session): void
    {
        $normalizedSession = $this->stripePayment->normalizeCheckoutSession($session);
        $patch = [
            'stripe_checkout_init' => $normalizedSession,
            'checkout_session_id' => $normalizedSession['checkout_session_id'] ?? null,
            'payment_intent' => $normalizedSession['payment_intent'] ?? null,
        ];

        if ($payment instanceof Cart) {
            $checkout = $payment->getCartCheckout();

            if (!$checkout instanceof CartCheckout) {
                return;
            }

            $checkout->setResponse($this->mergeStripeResponse($checkout->getResponse() ?? [], $patch));
            $this->cartRepository->save($payment);

            return;
        }

        if ($payment instanceof Payment) {
            $payment->setResponse($this->mergeStripeResponse($payment->getResponse(), $patch));
            $this->paymentRepository->save($payment);
        }
    }

    public function synchronizeCartReturn(Request $request): bool
    {
        $cartId = $request->get('cartId');

        if (!\is_string($cartId) || '' === $cartId) {
            $cartId = $this->getSession()?->get('cart');
        }

        if (!\is_string($cartId) || '' === $cartId) {
            return false;
        }

        $cart = $this->cartRepository->find($cartId);

        if (!$cart instanceof Cart) {
            return false;
        }

        $sessionId = $this->normalizeCheckoutSessionId($request->get('session_id'));

        return $this->synchronizeCart(
            $cart,
            $sessionId,
            $this->buildStripeReturnPayload($request)
        );
    }

    public function synchronizeCartById(string $cartId, ?string $sessionId = null): bool
    {
        $cart = $this->cartRepository->find($cartId);

        if (!$cart instanceof Cart) {
            return false;
        }

        return $this->synchronizeCart($cart, $sessionId);
    }

    public function synchronizeStandaloneReturn(Request $request): bool
    {
        $sessionId = $this->normalizeCheckoutSessionId($request->get('session_id'));
        $normalizedResponse = null;

        if (null !== $sessionId) {
            try {
                $stripeSession = $this->stripePayment->retrieveCheckoutSession($sessionId);
                $normalizedResponse = $this->stripePayment->normalizeCheckoutSession($stripeSession);
            } catch (\Throwable $exception) {
                $this->logger->warning('Impossible de relire la session Stripe standalone au retour.', [
                    'session_id' => $sessionId,
                    'exception' => $exception::class,
                    'message' => $exception->getMessage(),
                ]);
            }
        }

        $payment = null;
        $reference = \is_array($normalizedResponse) ? ($normalizedResponse['transaction_reference'] ?? null) : null;

        if (\is_string($reference) && '' !== $reference) {
            $payment = $this->paymentRepository->findOneBy(['slug' => $reference]);
        }

        if (!$payment instanceof Payment) {
            $paymentSlug = $this->getSession()?->get('payment_slug');

            if (\is_string($paymentSlug) && '' !== $paymentSlug) {
                $payment = $this->paymentRepository->findOneBy(['slug' => $paymentSlug]);
            }
        }

        if (!$payment instanceof Payment) {
            return false;
        }

        return $this->synchronizeStandalonePayment(
            $payment,
            $sessionId,
            $this->buildStripeReturnPayload($request),
            $normalizedResponse
        );
    }

    /**
     * @param array<string, mixed>|null $returnPatch
     * @param array<string, mixed>|null $normalizedResponse
     */
    private function synchronizeCart(Cart $cart, ?string $sessionId = null, ?array $returnPatch = null, ?array $normalizedResponse = null): bool
    {
        $checkout = $cart->getCartCheckout();

        if (!$checkout instanceof CartCheckout) {
            return false;
        }

        if (\is_array($returnPatch)) {
            $checkout->setResponse($this->mergeStripeResponse($checkout->getResponse() ?? [], $returnPatch));
        }

        if (null !== $checkout->getPayedAt()) {
            $this->cartRepository->save($cart);

            return true;
        }

        $sessionId ??= $this->resolveStoredSessionId($checkout->getResponse() ?? []);

        if (null === $normalizedResponse && null !== $sessionId) {
            try {
                $stripeSession = $this->stripePayment->retrieveCheckoutSession($sessionId, $cart->getPaymentMethod());
                $normalizedResponse = $this->stripePayment->normalizeCheckoutSession($stripeSession);
            } catch (\Throwable $exception) {
                $this->logger->warning('Impossible de relire la session Stripe du panier.', [
                    'cart_id' => $cart->getId()->toString(),
                    'session_id' => $sessionId,
                    'exception' => $exception::class,
                    'message' => $exception->getMessage(),
                ]);
            }
        }

        if (\is_array($normalizedResponse)) {
            $checkout->setResponse($this->mergeStripeResponse(
                $checkout->getResponse() ?? [],
                $this->buildStripeSessionPatch($normalizedResponse)
            ));
        }

        if (!$this->isPaid($normalizedResponse, $cart->getPaymentMethod())) {
            $this->cartRepository->save($cart);

            return false;
        }

        $checkout->setPayedAt(new \DateTime('now'));

        if (null === $checkout->getPayedToken()) {
            $checkout->setPayedToken($cart->getToken());
        }

        $this->cartRepository->save($cart);
        $this->mailer->dispatchPaidOrder($cart);

        return true;
    }

    /**
     * @param array<string, mixed>|null $returnPatch
     * @param array<string, mixed>|null $normalizedResponse
     */
    private function synchronizeStandalonePayment(
        Payment $payment,
        ?string $sessionId = null,
        ?array $returnPatch = null,
        ?array $normalizedResponse = null,
    ): bool {
        if (\is_array($returnPatch)) {
            $payment->setResponse($this->mergeStripeResponse($payment->getResponse(), $returnPatch));
        }

        if (null !== $payment->getPayedAt()) {
            $this->paymentRepository->save($payment);

            return true;
        }

        $sessionId ??= $this->resolveStoredSessionId($payment->getResponse());

        if (null === $normalizedResponse && null !== $sessionId) {
            try {
                $stripeSession = $this->stripePayment->retrieveCheckoutSession($sessionId, $payment->getPaymentMethod());
                $normalizedResponse = $this->stripePayment->normalizeCheckoutSession($stripeSession);
            } catch (\Throwable $exception) {
                $this->logger->warning('Impossible de relire la session Stripe du paiement standalone.', [
                    'payment_slug' => $payment->getSlug(),
                    'session_id' => $sessionId,
                    'exception' => $exception::class,
                    'message' => $exception->getMessage(),
                ]);
            }
        }

        if (\is_array($normalizedResponse)) {
            $payment->setResponse($this->mergeStripeResponse(
                $payment->getResponse(),
                $this->buildStripeSessionPatch($normalizedResponse)
            ));
        }

        if (!$this->isPaid($normalizedResponse, $payment->getPaymentMethod())) {
            $this->paymentRepository->save($payment);

            return false;
        }

        $payment->setPayedAt(new \DateTime('now'));
        $this->paymentRepository->save($payment);
        $this->mailer->sendPayment($payment);

        return true;
    }

    /**
     * @param array<string, mixed> $existing
     * @param array<string, mixed> $patch
     *
     * @return array<string, mixed>
     */
    private function mergeStripeResponse(array $existing, array $patch): array
    {
        return array_replace_recursive($existing, $patch);
    }

    /**
     * @return array<string, mixed>
     */
    private function buildStripeReturnPayload(Request $request): array
    {
        return [
            'stripe_return' => [
                'query' => $request->query->all(),
                'request' => $request->request->all(),
                'captured_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
            ],
        ];
    }

    /**
     * @param array<string, mixed> $normalizedResponse
     *
     * @return array<string, mixed>
     */
    private function buildStripeSessionPatch(array $normalizedResponse): array
    {
        return $this->mergeStripeResponse($normalizedResponse, [
            'stripe_checkout_sync' => $normalizedResponse,
            'checkout_session_id' => $normalizedResponse['checkout_session_id'] ?? null,
            'payment_intent' => $normalizedResponse['payment_intent'] ?? null,
            'synced_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
        ]);
    }

    /**
     * @param array<string, mixed>|null $normalizedResponse
     */
    private function isPaid(?array $normalizedResponse, mixed $paymentMethod): bool
    {
        if (!\is_array($normalizedResponse)) {
            return false;
        }

        if ($this->stripePayment->isSuccessfulCheckoutSessionPayload($normalizedResponse)) {
            return true;
        }

        $paymentIntentId = $this->stripePayment->extractPaymentIntentId($normalizedResponse);

        if (null === $paymentIntentId) {
            return false;
        }

        try {
            return $this->stripePayment->isSuccessfulPaymentIntent($paymentIntentId, $paymentMethod);
        } catch (\Throwable $exception) {
            $this->logger->warning('Impossible de relire le payment_intent Stripe.', [
                'payment_intent' => $paymentIntentId,
                'exception' => $exception::class,
                'message' => $exception->getMessage(),
            ]);

            return false;
        }
    }

    /**
     * @param array<string, mixed> $response
     */
    private function resolveStoredSessionId(array $response): ?string
    {
        $storedSessionId = $response['checkout_session_id']
            ?? $response['stripe_checkout_sync']['checkout_session_id']
            ?? $response['stripe_checkout_init']['checkout_session_id']
            ?? null;

        return $this->normalizeCheckoutSessionId($storedSessionId);
    }

    private function normalizeCheckoutSessionId(mixed $sessionId): ?string
    {
        if (!\is_string($sessionId)) {
            return null;
        }

        $normalizedSessionId = trim($sessionId);

        if ('' === $normalizedSessionId || '{CHECKOUT_SESSION_ID}' === $normalizedSessionId) {
            return null;
        }

        return str_starts_with($normalizedSessionId, 'cs_') ? $normalizedSessionId : null;
    }

    private function getSession(): ?SessionInterface
    {
        return $this->requestStack->getSession();
    }
}
