<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * HtpAi - KI-Steuerung (zentraler MCP-Orchestrator, Schritt 1: API-Modus).
 *
 * Wird ausschliesslich ueber die signierten Befehle ai.provision / ai.revoke / ai.status aufgerufen
 * (HtpLink::dispatch -> runAi). Legt MINIMAL-berechtigte Joomla-Dienst-User + API-Token an (bzw. nimmt
 * sie zurueck), damit die zentrale KI-Ebene die Web-Services-API dieser Website nutzen kann. Das Token
 * verlaesst die Site NUR verschluesselt-serverseitig ueber HtpLink::sendAiReport - nie ueber den Browser.
 *
 * ZWEI STUFEN (params.tier):
 *  - Stufe 1 (Standard): Konto 'htprotect_ai' in einer eng geschnittenen Gruppe (parent Public) mit
 *    core.login.api + com_content-Rechten. KEIN Super-User -> ein Leak ist Defacement, kein RCE.
 *  - Stufe 2 (RCE-Klasse, nur auf ausdruecklichen per-Site-Opt-in): ZUSAETZLICH ein GETRENNTES Konto
 *    'htprotect_ai_admin' in einer Gruppe, die KIND der Administrator-Gruppe (7) ist - erbt deren
 *    Rechte (Extensions verwalten), ist aber KEIN Super-User (core.admin bleibt Gruppe 8; keine
 *    Global-Config, keine Super-User-Zuweisung). Eigenes, kurzlebiges Token (server-seitig befristet).
 *
 * NUR Joomla 4+ (davor keine Web-Services-API). Token-Verfahren byte-genau wie Joomlas eigene Plugins
 * (plg_user_token + plg_api-authentication_token):
 *   token = base64("sha256:" . userId . ":" . hash_hmac('sha256', rawSeed32, $siteSecret))
 */

defined('_JEXEC') or die;

class HtpAi
{
    /** Stufe-1-Konto/-Gruppe (minimal). */
    const GROUP_TITLE   = 'HTProtect KI';
    const USER_USERNAME = 'htprotect_ai';
    const USER_NAME     = 'HTProtect KI';
    const USER_EMAIL    = 'htprotect-ai@htprotect.invalid';

    /** Stufe-2-Konto/-Gruppe (elevated, Kind von Administrator=7, aber KEIN Super-User). */
    const ADMIN_GROUP_TITLE   = 'HTProtect KI Admin';
    const ADMIN_USER_USERNAME = 'htprotect_ai_admin';
    const ADMIN_USER_NAME     = 'HTProtect KI Admin';
    const ADMIN_USER_EMAIL    = 'htprotect-ai-admin@htprotect.invalid';

    /** Joomla-Standard-Gruppen-IDs. */
    const GROUP_PUBLIC = 1;
    const GROUP_ADMIN  = 7; // Administrator (NICHT Super Users=8)

    // ─────────────────────────────────────────────────────────────────────────
    //  Oeffentliche Aktionen (von HtpLink::runAi aufgerufen)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Freischalten (bzw. Token rotieren). tier=2 legt ZUSAETZLICH das elevated Konto an.
     *
     * @return array{ok:bool,code:string,ai:array}
     */
    public static function provision($params)
    {
        if (self::jMajor() < 4) {
            return array('ok' => false, 'code' => 'ai_unsupported',
                'ai' => array('action' => 'error', 'reason' => 'Joomla 4 oder neuer erforderlich (Web-Services-API).'));
        }

        $tier = (int) (isset($params['tier']) ? $params['tier'] : 1);

        try {
            $db = htp_db();

            if (!$db) {
                throw new Exception('keine Datenbank');
            }

            self::enablePlugins($db); // Token-Auth + User-Token-Plugin (fuer beide Stufen noetig)

            // ── Stufe 1 (immer) ──
            $gid = self::ensureGroup($db, self::GROUP_TITLE, self::GROUP_PUBLIC);
            self::applyContentAcl($db, $gid);
            self::allowGroupForTokens($db, $gid);
            $uid   = self::ensureUser($db, $gid, self::USER_USERNAME, self::USER_NAME, self::USER_EMAIL);
            $token = self::mintToken($db, $uid);
            self::setApiAllow(true);

            $ai = array(
                'action'          => 'enroll',
                'tier'            => 1,
                'token'           => $token,
                'api_base'        => self::apiBase(),
                'joomla_uid'      => $uid,
                'joomla_group_id' => $gid,
            );

            // ── Stufe 2 (nur auf ausdruecklichen Wunsch) ──
            if ($tier >= 2) {
                $agid = self::ensureGroup($db, self::ADMIN_GROUP_TITLE, self::GROUP_ADMIN); // Kind von Administrator
                self::applyAdminAcl($db, $agid);
                self::allowGroupForTokens($db, $agid);
                $auid   = self::ensureUser($db, $agid, self::ADMIN_USER_USERNAME, self::ADMIN_USER_NAME, self::ADMIN_USER_EMAIL);
                $atoken = self::mintToken($db, $auid);

                $ai['tier']            = 2;
                $ai['admin_token']     = $atoken;
                $ai['admin_uid']       = $auid;
                $ai['admin_group_id']  = $agid;
            }

            return array('ok' => true, 'code' => $tier >= 2 ? 'ai_provisioned2' : 'ai_provisioned', 'ai' => $ai);
        } catch (Exception $e) {
            return self::fail($e->getMessage());
        } catch (Throwable $e) {
            return self::fail($e->getMessage());
        }
    }

