<?php

namespace IMATHUZH\Qfq\Core\Form\FormElement;

use CodeException;
use IMATHUZH\Qfq\Core\Ai\AiService;
use IMATHUZH\Qfq\Core\Ai\RagService;
use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Path;
use UserFormException;

/**
 * Class AiChatFormElement
 *
 * Native FormElement type 'aiChat': a RAG chatbot widget. The retrieval config
 * (source, ACL, projects, k, prompt, model) is packed into a SIP; the client only
 * ever sees an opaque token and turns the element into a chat box (AiChat.js).
 *
 * The model never gets DB access. Retrieval and per-user ACL happen server side in
 * the RAG service - the login is resolved from the session, never from the client.
 *
 * @package qfq
 */
class AiChatFormElement extends AbstractFormElement {

    /**
     * @param array $attributes
     * @param Form|null $form
     * @throws CodeException
     * @throws UserFormException
     * @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 \UserReportException
     */
    public function __construct(array $attributes, ?Form $form = null) {
        parent::__construct($attributes, $form);
        $this->prepareChat();
    }

    /**
     * Pack the retrieval config into a SIP and expose the data attributes the client
     * needs to build the chat widget. No-op (the field renders empty) if the element
     * is readonly or AI/RAG are not configured, so the form keeps working without the
     * chatbot.
     *
     * @return void
     * @throws CodeException
     * @throws UserFormException
     */
    protected function prepareChat(): void {

        if ($this->attributes[FE_MODE] == FE_MODE_READONLY) {
            return;
        }
        // The LLM is required for the chat; RAG (document retrieval) is optional.
        if (!AiService::isEnabled()) {
            return;
        }

        // Config -> FormElement inheritance (FE.parameter overrides by presence). An empty
        // aiRagSource (no config default, or 'aiRagSource=' on the FE) means: no document
        // retrieval - the chatbot answers as a plain LLM.
        $sources = array_values(array_filter(array_map('trim', explode(',', $this->aiInheritedAttr(FE_AI_RAG_SOURCE)))));
        $acl = trim($this->aiInheritedAttr(FE_AI_RAG_ACL)) ?: AI_RAG_ACL_USER;
        $projects = trim($this->attributes[FE_AI_RAG_PROJECT] ?? '');   // FE-only (project scoping is FE specific)
        $k = (int)$this->aiInheritedAttr(FE_AI_RAG_K) ?: AI_RAG_DEFAULT_K;
        // Default OFF: only an explicit truthy value enables it (booleParameter() treats '' as
        // true, which is the wrong default here).
        $allowGeneral = in_array(strtolower(trim($this->aiInheritedAttr(FE_AI_CHAT_ALLOW_GENERAL))), ['1', 'yes', 'true', 'on'], true);
        // Thinking off by default; enable via the aiThinking global checkbox or per-FE param.
        $thinking = $this->aiThinkingEnabled();

        if (!empty($sources)) {
            if (!RagService::isEnabled()) {
                throw new UserFormException("'" . FE_AI_RAG_SOURCE . "' is set but the retrieval service is not configured ('" . SYSTEM_AI_RAG_URL . "')");
            }
            // Validate the ACL grammar (modes, source references, off-needs-projects).
            RagService::validateAcl($sources, $acl, array_filter(array_map('trim', explode(',', $projects))));
        }

        // FE_TYPE_AI_CHAT => '1' is the server side marker the AiHandler validates before
        // treating the request as a chat. The prompt is base64 encoded because it might
        // contain '&' or '=' which would break the SIP query string parsing.
        $arr = [
            FE_TYPE_AI_CHAT => '1',
            FE_AI_RAG_SOURCE => implode(',', $sources),   // may be '' -> no document retrieval
            FE_AI_RAG_ACL => $acl,
            FE_AI_RAG_PROJECT => $projects,
            FE_AI_RAG_K => (string)$k,
            FE_AI_CHAT_ALLOW_GENERAL => $allowGeneral ? '1' : '',
            FE_AI_THINKING => $thinking ? '1' : '0',
            FE_AI_MODEL => trim($this->attributes[FE_AI_MODEL] ?? ''),
            SIP_PREFIX_BASE64 . FE_AI_PROMPT => base64_encode(trim($this->aiInheritedAttr(FE_AI_PROMPT))),
            // Optional developer-defined context (FE-only), like aiAssist: QFQ already evaluated
            // it at render time (any {{SELECT ...}} ran, {{feUser:T0}} substituted). base64
            // because it may contain '&'/'='. Works in both RAG and no-RAG (plain chat) mode.
            SIP_PREFIX_BASE64 . FE_AI_CONTEXT => base64_encode(trim($this->attributes[FE_AI_CONTEXT] ?? '')),
        ];

        $this->htmlAttributes[DATA_AI_SIP] = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP);
        // Single AI endpoint for both JSON and SSE; the client adds _ai_stream=1 to stream.
        $this->htmlAttributes[DATA_AI_URL] = Path::urlApi(API_AI_HANDLER_PHP);
        // Expose whether thinking is available so the client can show the per-request toggle.
        $this->htmlAttributes[DATA_AI_THINKING] = $thinking ? '1' : '0';
        if (!empty($this->attributes[FE_PLACEHOLDER])) {
            $this->htmlAttributes[DATA_AI_CHAT_PLACEHOLDER] = $this->attributes[FE_PLACEHOLDER];
        }
        // Optional extra CSS class(es) for the chat container (FE parameter aiChatClass), e.g.
        // 'qfq-ai-chat-dock' to float the chat at the bottom-right of the viewport.
        $extraClass = trim($this->attributes[FE_AI_CHAT_CLASS] ?? '');
        if ($extraClass !== '') {
            array_push($this->cssClasses, $extraClass);
        }
        array_push($this->cssClasses, CLASS_AI_CHAT);
    }

    /**
     * A chat widget has no stored value and is usually identified by name only,
     * so an empty label must not raise the "name and label are empty" error.
     *
     * @return bool
     */
    protected function allowEmptyNameAndLabel(): bool {
        return true;
    }
}
