<?php // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
/*
 * Copyright © Resurs Bank AB. All rights reserved.
 * See LICENSE for license details.
 */

declare(strict_types=1);

use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use Resursbank\MerchantApi\Exception\FakeContainerException;
use Resursbank\MerchantApi\Hook\ActionDispatcher;
use Resursbank\MerchantApi\Hook\ActionGetAdminOrderButtons;
use Resursbank\MerchantApi\Hook\DisplayAddressSelectorBottom;
use Resursbank\MerchantApi\Hook\DisplayAdminOrderMain;
use Resursbank\MerchantApi\Hook\DisplayFooter;
use Resursbank\MerchantApi\Hook\DisplayHeader;
use Resursbank\MerchantApi\Hook\DisplayOrderConfirmation;
use Resursbank\MerchantApi\Hook\DisplayPartPayment;
use Resursbank\MerchantApi\Hook\PaymentOptions;
use Resursbank\MerchantApi\Installer\Install;
use Resursbank\MerchantApi\Util\Log;
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;

// Make sure the file is loaded within the context of PrestaShop.
if (!defined('_PS_VERSION_')) {
    exit;
}

require_once __DIR__ . '/src/Service/Autoloader.php';

/**
 * Module main class.
 *
 * NOTE: Class must be named this way because of how it's loaded by PS.
 * NOTE: The Module class we extend from is in PS preload.
 */