    /**
     * Widerruf. tier=2 nimmt NUR das elevated Konto zurueck (Stufe 1 bleibt); sonst beide Konten +
     * /api-Freigabe. Konten werden gesperrt (nicht geloescht), damit Inhalts-Zuordnung erhalten bleibt.
     *
     * @return array{ok:bool,code:string,ai:array}
     */
    public static function revoke($params)
    {
        $tier = (int) (isset($params['tier']) ? $params['tier'] : 1);

        try {
            $db = htp_db();

            if (!$db) {
                throw new Exception('keine Datenbank');
            }

            if ($tier >= 2) {
                $rmAdmin = self::retireUser($db, self::ADMIN_USER_USERNAME);

                return array('ok' => true, 'code' => 'ai_revoked2',
                    'ai' => array('action' => 'revoke', 'tier' => 2, 'removed' => $rmAdmin));
            }

            // Voll: beide Konten loeschen (wenn sicher) bzw. sperren + Token weg, /api-Freigabe zuruecknehmen.
            $rmUser  = self::retireUser($db, self::USER_USERNAME);
            $rmAdmin = self::retireUser($db, self::ADMIN_USER_USERNAME);
            self::setApiAllow(false);

            return array('ok' => true, 'code' => 'ai_revoked',
                'ai' => array('action' => 'revoke', 'tier' => 1, 'removed' => array('user' => $rmUser, 'admin' => $rmAdmin)));
        } catch (Exception $e) {
            return self::fail($e->getMessage());
        } catch (Throwable $e) {
            return self::fail($e->getMessage());
        }
    }

    /**
     * Leichter Status: bestaetigt Version/Erreichbarkeit; der Relay stempelt daraufhin last_call.
     *
     * @return array{ok:bool,code:string,ai:array}
     */
    public static function status($params)
    {
        if (self::jMajor() < 4) {
            return array('ok' => false, 'code' => 'ai_unsupported',
                'ai' => array('action' => 'error', 'reason' => 'Joomla 4 oder neuer erforderlich.'));
        }

        return array('ok' => true, 'code' => 'ai_status', 'ai' => array('action' => 'status', 'api_base' => self::apiBase()));
    }

    // ─────────────────────────────────────────────────────────────────────────
    //  Bausteine
    // ─────────────────────────────────────────────────────────────────────────

    /** Gruppe finden oder ueber die Table-API anlegen (nested-set-sicher, KEIN Roh-INSERT). */
    private static function ensureGroup($db, $title, $parentId)
    {
        $db->setQuery(
            'SELECT ' . $db->quoteName('id') . ' FROM ' . $db->quoteName('#__usergroups')
            . ' WHERE ' . $db->quoteName('title') . ' = ' . $db->quote($title)
        );
        $gid = (int) $db->loadResult();

        if ($gid > 0) {
            return $gid;
        }

        if (!class_exists('Joomla\\CMS\\Table\\Table')) {
            throw new Exception('Table-API nicht verfuegbar');
        }

        $tbl = \Joomla\CMS\Table\Table::getInstance('Usergroup');

        if (!$tbl) {
            throw new Exception('Usergroup-Table nicht verfuegbar');
        }

        $tbl->bind(array('parent_id' => (int) $parentId, 'title' => $title));

        if (!$tbl->check() || !$tbl->store()) {
            throw new Exception('Gruppe konnte nicht angelegt werden: ' . $title);
        }

        return (int) $tbl->id;
    }

