<?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\Configuration\FlexForm;

use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureIdentifierInitializedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeFlexFormDataStructureParsedEvent;
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidDataStructureException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaSchemaException;
use TYPO3\CMS\Core\Configuration\Tca\TcaMigration;
use TYPO3\CMS\Core\Configuration\Tca\TcaPreparation;
use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Unified service class for TCA type="flex" operations.
 *
 * This service provides comprehensive FlexForm handling capabilities that work with both:
 * - TCA Schema objects (high-level, schema-aware operations)
 * - Raw TCA configuration arrays (low-level operations during schema building)
 *
 * Note: Using the raw TCA configuration is not recommended and only available to support
 * FlexFormTools during schema building. For extensions this might be the case on using
 * {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
 *
 * Usage examples:
 * ```php
 * // With TCA Schema (typical application usage)
 * $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $tcaSchema);
 *
 * // With raw TCA array (only during schema)
 * $flexFormTools->getDataStructureIdentifier($fieldTca, $table, $field, $row, $rawTcaArray);
 * ```
 *
 * The service automatically detects the input type and uses the appropriate resolution strategy.
 */
#[Autoconfigure(public: true)]
readonly class FlexFormTools
{
    public function __construct(
        private EventDispatcherInterface $eventDispatcher,
        private TcaMigration $tcaMigration,
        private TcaPreparation $tcaPreparation,
    ) {}

    /**
     * The method locates a specific data structure from given TCA and row combination
     * and returns an identifier string that can be handed around, and can be resolved
     * to a single data structure later without giving $row and $tca data again.
     *
     * Note: The returned syntax is meant to only specify the target location of the data structure.
     * It SHOULD NOT be abused and enriched with data from the record that is dealt with. For
     * instance, it is not allowed to add source record specific date like the "uid" or the "pid"!
     * If that is done, it is up to the hook consumer to take care of possible side effects, e.g. if
     * the DataHandler copies or moves records around and those references change.
     *
     * This method gets: Source data that influences the target location of a data structure
     * This method returns: Target specification of the data structure
     *
     * This method is "paired" with method parseDataStructureByIdentifier() that
     * will resolve the returned syntax again and returns the data structure itself.
     *
     * Both methods can be extended via events to return and accept additional
     * identifier strings if needed, and to transmit further information within the identifier strings.
     *
     * Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
     * as the "ds" config is a string, containing the data structure or a file pointer.
     *
     * Note: This method and the resolving methods below are well unit tested and document all
     * nasty details this way.
     *
     * @param array $fieldTca Full TCA of the field in question that has type=flex set
     * @param string $tableName The table name of the TCA field
     * @param string $fieldName The field name
     * @param array $row The data row
     * @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
     *                                     case handling is done via events. Otherwise, this will throw an exception
     *                                     on resolving the default identifier {@see InvalidTcaSchemaException}.
     *                                     Using the raw TCA configuration is furthermore not recommended and only
     *                                     available to support FlexFormTools during schema building. For extensions
     *                                     this might be the case on using {@see BeforeTcaOverridesEvent} or
     *                                     {@see AfterTcaCompilationEvent}.
     *
     * @return string Identifier JSON string
     * @throws \RuntimeException If TCA is misconfigured
     * @throws InvalidTcaException
     */
    public function getDataStructureIdentifier(array $fieldTca, string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): string
    {
        $dataStructureIdentifier = $this->eventDispatcher
            ->dispatch(new BeforeFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row))
            ->getIdentifier() ?? $this->getDefaultDataStructureIdentifier($tableName, $fieldName, $row, $schema);
        $dataStructureIdentifier = $this->eventDispatcher
            ->dispatch(new AfterFlexFormDataStructureIdentifierInitializedEvent($fieldTca, $tableName, $fieldName, $row, $dataStructureIdentifier))
            ->getIdentifier();
        return json_encode($dataStructureIdentifier, JSON_THROW_ON_ERROR);
    }

    /**
     * Parse a data structure identified by $identifier to the final data structure array.
     * This method is called after getDataStructureIdentifier(), finds the data structure
     * and returns it.
     *
     * Events allow to manipulate the find logic and to post process the data structure array.
     *
     * Important: The TCA for data structure definitions MUST be overridden by 'columnsOverrides'
     * as the "ds" config is a string, containing the data structure or a file pointer.
     *
     * After the data structure definition is found, the method resolves:
     * - FILE:EXT: prefix of the data structure itself - the ds is in a file
     * - FILE:EXT: prefix for sheets - if single sheets are in files
     * - Create a sDEF sheet if the data structure has non, yet.
     * - TCA Migration and Preparation is done for the resolved fields
     *
     * After that method is run, the data structure is fully resolved to an array,
     * and same base normalization is done: If the ds did not contain a sheet,
     * it will have one afterward as "sDEF".
     *
     * This method gets: Target specification of the data structure.
     * This method returns: The normalized data structure parsed to an array.
     *
     * @param string $identifier JSON string to find the data structure location
     * @param array|TcaSchema|null $schema Either be the Tca Schema object or raw TCA configuration. Only omit in
     *                                     case handling is done via events. Otherwise, this will throw an exception
     *                                     on resolving the default identifier {@see InvalidTcaSchemaException}.
     *                                     Using the raw TCA configuration is furthermore not recommended and only
     *                                     available to support FlexFormTools during schema building. For extensions
     *                                     this might be the case on using {@see BeforeTcaOverridesEvent} or
     *                                     {@see AfterTcaCompilationEvent}.
     *
     * @return array Parsed and normalized data structure
     * @throws InvalidIdentifierException
     * @throws InvalidTcaSchemaException
     * @throws InvalidDataStructureException
     */
    public function parseDataStructureByIdentifier(string $identifier, array|TcaSchema|null $schema = null): array
    {
        // Throw an exception for an empty string. This might be a valid use case for new
        // records in some situations, so this is catchable to give callers a chance to deal with that.
        if ($identifier === '') {
            throw new InvalidIdentifierException(
                'Empty string given to parseDataStructureByIdentifier(). This exception might '
                . ' be caught to handle some new record situations properly',
                1478100828
            );
        }
        $parsedIdentifier = json_decode($identifier, true);
        if (!is_array($parsedIdentifier) || $parsedIdentifier === []) {
            // If there is some identifier and it can't be decoded, programming error -> not catchable
            throw new \RuntimeException(
                'Identifier could not be decoded to an array.',
                1478345642
            );
        }
        $dataStructure = $this->eventDispatcher
            ->dispatch(new BeforeFlexFormDataStructureParsedEvent($parsedIdentifier))
            ->getDataStructure() ?? $this->getDefaultStructureForIdentifier($parsedIdentifier, $schema);
        $dataStructure = $this->convertDataStructureToArray($dataStructure);
        $dataStructure = $this->ensureDefaultSheet($dataStructure);
        $dataStructure = $this->resolveFileDirectives($dataStructure);
        $dataStructure = $this->checkMigratePrepareFlexTca($dataStructure);
        return $this->eventDispatcher
            ->dispatch(new AfterFlexFormDataStructureParsedEvent($dataStructure, $parsedIdentifier))
            ->getDataStructure();
    }

    /**
     * Clean up FlexForm value XML to hold only the values it may according to its Data Structure.
     * The order of tags will follow that of the data structure.
     *
     * @param array|TcaSchema $schema Main schema only, no sub schema! Using the raw TCA configuration is
     *                                furthermore not recommended and only available to support FlexFormTools
     *                                during schema building. For extensions this might be the case on
     *                                using {@see BeforeTcaOverridesEvent} or {@see AfterTcaCompilationEvent}.
     *
     * @internal Signature may change, for instance to split 'DS finding' and flexArray2Xml(),
     *           which would allow broader use of the method. It is currently consumed by
     *           cleanup:flexforms CLI only.
     */
    public function cleanFlexFormXML(string $table, string $field, array $row, array|TcaSchema $schema): string
    {
        if ((is_array($schema) && !isset($schema['columns'][$field]['config'])) || ($schema instanceof TcaSchema && !$schema->hasField($field)) || !isset($row[$field])) {
            throw new \RuntimeException('Can not clean up FlexForm XML for a column not declared in TCA or not in record.', 1697554398);
        }
        try {
            $fieldTca = is_array($schema) ? ['config' => $schema['columns'][$field]['config']] : ['config' => $schema->getField($field)->getConfiguration()];
            $dataStructureArray = $this->parseDataStructureByIdentifier($this->getDataStructureIdentifier($fieldTca, $table, $field, $row, $schema), $schema);
        } catch (InvalidIdentifierException) {
            // Data structure can not be resolved or parsed. Reset value to empty string.
            return '';
        }
        $valueArray = GeneralUtility::xml2array($row[$field]);
        if (!is_array($valueArray)) {
            // Current flex form values can not be parsed to an array. The entire thing is invalid. Reset to empty string.
            return '';
        }
        if (!is_array($dataStructureArray['sheets'] ?? false)) {
            // We might return empty string instead of throwing here, unsure.
            throw new \RuntimeException('Data structure should always declare at least one sheet', 1697555523);
        }
        $newValueArray = [];
        foreach ($dataStructureArray['sheets'] as $sheetKey => $sheetData) {
            foreach (($sheetData['ROOT']['el'] ?? []) as $sheetElementKey => $sheetElementData) {
                // For all elements allowed in Data Structure.
                if (($sheetElementData['type'] ?? '') === 'array') {
                    // This is a section.
                    if (!is_array($sheetElementData['el'] ?? false) || !is_array($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] ?? false)) {
                        // No possible containers defined for this section in DS, or no values set for this section.
                        continue;
                    }
                    foreach ($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'] as $valueSectionContainerKey => $valueSectionContainers) {
                        // We have containers for this section in values.
                        if (!is_array($valueSectionContainers ?? false)) {
                            // Values don't validate to an array, skip.
                            continue;
                        }
                        foreach ($valueSectionContainers as $valueContainerType => $valueContainerElements) {
                            // For all value containers in this section.
                            if (!is_array($sheetElementData['el'][$valueContainerType]['el'] ?? false)) {
                                // There is no DS for this container type, skip.
                                continue;
                            }
                            foreach (array_keys($sheetElementData['el'][$valueContainerType]['el']) as $containerElement) {
                                // Container type of this value container exists in DS. Iterate DS container to pick allowed single elements.
                                if (isset($valueContainerElements['el'][$containerElement]['vDEF'])) {
                                    $newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey][$valueContainerType]['el'][$containerElement]['vDEF']
                                        = $valueContainerElements['el'][$containerElement]['vDEF'];
                                }
                            }
                        }
                        if (isset($valueSectionContainers['_TOGGLE'])) {
                            // This was removed in TYPO3 v13, see #102551
                            unset($newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['el'][$valueSectionContainerKey]['_TOGGLE']);
                        }
                    }
                } elseif (isset($valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'])) {
                    // Not a section but a simple field. Keep value if set.
                    $newValueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'] = $valueArray['data'][$sheetKey]['lDEF'][$sheetElementKey]['vDEF'];
                }
            }
        }
        return $this->flexArray2Xml($newValueArray);
    }

    /**
     * Convert FlexForm data array to XML
     *
     * @internal
     */
    public function flexArray2Xml(array $array): string
    {
        // Map the weird keys from the internal array to tags and attributes.
        $options = [
            'parentTagMap' => [
                'data' => 'sheet',
                'sheet' => 'language',
                'language' => 'field',
                'el' => 'field',
                'field' => 'value',
                'field:el' => 'el',
                'el:_IS_NUM' => 'section',
                'section' => 'itemType',
            ],
            'disableTypeAttrib' => 2,
        ];
        return '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF
            . GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', 4, $options);
    }

    /**
     * Parses the flexForm XML string and converts it to an array.
     * The resulting array will be multidimensional, as a value "bla.blubb"
     * results in two levels, and a value "bla.blubb.bla" results in three levels.
     */
    public function convertFlexFormContentToArray(string $flexFormContent): array
    {
        $settings = [];
        $flexFormArray = GeneralUtility::xml2array($flexFormContent);
        $flexFormArray = $flexFormArray['data'] ?? [];
        foreach ($flexFormArray as $languages) {
            if (!is_array($languages['lDEF'] ?? false)) {
                continue;
            }
            foreach ($languages['lDEF'] as $valueKey => $valueDefinition) {
                if (!str_contains($valueKey, '.')) {
                    $settings[$valueKey] = $this->walkFlexFormNode($valueDefinition);
                } else {
                    $valueKeyParts = explode('.', $valueKey);
                    $currentNode = &$settings;
                    foreach ($valueKeyParts as $valueKeyPart) {
                        $currentNode = &$currentNode[$valueKeyPart];
                    }
                    if (is_array($valueDefinition)) {
                        if (array_key_exists('vDEF', $valueDefinition)) {
                            $currentNode = $valueDefinition['vDEF'];
                        } else {
                            $currentNode = $this->walkFlexFormNode($valueDefinition);
                        }
                    } else {
                        $currentNode = $valueDefinition;
                    }
                }
            }
        }
        return $settings;
    }

    /**
     * Parses the flexForm XML string and converts it to an array.
     * The resulting array will be multidimensional. Sheets are
     * respected to support property paths in multiple sheets.
     *
     * A value such as "settings.pageId" results in three levels:
     * "'sDEF' => ['settings' => ['pageId' => 123]]" and a value such
     * as "settings.storages.newsPid" results in four levels:
     * "'sDEF' => ['settings' => ['storages' => ['newsPid' => 123]]]"
     *
     * @param string $flexFormContent flexForm xml string
     */
    public function convertFlexFormContentToSheetsArray(string $flexFormContent): array
    {
        $settings = [];
        $flexFormArray = GeneralUtility::xml2array($flexFormContent);
        $flexFormArray = $flexFormArray['data'] ?? [];
        foreach ($flexFormArray as $sheetName => $sheet) {
            foreach ($sheet as $language => $fields) {
                if ($language !== 'lDEF') {
                    continue;
                }
                foreach ($fields as $valueKey => $valueDefinition) {
                    if (!str_contains($valueKey, '.')) {
                        $settings[$sheetName][$valueKey] = $this->walkFlexFormNode($valueDefinition);
                    } else {
                        $valueKeyParts = explode('.', $valueKey);
                        $currentNode = &$settings[$sheetName];
                        foreach ($valueKeyParts as $valueKeyPart) {
                            $currentNode = &$currentNode[$valueKeyPart];
                        }
                        if (is_array($valueDefinition)) {
                            if (array_key_exists('vDEF', $valueDefinition)) {
                                $currentNode = $valueDefinition['vDEF'];
                            } else {
                                $currentNode = $this->walkFlexFormNode($valueDefinition);
                            }
                        } else {
                            $currentNode = $valueDefinition;
                        }
                    }
                }
            }
        }
        return $settings;
    }

    /**
     * Finds data structure in TCA, defined in column config 'ds'
     *
     * fieldTca = [
     *     'config' => [
     *         'type' => 'flex',
     *         'ds' => '<T3DataStructure>...' OR 'FILE:...',
     *     ]
     * ]
     *
     * This method returns an array of the form:
     * [
     *     'type' => 'tca',
     *     'tableName' => $tableName,
     *     'fieldName' => $fieldName,
     *     'dataStructureKey' => $key,
     * ];
     *
     * Example:
     * [
     *     'type' => 'tca',
     *     'tableName' => 'tt_content',
     *     'fieldName' => 'pi_flexform',
     *     'dataStructureKey' => 'default',
     * ];
     *
     * In case the TCA table supports record types and the given $row uses a record type with a custom
     * data structure (via columnsOverrides) the record type is used as "dataStructureKey".
     *
     *  Example:
     *  [
     *      'type' => 'tca',
     *      'tableName' => 'tt_content',
     *      'fieldName' => 'pi_flexform',
     *      'dataStructureKey' => 'powermail_pi1',
     *  ];
     *
     * @return array Identifier as array, see example above
     * @throws InvalidTcaException
     * @throws InvalidTcaSchemaException
     */
    protected function getDefaultDataStructureIdentifier(string $tableName, string $fieldName, array $row, array|TcaSchema|null $schema = null): array
    {
        if ($schema === null) {
            throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182123);
        }

        $defaultIdentifier = [
            'type' => 'tca',
            'tableName' => $tableName,
            'fieldName' => $fieldName,
            'dataStructureKey' => null,
        ];

        return is_array($schema)
            ? $this->getDataStructureIdentifierFromRawTca($schema, $tableName, $fieldName, $row, $defaultIdentifier)
            : $this->getDataStructureIdentifierFromTcaSchema($schema, $tableName, $fieldName, $row, $defaultIdentifier);
    }

    /**
     * Finds and returns the data structure from TCA -  defined in column config 'ds'
     *
     * fieldTca = [
     *     'config' => [
     *         'type' => 'flex',
     *         'ds' => '<T3DataStructure>...' OR 'FILE:...',
     *     ]
     * ]
     *
     * Based on an identifier, e.g.:
     * [
     *     'type' => 'tca',
     *     'tableName' => 'tt_content',
     *     'fieldName' => 'pi_flexform',
     *     'dataStructureKey' => 'default',
     * ];
     *
     * this method returns '<T3DataStructure>...' OR 'FILE:...'.
     *
     * In case the TCA table supports record types and the "dataStructureKey" points to a record type,
     * which is only the case if a record type defines a custom flex config (via columnsOverrides), this
     * custom data structure is returned.
     *
     * @return string resolved data structure
     * @throws InvalidTcaSchemaException
     */
    protected function getDefaultStructureForIdentifier(array $identifier, array|TcaSchema|null $schema = null): string
    {
        // For the default only type "tca" is handled. Custom types need to be handled by corresponding events.
        if (($identifier['type'] ?? '') !== 'tca') {
            throw new InvalidIdentifierException(
                'Identifier ' . json_encode($identifier) . ' could not be resolved',
                1478104554
            );
        }

        $tableName = (string)($identifier['tableName'] ?? '');
        $fieldName = (string)($identifier['fieldName'] ?? '');
        $dataStructureKey = (string)($identifier['dataStructureKey'] ?? '');

        if ($tableName === '' || $fieldName === '' || $dataStructureKey === '') {
            throw new \RuntimeException(
                'Incomplete "tca" based identifier: ' . json_encode($identifier),
                1478113471
            );
        }

        if ($schema === null) {
            throw new InvalidTcaSchemaException('Can not resolve default data structure without TCA.', 1753182125);
        }

        $dataStructure = is_array($schema)
            ? $this->resolveDataStructureFromRawTca($schema, $fieldName, $dataStructureKey)
            : $this->resolveDataStructureFromTcaSchema($schema, $tableName, $fieldName, $dataStructureKey);

        if ($dataStructure === '') {
            throw new InvalidIdentifierException(
                'Specified identifier ' . json_encode($identifier) . ' does not resolve to a valid data structure',
                1732199538
            );
        }

        return $dataStructure;
    }

    protected function convertDataStructureToArray(string|array $dataStructure): array
    {
        if (is_array($dataStructure)) {
            return $dataStructure;
        }
        // Resolve FILE: prefix pointing to a DS in a file
        if (str_starts_with(trim($dataStructure), 'FILE:')) {
            $fileName = substr(trim($dataStructure), 5);
            $file = GeneralUtility::getFileAbsFileName($fileName);
            if (empty($file) || !is_file($file)) {
                throw new InvalidIdentifierException(
                    'Data structure file "' . $fileName . '" could not be resolved to an existing file',
                    1478105826
                );
            }
            $dataStructure = (string)file_get_contents($file);
        }
        // Parse main structure
        $dataStructure = GeneralUtility::xml2array($dataStructure);
        // Throw if it still is not an array, probably because GeneralUtility::xml2array() failed.
        // This also may happen if artificial identifiers were constructed which don't resolve. The
        // flex form "exclude" access rights systems does that -> catchable
        if (!is_array($dataStructure)) {
            throw new InvalidIdentifierException(
                'Parse error: Data structure could not be resolved to a valid structure.',
                1478106090
            );
        }

        return $dataStructure;
    }

    /**
     * Ensures a data structure has a default sheet, and no duplicate data
     */
    protected function ensureDefaultSheet(array $dataStructure): array
    {
        if (isset($dataStructure['ROOT']) && isset($dataStructure['sheets'])) {
            throw new \RuntimeException(
                'Parsed data structure has both ROOT and sheets on top level. That is invalid.',
                1440676540
            );
        }
        if (isset($dataStructure['ROOT']) && is_array($dataStructure['ROOT'])) {
            $dataStructure['sheets']['sDEF']['ROOT'] = $dataStructure['ROOT'];
            unset($dataStructure['ROOT']);
        }
        return $dataStructure;
    }

    /**
     * Resolve FILE:EXT and EXT: for single sheets
     */
    protected function resolveFileDirectives(array $dataStructure): array
    {
        if (isset($dataStructure['sheets']) && is_array($dataStructure['sheets'])) {
            foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
                if (!is_array($sheetStructure)) {
                    if (str_starts_with(trim($sheetStructure), 'FILE:')) {
                        $file = GeneralUtility::getFileAbsFileName(substr(trim($sheetStructure), 5));
                    } else {
                        $file = GeneralUtility::getFileAbsFileName(trim($sheetStructure));
                    }
                    if ($file && @is_file($file)) {
                        $sheetStructure = GeneralUtility::xml2array((string)file_get_contents($file));
                    }
                }
                $dataStructure['sheets'][$sheetName] = $sheetStructure;
            }
        }
        return $dataStructure;
    }

    /**
     * Check for invalid flex form structures, migrate and prepare single fields.
     * @throws InvalidDataStructureException
     */
    private function checkMigratePrepareFlexTca(array $dataStructure): array
    {
        if (!is_array($dataStructure['sheets'] ?? null)) {
            return $dataStructure;
        }
        $newStructure = $dataStructure;
        foreach ($dataStructure['sheets'] as $sheetName => $sheetStructure) {
            if (!is_array($sheetStructure['ROOT']['el'])) {
                continue;
            }
            foreach ($sheetStructure['ROOT']['el'] as $sheetElementName => $sheetElementConfig) {
                if (!is_array($sheetElementConfig)) {
                    continue;
                }
                if (($sheetElementConfig['type'] ?? null) === 'array' xor ($sheetElementConfig['section'] ?? null) === '1') {
                    // Section element, but type=array without section=1 or vice versa is not ok
                    throw new InvalidDataStructureException(
                        'Broken data structure on field name ' . $sheetElementName . '. section without type or vice versa is not allowed',
                        1440685208
                    );
                }
                if (($sheetElementConfig['type'] ?? null) === 'array' && ($sheetElementConfig['section'] ?? null) === '1') {
                    // Section element
                    if (!is_array($sheetElementConfig['el'] ?? null)) {
                        continue;
                    }
                    foreach ($sheetElementConfig['el'] as $containerName => $containerConfig) {
                        if (!is_array($containerConfig['el'] ?? null)) {
                            continue;
                        }
                        foreach ($containerConfig['el'] as $containerElementName => $containerElementConfig) {
                            if (!is_array($containerElementConfig)) {
                                continue;
                            }
                            if (
                                // inline, file, group and category are always DB relations
                                in_array($containerElementConfig['config']['type'] ?? [], ['inline', 'file', 'folder', 'group', 'category'], true)
                                // MM is not allowed (usually type=select, otherwise the upper check should kick in)
                                || isset($containerElementConfig['config']['MM'])
                                // foreign_table is not allowed (usually type=select, otherwise the upper check should kick in)
                                || isset($containerElementConfig['config']['foreign_table'])
                            ) {
                                // Nesting types that use DB relations in container sections is not supported.
                                throw new InvalidDataStructureException(
                                    'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
                                    . ' in section container "' . $containerName . '": Nesting elements that have database relations in flex form'
                                    . ' sections is not allowed.',
                                    1458745468
                                );
                            }
                            if (($containerElementConfig['type'] ?? null) === 'array' && ($containerElementConfig['section'] ?? null) === '1') {
                                // Nesting sections is not supported. Throw an exception if configured.
                                throw new InvalidDataStructureException(
                                    'Invalid flex form data structure on field name "' . $containerElementName . '" with element "' . $sheetElementName . '"'
                                    . ' in section container "' . $containerName . '": Nesting sections in container elements'
                                    . ' sections is not allowed.',
                                    1458745712
                                );
                            }
                            $containerElementConfig = $this->migrateFlexField($containerElementName, $containerElementConfig);
                            $containerElementConfig = $this->prepareFlexField($containerElementName, $containerElementConfig);
                            $newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName]['el'][$containerName]['el'][$containerElementName] = $containerElementConfig;
                        }
                    }
                } else {
                    // Normal element
                    $sheetElementConfig = $this->migrateFlexField($sheetElementName, $sheetElementConfig);
                    $sheetElementConfig = $this->prepareFlexField($sheetElementName, $sheetElementConfig);
                    $newStructure['sheets'][$sheetName]['ROOT']['el'][$sheetElementName] = $sheetElementConfig;
                }
            }
        }
        return $newStructure;
    }

    private function migrateFlexField(string $fieldName, array $fieldConfig): array
    {
        // TcaMigration of this field. Call the TcaMigration and log any deprecations.
        $dummyTca = [
            'dummyTable' => [
                'columns' => [
                    $fieldName => $fieldConfig,
                ],
            ],
        ];
        $tcaProcessingResult = $this->tcaMigration->migrate($dummyTca);
        // Messages are reset on each `migrate()` execution
        $messages = $tcaProcessingResult->getMessages();
        if (!empty($messages)) {
            $context = 'FlexFormTools did an on-the-fly migration of a flex form data structure. This is deprecated and will be removed.'
                . ' Merge the following changes into the flex form definition "' . $fieldName . '":';
            array_unshift($messages, $context);
            trigger_error(implode(LF, $messages), E_USER_DEPRECATED);
        }
        return $tcaProcessingResult->getTca()['dummyTable']['columns'][$fieldName];
    }

    private function prepareFlexField(string $fieldName, array $fieldConfig): array
    {
        $dummyTca = [
            'dummyTable' => [
                'columns' => [
                    $fieldName => $fieldConfig,
                ],
            ],
        ];
        $preparedTca = $this->tcaPreparation->prepare($dummyTca, true);
        return $preparedTca['dummyTable']['columns'][$fieldName];
    }

    /**
     * Resolve data structure identifier from raw TCA configuration.
     */
    private function getDataStructureIdentifierFromRawTca(array $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
    {
        // Check for record type specific configuration
        if (isset($schema['ctrl']['type'])) {
            $recordType = $row[$schema['ctrl']['type']] ?? '';
            if (isset($schema['types'][$recordType]) && ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $recordType, $fieldName)) !== []) {
                if ($fieldConfig['config']['type'] === 'flex' && $fieldConfig['config']['ds'] !== '') {
                    $defaultIdentifier['dataStructureKey'] = $recordType;
                    return $defaultIdentifier;
                }

                throw new InvalidTcaException(
                    'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
                    . ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
                    1751796941
                );
            }
        }

        // Fall back to base field configuration
        $baseField = $schema['columns'][$fieldName]['config'] ?? [];
        if (($baseField['type'] ?? '') === 'flex' && ($baseField['ds'] ?? '') !== '') {
            $defaultIdentifier['dataStructureKey'] = 'default';
            return $defaultIdentifier;
        }

        throw new InvalidTcaException(
            'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
            . ' The field is either not configured as type="flex" or no valid data structure is defined.',
            1732198005
        );
    }

    /**
     * Resolve data structure identifier from TCA Schema.
     */
    private function getDataStructureIdentifierFromTcaSchema(TcaSchema $schema, string $tableName, string $fieldName, array $row, array $defaultIdentifier): array
    {
        if ($schema->getName() !== $tableName) {
            throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $tableName . ' from data structure identifier.', 1753182124);
        }

        // Check for record type specific configuration
        if ($schema->supportsSubSchema()) {
            $recordType = (string)($row[$schema->getSubSchemaTypeInformation()->getFieldName()] ?? '');
            if ($recordType !== '' && $schema->hasSubSchema($recordType) && ($subSchema = $schema->getSubSchema($recordType))->hasField($fieldName)) {
                $flexField = $subSchema->getField($fieldName);
                if ($flexField instanceof FlexFormFieldType && $flexField->getDataStructure() !== '') {
                    $defaultIdentifier['dataStructureKey'] = $recordType;
                    return $defaultIdentifier;
                }

                throw new InvalidTcaException(
                    'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" with record type "' . $recordType . '"'
                    . ' The field is either not configured as type="flex" or no valid data structure is defined for this record type.',
                    1751796940
                );
            }
        }

        // Fall back to base field
        $baseField = $schema->getField($fieldName);
        if ($baseField instanceof FlexFormFieldType && $baseField->getDataStructure() !== '') {
            $defaultIdentifier['dataStructureKey'] = 'default';
            return $defaultIdentifier;
        }

        throw new InvalidTcaException(
            'TCA misconfiguration in table "' . $tableName . '" field "' . $fieldName . '" config section:'
            . ' The field is either not configured as type="flex" or no valid data structure is defined.',
            1732198004
        );
    }

    /**
     * Resolve data structure from raw TCA configuration.
     */
    private function resolveDataStructureFromRawTca(array $schema, string $fieldName, string $dataStructureKey): string
    {
        // Try record type specific configuration first
        if (isset($schema['ctrl']['type'], $schema['types'][$dataStructureKey])
            && ($fieldConfig = $this->getRecordTypeSpecificFieldConfig($schema, $dataStructureKey, $fieldName)) !== []
            && ($fieldConfig['config']['type'] ?? '') === 'flex'
            && is_string($fieldConfig['config']['ds'] ?? false)
        ) {
            return $fieldConfig['config']['ds'];
        }

        // Fall back to default configuration
        if ($dataStructureKey === 'default') {
            $baseField = $schema['columns'][$fieldName]['config'] ?? [];
            if (($baseField['type'] ?? '') === 'flex' && is_string($baseField['ds'] ?? false)) {
                return $baseField['ds'];
            }
        }

        return '';
    }

    /**
     * Resolve data structure from TCA Schema.
     */
    private function resolveDataStructureFromTcaSchema(TcaSchema $schema, string $table, string $field, string $dataStructureKey): string
    {
        if ($schema->getName() !== $table) {
            throw new InvalidTcaSchemaException('Given Tca Schema does not match table ' . $table . ' from data structure identifier.', 1753182126);
        }

        // Try record type specific configuration first
        if ($schema->supportsSubSchema()
            && $schema->hasSubSchema($dataStructureKey)
            && ($subSchema = $schema->getSubSchema($dataStructureKey))->hasField($field)
            && ($flexField = $subSchema->getField($field)) instanceof FlexFormFieldType
        ) {
            return $flexField->getDataStructure();
        }

        // Fall back to default configuration
        if ($dataStructureKey === 'default' && ($flexField = $schema->getField($field)) instanceof FlexFormFieldType) {
            return $flexField->getDataStructure();
        }

        return '';
    }

    /**
     * Returns the record type specific configuration, also already taking columnsOverrides into account.
     * In case the field is not defined for the record type, no configuration is returned.
     */
    protected function getRecordTypeSpecificFieldConfig(array $tcaForTable, string $recordType, string $fieldName): array
    {
        $recordTypeConfig = $tcaForTable['types'][$recordType];
        $showItemArray = GeneralUtility::trimExplode(',', $recordTypeConfig['showitem'] ?? '', true);
        foreach ($showItemArray as $aShowItemFieldString) {
            [$name, , $paletteName] = GeneralUtility::trimExplode(';', $aShowItemFieldString . ';;;');
            if ($name === '--div--') {
                continue;
            }
            if ($name === '--palette--' && !empty($paletteName)) {
                if (!isset($tcaForTable['palettes'][$paletteName]['showitem'])) {
                    continue;
                }
                $palettesArray = GeneralUtility::trimExplode(',', $tcaForTable['palettes'][$paletteName]['showitem']);
                foreach ($palettesArray as $aPalettesString) {
                    [$name] = GeneralUtility::trimExplode(';', $aPalettesString . ';;');
                    if ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
                        return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
                    }
                }
            } elseif ($name === $fieldName && isset($tcaForTable['columns'][$name])) {
                return array_replace_recursive($tcaForTable['columns'][$name], $recordTypeConfig['columnsOverrides'][$name] ?? []);
            }
        }
        return [];
    }

    /**
     * Parses a flexForm node recursively and takes care of sections etc.
     * Helper method of convertFlexFormContentToArray() and convertFlexFormContentToSheetsArray().
     */
    private function walkFlexFormNode(mixed $nodeArray): mixed
    {
        if (!is_array($nodeArray)) {
            return $nodeArray;
        }
        $result = [];
        foreach ($nodeArray as $nodeKey => $nodeValue) {
            if ($nodeKey === 'vDEF') {
                return $nodeValue;
            }
            if (in_array($nodeKey, ['el', '_arrayContainer'])) {
                return $this->walkFlexFormNode($nodeValue);
            }
            if (($nodeKey[0] ?? '') === '_') {
                continue;
            }
            if (strpos((string)$nodeKey, '.')) {
                $nodeKeyParts = explode('.', $nodeKey);
                $currentNode = &$result;
                $nodeKeyPartsCount = count($nodeKeyParts);
                for ($i = 0; $i < $nodeKeyPartsCount - 1; $i++) {
                    $currentNode = &$currentNode[$nodeKeyParts[$i]];
                }
                $newNode = [next($nodeKeyParts) => $nodeValue];
                $subVal = $this->walkFlexFormNode($newNode);
                $currentNode[key($subVal)] = current($subVal);
            } elseif (is_array($nodeValue)) {
                if (array_key_exists('vDEF', $nodeValue)) {
                    $result[$nodeKey] = $nodeValue['vDEF'];
                } else {
                    $result[$nodeKey] = $this->walkFlexFormNode($nodeValue);
                }
            } else {
                $result[$nodeKey] = $nodeValue;
            }
        }
        return $result;
    }
}
