/*******************************************************************************
 * Copyright (c) 2025, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <algorithm>
#include <cstdlib>
#include <stack>

#include <authdb/client.h>
#include <authdb/user.h>

#include <htmlpp/html.h>
#include <uuidp.h>

#include "edit.h"
#include "plugin.h"

#include "blogi.h"
#include "theme.h"


blogi::webedit::WebEditor::WebEditor(PluginArgs &plgargs) : Args (plgargs){
    EditPlugin::setPrefix(Args.config->getprefix());
    std::vector<std::string> edplgdirs;
    for(size_t i=0; i<Args.config->getplgdirs(); ++i){
        edplgdirs.emplace_back(Args.config->getplgdir(i)+"/webedit");
    }
    Plugins=std::make_unique<EditPluginLoad>(edplgdirs);
}

#define GPO_EDITOR_PUBLISH    "f85e123e-596d-4cd2-85c0-5af5529f3526"
#define GPO_EDITOR_SHARE      "311ec70b-e7d8-4f46-ac76-ed5d2dd2102d"
#define GPO_EDITOR_TEMPLATES  "cb18e7f6-92e5-4193-a2a6-1fcd2c8930ff"
#define GPO_EDITOR_IMPORTEXPORT "f46a4fd3-a69a-4fb4-a908-c5890a244386"

void blogi::webedit::WebEditor::registerEditorGPOs(){
    if(Args.config->getAuthSourceCount() < 1)
        return;

    struct GpoDef { const char *id; const char *name; const char *desc; };
    GpoDef gpos[] = {
        {GPO_EDITOR_PUBLISH,      "Blogi: Publish Content",   "Controls access to publishing content from the editor"},
        {GPO_EDITOR_SHARE,        "Blogi: Share Posts",        "Controls access to sharing posts to social networks"},
        {GPO_EDITOR_TEMPLATES,    "Blogi: Manage Templates",   "Controls access to saving and loading editor templates"},
        {GPO_EDITOR_IMPORTEXPORT, "Blogi: Import/Export",      "Controls access to importing and exporting editor content"},
    };

    for(size_t i = 0; i < Args.config->getAuthSourceCount(); ++i){
        try {
            const AuthSource &src = Args.config->getAuthSource(i);
            authdb::client::ClientConnection authcon;
            authcon.setUrl(src.url);
            authcon.setClientName(src.clientName);
            authcon.setClientSecret(src.clientSecret);
            authdb::client::Client clt(authcon);
            if(clt.ClientAuth()){
                for(auto &g : gpos){
                    try {
                        uuid::uuid gid(g.id);
                        if(!clt.GPOexits(gid))
                            clt.GPOadd(gid, g.name, g.desc, true, {});
                    } catch(const std::exception &e) {
                        std::cerr << "Editor GPO registration failed for " << g.name << ": " << e.what() << std::endl;
                    }
                }
            }
        } catch(const std::exception &e) {
            std::cerr << "Editor GPO auth failed: " << e.what() << std::endl;
        } catch(...) {
            std::cerr << "Editor GPO auth failed" << std::endl;
        }
    }
}

bool blogi::webedit::WebEditor::checkEditorGPO(json_object *request, const char *gpoid){
    if(Args.config->getAuthSourceCount() < 1)
        return false;
    // Extract authid from request array
    size_t reqLen = json_object_array_length(request);
    for(size_t ri = 0; ri < reqLen; ++ri){
        json_object *reqItem = json_object_array_get_idx(request, ri);
        json_object *authObj = nullptr;
        if(json_object_object_get_ex(reqItem, "authid", &authObj)){
            const char *authIdStr = json_object_get_string(authObj);
            if(authIdStr && authIdStr[0] != '\0'){
                for(size_t si = 0; si < Args.config->getAuthSourceCount(); ++si){
                    const AuthSource &src = Args.config->getAuthSource(si);
                    try {
                        authdb::client::ClientConnection authcon;
                        authcon.setUrl(src.url);
                        authcon.setClientName(src.clientName);
                        authcon.setClientSecret(src.clientSecret);
                        authcon.setSessionID(uuid::uuid(authIdStr));
                        authdb::client::Client clt(authcon);
                        if(clt.ClientAuth() && clt.GPOcheck(uuid::uuid(gpoid))){
                            return true;
                        }
                    } catch(...) {
                        continue;
                    }
                }
            }
            break;
        }
    }
    return false;
}

void blogi::webedit::WebEditor::initTables(dbpp::Database &db) {
    dbpp::SQL sql;
    dbpp::DBResult res;

    // Table 1: editor_templates — stores template metadata
    sql << "CREATE TABLE IF NOT EXISTS editor_templates ("
        << "id " << db.getUUIDType(sql) << " PRIMARY KEY,"
        << "name character varying(255) NOT NULL,"
        << "description text DEFAULT '',"
        << "created timestamp DEFAULT CURRENT_TIMESTAMP,"
        << "updated timestamp DEFAULT CURRENT_TIMESTAMP"
        << ");";
    db.exec(sql, res);

    sql.clear();

    // Table 2: editor_template_revisions — stores XML data with revision history
    sql << "CREATE TABLE IF NOT EXISTS editor_template_revisions ("
        << "id integer PRIMARY KEY " << db.autoincrement(sql) << ","
        << "template_id " << db.getUUIDType(sql) << " NOT NULL,"
        << "revision integer NOT NULL DEFAULT 1,"
        << "xml_data text NOT NULL,"
        << "comment character varying(512) DEFAULT '',"
        << "created timestamp DEFAULT CURRENT_TIMESTAMP"
        << ");";
    db.exec(sql, res);
}


void blogi::webedit::WebEditor::Editor(libhttppp::HttpRequest& curreq, const int tid, const std::string &sessionid){
    auto string_to_lower =[](std::string s) {
        for (char &c : s) {
            c = std::tolower(static_cast<unsigned char>(c));
        }
        return s;
    };

    // --- Handle XML Template Save ---
    const std::string save_url = "editor/save";
    size_t saveurllen = Args.config->buildurl(save_url).length();
    if (curreq.getRequestURL().compare(0, saveurllen, Args.config->buildurl(save_url)) == 0) {
        saveDocumentXml(curreq);
        libhttppp::HttpResponse curres;
        curres.setState(HTTP200);
        curres.setContentType("application/json");
        const char *ok = "{\"status\":\"success\",\"message\":\"Document saved.\"}";
        curres.send(curreq, ok, strlen(ok));
        return;
    }

    // --- Handle XML Template Load ---
    const std::string load_url = "editor/load";
    size_t loadurllen = Args.config->buildurl(load_url).length();
    if (curreq.getRequestURL().compare(0, loadurllen, Args.config->buildurl(load_url)) == 0) {
        // Extract template name from URL: editor/load/<templatename>
        std::string templateName = "default";
        size_t start_pos = loadurllen;
        if (curreq.getRequestURL().length() > loadurllen && curreq.getRequestURL()[loadurllen] == '/') {
            start_pos++;
        }
        if (start_pos < curreq.getRequestURL().length()) {
            templateName = curreq.getRequestURL().substr(start_pos);
        }
        loadDocumentXml(templateName);
        libhttppp::HttpResponse curres;
        curres.setState(HTTP200);
        curres.setContentType("application/json");
        const char *ok = "{\"status\":\"success\",\"message\":\"Document loaded.\"}";
        curres.send(curreq, ok, strlen(ok));
        return;
    }

    // --- Handle XML Template Export (returns XML as response) ---
    const std::string export_url = "editor/export";
    size_t exporturllen = Args.config->buildurl(export_url).length();
    if (curreq.getRequestURL().compare(0, exporturllen, Args.config->buildurl(export_url)) == 0) {
        tinyxml2::XMLPrinter printer;
        CurDoc.Print(&printer);
        std::string xmlStr = printer.CStr();

        libhttppp::HttpResponse curres;
        curres.setState(HTTP200);
        curres.setContentType("application/xml");
        curres.send(curreq, xmlStr.data(), xmlStr.length());
        return;
    }

    // Determine the full base URL for plugin settings, e.g., "https://myblog.com/editor/elsettings"
    const std::string settings_base_url = "editor/elsettings";
    size_t urllen=Args.config->buildurl(settings_base_url).length();

    // Check if the current request URL starts with the settings base URL
    if(curreq.getRequestURL().compare(0,urllen,Args.config->buildurl(settings_base_url))==0){

        // Loop through all loaded plugins to check for a name match in the URL segment
        for(const EditPluginLoad::PluginData *curplg=Plugins->getFirstPlugin(); curplg; curplg=curplg->getNextPlg() ){
            // Get the plugin name (e.g., "Container" or "Div")
            std::string pname=string_to_lower(curplg->getInstace()->getName());
            size_t start_pos = urllen;

            if (curreq.getRequestURL().length() > urllen && curreq.getRequestURL()[urllen] == '/') {
                start_pos++;
            }

            if(curreq.getRequestURL().compare(start_pos, pname.length(), pname) == 0){
                libhttppp::HttpResponse curres;
                // Initialize HtmlElement with a tag, e.g., "div"
                libhtmlpp::HtmlElement edithtml("div");
                libhtmlpp::HtmlString out;

                // Call the plugin's Settings method to generate the UI content
                curplg->getInstace()->Settings(curreq,edithtml);

                // Serialize the generated HTML
                libhtmlpp::print(edithtml,out,false);

                // Send the response and return, bypassing the main editor render
                curres.setState(HTTP200);
                curres.setContentType("text/html");
                curres.send(curreq,out.data(),out.length());
                return;
            }
        }
    }
    libhtmlpp::HtmlElement edithtml("div");
    libhtmlpp::HtmlPage editpage;
    libhtmlpp::HtmlString ouput;

    // Initialize XML document root if not set
    if (!CurRootEl) {
        CurRootEl=CurDoc.NewElement("WebEditor");
        CurRootEl->SetAttribute("Version", "1.0");
        CurRootEl->SetAttribute("editor","blogi");
        CurDoc.InsertEndChild(CurRootEl);
    }

    Args.theme->renderPage(tid,"editor.html",editpage,edithtml);

    Args.theme->injectThemeColors(tid, &edithtml);

    libhtmlpp::print(edithtml,ouput,false);

    libhttppp::HttpResponse curres;
    curres.setState(HTTP200);
    curres.setContentType("text/html");
    curres.send(curreq,ouput.data(),ouput.length());
}

std::vector<std::string> blogi::webedit::WebEditor::getPluginNames() const{
    std::vector<std::string> names;
    for(const EditPluginLoad::PluginData *curplg = Plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()){
        names.push_back(curplg->getInstace()->getName());
    }
    return names;
}

json_object *blogi::webedit::WebEditor::serializeDocumentTree(const blogi::webedit::EditPlugin *root) {
    if (!root) {
        return nullptr;
    }
    json_object *el_obj = json_object_new_object();

    json_object_object_add(el_obj, "uuid", json_object_new_string(root->getInstanceId().c_str()));
    json_object_object_add(el_obj, "name", json_object_new_string(root->getName().c_str()));
    json_object_object_add(el_obj, "typeId", json_object_new_string(root->getTypeId().c_str()));

    json_object *children_array = json_object_new_array();

    int child_count = 0;

    const blogi::webedit::EditPlugin *child = root->getChildElement();

    for (; child; child = child->nextElement()) {
        json_object_array_add(children_array, serializeDocumentTree(child));
        child_count++;
    }

    json_object_object_add(el_obj, "children", children_array);
    json_object_object_add(el_obj, "children_count", json_object_new_int(child_count));
    return el_obj;
}

// --- Tree Traversal Helpers ---

blogi::webedit::EditPlugin* blogi::webedit::WebEditor::findElementByUuid(EditPlugin *root, const std::string &uuid) {
    if (!root) return nullptr;

    // Traverse the node itself and all its siblings
    EditPlugin *current = root;
    while (current) {
        if (current->getInstanceId() == uuid) return current;

        // Search children recursively
        EditPlugin *child = const_cast<EditPlugin*>(current->getChildElement());
        while (child) {
            EditPlugin *found = findElementByUuid(child, uuid);
            if (found) return found;
            child = const_cast<EditPlugin*>(child->nextElement());
        }

        current = const_cast<EditPlugin*>(current->nextElement());
    }

    return nullptr;
}

blogi::webedit::WebEditor::NodeContext blogi::webedit::WebEditor::findNodeContext(const std::string &uuid) {
    NodeContext ctx;

    // Search top-level sibling chain (DocRoot chain)
    EditPlugin *prev = nullptr;
    EditPlugin *cur = DocRoot;
    while (cur) {
        if (cur->getInstanceId() == uuid) {
            ctx.node = cur;
            ctx.prev = prev;
            ctx.parent = nullptr;
            ctx.isTopLevel = true;
            return ctx;
        }

        // Search children of this node recursively (BFS-style with stack)
        // We need to find the target inside any subtree, tracking parent + prev
        struct Frame {
            EditPlugin *parentNode;
            EditPlugin *firstChild;
        };
        std::vector<Frame> stack;
        EditPlugin *child = const_cast<EditPlugin*>(cur->getChildElement());
        if (child) {
            stack.push_back({cur, child});
        }

        while (!stack.empty()) {
            Frame f = stack.back();
            stack.pop_back();

            EditPlugin *cprev = nullptr;
            EditPlugin *c = f.firstChild;
            while (c) {
                if (c->getInstanceId() == uuid) {
                    ctx.node = c;
                    ctx.prev = cprev;
                    ctx.parent = f.parentNode;
                    ctx.isTopLevel = false;
                    return ctx;
                }
                // Push this node's children for further searching
                EditPlugin *grandchild = const_cast<EditPlugin*>(c->getChildElement());
                if (grandchild) {
                    stack.push_back({c, grandchild});
                }
                cprev = c;
                c = const_cast<EditPlugin*>(c->nextElement());
            }
        }

        prev = cur;
        cur = const_cast<EditPlugin*>(cur->nextElement());
    }

    return ctx; // node == nullptr means not found
}

// --- XML Template Methods ---

const blogi::webedit::EditPluginLoad::PluginData* blogi::webedit::WebEditor::findPluginDataByName(const std::string &name) {
    std::string lowerName = name;
    std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
        [](unsigned char c) { return std::tolower(c); });

    for (const EditPluginLoad::PluginData *curplg = Plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) {
        // Match by plugin name (case-insensitive)
        std::string plgName = curplg->getInstace()->getName();
        std::transform(plgName.begin(), plgName.end(), plgName.begin(),
            [](unsigned char c) { return std::tolower(c); });
        if (plgName == lowerName) {
            return curplg;
        }
        // Also match by typeId UUID string
        std::string typeIdStr = curplg->getInstace()->getTypeId().c_str();
        if (typeIdStr == name) {
            return curplg;
        }
    }
    return nullptr;
}

blogi::webedit::EditPlugin* blogi::webedit::WebEditor::createPluginByName(const std::string &name) {
    const EditPluginLoad::PluginData *pd = findPluginDataByName(name);
    if (pd) {
        EditPlugin *newInst = pd->createNewInstance();
        if (newInst) {
            // Take ownership
            DocElements.emplace_back(newInst);
            return newInst;
        }
    }
    return nullptr;
}

void blogi::webedit::WebEditor::loadWidgetsFromXml(tinyxml2::XMLElement *xmlEl, EditPlugin *parent) {
    if (!xmlEl) return;

    // If loading from root element, read GlobalCss attribute
    if (!parent) {
        const char *gcss = xmlEl->Attribute("GlobalCss");
        if (gcss) GlobalCss = gcss;
        else GlobalCss.clear();
    }

    EditPlugin *lastSibling = nullptr;

    tinyxml2::XMLElement *childXml = xmlEl->FirstChildElement();
    while (childXml) {
        const char *elementName = childXml->Name();
        if (!elementName) {
            childXml = childXml->NextSiblingElement();
            continue;
        }

        // Create a new instance for this XML element
        EditPlugin *newWidget = createPluginByName(elementName);
        if (newWidget) {
            // Load properties from XML
            newWidget->fromXmlObject(childXml);

            // Link into the tree
            if (parent) {
                // Add as child of parent
                if (!lastSibling) {
                    parent->setChildElement(*newWidget);
                } else {
                    lastSibling->setNextEl(newWidget);
                }
            } else {
                // Top-level element
                if (!DocRoot) {
                    DocRoot = newWidget;
                } else if (!lastSibling) {
                    // Find end of top-level chain
                    EditPlugin *tail = DocRoot;
                    while (tail->nextElement()) {
                        tail = const_cast<EditPlugin*>(tail->nextElement());
                    }
                    tail->setNextEl(newWidget);
                } else {
                    lastSibling->setNextEl(newWidget);
                }
            }
            lastSibling = newWidget;

            // Recursively load children if this plugin supports them
            if (newWidget->canHaveChildren()) {
                loadWidgetsFromXml(childXml, newWidget);
            }
        }

        childXml = childXml->NextSiblingElement();
    }
}

blogi::webedit::EditPlugin* blogi::webedit::WebEditor::deepCopySubtree(tinyxml2::XMLElement *xmlEl, EditPlugin *parent) {
    if (!xmlEl) return nullptr;

    const char *elementName = xmlEl->Name();
    if (!elementName) return nullptr;

    EditPlugin *newWidget = createPluginByName(elementName);
    if (!newWidget) return nullptr;

    // Load properties from XML, then regenerate the instance ID
    // so the pasted copy always has a unique UUID
    newWidget->fromXmlObject(xmlEl);
    newWidget->regenerateId();

    // Recursively copy children
    if (newWidget->canHaveChildren()) {
        EditPlugin *lastChild = nullptr;
        tinyxml2::XMLElement *childXml = xmlEl->FirstChildElement();
        while (childXml) {
            EditPlugin *childCopy = deepCopySubtree(childXml, newWidget);
            if (childCopy) {
                if (!lastChild) {
                    newWidget->setChildElement(*childCopy);
                } else {
                    lastChild->setNextEl(childCopy);
                }
                lastChild = childCopy;
            }
            childXml = childXml->NextSiblingElement();
        }
    }

    return newWidget;
}

// ============================================================================
// HTML Import: Convert arbitrary HTML to widget tree
// ============================================================================

std::map<std::string,std::string> blogi::webedit::WebEditor::declarationToMap(
    const libhtmlpp::CSSDeclaration &decl)
{
    std::map<std::string,std::string> result;
    for (const auto &prop : decl.getProperties()) {
        result[prop.getName()] = prop.getValue();
    }
    return result;
}

void blogi::webedit::WebEditor::mergeStylesheetRules(
    const libhtmlpp::CSSStyleSheet &sheet,
    const std::string &tag,
    const std::string &cssClass,
    const std::string &id,
    std::map<std::string,std::string> &props,
    std::string &mediaRules)
{
    // Split cssClass into individual classes
    std::vector<std::string> classes;
    if (!cssClass.empty()) {
        std::istringstream iss(cssClass);
        std::string cls;
        while (iss >> cls) {
            classes.push_back(cls);
        }
    }

    for (const auto &rule : sheet.getRules()) {
        const std::string &sel = rule.getSelector();
        if (sel.empty()) continue;

        // For @-rules (e.g. "@media (max-width: 1024px) .cards"), split into
        // the @-wrapper and the inner selector, then match the inner selector.
        bool isAtRule = (!sel.empty() && sel[0] == '@');
        std::string atWrapper;   // e.g. "@media (max-width: 1024px)"
        std::string innerSel;    // e.g. ".cards"

        if (isAtRule) {
            // libhtmlpp flattens nested @-rules as: "@media (...) <inner-selector>"
            // Find the closing ')' of the @-rule condition, then take the rest as inner selector.
            size_t parenDepth = 0;
            size_t splitPos = std::string::npos;
            for (size_t i = 0; i < sel.size(); ++i) {
                if (sel[i] == '(') ++parenDepth;
                else if (sel[i] == ')') {
                    if (parenDepth > 0) --parenDepth;
                    if (parenDepth == 0) {
                        splitPos = i + 1;
                        break;
                    }
                }
            }
            if (splitPos != std::string::npos && splitPos < sel.size()) {
                atWrapper = sel.substr(0, splitPos);
                // Trim leading whitespace from inner selector
                size_t innerStart = sel.find_first_not_of(" \t\n\r", splitPos);
                if (innerStart != std::string::npos) {
                    innerSel = sel.substr(innerStart);
                }
            }
            // If no inner selector could be extracted (e.g. @font-face, @keyframes),
            // skip — these are global rules handled separately in importHtmlToWidgets.
            if (innerSel.empty()) continue;
        }

        // The selector to match: either the full selector or the inner part of an @-rule
        const std::string &matchTarget = isAtRule ? innerSel : sel;

        // Handle comma-separated selectors
        std::istringstream selStream(matchTarget);
        std::string singleSel;
        while (std::getline(selStream, singleSel, ',')) {
            // Trim
            size_t start = singleSel.find_first_not_of(" \t\n\r");
            size_t end = singleSel.find_last_not_of(" \t\n\r");
            if (start == std::string::npos) continue;
            singleSel = singleSel.substr(start, end - start + 1);

            // For compound/descendant selectors (e.g. "#parent .child > tag"),
            // extract the last simple selector and match against that.
            // This is an approximation but catches the vast majority of real-world CSS.
            std::string matchSel = singleSel;
            {
                // Find the last space, '>' or '+' combinator
                size_t lastSep = matchSel.find_last_of(" >+~");
                if (lastSep != std::string::npos) {
                    size_t mstart = matchSel.find_first_not_of(" >+~", lastSep);
                    if (mstart != std::string::npos) {
                        matchSel = matchSel.substr(mstart);
                    }
                }
            }

            bool matches = false;

            if (!matchSel.empty() && matchSel[0] == '#') {
                // ID selector
                if (!id.empty() && matchSel == "#" + id) matches = true;
            } else if (!matchSel.empty() && matchSel[0] == '.') {
                // Class selector
                std::string clsSel = matchSel.substr(1);
                for (const auto &c : classes) {
                    if (c == clsSel) { matches = true; break; }
                }
            } else {
                // Tag selector (case-insensitive)
                std::string selLower = matchSel;
                std::transform(selLower.begin(), selLower.end(), selLower.begin(),
                    [](unsigned char c) { return std::tolower(c); });
                if (selLower == tag) matches = true;

                // Tag.class selector (e.g., "div.header")
                auto dotPos = matchSel.find('.');
                if (!matches && dotPos != std::string::npos) {
                    std::string tagPart = matchSel.substr(0, dotPos);
                    std::string clsPart = matchSel.substr(dotPos + 1);
                    std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    if (tagPart == tag) {
                        for (const auto &c : classes) {
                            if (c == clsPart) { matches = true; break; }
                        }
                    }
                }

                // Tag#id selector (e.g., "div#main")
                auto hashPos = matchSel.find('#');
                if (!matches && hashPos != std::string::npos) {
                    std::string tagPart = matchSel.substr(0, hashPos);
                    std::string idPart = matchSel.substr(hashPos + 1);
                    std::transform(tagPart.begin(), tagPart.end(), tagPart.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    if (tagPart == tag && idPart == id) matches = true;
                }
            }

            if (matches) {
                if (isAtRule) {
                    // Wrap matched properties in the @-rule block for CustomCss
                    std::string block = atWrapper + " { " + singleSel + " { ";
                    for (const auto &prop : rule.getDeclaration().getProperties()) {
                        block += prop.getName() + ": " + prop.getValue() + "; ";
                    }
                    block += "} } ";
                    mediaRules += block;
                } else {
                    // Stylesheet rules go first, inline styles override (already in props)
                    for (const auto &prop : rule.getDeclaration().getProperties()) {
                        if (props.find(prop.getName()) == props.end()) {
                            props[prop.getName()] = prop.getValue();
                        }
                    }
                }
            }
        }
    }
}

std::string blogi::webedit::WebEditor::extractCustomCss(
    std::map<std::string,std::string> &props,
    const std::vector<std::string> &knownKeys,
    const std::string &mediaRules)
{
    std::string customCss;
    std::vector<std::string> toRemove;
    for (auto &[k, v] : props) {
        bool known = false;
        for (auto &kk : knownKeys) {
            if (k == kk) { known = true; break; }
        }
        if (!known) {
            if (!customCss.empty()) customCss += " ";
            customCss += k + ": " + v + ";";
            toRemove.push_back(k);
        }
    }
    for (auto &k : toRemove) props.erase(k);
    if (!mediaRules.empty()) {
        if (!customCss.empty()) customCss += " ";
        customCss += mediaRules;
    }
    return customCss;
}

std::string blogi::webedit::WebEditor::serializeElement(const libhtmlpp::Element *el) {
    if (!el) return "";
    if (el->getType() == libhtmlpp::HtmlEl || el->getType() == libhtmlpp::SvgEL ||
        el->getType() == libhtmlpp::TextAreaEL) {
        const libhtmlpp::HtmlElement *hel = static_cast<const libhtmlpp::HtmlElement*>(el);
        std::string tag = hel->getTagname();
        std::string result = "<" + tag;
        // Iterate all attributes using the public API
        for (const auto *attr = hel->firstAttribute(); attr; attr = attr->nextAttribute()) {
            result += " " + attr->getKey();
            std::string val = attr->getValue();
            if (!val.empty()) {
                result += "=\"" + val + "\"";
            }
        }
        result += ">";
        result += serializeInnerHtml(const_cast<libhtmlpp::HtmlElement*>(hel));
        result += "</" + tag + ">";
        return result;
    } else if (el->getType() == libhtmlpp::TextEl) {
        return const_cast<libhtmlpp::TextElement*>(static_cast<const libhtmlpp::TextElement*>(el))->getText();
    } else if (el->getType() == libhtmlpp::ScriptEL) {
        const libhtmlpp::ScriptElement *sel = static_cast<const libhtmlpp::ScriptElement*>(el);
        std::string tag = sel->getTagname();
        std::string result = "<" + tag;
        for (const auto *attr = sel->firstAttribute(); attr; attr = attr->nextAttribute()) {
            result += " " + attr->getKey();
            std::string val = attr->getValue();
            if (!val.empty()) {
                result += "=\"" + val + "\"";
            }
        }
        result += ">";
        result += const_cast<libhtmlpp::ScriptElement*>(sel)->getScript();
        result += "</" + tag + ">";
        return result;
    } else if (el->getType() == libhtmlpp::CommentEl) {
        return "<!--" + const_cast<libhtmlpp::CommentElement*>(
            static_cast<const libhtmlpp::CommentElement*>(el))->getComment() + "-->";
    }
    return "";
}

std::string blogi::webedit::WebEditor::serializeInnerHtml(libhtmlpp::HtmlElement *el) {
    if (!el) return "";
    libhtmlpp::Element *child = el->firstChild();
    if (!child) return "";
    // print() serializes the element AND all its siblings,
    // so calling it once on firstChild gives all children
    libhtmlpp::HtmlString out;
    libhtmlpp::print(*child, out);
    return out.str();
}

void blogi::webedit::WebEditor::htmlElementToWidgetXml(
    libhtmlpp::Element *htmlEl,
    tinyxml2::XMLDocument &doc,
    tinyxml2::XMLElement *parentXml,
    const libhtmlpp::CSSStyleSheet &sheet)
{
    if (!htmlEl) return;

    // Iterative traversal using an explicit heap-allocated stack to avoid
    // thread stack overflow (musl default thread stack is only 128KB).
    struct Frame { libhtmlpp::Element *cur; tinyxml2::XMLElement *pXml; };
    std::stack<Frame> workStack;
    workStack.push({htmlEl, parentXml});

    while (!workStack.empty()) {
        auto frame = workStack.top();
        workStack.pop();

        // Collect child frames to push later (in reverse for document order)
        std::vector<Frame> childFrames;

        libhtmlpp::Element *cur = frame.cur;
        tinyxml2::XMLElement *pXml = frame.pXml;

        while (cur) {
            int type = cur->getType();

            if (type == libhtmlpp::TextEl) {
                libhtmlpp::TextElement *textEl = static_cast<libhtmlpp::TextElement*>(cur);
                std::string text = textEl->getText();
                if (text.find_first_not_of(" \t\n\r") != std::string::npos) {
                    tinyxml2::XMLElement *tb = doc.NewElement("TextBox");
                    tb->SetText(text.c_str());
                    pXml->InsertEndChild(tb);
                }
                cur = cur->nextElement();
                continue;
            }

            if (type == libhtmlpp::CommentEl) {
                cur = cur->nextElement();
                continue;
            }

            if (type == libhtmlpp::ScriptEL) {
                cur = cur->nextElement();
                continue;
            }

            // HtmlEl, SvgEL, TextAreaEL
            if (type != libhtmlpp::HtmlEl && type != libhtmlpp::SvgEL && type != libhtmlpp::TextAreaEL) {
                cur = cur->nextElement();
                continue;
            }

            libhtmlpp::HtmlElement *el = static_cast<libhtmlpp::HtmlElement*>(cur);
            std::string tag = el->getTagname();
            std::transform(tag.begin(), tag.end(), tag.begin(),
                [](unsigned char c) { return std::tolower(c); });

            // Skip structural HTML tags, process their children instead
            if (tag == "html" || tag == "body" || tag == "head" || tag == "main" ||
                tag == "header" || tag == "footer" || tag == "nav" ||
                tag == "aside") {
                libhtmlpp::Element *child = el->firstChild();
                if (child) {
                    childFrames.push_back({child, pXml});
                }
                cur = cur->nextElement();
                continue;
            }

            // Get inline style
            std::string style = el->getAtributte("style");
            auto cssProps = declarationToMap(libhtmlpp::CSSStyleSheet::parseInlineStyle(style));

            std::string cssClass = el->getAtributte("class");
            std::string elemId = el->getAtributte("id");

            // Merge matching stylesheet rules (including @media → mediaRules)
            std::string mediaRules;
            mergeStylesheetRules(sheet, tag, cssClass, elemId, cssProps, mediaRules);

            // ---- Map HTML tags to widget types ----

            if (tag == "section") {
                tinyxml2::XMLElement *w = doc.NewElement("Section");
                if (cssProps.count("background-color")) w->SetAttribute("background_color", cssProps["background-color"].c_str());
                if (cssProps.count("border-radius")) w->SetAttribute("border_radius", cssProps["border-radius"].c_str());
                if (cssProps.count("border")) w->SetAttribute("border_style", cssProps["border"].c_str());
                if (cssProps.count("opacity")) w->SetAttribute("opacity", cssProps["opacity"].c_str());
                if (cssProps.count("padding")) w->SetAttribute("padding", cssProps["padding"].c_str());
                if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str());
                if (cssProps.count("max-width")) w->SetAttribute("max_width", cssProps["max-width"].c_str());
                if (cssProps.count("min-height")) w->SetAttribute("min_height", cssProps["min-height"].c_str());
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());
                if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str());
                if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str());

                std::vector<std::string> knownCss = {"background-color","border-radius","border","opacity",
                    "padding","margin","max-width","min-height","width","height","display","flex-direction","justify-content"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);
                libhtmlpp::Element *child = el->firstChild();
                if (child) childFrames.push_back({child, w});

            } else if (tag == "article") {
                tinyxml2::XMLElement *w = doc.NewElement("Article");
                if (cssProps.count("background-color")) w->SetAttribute("background_color", cssProps["background-color"].c_str());
                if (cssProps.count("border-radius")) w->SetAttribute("border_radius", cssProps["border-radius"].c_str());
                if (cssProps.count("border")) w->SetAttribute("border_style", cssProps["border"].c_str());
                if (cssProps.count("opacity")) w->SetAttribute("opacity", cssProps["opacity"].c_str());
                if (cssProps.count("padding")) w->SetAttribute("padding", cssProps["padding"].c_str());
                if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str());
                if (cssProps.count("max-width")) w->SetAttribute("max_width", cssProps["max-width"].c_str());
                if (cssProps.count("min-height")) w->SetAttribute("min_height", cssProps["min-height"].c_str());
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());
                if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str());
                if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str());

                std::vector<std::string> knownCss = {"background-color","border-radius","border","opacity",
                    "padding","margin","max-width","min-height","width","height","display","flex-direction","justify-content"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);
                libhtmlpp::Element *child = el->firstChild();
                if (child) childFrames.push_back({child, w});

            } else if (tag == "div") {
                tinyxml2::XMLElement *w = doc.NewElement("Container");
                if (cssProps.count("background-color")) w->SetAttribute("background_color", cssProps["background-color"].c_str());
                if (cssProps.count("border-radius")) w->SetAttribute("border_radius", cssProps["border-radius"].c_str());
                if (cssProps.count("border")) w->SetAttribute("border_style", cssProps["border"].c_str());
                if (cssProps.count("opacity")) w->SetAttribute("opacity", cssProps["opacity"].c_str());
                if (cssProps.count("padding")) w->SetAttribute("padding", cssProps["padding"].c_str());
                if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str());
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());
                if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str());
                if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str());

                bool isGrid = cssProps.count("display") && cssProps["display"] == "grid";

                if (isGrid) {
                    w->SetName("Grid");
                    if (cssProps.count("grid-template-columns")) w->SetAttribute("columns", cssProps["grid-template-columns"].c_str());
                    if (cssProps.count("grid-template-rows")) w->SetAttribute("rows", cssProps["grid-template-rows"].c_str());
                    if (cssProps.count("gap")) w->SetAttribute("gap", cssProps["gap"].c_str());
                    std::vector<std::string> knownCss = {"background-color","border-radius","border","opacity",
                        "padding","margin","width","height","display","grid-template-columns","grid-template-rows","gap","align-items"};
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
                        w->InsertEndChild(ccEl);
                    }
                } else {
                    std::vector<std::string> knownCss = {"background-color","border-radius","border","opacity",
                        "padding","margin","width","height","display","flex-direction","justify-content"};
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
                        w->InsertEndChild(ccEl);
                    }
                }
                pXml->InsertEndChild(w);
                libhtmlpp::Element *child = el->firstChild();
                if (child) childFrames.push_back({child, w});

            } else if (tag == "img") {
                tinyxml2::XMLElement *w = doc.NewElement("Image");
                std::string src = el->getAtributte("src");
                std::string alt = el->getAtributte("alt");
                if (!src.empty()) w->SetAttribute("src", src.c_str());
                if (!alt.empty()) w->SetAttribute("alt", alt.c_str());
                std::string widthAttr = el->getAtributte("width");
                std::string heightAttr = el->getAtributte("height");
                if (!widthAttr.empty() && !cssProps.count("width")) cssProps["width"] = widthAttr + "px";
                if (!heightAttr.empty() && !cssProps.count("height")) cssProps["height"] = heightAttr + "px";
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());
                if (cssProps.count("object-fit")) w->SetAttribute("object_fit", cssProps["object-fit"].c_str());
                if (cssProps.count("border-radius")) w->SetAttribute("border_radius", cssProps["border-radius"].c_str());

                std::vector<std::string> knownCss = {"width","height","object-fit","border-radius","display"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);

            } else if (tag == "video") {
                tinyxml2::XMLElement *w = doc.NewElement("Video");
                std::string src = el->getAtributte("src");
                if (!src.empty()) w->SetAttribute("src", src.c_str());
                if (!el->getAtributte("controls").empty()) w->SetAttribute("controls", "true");
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());

                std::vector<std::string> knownCss = {"width","height","display"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);

            } else if (tag == "table") {
                tinyxml2::XMLElement *w = doc.NewElement("Table");
                if (cssProps.count("border")) w->SetAttribute("border_style", cssProps["border"].c_str());
                if (cssProps.count("background-color")) w->SetAttribute("background_color", cssProps["background-color"].c_str());

                int numRows = 0, numCols = 0;
                bool hasHeader = false;

                struct RowData { std::vector<std::string> cells; bool isHeader; };
                std::vector<RowData> rows;

                auto processTr = [&](libhtmlpp::HtmlElement *tr, bool header) {
                    RowData rd;
                    rd.isHeader = header;
                    libhtmlpp::Element *tdEl = tr->firstChild();
                    while (tdEl) {
                        if (tdEl->getType() == libhtmlpp::HtmlEl) {
                            libhtmlpp::HtmlElement *td = static_cast<libhtmlpp::HtmlElement*>(tdEl);
                            std::string tdTag = td->getTagname();
                            std::transform(tdTag.begin(), tdTag.end(), tdTag.begin(),
                                [](unsigned char c) { return std::tolower(c); });
                            if (tdTag == "td" || tdTag == "th") {
                                rd.cells.push_back(serializeInnerHtml(td));
                            }
                        }
                        tdEl = tdEl->nextElement();
                    }
                    if ((int)rd.cells.size() > numCols) numCols = (int)rd.cells.size();
                    rows.push_back(rd);
                    numRows++;
                    if (header) hasHeader = true;
                };

                libhtmlpp::Element *tblChild = el->firstChild();
                while (tblChild) {
                    if (tblChild->getType() == libhtmlpp::HtmlEl) {
                        libhtmlpp::HtmlElement *tblHEl = static_cast<libhtmlpp::HtmlElement*>(tblChild);
                        std::string tblTag = tblHEl->getTagname();
                        std::transform(tblTag.begin(), tblTag.end(), tblTag.begin(),
                            [](unsigned char c) { return std::tolower(c); });

                        if (tblTag == "thead") {
                            libhtmlpp::Element *headChild = tblHEl->firstChild();
                            while (headChild) {
                                if (headChild->getType() == libhtmlpp::HtmlEl) {
                                    libhtmlpp::HtmlElement *tr = static_cast<libhtmlpp::HtmlElement*>(headChild);
                                    std::string trTag = tr->getTagname();
                                    std::transform(trTag.begin(), trTag.end(), trTag.begin(),
                                        [](unsigned char c) { return std::tolower(c); });
                                    if (trTag == "tr") processTr(tr, true);
                                }
                                headChild = headChild->nextElement();
                            }
                        } else if (tblTag == "tbody") {
                            libhtmlpp::Element *bodyChild = tblHEl->firstChild();
                            while (bodyChild) {
                                if (bodyChild->getType() == libhtmlpp::HtmlEl) {
                                    libhtmlpp::HtmlElement *tr = static_cast<libhtmlpp::HtmlElement*>(bodyChild);
                                    std::string trTag = tr->getTagname();
                                    std::transform(trTag.begin(), trTag.end(), trTag.begin(),
                                        [](unsigned char c) { return std::tolower(c); });
                                    if (trTag == "tr") processTr(tr, false);
                                }
                                bodyChild = bodyChild->nextElement();
                            }
                        } else if (tblTag == "tr") {
                            processTr(tblHEl, false);
                        }
                    }
                    tblChild = tblChild->nextElement();
                }

                w->SetAttribute("num_rows", numRows);
                w->SetAttribute("num_cols", numCols);
                w->SetAttribute("has_header", hasHeader ? "true" : "false");

                int row = 0;
                for (const auto &rd : rows) {
                    tinyxml2::XMLElement *rowEl = doc.NewElement("Row");
                    rowEl->SetAttribute("index", row);
                    int col = 0;
                    for (const auto &cellText : rd.cells) {
                        tinyxml2::XMLElement *cellEl = doc.NewElement("Cell");
                        cellEl->SetAttribute("col", col);
                        cellEl->SetText(cellText.c_str());
                        rowEl->InsertEndChild(cellEl);
                        col++;
                    }
                    w->InsertEndChild(rowEl);
                    row++;
                }

                std::vector<std::string> knownCss = {"border","background-color","border-collapse","width","height"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);

            } else if (tag == "ul" || tag == "ol") {
                tinyxml2::XMLElement *w = doc.NewElement("List");
                w->SetAttribute("list_type", tag.c_str());

                if (cssProps.count("font-size")) w->SetAttribute("font_size", cssProps["font-size"].c_str());
                if (cssProps.count("color")) w->SetAttribute("font_color", cssProps["color"].c_str());
                if (cssProps.count("line-height")) w->SetAttribute("line_height", cssProps["line-height"].c_str());
                if (cssProps.count("list-style-type")) w->SetAttribute("list_style_type", cssProps["list-style-type"].c_str());
                if (cssProps.count("padding")) w->SetAttribute("padding", cssProps["padding"].c_str());
                if (cssProps.count("margin")) w->SetAttribute("margin", cssProps["margin"].c_str());
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());

                libhtmlpp::Element *liEl = el->firstChild();
                while (liEl) {
                    if (liEl->getType() == libhtmlpp::HtmlEl) {
                        libhtmlpp::HtmlElement *li = static_cast<libhtmlpp::HtmlElement*>(liEl);
                        std::string liTag = li->getTagname();
                        std::transform(liTag.begin(), liTag.end(), liTag.begin(),
                            [](unsigned char c) { return std::tolower(c); });
                        if (liTag == "li") {
                            tinyxml2::XMLElement *itemEl = doc.NewElement("Item");
                            std::string itemHtml = serializeInnerHtml(li);
                            itemEl->SetText(itemHtml.c_str());
                            w->InsertEndChild(itemEl);
                        }
                    }
                    liEl = liEl->nextElement();
                }

                std::vector<std::string> knownCss = {"font-size","color","line-height","list-style-type",
                    "padding","margin","width","height","display","flex-direction","gap"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);

            } else if (tag == "a") {
                bool hasChildElements = false;
                libhtmlpp::Element *aChild = el->firstChild();
                while (aChild) {
                    if (aChild->getType() == libhtmlpp::HtmlEl) { hasChildElements = true; break; }
                    aChild = aChild->nextElement();
                }

                if (!hasChildElements) {
                    tinyxml2::XMLElement *w = doc.NewElement("Button");
                    std::string href = el->getAtributte("href");
                    if (!href.empty()) w->SetAttribute("link_url", href.c_str());
                    std::string target = el->getAtributte("target");
                    if (target == "_blank") w->SetAttribute("open_new_tab", "true");

                    std::string label = serializeInnerHtml(el);
                    w->SetAttribute("label", label.c_str());

                    if (cssProps.count("background-color")) w->SetAttribute("background_color", cssProps["background-color"].c_str());
                    if (cssProps.count("color")) w->SetAttribute("text_color", cssProps["color"].c_str());
                    if (cssProps.count("font-size")) w->SetAttribute("font_size", cssProps["font-size"].c_str());
                    if (cssProps.count("padding")) w->SetAttribute("padding", cssProps["padding"].c_str());
                    if (cssProps.count("border-radius")) w->SetAttribute("border_radius", cssProps["border-radius"].c_str());
                    if (cssProps.count("border")) w->SetAttribute("border_style", cssProps["border"].c_str());
                    if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                    if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());

                    std::vector<std::string> knownCss = {"background-color","color","font-size","padding",
                        "border-radius","border","width","height","display","text-decoration","cursor","text-align"};
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
                        w->InsertEndChild(ccEl);
                    }
                    pXml->InsertEndChild(w);
                } else {
                    tinyxml2::XMLElement *w = doc.NewElement("CustomHtml");
                    tinyxml2::XMLElement *htmlContent = doc.NewElement("HtmlContent");
                    htmlContent->SetText(serializeElement(cur).c_str());
                    w->InsertEndChild(htmlContent);
                    pXml->InsertEndChild(w);
                }

            } else if (tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4" ||
                       tag == "h5" || tag == "h6" || tag == "p" || tag == "span" ||
                       tag == "b" || tag == "i" || tag == "em" || tag == "strong" ||
                       tag == "blockquote" || tag == "pre" || tag == "code") {
                tinyxml2::XMLElement *w = doc.NewElement("TextBox");

                std::string innerHtml;
                if ((tag[0] == 'h' && tag.size() == 2) || tag == "blockquote" || tag == "pre") {
                    innerHtml = "<" + tag + ">" + serializeInnerHtml(el) + "</" + tag + ">";
                } else {
                    innerHtml = serializeInnerHtml(el);
                }
                w->SetText(innerHtml.c_str());

                if (cssProps.count("font-family")) w->SetAttribute("font_family", cssProps["font-family"].c_str());
                if (cssProps.count("font-size")) w->SetAttribute("font_size", cssProps["font-size"].c_str());
                if (cssProps.count("color")) w->SetAttribute("font_color", cssProps["color"].c_str());
                if (cssProps.count("text-align")) w->SetAttribute("text_align", cssProps["text-align"].c_str());
                if (cssProps.count("font-weight")) w->SetAttribute("font_weight", cssProps["font-weight"].c_str());
                if (cssProps.count("font-style")) w->SetAttribute("font_style", cssProps["font-style"].c_str());
                if (cssProps.count("width")) w->SetAttribute("width", cssProps["width"].c_str());
                if (cssProps.count("height")) w->SetAttribute("height", cssProps["height"].c_str());
                if (!cssClass.empty()) w->SetAttribute("css_class", cssClass.c_str());
                if (!elemId.empty()) w->SetAttribute("css_id", elemId.c_str());

                std::vector<std::string> knownCss = {"font-family","font-size","color","text-align",
                    "font-weight","font-style","width","height","padding","margin","line-height"};
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
                    w->InsertEndChild(ccEl);
                }
                pXml->InsertEndChild(w);

            } else if (tag == "style") {
                // CSS rules already collected and applied via mergeStylesheetRules — skip

            } else if (tag == "br" || tag == "hr") {
                tinyxml2::XMLElement *w = doc.NewElement("CustomHtml");
                tinyxml2::XMLElement *htmlContent = doc.NewElement("HtmlContent");
                htmlContent->SetText(("<" + tag + ">").c_str());
                w->InsertEndChild(htmlContent);
                pXml->InsertEndChild(w);

            } else if (tag == "meta" || tag == "link" || tag == "title" || tag == "base" ||
                       tag == "DOCTYPE" || tag == "!doctype") {
                // Skip document-level elements

            } else {
                // Everything else → CustomHtml widget
                tinyxml2::XMLElement *w = doc.NewElement("CustomHtml");
                tinyxml2::XMLElement *htmlContent = doc.NewElement("HtmlContent");
                htmlContent->SetText(serializeElement(cur).c_str());
                w->InsertEndChild(htmlContent);

                if (!style.empty()) {
                    tinyxml2::XMLElement *cssEl = doc.NewElement("CssContent");
                    cssEl->SetText(style.c_str());
                    w->InsertEndChild(cssEl);
                }
                pXml->InsertEndChild(w);
            }

            cur = cur->nextElement();
        }

        // Push child frames in reverse so they are processed in document order (LIFO)
        for (auto it = childFrames.rbegin(); it != childFrames.rend(); ++it) {
            workStack.push(*it);
        }
    }
}

void blogi::webedit::WebEditor::importHtmlToWidgets(const std::string &html) {
    // Clear existing document
    DocElements.clear();
    DocRoot = nullptr;
    CurDoc.Clear();
    CurRootEl = nullptr;

    // Parse the HTML using libhtmlpp (proper HTML parser)
    libhtmlpp::HtmlString htmlStr(html);
    libhtmlpp::Element *root = nullptr;
    try {
        root = &htmlStr.parse();
    } catch (...) {
        // Fallback: import as single CustomHtml widget
        CurDoc.Clear();
        CurRootEl = CurDoc.NewElement("WebEditor");
        CurRootEl->SetAttribute("Version", "1.0");
        CurDoc.InsertEndChild(CurRootEl);

        tinyxml2::XMLElement *w = CurDoc.NewElement("CustomHtml");
        tinyxml2::XMLElement *htmlContent = CurDoc.NewElement("HtmlContent");
        htmlContent->SetText(html.c_str());
        w->InsertEndChild(htmlContent);
        CurRootEl->InsertEndChild(w);

        loadWidgetsFromXml(CurRootEl, nullptr);
        return;
    }

    // Collect all <style> elements from the DOM and parse CSS
    libhtmlpp::CSSStyleSheet stylesheet;
    std::stack<libhtmlpp::Element*> searchStack;
    searchStack.push(root);
    while (!searchStack.empty()) {
        libhtmlpp::Element *cur = searchStack.top();
        searchStack.pop();
        while (cur) {
            if (cur->getType() == libhtmlpp::HtmlEl) {
                libhtmlpp::HtmlElement *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                std::string tagName = hel->getTagname();
                std::transform(tagName.begin(), tagName.end(), tagName.begin(),
                    [](unsigned char c) { return std::tolower(c); });
                if (tagName == "style") {
                    // Get text content of style element
                    std::string cssText;
                    libhtmlpp::Element *styleChild = hel->firstChild();
                    while (styleChild) {
                        if (styleChild->getType() == libhtmlpp::TextEl) {
                            cssText += static_cast<libhtmlpp::TextElement*>(styleChild)->getText();
                        }
                        styleChild = styleChild->nextElement();
                    }
                    if (!cssText.empty()) {
                        libhtmlpp::CSSStyleSheet partial;
                        partial.parse(cssText);
                        for (const auto &rule : partial.getRules()) {
                            stylesheet.addRule(rule);
                        }
                    }
                }
                // Also search children of non-style elements
                if (tagName != "style") {
                    libhtmlpp::Element *child = hel->firstChild();
                    if (child) searchStack.push(child);
                }
            }
            cur = cur->nextElement();
        }
    }

    // Build widget XML
    CurDoc.Clear();
    CurRootEl = CurDoc.NewElement("WebEditor");
    CurRootEl->SetAttribute("Version", "1.0");
    CurDoc.InsertEndChild(CurRootEl);

    // Emit global CSS rules that can't be matched to individual elements:
    // :root (CSS variables), @keyframes, @font-face, *, html, body,
    // ::selection, ::-webkit-scrollbar, and other pseudo-elements.
    std::string globalCss;
    for (const auto &rule : stylesheet.getRules()) {
        const std::string &sel = rule.getSelector();
        if (sel.empty()) continue;

        bool isGlobal = false;

        if (sel[0] == '@') {
            // @media with inner selectors are handled per-element in mergeStylesheetRules.
            // Everything else (@keyframes, @font-face, @import, @charset, bare @media
            // with :root/::-scrollbar, etc.) is global.
            // Check if it has parentheses with an inner selector — those go per-element.
            size_t parenClose = std::string::npos;
            size_t parenDepth = 0;
            for (size_t i = 0; i < sel.size(); ++i) {
                if (sel[i] == '(') ++parenDepth;
                else if (sel[i] == ')') {
                    if (parenDepth > 0) --parenDepth;
                    if (parenDepth == 0) { parenClose = i; break; }
                }
            }
            if (parenClose != std::string::npos && parenClose + 1 < sel.size()) {
                // Has inner selector (e.g. "@media (...) .class") → per-element, skip
                // unless the inner selector is :root or other global pseudo
                size_t innerStart = sel.find_first_not_of(" \t\n\r", parenClose + 1);
                if (innerStart != std::string::npos) {
                    std::string inner = sel.substr(innerStart);
                    std::string innerLower = inner;
                    std::transform(innerLower.begin(), innerLower.end(), innerLower.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    if (innerLower.compare(0, 5, ":root") == 0 ||
                        innerLower.find("::") != std::string::npos ||
                        innerLower == "*" || innerLower == "html" || innerLower == "body") {
                        isGlobal = true;
                    }
                }
            } else {
                // No inner selector: @keyframes, @font-face, @import, @charset, etc.
                isGlobal = true;
            }
        } else {
            std::string selLower = sel;
            std::transform(selLower.begin(), selLower.end(), selLower.begin(),
                [](unsigned char c) { return std::tolower(c); });
            if (selLower == ":root" || selLower == "*" || selLower == "html" ||
                selLower == "body" || selLower.compare(0, 5, ":root") == 0 ||
                selLower.find("::") != std::string::npos) {
                isGlobal = true;
            }
        }

        if (isGlobal) {
            globalCss += rule.serialize(true);
            globalCss += "\n";
        }
    }

    if (!globalCss.empty()) {
        GlobalCss = globalCss;
        CurRootEl->SetAttribute("GlobalCss", GlobalCss.c_str());
    } else {
        GlobalCss.clear();
    }

    if (root) {
        htmlElementToWidgetXml(root, CurDoc, CurRootEl, stylesheet);
    }

    // Load the generated widget XML into the live tree
    loadWidgetsFromXml(CurRootEl, nullptr);
}

void blogi::webedit::WebEditor::saveDocumentXml(libhttppp::HttpRequest &curreq) {
    // Serialize the current document tree to XML
    CurDoc.Clear();
    CurRootEl = CurDoc.NewElement("WebEditor");
    CurRootEl->SetAttribute("Version", "1.0");
    CurRootEl->SetAttribute("editor", "blogi");
    if (!GlobalCss.empty())
        CurRootEl->SetAttribute("GlobalCss", GlobalCss.c_str());
    CurDoc.InsertEndChild(CurRootEl);

    // Serialize each document element
    EditPlugin *el = DocRoot;
    while (el) {
        tinyxml2::XMLElement *xmlEl = el->toXmlObject(&CurDoc);
        if (xmlEl) {
            CurRootEl->InsertEndChild(xmlEl);
        }
        el = const_cast<EditPlugin*>(el->nextElement());
    }
}

void blogi::webedit::WebEditor::loadDocumentXml(const std::string &templateName) {
    // Build template path from config
    std::string templatePath;
    for (size_t i = 0; i < Args.config->getplgdirs(); ++i) {
        std::string candidate = Args.config->getplgdir(i) + "/webedit/templates/" + templateName + ".xml";
        if (std::filesystem::exists(candidate)) {
            templatePath = candidate;
            break;
        }
    }

    if (templatePath.empty()) {
        std::cerr << "WebEditor: Template not found: " << templateName << std::endl;
        return;
    }

    DocElements.clear();
    DocRoot = nullptr;
    CurDoc.Clear();
    CurRootEl = nullptr;
    tinyxml2::XMLError err = CurDoc.LoadFile(templatePath.c_str());
    if (err != tinyxml2::XML_SUCCESS) {
        std::cerr << "WebEditor: Failed to load XML template: " << templatePath
                  << " (error: " << static_cast<int>(err) << ")" << std::endl;
        return;
    }

    CurRootEl = CurDoc.RootElement();
    if (!CurRootEl) {
        std::cerr << "WebEditor: No root element in template: " << templatePath << std::endl;
        return;
    }

    // Parse widgets from the XML structure
    loadWidgetsFromXml(CurRootEl, nullptr);
}

std::string blogi::webedit::WebEditor::exportDocumentXml() {
    tinyxml2::XMLPrinter printer;
    CurDoc.Print(&printer);
    return printer.CStr();
}

std::string blogi::webedit::WebEditor::serializeCurrentXml() {
    // First make sure the in-memory XML document is up-to-date
    CurDoc.Clear();
    CurRootEl = CurDoc.NewElement("WebEditor");
    CurRootEl->SetAttribute("Version", "1.0");
    CurRootEl->SetAttribute("editor", "blogi");
    if (!GlobalCss.empty())
        CurRootEl->SetAttribute("GlobalCss", GlobalCss.c_str());
    CurDoc.InsertEndChild(CurRootEl);

    // Serialize each document element
    EditPlugin *el = DocRoot;
    while (el) {
        tinyxml2::XMLElement *xmlEl = el->toXmlObject(&CurDoc);
        if (xmlEl) {
            CurRootEl->InsertEndChild(xmlEl);
        }
        el = const_cast<EditPlugin*>(el->nextElement());
    }

    tinyxml2::XMLPrinter printer;
    CurDoc.Print(&printer);
    return printer.CStr();
}

std::string blogi::webedit::WebEditor::saveTemplateToDb(const int tid, const std::string &name,
                                                         const std::string &description, const std::string &comment) {
    std::vector<char> sbuf;
    dbpp::SQL sql;
    dbpp::DBResult res;

    std::string xmlData = serializeCurrentXml();

    // Check if a template with this name already exists
    sql << "SELECT id FROM editor_templates WHERE name='" << dbpp::SQL::escaped(sbuf, name.c_str()) << "' LIMIT 1";
    int exists = Args.database[tid]->exec(sql, res);

    std::string templateId;

    if (exists > 0) {
        // Template exists — use existing ID
        templateId = res[0][0];

        // Update the updated timestamp
        sql.clear();
        sql << "UPDATE editor_templates SET updated=CURRENT_TIMESTAMP WHERE id='" << templateId.c_str() << "'";
        Args.database[tid]->exec(sql, res);
    } else {
        // Create new template entry
        uuid::uuid newId;
        newId.generate();
        templateId = newId.c_str();

        sql.clear();
        sql << "INSERT INTO editor_templates (id, name, description) VALUES ('"
            << templateId.c_str() << "', '"
            << dbpp::SQL::escaped(sbuf, name.c_str()) << "', '"
            << dbpp::SQL::escaped(sbuf, description.c_str()) << "')";
        Args.database[tid]->exec(sql, res);
    }

    // Determine the next revision number
    sql.clear();
    sql << "SELECT COALESCE(MAX(revision), 0) FROM editor_template_revisions WHERE template_id='" << templateId.c_str() << "'";
    Args.database[tid]->exec(sql, res);
    int nextRevision = std::atoi(res[0][0]) + 1;

    // Insert the new revision
    sql.clear();
    sql << "INSERT INTO editor_template_revisions (template_id, revision, xml_data, comment) VALUES ('"
        << templateId.c_str() << "', "
        << std::to_string(nextRevision).c_str() << ", '"
        << dbpp::SQL::escaped(sbuf, xmlData.c_str()) << "', '"
        << dbpp::SQL::escaped(sbuf, comment.c_str()) << "')";
    Args.database[tid]->exec(sql, res);

    return templateId;
}

bool blogi::webedit::WebEditor::loadTemplateFromDb(const int tid, const std::string &templateId, int revision) {
    std::vector<char> sbuf;
    dbpp::SQL sql;
    dbpp::DBResult res;

    if (revision < 0) {
        // Load latest revision
        sql << "SELECT xml_data FROM editor_template_revisions WHERE template_id='"
            << dbpp::SQL::escaped(sbuf, templateId.c_str())
            << "' ORDER BY revision DESC LIMIT 1";
    } else {
        // Load specific revision
        sql << "SELECT xml_data FROM editor_template_revisions WHERE template_id='"
            << dbpp::SQL::escaped(sbuf, templateId.c_str())
            << "' AND revision=" << std::to_string(revision).c_str() << " LIMIT 1";
    }

    int ncount = Args.database[tid]->exec(sql, res);
    if (ncount < 1) {
        std::cerr << "WebEditor: No revision found for template: " << templateId << std::endl;
        return false;
    }

    const char *xmlData = res[0][0];
    if (!xmlData || xmlData[0] == '\0') {
        std::cerr << "WebEditor: Empty XML data for template: " << templateId << std::endl;
        return false;
    }

    DocElements.clear();
    DocRoot = nullptr;
    CurDoc.Clear();
    CurRootEl = nullptr;
    tinyxml2::XMLError err = CurDoc.Parse(xmlData);
    if (err != tinyxml2::XML_SUCCESS) {
        std::cerr << "WebEditor: Failed to parse XML from DB (template: " << templateId
                  << ", error: " << static_cast<int>(err) << ")" << std::endl;
        return false;
    }

    CurRootEl = CurDoc.RootElement();
    if (!CurRootEl) {
        std::cerr << "WebEditor: No root element in loaded XML" << std::endl;
        return false;
    }

    loadWidgetsFromXml(CurRootEl, nullptr);
    return true;
}

json_object* blogi::webedit::WebEditor::listTemplates(const int tid) {
    dbpp::SQL sql;
    dbpp::DBResult res;

    sql << "SELECT t.id, t.name, t.description, t.created, t.updated, "
           "COALESCE((SELECT MAX(r.revision) FROM editor_template_revisions r WHERE r.template_id=t.id), 0) as latest_rev "
           "FROM editor_templates t ORDER BY t.updated DESC";

    int ncount = Args.database[tid]->exec(sql, res);

    json_object *arr = json_object_new_array();
    for (int i = 0; i < ncount; ++i) {
        json_object *obj = json_object_new_object();
        json_object_object_add(obj, "id", json_object_new_string(res[i][0]));
        json_object_object_add(obj, "name", json_object_new_string(res[i][1]));
        json_object_object_add(obj, "description", json_object_new_string(res[i][2] ? res[i][2] : ""));
        json_object_object_add(obj, "created", json_object_new_string(res[i][3] ? res[i][3] : ""));
        json_object_object_add(obj, "updated", json_object_new_string(res[i][4] ? res[i][4] : ""));
        json_object_object_add(obj, "latest_revision", json_object_new_int(std::atoi(res[i][5])));
        json_object_array_add(arr, obj);
    }

    return arr;
}

json_object* blogi::webedit::WebEditor::getTemplateRevisions(const int tid, const std::string &templateId) {
    std::vector<char> sbuf;
    dbpp::SQL sql;
    dbpp::DBResult res;

    sql << "SELECT id, revision, comment, created FROM editor_template_revisions WHERE template_id='"
        << dbpp::SQL::escaped(sbuf, templateId.c_str())
        << "' ORDER BY revision DESC";

    int ncount = Args.database[tid]->exec(sql, res);

    json_object *arr = json_object_new_array();
    for (int i = 0; i < ncount; ++i) {
        json_object *obj = json_object_new_object();
        json_object_object_add(obj, "id", json_object_new_int(std::atoi(res[i][0])));
        json_object_object_add(obj, "revision", json_object_new_int(std::atoi(res[i][1])));
        json_object_object_add(obj, "comment", json_object_new_string(res[i][2] ? res[i][2] : ""));
        json_object_object_add(obj, "created", json_object_new_string(res[i][3] ? res[i][3] : ""));
        json_object_array_add(arr, obj);
    }

    return arr;
}

/**
 * @brief Handles API requests, ensuring commands are routed correctly.
 * * Commands are prioritized:
 * 1. Global (e.g., GetPluginTypes, add_plugin_element).
 * 2. Element-specific (e.g., get_element_properties, update_element).
 * 3. Fallback: Check if any loaded plugin explicitly handles the command
 * (usually not ideal, but maintained as a last resort).
 * * @param request JSON object containing the command and data.
 * @param response JSON object to populate with the result.
 */