    /**
     * Stufe-1-ACL: core.login.api (Root) + Inhalts-/Struktur-Rechte fuer die Gruppe. Bewusst auf die
     * Inhaltspflege begrenzt (Artikel, Kategorien, Tags, Menuepunkte, Module, Medien) - KEIN
     * com_users/com_config/com_installer. Ein Leak dieses Tokens bleibt damit auf Defacement begrenzt,
     * nie Rechteausweitung. Alle diese Bereiche werden ueber die Joomla-Web-Services-API bedient.
     */
    private static function applyContentAcl($db, $gid)
    {
        self::grantOnAsset($db, 1, null, array('core.login.api'), $gid);

        $content = array('core.create', 'core.edit', 'core.edit.state', 'core.delete');
        foreach (array('com_content', 'com_categories', 'com_tags', 'com_menus', 'com_modules', 'com_media') as $component) {
            self::grantOnAsset($db, 0, $component, $content, $gid);
        }

        // com_menus zusaetzlich core.manage: die Menuepunkt-LISTE der Web-Services-API laedt bei
        // leerem menutype-Filter nur Menues, auf die der Nutzer core.manage hat - sonst kommt eine
        // leere Liste zurueck (list_menu_items = 0). Ohne dies waeren Menuepunkte praktisch unlesbar.
        self::grantOnAsset($db, 0, 'com_menus', array('core.manage'), $gid);
    }

    /**
     * Stufe-2-ACL: NUR core.login.api auf dem Root. Die eigentlichen Verwaltungsrechte erbt die Gruppe
     * als KIND der Administrator-Gruppe (parent 7) - inklusive Extensions verwalten, ABER OHNE core.admin
     * (Super-User) und damit ohne Global-Config/Super-User-Zuweisung.
     */
    private static function applyAdminAcl($db, $gid)
    {
        self::grantOnAsset($db, 1, null, array('core.login.api'), $gid);
    }

    /** Ein #__assets.rules-JSON um {action:{gid:1}} ergaenzen (per id ODER name). */
    private static function grantOnAsset($db, $id, $name, $actions, $gid)
    {
        $where = $id > 0
            ? ($db->quoteName('id') . ' = ' . (int) $id)
            : ($db->quoteName('name') . ' = ' . $db->quote((string) $name));

        $db->setQuery('SELECT ' . $db->quoteName('id') . ', ' . $db->quoteName('rules') . ' FROM ' . $db->quoteName('#__assets') . ' WHERE ' . $where);
        $row = $db->loadObject();

        if (!$row) {
            return; // Asset fehlt (untypisch) -> ueberspringen statt scheitern
        }

        $rules = json_decode((string) $row->rules, true);

        if (!is_array($rules)) {
            $rules = array();
        }

        foreach ($actions as $a) {
            if (!isset($rules[$a]) || !is_array($rules[$a])) {
                $rules[$a] = array();
            }

            $rules[$a][(string) $gid] = 1;
        }

        $db->setQuery(
            'UPDATE ' . $db->quoteName('#__assets') . ' SET ' . $db->quoteName('rules') . ' = ' . $db->quote(json_encode($rules))
            . ' WHERE ' . $db->quoteName('id') . ' = ' . (int) $row->id
        );
        $db->execute();
    }

    /** Die beiden noetigen Core-Plugins aktivieren (nur enabled=1 flippen, kein Installer). */
    private static function enablePlugins($db)
    {
        self::enablePlugin($db, 'api-authentication', 'token'); // Verifizierer des X-Joomla-Tokens
        self::enablePlugin($db, 'user', 'token');               // liefert allowedUserGroups
    }

    private static function enablePlugin($db, $folder, $element)
    {
        $db->setQuery(
            'UPDATE ' . $db->quoteName('#__extensions') . ' SET ' . $db->quoteName('enabled') . ' = 1'
            . ' WHERE ' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
            . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote($folder)
            . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote($element)
        );
        $db->execute();
    }

