<?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\Site\Set;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlParseException;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Settings\CategoryDefinition;
use TYPO3\CMS\Core\Settings\InvalidSettingDefinitionException;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinitionValidation;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * @internal
 */
#[Autoconfigure(public: true)]
class YamlSetDefinitionProvider
{
    /** @var array<string, SetDefinition> */
    protected array $sets = [];
    public function __construct(
        protected readonly SettingDefinitionValidation $settingDefinitionValidation,
        protected readonly YamlFileLoader $yamlFileLoader,
    ) {}

    /**
     * @return array<string, SetDefinition>
     */
    public function getSetDefinitions(): array
    {
        return $this->sets;
    }

    public function addSet(SetDefinition $set): void
    {
        $this->sets[$set->name] = $set;
    }

    public function get(\SplFileInfo $fileInfo, ?string $virtualSetPath = null): SetDefinition
    {
        $filename = GeneralUtility::fixWindowsFilePath($fileInfo->getPathname());
        $path = dirname($filename);
        $virtualSetPath ??= $path . '/';
        // No placeholders or imports processed on purpose
        // Use dependencies for shared sets
        try {
            $set = Yaml::parseFile($filename);
        } catch (ParseException $e) {
            $source = $virtualSetPath . basename($filename);
            throw new InvalidSetException('Failed to parse set definition from "' . $source . '": ' . $e->getMessage(), 1711024370, $e);
        }
        $setName = $set['name'] ?? '';

        $settingsDefinitionsFile = $path . '/settings.definitions.yaml';
        if (is_file($settingsDefinitionsFile)) {
            try {
                $settingsDefinitions = Yaml::parseFile($settingsDefinitionsFile, Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP);
            } catch (ParseException $e) {
                $source = $virtualSetPath . basename($settingsDefinitionsFile);
                throw new InvalidSettingsDefinitionsException(
                    'Invalid settings definition. Source: ' . $source,
                    1711024374,
                    $e,
                    $setName
                );
            }
            if (!is_object($settingsDefinitions->settings ?? null)) {
                $source = $virtualSetPath . basename($settingsDefinitionsFile);
                throw new InvalidSettingsDefinitionsException(
                    'Missing "settings" key in settings definitions. Source: ' . $source,
                    1711024378,
                    null,
                    $setName
                );
            }
            // YAML maps are decoded as objects. Normalize them to arrays so
            // settings/category definitions can be handled uniformly below.
            $set['settingsDefinitions'] = array_map(
                static fn(?object $value): ?array => $value === null ? null : (array)$value,
                get_object_vars($settingsDefinitions->settings)
            );
            $set['categoryDefinitions'] = [];
            if (isset($settingsDefinitions->categories)) {
                $set['categoryDefinitions'] = array_map(
                    static fn(?object $value): ?array => $value === null ? null : (array)$value,
                    get_object_vars($settingsDefinitions->categories)
                );
            }
        }

        $settingsFile = $path . '/settings.yaml';
        if (is_file($settingsFile)) {
            try {
                // HEADS UP: YamlFileLoader::PROCESS_PLACEHOLDERS is omitted on purpose and MUST NOT be added.
                // Site sets are intended to be self-contained and must not rely on implicit
                // dependencies to global (environment) variables.
                $settings = $this->yamlFileLoader->load($settingsFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE);
            } catch (YamlParseException $e) {
                $source = $virtualSetPath . basename($settingsFile);
                throw new InvalidSettingsException('Invalid settings format. Source: ' . $source, 1711024380, $e, $setName);
            }
            $set['settings'] = $settings;
        }

        $routeEnhancersFile = $path . '/route-enhancers.yaml';
        if (is_file($routeEnhancersFile)) {
            try {
                $routeEnhancers = $this->yamlFileLoader->load($routeEnhancersFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE);
            } catch (YamlParseException $e) {
                $source = $virtualSetPath . basename($routeEnhancersFile);
                throw new InvalidSetRouteEnhancersException(
                    'Invalid route enhancers format. Source: ' . $source,
                    1764749081,
                    $e,
                    $setName
                );
            }
            if ($routeEnhancers !== [] && !is_array($routeEnhancers['routeEnhancers'] ?? null)) {
                $source = $virtualSetPath . basename($routeEnhancersFile);
                throw new InvalidSetRouteEnhancersException(
                    'Missing "routeEnhancers" key in route enhancers file. Source: ' . $source,
                    1764749082,
                    null,
                    $setName
                );
            }
            if ($routeEnhancers !== [] && array_keys($routeEnhancers) !== ['routeEnhancers']) {
                $source = $virtualSetPath . basename($routeEnhancersFile);
                throw new InvalidSetRouteEnhancersException(
                    'Superfluous keys in route enhancers file. Use "routeEnhancers" as root level key. Source: ' . $source,
                    1764749083,
                    null,
                    $setName
                );
            }
            $set['routeEnhancers'] = $routeEnhancers['routeEnhancers'] ?? [];
        }

        if (($set['labels'] ?? '') === '') {
            if (is_file($path . '/labels.xlf')) {
                $set['labels'] = $virtualSetPath . 'labels.xlf';
            }
        }

        return $this->createDefinition($set, $virtualSetPath);
    }

