<?php
/**
 * Created by PhpStorm.
 * User: Pascal Roessler
 * Date: 05/22/23
 * Time: 10:45 AM
 */

namespace IMATHUZH\Qfq\Core\Form\FormElement;

use CodeException;
use DbException;
use IMATHUZH\Qfq\Core\Ai\AiService;
use IMATHUZH\Qfq\Core\Ai\RagService;
use IMATHUZH\Qfq\Core\BodytextParser;
use IMATHUZH\Qfq\Core\Database\DatabaseManager;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\Ldap;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Report\Report;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;
use PHPUnit\Util\Exception;
use UserFormException;
use UserReportException;

/**
 * Class AbstractFormElement
 * @package qfq
 */
abstract class AbstractFormElement {

    /**
     * The unaltered array of attributes as it is passed in the constructor.
     * In a few cases it is necessary to have access to this array.
     * E.g.: Template Group child elements are instantiated using the initialAttributes of the 'template'
     */
    public ?array $initialAttributes = null;
    /**
     * Contains all attributes of the form element.
     * Starts out as the array that is passed in the constructor, but is modified during the object's lifetime
     */
    public ?array $attributes = null;
    public ?Form $form;

    /**
     * @var array contains html attributes and their values to be processed by a renderer class.
     */
    public array $htmlAttributes = array();
    /**
     * @var array contains css classes to be added to the HTML element by a Renderer class.
     * IMPORTANT: Must not contain Renderer-Specific classes (e.g. Bootstrap 3, Bootstrap 5, etc...) - they are added by their respective renderers.
     *            Only classes that will be needed independent of choice of Renderer.
     */
    public array $cssClasses = array();
    public array $addClassRequired = array();
    /**
     * @var string The final value as it is assigned to the html element.
     * Not to be confused with $this->attributes[FE_VALUE], which describes the Form Element's value field.
     */
    public string $value;
    /**
     * If the evaluated value of the FE is an array, it is stored in this field instead of $this->value
     */
    public array $valueArray = array();
    /**
     * @var string The url to the edit form of this FE
     */
    public string $formEditorUrl;
    /**
     * @var string The tooltip for the url to the edit form of this FE
     */
    public string $formEditorTooltip;

    protected ?Store $store = null;
    protected ?DatabaseManager $databaseManager = null;
    public ?Evaluate $evaluate = null;
    protected ?Sip $sip = null;
    protected ?Link $link = null;
    public bool $showDebugInfoFlag;