    /**
     * plg_user_token->allowedUserGroups um die Gruppe erweitern. KRITISCH: Default ist nur [8]
     * (Super Users) - ohne diesen Schritt lehnt der Token-Verifizierer den Nicht-Super-User ab.
     * Gruppe 8 bleibt drin (sonst verloeren Super-User ihre eigenen API-Token).
     */
    private static function allowGroupForTokens($db, $gid)
    {
        $db->setQuery(
            'SELECT ' . $db->quoteName('extension_id') . ', ' . $db->quoteName('params')
            . ' FROM ' . $db->quoteName('#__extensions')
            . ' WHERE ' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
            . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('user')
            . ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('token')
        );
        $row = $db->loadObject();

        if (!$row) {
            return;
        }

        $params = json_decode((string) $row->params, true);

        if (!is_array($params)) {
            $params = array();
        }

        $groups = (isset($params['allowedUserGroups']) && is_array($params['allowedUserGroups']))
            ? array_map('intval', $params['allowedUserGroups'])
            : array(8);

        if (!in_array((int) $gid, $groups, true)) {
            $groups[] = (int) $gid;
        }

        if (!in_array(8, $groups, true)) {
            $groups[] = 8;
        }

        $params['allowedUserGroups'] = array_values(array_unique($groups));

        $db->setQuery(
            'UPDATE ' . $db->quoteName('#__extensions') . ' SET ' . $db->quoteName('params') . ' = ' . $db->quote(json_encode($params))
            . ' WHERE ' . $db->quoteName('extension_id') . ' = ' . (int) $row->extension_id
        );
        $db->execute();
    }

    /** Dienst-User finden + entsperren/zuweisen, oder neu anlegen (via User-API, Passwort wird gehasht). */
    private static function ensureUser($db, $gid, $username, $name, $email)
    {
        $uid = self::findUserId($db, $username);

        if (!class_exists('Joomla\\CMS\\User\\User')) {
            throw new Exception('User-API nicht verfuegbar');
        }

        if ($uid > 0) {
            $user = new \Joomla\CMS\User\User($uid);
            $changed = false;

            if ((int) $user->block !== 0) {
                $user->block = 0;
                $changed = true;
            }

            $groups = is_array($user->groups) ? array_map('intval', $user->groups) : array();

            if (!in_array((int) $gid, $groups, true)) {
                $groups[] = (int) $gid;
                $user->groups = array_values(array_unique($groups));
                $changed = true;
            }

            if ($changed && !$user->save()) {
                throw new Exception('Dienst-User konnte nicht aktualisiert werden');
            }

            return $uid;
        }

        $user = new \Joomla\CMS\User\User();
        $pw = bin2hex(random_bytes(24)); // nie fuer Login genutzt (API-Only), aber Pflichtfeld

        // WICHTIG: User::bind() nimmt $array BY REFERENCE (hasht u. a. das Passwort in-place). Ein
        // Array-LITERAL wirft daher unter PHP 8 "could not be passed by reference" - also erst in eine
        // Variable, dann binden.
        $data = array(
            'name'         => $name,
            'username'     => $username,
            'email'        => $email,
            'password'     => $pw,
            'password2'    => $pw,
            'block'        => 0,
            'requireReset' => 0,
            'groups'       => array((int) $gid),
        );
        $bound = $user->bind($data);

        if (!$bound) {
            throw new Exception('Dienst-User konnte nicht gebunden werden');
        }

        if (!$user->save()) {
            $err = method_exists($user, 'getError') ? (string) $user->getError() : '';
            throw new Exception('Dienst-User konnte nicht angelegt werden' . ($err !== '' ? ': ' . $err : ''));
        }

        return (int) $user->id;
    }