void blogi::webedit::WebEditor::JsonApi(const int tid, json_object *request, json_object *response, Plugin *plugins) {
    // --- Initialize Plugins ONCE per API request (Moved for performance) ---
    std::vector<std::string> edplgdirs;
    for(size_t i=0; i<Args.config->getplgdirs(); ++i){
        edplgdirs.emplace_back(Args.config->getplgdir(i)+"/webedit");
    }

    auto jsonCommand = [&] (json_object *single_request, json_object *single_response) {
        json_object *cmd_obj;
        // Check if the overall request is not an array (which is expected)
        if (json_object_get_type(request) != json_type_array) {
            if (json_object_get_type(response) == json_type_array) {
                json_object *error_obj = json_object_new_object();
                json_object_object_add(error_obj, "status", json_object_new_string("error"));
                json_object_object_add(error_obj, "message", json_object_new_string("API request must be a JSON array."));
                json_object_array_add(response, error_obj);
            }
            return;
        }

        if (json_object_object_get_ex(single_request, "login", &cmd_obj)) {
                // Silently ignore auth command, treated as success in the array.
                json_object_object_add(single_response, "auth_success", json_object_new_object());
                json_object_object_add(json_object_object_get(single_response, "auth_success"), "is_valid", json_object_new_boolean(true));
                // NOTE: Real auth should set a unique authid here
                return;
        }

        if (json_object_object_get_ex(single_request, "authid", &cmd_obj)) {
                // Silently ignore auth command, treated as success in the array.
                json_object_object_add(single_response, "auth_success", json_object_new_object());
                json_object_object_add(json_object_object_get(single_response, "auth_success"), "is_valid", json_object_new_boolean(true));
                // NOTE: Real auth should set a unique authid here
                return;
        }

        if (!json_object_object_get_ex(single_request, "command", &cmd_obj)) {
            // Check for 'auth' token, which is handled silently without error
            json_object_object_add(single_response, "status", json_object_new_string("error"));
            json_object_object_add(single_response, "message", json_object_new_string("Command field missing in request object."));
            return;
        }

        const char *command = json_object_get_string(cmd_obj);

        bool handled = false;

        // --- 1. Handle Known Global Commands ---
        if (strcmp(command, "GetPluginTypes") == 0) {
            json_object *plugin_array = json_object_new_array();

            // Iterate directly over loaded plugins to get both name and typeId
            for(const EditPluginLoad::PluginData *curplg = Plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()){
                json_object *plugin_obj = json_object_new_object();

                // Assuming EditPlugin exposes getName() and getTypeId()
                const std::string name = curplg->getInstace()->getName();
                // Assuming getInstace()->getTypeId() exists and returns the ID string
                const char* typeId = curplg->getInstace()->getTypeId().c_str();

                if (!name.empty() && typeId) {
                    json_object_object_add(plugin_obj, "name", json_object_new_string(name.c_str()));
                    json_object_object_add(plugin_obj, "typeId", json_object_new_string(typeId));
                    json_object_array_add(plugin_array, plugin_obj);
                } else {
                    // Clean up object if data is invalid/missing
                    json_object_put(plugin_obj);
                }
            }

            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "pluginTypes", plugin_array);
            // Note: The main loop adds single_response to the overall 'response' array,
            // so we don't call json_object_array_add(response, single_response) here.
            handled = true;
        } else if (strcmp(command, "GetDocTree") == 0) {
            // Always return a virtual "Document" root wrapper with uuid "root"
            json_object *root_obj = json_object_new_object();
            json_object_object_add(root_obj, "uuid", json_object_new_string("root"));
            json_object_object_add(root_obj, "name", json_object_new_string("Document"));
            json_object_object_add(root_obj, "typeId", json_object_new_string(""));

            json_object *children_array = json_object_new_array();
            int child_count = 0;

            if (DocRoot) {
                // Serialize all top-level siblings as children of virtual root
                const EditPlugin *el = DocRoot;
                while (el) {
                    json_object *child_obj = this->serializeDocumentTree(el);
                    if (child_obj) {
                        json_object_array_add(children_array, child_obj);
                        child_count++;
                    }
                    el = el->nextElement();
                }
            }

            json_object_object_add(root_obj, "children", children_array);
            json_object_object_add(root_obj, "children_count", json_object_new_int(child_count));
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "doc_tree", root_obj);
            handled = true;
        } else if (strcmp(command, "add_plugin_element") == 0) {
            // Handle element addition request: Command to modify the document tree

            const char *parent_uuid = nullptr;
            const char *plugin_type_name = nullptr;
            const char *position = nullptr;

            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "parent_uuid", &param_obj)) {
                parent_uuid = json_object_get_string(param_obj);
            }
            if (json_object_object_get_ex(single_request, "plugin_name", &param_obj)) {
                plugin_type_name = json_object_get_string(param_obj);
            }
            if (json_object_object_get_ex(single_request, "position", &param_obj)) {
                position = json_object_get_string(param_obj);
            }

            if (plugin_type_name) {
                // Create a new widget instance via factory
                EditPlugin *newWidget = createPluginByName(plugin_type_name);
                if (!newWidget) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message",
                        json_object_new_string("Unknown plugin type or failed to create instance."));
                    handled = true;
                } else {
                    bool isAppendChild = (!position || strcmp(position, "append") == 0);
                    bool isVirtualRoot = parent_uuid && strcmp(parent_uuid, "root") == 0;

                    if (!DocRoot) {
                        // First element becomes the document root
                        DocRoot = newWidget;
                    } else if (isVirtualRoot || !parent_uuid) {
                        // "root" is the virtual Document node — add as top-level sibling
                        // Walk to the end of the top-level chain
                        EditPlugin *tail = DocRoot;
                        while (tail->nextElement()) {
                            tail = const_cast<EditPlugin*>(tail->nextElement());
                        }
                        tail->setNextEl(newWidget);
                    } else {
                        EditPlugin *parent = findElementByUuid(DocRoot, parent_uuid);
                        if (parent && isAppendChild && parent->canHaveChildren()) {
                            parent->addNextElement(*newWidget);
                        } else if (parent && isAppendChild && !parent->canHaveChildren()) {
                            // Widget does not support children — reject the operation
                            // Remove the widget we just created
                            DocElements.pop_back();
                            json_object_object_add(single_response, "status", json_object_new_string("error"));
                            json_object_object_add(single_response, "message",
                                json_object_new_string("This element cannot have children."));
                            handled = true;
                        } else if (parent) {
                            // Insert after parent as sibling, preserving existing chain
                            EditPlugin *oldNext = const_cast<EditPlugin*>(parent->nextElement());
                            parent->setNextEl(newWidget);
                            newWidget->setNextEl(oldNext);
                        } else {
                            // Parent not found — add as top-level sibling
                            EditPlugin *tail = DocRoot;
                            while (tail->nextElement()) {
                                tail = const_cast<EditPlugin*>(tail->nextElement());
                            }
                            tail->setNextEl(newWidget);
                        }
                    }

                    if (!handled) {
                        json_object_object_add(single_response, "status", json_object_new_string("success"));
                        json_object_object_add(single_response, "message",
                            json_object_new_string("Element added to document."));
                        json_object_object_add(single_response, "uuid",
                            json_object_new_string(newWidget->getInstanceId().c_str()));
                        json_object_object_add(single_response, "name",
                            json_object_new_string(newWidget->getName().c_str()));
                        handled = true;
                    }
                }
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'plugin_name' for add_plugin_element.")
                );
                handled = true;
            }
        } else if (strcmp(command, "remove_plugin_element") == 0) {
            // Remove an element from the document tree by UUID
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                NodeContext ctx = findNodeContext(target_uuid);
                if (ctx.node) {
                    // Unlink the node from its sibling chain
                    EditPlugin *next = const_cast<EditPlugin*>(ctx.node->nextElement());
                    if (ctx.isTopLevel && !ctx.prev) {
                        // Node is DocRoot — replace DocRoot with next sibling
                        DocRoot = next;
                    } else if (ctx.prev) {
                        // Has a previous sibling — skip over this node
                        ctx.prev->setNextEl(next);
                    } else if (ctx.parent) {
                        // First child of parent — update parent's child pointer
                        ctx.parent->setChildEl(next);
                    }
                    // Detach the node from the chain
                    ctx.node->setNextEl(nullptr);
                    // Remove from ownership vector (this deletes the object)
                    for (auto it = DocElements.begin(); it != DocElements.end(); ++it) {
                        if (it->get() == ctx.node) {
                            DocElements.erase(it);
                            break;
                        }
                    }
                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element removed."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for remove_plugin_element."));
                handled = true;
            }
        } else if (strcmp(command, "move_element_up") == 0) {
            // Move an element up (swap with previous sibling)
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                NodeContext ctx = findNodeContext(target_uuid);
                if (ctx.node && ctx.prev) {
                    // We need to swap ctx.prev and ctx.node in the sibling chain.
                    // Find the element before ctx.prev (the "grandprev")
                    NodeContext prevCtx = findNodeContext(std::string(ctx.prev->getInstanceId().c_str()));
                    EditPlugin *nodeNext = const_cast<EditPlugin*>(ctx.node->nextElement());

                    // prev -> node -> nodeNext  becomes  node -> prev -> nodeNext
                    ctx.prev->setNextEl(nodeNext);
                    ctx.node->setNextEl(ctx.prev);

                    // Fix the link pointing to prev (either grandprev, parent's child, or DocRoot)
                    if (prevCtx.prev) {
                        prevCtx.prev->setNextEl(ctx.node);
                    } else if (prevCtx.isTopLevel && !prevCtx.prev) {
                        DocRoot = ctx.node;
                    } else if (prevCtx.parent) {
                        prevCtx.parent->setChildEl(ctx.node);
                    }

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element moved up."));
                } else if (ctx.node && !ctx.prev) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element is already at the top."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for move_element_up."));
                handled = true;
            }
        } else if (strcmp(command, "move_element_down") == 0) {
            // Move an element down (swap with next sibling)
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                NodeContext ctx = findNodeContext(target_uuid);
                EditPlugin *next = ctx.node ? const_cast<EditPlugin*>(ctx.node->nextElement()) : nullptr;
                if (ctx.node && next) {
                    // node -> next -> nextNext  becomes  next -> node -> nextNext
                    EditPlugin *nextNext = const_cast<EditPlugin*>(next->nextElement());
                    ctx.node->setNextEl(nextNext);
                    next->setNextEl(ctx.node);

                    // Fix the link pointing to node (either prev, parent's child, or DocRoot)
                    if (ctx.prev) {
                        ctx.prev->setNextEl(next);
                    } else if (ctx.isTopLevel && !ctx.prev) {
                        DocRoot = next;
                    } else if (ctx.parent) {
                        ctx.parent->setChildEl(next);
                    }

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element moved down."));
                } else if (ctx.node && !next) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element is already at the bottom."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for move_element_down."));
                handled = true;
            }
        } else if (strcmp(command, "indent_element") == 0) {
            // Indent element: make it a child of the previous sibling
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                NodeContext ctx = findNodeContext(target_uuid);
                if (ctx.node && ctx.prev && ctx.prev->canHaveChildren()) {
                    // 1. Unlink node from current sibling chain
                    //    ctx.prev->nextEl currently points to ctx.node
                    EditPlugin *nodeNext = const_cast<EditPlugin*>(ctx.node->nextElement());
                    ctx.prev->setNextEl(nodeNext);

                    // 2. Append node as last child of ctx.prev
                    EditPlugin *lastChild = const_cast<EditPlugin*>(ctx.prev->getChildElement());
                    if (!lastChild) {
                        ctx.prev->setChildEl(ctx.node);
                    } else {
                        while (lastChild->nextElement()) {
                            lastChild = const_cast<EditPlugin*>(lastChild->nextElement());
                        }
                        lastChild->setNextEl(ctx.node);
                    }
                    ctx.node->setNextEl(nullptr); // Now last child

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element indented."));
                } else if (ctx.node && !ctx.prev) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("No previous sibling to indent into."));
                } else if (ctx.node && ctx.prev && !ctx.prev->canHaveChildren()) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Previous sibling cannot have children."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for indent_element."));
                handled = true;
            }
        } else if (strcmp(command, "outdent_element") == 0) {
            // Outdent element: move to parent's sibling level
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                NodeContext ctx = findNodeContext(target_uuid);
                if (ctx.node && !ctx.isTopLevel && ctx.parent) {
                    // 1. Unlink node from parent's child chain
                    EditPlugin *nodeNext = const_cast<EditPlugin*>(ctx.node->nextElement());
                    if (ctx.prev) {
                        // Not first child — previous sibling skips over this node
                        ctx.prev->setNextEl(nodeNext);
                    } else {
                        // First child — parent now points to next sibling (or nullptr)
                        ctx.parent->setChildEl(nodeNext);
                    }

                    // 2. Insert node as next sibling of parent
                    EditPlugin *parentNext = const_cast<EditPlugin*>(ctx.parent->nextElement());
                    ctx.parent->setNextEl(ctx.node);
                    ctx.node->setNextEl(parentNext);

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element outdented."));
                } else if (ctx.node && ctx.isTopLevel) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element is already at the top level."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for outdent_element."));
                handled = true;
            }
        } else if (strcmp(command, "copy_element") == 0) {
            // Copy element (+ children) to clipboard via XML serialization
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;
            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }
            if (target_uuid) {
                EditPlugin *el = DocRoot ? findElementByUuid(DocRoot, target_uuid) : nullptr;
                if (el) {
                    ClipboardDoc.Clear();
                    // Temporarily detach nextel so we only serialize this subtree
                    EditPlugin *savedNext = const_cast<EditPlugin*>(el->nextElement());
                    el->setNextEl(nullptr);
                    tinyxml2::XMLElement *xmlEl = el->toXmlObject(&ClipboardDoc);
                    el->setNextEl(savedNext);
                    if (xmlEl) {
                        ClipboardDoc.InsertEndChild(xmlEl);
                    }
                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element copied to clipboard."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element not found."));
                }
                handled = true;
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for copy_element."));
                handled = true;
            }
        } else if (strcmp(command, "paste_element") == 0) {
            // Paste clipboard content after a target element (or at root)
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;
            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }
            tinyxml2::XMLElement *clipRoot = ClipboardDoc.FirstChildElement();
            if (!clipRoot) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("Clipboard is empty. Copy an element first."));
                handled = true;
            } else {
                // Deep-copy the clipboard subtree into the document
                bool isRoot = target_uuid && strcmp(target_uuid, "root") == 0;
                EditPlugin *parent = nullptr;
                if (target_uuid && !isRoot) {
                    parent = DocRoot ? findElementByUuid(DocRoot, target_uuid) : nullptr;
                }

                EditPlugin *newRoot = deepCopySubtree(clipRoot, nullptr);
                if (newRoot) {
                    if (!DocRoot) {
                        DocRoot = newRoot;
                    } else if (isRoot || !target_uuid) {
                        // Append as top-level sibling
                        EditPlugin *tail = DocRoot;
                        while (tail->nextElement()) tail = const_cast<EditPlugin*>(tail->nextElement());
                        tail->setNextEl(newRoot);
                    } else if (parent && parent->canHaveChildren()) {
                        parent->addNextElement(*newRoot);
                    } else if (parent) {
                        // Insert after target as sibling
                        EditPlugin *afterTarget = const_cast<EditPlugin*>(parent->nextElement());
                        parent->setNextEl(newRoot);
                        // Append old chain after pasted subtree
                        EditPlugin *tail = newRoot;
                        while (tail->nextElement()) tail = const_cast<EditPlugin*>(tail->nextElement());
                        tail->setNextEl(afterTarget);
                    } else {
                        // Fallback: append at top-level
                        EditPlugin *tail = DocRoot;
                        while (tail->nextElement()) tail = const_cast<EditPlugin*>(tail->nextElement());
                        tail->setNextEl(newRoot);
                    }
                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("Element pasted."));
                    json_object_object_add(single_response, "uuid",
                        json_object_new_string(newRoot->getInstanceId().c_str()));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Failed to create element from clipboard."));
                }
                handled = true;
            }
        } else if (strcmp(command, "get_element_properties") == 0) {
            // Fetch properties of a specific element by UUID
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            if (target_uuid) {
                // Find the element in the document tree
                EditPlugin *el = DocRoot ? findElementByUuid(DocRoot, target_uuid) : nullptr;

                if (el) {
                    // Delegate to the plugin's JsonApi with action=read
                    json_object *read_req = json_object_new_object();
                    json_object_object_add(read_req, "action", json_object_new_string("read"));
                    json_object *plugin_resp = json_object_new_object();
                    el->JsonApi(read_req, plugin_resp);

                    // Extract properties from plugin response
                    json_object *data_obj = nullptr;
                    json_object *props_obj = nullptr;
                    std::string props_str;

                    if (json_object_object_get_ex(plugin_resp, "data", &data_obj) &&
                        json_object_object_get_ex(data_obj, "properties", &props_obj)) {
                        const char *ps = json_object_get_string(props_obj);
                        if (ps) props_str = ps;
                    }

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "widget_name",
                        json_object_new_string(el->getName().c_str()));
                    json_object_object_add(single_response, "widget_uuid",
                        json_object_new_string(el->getInstanceId().c_str()));

                    // Parse the properties string to a JSON object for the frontend
                    json_object *parsed_props = json_tokener_parse(props_str.c_str());
                    if (parsed_props) {
                        json_object_object_add(single_response, "properties", parsed_props);
                    } else {
                        json_object_object_add(single_response, "properties",
                            json_object_new_string(props_str.c_str()));
                    }

                    json_object_put(read_req);
                    json_object_put(plugin_resp);

                    // Fetch widget schema
                    json_object *schema_req = json_object_new_object();
                    json_object_object_add(schema_req, "action", json_object_new_string("get_schema"));
                    json_object *schema_resp = json_object_new_object();
                    el->JsonApi(schema_req, schema_resp);

                    json_object *schema_obj = nullptr;
                    if (json_object_object_get_ex(schema_resp, "schema", &schema_obj)) {
                        json_object_object_add(single_response, "schema", json_object_get(schema_obj));
                    }

                    json_object_put(schema_req);
                    json_object_put(schema_resp);

                    handled = true;
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message",
                        json_object_new_string("Element not found in document tree."));
                    handled = true;
                }
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing required parameter 'uuid' for get_element_properties."));
                handled = true;
            }
        } else if (strcmp(command, "update_element_properties") == 0) {
            // Update properties of a specific element by UUID
            const char *target_uuid = nullptr;
            json_object *param_obj = nullptr;

            if (json_object_object_get_ex(single_request, "uuid", &param_obj)) {
                target_uuid = json_object_get_string(param_obj);
            }

            json_object *properties_obj = nullptr;
            json_object_object_get_ex(single_request, "properties", &properties_obj);

            if (target_uuid && properties_obj) {
                // Find the element in the document tree
                EditPlugin *el = DocRoot ? findElementByUuid(DocRoot, target_uuid) : nullptr;

                if (el) {
                    // Delegate to the plugin's JsonApi with action=modify_bulk
                    json_object *modify_req = json_object_new_object();
                    json_object_object_add(modify_req, "action", json_object_new_string("modify_bulk"));
                    json_object_object_add(modify_req, "properties",
                        json_object_get(properties_obj)); // increment ref
                    json_object *plugin_resp = json_object_new_object();
                    el->JsonApi(modify_req, plugin_resp);

                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message",
                        json_object_new_string("Properties updated."));

                    json_object_put(modify_req);
                    json_object_put(plugin_resp);
                    handled = true;
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message",
                        json_object_new_string("Element not found in document tree."));
                    handled = true;
                }
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing 'uuid' or 'properties' for update_element_properties."));
                handled = true;
            }
        } else if (strcmp(command, "ClearDocument") == 0) {
            // Clear the entire document tree, starting fresh
            DocElements.clear();
            DocRoot = nullptr;
            CurDoc.Clear();
            CurRootEl = nullptr;
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "message", json_object_new_string("Document cleared."));
            handled = true;
        } else if (strcmp(command, "ExportXml") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_IMPORTEXPORT)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Import/Export"));
                handled = true;
            } else {
            // Export the current document tree as XML (serialized from live tree)
            std::string xmlStr = serializeCurrentXml();
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "xml", json_object_new_string(xmlStr.c_str()));
            handled = true;
            }
        } else if (strcmp(command, "ExportHtml") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_IMPORTEXPORT)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Import/Export"));
                handled = true;
            } else {
            // Export the current document tree as rendered HTML
            std::string renderedHtml;
            if (DocRoot) {
                libhtmlpp::HtmlElement wrapper("div");
                if (!GlobalCss.empty()) {
                    libhtmlpp::HtmlElement globalStyle("style");
                    globalStyle.appendChild(libhtmlpp::TextElement(GlobalCss.c_str()));
                    wrapper.appendChild(globalStyle);
                }
                const EditPlugin *el = DocRoot;
                while (el) {
                    el->Render(wrapper);
                    el = el->nextElement();
                }
                libhtmlpp::HtmlString htmlOut;
                libhtmlpp::print(wrapper, htmlOut,true);
                renderedHtml = htmlOut.str();
            }
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "html", json_object_new_string(renderedHtml.c_str()));
            handled = true;
            }
        } else if (strcmp(command, "ImportXml") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_IMPORTEXPORT)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Import/Export"));
                handled = true;
            } else {
            // Import raw XML string into the document tree
            json_object *xml_obj = nullptr;
            if (json_object_object_get_ex(single_request, "xml", &xml_obj)) {
                const char *xmlData = json_object_get_string(xml_obj);
                if (xmlData && xmlData[0] != '\0') {
                    // Clear existing document
                    DocElements.clear();
                    DocRoot = nullptr;
                    CurDoc.Clear();
                    CurRootEl = nullptr;

                    tinyxml2::XMLError err = CurDoc.Parse(xmlData);
                    if (err == tinyxml2::XML_SUCCESS) {
                        CurRootEl = CurDoc.RootElement();
                        if (CurRootEl) {
                            loadWidgetsFromXml(CurRootEl, nullptr);
                            json_object_object_add(single_response, "status", json_object_new_string("success"));
                            json_object_object_add(single_response, "message", json_object_new_string("XML imported successfully."));
                        } else {
                            json_object_object_add(single_response, "status", json_object_new_string("error"));
                            json_object_object_add(single_response, "message", json_object_new_string("No root element found in XML."));
                        }
                    } else {
                        json_object_object_add(single_response, "status", json_object_new_string("error"));
                        json_object_object_add(single_response, "message", json_object_new_string("Failed to parse XML data."));
                    }
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Empty XML data."));
                }
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("Missing 'xml' parameter."));
            }
            handled = true;
            }
        } else if (strcmp(command, "ImportHtml") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_IMPORTEXPORT)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Import/Export"));
                handled = true;
            } else {
            // Import raw HTML and convert to widget tree
            json_object *html_obj = nullptr;
            if (json_object_object_get_ex(single_request, "html", &html_obj)) {
                const char *htmlData = json_object_get_string(html_obj);
                if (htmlData && htmlData[0] != '\0') {
                    importHtmlToWidgets(htmlData);
                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "message", json_object_new_string("HTML imported successfully."));
                } else {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string("Empty HTML data."));
                }
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("Missing 'html' parameter."));
            }
            handled = true;
            }
        } else if (strcmp(command, "GetGlobalCss") == 0) {
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "global_css", json_object_new_string(GlobalCss.c_str()));
            handled = true;
        } else if (strcmp(command, "SetGlobalCss") == 0) {
            json_object *css_obj = nullptr;
            if (json_object_object_get_ex(single_request, "global_css", &css_obj)) {
                const char *cssData = json_object_get_string(css_obj);
                GlobalCss = cssData ? cssData : "";
                json_object_object_add(single_response, "status", json_object_new_string("success"));
            } else {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("Missing 'global_css' parameter."));
            }
            handled = true;
        } else if (strcmp(command, "LoadXml") == 0) {
            // Load an XML template by name
            json_object *name_obj = nullptr;
            std::string templateName = "default";
            if (json_object_object_get_ex(single_request, "template_name", &name_obj)) {
                const char *tn = json_object_get_string(name_obj);
                if (tn) templateName = tn;
            }
            loadDocumentXml(templateName);
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "message", json_object_new_string("Template loaded."));
            handled = true;
        } else if (strcmp(command, "SaveXml") == 0) {
            // Save the current document tree (in-memory only, legacy)
            saveDocumentXml(*(libhttppp::HttpRequest*)nullptr);
            tinyxml2::XMLPrinter printer;
            CurDoc.Print(&printer);
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "xml", json_object_new_string(printer.CStr()));
            handled = true;
        } else if (strcmp(command, "SaveTemplateDb") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_TEMPLATES)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Manage Templates"));
                handled = true;
            } else {
            // Save current template to database with revision
            json_object *param_obj = nullptr;
            std::string tplName = "untitled";
            std::string tplDesc = "";
            std::string tplComment = "";

            if (json_object_object_get_ex(single_request, "name", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) tplName = v;
            }
            if (json_object_object_get_ex(single_request, "description", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) tplDesc = v;
            }
            if (json_object_object_get_ex(single_request, "comment", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) tplComment = v;
            }

            try {
                std::string templateId = saveTemplateToDb(tid, tplName, tplDesc, tplComment);
                json_object_object_add(single_response, "status", json_object_new_string("success"));
                json_object_object_add(single_response, "template_id", json_object_new_string(templateId.c_str()));
                json_object_object_add(single_response, "message", json_object_new_string("Template saved to database."));
            } catch (std::exception &e) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string(e.what()));
            }
            handled = true;
            }
        } else if (strcmp(command, "LoadTemplateDb") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_TEMPLATES)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Manage Templates"));
                handled = true;
            } else {
            // Load a template from database (optionally a specific revision)
            json_object *param_obj = nullptr;
            std::string templateId = "";
            int revision = -1; // -1 = latest

            if (json_object_object_get_ex(single_request, "template_id", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) templateId = v;
            }
            if (json_object_object_get_ex(single_request, "revision", &param_obj)) {
                revision = json_object_get_int(param_obj);
            }

            if (templateId.empty()) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing 'template_id' parameter."));
            } else {
                try {
                    bool ok = loadTemplateFromDb(tid, templateId, revision);
                    if (ok) {
                        json_object_object_add(single_response, "status", json_object_new_string("success"));
                        json_object_object_add(single_response, "message", json_object_new_string("Template loaded from database."));
                    } else {
                        json_object_object_add(single_response, "status", json_object_new_string("error"));
                        json_object_object_add(single_response, "message", json_object_new_string("Template or revision not found."));
                    }
                } catch (std::exception &e) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string(e.what()));
                }
            }
            handled = true;
            }
        } else if (strcmp(command, "ListTemplates") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_TEMPLATES)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Manage Templates"));
                handled = true;
            } else {
            // List all saved templates from database
            try {
                json_object *templates = listTemplates(tid);
                json_object_object_add(single_response, "status", json_object_new_string("success"));
                json_object_object_add(single_response, "templates", templates);
            } catch (std::exception &e) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string(e.what()));
            }
            handled = true;
            }
        } else if (strcmp(command, "GetRevisions") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_TEMPLATES)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Manage Templates"));
                handled = true;
            } else {
            // Get revision history for a template
            json_object *param_obj = nullptr;
            std::string templateId = "";

            if (json_object_object_get_ex(single_request, "template_id", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) templateId = v;
            }

            if (templateId.empty()) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing 'template_id' parameter."));
            } else {
                try {
                    json_object *revisions = getTemplateRevisions(tid, templateId);
                    json_object_object_add(single_response, "status", json_object_new_string("success"));
                    json_object_object_add(single_response, "revisions", revisions);
                } catch (std::exception &e) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message", json_object_new_string(e.what()));
                }
            }
            handled = true;
            }
        } else if (strcmp(command, "list_publish_targets") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_PUBLISH)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Publish Content"));
                handled = true;
            } else {
            // List all plugins that support publishing
            json_object *targets = json_object_new_array();
            if (plugins) {
                for (const Plugin::PluginData *curplg = plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) {
                    PluginApi *api = curplg->getInstace();
                    if (api && api->canPublish()) {
                        json_object *target = json_object_new_object();
                        json_object_object_add(target, "name", json_object_new_string(api->getName().c_str()));
                        json_object_object_add(target, "version", json_object_new_string(api->getVersion().c_str()));
                        // Get publish form fields
                        json_object *fields = api->getPublishForm(tid);
                        json_object_object_add(target, "fields", fields);
                        json_object_array_add(targets, target);
                    }
                }
            }
            json_object_object_add(single_response, "status", json_object_new_string("success"));
            json_object_object_add(single_response, "targets", targets);
            handled = true;
            }
        } else if (strcmp(command, "publish") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_PUBLISH)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Publish Content"));
                handled = true;
            } else {
            // Publish rendered HTML to a target plugin
            json_object *param_obj = nullptr;
            std::string targetPlugin;

            if (json_object_object_get_ex(single_request, "target", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) targetPlugin = v;
            }

            if (targetPlugin.empty()) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing 'target' parameter."));
            } else {
                // Render current document tree to HTML string
                std::string renderedHtml;
                if (DocRoot) {
                    libhtmlpp::HtmlElement wrapper("div");
                    if (!GlobalCss.empty()) {
                        libhtmlpp::HtmlElement globalStyle("style");
                        globalStyle.appendChild(libhtmlpp::TextElement(GlobalCss.c_str()));
                        wrapper.appendChild(globalStyle);
                    }
                    const EditPlugin *el = DocRoot;
                    while (el) {
                        el->Render(wrapper);
                        el = el->nextElement();
                    }
                    libhtmlpp::HtmlString htmlOut;
                    libhtmlpp::print(wrapper, htmlOut);
                    renderedHtml = htmlOut.str();
                }

                // Find the target plugin and call Publish
                bool found = false;
                if (plugins) {
                    for (const Plugin::PluginData *curplg = plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) {
                        PluginApi *api = curplg->getInstace();
                        if (api && api->canPublish() && api->getName() == targetPlugin) {
                            // Get plugin-specific params from request
                            json_object *params = nullptr;
                            json_object_object_get_ex(single_request, "params", &params);

                            // Resolve author from authid via authdb
                            int authorId = 0;
                            size_t reqLen = json_object_array_length(request);
                            for (size_t ri = 0; ri < reqLen; ++ri) {
                                json_object *reqItem = json_object_array_get_idx(request, ri);
                                json_object *authObj = nullptr;
                                if (json_object_object_get_ex(reqItem, "authid", &authObj)) {
                                    const char *authIdStr = json_object_get_string(authObj);
                                    if (authIdStr && authIdStr[0] != '\0') {
                                        try {
                                            // Connect to authdb and get session + user info
                                            uuid::uuid authSessionId(authIdStr);
                                            for (size_t si = 0; si < Args.config->getAuthSourceCount(); ++si) {
                                                const AuthSource &src = Args.config->getAuthSource(si);
                                                try {
                                                    authdb::client::ClientConnection authcon;
                                                    authcon.setUrl(src.url);
                                                    authcon.setClientName(src.clientName);
                                                    authcon.setClientSecret(src.clientSecret);
                                                    authcon.setSessionID(authSessionId);

                                                    authdb::client::Client clt(authcon);
                                                    if (!clt.ClientAuth()) continue;

                                                    // Get user uid from session
                                                    authdb::SessionData sdat;
                                                    clt.SessionInfo(sdat);
                                                    // Get user uid and username from session
                                                    uuid::uuid uid;
                                                    sdat.getUid(uid);
                                                    class authdb::UserData ud(uid);
                                                    clt.UserInfo(ud);
                                                    std::string username = ud.getUsername();
                                                    std::string displayname = ud.getFirstname() + " " + ud.getLastname();
                                                    std::string email = ud.getMail();

                                                    // Find or create local user by authdb uid
                                                    std::vector<char> sbuf;
                                                    dbpp::SQL asql;
                                                    dbpp::DBResult ares;
                                                    asql << "SELECT id FROM users WHERE sid='" << dbpp::SQL::escaped(sbuf, uid.c_str()) << "' LIMIT 1;";
                                                    int ucount = Args.database[tid]->exec(asql, ares);
                                                    if (ucount > 0) {
                                                        authorId = atoi(ares[0][0]);
                                                        // Update displayname in case it changed
                                                        dbpp::SQL usql;
                                                        dbpp::DBResult ures;
                                                        usql << "UPDATE users SET displayname='" << dbpp::SQL::escaped(sbuf, displayname.c_str())
                                                             << "', username='" << dbpp::SQL::escaped(sbuf, username.c_str())
                                                             << "', email='" << dbpp::SQL::escaped(sbuf, email.c_str())
                                                             << "' WHERE id=" << authorId << ";";
                                                        try { Args.database[tid]->exec(usql, ures); } catch (...) {}
                                                    } else {
                                                        // Create new local user from authdb info
                                                        dbpp::SQL isql;
                                                        dbpp::DBResult ires;
                                                        isql << "INSERT INTO users (sid,username,email,displayname) VALUES ('"
                                                             << dbpp::SQL::escaped(sbuf, uid.c_str()) << "','"
                                                             << dbpp::SQL::escaped(sbuf, username.c_str()) << "','"
                                                             << dbpp::SQL::escaped(sbuf, email.c_str()) << "','"
                                                             << dbpp::SQL::escaped(sbuf, displayname.c_str()) << "') RETURNING id;";
                                                        Args.database[tid]->exec(isql, ires);
                                                        authorId = atoi(ires[0][0]);
                                                    }
                                                    break;
                                                } catch (...) {
                                                    continue;
                                                }
                                            }
                                        } catch (...) {}
                                    }
                                    break;
                                }
                            }

                            // Inject author_id into params so the plugin can use it
                            if (authorId > 0) {
                                if (!params) {
                                    params = json_object_new_object();
                                    json_object_object_add(single_request, "params", params);
                                }
                                json_object_object_add(params, "author_id", json_object_new_int(authorId));
                            }

                            api->Publish(tid, renderedHtml, params, single_response);

                            // After successful publish, broadcast sharePost to all plugins
                            json_object *jstatus = nullptr;
                            if(json_object_object_get_ex(single_response, "status", &jstatus) &&
                               strcmp(json_object_get_string(jstatus), "success") == 0) {
                                std::string pubUrl, pubTitle, pubDesc;
                                json_object *jurl = nullptr;
                                if(json_object_object_get_ex(single_response, "url", &jurl))
                                    pubUrl = json_object_get_string(jurl);
                                if(params){
                                    json_object *jt = nullptr, *jd = nullptr;
                                    if(json_object_object_get_ex(params, "title", &jt))
                                        pubTitle = json_object_get_string(jt);
                                    if(json_object_object_get_ex(params, "description", &jd))
                                        pubDesc = json_object_get_string(jd);
                                }
                                for(const Plugin::PluginData *splg = plugins->getFirstPlugin(); splg; splg = splg->getNextPlg()){
                                    splg->getInstace()->sharePost(tid, ShareText, pubUrl, pubTitle, pubDesc);
                                }
                            }

                            found = true;
                            break;
                        }
                    }
                }
                if (!found) {
                    json_object_object_add(single_response, "status", json_object_new_string("error"));
                    json_object_object_add(single_response, "message",
                        json_object_new_string("Target plugin not found or does not support publishing."));
                }
            }
            handled = true;
            }
        } else if (strcmp(command, "share_post") == 0) {
            if(!checkEditorGPO(request, GPO_EDITOR_SHARE)){
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message", json_object_new_string("GPO denied: Share Posts"));
                handled = true;
            } else {
            // Manually share a URL to social networks via sharePost broadcast
            std::string shareUrl, shareTitle, shareDesc;
            json_object *param_obj = nullptr;
            if (json_object_object_get_ex(single_request, "url", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) shareUrl = v;
            }
            if (json_object_object_get_ex(single_request, "title", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) shareTitle = v;
            }
            if (json_object_object_get_ex(single_request, "description", &param_obj)) {
                const char *v = json_object_get_string(param_obj);
                if (v) shareDesc = v;
            }
            if (shareUrl.empty()) {
                json_object_object_add(single_response, "status", json_object_new_string("error"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Missing 'url' parameter."));
            } else {
                if (plugins) {
                    for (const Plugin::PluginData *splg = plugins->getFirstPlugin(); splg; splg = splg->getNextPlg()) {
                        splg->getInstace()->sharePost(tid, ShareText, shareUrl, shareTitle, shareDesc);
                    }
                }
                json_object_object_add(single_response, "status", json_object_new_string("success"));
                json_object_object_add(single_response, "message",
                    json_object_new_string("Shared successfully."));
            }
            handled = true;
            }
        }


        // The original code had the plugin loading here, which is now moved.

        // --- 2. Broadcast to Plugins (For Element-Specific Commands) ---
        if (!handled) {
            for(const EditPluginLoad::PluginData *curplg = Plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()){
                // Call the plugin's API method. The plugin should check if the command is for it.
                curplg->getInstace()->JsonApi(single_request, single_response);

                // We must check if the response was populated by the plugin to stop routing.
                if (json_object_object_get(single_response, "status") != nullptr || json_object_object_get(single_response, "success") != nullptr) {
                    handled = true;
                    break; // Command was handled by this plugin, stop broadcasting
                }
            }
        }

        // --- 3. Delegate to main plugins (e.g., media) ---
        if (!handled && plugins) {
            uuid::uuid authid;
            size_t reqLen = json_object_array_length(request);
            for (size_t ri = 0; ri < reqLen; ++ri) {
                json_object *reqItem = json_object_array_get_idx(request, ri);
                json_object *authObj = nullptr;
                if (json_object_object_get_ex(reqItem, "authid", &authObj)) {
                    const char *authIdStr = json_object_get_string(authObj);
                    if (authIdStr && authIdStr[0] != '\0') {
                        authid = authIdStr;
                    }
                    break;
                }
            }
            for (const Plugin::PluginData *curplg = plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) {
                PluginApi *api = curplg->getInstace();
                if (api) {
                    api->JsonApi(tid, single_request, single_response, authid);
                    if (json_object_object_get(single_response, "status") != nullptr) {
                        handled = true;

                        // After media_upload success, broadcast sharePost with media type
                        json_object *jcmd2 = nullptr;
                        json_object_object_get_ex(single_request, "command", &jcmd2);
                        const char *cmd2 = jcmd2 ? json_object_get_string(jcmd2) : "";
                        if(cmd2 && strcmp(cmd2, "media_upload") == 0){
                            json_object *jst2 = nullptr;
                            if(json_object_object_get_ex(single_response, "status", &jst2) &&
                               strcmp(json_object_get_string(jst2), "success") == 0){
                                json_object *jmedia = nullptr;
                                if(json_object_object_get_ex(single_response, "media", &jmedia)){
                                    json_object *jkind = nullptr, *jmurl = nullptr, *jmfn = nullptr;
                                    json_object_object_get_ex(jmedia, "kind", &jkind);
                                    json_object_object_get_ex(jmedia, "url", &jmurl);
                                    json_object_object_get_ex(jmedia, "filename", &jmfn);
                                    const char *kind = jkind ? json_object_get_string(jkind) : "";
                                    const char *murl = jmurl ? json_object_get_string(jmurl) : "";
                                    const char *mfn = jmfn ? json_object_get_string(jmfn) : "";
                                    SharePostType spt = SharePicture;
                                    if(kind && strcmp(kind, "video") == 0) spt = ShareVideo;
                                    else if(kind && strcmp(kind, "audio") == 0) spt = ShareAudio;
                                    for(const Plugin::PluginData *splg = plugins->getFirstPlugin(); splg; splg = splg->getNextPlg()){
                                        splg->getInstace()->sharePost(tid, spt, murl ? murl : "", mfn ? mfn : "", "");
                                    }
                                }
                            }
                        }

                        break;
                    }
                }
            }
        }

        if (!handled) {
            // If the command was not a global command and was not handled by any plugin
            json_object_object_add(single_response, "status", json_object_new_string("error"));
            json_object_object_add(single_response, "message",
                json_object_new_string("Command not recognized by WebEditor or any loaded plugin.")
            );
        }
    };

    if (json_object_get_type(request) == json_type_array) {
        // Reuse existing response objects already created by blogi.cpp's api() handler.
        // blogi.cpp creates one response entry per request entry; we fill in any that
        // are still empty (i.e. commands it didn't handle like "SaveTemplateDb").
        int array_len = json_object_array_length(request);
        int resp_len  = json_object_array_length(response);
        for (int i = 0; i < array_len; i++) {
            json_object *single_request = json_object_array_get_idx(request, i);
            json_object *single_response = nullptr;
            if (i < resp_len) {
                // Reuse the existing response slot from blogi.cpp
                single_response = json_object_array_get_idx(response, i);
            }
            if (!single_response) {
                // Shouldn't happen, but create a new one if needed
                single_response = json_object_new_object();
                json_object_array_add(response, single_response);
            }
            jsonCommand(single_request, single_response);
        }
    } else {
        // If it's a single object (backward compatibility), process it directly.
        jsonCommand(request, response);
    }
}