// phpcs:disable
class Resursbank extends PaymentModule
// phpcs:enable
{
    /** @var string */
    public const PHP_VERSION_MIN = '8.1.0';

    /** @var string */
    public const PHP_VERSION_MAX = '8.1.' . PHP_INT_MAX;

    public function __construct()
    {
        // Module identifier. Should match the name of the main PHP file (excluding file type suffix).
        // PrestaShop docs specify only lowercase
        // and numbers are allowed.
        $this->name = 'resursbank';

        // Tab in the backoffice where the module is displayed.
        $this->tab = 'payments_gateways';

        // Module version.
        $this->version = '1.0.8';

        // Module author. Displayed in the module manager, and on **Payments ->
        // Payment methods** page in the back office. Note that the rendering
        // looks a little odd in the module manager because of the width of the
        // title element. Injecting HTML here solves that, but the HTML is
        // rendered as text in the **Payments -> Payment methods** page instead.
        $this->author = 'Resurs Bank AB';

        // Whether to load the module instance when displaying the module list.
        $this->need_instance = 0;

        // PrestaShop version constraints.
        $this->ps_versions_compliancy = [
            'min' => '8.2.0',
            'max' => _PS_VERSION_,
        ];

        // Whether the module uses Bootstrap for styling in the back office.
        $this->bootstrap = true;

        parent::__construct();

        // Name of the module displayed in back office, module manager.
        $this->displayName = 'Resurs Bank';

        // Description of the module displayed in back office, module manager.
        $this->description = 'Enables payments through Resurs Bank';

        // Confirmation message displayed when uninstalling the module.
        $this->confirmUninstall = 'Are you sure you want to uninstall?';

        $this->getCheckoutErrorMessages();
    }

    /**
     * Feature that does not require hooks to show error messages on checkout page.
     * @return void
     */
    public function getCheckoutErrorMessages(): void
    {
        if (isset($this->context->cookie) && !empty($this->context->cookie->resursbank_fail_error)) {
            try {
                $errors = json_decode(
                    json: $this->context->cookie->resursbank_fail_error,
                    associative: true,
                    depth: 512,
                    flags: JSON_THROW_ON_ERROR
                );
                if (is_array(value: $errors) && isset($this->context->controller)) {
                    foreach ($errors as $err) {
                        $this->context->controller->errors[] = $err;
                    }
                }
            } catch (Exception) {
                if (isset($this->context->controller)) {
                    $this->context->controller->errors[] = 'Ett oväntat fel uppstod vid betalningen.';
                }
            }
            unset($this->context->cookie->resursbank_fail_error);
            $this->context->cookie->write();
        }
    }

    /**
     * We create a custom table, `resursbank_order` to contain our payment
     * information related to PS orders. We avoid using the PS order table
     * because it's unclear how we would implement the corresponding properties
     * properly on the PS order object.
     */
    public function install(): bool
    {
        if (!parent::install()) {
            return false;
        }

        if (!$this->runInstallSql()) {
            return false;
        }

        try {
            Install::exec(module: $this);
        } catch (Throwable $e) {
            Log::error(message: $e);
            return false;
        }

        // Make sure all caches are in sync right after install (no-op if already fresh)
        try {
            $this->ensureCachesAreFreshOnce();
        } catch (Throwable) {
        }

        return true;
    }

    /**
     * Light weight ModuleCore override hook for module enabling and cache needs to clear.
     *
     * @param bool $force_all
     * @return bool
     * @noinspection PhpArgumentWithoutNamedIdentifierInspection
     */
    public function enable($force_all = false): bool
    {
        $result = parent::enable($force_all);
        if ($result) {
            try {
                $this->ensureCachesAreFreshOnce();
            } catch (Throwable) {
            }
        }
        return $result;
    }

    /**
     * Run raw SQL install file manually.
     */
    private function runInstallSql(): bool
    {
        $file = __DIR__ . '/sql/install.sql';

        if (!file_exists($file)) {
            return false;
        }

        $sqlContent = file_get_contents($file);
        if ($sqlContent === false) {
            return false;
        }

        $sqlContent = str_replace('PREFIX_', _DB_PREFIX_, $sqlContent);
        $queries = array_filter(array_map('trim', explode(';', $sqlContent)));

        foreach ($queries as $query) {
            /** @noinspection PhpArgumentWithoutNamedIdentifierInspection */
            if (!Db::getInstance()->execute($query)) {
                return false;
            }
        }

        return true;
    }


    /**
     * Uninstall the module and remove related configuration.
     *
     * @return bool
     */
    public function uninstall(): bool
    {
        if (!parent::uninstall()) {
            return false;
        }

        // Remove all configuration entries related to Resurs Bank, but not the ones
        // related to order states. We need to keep those because they're required for
        // historical state tracking in the back office. If the module is reinstalled,
        // the order states will be re-used.
        try {
            Db::getInstance()->execute(
                "DELETE FROM " . _DB_PREFIX_ . "configuration
                WHERE name LIKE '%RESURSBANK%'
                AND name NOT LIKE '%PS_OS_RESURSBANK%'"
            );

            try {
                Configuration::deleteByName('RESURS_CACHED_FOR_VERSION');
            } catch (Throwable) {
            }

        } catch (Throwable $e) {
            Log::error($e);
            return false;
        }

        return true;
    }

    /**
     * Display the module configuration page.
     *
     * This is a native method which PrestaShop calls, we do not call it manually.
     *
     * @throws Exception
     */
    public function getContent()
    {
        try {
            $router = $this->get('router');
            $routeCollection = $router->getRouteCollection();

            if ($routeCollection->get('resursbank_admin_config')) {
                $route = $this->get('router')->generate('resursbank_admin_config');
                Tools::redirectAdmin($route);
            }
        } catch (RouteNotFoundException $e) {
            $this->ensureCachesAreFreshOnce();
            return $this->renderCacheHint();
        } catch (Throwable $e) {
            Log::error(message: $e);
            return '<div class="alert alert-danger">' . htmlentities(string: $e->getMessage()) . '</div>';
        }
    }

    /**
     * @return string
     */
    protected function renderCacheHint(): string
    {
        return '
        <div class="alert alert-warning">
            Module routing is not active yet.<br>
            Go to <strong>Advanced Parameters → Performance</strong> and click
            <em>Clear cache</em>, then reload this page.
        </div>';
    }

    /**
     * One-shot cache invalidation after install/upgrade.
     * Triggers only when a stored module version differs from the current $this->version.
     * @noinspection PhpArgumentWithoutNamedIdentifierInspection
     */
    protected function ensureCachesAreFreshOnce(): void
    {
        try {
            $storedVersion = (string)Configuration::get('RESURS_CACHED_FOR_VERSION');
        } catch (Throwable) {
            $storedVersion = '';
        }

        if ($storedVersion === $this->version) {
            return;
        }

        $this->clearPlatformCaches();
        Configuration::updateValue('RESURS_CACHED_FOR_VERSION', $this->version);
    }

    /**
     * Clear Symfony container/routes, Smarty, XML and media caches,
     * invalidate hook cache and reset PHP opcache when available.
     * @noinspection PhpArgumentWithoutNamedIdentifierInspection
     */
    protected function clearPlatformCaches(): void
    {
        // Prefer PrestaShop helpers when present; fall back to Symfony services otherwise.
        try {
            if (method_exists(Tools::class, 'clearSf2Cache')) {
                Tools::clearSf2Cache();
            } else {
                $container = SymfonyContainer::getInstance();
                /** @var CacheClearerInterface $clearer */
                $clearer = $container->get('cache_clearer');
                /** @var KernelInterface $kernel */
                $kernel = $container->get('kernel');
                $clearer->clear($kernel->getCacheDir());
            }
        } catch (Throwable $e) {
            // swallow; continue clearing the rest
        }

        try {
            if (method_exists(Tools::class, 'clearSmartyCache')) {
                Tools::clearSmartyCache();
            }
        } catch (Throwable $e) {
        }
        try {
            if (method_exists(Tools::class, 'clearXMLCache')) {
                Tools::clearXMLCache();
            }
        } catch (Throwable $e) {
        }
        try {
            if (class_exists(\Media::class)) {
                \Media::clearCache();
            }
        } catch (Throwable $e) {
        }
        try {
            if (class_exists(\Cache::class)) {
                \Cache::clean('hook_module_list');
            }
        } catch (Throwable $e) {
        }

        if (function_exists('opcache_reset')) {
            @opcache_reset();
        }
    }

    /**
     * Configure payment options.
     *
     * @return array
     */
    public function hookPaymentOptions(): array
    {
        return PaymentOptions::exec();
    }

    /**
     * Set up Ecom.
     * Listed to PrestaShop page request and configure Ecom.
     *
     * FRONTEND & BACKEND
     *
     * @param $params
     * @return void
     */
    public function hookActionDispatcher($params)
    {
        $this->ensureCachesAreFreshOnce();
        ActionDispatcher::exec();
    }

    /**
     * Hook executes when we reach the order confirmation page.
     *
     * @param $params
     * @return void
     */
    public function hookDisplayOrderConfirmation($params)
    {
        (new DisplayOrderConfirmation())->exec($params);
    }

    /**
     * Display Part payment widget on frontend.
     *
     * FRONTEND
     *
     * @param $params
     * @return string
     * @throws FakeContainerException
     */
    public function hookDisplayProductActions($params): string
    {
        return '<div class="rb-pp-widget-container" style="display:none;">' . DisplayPartPayment::exec(
                context: $this->context,
                product: $params['product'],
                path: $this->_path
            ) . '</div>';
    }

    /**
     * Add custom style to the checkout page.
     *
     * FRONTEND
     *
     * @return void
     * @throws FakeContainerException
     */
    public function hookDisplayHeader(): void
    {
        DisplayHeader::exec(
            context: $this->context,
            path: $this->_path
        );
    }

    /**
     * Display payment information in the admin order view.
     *
     * BACKEND
     *
     * @param array $parameters
     * @return string
     */
    public function hookDisplayAdminOrderMain(array $parameters): string
    {
        try {
            /** @var DisplayAdminOrderMain $service */
            $service = $this->get('resursbank.merchantapi.hook.display_admin_order_main');
            return $service->exec(parameters: $parameters);
        } catch (Throwable $error) {
            Log::error(message: $error);
        }

        return '';
    }

    /**
     * Add action buttons to the admin order view.
     *
     * BACKEND
     *
     * @param array $parameters
     * @return void
     */
    public function hookActionGetAdminOrderButtons(array $parameters): void
    {
        try {
            /** @var ActionGetAdminOrderButtons $service */
            $service = $this->get('resursbank.merchantapi.hook.action_get_admin_order_buttons');
            $service->exec($parameters);
        } catch (Throwable $error) {
            Log::error($error);
        }
    }

    /**
     * Append custom styling to admin.
     *
     * BACKEND
     *
     * @param array $params
     * @return string
     */
    public function hookDisplayBackOfficeHeader(array $params): string
    {
        if ($this->context->controller->php_self === 'AdminOrders') {
            $this->context->controller->addCss(
                $this->_path . 'views/css/admin/order-view.css'
            );
        }

        return '';
    }

    /**
     * Add address fetcher to the checkout page.
     *
     * FRONTEND
     *
     * @return string
     */
    public function hookDisplayAddressSelectorBottom(): string
    {
        return DisplayAddressSelectorBottom::exec();
    }

    /**
     * Handle new order slips (partial refunds).
     *
     * @param array $parameters
     * @return bool
     * @throws Exception
     */
    public function hookActionOrderSlipAdd(array $parameters): bool
    {
        $service = $this->get('resursbank.merchantapi.hook.action_order_slip_add');
        return $service->exec(parameters: $parameters);
    }

    /**
     * @param array $parameters
     * @return void
     * @throws Exception
     */
    public function hookActionOrderEdited(array $parameters): void
    {
        $service = $this->get(
            serviceName: 'resursbank.merchantapi.hook.action_order_edited'
        );
        $service->exec(parameters: $parameters);
    }

    /**
     * @param array $parameters
     * @return string
     * @throws FakeContainerException
     */
    public function hookDisplayBeforeBodyClosingTag(array $parameters): string
    {
        // Not adding both class an id here means we will get Uncaught TypeError in console:
        // Cannot read properties of null (reading 'querySelector')
        return '<div class="rb-pp-widget-container" id="rb-pp-widget-container" style="display:none;">' . DisplayFooter::exec(
                context: $this->context,
            ) . '</div>';
    }
}