    protected function createDefinition(array $set, string $basePath): SetDefinition
    {
        $settingsDefinitions = [];
        $labels = $set['labels'] ?? null;
        unset($set['labels']);

        if ($labels) {
            $set['label'] ??= 'LLL:' . $labels . ':label';
        }

        foreach (($set['settingsDefinitions'] ?? []) as $setting => $options) {
            // Cast objects to arrays
            if (is_object($options['options'] ?? null)) {
                $options['options'] = (array)$options['options'];
            }

            if (is_array($options['enum'] ?? null)) {
                $options['enum'] = array_combine(
                    $options['enum'],
                    array_map(
                        static fn(string|int|float|bool $value): string => sprintf(
                            '{label}:settings.%s.enum.%s',
                            $setting,
                            is_bool($value) ? ($value ? 'true' : 'false') : (string)$value
                        ),
                        $options['enum']
                    )
                );
            } elseif (is_object($options['enum'] ?? null)) {
                $options['enum'] = (array)$options['enum'];
            }
            if (is_array($options['enum'] ?? null)) {
                foreach ($options['enum'] as $enumValue => $enumLabel) {
                    if ($enumLabel === null) {
                        $options['enum'][$enumValue] = (string)$enumValue;
                    }
                }
            }

            if (is_object($options['tags'] ?? null)) {
                $options['tags'] = array_values((array)$options['tags']);
            }
            if ($labels) {
                $domain = 'LLL:' . $labels . ':';
                $options['label'] ??= $domain . 'settings.' . $setting;
                $options['description'] ??= $domain . 'settings.description.' . $setting;
                if (is_array($options['enum'] ?? null)) {
                    foreach ($options['enum'] as $enumValue => $enumLabel) {
                        if (is_string($enumLabel) && str_starts_with($enumLabel, '{label}:')) {
                            $options['enum'][$enumValue] = $domain . substr($enumLabel, 8);
                        }
                    }
                }
            }
            $settingDefinitionData = [...['key' => $setting], ...$options];
            try {
                $definition = new SettingDefinition(...$settingDefinitionData);
            } catch (\Error $e) {
                throw new InvalidSettingsDefinitionsException(
                    'Invalid setting definition "' . $setting . '": ' . json_encode($options) . ' – ' . $this->getObjectConstructionErrors($e, SettingDefinition::class, $settingDefinitionData),
                    1702623312,
                    $e,
                    $set['name'] ?? ''
                );
            }
            try {
                $this->settingDefinitionValidation->validate($definition);
            } catch (InvalidSettingDefinitionException $e) {
                throw new InvalidSettingsDefinitionsException(
                    $e->getMessage(),
                    1752483401,
                    $e,
                    $set['name'] ?? ''
                );
            }
            $settingsDefinitions[] = $definition;
        }

        $categoryDefinitions = [];
        foreach (($set['categoryDefinitions'] ?? []) as $category => $options) {
            if ($labels) {
                $options['label'] ??= 'LLL:' . $labels . ':categories.' . $category;
                $options['description'] ??= 'LLL:' . $labels . ':categories.description.' . $category;
            }
            try {
                $definition = new CategoryDefinition(...[...['key' => $category], ...$options]);
            } catch (\Error $e) {
                throw new InvalidCategoryDefinitionsException(
                    'Invalid category definition "' . $category . '": ' . json_encode($options),
                    1702623313,
                    $e,
                    $set['name'] ?? ''
                );
            }
            $categoryDefinitions[] = $definition;
        }

        foreach (($set['routeEnhancers'] ?? []) as $identifier => $config) {
            if (!is_array($config)) {
                throw new InvalidSetRouteEnhancersException(
                    sprintf('Invalid route enhancer definition "%s": expected array, got %s', $identifier, gettype($config)),
                    1732800002,
                    null,
                    $set['name'] ?? ''
                );
            }
        }

        $setData = [
            ...$set,
            'settingsDefinitions' => $settingsDefinitions,
            'categoryDefinitions' => $categoryDefinitions,
        ];
        $setData['typoscript'] ??= $basePath;
        $setData['pagets'] ??= $basePath . 'page.tsconfig';
        try {
            return new SetDefinition(...$setData);
        } catch (\Error $e) {
            throw new InvalidSetException(
                'Invalid set definition: ' . json_encode($set) . ' – ' . $this->getObjectConstructionErrors($e, SetDefinition::class, $setData),
                1170859526,
                $e,
                $set['name'] ?? ''
            );
        }
    }

