<?php

namespace StripePayment;

use Propel\Runtime\ActiveQuery\Criteria;
use Propel\Runtime\Connection\ConnectionInterface;
use StripePayment\Classes\StripePaymentException;
use StripePayment\Classes\StripePaymentLog;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Thelia\Core\Event\Image\ImageEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\HttpFoundation\Response;
use Thelia\Core\Template\ParserInterface;
use Thelia\Core\Translation\Translator;
use Thelia\Log\Tlog;
use Thelia\Model\ConfigQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Model\Message;
use Thelia\Model\MessageQuery;
use Thelia\Model\ModuleQuery;
use Thelia\Model\Order;
use Thelia\Model\Order as OrderModel;
use Thelia\Model\OrderCouponQuery;
use Thelia\Model\OrderProductAttributeCombinationQuery;
use Thelia\Model\OrderProductQuery;
use Thelia\Model\ProductImageQuery;
use Thelia\Model\ProductQuery;
use Thelia\Module\AbstractPaymentModule;
use Thelia\Tools\URL;

/**
 * Class StripePayment
 * @package StripePayment
 * @author Etienne Perriere - OpenStudio <eperriere@openstudio.fr>
 */
class StripePayment extends AbstractPaymentModule
{
    const MESSAGE_DOMAIN = "stripepayment";
    const CONFIRMATION_MESSAGE_NAME = "stripe_confirm_payment";

    const PAYMENT_INTENT_ID_SESSION_KEY = 'payment_intent_id';
    const PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY = 'payment_intent_customer_id';
    const PAYMENT_INTENT_SECRET_SESSION_KEY = 'payment_intent_secret';

    const ENABLED = "enabled";
    const STRIPE_ELEMENT = "stripe_element";
    const ONE_CLICK_PAYMENT = "one_click_payment";
    const SECRET_KEY = "secret_key";
    const PUBLISHABLE_KEY = "publishable_key";
    const WEBHOOKS_KEY = "webhooks_key";
    const SECURE_URL = "secure_url";

    public function preActivation(ConnectionInterface $con = null)
    {
        // Check if Stripe API is present
        try {
            $this->checkApi();
        } catch (\Exception $ex) {
            throw $ex;
        }

        return true;
    }

    public function postActivation(ConnectionInterface $con = null): void
    {
        // Module image
        $moduleModel = $this->getModuleModel();

        if (! $moduleModel->isModuleImageDeployed($con)) {
            $this->deployImageFolder($moduleModel, sprintf('%s'.DS.'Resource'.DS.'images'.DS.'module', __DIR__), $con);
        }

        $this->createMailMessage();
    }

    public function createMailMessage()
    {
        // Create payment confirmation message from templates, if not already defined
        if (null === MessageQuery::create()->findOneByName(self::CONFIRMATION_MESSAGE_NAME)) {

            $languages = LangQuery::create()->find();

            $message = new Message();
            $message
                ->setName(self::CONFIRMATION_MESSAGE_NAME)
                ->setHtmlTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.html')
                ->setTextTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.txt')
            ;

            foreach ($languages as $language) {
                /** @var Lang $language */
                $locale = $language->getLocale();
                $message
                    ->setLocale($locale)
                    ->setTitle(
                        Translator::getInstance()->trans(
                            "Payment confirmation for Stripe Payment",
                            [],
                            self::MESSAGE_DOMAIN,
                            $locale
                        )
                    )
                    ->setSubject(
                        Translator::getInstance()->trans(
                            'Payment confirmation of your order {$order_ref} on {$store_name}',
                            [],
                            self::MESSAGE_DOMAIN,
                            $locale
                        )
                    )
                ;
            }

            $message->save();
        }
    }

    public function checkApi()
    {
        try {
            $ReflectedClass = new \ReflectionClass('Stripe\Stripe');
        } catch (\Exception $ex) {
            throw new \Exception(
                Translator::getInstance()->trans(
                    "Stripe library is missing.",
                    [],
                    self::MESSAGE_DOMAIN
                )
            );
        }
    }

    /**
     *
     *  Method used by payment gateway.
     *
     *  If this method return a \Thelia\Core\HttpFoundation\Response instance, this response is send to the
     *  browser.
     *
     *  In many cases, it's necessary to send a form to the payment gateway. On your response you can return this form already
     *  completed, ready to be sent
     *
     * @param  \Thelia\Model\Order $order processed order
     * @return null|\Thelia\Core\HttpFoundation\Response
     */
    public function pay(Order $order)
    {
        if (!$this->isValidPayment()) {
            throw new Exception("Your connection is not secured. Check that 'https' is present at the beginning of the site's address.");
        }

        return $this->doPay($order);
    }

    protected function doPay(Order $order)
    {
        $session = $this->getRequest()->getSession();

        try {

            if(StripePayment::getConfigValue('stripe_element')){
                $order->setTransactionRef($session->get(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY))
                    ->save();
                $session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null);
                $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null);
                $session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null);

