<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Core\Localization\Loader;

use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
use TYPO3\CMS\Core\Localization\Exception\InvalidXmlFileException;

/**
 * TYPO3-specific XLIFF file loader that implements Symfony's LoaderInterface
 * while maintaining TYPO3's specific features like approval states and plurals.
 *
 * This loader only converts ONE SINGLE FILE. It is not responsible for detecting the file location of
 * translation! This is done - currently - by the LocalizationFactory and should be moved to a dedicated class.
 *
 * This loader expects that the source is always english, and the translation is something else.
 *
 * @internal This class is not part of the TYPO3 Core API.
 */
final readonly class XliffLoader implements LoaderInterface
{
    public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
    {
        if (!is_string($resource)) {
            throw new InvalidXmlFileException('XliffLoader only accepts string types (file names, XML content).', 1757588428);
        }

        if (!$this->isXmlString($resource)) {
            if (!file_exists($resource)) {
                throw new InvalidXmlFileException('File "' . $resource . '" not found.', 1757588429);
            }

            if (!is_file($resource)) {
                throw new InvalidResourceException('The given resource is neither a file nor an XLIFF string "' . $resource . '".', 1757588430);
            }
        }

        $rootXmlNode = $this->convertResourceToXml($resource);
        return $this->parseXliffFromRoot($rootXmlNode, $locale, $domain);
    }

    /**
     * Parse XLIFF file and return TYPO3-compatible data structure
     */
    private function convertResourceToXml(string $resource): \SimpleXMLElement
    {
        if ($this->isXmlString($resource)) {
            $xmlContent = $resource;
        } else {
            $xmlContent = @file_get_contents($resource);
            if ($xmlContent === false) {
                throw new InvalidXmlFileException('The path provided does not point to an existing and accessible file.', 1757537341);
            }
        }
        $rootXmlNode = @simplexml_load_string($xmlContent, \SimpleXMLElement::class, LIBXML_NOWARNING);
        if ($rootXmlNode === false) {
            $xmlError = libxml_get_last_error();
            throw new InvalidXmlFileException(
                'The path provided does not point to an existing and accessible well-formed XML file. Reason: ' . $xmlError->message . ' in ' . $resource . ', line ' . $xmlError->line,
                1757537342
            );
        }
        return $rootXmlNode;
    }

    /**
     * Parse XLIFF content from root element and build a catalogue
     */
    private function parseXliffFromRoot(\SimpleXMLElement $root, string $locale, string $domain): MessageCatalogue
    {
        $catalogue = new MessageCatalogue($locale);
        $version = $this->getXliffVersion($root);

        if ($version === '2.0') {
            $this->parseXliff2($root, $catalogue, $domain);
        } else {
            // Default to XLIFF 1.2 parsing
            $this->parseXliff1($root, $catalogue, $domain);
        }

        return $catalogue;
    }

    /**
     * Detect XLIFF version from the root element
     */
    private function getXliffVersion(\SimpleXMLElement $root): string
    {
        $namespaces = $root->getNamespaces(true);

        // Check if XLIFF 2.x namespace is present (matches 2.0, 2.1, 2.2, etc.)
        foreach ($namespaces as $namespace) {
            if (str_starts_with($namespace, 'urn:oasis:names:tc:xliff:document:2.')) {
                return '2.0';
            }
        }

        // Check version attribute
        $version = (string)$root['version'];
        if (str_starts_with($version, '2.')) {
            return '2.0';
        }

        // Default to 1.2
        return '1.2';
    }

    /**
     * Parse XLIFF 1.2 format
     */
    private function parseXliff1(\SimpleXMLElement $root, MessageCatalogue $catalogue, string $domain): void
    {
        $fileTag = $root->file;
        $isDefaultLanguage = !isset($fileTag['target-language']); // Default language from XLIFF template (no target element)
        $bodyOfFileTag = $root->file->body;
        $requireApprovedLocalizations = (bool)($GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations'] ?? true);

        if ($bodyOfFileTag instanceof \SimpleXMLElement) {
            foreach ($bodyOfFileTag->children() as $translationElement) {
                $deprecated = false;
                /** @var \SimpleXMLElement $translationElement */
                if ($translationElement->getName() === 'trans-unit' && !isset($translationElement['restype'])) {
                    // Regular translation unit
                    $id = (string)$translationElement['id'];
                    $deprecated = isset($translationElement['x-unused-since']);
                    if ($isDefaultLanguage) {
                        // Default language from XLIFF template (no target element)
                        $sourceElement = $translationElement->source;
                        $translation = $this->extractText($sourceElement, $translationElement);
                        $catalogue->set($id . ($deprecated ? '.x-unused' : ''), $translation, $domain);
                    } else {
                        $approved = (string)($translationElement['approved'] ?? 'yes');
                        if (!$requireApprovedLocalizations || $approved === 'yes') {
                            $catalogue->set($id . ($deprecated ? '.x-unused' : ''), $this->extractText($translationElement->target, $translationElement), $domain);
                        }
                    }
                } elseif ($translationElement->getName() === 'group' && isset($translationElement['restype']) && (string)$translationElement['restype'] === 'x-gettext-plurals') {
                    // Translation with plural forms
                    $parsedTranslationElement = [];
                    foreach ($translationElement->children() as $translationPluralForm) {
                        /** @var \SimpleXMLElement $translationPluralForm */
                        if ($translationPluralForm->getName() === 'trans-unit') {
                            $deprecated = isset($translationPluralForm['x-unused-since']);
                            // Extract plural form index from ID like "1[0]", "1[1]"
                            $formIndex = substr((string)$translationPluralForm['id'], strpos((string)$translationPluralForm['id'], '[') + 1, -1);
                            if ($isDefaultLanguage) {
                                // Default language from XLIFF template (no target element)
                                $parsedTranslationElement[(int)$formIndex] = $this->extractText($translationPluralForm->source, $translationPluralForm);
                            } else {
                                $approved = (string)($translationPluralForm['approved'] ?? 'yes');
                                if (!$requireApprovedLocalizations || $approved === 'yes') {
                                    $parsedTranslationElement[(int)$formIndex] = $this->extractText($translationPluralForm->target, $translationPluralForm);
                                }
                            }
                        }
                    }
                    if ($parsedTranslationElement !== []) {
                        if (isset($translationElement['id'])) {
                            $id = (string)$translationElement['id'];
                        } else {
                            $id = (string)$translationElement->{'trans-unit'}[0]['id'];
                            $id = substr($id, 0, (int)strpos($id, '['));
                        }
                        // Handle plurals - Symfony uses ICU format
                        $catalogue->set($id . ($deprecated ? '.x-unused' : ''), $this->convertToIcuPlural(array_values($parsedTranslationElement)), $domain);
                    }
                }
            }
        }
    }

    /**
     * Parse XLIFF 2.0 format
     */
    private function parseXliff2(\SimpleXMLElement $root, MessageCatalogue $catalogue, string $domain): void
    {
        $requireApprovedLocalizations = (bool)($GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations'] ?? true);

        $ns = $root->getDocNamespaces();
        $ns = reset($ns) ?: 'urn:oasis:names:tc:xliff:document:2.0';
        // Register the XLIFF 2.0 namespace
        $root->registerXPathNamespace('xliff', $ns);
        $isDefaultLanguage = !isset($root['trgLang']); // Default language from XLIFF template (no target element)

        // Get all file elements
        $files = $root->xpath('//xliff:file');
        if ($files === false) {
            return;
        }

        foreach ($files as $file) {
            $file->registerXPathNamespace('xliff', $ns);

            // Get all unit elements within this file
            $units = $file->xpath('.//xliff:unit');
            if ($units === false) {
                continue;
            }

            foreach ($units as $unit) {
                $unit->registerXPathNamespace('xliff', $ns);
                $unitId = (string)$unit['id'];

                // Check if this is a plural unit (contains multiple segments)
                $segments = $unit->xpath('.//xliff:segment');
                if ($segments === false) {
                    continue;
                }

                if (count($segments) === 1) {
                    // Regular translation unit
                    $segment = $segments[0];
                    $segment->registerXPathNamespace('xliff', $ns);

                    $source = $segment->xpath('.//xliff:source');
                    $target = $segment->xpath('.//xliff:target');

                    $deprecated = ((string)($segment['subState'] ?? '')) === 'deprecated';

                    if ($isDefaultLanguage) {
                        // Default language from XLIFF template (no target element)
                        $translation = $this->extractText($source[0], $segment);
                        $catalogue->set($unitId . ($deprecated ? '.x-unused' : ''), $translation, $domain);
                    } else {
                        // Check approval state (XLIFF 2.0 uses 'state' attribute on segment)
                        $approved = 'yes';
                        if (isset($segment['state'])) {
                            $state = (string)$segment['state'];
                            // XLIFF 2.0 states: initial, translated, reviewed, final
                            // We consider 'final' as approved, others depend on config
                            if ($state === 'initial' || $state === 'translated') {
                                $approved = 'no';
                            }
                        }

                        if (!$requireApprovedLocalizations || $approved === 'yes') {
                            if ($target !== false && isset($target[0])) {
                                $catalogue->set($unitId . ($deprecated ? '.x-unused' : ''), $this->extractText($target[0], $segment), $domain);
                            }
                        }
                    }
                } else {
                    // Plural forms (multiple segments)
                    $parsedTranslationElement = [];
                    $formIndex = 0;
                    $deprecated = false;

                    foreach ($segments as $segment) {
                        $segment->registerXPathNamespace('xliff', $ns);

                        $source = $segment->xpath('.//xliff:source');
                        $target = $segment->xpath('.//xliff:target');
                        $deprecated = $deprecated || ((string)($segment['subState'] ?? '')) === 'deprecated';

                        if ($isDefaultLanguage) {
                            // Default language from XLIFF template (no target element)
                            $parsedTranslationElement[$formIndex] = $this->extractText($source[0], $segment);
                        } else {
                            $approved = 'yes';
                            if (isset($segment['state'])) {
                                $state = (string)$segment['state'];
                                if ($state === 'initial' || $state === 'translated') {
                                    $approved = 'no';
                                }
                            }

                            if (!$requireApprovedLocalizations || $approved === 'yes') {
                                if ($target !== false && isset($target[0])) {
                                    $parsedTranslationElement[$formIndex] = $this->extractText($target[0], $segment);
                                }
                            }
                        }
                        $formIndex++;
                    }

                    if ($parsedTranslationElement !== []) {
                        $catalogue->set($unitId . ($deprecated ? '.x-unused' : ''), $this->convertToIcuPlural(array_values($parsedTranslationElement)), $domain);
                    }
                }
            }
        }
    }

    /**
     * Convert plural forms to ICU plural format
     * This is a simplified conversion - could be enhanced based on actual usage
     */
    private function convertToIcuPlural(array $pluralValues): string
    {
        if (count($pluralValues) === 1) {
            return $pluralValues[0];
        }

        // Simple mapping: [0] = one, [1] = other
        $icuFormat = '';
        if (isset($pluralValues[0])) {
            $icuFormat .= '{0, plural, one {' . $pluralValues[0] . '}';
        }
        if (isset($pluralValues[1])) {
            $icuFormat .= ' other {' . $pluralValues[1] . '}';
        }
        $icuFormat .= '}';

        return $icuFormat;
    }

    private function isXmlString(string $resource): bool
    {
        return str_starts_with($resource, '<?xml');
    }

    /**
     * Extract text content from an XML element, respecting xml:space attribute.
     * Per XML spec: xml:space="preserve" keeps whitespace as-is,
     * otherwise whitespace should be normalized (multiple spaces/newlines collapsed to single space).
     * see https://www.w3.org/TR/xml/#sec-white-space
     */
    private function extractText(\SimpleXMLElement $element, ?\SimpleXMLElement $parentElement = null): string
    {
        $text = (string)$element;

        // Check xml:space on the element itself or parent (trans-unit/unit/segment)
        $xmlSpace = $this->getXmlSpaceAttribute($element, $parentElement);

        if ($xmlSpace !== 'preserve') {
            // Normalize whitespace: collapse multiple whitespace characters to single space
            $text = preg_replace('/\s+/', ' ', $text);
            $text = trim($text);
        }

        return $text;
    }

    /**
     * Get the xml:space attribute value, checking element and parent (attribute is inherited per XML spec).
     */
    private function getXmlSpaceAttribute(\SimpleXMLElement $element, ?\SimpleXMLElement $parent): string
    {
        // Check element's xml:space attribute
        $attributes = $element->attributes('xml', true);
        if (isset($attributes['space'])) {
            return (string)$attributes['space'];
        }

        // Check parent's xml:space attribute (inherited per XML spec)
        if ($parent !== null) {
            $parentAttributes = $parent->attributes('xml', true);
            if (isset($parentAttributes['space'])) {
                return (string)$parentAttributes['space'];
            }
        }

        return 'default';
    }
}