    protected array $debugStack = array();
    /**
     * These params will not be evaluated for variables and sql statements on load.
     */
    protected array $skipEvaluationOnLoad = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
        FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE,
        FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION, FE_TYPEAHEAD_GLUE_UPDATE];

    /**
     * Any deriving class should call parent::__construct($attributes, $form) at the beginning of its constructor!
     * After that, the deriving class can apply specific settings/logic.
     *
     * @param array $attributes
     * @param Form|null $form
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function __construct(array $attributes, ?Form $form = null) {
        $this->initialAttributes = $attributes;
        $this->attributes = $attributes;

        Store::setVar(SYSTEM_FORM_ELEMENT_ID, $attributes[FE_ID], STORE_SYSTEM); // If Exception occurs, link can be built to this FE
        Store::setVar(SYSTEM_FORM_ELEMENT, $attributes[FE_NAME], STORE_SYSTEM);
        $this->form = $form;
        $this->store = Store::getInstance();
        $this->databaseManager = DatabaseManager::getInstance();
        $dbIndexEvaluate = $this->form != null ? $this->form->specFinal[F_DB_INDEX] : $this->databaseManager->indexData;
        $this->evaluate = new Evaluate($this->store, $this->databaseManager->getDbByIndex($dbIndexEvaluate));
        $this->showDebugInfoFlag = Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM));
        $this->sip = $this->store->getSipInstance();
        $this->link = new Link($this->sip, $this->databaseManager->indexData);

        $allowOverwrite = $this->attributes[FE_ALLOW_PARAMETER_OVERWRITE] ?? false;
        HelperFormElement::explodeParameter($this->attributes, F_PARAMETER, $allowOverwrite);
        $this->handleFillStoreVar();
        $this->handleFillStoreLdap();
        $this->handleSlaveId();

        $this->handleEvaluate();
        $this->attributes = Support::setFeDefaults($this->attributes, $this->form != null ? $this->form->specFinal : array());

        $this->handleEmptyNameOrLabel();

        // Retype htmlId needs to differ from original htmlId or the eye button will toggle both at the same time
        $htmlIdAppendix = isset($this->attributes[FE_RETYPE_SOURCE_NAME]) ? '-retype' : '';

        $this->htmlAttributes[HTML_ATTR_ID] = HelperFormElement::buildFormElementId($form == null ? "" : $form->getHtmlId(), $this->attributes[FE_ID], $form != null ? $form->recordId : 0, $htmlIdAppendix);
        $this->attributes[FE_HTML_ID] = $this->htmlAttributes[HTML_ATTR_ID];
        $this->htmlAttributes[HTML_ATTR_NAME] = HelperFormElement::buildFormElementName($this->attributes, $form != null ? $form->recordId : 0);

        $this->setValue();

        $this->handleFeMode();
        $this->handleLanguage();
        $this->handleFormEditorUrl();
        $this->handleDebugToolTip();
        $this->handleToggleInput();
    }

    /**
     * All values in $this->attributes are parsed (= "{{...}}" statements and variables are replaced).
     * Additionally, the value and note fields are checked for report syntax and processed
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    protected function handleEvaluate(): void {
        $this->attributes[FE_VALUE] = $this->processReportSyntax($this->attributes[FE_VALUE]);
        $this->attributes[FE_NOTE] = $this->processReportSyntax($this->attributes[FE_NOTE]);

        $this->attributes = $this->evaluate->parseArray($this->attributes, $this->skipEvaluationOnLoad, $this->debugStack);
    }

    /**
     * Throw an exception if name AND label of the FE are not set.
     * This method should be overridden by specific FE types that require both or none of the fields.
     *
     * @return void
     * @throws CodeException
     * @throws UserFormException
     */
    protected function handleEmptyNameOrLabel(): void {
        if ($this->allowEmptyNameAndLabel()) return;
        if ($this->attributes[FE_NAME] === '' && $this->attributes[FE_LABEL] === '') {
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($this->attributes), STORE_SYSTEM);
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'name, label', STORE_SYSTEM);
            throw new \UserFormException("Field 'name' and 'label' are empty", ERROR_NAME_LABEL_EMPTY);
        }
    }

    /**
     * If true then no expection will be thrown if the FE has empty name and label when loaded.
     * Is overridden by deriving class, e.g. Note FE.
     *
     * @return bool
     */
    protected function allowEmptyNameAndLabel(): bool {
        return false;
    }

    /**
     * Detects the language and adjusts the FE accordingly
     *
     * @return void
     */
    private function handleLanguage(): void {
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
        $this->attributes = HelperFormElement::setLanguage($this->attributes, $parameterLanguageFieldName);
    }

    /**
     * set various htmlAttributes for this FE, depending on $this->attributes[FE_MODE]
     *
     * @param $cssDisable
     * @return void
     * @throws CodeException
     * @throws UserFormException
     */
    private function handleFeMode($cssDisable = true): void {

        HelperFormElement::getFeMode($this->attributes[FE_MODE], $hidden, $disabled, $required);

        // Required Mode
        if ($this->attributes[FE_MODE] == FE_MODE_REQUIRED) {
            Support::addAttributeToArray($this->htmlAttributes, F_FE_DATA_REQUIRED_ERROR, $this->attributes[F_FE_DATA_REQUIRED_ERROR]);
        }
        // Data Required Error
        if (empty($this->attributes[F_FE_DATA_MATCH_ERROR])) {
            $this->attributes[F_FE_DATA_REQUIRED_ERROR] = F_FE_DATA_REQUIRED_ERROR_DEFAULT;
        }

        switch ($this->attributes[FE_MODE]) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
            case FE_MODE_SHOW_REQUIRED:
                break;
            case FE_MODE_REQUIRED:
                Support::addAttributeToArray($this->htmlAttributes, FE_MODE_REQUIRED, FE_MODE_REQUIRED);
                break;
            case FE_MODE_READONLY:
                Support::addAttributeToArray($this->htmlAttributes, ATTRIBUTE_DISABLED, ATTRIBUTE_DISABLED);
                break;
            default:
                throw new \UserFormException("Unknown mode '" . $this->attributes[FE_MODE] . "'", ERROR_UNKNOWN_MODE);
                break;
        }

        // Attributes: data-...
        Support::addAttributeToArray($this->htmlAttributes, DATA_HIDDEN, $hidden);
        if ($cssDisable) Support::addAttributeToArray($this->htmlAttributes, DATA_DISABLED, $disabled);
        Support::addAttributeToArray($this->htmlAttributes, DATA_REQUIRED, $required);

        // Set $addClassRequired -> Renderer will add classes to label/input/note if the field is required.
        if (($this->attributes[FE_MODE] == FE_MODE_REQUIRED || $this->attributes[FE_MODE] == FE_MODE_SHOW_REQUIRED) &&
            $this->attributes[FE_INDICATE_REQUIRED] == '1') {
            $this->addClassRequired = HelperFormElement::getRequiredPositionClass($this->attributes[F_FE_REQUIRED_POSITION]);
        }
    }

    /**
     * @return void
     * @throws CodeException
     * @throws UserFormException
     */
    private function handleDebugAndLog(): void {
        // Preparation for Log, Debug
        $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($this->attributes), STORE_SYSTEM);
        $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $this->attributes[FE_ID], STORE_SYSTEM);
    }

    /**
     * Sets verious required key/value pairs on $this->attributes, which are required for LDAP functionality
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function handleFillStoreLdap(): void {
        if (isset($this->attributes[FE_FILL_STORE_LDAP]) || isset($this->attributes[FE_TYPEAHEAD_LDAP])) {
            $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES,
                F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN, F_TYPEAHEAD_LDAP_SEARCH_PREFETCH,
                F_TYPEAHEAD_LIMIT, F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF,
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
            $this->attributes = OnArray::copyArrayItemsIfNotAlreadyExist($this->form->specFinal, $this->attributes, $keyNames);
        } else {
            return; // no ldap enabled on this FE
        }

        if (isset($this->attributes[FE_FILL_STORE_LDAP])) {

            // Extract necessary elements
            $config = OnArray::getArrayItems($this->attributes, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]);

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN,
                FE_LDAP_SERVER . ',' . FE_LDAP_BASE_DN . ',' . FE_LDAP_SEARCH . ',' . FE_LDAP_ATTRIBUTES, STORE_SYSTEM);
            $config = $this->evaluate->parseArray($config);

            if ($this->attributes[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
                $config[SYSTEM_LDAP_1_RDN] = $this->store->getVar(SYSTEM_LDAP_1_RDN, STORE_SYSTEM);
                $config[SYSTEM_LDAP_1_PASSWORD] = $this->store->getVar(SYSTEM_LDAP_1_PASSWORD, STORE_SYSTEM);
            }

            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

    }

    /**
     * Fire FillStoreVar if given
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function handleFillStoreVar(): void {
        if (isset($this->attributes[FE_FILL_STORE_VAR])) {
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug
            //$this->store[FE_FILL_STORE_VAR] = $this->evaluate->parse($this->attributes[FE_FILL_STORE_VAR], ROW_EXPECT_0_1);
            //$this->store->appendToStore($this->attributes[FE_FILL_STORE_VAR], STORE_VAR);
            $this->store->appendToStore($this->evaluate->parse($this->attributes[FE_FILL_STORE_VAR], ROW_EXPECT_0_1), STORE_VAR);
        }
    }

    /**
     * Fire FE_SLAVE_ID if given.
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function handleSlaveId(): void {
        // If given, slaveId will be copied to STORE_VAR
        if (isset($this->attributes[FE_SLAVE_ID])) {
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
            $slaveId = Support::falseEmptyToZero($this->evaluate->parse($this->attributes[FE_SLAVE_ID]));
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
        }
    }

    /**
     * Set $this->value
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function setValue(): void {
        $storeUse = STORE_USE_DEFAULT;
        if (!is_array($this->attributes[FE_VALUE])) { // value might be an array (Template Group)
            $this->value = $this->attributes[FE_VALUE];
        } else {
            $this->value = '';
            $this->valueArray = $this->attributes[FE_VALUE];
        }

        if ($this->value === '') {
            // #2064 / Only take the column default, if the FE is a real table column.
            // #3426 / Dynamic Update: Inputs loose the new content and shows the old value.
            if ($this->store->getVar($this->attributes[FE_NAME], STORE_TABLE_COLUMN_TYPES) === false
                // #21474 / Do not get the default if 'emptyItemAtStart' is set and recordId=0 (mode new)
                || (isset($this->attributes[FE_EMPTY_ITEM_AT_START]) && ($this->form->recordId ?? 0) === 0)) {
                // Remove STORE_DEFAULT
                $storeUse = str_replace(STORE_TABLE_DEFAULT, '', $storeUse);
            }

            $sanitizeClass = $this->attributes[FE_CHECK_TYPE];
            //TODO: $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
            $this->value = $this->store->getVar($this->attributes[FE_NAME], $storeUse, $sanitizeClass, $foundInStore);

        }

        if ($this->attributes[FE_TYPE] === FE_TYPE_RADIO || $this->attributes[FE_TYPE] === FE_TYPE_CHECKBOX) {
            // When a default value is set for Radio in FE.value also check the Client Store.
            // If there is a Value given in the Client Store then this is a Dynamic Update and the Value sent From the Client should be used. #21440
            // Get the whole Client store since the value could be of type array and that breaks getVar(). #21489
            $client = $this->store->getStore(STORE_CLIENT);
            $key = $this->htmlAttributes[HTML_ATTR_NAME] ?? '';

            if (isset($client[$key])) {
                $clientValue = $client[$key];
                if (is_array($clientValue)) {
                    $clientValue = implode(',', $clientValue);
                }
                $clientValue = Sanitize::sanitize($clientValue, $this->attributes[FE_CHECK_TYPE]);
                $this->value = $clientValue;
            }
        }

        // Decrypt value if its encrypted
        if (EncryptDecrypt::checkForEncryptedValue($this->value)) {
            $encryptionKey = $this->store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
            $this->value = EncryptDecrypt::getPlaintext($this->value, $encryptionKey);
        }

        if ($this->attributes[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
            $this->value = Support::htmlEntityEncodeDecode(MODE_DECODE, $this->value);
        } elseif ($this->attributes[FE_ENCODE] === FE_ENCODE_SINGLE_TICK) {
            $this->value = OnString::escapeSingleTickInHtml($this->value);
        }
    }


    /**
     * @param array|string $value
     *
     * @return array|string
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
     */
    protected function processReportSyntax($value, ?Report $report = null, ?BodytextParser $bodytextParser = null): array|string {

        if (is_array($value)) {
            $new = array();

            //might happen for e.g Template Groups
            foreach ($value as $item) {
                $new[] = $this->processReportSyntax($item, $report, $bodytextParser);
            }

            return $new;
        }

        $value = trim($value);
        if (substr($value, 0, 8) == SHEBANG_REPORT) {
            if ($report === null) {
                $report = new Report(array(), $this->evaluate, false);
            }

            if ($bodytextParser === null) {
                $bodytextParser = new BodytextParser();
            }

            $storeRecord = $this->store->getStore(STORE_RECORD);
            $value = $report->process($bodytextParser->process($value));
            $this->store->setStore($storeRecord, STORE_RECORD, true);
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
        }

        return $value;
    }

    /**
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit this FE.
     * Sets $this->formEditorUrl and $this->formEditorTooltip
     *
     * @return void
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    public function handleFormEditorUrl(): void {

        $param = [FE_FORM_ID => $this->attributes[FE_FORM_ID]];
        if (!$this->showDebugInfoFlag) {
            return;
        }

        $queryStringArray = [
            F_ID => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
            FORM_NAME_FORM => FORM_NAME_FORM_ELEMENT,
            CLIENT_RECORD_ID => $this->attributes[FE_ID],
            PARAM_DB_INDEX_DATA => $this->databaseManager->indexQfq
        ];

        if (T3Handler::useSlugsInsteadOfPageAlias()) {
            unset($queryStringArray['id']);
        }

        $queryStringArray = array_merge($queryStringArray, $param);

        $queryString = Support::arrayToQueryString($queryStringArray);

        if (T3Handler::useSlugsInsteadOfPageAlias()) {
            $queryString = Path::urlApp($this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM)) . '?' . $queryString;
        }

        $sip = $this->store->getSipInstance();
        $this->formEditorUrl = $sip->queryStringToSip($queryString);

        $formName = $this->form?->specFinal[F_NAME] ?? F_NAME_ONLY_FORM_ELEMENT;
        $this->formEditorTooltip = $formName . ' / ' . $this->attributes[FE_NAME] . ' (' . $this->attributes[FE_TYPE] . ') [' . $this->attributes[FE_ID] . ']';
    }

    private function handleDebugToolTip() {

        if ($this->showDebugInfoFlag && count($this->debugStack) > 0) {
            $debugInfo = Support::escapeDoubleTick(implode("\n", $this->debugStack), ESCAPE_WITH_HTML_QUOTE);
            $existingTooltip = $this->attributes[FE_TOOLTIP] ?? '';
            if (!empty($existingTooltip)) {
                // Prepend existing tooltip before debug info
                $this->attributes[FE_TOOLTIP] = $existingTooltip . "\n\n" . $debugInfo;
            } else {
                $this->attributes[FE_TOOLTIP] = $debugInfo;
            }
        }
    }

    protected function handleToggleInput(): void {
        if (!isset($this->initialAttributes[FE_INPUT_TOGGLE])) {
            return;
        }

        // Check for unsupported Types
        switch ($this->attributes[FE_TYPE]) {
            case FE_TYPE_CHECKBOX:
            case FE_TYPE_RADIO:
            case FE_TYPE_UPLOAD:
            case FE_TYPE_CHAT:
            case FE_TYPE_AI_CHAT:
            case FE_TYPE_IMAGE_CUT:
            case FE_TYPE_EDITOR:
                throw new UserFormException(FE_INPUT_TOGGLE . ' is not Implemented for FE.type = ' . $this->attributes[FE_TYPE]);
                break;
        }

        $toggleConfig = [FE_INPUT_TOGGLE_CONFIG_HIDDEN => true, FE_INPUT_TOGGLE_CONFIG_DEFAULT => '', FE_INPUT_TOGGLE_CONFIG_LABEL => ''];

        $hasValue = !empty($this->value);
        $hasCustomLabel = !empty($this->initialAttributes[FE_INPUT_TOGGLE]);

        // Set Label from param
        if ($hasCustomLabel) {
            $toggleConfig[FE_INPUT_TOGGLE_CONFIG_LABEL] = $this->attributes[FE_INPUT_TOGGLE];
        } 
        
        // Set a Default based on FE Type
        switch ($this->attributes[FE_TYPE]) {
            case FE_TYPE_DATE:
                // dd.mm.yyyy
                $toggleConfig[FE_INPUT_TOGGLE_CONFIG_DEFAULT] = '01.01.1000';
                break;
            case FE_TYPE_DATETIME:
                // dd.mm.yyyy HH:MM
                $toggleConfig[FE_INPUT_TOGGLE_CONFIG_DEFAULT] = '01.01.1000 00:00';
                break;
            case FE_TYPE_TIME:
                // HH:MM
                $toggleConfig[FE_INPUT_TOGGLE_CONFIG_DEFAULT] = '00:00';
                break;
            default:
                $toggleConfig[FE_INPUT_TOGGLE_CONFIG_DEFAULT] = '';
                break;
        }
        // set default via Store Default
        $columnDefault = $this->store->getVar($this->attributes[FE_NAME], STORE_DEFAULT, $this->attributes[FE_CHECK_TYPE]);

        // Already has a Value that is not the Default
        if ($hasValue && ($this->value !== $toggleConfig[FE_INPUT_TOGGLE_CONFIG_DEFAULT])) {
            $toggleConfig[FE_INPUT_TOGGLE_CONFIG_HIDDEN] = false;
        }


        $this->attributes[FE_INPUT_TOGGLE] = $toggleConfig;
    }

    /**
     * Resolve an AI parameter with extension-config -> FormElement inheritance: if the FE set
     * the key in its 'parameter' (PRESENT, even an empty value) that wins; otherwise fall back
     * to the extension config (STORE_SYSTEM). Presence-based, so 'aiPrompt=' on the FE clears
     * an inherited global prompt. The FE attribute key and the config key are the same string.
     *
     * @param string $key
     * @return string
     * @throws CodeException
     */
    protected function aiInheritedAttr(string $key): string {
        if (array_key_exists($key, $this->attributes)) {
            return (string)$this->attributes[$key];
        }
        return (string)$this->store->getVar($key, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
    }

    /**
     * Resolve the enable_thinking flag for an AI request. Off by default (editor assist
     * and chatbot alike). Turn it on globally with the 'aiThinking' extension-config
     * checkbox, or per FormElement with the 'aiThinking' parameter (presence-based,
     * tri-state: 1/yes/true/on = on, anything else = off). The FE parameter wins over the
     * global checkbox; an empty FE value falls back to the checkbox.
     *
     * @return bool
     * @throws CodeException
     */
    protected function aiThinkingEnabled(): bool {
        if (array_key_exists(FE_AI_THINKING, $this->attributes)) {
            $fe = strtolower(trim((string)$this->attributes[FE_AI_THINKING]));
            if ($fe !== '') {
                return in_array($fe, ['1', 'yes', 'true', 'on'], true);
            }
        }
        return (string)$this->store->getVar(SYSTEM_AI_THINKING, STORE_SYSTEM, SANITIZE_ALLOW_ALL) === '1';
    }

    /**
     * AI assist (parameter 'aiAssist'): pack the AI configuration of this FormElement
     * into a SIP and expose data attributes for the client - analog typeAhead.
     * The client can neither read nor manipulate prompt, context SQL or model.
     *
     * No-op if the FE has no 'aiAssist' parameter, the element is readonly or AI is
     * disabled/unconfigured system wide (the form keeps working without AI).
     *
     * @return void
     * @throws CodeException
     * @throws UserFormException
     */
    protected function handleAiAssist(bool $extraButton = true): void {

        $modes = trim($this->attributes[FE_AI_ASSIST] ?? '');
        if ($modes === '' || $this->attributes[FE_MODE] == FE_MODE_READONLY) {
            return;
        }

        if (!AiService::isEnabled()) {
            return;
        }

        $modeList = array();
        foreach (explode(',', $modes) as $mode) {
            $mode = trim($mode);
            if (!in_array($mode, [AI_MODE_IMPROVE, AI_MODE_GENERATE, AI_MODE_TRANSLATE], true)) {
                throw new UserFormException("Unknown value in '" . FE_AI_ASSIST . "': " . $mode);
            }
            $modeList[] = $mode;
        }
        $modes = implode(',', $modeList);

        // Normalize the offered translate targets. First entry is the default.
        $targetLanguages = implode(',', array_filter(array_map('trim',
            explode(',', $this->attributes[FE_AI_TARGET_LANGUAGE] ?? ''))));
        if ($targetLanguages === '' && in_array(AI_MODE_TRANSLATE, $modeList, true)) {
            $targetLanguages = AI_TRANSLATE_LANGUAGES_DEFAULT;
        }

        // Prompt and context SQL are base64 encoded: they might contain '&' or '='
        // which would break the SIP query string parsing.
        $arr = [
            FE_AI_ASSIST => $modes,
            SIP_PREFIX_BASE64 . FE_AI_PROMPT => base64_encode(trim($this->aiInheritedAttr(FE_AI_PROMPT))),
            // aiContext is already QFQ-evaluated (any {{SELECT ...}} ran at render); pass the result.
            SIP_PREFIX_BASE64 . FE_AI_CONTEXT => base64_encode(trim($this->attributes[FE_AI_CONTEXT] ?? '')),
            FE_AI_MODEL => trim($this->attributes[FE_AI_MODEL] ?? ''),
            FE_AI_TARGET_LANGUAGE => $targetLanguages,
            // Thinking off by default (saves tokens); enable via the aiThinking
            // global checkbox or the per-FE aiThinking parameter.
            FE_AI_THINKING => $this->aiThinkingEnabled() ? '1' : '0',
        ];

        // Optional: also feed context from the RAG source into the AI action (same
        // retrieval + per-user ACL as the aiChat type). Only active if 'aiRagSource'
        // is set and RAG is configured - the AI action keeps working without it.
        $ragSources = array_values(array_filter(array_map('trim', explode(',', $this->attributes[FE_AI_RAG_SOURCE] ?? ''))));
        if (!empty($ragSources) && RagService::isEnabled()) {
            $ragAcl = trim($this->attributes[FE_AI_RAG_ACL] ?? AI_RAG_ACL_USER);
            $ragProjects = trim($this->attributes[FE_AI_RAG_PROJECT] ?? '');
            // Validate the ACL grammar (modes, source references, off-needs-projects).
            RagService::validateAcl($ragSources, $ragAcl, array_filter(array_map('trim', explode(',', $ragProjects))));
            $arr[FE_AI_RAG_SOURCE] = implode(',', $ragSources);
            $arr[FE_AI_RAG_ACL] = $ragAcl;
            $arr[FE_AI_RAG_PROJECT] = $ragProjects;
            $arr[FE_AI_RAG_K] = (string)((int)($this->attributes[FE_AI_RAG_K] ?? AI_RAG_DEFAULT_K));
        }

        $this->htmlAttributes[DATA_AI_SIP] = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP);
        $this->htmlAttributes[DATA_AI_ASSIST] = $modes;
        if ($targetLanguages !== '' && in_array(AI_MODE_TRANSLATE, $modeList, true)) {
            $this->htmlAttributes[DATA_AI_TRANSLATE_LANGUAGES] = $targetLanguages;
        }
        $this->htmlAttributes[DATA_AI_URL] = Path::urlApi(API_AI_HANDLER_PHP);
        // Expose whether thinking is on so the client can label the running box ("Thinking…"
        // vs "Generating…"); the actual setting lives (authoritatively) in the SIP.
        $this->htmlAttributes[DATA_AI_THINKING] = $this->aiThinkingEnabled() ? '1' : '0';
        array_push($this->cssClasses, CLASS_AI_ASSIST);

        // Normalized values for HelperFormElement::prepareExtraButton, which renders
        // the 'AI' dropdown in the extra button area (not for TinyMCE - there the
        // actions live in the editor toolbar, hence $extraButton=false).
        $this->attributes[FE_AI_ASSIST] = $modes;
        $this->attributes[FE_AI_TARGET_LANGUAGE] = $targetLanguages;
        if ($extraButton) {
            $this->attributes[FE_TMP_AI_ASSIST_ACTIVE] = '1';
        }
    }
}