    /**
     * Token minten: Seed erzeugen, base64 in #__user_profiles ablegen (joomlatoken.token/.enabled),
     * und das praesentierbare Token nach Joomlas exaktem Verfahren berechnen. Rotiert bei jedem Aufruf.
     */
    private static function mintToken($db, $uid)
    {
        $secret = self::siteSecret();

        if ($secret === '') {
            throw new Exception('Site-Secret nicht lesbar');
        }

        $seed = random_bytes(32);
        $b64  = base64_encode($seed);

        self::deleteTokenRows($db, $uid); // alte Rotation entfernen

        $db->setQuery(
            'INSERT INTO ' . $db->quoteName('#__user_profiles')
            . ' (' . $db->quoteName('user_id') . ', ' . $db->quoteName('profile_key') . ', ' . $db->quoteName('profile_value') . ', ' . $db->quoteName('ordering') . ') VALUES'
            . ' (' . (int) $uid . ', ' . $db->quote('joomlatoken.token') . ', ' . $db->quote($b64) . ', 1),'
            . ' (' . (int) $uid . ', ' . $db->quote('joomlatoken.enabled') . ', ' . $db->quote('1') . ', 2)'
        );
        $db->execute();

        // hash_hmac OHNE 4. Arg -> HEX (byte-identisch zu Joomlas getTokenForDisplay()).
        $hmac = hash_hmac('sha256', $seed, $secret);

        return base64_encode('sha256:' . (int) $uid . ':' . $hmac);
    }

    private static function deleteTokenRows($db, $uid)
    {
        $db->setQuery(
            'DELETE FROM ' . $db->quoteName('#__user_profiles')
            . ' WHERE ' . $db->quoteName('user_id') . ' = ' . (int) $uid
            . ' AND ' . $db->quoteName('profile_key') . ' IN (' . $db->quote('joomlatoken.token') . ', ' . $db->quote('joomlatoken.enabled') . ')'
        );
        $db->execute();
    }

    /**
     * KI-Dienstkonto zuruecknehmen. Token-Profile IMMER weg. Danach:
     *   - besitzt das Konto NICHTS (kein created_by/modified_by in Kern-Inhalten) -> KOMPLETT LOESCHEN
     *     (Zero-Footprint, sauber ueber die Joomla-User-API: raeumt Gruppen-Map + via user-Plugins
     *     Profile/Keys mit ab),
     *   - besitzt es Inhalte (Artikel/Kategorien/Tags/Kontakte) ODER laesst es sich nicht sauber loeschen
     *     -> STILLLEGEN (block=1), damit keine Autor-Verknuepfung verwaist.
     * Idempotent; no-op wenn der User nicht existiert.
     *
     * @return string 'absent' | 'deleted' | 'blocked'
     */
    private static function retireUser($db, $username)
    {
        $uid = self::findUserId($db, $username);

        if ($uid <= 0) {
            return 'absent';
        }

        self::deleteTokenRows($db, $uid);

        // Nur loeschen, wenn wirklich nichts daran haengt (sonst waeren Inhalte ohne Autor).
        if (!self::ownsContent($db, $uid) && self::deleteUserAccount($uid)) {
            return 'deleted';
        }

        $db->setQuery(
            'UPDATE ' . $db->quoteName('#__users') . ' SET ' . $db->quoteName('block') . ' = 1'
            . ' WHERE ' . $db->quoteName('id') . ' = ' . (int) $uid
        );
        $db->execute();

        return 'blocked';
    }

    /**
     * Besitzt der User Inhalte, deren Autor-Verknuepfung beim Loeschen verwaisen wuerde? Prueft die
     * Kern-Tabellen mit einem created_by/created_user_id-Feld (Artikel/Kategorien/Tags/Kontakte - genau
     * das, was die KI-Schreibwerkzeuge anlegen). FAIL-SAFE: bei jeder Unsicherheit (Query-Fehler) true,
     * damit im Zweifel gesperrt statt geloescht wird - lieber ein inaktives Konto als verwaiste Inhalte.
     *
     * @return boolean
     */
    private static function ownsContent($db, $uid)
    {
        $uid    = (int) $uid;
        $checks = array(
            array('#__content',         array('created_by', 'modified_by')),
            array('#__categories',      array('created_user_id', 'modified_user_id')),
            array('#__tags',            array('created_user_id')),
            array('#__contact_details', array('created_by', 'modified_by')),
        );

        foreach ($checks as $c) {
            $conds = array();

            foreach ($c[1] as $col) {
                $conds[] = $db->quoteName($col) . ' = ' . $uid;
            }

            try {
                $db->setQuery(
                    'SELECT COUNT(*) FROM ' . $db->quoteName($c[0]) . ' WHERE ' . implode(' OR ', $conds)
                );

                if ((int) $db->loadResult() > 0) {
                    return true;
                }
            } catch (Exception $e) {
                return true; // Tabelle/Spalte unklar -> konservativ als "besitzt" behandeln
            } catch (Throwable $e) {
                return true;
            }
        }

        return false;
    }

