<?php
/**
 * 2026 Paysera
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * @author Paysera <plugins@paysera.com>
 * @copyright 2026 Paysera
 * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
 * International Registered Trademark & Property of Paysera
 */

declare(strict_types=1);

namespace Paysera\PrestashopPayments\Diagnostic\Service\Checker;

use Paysera\CheckoutSdk\SdkFacade;
use Paysera\PrestashopPayments\Diagnostic\Dto\CheckResult;
use Paysera\PrestashopPayments\Diagnostic\Dto\DiagnosticSection;
use Paysera\PrestashopPayments\Diagnostic\Service\PlatformInfoService;
use Paysera\PrestashopPayments\Provider\ModulePathsProvider;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;

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

class TranslationChecker implements CheckerInterface
{
    public const SECTION_KEY = 'translations';
    public const SECTION_LABEL = 'Translations information';

    private const XLIFF_FILENAME_FORMAT = 'ModulesPayserapaymentsGlobal.%s.xlf';
    private const BASE_TRANSLATIONS_FILE = 'messages.en.json';

    public function __construct(
        private SdkFacade $sdkFacade,
        private ClientInterface $httpClient,
        private RequestFactoryInterface $requestFactory,
        private ModulePathsProvider $modulePaths,
        private PlatformInfoService $platformInfo
    ) {
    }

    public function check(): DiagnosticSection
    {
        return new DiagnosticSection(self::SECTION_KEY, self::SECTION_LABEL, [
            $this->checkProxyConnectivity(),
            $this->checkFilesPresence(),
            $this->checkFilesCompleteness(),
        ]);
    }

    private function checkProxyConnectivity(): CheckResult
    {
        $name = 'Translation Proxy Connectivity';

        try {
            $url = $this->sdkFacade
                ->getTranslationsFacade()
                ->getTranslationsUrl(\PayseraPayments::PLUGIN_NAMESPACE);
        } catch (\Throwable $exception) {
            return CheckResult::fail($name, sprintf('Unable to resolve translation proxy URL: %s', $exception->getMessage()));
        }

        try {
            $request = $this->requestFactory->createRequest('GET', $url);
            $response = $this->httpClient->sendRequest($request);
        } catch (ClientExceptionInterface $exception) {
            return CheckResult::fail($name, sprintf('Translation proxy is not reachable at %s.', $url));
        }

        $statusCode = $response->getStatusCode();
        if ($statusCode < 200 || $statusCode >= 300) {
            return CheckResult::fail($name, sprintf('Translation proxy returned HTTP %d at %s.', $statusCode, $url));
        }

        return CheckResult::pass($name, sprintf('Translation proxy is reachable (HTTP %d).', $statusCode));
    }

    private function checkFilesPresence(): CheckResult
    {
        $name = 'Translation Files Presence';
        $locales = $this->platformInfo->getInstalledLocales();

        if ($locales === []) {
            return CheckResult::fail($name, 'No installed languages detected.');
        }

        $missing = [];
        foreach ($locales as $locale) {
            $path = $this->getXliffPath($locale);
            if (!is_file($path)) {
                $missing[] = basename($path);
            }
        }

        return $missing === []
            ? CheckResult::pass($name, sprintf('All translation files present for %d locale(s).', count($locales)))
            : CheckResult::fail($name, sprintf('Missing translation files: %s', implode(', ', $missing)));
    }

    private function checkFilesCompleteness(): CheckResult
    {
        $name = 'Translation Files Completeness';
        $requiredKeys = $this->loadBaseEnglishKeys();

        if ($requiredKeys === []) {
            return CheckResult::fail($name, sprintf('Base translation file %s is missing or empty.', self::BASE_TRANSLATIONS_FILE));
        }

        $locales = $this->platformInfo->getInstalledLocales();
        $descriptions = [];

        foreach ($locales as $locale) {
            $path = $this->getXliffPath($locale);
            $existingSources = $this->extractXliffSources($path);

            if ($existingSources === null) {
                $descriptions[] = sprintf('%s: file missing', $locale);
                continue;
            }

            $missing = array_values(array_filter(
                $requiredKeys,
                static fn (string $key): bool => !isset($existingSources[$key])
            ));

            if ($missing !== []) {
                $previewCount = 5;
                $preview = implode(', ', array_slice($missing, 0, $previewCount));
                $suffix = count($missing) > $previewCount
                    ? sprintf(' (+%d more)', count($missing) - $previewCount)
                    : '';
                $descriptions[] = sprintf('%s: %s%s', $locale, $preview, $suffix);
            }
        }

        return $descriptions === []
            ? CheckResult::pass($name, sprintf('All required translation sources present across %d locale(s).', count($locales)))
            : CheckResult::fail($name, sprintf('Missing required translation sources: %s', implode('; ', $descriptions)));
    }

    private function getXliffPath(string $locale): string
    {
        return sprintf('%s/%s/%s', rtrim($this->modulePaths->getTranslationsPath(), '/'), $locale, sprintf(self::XLIFF_FILENAME_FORMAT, $locale));
    }

    /**
     * @return list<string>
     */
    private function loadBaseEnglishKeys(): array
    {
        $path = sprintf('%s/%s', rtrim($this->modulePaths->getTranslationsPath(), '/'), self::BASE_TRANSLATIONS_FILE);

        if (!is_file($path)) {
            return [];
        }

        $contents = (string) file_get_contents($path);
        $decoded = json_decode($contents, true);

        if (!is_array($decoded)) {
            return [];
        }

        $keys = [];
        foreach ($decoded as $value) {
            if (is_string($value) && $value !== '') {
                $keys[] = $value;
            }
        }

        return $keys;
    }

    /**
     * @return array<string, bool>|null Null when file missing or unparseable
     */
    private function extractXliffSources(string $path): ?array
    {
        if (!is_file($path)) {
            return null;
        }

        $contents = (string) file_get_contents($path);
        if ($contents === '') {
            return null;
        }

        $previousUseInternalErrors = libxml_use_internal_errors(true);

        try {
            $document = new \SimpleXMLElement($contents);
        } catch (\Throwable $exception) {
            libxml_use_internal_errors($previousUseInternalErrors);

            return null;
        }

        libxml_use_internal_errors($previousUseInternalErrors);

        $sources = [];
        $elements = $document->xpath('//*[local-name()="source"]');
        if ($elements === null) {
            return [];
        }

        foreach ($elements as $element) {
            $value = (string) $element;
            if ($value !== '') {
                $sources[$value] = true;
            }
        }

        return $sources;
    }
}
