<?php
/**
 * Trait
 *
 * @version     1.0.$Revision:$
 * @version     SVN: $Id:$
 * @package     bplan-base/globals
 * @subpackage  Traits
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 * @copyright   Copyright (C) 2025, 2026 bplan-solutions GmbH & Co. KG <https://www.bplan-solutions.de/>
 * /Δ\
 */

namespace BplanBase\Globals\Traits;


use BplanBase\Globals\Support\Arr;
use Illuminate\Support\Facades\App;


/**
 * Trait
 *
 * Kapselt die Verarbeitungslogik für JSON-basierte Konfigurations-Templates
 * (`config_template`) und deren gespeicherte Einstellungen (`config_settings`).
 *
 * Verwendbar in Livewire-Formularkomponenten, die ein Modell mit einem
 * `config_template`-Feld (Typdefinition) und einem `config_settings`-Feld
 * (Instanzwerte) verwalten.
 *
 * ---
 *
 * ## JSON-Struktur für `config_template`
 *
 * Das Template kann als JSON-Array oder als JSON-Objekt angegeben werden.
 *
 * ### Minimales Array-Format:
 *
 *      [
 *          { "name": "host" },
 *          { "name": "port" }
 *      ]
 *
 * ### Minimales Objekt-Format:
 *
 *      {
 *          "host": { "name": "host" },
 *          "port": { "name": "port" }
 *      }
 *
 * Bei Objekt-Notation ist das `name`-Attribut optional, da der Schlüssel als
 * Name verwendet wird.
 *
 * ---
 *
 * ## Unterstützte Feldattribute:
 *
 *  - `crypted`    Steuert die Verschlüsselung des Wertes.
 *                 Werte: 0|1, Standard: 0
 *
 *  - `default`    Standardwert bei der Neuanlage.
 *                 Werte: mixed, Standard: null
 *
 *  - `label`      Abweichende Beschriftung des Eingabefeldes.
 *                 Werte: string, Standard: Wert aus `name`
 *
 *  - `mandatory`  Pflichtfeld.
 *                 Werte: bool, Standard: false
 *
 *  - `name`       Feldbezeichner. Pflicht bei Array-Format, optional bei Objekt-Format.
 *                 Werte: string
 *
 *  - `order`      Sortierposition innerhalb des Formulars.
 *                 Werte: integer, Standard: keiner (wird ans Ende gestellt)
 *                 Hinweis: Wird ignoriert wenn `.meta.order` vorhanden ist.
 *
 *  - `section`    Identifier der Formularsektion in der das Feld erscheint.
 *                 Werte: string, Standard: "default"
 *
 *  - `subtype`    Sub-Typ für zukünftige Validierungssteuerung (noch nicht implementiert).
 *
 *  - `type`       Typ des Eingabefeldes (noch nicht vollständig implementiert).
 *                 Werte: number|select|string|text|toggle, Standard: string
 *                 Hinweis: `select` erfordert zusätzlich das Attribut `options`.
 *
 * ---
 *
 * ## `.meta`-Element (nur Objekt-Format):
 *
 * Das optionale `.meta`-Element ermöglicht Mehrsprachigkeit und Sortiersteuerung:
 *
 *      {
 *          ".meta": {
 *              "labels": {
 *                  "de_DE": {
 *                      "host":            "Hostname",
 *                      "port":            "Port",
 *                      "section.default": "Allgemein",
 *                      "section.network": "Netzwerk"
 *                  },
 *                  "en_GB": {
 *                      "host":            "Hostname",
 *                      "port":            "Port",
 *                      "section.default": "General",
 *                      "section.network": "Network"
 *                  }
 *              },
 *              "order": ["host", "port"],
 *              "sectionorder": ["network", "default"]
 *          },
 *          "host": { "name": "host", "section": "network" },
 *          "port": { "name": "port", "section": "network" }
 *      }
 *
 *  - `labels`       Locale-abhängige Label-Übersetzungen für Felder und Sektionen.
 *                   Sektions-Labels verwenden den Schlüssel `section.<sectionName>`.
 *                   Zwei Varianten:
 *                   - String: Laravel-Translation-Key (empfohlen), z.B. "globals::printers.my-labels"
 *                   - Objekt: Inline-Labels mit Locale-Codes als Schlüssel (Legacy)
 *
 *  - `order`        Globale Feldreihenfolge (überschreibt `order`-Attribute in Feldern).
 *                   Felder die nicht aufgelistet sind werden ans Ende gestellt.
 *
 *  - `sectionorder` Reihenfolge der Formularsektionen.
 *
 * ---
 *
 * ## Verwendung in einer Livewire-Formularkomponente:
 *
 *      use BplanBase\Globals\Traits\ProcessesConfigTemplate;
 *
 *      class Form extends BaseForm
 *      {
 *          use ProcessesConfigTemplate;
 *
 *          public array  $configSections = [];
 *          public ?array $configTemplate = null;
 *          public mixed  $configSettings = null;
 *
 *          private function _initConfigTemplate(): void
 *          {
 *              $PrinterType = PrinterType::find($this->main['printer_type_id']);
 *              $result = $this->_prepareConfigTemplate($PrinterType->config_template);
 *              $this->configTemplate = $result['template'];
 *              $this->configSections = $result['sections'];
 *          }
 *
 *          public function mount(): void
 *          {
 *              parent::mount();
 *              $this->_initConfigTemplate();
 *
 *              if ($this->exists === true) {
 *                  $this->configSettings = $this->_decodeConfigSettings($this->main['config_settings']);
 *              }
 *          }
 *
 *          protected function _onBeforeValidation(): void
 *          {
 *              parent::_onBeforeValidation();
 *              $this->main['config_settings'] = $this->_encodeConfigSettings($this->configSettings);
 *          }
 *      }
 *
 * @version     1.3.0 / 2026-05-24
 * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
 */