    protected function getObjectConstructionErrors(
        \Error $error,
        string $className,
        array $arguments,
    ): string {
        $reflection = new \ReflectionClass($className);
        $constructor = $reflection->getConstructor();
        $parameters = $constructor->getParameters();
        $missingParameters = [];
        $typeErrors = [];
        foreach ($parameters as $parameter) {
            if (isset($arguments[$parameter->name])) {
                $value = $arguments[$parameter->name];
                unset($arguments[$parameter->name]);
                $type = $parameter->getType();
                if (!$this->typeMatches($type, $value)) {
                    $typeErrors[$parameter->name] = (string)$type;
                }
            } elseif (!$parameter->isDefaultValueAvailable()) {
                $missingParameters[] = $parameter->name;
            }
        }

        $errors = [];
        if ($missingParameters !== []) {
            $errors[] = 'Missing properties: ' . implode(', ', $missingParameters);
        }
        if ($arguments !== []) {
            $errors[] = 'Invalid properties: ' . implode(', ', array_keys($arguments));
        }
        if ($typeErrors !== []) {
            $errors[] = 'Invalid type: ' . implode(', ', array_keys($typeErrors));
        }

        if ($errors === []) {
            return $error->getMessage();
        }

        return implode('; ', $errors);
    }

    protected function typeMatches(
        \ReflectionType $type,
        mixed $value
    ): bool {
        if ($type->allowsNull() && $value === null) {
            return true;
        }

        if ($type instanceof \ReflectionUnionType) {
            foreach ($type->getTypes() as $t) {
                if ($this->typeMatches($t, $value)) {
                    return true;
                }
            }
            return false;
        }

        if ($type instanceof \ReflectionIntersectionType) {
            foreach ($type->getTypes() as $t) {
                if (!$this->typeMatches($t, $value)) {
                    return false;
                }
            }
            return true;
        }

        if ($type instanceof \ReflectionNamedType) {
            $typeName = $type->getName();
            $valueType = gettype($value);
            if ($valueType === 'object') {
                return is_subclass_of($value, $typeName);
            }
            return $valueType === $typeName;
        }

        return true;
    }
}
