<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\BackOffice;

use Context;
use OrderState;
use Simplyin;
use SimplyIn\Api\ConnectionOutcome;
use SimplyIn\Form\DataProvider\SettingsDataProvider;
use SimplyIn\Form\Type\SimplyInSettingsType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;

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

/**
 * Renders the BO configuration screen through Symfony Form + Twig.
 *
 * Replaces the HelperForm-driven flow that used to live on ConfigurationFormBuilder.
 * The entry point stays `Simplyin::getContent()` — the module resolves
 * FormFactory/Twig from the PS container and hands them in.
 *
 * @phpstan-type SubmitResult array{confirmations: array<int, string>, errors: array<int, string>}
 */
final class ConfigurationController
{
    private const TEMPLATE = '@Modules/simplyin/views/templates/admin/configure.html.twig';
    private const SUBMIT_FIELD = 'submitSimplyinModule';

    public function __construct(
        private readonly Simplyin $module,
        private readonly ConfigurationRepository $config,
        private readonly SettingsDataProvider $dataProvider,
        private readonly FormFactoryInterface $formFactory,
        private readonly Environment $twig,
    ) {
    }

    /**
     * Processes the admin request (submit + render) in one pass.
     *
     * @return array{html: string, confirmations: array<int, string>, errors: array<int, string>}
     */
    public function process(?Request $request = null): array
    {
        $request ??= Request::createFromGlobals();
        $form = $this->buildForm();

        $confirmations = [];
        $errors = [];

        if ($request->isMethod('POST') && $request->request->has(self::SUBMIT_FIELD)) {
            $form->handleRequest($request);
            if ($form->isSubmitted() && $form->isValid()) {
                /** @var array<string, mixed> $data */
                $data = $form->getData();
                $result = $this->persistAndSync($data);
                $confirmations = $result['confirmations'];
                $errors = $result['errors'];
                $form = $this->buildForm();
            }
        }

        $html = $this->twig->render(self::TEMPLATE, [
            'form' => $form->createView(),
            'cron_link' => $this->cronLink(),
            // PasswordType blanks its own view value on every unsubmitted render, so the
            // stored key is injected at render time instead; the eye button reveals it.
            'secret_key_value' => $this->config->getString(ConfigurationKey::SECRET_KEY),
        ]);

        return ['html' => $html, 'confirmations' => $confirmations, 'errors' => $errors];
    }

    private function buildForm(): FormInterface
    {
        return $this->formFactory->create(
            SimplyInSettingsType::class,
            $this->dataProvider->getData(),
            [
                'carrier_choices' => $this->buildCarrierChoices(),
                'order_state_choices' => $this->buildOrderStateChoices(),
                'save_checkbox_forced' => $this->config->getBool(ConfigurationKey::CHECKBOX_FORCED_DISABLED),
            ],
        );
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return SubmitResult
     */
    private function persistAndSync(array $data): array
    {
        $confirmations = [];
        $errors = [];

        // An empty submit means "keep the stored key", not "clear it".
        $storedKey = $this->config->getString(ConfigurationKey::SECRET_KEY);
        $submittedKey = (string) ($data['secret_key'] ?? '');
        $effectiveKey = $submittedKey !== '' ? $submittedKey : $storedKey;

        // Validate BEFORE persisting, so a rejected key never overwrites a working one.
        $syncOutcome = $this->module->simplyInUpdateMerchantSettings($effectiveKey, true);
        $connected = $syncOutcome === ConnectionOutcome::SUCCESS;
        $this->config->set(ConfigurationKey::CONNECTED, $connected);

        // Never store a rejected key over the previously working one.
        $data['secret_key'] = $connected ? $effectiveKey : $storedKey;
        $this->dataProvider->saveData($data);

        if ($connected) {
            $confirmations[] = $this->module->l('Successfully connected to SimplyIN.');
            // Register-on-save: bring RSA request signing up immediately for the just-saved key
            // instead of waiting for the throttled back-office/cron backstop. Best-effort; it never
            // blocks saving the configuration, but a failure is now surfaced so the merchant isn't
            // left guessing why checkout request signing didn't activate.
            $rsaOutcome = $this->module->registerRsaForApiKey($effectiveKey);
            if ($rsaOutcome === ConnectionOutcome::ALREADY_REGISTERED) {
                $errors[] = $this->module->l('Simply.IN: An RSA key has already been generated for this store. To create a new one, revoke the current key using the "Revoke key" button in the <a href="https://merchant.simply.in" target="_blank" rel="noopener noreferrer">Simply.IN merchant panel</a>, then click "Save" again.');
            } elseif ($rsaOutcome === ConnectionOutcome::AUTH_REJECTED) {
                $errors[] = $this->module->l('Your API key appears to be invalid, please check it.');
            } elseif ($rsaOutcome === ConnectionOutcome::TECHNICAL_FAILURE) {
                $errors[] = $this->module->l('There was a problem completing setup, please click Save again to retry.');
            }
        } else {
            $errors[] = $syncOutcome === ConnectionOutcome::AUTH_REJECTED
                ? $this->module->l('Your API key appears to be invalid, please check it.')
                : $this->module->l('There was a problem completing setup, please click Save again to retry (re-enter your API key first if you just changed it, since it is not carried over automatically).');
            // Tell the merchant the previous key is still in effect, instead of silently keeping it.
            if ($storedKey !== '') {
                $errors[] = $this->module->l('The submitted API key was rejected — the previously saved key remains active.');
            }
        }

        $confirmations[] = $this->module->l('Configuration updated.');

        return ['confirmations' => $confirmations, 'errors' => $errors];
    }

    /**
     * @return array<string, int>
     */
    private function buildCarrierChoices(): array
    {
        $choices = ['--' => 0];
        foreach ($this->module->getAllAvailableShippingMethods() as $carrier) {
            if (!is_array($carrier) || !isset($carrier['id_carrier'], $carrier['name'])) {
                continue;
            }
            $choices[(string) $carrier['name']] = (int) $carrier['id_carrier'];
        }

        return $choices;
    }

    /**
     * @return array<string, int>
     */
    private function buildOrderStateChoices(): array
    {
        $idLang = (int) Context::getContext()->language->id;
        $choices = [];
        foreach (OrderState::getOrderStates($idLang) as $state) {
            if (!isset($state['id_order_state'], $state['name'])) {
                continue;
            }
            $choices[(string) $state['name']] = (int) $state['id_order_state'];
        }

        return $choices;
    }

    private function cronLink(): string
    {
        return Context::getContext()->link->getModuleLink(
            $this->module->name,
            'cron',
            ['secure_key' => $this->module->getCronSecureKey()],
            true,
        );
    }
}