void blogi::webedit::WebEditor::Renderer(
    libhttppp::HttpRequest& curreq, const int tid,
    const std::string &sessionid,const libhtmlpp::HtmlElement &index,
    Plugin *plugins,
    const std::string &publishTarget,
    json_object *publishParams,
    const std::string &html
){
    libhtmlpp::HtmlString out;
    libhtmlpp::HtmlElement ridx=index;
    libhtmlpp::HtmlPage curpage;

    libhtmlpp::HtmlElement *root=ridx.getElementbyID("main");

    std::vector<std::string> edplgdirs;
    for(size_t i=0; i<Args.config->getplgdirs(); ++i){
        edplgdirs.emplace_back(Args.config->getplgdir(i)+"/webedit");
    }

    if(root){
        // Emit global CSS (e.g. :root vars, @keyframes, @font-face) before widgets
        if (!GlobalCss.empty()) {
            libhtmlpp::HtmlElement globalStyle("style");
            globalStyle.appendChild(libhtmlpp::TextElement(GlobalCss.c_str()));
            root->appendChild(globalStyle);
        }

        if (!html.empty()) {
            // Client sent pre-rendered HTML — inject it directly
            libhtmlpp::HtmlString htmlStr;
            htmlStr << html.c_str();
            root->appendChild(htmlStr.parse());
        } else {
            // Render user-created document elements into root (server-side fallback)
            if (DocRoot) {
                const EditPlugin *el = DocRoot;
                while (el) {
                    el->Render(*root);
                    el = el->nextElement();
                }
            }
        }

        // Inject click-to-select and highlight script into rendered preview
        libhtmlpp::HtmlElement selectScript("script");
        selectScript.appendChild(libhtmlpp::TextElement(
            "(function(){"
            "var currentHighlight=null;"
            "function highlight(el){"
            "  if(currentHighlight)currentHighlight.style.outline=currentHighlight._oldOutline||'';"
            "  if(el){"
            "    el._oldOutline=el.style.outline;"
            "    el.style.outline='2px solid red';"
            "  }"
            "  currentHighlight=el;"
            "}"
            "document.addEventListener('click',function(e){"
            "  var t=e.target;"
            "  while(t&&t!==document.body){"
            "    if(t.id&&t.id.indexOf('we-')===0){"
            "      e.preventDefault();e.stopPropagation();"
            "      var uid=t.id.substring(3);"
            "      highlight(t);"
            "      if(window.parent&&window.parent!==window){"
            "        window.parent.postMessage({type:'elementClicked',elementId:uid},'*');"
            "      }"
            "      return;"
            "    }"
            "    t=t.parentElement;"
            "  }"
            "},true);"
            "window.addEventListener('message',function(e){"
            "  if(e.data&&e.data.type==='highlightElement'){"
            "    var uid=e.data.elementId;"
            "    if(!uid){highlight(null);return;}"
            "    var el=document.getElementById('we-'+uid);"
            "    highlight(el||null);"
            "    if(el)el.scrollIntoView({behavior:'smooth',block:'nearest'});"
            "  }"
            "});"
            "})();"
        ));
        root->appendChild(selectScript);
    }

    // If publish preview requested, wrap the rendered content through the target plugin
    if (!publishTarget.empty() && plugins && root) {
        libhtmlpp::HtmlString rootHtml;
        libhtmlpp::print(*root, rootHtml);
        std::string renderedHtml = rootHtml.str();

        for (const Plugin::PluginData *curplg = plugins->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) {
            PluginApi *api = curplg->getInstace();
            if (api && api->canPublish() && api->getName() == publishTarget) {
                api->RenderPublished(tid, renderedHtml, publishParams, ridx);
                break;
            }
        }
    }

    Args.theme->printSite(tid,out,&ridx,curreq.getRequestURL(),false);

    libhttppp::HttpResponse curres;
    curres.setState(HTTP200);
    curres.setContentType("text/html");
    curres.send(curreq,out.data(),out.length());
}