                return;
            }

            $session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null);
            $session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null);
            $session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null);

            // Create the session on Stripe's servers - this will charge the user's order and save session id into order transaction reference
            return $this->createStripeSession($order);
        } catch(\Stripe\Exception\CardException $e) {
            // The card has been declined
            // FIXME Translate message here
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Card declined. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'Your card has been declined.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (\Stripe\Exception\RateLimitException $e) {
            // Too many requests made to the API too quickly
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Too many requests. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'Too many requests too quickly.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (\Stripe\Exception\InvalidRequestException $e) {
            // Invalid parameters were supplied to Stripe's API
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Invalid parameters. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'Invalid parameters were supplied to Stripe.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (\Stripe\Exception\AuthenticationException $e) {
            // Authentication with Stripe's API failed
            // (maybe you changed API keys recently)
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Authentication failed: API key changed? Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'Authentication with Stripe failed. Please contact administrators.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (\Stripe\Exception\ApiConnectionException $e) {
            // Network communication with Stripe failed
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Network communication failed. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'Network communication failed.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (\Stripe\Exception\ApiErrorException $e) {
            // Display a very generic error to the user
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'An error occurred with Stripe.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        } catch (StripePaymentException $e) {
            // Amount shown to the user by Stripe & order amount are not equal
            $logMessage = sprintf(
                'Error paying order %d with Stripe. Amounts are different. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = $e->getMessage();
        } catch (\Exception $e) {
            // Something else happened, completely unrelated to Stripe
            $logMessage = sprintf(
                'Error paying order %d with Stripe but maybe unrelated with it. Message: %s',
                $order->getId(),
                $e->getMessage()
            );

            $userMessage = Translator::getInstance()
                ->trans(
                    'An error occurred during payment.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                );
        }

        if ($logMessage !== NULL) {
            (new StripePaymentLog())->logText($logMessage);

            return new RedirectResponse(
                URL::getInstance()->absoluteUrl("/order/failed/".$order->getId()."/".$userMessage)
                );
        }

        return new Response();
    }

    public function createStripeSession(OrderModel $order)
    {
        /* Impossible d'ajouter une ligne spécifique pour la remise, cette partie est mise de côté en attendant que stripe ajoute cette possibilité

        $lineItems = $this->prepareLineItems($order);

        */

        $currency = $order->getCurrency();

        if (null === $currency) {
            $currency = $this->getRequest()->getSession()->getCurrency();
        }

        $lineItems[] = [
            'name'=> Translator::getInstance()->trans('Total', [], StripePayment::MESSAGE_DOMAIN ),
            'quantity'=> 1,
            'currency' => strtolower($currency->getCode()),
            'amount' => round($order->getTotalAmount(), 2) * 100
        ];

        if(empty($lineItems)){
            throw new \Exception("Sorry, your cart is empty. There's nothing to pay.");
        }

        $stripe = new \Stripe\StripeClient(StripePayment::getConfigValue('secret_key'));

        $session = $stripe->checkout->sessions->create([
            'customer_email' => $order->getCustomer()->getEmail(),
            'client_reference_id' => $order->getRef(),
            'payment_method_types' => ['card'],
            'line_items' => $lineItems,
            'mode' => 'payment',
            'success_url' => URL::getInstance()->absoluteUrl('/order/placed/' . $order->getId()),
            'cancel_url' => URL::getInstance()->absoluteUrl('/order/failed/' . $order->getId() . '/error'),
        ]);

        $order->setTransactionRef($session->payment_intent)->save();

        /** @var ParserInterface $parser */
        $parser = $this->getContainer()->get("thelia.parser");

        $parser->setTemplateDefinition(
            $parser->getTemplateHelper()->getActiveFrontTemplate(),
            true
        );

        $renderedTemplate = $parser->render(
            "stripe-paiement.html",
            [
                'checkout_session_id' => $session->id,
                'public_key' => StripePayment::getConfigValue('publishable_key')
            ]
        );

        return new Response($renderedTemplate);
    }

    /**
     *
     * This method is call on Payment loop.
     *
     * If you return true, the payment method will be display
     * If you return false, the payment method will not be display
     *
     * @return boolean
     */
    public function isValidPayment()
    {
        $secretKey = self::getConfigValue(self::SECRET_KEY);
        return ( (($this->isDevEnvironment() || $this->isSslEnabled()) && self::getConfigValue('enabled')) && $secretKey && $this->getCurrentOrderTotalAmount() > 0);
    }

    /**
     * Return true if the current environment is in Dev mode
     *
     * @return bool
     */
    protected function isDevEnvironment()
    {
        return 'dev' === $this->getContainer()->getParameter('kernel.environment');
    }

    /**
     * return true if SSL is enabled
     *
     * @return bool
     */
    protected function isSslEnabled()
    {
        return $this->getRequest()->isSecure();
    }

    public function checkOrderAmount(OrderModel $order, $stripeAmount)
    {
        $orderAmount = $order->getTotalAmount() * 100;

        if (strval($stripeAmount) != strval($orderAmount)) {
            throw new StripePaymentException(Translator::getInstance()
                ->trans(
                    'The payment mean does not have the same amount as your cart. Please reload and try again.',
                    [],
                    StripePayment::MESSAGE_DOMAIN
                )
            );
        }
    }

    protected function prepareLineItems(Order $order, $currency)
    {
        $stripeAmount = 0;
        $lineItems = [];

        $baseSourceFilePath = ConfigQuery::read('images_library_path');
        if ($baseSourceFilePath === null) {
            $baseSourceFilePath = THELIA_LOCAL_DIR . 'media' . DS . 'images';
        } else {
            $baseSourceFilePath = THELIA_ROOT . $baseSourceFilePath;
        }
        if(null !== $orderProducts = OrderProductQuery::create()->filterByOrderId($order->getId())->joinOrderProductTax('opt', Criteria::LEFT_JOIN)->withColumn('SUM(`opt`.AMOUNT)', 'TOTAL_TAX')->withColumn('SUM(`opt`.PROMO_AMOUNT)', 'TOTAL_PROMO_TAX')->groupById()->find()){
            foreach ($orderProducts as $orderProduct) {
                $description='';
                if(null !== $orderProductAttributeCombinations = OrderProductAttributeCombinationQuery::create()->filterByOrderProductId($orderProduct->getId())->find()){
                    foreach ($orderProductAttributeCombinations as $orderProductAttributeCombination) {
                        if($description) $description .= ', ';
                        $description .= $orderProductAttributeCombination->getAttributeTitle() . ' ' . $orderProductAttributeCombination->getAttributeAvTitle();
                    }
                }
                $images=array();
                if(null !== $product = ProductQuery::create()->filterByRef($orderProduct->getProductRef())->findOne()){
                    if(null !== $productImages = ProductImageQuery::create()->filterByProductId($product->getId())->filterByVisible(1)->orderBy('position')->find()){
                        foreach ($productImages as $productImage) {
                            // Put source image file path
                            $sourceFilePath = sprintf(
                                '%s/%s/%s',
                                $baseSourceFilePath,
                                'product',
                                $productImage->getFile()
                            );

                            // Create image processing event
                            $event = new ImageEvent();
                            $event->setSourceFilepath($sourceFilePath);
                            $event->setCacheSubdirectory('product');
                            $width=100;
                            try {
                                // Dispatch image processing event
                                $event->setWidth($width);
                                $order->getDispatcher()->dispatch(TheliaEvents::IMAGE_PROCESS, $event);
                                $images[]=$event->getFileUrl();
                            } catch (\Exception $ex) {
                                // Ignore the result and log an error
                                Tlog::getInstance()->addError(sprintf("Failed to process image in image loop: %s", $ex->getMessage()));
                            }
                        }
                    }
                }
                if($orderProduct->getWasInPromo()){
                    $amount = (float) $orderProduct->getPromoPrice() + (float) $orderProduct->getVirtualColumn('TOTAL_PROMO_TAX');
                }else{
                    $amount = (float) $orderProduct->getPrice() + (float) $orderProduct->getVirtualColumn('TOTAL_TAX');
                }

                $stripeAmount += $amount * $orderProduct->getQuantity() * 100;
                $lineItems[] = [
                    'name' => $orderProduct->getTitle(),
                    'description' => $description,
                    'images' => $images,
                    'amount' => $amount*100,
                    'currency' => $currency,
                    'quantity' => $orderProduct->getQuantity(),
                ];
            }
        }
        if ($order->getPostage()){
            if (null !== $module = ModuleQuery::create()->findPk($order->getDeliveryModuleId())){
                $locale = $this->getRequest()->getLocale();
                if ($locale === 'en') {
                    $locale = 'en_US';
                }
                $module->setLocale($locale);

                if (!$module->getTitle()) {
                    $module->setLocale('fr_FR');
                }
                $lineItems[] = ['name'=> $module->getTitle(), 'description' => $module->getChapo(), 'quantity'=> 1, 'currency' => $currency, 'amount' => ($order->getPostage()*100)];
                $stripeAmount += $order->getPostage() * 100;
            }
        }

        if($order->getDiscount() > 0){
            $description=null;
            if(null !== $orderCoupons = OrderCouponQuery::create()->filterByOrderId($order->getId())->find()){
                foreach($orderCoupons as $orderCoupon){
                    if($description)$description .= ', ';
                    $description .= $orderCoupon->getTitle();
                }
            }
            $lineItems[] = ['name'=> Translator::getInstance()->trans('Discount', [], StripePayment::MESSAGE_DOMAIN ), 'description' => $description, 'quantity'=> 1, 'currency' => $currency, 'amount' => -($order->getDiscount()*100)];
            $stripeAmount -= $order->getDiscount() * 100;
        }

        $this->checkOrderAmount($order, $stripeAmount);

        return $lineItems;
    }


    /**
     * if you want, you can manage stock in your module instead of order process.
     * Return false to decrease the stock when order status switch to pay
     *
     * @return bool
     */
    public function manageStockOnCreation()
    {
        return false;
    }

    public static function configureServices(ServicesConfigurator $servicesConfigurator): void
    {
        $servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
            ->exclude([THELIA_MODULE_DIR . ucfirst(self::getModuleCode()). "/I18n/*"])
            ->autowire(true)
            ->autoconfigure(true);
    }
}
