<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

use InPost\International\Checkout\Exception\AbortCheckoutException;
use InPost\International\Controller\Admin\ConfigurationController;
use InPost\International\Controller\Admin\PickupOrderController;
use InPost\International\Controller\Admin\ShipmentController;
use InPost\International\Delivery\Exception\DeliveryUnavailableException;
use InPost\International\Delivery\ShippingCostCalculationHandlerInterface;
use InPost\International\Delivery\ShippingCostCalculationParameters;
use InPost\International\DependencyInjection\ContainerFactory;
use InPost\International\Hook\HookDispatcherInterface;
use InPost\International\HttpFoundation\RequestStackProvider;
use InPost\International\Installer\Exception\CoreInstallationException;
use InPost\International\Installer\Exception\InstallerException;
use InPost\International\Installer\InstallerInterface;
use InPost\International\Installer\UninstallerInterface;
use InPost\International\Translation\Util\TranslationFinder;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

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

if (file_exists(__DIR__ . '/vendor/autoload.php')) {
    include_once __DIR__ . '/vendor/autoload.php';
}

class InPostInternational extends CarrierModule
{
    public const MODULE_NAME = 'inpostinternational';

    private const TRANSLATION_LOCALES = ['en-US', 'pl-PL'];

    /**
     * @var int identifier of the module's {@see Carrier} for which PrestaShop is currently performing shipping cost calculation.
     *          Must be initialized with a non-falsy value in order for the carrier ID to be set by the core on PS 9.0.0.
     *
     * @see Cart::getPackageShippingCostFromModule()
     */
    public $id_carrier = -1;

    /**
     * @var RequestStack|null
     */
    private $requestStack;

    public function __construct()
    {
        $this->name = 'inpostinternational';
        $this->version = '0.4.0';
        $this->author = 'InPost S.A.';
        $this->tab = 'shipping_logistics';
        $this->ps_versions_compliancy = ['min' => '1.7.7', 'max' => '9.0.99'];
        $this->controllers = ['checkout'];

        parent::__construct();

        try {
            $this->initTranslations();
        } catch (Exception $e) {
            // ignore silently
        }
        $this->displayName = $this->trans('InPost International', [], 'Modules.Inpostinternational.Admin');
        $this->description = $this->trans('Official InPost International integration module for PrestaShop', [], 'Modules.Inpostinternational.Admin');
        $this->confirmUninstall = $this->trans('Are you sure you want to uninstall this module?', [], 'Admin.Notifications.Warning');
    }

    public function isUsingNewTranslationSystem(): bool
    {
        return true;
    }

    public function install(): bool
    {
        if (70205 > PHP_VERSION_ID) {
            $this->_errors[] = $this->trans('This module requires PHP 7.2.5 or later.', [], 'Modules.Inpostinternational.Installer');

            return false;
        }

        try {
            $this->getInstaller()->install($this);

            return true;
        } catch (CoreInstallationException $e) {
            // the core `install()` method should have already added errors
            return false;
        } catch (InstallerException $e) {
            $this->_errors[] = $e->getMessage();

            return false;
        }
    }

    public function uninstall(bool $keepData = null): bool
    {
        $keepData = $keepData ?? $this->isResetAction();

        try {
            $this->getUninstaller()->uninstall($this, $keepData);

            return true;
        } catch (CoreInstallationException $e) {
            // the core `uninstall()` method should have already added errors
            return false;
        } catch (InstallerException $e) {
            $this->_errors[] = $e->getMessage();

            return false;
        }
    }

    public function reset(): bool
    {
        return $this->uninstall(true) && $this->install();
    }

    public function getContent(): ?string
    {
        /** @var UrlGeneratorInterface $router */
        $router = $this->get('router');

        try {
            Tools::redirectAdmin($router->generate('admin_inpost_intl_config_api'));
        } catch (RouteNotFoundException $e) {
            if ($this->isModuleConfigLoaded()) {
                return $this->displayError($this->trans('Cannot access the configuration page. Please clear the application cache.', [], 'Modules.Inpostinternational.Admin'));
            }

            $this->addSessionError($this->trans('Please enable the module to access the configuration page.', [], 'Modules.Inpostinternational.Admin'));
            Tools::redirectAdmin($router->generate('admin_module_manage'));
        }

        return null;
    }