trait ProcessesConfigTemplate
{


/* +++ OBJECT METHODS +++ +++++++++++++++++++++++++++++++++++++++++++++++++++ */


    /**
     * Befüllt ein Settings-Array mit Default-Werten aus dem aufbereiteten Template.
     *
     * Für jedes Feld im Template wird geprüft, ob im übergebenen `$settings`-Array
     * bereits ein Eintrag existiert. Fehlt er, wird der `default`-Wert des Felds
     * eingetragen (als String, da Formularfelder String-Werte erwarten).
     * Felder ohne `default` erhalten einen leeren String.
     *
     * Typischer Aufruf nach `_prepareConfigTemplate()` bei der Neuanlage eines Datensatzes.
     *
     * @param       array|null $preparedTemplate   Rückgabe von `_prepareConfigTemplate()['template']`
     * @param       array      $settings           Bestehendes Settings-Array (wird befüllt)
     *
     * @return      array   Das befüllte Settings-Array
     *
     * @version     1.0.0 / 2026-05-24
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _applyConfigDefaults(?array $preparedTemplate, array $settings = []): array
    {
        if ($preparedTemplate === null) {
            return $settings;
        }

        foreach ($preparedTemplate as $sectionFields) {
            foreach ($sectionFields as $Field) {
                if (($Field->type ?? null) === 'spacer') {
                    continue;
                }
                if (isset($settings[$Field->name]) === false) {
                    $settings[$Field->name] = isset($Field->default) && $Field->default !== null
                        ? (string) $Field->default
                        : '';
                }
            }
        }

        return $settings;

    } // _applyConfigDefaults()


    /**
     * Dekodiert einen JSON-`config_settings`-String in ein Objekt für das Formular.
     *
     * @param       string|null $configSettingsJson
     *
     * @return      object|null
     *
     * @version     1.0.0 / 2026-05-06
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _decodeConfigSettings(?string $configSettingsJson): ?object
    {
        if ($configSettingsJson === null) {
            return null;
        }

        return json_decode($configSettingsJson);

    } // _decodeConfigSettings()


    /**
     * Kodiert die Formular-Einstellungen zurück in einen JSON-String für die Datenbank.
     *
     * @param       mixed $configSettings
     *
     * @return      string|null
     *
     * @version     1.1.0 / 2026-05-12
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _encodeConfigSettings(mixed $configSettings): mixed
    {
        if ($configSettings === null) {
            return null;
        }
        return $configSettings;

    } // _encodeConfigSettings()


    /**
     * Verarbeitet ein JSON-Config-Template in eine formular-bereite Datenstruktur.
     *
     * Gibt ein assoziatives Array mit zwei Schlüsseln zurück:
     *  - `template`  — Das nach Sektionen gruppierte und sortierte Template-Array.
     *                  null wenn kein Template übergeben wurde.
     *  - `sections`  — Assoziatives Array der Sektions-Identifier → Sektions-Label.
     *                  Enthält immer mindestens `['default' => 'default']`.
     *
     * @param       string|null $configTemplateJson
     *
     * @return      array{template: array|null, sections: array}
     *
     * @version     1.1.0 / 2025-05-10
     * @author      Wassilios Meletiadis <wassilios.meletiadis@bplan-solutions.de>
     */
    protected function _prepareConfigTemplate(?string $configTemplateJson): array
    {
        $configSections  = ['default' => trans('globals::dynamic-form.default-section-label')];
        $preparedTemplate = null;

        if ($configTemplateJson === null) {
            return [
                'template' => null,
                'sections' => $configSections
            ];
        }

        $locale   = App::getLocale();
        $template = (array) json_decode($configTemplateJson);

        $preparedTemplate = ['default' => []];
        $metaLabels       = [];
        $metaOrder        = false;
        $orderKeys        = [];
        $sectionOrderKeys = [];

        if (isset($template['.meta'])) {
            $Meta = $template['.meta'];
            /*
            **  Wenn es ein .meta-Element gibt, dann ist der Array definitiv nicht nummerisch
            **  indiziert und die Elemente in $template sind Objekte.
            **  In diesem Fall wird zunächst dafür gesorgt, dass die Elemente, mit denen in der
            **  Folge gearbeitet wird, in Arrays gecastet werden. So lässt sich verhindern, dass
            **  zwei unterschiedliche Logiken implementiert werden müssen, unabhängig von der
            **  Ausgangssituation. */
            if (isset($Meta->labels)) {
                if (is_string($Meta->labels)) {
                    /*
                    **  Variante 1: labels ist ein Laravel-Translation-Key (z.B. "globals::printers.my-labels").
                    **  trans() übersetzt locale-abhängig — keine Inline-Übersetzungen in der Config nötig. */
                    $metaLabels = (array) trans($Meta->labels);

                } elseif (isset($Meta->labels->{$locale})) {
                    /*
                    **  Variante 2 (Legacy): labels ist ein Inline-Objekt mit Locale-Codes als Schlüssel. */
                    $metaLabels = (array) $Meta->labels->{$locale};
                }
            }
            if (isset($Meta->order)) {
                $orderKeys = (array) $Meta->order;
                $metaOrder = true;
            }
            if (isset($Meta->sectionorder)) {
                $sectionOrderKeys = (array) $Meta->sectionorder;
            }
        }

        foreach ($template as $key => $Item) {
            if ($key === '.meta' || $key === 'x.meta') {
                continue;
            }

            $item = (array) $Item;

            /*
            **  Bei einer Konfiguration mit assoziativer Indizierung ist das name-Attribut
            **  optional, weil der Name bereits im Schlüssel steckt. Um auch bei fehlendem
            **  name-Attribut im weiteren Verlauf einheitlich arbeiten zu können, wird der Name
            **  hier, falls erforderlich, ergänzt. */
            if (is_numeric($key) === false) {
                if (empty($item['name'])) {
                    $template[$key]->name = $key;
                }
            }

            if (isset($metaLabels[$key])) {
                /*
                **  Wenn es ein Label in der korrekten Sprache gibt, dann wird dieses genutzt. */
                $template[$key]->label = $metaLabels[$key];

            } elseif (isset($item['label']) === false) {
                if (isset($item['name'])) {
                    /*
                    **  Ist ein Name angegeben (der in einem nummerisch indizierten Array als
                    **  Ersatz für den fehlenden Schlüssel fungiert, in einem assoziativen Array
                    **  aber auch vom Schlüssel abweichen oder fehlen kann), dann wird dieser
                    **  Name als Label eingesetzt. */
                    $template[$key]->label = $item['name'];

                } else {
                    /*
                    **  Wenn bis hier hin kein Label gefunden werden konnte, dann wird der
                    **  aktuelle Schlüssel als Label eingesetzt.
                    **
                    **  Dieser Fall darf bei nummerischer Indizierung nie eintreten. Dort ist
                    **  die Angabe eines name-Attributs zwingend erforderlich. */
                    $template[$key]->label = $key;
                }
            }

            if ($metaOrder === false) {
                /*
                **  Für den Fall, dass es kein meta-order-Element gibt, werden die einzelnen
                **  Elemente auf das Vorhandensein eines order-Elements hin überprüft. */
                if (isset($item['order'])) {
                    $orderKeys[$key] = $item['order'];
                }
            }

            /*
            **  Weitere optionale Attribute prüfen und gegebenenfalls ergänzen. */
            if (isset($item['crypted']) === false) {
                $template[$key]->crypted = 0;
            }
            if (isset($item['default']) === false) {
                $template[$key]->default = null;
            }
            if (isset($item['mandatory']) === false) {
                $template[$key]->mandatory = false;
            }

            $section = isset($item['section']) ? $item['section'] : 'default';

            $preparedTemplate[$section][] = $template[$key];
        }

        /*
        **  Wenn es OrderKeys gibt, dann können die Felder sortiert werden. */
        if (empty($orderKeys) === false) {
            if ($metaOrder === false) {
                /*
                **  Wurden die OrderKeys aus den order-Attributen der einzelnen Elemente erstellt,
                **  dann befinden sich die für die Sortierung relevanten Daten in den Schlüsseln.
                **  Vor dem Extrahieren der Schlüssel wird der Array noch anhand seiner Werte
                **  sortiert, ohne die Schlüssel zu verändern. */
                asort($orderKeys);
                $orderKeys = array_keys($orderKeys);
            }

            Arr::ksortArray($template, $orderKeys, true);

            if ($metaOrder === true) {
                /*
                **  Bei meta-order werden Spacer-Platzhalter ("_"-Einträge in der Order-Liste) dem
                **  jeweils nachfolgenden Feld zugeordnet: ein Spacer gehört zur Section des Felds,
                **  das ihm in der Order-Liste folgt. Spacer ohne nachfolgendes Feld werden verworfen.
                **
                **  Dazu wird das globale orderKeys-Array einmalig vorab durchlaufen und je Section
                **  eine eigene geordnete Liste aufgebaut, die an den richtigen Positionen synthetische
                **  Spacer-Objekte enthält. */
                $perSectionOrder = [];
                $pendingSpacers  = 0;
                $spacerCount     = 0;

                /*
                **  Schnelle Lookup-Map: Feldname → Section-Key */
                $fieldSectionMap = [];
                foreach ($preparedTemplate as $sectionKey => $sectionFields) {
                    foreach ($sectionFields as $Field) {
                        $fieldSectionMap[$Field->name] = $sectionKey;
                    }
                }

                foreach ($orderKeys as $orderKey) {
                    if (str_starts_with((string) $orderKey, '_') === true) {
                        $pendingSpacers++;

                    } elseif (isset($fieldSectionMap[$orderKey]) === true) {
                        $targetSection = $fieldSectionMap[$orderKey];

                        for ($i = 0; $i < $pendingSpacers; $i++) {
                            $spacerCount++;
                            $perSectionOrder[$targetSection][] = (object) ['type' => 'spacer', 'name' => '_' . $spacerCount];
                        }
                        $pendingSpacers = 0;
                        $perSectionOrder[$targetSection][] = $orderKey;
                    }
                    /*
                    **  Felder, die nicht in $fieldSectionMap vorhanden sind, werden übersprungen.
                    **  Spacer ohne nachfolgendes Feld ($pendingSpacers > 0 nach der Schleife)
                    **  werden verworfen. */
                }

                foreach ($preparedTemplate as $section => &$items) {
                    $itemOrder = $perSectionOrder[$section] ?? [];

                    /*
                    **  Felder aus $items die in sectionOrderKeys stehen → in die richtige Reihenfolge bringen.
                    **  Felder die nicht in sectionOrderKeys stehen → ans Ende anhängen. */
                    $fieldMap    = [];
                    foreach ($items as $Field) {
                        $fieldMap[$Field->name] = $Field;
                    }
                    $sortedItems = [];

                    foreach ($itemOrder as $entry) {
                        if (is_object($entry) === true) {
                            $sortedItems[] = $entry;
                        } elseif (isset($fieldMap[$entry]) === true) {
                            $sortedItems[] = $fieldMap[$entry];
                            unset($fieldMap[$entry]);
                        }
                    }
                    foreach ($fieldMap as $Field) {
                        $sortedItems[] = $Field;
                    }
                    $items = $sortedItems;

                    if (isset($metaLabels['section.'.$section])) {
                        $configSections[$section] = $metaLabels['section.'.$section];
                    } else {
                        $configSections[$section] = $section;
                    }
                }
                unset($items);

            } else {
                foreach ($preparedTemplate as $section => &$items) {
                    Arr::ksortArray($items, $orderKeys, true);

                    if (isset($metaLabels['section.'.$section])) {
                        $configSections[$section] = $metaLabels['section.'.$section];
                    } else {
                        $configSections[$section] = $section;
                    }
                }
                unset($items);
            }
        }

        /*
        **  Für dotted Section-Keys (z.B. "network.basic") den übergeordneten Tab-Key ("network")
        **  in $configSections eintragen, falls noch nicht vorhanden. Wird für die Tab-Darstellung
        **  in der Dynamic-Form-Komponente benötigt, da dort der Tab-Key als Label-Schlüssel
        **  verwendet wird. */
        foreach (array_keys($configSections) as $sectionKey) {
            if (str_contains($sectionKey, '.') === true) {
                $tabKey = explode('.', $sectionKey, 2)[0];

                if (isset($configSections[$tabKey]) === false) {
                    $configSections[$tabKey] = $metaLabels['section.'.$tabKey] ?? $tabKey;
                }
            }
        }

        /*
        **  Leere Sektionen entfernen (z.B. die immer vorinitialisierte "default"-Sektion,
        **  wenn kein Feld dieser Sektion zugeordnet wurde). */
        $preparedTemplate = array_filter($preparedTemplate, fn ($fields) => empty($fields) === false);

        /*
        **  Felder ohne explizite Section-Angabe landen in der "default"-Sektion.
        **  Existieren daneben weitere Sektionen, werden diese Felder an den Anfang der
        **  ersten anderen Sektion verschoben, damit keine eigene "default"-Gruppe erscheint.
        **  Existiert nur "default" (kein Feld hat eine Section), bleibt das Verhalten
        **  unverändert — "default" ist dann die einzige Sektion. */
        if (isset($preparedTemplate['default']) === true && count($preparedTemplate) > 1) {
            $firstOtherKey = null;

            foreach ($preparedTemplate as $sectionKey => $fields) {
                if ($sectionKey !== 'default') {
                    $firstOtherKey = $sectionKey;

                    break;
                }
            }
            $preparedTemplate[$firstOtherKey] = array_merge(
                $preparedTemplate['default'],
                $preparedTemplate[$firstOtherKey]
            );
            unset($preparedTemplate['default']);
            unset($configSections['default']);
        }

        /*
        **  Wenn es SectionOrderKeys gibt, dann können auch die Formularsektionen sortiert werden. */
        if (empty($sectionOrderKeys) === false) {
            Arr::ksortArray($preparedTemplate, $sectionOrderKeys, false);
        }
        return [
            'template' => $preparedTemplate,
            'sections' => $configSections
        ];
    } // _prepareConfigTemplate()


} // trait ProcessesConfigTemplate {}