    /**
     * Konto sauber ueber die Joomla-User-API loeschen (feuert user-Plugins -> raeumt Profile/Keys/Notes;
     * die User-Table entfernt die Gruppen-Zuordnung). Gibt false zurueck, wenn kein sauberes Loeschen
     * moeglich ist (dann sperrt der Aufrufer) - z. B. Joomla verweigert das Loeschen des letzten
     * Super-Users. NIE Roh-DELETE auf #__users (haette verwaiste Satelliten-Zeilen zur Folge).
     *
     * @return boolean
     */
    private static function deleteUserAccount($uid)
    {
        try {
            if (!class_exists('Joomla\\CMS\\User\\User')) {
                return false;
            }

            if (class_exists('Joomla\\CMS\\Plugin\\PluginHelper')) {
                \Joomla\CMS\Plugin\PluginHelper::importPlugin('user');
            }

            $u = new \Joomla\CMS\User\User((int) $uid);

            if ((int) $u->id !== (int) $uid) {
                return false; // konnte nicht geladen werden
            }

            return (bool) $u->delete();
        } catch (Exception $e) {
            return false;
        } catch (Throwable $e) {
            return false;
        }
    }

    private static function findUserId($db, $username)
    {
        $db->setQuery(
            'SELECT ' . $db->quoteName('id') . ' FROM ' . $db->quoteName('#__users')
            . ' WHERE ' . $db->quoteName('username') . ' = ' . $db->quote($username)
        );

        return (int) $db->loadResult();
    }

    /** /api-Freigabe in der eigenen Haertung setzen/zuruecknehmen + Schild best-effort neu schreiben. */
    private static function setApiAllow($on)
    {
        try {
            if (class_exists('HtpConfig')) {
                HtpConfig::saveKeys(array('ai_api_allow' => $on ? 1 : 0));
            }
        } catch (Exception $e) {
        } catch (Throwable $e) {
        }

        try {
            if (class_exists('HtpApp') && method_exists('HtpApp', 'selfHealShield')) {
                HtpApp::selfHealShield();
            }
        } catch (Exception $e) {
        } catch (Throwable $e) {
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    //  Kleinhelfer
    // ─────────────────────────────────────────────────────────────────────────

    private static function jMajor()
    {
        $v = function_exists('htp_j_version') ? (string) htp_j_version() : (defined('JVERSION') ? JVERSION : '');
        $parts = explode('.', $v);

        return (int) (isset($parts[0]) ? $parts[0] : 0);
    }

    /** Oeffentliche API-Basis dieser Site (kein SEF): <site>/api/index.php/v1. */
    private static function apiBase()
    {
        $u = function_exists('htp_site_url') ? rtrim((string) htp_site_url(), '/') : '';

        return $u === '' ? '' : ($u . '/api/index.php/v1');
    }

    /** Site-Secret ($secret aus configuration.php) - Schluessel des Token-HMAC. */
    private static function siteSecret()
    {
        try {
            if (class_exists('Joomla\\CMS\\Factory')) {
                $app = \Joomla\CMS\Factory::getApplication();

                if ($app && method_exists($app, 'get')) {
                    $s = (string) $app->get('secret');

                    if ($s !== '') {
                        return $s;
                    }
                }
            }
        } catch (Exception $e) {
        } catch (Throwable $e) {
        }

        try {
            if (class_exists('JFactory')) {
                $cfg = JFactory::getConfig();

                if ($cfg && method_exists($cfg, 'get')) {
                    $s = (string) $cfg->get('secret');

                    if ($s !== '') {
                        return $s;
                    }
                }
            }
        } catch (Exception $e) {
        } catch (Throwable $e) {
        }

        return '';
    }

    private static function fail($reason)
    {
        $reason = trim((string) $reason);

        if ($reason === '') {
            $reason = 'unbekannter Fehler';
        }

        return array('ok' => false, 'code' => 'ai_error', 'ai' => array('action' => 'error', 'reason' => substr($reason, 0, 190)));
    }
}