    /**
     * @param Cart $params
     * @param float $shipping_cost
     *
     * @return float|false
     */
    public function getOrderShippingCost($params, $shipping_cost)
    {
        if (!$params instanceof Cart) {
            throw new InvalidArgumentException(sprintf('Argument 1 passed to %s() must be an instance of "%s", "%s" given.', __METHOD__, Cart::class, get_debug_type($params)));
        }

        return $this->getShippingCost($params, (float) $shipping_cost);
    }

    /**
     * @param Cart $params
     *
     * @return float|false
     */
    public function getOrderShippingCostExternal($params)
    {
        if (!$params instanceof Cart) {
            throw new InvalidArgumentException(sprintf('Argument 1 passed to %s() must be an instance of "%s", "%s" given.', __METHOD__, Cart::class, get_debug_type($params)));
        }

        return $this->getShippingCost($params);
    }

    /**
     * @template T of object
     *
     * @param string|class-string<T> $serviceName
     *
     * @phpstan-return ($serviceName is class-string<T> ? T : object)
     *
     * @return T|object
     */
    public function get($serviceName): object
    {
        return $this->getContainer()->get($serviceName);
    }

    public function getContainer(): ContainerInterface
    {
        if (
            $this->context->controller instanceof AdminController
            && Tools::version_compare(_PS_VERSION_, '1.7.8')
            && null !== $container = SymfonyContainer::getInstance()
        ) {
            return $container;
        }

        return parent::getContainer();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getTabs(): array
    {
        if ([] !== $this->tabs) {
            return $this->tabs;
        }

        return $this->tabs = [
            [
                'class_name' => ConfigurationController::TAB_NAME,
                'route_name' => 'admin_inpost_intl_config_api',
                'visible' => false,
                'name' => [
                    'en' => $this->trans('InPost International', [], 'Modules.Inpostinternational.Admin', 'en-US'),
                    'pl' => $this->trans('InPost International', [], 'Modules.Inpostinternational.Admin', 'pl-PL'),
                ],
                'wording' => 'InPost International',
                'wording_domain' => 'Modules.Inpostinternational.Admin',
            ],
            [
                'class_name' => ShipmentController::TAB_NAME,
                'parent_class_name' => 'AdminParentShipping',
                'route_name' => 'admin_inpost_intl_shipments_index',
                'name' => [
                    'en' => $this->trans('InPost International shipments', [], 'Modules.Inpostinternational.Admin', 'en-US'),
                    'pl' => $this->trans('InPost International shipments', [], 'Modules.Inpostinternational.Admin', 'pl-PL'),
                ],
                'wording' => 'InPost International shipments',
                'wording_domain' => 'Modules.Inpostinternational.Admin',
            ],
            [
                'class_name' => PickupOrderController::TAB_NAME,
                'parent_class_name' => ShipmentController::TAB_NAME,
                'route_name' => 'admin_inpost_intl_pickup_orders_index',
                'name' => [
                    'en' => $this->trans('Pickup orders', [], 'Modules.Inpostinternational.Pickup', 'en-US'),
                    'pl' => $this->trans('Pickup orders', [], 'Modules.Inpostinternational.Pickup', 'pl-PL'),
                ],
                'wording' => 'Pickup orders',
                'wording_domain' => 'Modules.Inpostinternational.Pickup',
            ],
        ];
    }

    /**
     * Handles hook calls.
     *
     * @param array{0?: array<string, mixed>} $arguments
     *
     * @return mixed hook result
     */
    public function __call(string $name, array $arguments)
    {
        $hookName = str_starts_with($name, 'hook') ? lcfirst(substr($name, 4)) : $name;
        $parameters = $arguments[0] ?? [];

        try {
            $dispatcher = $this->get(HookDispatcherInterface::class);
        } catch (ServiceNotFoundException $e) {
            return null;
        }

        try {
            return $dispatcher->dispatch($hookName, $parameters);
        } catch (AbortCheckoutException $e) {
            $this->terminate($e->getResponse());
        }
    }

    /**
     * @internal
     */
    public function getLogger(): LoggerInterface
    {
        try {
            return $this->get('inpost.intl.logger');
        } catch (Throwable $e) {
            return new NullLogger();
        }
    }

    /**
     * @internal
     */
    public function getRequestStack(): RequestStack
    {
        if (isset($this->requestStack)) {
            return $this->requestStack;
        }

        $provider = new RequestStackProvider($this->getContainer(), $this->context);

        return $this->requestStack = $provider->getRequestStack();
    }

    /**
     * @internal
     *
     * @return never-returns
     */
    public function terminate(Response $response): void
    {
        $this->saveSession();
        $this->context->cookie->write();

        $response->send();

        exit;
    }

    private function saveSession(): void
    {
        if (null === $session = $this->getSession()) {
            return;
        }

        if (!$session->isStarted()) {
            return;
        }

        $session->save();
    }

    private function getSession(): ?SessionInterface
    {
        if (null === $request = $this->getRequestStack()->getCurrentRequest()) {
            return null;
        }

        if (!$request->hasSession()) {
            return null;
        }

        return $request->getSession();
    }

    private function getInstaller(): InstallerInterface
    {
        try {
            return $this->get(InstallerInterface::class);
        } catch (ServiceNotFoundException $e) {
            return $this->createInstaller();
        }
    }

    private function getUninstaller(): UninstallerInterface
    {
        try {
            return $this->get(UninstallerInterface::class);
        } catch (ServiceNotFoundException $e) {
            return $this->createInstaller();
        }
    }

    /**
     * @return (InstallerInterface&UninstallerInterface)
     */
    private function createInstaller(): object
    {
        /** @var Container|null $container */
        $container = SymfonyContainer::getInstance();

        if (null === $container) {
            throw new RuntimeException('Cannot create installer: kernel container is not available.');
        }

        $installer = (new ContainerFactory($container))
            ->buildContainer([
                $this->getLocalPath() . 'config/services/installer.yml',
            ])
            ->get(InstallerInterface::class);

        $container->set(InstallerInterface::class, $installer);
        $container->set(UninstallerInterface::class, $installer);

        return $installer;
    }

    private function isResetAction(): bool
    {
        if (null !== $request = $this->getRequestStack()->getCurrentRequest()) {
            return 'admin_module_manage_action' === $request->attributes->get('_route') && 'reset' === $request->attributes->get('action');
        }

        if ('cli' !== PHP_SAPI) {
            return false;
        }

        $input = new ArgvInput();

        return 'prestashop:module' === $input->getFirstArgument() && 'reset' === $input->getArgument('action');
    }

    /**
     * @param float|null $shippingCost shipping cost calculated by core
     *
     * @return float|false final shipping cost or false if delivery for the cart/carrier is not available
     */
    private function getShippingCost(Cart $cart, float $shippingCost = null)
    {
        // disable the delivery option if the module is not enabled
        if (!$this->active) {
            return false;
        }

        // disable the delivery option if the carrier ID is not set or is not a valid identifier
        if (0 >= $carrierId = (int) $this->id_carrier) {
            return false;
        }

        try {
            $handler = $this->get(ShippingCostCalculationHandlerInterface::class);
        } catch (ServiceNotFoundException $e) {
            return false;
        }

        $params = new ShippingCostCalculationParameters($cart, $carrierId, $shippingCost);

        try {
            return $handler->getShippingCost($params);
        } catch (DeliveryUnavailableException $e) {
            return false;
        } catch (Throwable $e) {
            if (_PS_MODE_DEV_) {
                throw $e;
            }

            $this->getLogger()->critical('An error occurred while calculating shipping cost: {exception}', [
                'exception' => $e,
            ]);

            return false;
        }
    }

    /**
     * Loads the module's translations if it has not yet been installed.
     */
    private function initTranslations(): void
    {
        if (null !== $this->id) {
            return;
        }

        $translator = $this->getTranslator();
        $translationFinder = new TranslationFinder();

        foreach (self::TRANSLATION_LOCALES as $locale) {
            $baseCatalogue = $translator->getCatalogue($locale);

            if (null === $catalogue = $translationFinder->getCatalogue($this->getLocalPath() . 'translations', $locale)) {
                continue;
            }

            $baseCatalogue->addCatalogue($catalogue);
        }
    }

    /**
     * @return bool whether PS should have loaded the module's configuration files
     */
    private function isModuleConfigLoaded(): bool
    {
        if ($this->active) {
            return true;
        }

        if (Tools::version_compare(_PS_VERSION_, '8.0.0')) {
            return true;
        }

        return $this->hasShopAssociations();
    }

    private function addSessionError(string $message): void
    {
        /** @var Session|null $session */
        $session = $this->getSession();

        if (null !== $session) {
            $session->getFlashBag()->add('error', $message);
        }
    }
}
