/*******************************************************************************
 * Survey Plugin for Blogi
 *
 * Tables:
 *   survey_templates (name PK, xml_data, title)
 *   survey_results   (id UUID PK, survey_name, firstname, lastname, company,
 *                      email, response TEXT, created_at)
 *
 * Routes (Controller):
 *   GET  PREFIX/survey/<name>   -> HTML survey form (participant step + questions)
 *   POST PREFIX/survey/<name>   -> store JSON response + images, send emails
 *
 * Settings:
 *   list / add / delete surveys, view results,
 *   configure receiver email + temp directory
 *******************************************************************************/

#include <iostream>
#include <sstream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <filesystem>

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

#include <httppp/http.h>
#include <httppp/exception.h>

#include <dbpp/database.h>

#include <tinyxml2.h>
#include <json-c/json.h>

#include <conf.h>
#include <plugin.h>
#include <blogi.h>
#include <mail.h>
#include <i18n.h>
#include <compress.h>

#include <netplus/crypto/rsa.h>
#include <netplus/crypto/rsa_pkcs1_sha256.h>

namespace fs = std::filesystem;

namespace blogi {

    struct SurveyOption {
        std::string value;
        std::string label;
        std::string excludes;  // comma-separated question IDs to skip when selected
        std::string icon;      // media URL for icon type
    };

    struct SurveyQuestion {
        std::string id;
        std::string type;     // radio, checkbox, text, select, rating, image, icon
        std::string text;
        bool        required = false;
        bool        multiSelect = false;
        int         order = 0;
        int         ratingMin = 1;
        int         ratingMax = 5;
        std::string acceptTypes;
        int         maxFileSize = 5 * 1024 * 1024;
        int         maxFileCount = 5;
        std::vector<SurveyOption> options;
    };

    struct SurveyTemplate {
        std::string name;
        std::string id;
        std::string title;
        std::string description;
        std::vector<SurveyQuestion> questions;
    };

    class Survey : public PluginApi {
    public:
        Survey() {}
        ~Survey() {}

        void initPlugin() {
            registerGPO("254f20a3-d22e-4bf5-8bc8-4252cebde29d",
                        "Blogi: Survey Settings",
                        "Controls access to survey management",
                        true);

            dbpp::SQL sql;
            dbpp::DBResult res;

            sql << "CREATE TABLE IF NOT EXISTS survey_templates ("
                << "name character varying(255) PRIMARY KEY,"
                << "xml_data text NOT NULL,"
                << "title character varying(512)"
                << ");";
            Args->database[0]->exec(sql, res);

            sql.clear();
            sql << "CREATE TABLE IF NOT EXISTS survey_results ("
                << "id " << Args->database[0]->getUUIDType(sql) << " PRIMARY KEY,"
                << "survey_name character varying(255) NOT NULL,"
                << "firstname character varying(255) NOT NULL,"
                << "lastname character varying(255) NOT NULL,"
                << "company character varying(255) DEFAULT '',"
                << "email character varying(255) NOT NULL,"
                << "response text NOT NULL,"
                << "created_at character varying(64) NOT NULL"
                << ");";
            Args->database[0]->exec(sql, res);

            sql.clear();
            sql << "CREATE TABLE IF NOT EXISTS survey_email_templates ("
                << "name character varying(255) PRIMARY KEY,"
                << "html_body text NOT NULL"
                << ");";
            Args->database[0]->exec(sql, res);

            // Load settings
            try { getConfig(*Args->database[0], "SURVEY_RECEIVER", _receiverEmail); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_COMPANY_REQUIRED", _companyRequired); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_ENABLED", _gdriveEnabled); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_AUTH_MODE", _gdriveAuthMode); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_SA_FILE", _gdriveServiceAccountFile); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_SPREADSHEET_ID", _gdriveSpreadsheetId); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_CLIENT_ID", _gdriveClientId); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_CLIENT_SECRET", _gdriveClientSecret); } catch (...) {}
            try { getConfig(*Args->database[0], "SURVEY_GDRIVE_REFRESH_TOKEN", _gdriveRefreshToken); } catch (...) {}
            _tmpDir = Args->config->getTmpDir();

            loadTemplates(0);
            std::cout << "survey plugin loaded (" << _templates.size() << " surveys)" << std::endl;
        }

        bool haveSettings() { return true; }

        const std::string getName() { return "survey"; }
        const std::string getVersion() { return "1.2"; }
        const std::string getAuthor() { return "Jan Koester"; }

        // ----- Settings -----
        json_object* Settings(const int tid, libhttppp::HttpRequest &req, const std::string &sessionid) {
            std::string curl = req.getRequestURL();
            std::string base = Args->config->buildurl("settings/survey/");

            if (curl.length() > base.length()) {
                std::string sub = curl.substr(base.length());
                if (sub.find("editor") == 0) return settingsEditor(tid, req);
                else if (sub.find("add") == 0)        return settingsAdd(tid, req);
                else if (sub.find("delete") == 0) return settingsDelete(tid, req);
                else if (sub.find("results") == 0) return settingsResults(tid, req);
                else if (sub.find("config") == 0)  return settingsConfig(tid, req);
                else if (sub.find("import") == 0) return settingsImportForm(tid, req);
                else if (sub.find("emailtemplate") == 0) return settingsEmailTemplate(tid, req);
                else if (sub.find("gdrive") == 0) return settingsGDrive(tid, req);
                else if (sub.find("api") == 0) return settingsApi(tid, req);
            }
            return settingsIndex(tid, req);
        }

        // ----- JSON API (called from /api) -----
        void JsonApi(const int tid, json_object *request, json_object *response, const uuid::uuid &authid) override {
            json_object *cmd_obj = nullptr;
            if (!json_object_object_get_ex(request, "command", &cmd_obj))
                return;
            const char *command = json_object_get_string(cmd_obj);
            if (!command) return;

            if (strcmp(command, "list_surveys") == 0) {
                json_object *surveys = json_object_new_array();
                try {
                    dbpp::SQL sql;
                    dbpp::DBResult res;
                    sql << "SELECT name FROM survey_templates ORDER BY name";
                    int n = Args->database[tid]->exec(sql, res);
                    for (int si = 0; si < n; si++) {
                        if (res[si][0])
                            json_object_array_add(surveys, json_object_new_string(res[si][0]));
                    }
                } catch (...) {
                    std::cerr << "list_surveys: DB error" << std::endl;
                }
                json_object_object_add(response, "status", json_object_new_string("success"));
                json_object_object_add(response, "surveys", surveys);
            }
        }

        // ----- Controller -----
        bool Controller(const int tid, libhttppp::HttpRequest &req,
                        libhtmlpp::HtmlElement *page, const std::string &sessionid) {
            std::string url = req.getRequestURL();
            std::string surveyBase = Args->config->buildurl("survey/");

            if (url.compare(0, surveyBase.length(), surveyBase) != 0)
                return false;

            std::string surveyName = url.substr(surveyBase.length());
            if (!surveyName.empty() && surveyName.back() == '/')
                surveyName.pop_back();
            if (surveyName.empty()) return false;

            // Authenticated API save endpoint
            if (surveyName == "_api/save" && req.getRequestType() == POSTREQUEST) {
                return handleApiSave(tid, req, sessionid);
            }

            // Export endpoint: survey/_api/export?name=xxx
            if (surveyName == "_api/export") {
                return handleExport(tid, req, sessionid);
            }

            // Import endpoint: survey/_api/import (POST)
            if (surveyName == "_api/import" && req.getRequestType() == POSTREQUEST) {
                return handleImport(tid, req, sessionid);
            }

            // OAuth2 callback for Google Sheets
            if (surveyName.find("_api/gdrive/callback") == 0) {
                return handleGDriveCallback(tid, req, sessionid);
            }

            auto it = _templates.find(surveyName);
            if (it == _templates.end()) return false;

            if (req.getRequestType() == POSTREQUEST)
                return handleSubmit(tid, req, it->second);

            return renderSurvey(tid, req, page, sessionid, it->second);
        }

    private:
        std::map<std::string, SurveyTemplate> _templates;
        std::string _receiverEmail;
        std::string _tmpDir = "/tmp/blogi";
        bool _companyRequired = false;
        bool _gdriveEnabled = false;
        std::string _gdriveAuthMode = "serviceaccount"; // "serviceaccount" or "oauth"
        std::string _gdriveServiceAccountFile;
        std::string _gdriveSpreadsheetId;
        std::string _gdriveAccessToken;
        time_t _gdriveTokenExpiry = 0;
        // OAuth2 fields
        std::string _gdriveClientId;
        std::string _gdriveClientSecret;
        std::string _gdriveRefreshToken;
        std::set<std::string> _gdriveSheetHeadersDone;

        // ----- Load templates from DB -----
        void loadTemplates(const int tid) {
            _templates.clear();
            dbpp::SQL sql;
            dbpp::DBResult res;
            sql << "SELECT name, xml_data FROM survey_templates";
            int count = Args->database[tid]->exec(sql, res);
            for (int i = 0; i < count; ++i) {
                std::string name = res[i][0];
                std::string data = res[i][1];
                SurveyTemplate tpl;
                // Try JSON first, fall back to XML for backward compatibility
                json_object *jdata = json_tokener_parse(data.c_str());
                if (jdata && json_object_is_type(jdata, json_type_object) && templateFromJson(jdata, tpl)) {
                    json_object_put(jdata);
                    tpl.name = name;
                    _templates[name] = std::move(tpl);
                } else {
                    if (jdata) json_object_put(jdata);
                    if (parseXml(data, tpl)) {
                        tpl.name = name;
                        _templates[name] = std::move(tpl);
                        // Migrate XML to JSON in DB
                        json_object *jtpl = templateToJson(tpl);
                        const char *jsonStr = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE);
                        dbpp::SQL upd;
                        dbpp::DBResult updRes;
                        std::vector<char> sbuf;
                        upd << "UPDATE survey_templates SET xml_data='" << upd.escaped(sbuf, jsonStr) << "' WHERE name='" << upd.escaped(sbuf, name.c_str()) << "';";
                        try { Args->database[tid]->exec(upd, updRes); } catch (...) {}
                        json_object_put(jtpl);
                    }
                }
            }
        }

        // ----- Parse XML -----
        bool parseXml(const std::string &xmlStr, SurveyTemplate &tpl) {
            tinyxml2::XMLDocument doc;
            if (doc.Parse(xmlStr.c_str(), xmlStr.size()) != tinyxml2::XML_SUCCESS)
                return false;

            tinyxml2::XMLElement *surveyEl = doc.FirstChildElement("survey");
            if (!surveyEl) return false;

            const char *id = surveyEl->Attribute("id");
            const char *title = surveyEl->Attribute("title");
            tpl.id    = id    ? id    : "";
            tpl.title = title ? title : "";

            tinyxml2::XMLElement *descEl = surveyEl->FirstChildElement("description");
            if (descEl && descEl->GetText()) tpl.description = descEl->GetText();

            for (tinyxml2::XMLElement *qEl = surveyEl->FirstChildElement("question");
                 qEl; qEl = qEl->NextSiblingElement("question")) {
                SurveyQuestion q;
                const char *qid = qEl->Attribute("id");
                const char *qtype = qEl->Attribute("type");
                const char *qreq = qEl->Attribute("required");
                q.id   = qid   ? qid   : "";
                q.type = qtype ? qtype : "text";
                q.required = qreq && std::string(qreq) == "true";
                const char *qmulti = qEl->Attribute("multiselect");
                q.multiSelect = qmulti && std::string(qmulti) == "true";
                qEl->QueryIntAttribute("order", &q.order);
                qEl->QueryIntAttribute("min", &q.ratingMin);
                qEl->QueryIntAttribute("max", &q.ratingMax);

                const char *accept = qEl->Attribute("accept");
                if (accept) q.acceptTypes = accept;
                qEl->QueryIntAttribute("maxsize",  &q.maxFileSize);
                qEl->QueryIntAttribute("maxcount", &q.maxFileCount);

                tinyxml2::XMLElement *textEl = qEl->FirstChildElement("text");
                if (textEl && textEl->GetText()) q.text = textEl->GetText();

                for (tinyxml2::XMLElement *optEl = qEl->FirstChildElement("option");
                     optEl; optEl = optEl->NextSiblingElement("option")) {
                    SurveyOption opt;
                    const char *val = optEl->Attribute("value");
                    opt.value = val ? val : "";
                    opt.label = optEl->GetText() ? optEl->GetText() : "";
                    const char *exc = optEl->Attribute("excludes");
                    if (exc) opt.excludes = exc;
                    const char *ico = optEl->Attribute("icon");
                    if (ico) opt.icon = ico;
                    q.options.push_back(std::move(opt));
                }
                tpl.questions.push_back(std::move(q));
            }

            // Auto-migrate old qN-style IDs to UUIDs and fix duplicates
            std::set<std::string> seenIds;
            bool needsMigration = false;
            for (auto &q : tpl.questions) {
                bool isOldStyle = (q.id.size() >= 2 && q.id[0] == 'q' &&
                    std::all_of(q.id.begin() + 1, q.id.end(), ::isdigit));
                if (q.id.empty() || isOldStyle || seenIds.count(q.id)) {
                    uuid::uuid newId;
                    newId.generate();
                    q.id = newId.c_str();
                    needsMigration = true;
                }
                seenIds.insert(q.id);
            }

            // Auto-assign order if all questions have order 0
            bool allZero = true;
            for (const auto &q : tpl.questions) {
                if (q.order != 0) { allZero = false; break; }
            }
            if (allZero) {
                for (size_t i = 0; i < tpl.questions.size(); ++i)
                    tpl.questions[i].order = static_cast<int>(i + 1);
                needsMigration = true;
            }

            return true;
        }

        // ----- JSON escape -----
        static std::string jsonEscape(const std::string &s) {
            std::string out;
            out.reserve(s.size() + 8);
            for (char c : s) {
                switch (c) {
                    case '"':  out += "\\\""; break;
                    case '\\': out += "\\\\"; break;
                    case '\n': out += "\\n";  break;
                    case '\r': out += "\\r";  break;
                    case '\t': out += "\\t";  break;
                    default:   out += c;
                }
            }
            return out;
        }

        // ----- Base64 decode -----
        static std::string base64Decode(const std::string &encoded) {
            static const std::string chars =
                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
            std::vector<int> T(256, -1);
            for (int i = 0; i < 64; i++) T[chars[i]] = i;
            std::string out;
            out.reserve(encoded.size() * 3 / 4);
            int val = 0, valb = -8;
            for (unsigned char c : encoded) {
                if (T[c] == -1) continue;
                val = (val << 6) + T[c];
                valb += 6;
                if (valb >= 0) { out.push_back(char((val >> valb) & 0xFF)); valb -= 8; }
            }
            return out;
        }

        // ----- Save data-URI image to temp dir, return file path -----
        std::string saveImage(const std::string &dataUri, const std::string &submissionId) {
            // data:image/jpeg;base64,/9j/4AAQ...
            size_t commaPos = dataUri.find(',');
            if (commaPos == std::string::npos) return "";

            std::string b64 = dataUri.substr(commaPos + 1);
            std::string decoded = base64Decode(b64);
            if (decoded.empty()) return "";

            // Determine extension from MIME
            std::string ext = ".png";
            size_t colonPos = dataUri.find(':');
            size_t semiPos  = dataUri.find(';');
            if (colonPos != std::string::npos && semiPos != std::string::npos && semiPos > colonPos) {
                std::string mime = dataUri.substr(colonPos + 1, semiPos - colonPos - 1);
                if (mime == "image/jpeg" || mime == "image/jpg") ext = ".jpg";
                else if (mime == "image/png")  ext = ".png";
                else if (mime == "image/gif")  ext = ".gif";
                else if (mime == "image/webp") ext = ".webp";
            }

            // Create directory: tmpDir/submissionId/
            fs::path dir = fs::path(_tmpDir) / submissionId;
            std::error_code ec;
            fs::create_directories(dir, ec);
            if (ec) { std::cerr << "survey: mkdir error: " << ec.message() << std::endl; return ""; }

            // Generate filename
            uuid::uuid fileId;
            fileId.generate();
            std::string filename = std::string(fileId.c_str()) + ext;
            fs::path filePath = dir / filename;

            std::ofstream ofs(filePath, std::ios::binary);
            if (!ofs) { std::cerr << "survey: write error: " << filePath << std::endl; return ""; }
            ofs.write(decoded.data(), decoded.size());
            ofs.close();

            return filePath.string();
        }

        // ----- Validate response -----
        std::string validateResponse(const SurveyTemplate &tpl, json_object *root) {
            if (!json_object_is_type(root, json_type_object))
                return "Response must be a JSON object";

            // Require participant fields
            json_object *fn = nullptr, *ln = nullptr, *em = nullptr;
            json_object_object_get_ex(root, "_firstname", &fn);
            json_object_object_get_ex(root, "_lastname", &ln);
            json_object_object_get_ex(root, "_email", &em);
            if (!fn || !json_object_is_type(fn, json_type_string) || strlen(json_object_get_string(fn)) == 0)
                return "First name is required";
            if (!ln || !json_object_is_type(ln, json_type_string) || strlen(json_object_get_string(ln)) == 0)
                return "Last name is required";
            if (!em || !json_object_is_type(em, json_type_string) || strlen(json_object_get_string(em)) == 0)
                return "Email is required";

            // Company: validate if required
            if (_companyRequired) {
                json_object *co = nullptr;
                json_object_object_get_ex(root, "_company", &co);
                if (!co || !json_object_is_type(co, json_type_string) || strlen(json_object_get_string(co)) == 0)
                    return "Company is required";
            }

            // Build set of excluded question IDs based on selected answers
            std::set<std::string> excludedIds;
            for (const auto &qq : tpl.questions) {
                json_object *f = nullptr;
                if (!json_object_object_get_ex(root, qq.id.c_str(), &f) || !f) continue;

                auto collectExcludes = [&](const std::string &selectedValue) {
                    for (const auto &opt : qq.options) {
                        if (opt.value == selectedValue && !opt.excludes.empty()) {
                            std::istringstream ss(opt.excludes);
                            std::string tok;
                            while (std::getline(ss, tok, ',')) {
                                size_t start = tok.find_first_not_of(' ');
                                size_t end = tok.find_last_not_of(' ');
                                if (start != std::string::npos)
                                    excludedIds.insert(tok.substr(start, end - start + 1));
                            }
                        }
                    }
                };

                if ((qq.type == "radio" || qq.type == "select" || (qq.type == "icon" && !qq.multiSelect)) && json_object_is_type(f, json_type_string)) {
                    collectExcludes(json_object_get_string(f));
                } else if ((qq.type == "checkbox" || (qq.type == "icon" && qq.multiSelect)) && json_object_is_type(f, json_type_array)) {
                    size_t len = json_object_array_length(f);
                    for (size_t i = 0; i < len; ++i) {
                        json_object *item = json_object_array_get_idx(f, i);
                        if (json_object_is_type(item, json_type_string))
                            collectExcludes(json_object_get_string(item));
                    }
                }
            }

            for (const auto &q : tpl.questions) {
                // Skip excluded questions
                if (excludedIds.count(q.id)) continue;

                json_object *field = nullptr;
                bool has = json_object_object_get_ex(root, q.id.c_str(), &field);

                if (q.required) {
                    if (!has || !field || json_object_is_type(field, json_type_null))
                        return "Required field missing: " + q.id;
                    if (json_object_is_type(field, json_type_string) &&
                        strlen(json_object_get_string(field)) == 0)
                        return "Required field empty: " + q.id;
                    if (json_object_is_type(field, json_type_array) &&
                        json_object_array_length(field) == 0)
                        return "Required field empty: " + q.id;
                }

                if (!has || !field || json_object_is_type(field, json_type_null))
                    continue;

                if (q.type == "radio" || q.type == "select" || (q.type == "icon" && !q.multiSelect)) {
                    if (!json_object_is_type(field, json_type_string))
                        return "Field " + q.id + " must be a string";
                    std::string val = json_object_get_string(field);
                    if (!val.empty()) {
                        bool found = false;
                        for (const auto &opt : q.options)
                            if (opt.value == val) { found = true; break; }
                        if (!found) {
                            std::cerr << "survey: validation failed for " << q.id << " type=" << q.type
                                      << " multiSelect=" << q.multiSelect << " submitted='" << val << "' options=[";
                            for (size_t oi = 0; oi < q.options.size(); ++oi)
                                std::cerr << (oi ? "," : "") << "'" << q.options[oi].value << "'";
                            std::cerr << "]" << std::endl;
                            return "Invalid value for " + q.id;
                        }
                    }
                } else if (q.type == "checkbox" || (q.type == "icon" && q.multiSelect)) {
                    if (!json_object_is_type(field, json_type_array))
                        return "Field " + q.id + " must be an array";
                    size_t len = json_object_array_length(field);
                    for (size_t i = 0; i < len; ++i) {
                        json_object *item = json_object_array_get_idx(field, i);
                        if (!json_object_is_type(item, json_type_string))
                            return "Field " + q.id + ": element must be string";
                        std::string val = json_object_get_string(item);
                        bool found = false;
                        for (const auto &opt : q.options)
                            if (opt.value == val) { found = true; break; }
                        if (!found) return "Invalid value in " + q.id;
                    }
                } else if (q.type == "rating") {
                    int rating = 0;
                    if (json_object_is_type(field, json_type_string)) {
                        try { rating = std::stoi(json_object_get_string(field)); }
                        catch (...) { return "Field " + q.id + ": invalid rating"; }
                    } else if (json_object_is_type(field, json_type_int)) {
                        rating = json_object_get_int(field);
                    } else {
                        return "Field " + q.id + " must be number or string";
                    }
                    if (rating < q.ratingMin || rating > q.ratingMax)
                        return "Field " + q.id + ": value out of range";
                } else if (q.type == "text") {
                    if (!json_object_is_type(field, json_type_string))
                        return "Field " + q.id + " must be a string";
                } else if (q.type == "image") {
                    // image: string or array of data URIs
                    if (json_object_is_type(field, json_type_array)) {
                        size_t len = json_object_array_length(field);
                        if (q.maxFileCount > 0 && (int)len > q.maxFileCount)
                            return "Field " + q.id + ": too many images";
                        for (size_t i = 0; i < len; ++i) {
                            json_object *item = json_object_array_get_idx(field, i);
                            if (!json_object_is_type(item, json_type_string))
                                return "Field " + q.id + ": image must be base64 string";
                            const char *v = json_object_get_string(item);
                            if (v && strncmp(v, "data:image/", 11) != 0)
                                return "Field " + q.id + ": invalid image format";
                        }
                    } else if (json_object_is_type(field, json_type_string)) {
                        const char *v = json_object_get_string(field);
                        if (v && strlen(v) > 0 && strncmp(v, "data:image/", 11) != 0)
                            return "Field " + q.id + ": invalid image format";
                    }
                }
            }
            return "";
        }

        // ----- HTML escape -----
        static std::string htmlEscape(const std::string &s) {
            std::string out;
            out.reserve(s.size() + 16);
            for (char c : s) {
                switch (c) {
                    case '&':  out += "&amp;";  break;
                    case '<':  out += "&lt;";   break;
                    case '>':  out += "&gt;";   break;
                    case '"':  out += "&quot;";  break;
                    case '\'': out += "&#39;";  break;
                    default:   out += c;
                }
            }
            return out;
        }

        // ----- Generate hue (0-359) from string via djb2 hash -----
        static int hueFromId(const std::string &id) {
            unsigned long h = 5381;
            for (char c : id)
                h = ((h << 5) + h) + static_cast<unsigned char>(c);
            return static_cast<int>(h % 360);
        }

        // ----- Format HSL color string -----
        static std::string hsl(int h, int s, int l) {
            return "hsl(" + std::to_string(h) + "," + std::to_string(s) + "%," + std::to_string(l) + "%)";
        }

        // ----- Build Google Sheet tab name: domain (dots->underscores) + survey name -----
        std::string buildSheetName(const std::string &surveyName) {
            std::string domain = Args->config->getDomain();
            std::string sheet;
            sheet.reserve(domain.size() + 1 + surveyName.size());
            for (char c : domain) {
                sheet += (c == '.') ? '_' : c;
            }
            sheet += '_';
            sheet += surveyName;
            return sheet;
        }

        // ----- Ensure a sheet tab exists in the spreadsheet -----
        bool ensureSheetExists(const std::string &sheetName, const SurveyTemplate *tpl = nullptr) {
            if (!refreshGoogleAccessToken()) return false;
            bool created = false;
            try {
                std::string path = "/v4/spreadsheets/" + _gdriveSpreadsheetId + ":batchUpdate";
                libhttppp::HttpUrl url("https://sheets.googleapis.com" + path);
                libhttppp::HttpClient client(url);

                libhttppp::HttpRequest addReq;
                addReq.setRequestType(POSTREQUEST);
                addReq.setRequestURL(path);
                addReq.setHeaderData("host")->push_back("sheets.googleapis.com");
                addReq.setHeaderData("content-type")->push_back("application/json");
                addReq.setHeaderData("authorization")->push_back("Bearer " + _gdriveAccessToken);

                std::string body = "{\"requests\":[{\"addSheet\":{\"properties\":{\"title\":\"" + jsonEscape(sheetName) + "\"}}}]}";
                std::vector<char> postData(body.begin(), body.end());
                std::vector<char> resp = client.Post(addReq, postData);
                std::string respStr(resp.begin(), resp.end());
                // Check if sheet was created (no error means new)
                json_object *respObj = json_tokener_parse(respStr.c_str());
                if (respObj) {
                    json_object *jErr = nullptr;
                    if (!json_object_object_get_ex(respObj, "error", &jErr))
                        created = true;
                    json_object_put(respObj);
                }
            } catch (...) {
                return false;
            }

            // Write header row when sheet is newly created or not yet initialized
            if (tpl && (created || _gdriveSheetHeadersDone.find(sheetName) == _gdriveSheetHeadersDone.end())) {
                writeSheetHeaders(sheetName, *tpl);
                _gdriveSheetHeadersDone.insert(sheetName);
            }
            return true;
        }

        // ----- Write header row to a Google Sheet tab -----
        void writeSheetHeaders(const std::string &sheetName, const SurveyTemplate &tpl) {
            // First check if there's already data (don't overwrite existing headers)
            try {
                std::string checkPath = "/v4/spreadsheets/" + _gdriveSpreadsheetId
                    + "/values/" + sheetName + "!A1:A1";
                libhttppp::HttpUrl checkUrl("https://sheets.googleapis.com" + checkPath);
                libhttppp::HttpClient checkClient(checkUrl);
                libhttppp::HttpRequest checkReq;
                checkReq.setRequestURL(checkPath);
                checkReq.setHeaderData("host")->push_back("sheets.googleapis.com");
                checkReq.setHeaderData("authorization")->push_back("Bearer " + _gdriveAccessToken);
                std::vector<char> checkResp = checkClient.Get(checkReq);
                std::string checkStr(checkResp.begin(), checkResp.end());
                json_object *checkObj = json_tokener_parse(checkStr.c_str());
                if (checkObj) {
                    json_object *jvals = nullptr;
                    if (json_object_object_get_ex(checkObj, "values", &jvals) &&
                        json_object_array_length(jvals) > 0) {
                        json_object_put(checkObj);
                        return; // Headers already exist
                    }
                    json_object_put(checkObj);
                }
            } catch (...) {}

            // Build header row: Timestamp, Vorname, Nachname, Firma, Email, [Question texts...]
            json_object *jvalues = json_object_new_array();
            json_object *jrow = json_object_new_array();
            json_object_array_add(jrow, json_object_new_string("Zeitstempel"));
            json_object_array_add(jrow, json_object_new_string("Vorname"));
            json_object_array_add(jrow, json_object_new_string("Nachname"));
            json_object_array_add(jrow, json_object_new_string("Firma"));
            json_object_array_add(jrow, json_object_new_string("Email"));

            // Sort questions by order for consistent column ordering
            std::vector<SurveyQuestion> sortedQ = tpl.questions;
            std::sort(sortedQ.begin(), sortedQ.end(),
                [](const SurveyQuestion &a, const SurveyQuestion &b){ return a.order < b.order; });

            for (const auto &q : sortedQ) {
                std::string header = q.text;
                if (header.empty()) header = q.id;
                json_object_array_add(jrow, json_object_new_string(header.c_str()));
            }

            json_object_array_add(jvalues, jrow);
            json_object *jbody = json_object_new_object();
            json_object_object_add(jbody, "values", jvalues);

            const char *bodyStr = json_object_to_json_string_ext(jbody, JSON_C_TO_STRING_NOSLASHESCAPE);
            std::string postBody = bodyStr ? bodyStr : "{}";
            json_object_put(jbody);

            try {
                std::string path = "/v4/spreadsheets/" + _gdriveSpreadsheetId
                    + "/values/" + sheetName + "!A1:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS";
                libhttppp::HttpUrl sheetsUrl("https://sheets.googleapis.com" + path);
                libhttppp::HttpClient client(sheetsUrl);
                libhttppp::HttpRequest sheetsReq;
                sheetsReq.setRequestType(POSTREQUEST);
                sheetsReq.setRequestURL(path);
                sheetsReq.setHeaderData("host")->push_back("sheets.googleapis.com");
                sheetsReq.setHeaderData("content-type")->push_back("application/json");
                sheetsReq.setHeaderData("authorization")->push_back("Bearer " + _gdriveAccessToken);
                std::vector<char> postData(postBody.begin(), postBody.end());
                client.Post(sheetsReq, postData);
            } catch (const std::exception &e) {
                std::cerr << "survey: Google Sheets header write failed: " << e.what() << std::endl;
            }
        }

        // ----- Base64 URL-safe encode (no padding) -----
        static std::string base64UrlEncode(const unsigned char *data, size_t len) {
            static const char tbl[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
            std::string b64;
            b64.reserve((len + 2) / 3 * 4);
            for (size_t i = 0; i < len; i += 3) {
                unsigned int n = ((unsigned int)data[i]) << 16;
                if (i + 1 < len) n |= ((unsigned int)data[i + 1]) << 8;
                if (i + 2 < len) n |= ((unsigned int)data[i + 2]);
                b64 += tbl[(n >> 18) & 0x3F];
                b64 += tbl[(n >> 12) & 0x3F];
                b64 += (i + 1 < len) ? tbl[(n >> 6) & 0x3F] : '=';
                b64 += (i + 2 < len) ? tbl[n & 0x3F] : '=';
            }
            // URL-safe: replace + with -, / with _, strip =
            for (auto &c : b64) {
                if (c == '+') c = '-';
                else if (c == '/') c = '_';
            }
            while (!b64.empty() && b64.back() == '=') b64.pop_back();
            return b64;
        }

        static std::string base64UrlEncode(const std::string &s) {
            return base64UrlEncode(reinterpret_cast<const unsigned char *>(s.data()), s.size());
        }

        // ----- Create JWT for Google service account -----
        std::string createJWT(const std::string &clientEmail, const std::string &privateKeyPem,
                              const std::string &scope) {
            time_t now = time(nullptr);
            time_t exp = now + 3600;

            // Header
            std::string header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";

            // Payload
            std::ostringstream payload;
            payload << "{\"iss\":\"" << jsonEscape(clientEmail) << "\","
                    << "\"scope\":\"" << jsonEscape(scope) << "\","
                    << "\"aud\":\"https://oauth2.googleapis.com/token\","
                    << "\"iat\":" << now << ","
                    << "\"exp\":" << exp << "}";

            std::string headerB64 = base64UrlEncode(header);
            std::string payloadB64 = base64UrlEncode(payload.str());
            std::string signingInput = headerB64 + "." + payloadB64;

            // Sign with RSASSA-PKCS1-v1_5-SHA256 via libnetplus
            try {
                // PEM to DER: strip header/footer lines, base64-decode the body
                std::string pemBody;
                std::istringstream pemStream(privateKeyPem);
                std::string line;
                while (std::getline(pemStream, line)) {
                    // Remove trailing \r
                    if (!line.empty() && line.back() == '\r') line.pop_back();
                    if (line.find("-----") == 0) continue;
                    pemBody += line;
                }
                std::vector<uint8_t> derBytes;
                std::string decoded = base64Decode(pemBody);
                derBytes.assign(decoded.begin(), decoded.end());

                netplus::rsa key(derBytes);
                if (!key) {
                    std::cerr << "survey: failed to load RSA private key" << std::endl;
                    return "";
                }

                std::vector<uint8_t> message(signingInput.begin(), signingInput.end());
                std::vector<uint8_t> sig = netplus::rsa_pkcs1_sha256::sign(key, message);
                if (sig.empty()) {
                    std::cerr << "survey: RSA signing failed" << std::endl;
                    return "";
                }

                return signingInput + "." + base64UrlEncode(sig.data(), sig.size());
            } catch (const std::exception &e) {
                std::cerr << "survey: JWT sign error: " << e.what() << std::endl;
                return "";
            }
        }

        // ----- Get Google access token -----
        bool refreshGoogleAccessToken() {
            if (!_gdriveAccessToken.empty() && time(nullptr) < _gdriveTokenExpiry - 60)
                return true; // Still valid

            if (_gdriveAuthMode == "oauth")
                return refreshGoogleAccessTokenOAuth();
            return refreshGoogleAccessTokenSA();
        }

        // ----- OAuth2 refresh token flow -----
        bool refreshGoogleAccessTokenOAuth() {
            if (_gdriveClientId.empty() || _gdriveClientSecret.empty() || _gdriveRefreshToken.empty()) {
                std::cerr << "survey: OAuth2 not configured (missing client_id, client_secret, or refresh_token)" << std::endl;
                return false;
            }
            try {
                libhttppp::HttpUrl tokenUrl("https://oauth2.googleapis.com/token");
                libhttppp::HttpClient client(tokenUrl);

                libhttppp::HttpRequest tokenReq;
                tokenReq.setRequestType(POSTREQUEST);
                tokenReq.setRequestURL("/token");
                tokenReq.setHeaderData("host")->push_back("oauth2.googleapis.com");
                tokenReq.setHeaderData("content-type")->push_back("application/x-www-form-urlencoded");

                std::string postBody = "grant_type=refresh_token"
                    "&client_id=" + _gdriveClientId
                    + "&client_secret=" + _gdriveClientSecret
                    + "&refresh_token=" + _gdriveRefreshToken;
                std::vector<char> postData(postBody.begin(), postBody.end());

                std::vector<char> resp = client.Post(tokenReq, postData);
                std::string respStr(resp.begin(), resp.end());

                json_object *respObj = json_tokener_parse(respStr.c_str());
                if (!respObj) {
                    std::cerr << "survey: invalid OAuth token response" << std::endl;
                    return false;
                }

                json_object *jToken = nullptr, *jExpIn = nullptr;
                json_object_object_get_ex(respObj, "access_token", &jToken);
                json_object_object_get_ex(respObj, "expires_in", &jExpIn);
                if (!jToken) {
                    std::cerr << "survey: no access_token in OAuth response: " << respStr << std::endl;
                    json_object_put(respObj);
                    return false;
                }

                _gdriveAccessToken = json_object_get_string(jToken);
                int expiresIn = jExpIn ? json_object_get_int(jExpIn) : 3600;
                _gdriveTokenExpiry = time(nullptr) + expiresIn;
                json_object_put(respObj);
                return true;
            } catch (const std::exception &e) {
                std::cerr << "survey: OAuth token refresh failed: " << e.what() << std::endl;
                return false;
            }
        }

        // ----- Service Account JWT flow -----
        bool refreshGoogleAccessTokenSA() {

            // Read service account JSON
            std::ifstream saFile(_gdriveServiceAccountFile);
            if (!saFile) {
                std::cerr << "survey: cannot read service account file: " << _gdriveServiceAccountFile << std::endl;
                return false;
            }
            std::string saJson((std::istreambuf_iterator<char>(saFile)), std::istreambuf_iterator<char>());
            saFile.close();

            json_object *saObj = json_tokener_parse(saJson.c_str());
            if (!saObj) {
                std::cerr << "survey: invalid service account JSON" << std::endl;
                return false;
            }

            json_object *jEmail = nullptr, *jKey = nullptr;
            json_object_object_get_ex(saObj, "client_email", &jEmail);
            json_object_object_get_ex(saObj, "private_key", &jKey);
            if (!jEmail || !jKey) {
                json_object_put(saObj);
                std::cerr << "survey: missing client_email or private_key in SA file" << std::endl;
                return false;
            }

            std::string clientEmail = json_object_get_string(jEmail);
            std::string privateKey = json_object_get_string(jKey);
            json_object_put(saObj);

            std::string jwt = createJWT(clientEmail, privateKey, "https://www.googleapis.com/auth/spreadsheets");
            if (jwt.empty()) {
                std::cerr << "survey: JWT creation failed" << std::endl;
                return false;
            }

            // Exchange JWT for access token via libhttppp
            try {
                libhttppp::HttpUrl tokenUrl("https://oauth2.googleapis.com/token");
                libhttppp::HttpClient client(tokenUrl);

                libhttppp::HttpRequest tokenReq;
                tokenReq.setRequestType(POSTREQUEST);
                tokenReq.setRequestURL("/token");
                tokenReq.setHeaderData("host")->push_back("oauth2.googleapis.com");
                tokenReq.setHeaderData("content-type")->push_back("application/x-www-form-urlencoded");

                std::string postBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + jwt;
                std::vector<char> postData(postBody.begin(), postBody.end());

                std::vector<char> resp = client.Post(tokenReq, postData);
                std::string respStr(resp.begin(), resp.end());

                json_object *respObj = json_tokener_parse(respStr.c_str());
                if (!respObj) {
                    std::cerr << "survey: invalid token response" << std::endl;
                    return false;
                }

                json_object *jToken = nullptr, *jExpIn = nullptr;
                json_object_object_get_ex(respObj, "access_token", &jToken);
                json_object_object_get_ex(respObj, "expires_in", &jExpIn);
                if (!jToken) {
                    std::cerr << "survey: no access_token in response: " << respStr << std::endl;
                    json_object_put(respObj);
                    return false;
                }

                _gdriveAccessToken = json_object_get_string(jToken);
                int expiresIn = jExpIn ? json_object_get_int(jExpIn) : 3600;
                _gdriveTokenExpiry = time(nullptr) + expiresIn;
                json_object_put(respObj);
                return true;
            } catch (const std::exception &e) {
                std::cerr << "survey: token request failed: " << e.what() << std::endl;
                return false;
            }
        }

        // ----- Write survey result to Google Sheet -----
        void writeToGoogleSheet(const SurveyTemplate &tpl,
                                const std::string &firstname, const std::string &lastname,
                                const std::string &company, const std::string &email,
                                json_object *jroot) {
            if (!refreshGoogleAccessToken()) return;

            // Each survey gets its own sheet tab: domain_surveyname
            std::string sheetName = buildSheetName(tpl.name);
            ensureSheetExists(sheetName, &tpl);

            // Timestamp
            time_t now = time(nullptr);
            struct tm tmBuf;
#ifdef _WIN32
            gmtime_s(&tmBuf, &now);
#else
            gmtime_r(&now, &tmBuf);
#endif
            char timeBuf[64];
            strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", &tmBuf);

            // Build row: [Timestamp, Firstname, Lastname, Company, Email, Q1, Q2, ...]
            json_object *jvalues = json_object_new_array();
            json_object *jrow = json_object_new_array();
            json_object_array_add(jrow, json_object_new_string(timeBuf));
            json_object_array_add(jrow, json_object_new_string(firstname.c_str()));
            json_object_array_add(jrow, json_object_new_string(lastname.c_str()));
            json_object_array_add(jrow, json_object_new_string(company.c_str()));
            json_object_array_add(jrow, json_object_new_string(email.c_str()));

            // Add answers for each question
            for (const auto &q : tpl.questions) {
                json_object *field = nullptr;
                if (!json_object_object_get_ex(jroot, q.id.c_str(), &field) || !field) {
                    json_object_array_add(jrow, json_object_new_string(""));
                    continue;
                }
                if (json_object_is_type(field, json_type_string)) {
                    std::string val = json_object_get_string(field);
                    // Resolve label for radio/select
                    if (q.type == "radio" || q.type == "select" || (q.type == "icon" && !q.multiSelect)) {
                        for (const auto &opt : q.options)
                            if (opt.value == val) { val = opt.label; break; }
                    }
                    json_object_array_add(jrow, json_object_new_string(val.c_str()));
                } else if (json_object_is_type(field, json_type_array)) {
                    std::ostringstream vals;
                    size_t len = json_object_array_length(field);
                    for (size_t i = 0; i < len; ++i) {
                        json_object *item = json_object_array_get_idx(field, i);
                        if (!json_object_is_type(item, json_type_string)) continue;
                        std::string val = json_object_get_string(item);
                        if (q.type == "checkbox" || (q.type == "icon" && q.multiSelect)) {
                            for (const auto &opt : q.options)
                                if (opt.value == val) { val = opt.label; break; }
                        }
                        if (i > 0) vals << ", ";
                        vals << val;
                    }
                    json_object_array_add(jrow, json_object_new_string(vals.str().c_str()));
                } else if (json_object_is_type(field, json_type_int)) {
                    json_object_array_add(jrow, json_object_new_string(std::to_string(json_object_get_int(field)).c_str()));
                } else {
                    json_object_array_add(jrow, json_object_new_string(""));
                }
            }

            json_object_array_add(jvalues, jrow);

            json_object *jbody = json_object_new_object();
            json_object_object_add(jbody, "values", jvalues);

            const char *bodyStr = json_object_to_json_string_ext(jbody, JSON_C_TO_STRING_NOSLASHESCAPE);
            std::string postBody = bodyStr ? bodyStr : "{}";
            json_object_put(jbody);

            try {
                std::string path = "/v4/spreadsheets/" + _gdriveSpreadsheetId
                    + "/values/" + sheetName + ":append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS";

                libhttppp::HttpUrl sheetsUrl("https://sheets.googleapis.com" + path);
                libhttppp::HttpClient client(sheetsUrl);

                libhttppp::HttpRequest sheetsReq;
                sheetsReq.setRequestType(POSTREQUEST);
                sheetsReq.setRequestURL(path);
                sheetsReq.setHeaderData("host")->push_back("sheets.googleapis.com");
                sheetsReq.setHeaderData("content-type")->push_back("application/json");
                sheetsReq.setHeaderData("authorization")->push_back("Bearer " + _gdriveAccessToken);

                std::vector<char> postData(postBody.begin(), postBody.end());
                std::vector<char> resp = client.Post(sheetsReq, postData);
                std::string respStr(resp.begin(), resp.end());

                // Check for errors
                json_object *respObj = json_tokener_parse(respStr.c_str());
                if (respObj) {
                    json_object *jErr = nullptr;
                    if (json_object_object_get_ex(respObj, "error", &jErr)) {
                        std::cerr << "survey: Google Sheets error: " << respStr << std::endl;
                    }
                    json_object_put(respObj);
                }
            } catch (const std::exception &e) {
                std::cerr << "survey: Google Sheets append failed: " << e.what() << std::endl;
            }
        }

        // ----- Load email template from DB -----
        std::string loadEmailTemplate(const int tid, const std::string &name) {
            dbpp::SQL sql;
            dbpp::DBResult res;
            std::vector<char> sbuf;
            sql << "SELECT html_body FROM survey_email_templates WHERE name='"
                << sql.escaped(sbuf, name.c_str()) << "'";
            int count = Args->database[tid]->exec(sql, res);
            if (count > 0) return res[0][0];
            return "";
        }

        // ----- Build HTML email body with inline images -----
        std::string buildHtmlEmailBody(const int tid, const SurveyTemplate &tpl,
                                       const std::string &firstname, const std::string &lastname,
                                       const std::string &company, const std::string &email,
                                       json_object *root,
                                       std::vector<MailAttachment> &attachments) {
            // Try to load custom template; use default if not found
            std::string tplHtml = loadEmailTemplate(tid, tpl.name);
            if (tplHtml.empty()) tplHtml = loadEmailTemplate(tid, "default");

            // Build answers HTML
            std::ostringstream answersHtml;
            int cidCounter = 0;

            for (const auto &q : tpl.questions) {
                json_object *field = nullptr;
                if (!json_object_object_get_ex(root, q.id.c_str(), &field)) continue;

                answersHtml << "<tr><td style=\"padding:8px 12px;border-bottom:1px solid #eee;font-weight:600;vertical-align:top;width:40%;\">"
                            << htmlEscape(q.text) << "</td><td style=\"padding:8px 12px;border-bottom:1px solid #eee;\">";

                if (json_object_is_type(field, json_type_string)) {
                    std::string val = json_object_get_string(field);
                    if (q.type == "radio" || q.type == "select" || (q.type == "icon" && !q.multiSelect)) {
                        for (const auto &opt : q.options)
                            if (opt.value == val) { val = opt.label; break; }
                    }
                    if (q.type == "image" && !val.empty()) {
                        // Image file path -> embed as CID
                        std::string cid = "img" + std::to_string(++cidCounter);
                        attachments.push_back({val, fs::path(val).filename().string(), cid});
                        answersHtml << "<img src=\"cid:" << cid << "\" style=\"max-width:300px;max-height:200px;border-radius:6px;\" />";
                    } else {
                        answersHtml << htmlEscape(val);
                    }
                } else if (json_object_is_type(field, json_type_array)) {
                    size_t len = json_object_array_length(field);
                    for (size_t i = 0; i < len; ++i) {
                        json_object *item = json_object_array_get_idx(field, i);
                        if (!json_object_is_type(item, json_type_string)) continue;
                        std::string val = json_object_get_string(item);
                        if (q.type == "checkbox" || (q.type == "icon" && q.multiSelect)) {
                            for (const auto &opt : q.options)
                                if (opt.value == val) { val = opt.label; break; }
                            if (i > 0) answersHtml << ", ";
                            answersHtml << htmlEscape(val);
                        } else if (q.type == "image" && !val.empty()) {
                            std::string cid = "img" + std::to_string(++cidCounter);
                            attachments.push_back({val, fs::path(val).filename().string(), cid});
                            answersHtml << "<img src=\"cid:" << cid << "\" style=\"max-width:300px;max-height:200px;border-radius:6px;margin:4px;\" />";
                        }
                    }
                } else if (json_object_is_type(field, json_type_int)) {
                    answersHtml << json_object_get_int(field);
                }

                answersHtml << "</td></tr>";
            }

            // If custom template exists, replace placeholders
            if (!tplHtml.empty()) {
                auto replaceAll = [](std::string &str, const std::string &from, const std::string &to) {
                    size_t pos = 0;
                    while ((pos = str.find(from, pos)) != std::string::npos) {
                        str.replace(pos, from.length(), to);
                        pos += to.length();
                    }
                };
                replaceAll(tplHtml, "{{SURVEY_TITLE}}", htmlEscape(tpl.title));
                replaceAll(tplHtml, "{{FIRSTNAME}}", htmlEscape(firstname));
                replaceAll(tplHtml, "{{LASTNAME}}", htmlEscape(lastname));
                replaceAll(tplHtml, "{{COMPANY}}", htmlEscape(company));
                replaceAll(tplHtml, "{{EMAIL}}", htmlEscape(email));
                replaceAll(tplHtml, "{{ANSWERS}}", answersHtml.str());
                return tplHtml;
            }

            // Default HTML template
            std::ostringstream html;
            html << "<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head>"
                 << "<body style=\"margin:0;padding:0;font-family:Arial,Helvetica,sans-serif;background:#f4f4f4;\">"
                 << "<div style=\"max-width:640px;margin:20px auto;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08);\">"
                 << "<div style=\"background:linear-gradient(135deg,#84CB3A,#6ab02e);padding:24px 32px;color:#fff;\">"
                 << "<h1 style=\"margin:0;font-size:22px;\">" << htmlEscape(tpl.title) << "</h1>"
                 << "</div>"
                 << "<div style=\"padding:24px 32px;\">"
                 << "<table style=\"width:100%;border-collapse:collapse;margin-bottom:16px;\">"
                 << "<tr><td style=\"padding:6px 12px;font-weight:600;\">Name:</td>"
                 << "<td style=\"padding:6px 12px;\">" << htmlEscape(firstname) << " " << htmlEscape(lastname) << "</td></tr>";
            if (!company.empty())
                 html << "<tr><td style=\"padding:6px 12px;font-weight:600;\">Company:</td>"
                 << "<td style=\"padding:6px 12px;\">" << htmlEscape(company) << "</td></tr>";
            html << "<tr><td style=\"padding:6px 12px;font-weight:600;\">Email:</td>"
                 << "<td style=\"padding:6px 12px;\"><a href=\"mailto:" << htmlEscape(email) << "\">" << htmlEscape(email) << "</a></td></tr>"
                 << "</table>"
                 << "<h2 style=\"font-size:16px;margin:20px 0 12px;color:#333;\">Antworten</h2>"
                 << "<table style=\"width:100%;border-collapse:collapse;\">"
                 << answersHtml.str()
                 << "</table>"
                 << "</div>"
                 << "<div style=\"padding:16px 32px;background:#f9f9f9;color:#888;font-size:12px;text-align:center;\">"
                 << "Powered by Blogi"
                 << "</div>"
                 << "</div></body></html>";
            return html.str();
        }

        // ----- Handle POST submission -----
        bool handleSubmit(const int tid, libhttppp::HttpRequest &req, const SurveyTemplate &tpl) {
            size_t clen = req.getContentLength();
            libhttppp::HttpResponse resp;

            if (clen == 0 || clen > req.RecvData.size()) {
                std::string err = "{\"status\":\"error\",\"message\":\"Empty request body\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string body(req.RecvData.data(), clen);

            json_tokener_error jerr;
            json_object *jroot = json_tokener_parse_verbose(body.c_str(), &jerr);
            if (!jroot) {
                std::string err = "{\"status\":\"error\",\"message\":\"Invalid JSON\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string valError = validateResponse(tpl, jroot);
            if (!valError.empty()) {
                json_object_put(jroot);
                std::string err = "{\"status\":\"error\",\"message\":\"" + jsonEscape(valError) + "\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            // Extract participant info
            json_object *fn_obj = nullptr, *ln_obj = nullptr, *co_obj = nullptr, *em_obj = nullptr;
            json_object_object_get_ex(jroot, "_firstname", &fn_obj);
            json_object_object_get_ex(jroot, "_lastname",  &ln_obj);
            json_object_object_get_ex(jroot, "_company",   &co_obj);
            json_object_object_get_ex(jroot, "_email",     &em_obj);
            std::string firstname = json_object_get_string(fn_obj);
            std::string lastname  = json_object_get_string(ln_obj);
            std::string company   = (co_obj && json_object_is_type(co_obj, json_type_string)) ? json_object_get_string(co_obj) : "";
            std::string email     = json_object_get_string(em_obj);

            // Generate submission ID
            uuid::uuid rid;
            rid.generate();
            std::string submissionId = rid.c_str();

            // Process image questions: save data URIs to temp dir, replace with file paths
            for (const auto &q : tpl.questions) {
                if (q.type != "image") continue;
                json_object *field = nullptr;
                if (!json_object_object_get_ex(jroot, q.id.c_str(), &field)) continue;

                if (json_object_is_type(field, json_type_array)) {
                    size_t len = json_object_array_length(field);
                    json_object *newArr = json_object_new_array();
                    for (size_t i = 0; i < len; ++i) {
                        json_object *item = json_object_array_get_idx(field, i);
                        if (json_object_is_type(item, json_type_string)) {
                            std::string dataUri = json_object_get_string(item);
                            std::string savedPath = saveImage(dataUri, submissionId);
                            if (!savedPath.empty())
                                json_object_array_add(newArr, json_object_new_string(savedPath.c_str()));
                        }
                    }
                    json_object_object_del(jroot, q.id.c_str());
                    json_object_object_add(jroot, q.id.c_str(), newArr);
                } else if (json_object_is_type(field, json_type_string)) {
                    std::string dataUri = json_object_get_string(field);
                    if (!dataUri.empty() && dataUri.find("data:image/") == 0) {
                        std::string savedPath = saveImage(dataUri, submissionId);
                        if (!savedPath.empty()) {
                            json_object_object_del(jroot, q.id.c_str());
                            json_object_object_add(jroot, q.id.c_str(), json_object_new_string(savedPath.c_str()));
                        }
                    }
                }
            }

            // Serialize modified JSON (with file paths instead of data URIs)
            const char *modifiedBody = json_object_to_json_string_ext(jroot, JSON_C_TO_STRING_NOSLASHESCAPE);
            std::string responseJson = modifiedBody ? modifiedBody : body;

            // Build HTML email with inline images before freeing jroot
            std::vector<MailAttachment> attachments;
            std::string emailBody = buildHtmlEmailBody(tid, tpl, firstname, lastname, company, email, jroot, attachments);
            std::string subject = "Survey: " + tpl.title + " - " + firstname + " " + lastname;

            // Write to Google Sheet (before freeing jroot, so we have answers)
            bool gdriveReady = _gdriveEnabled && !_gdriveSpreadsheetId.empty() &&
                ((_gdriveAuthMode == "oauth" && !_gdriveRefreshToken.empty()) ||
                 (_gdriveAuthMode != "oauth" && !_gdriveServiceAccountFile.empty()));
            if (gdriveReady) {
                try {
                    writeToGoogleSheet(tpl, firstname, lastname, company, email, jroot);
                } catch (...) {
                    std::cerr << "survey: Google Sheets write failed" << std::endl;
                }
            }

            json_object_put(jroot);

            // Timestamp
            time_t now = time(nullptr);
            struct tm tmBuf;
#ifdef _WIN32
            gmtime_s(&tmBuf, &now);
#else
            gmtime_r(&now, &tmBuf);
#endif
            char timeBuf[64];
            strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", &tmBuf);

            // Store in DB
            dbpp::SQL sql;
            dbpp::DBResult res;
            std::vector<char> sbuf;

            sql << "INSERT INTO survey_results (id, survey_name, firstname, lastname, company, email, response, created_at) VALUES('"
                << submissionId.c_str() << "','"
                << sql.escaped(sbuf, tpl.name.c_str()) << "','"
                << sql.escaped(sbuf, firstname.c_str()) << "','"
                << sql.escaped(sbuf, lastname.c_str()) << "','"
                << sql.escaped(sbuf, company.c_str()) << "','"
                << sql.escaped(sbuf, email.c_str()) << "','"
                << sql.escaped(sbuf, responseJson.c_str()) << "','"
                << timeBuf << "');";

            try {
                Args->database[tid]->exec(sql, res);
            } catch (const std::exception &e) {
                std::cerr << "survey: store error: " << e.what() << std::endl;
                std::string err = "{\"status\":\"error\",\"message\":\"Storage failed\"}";
                resp.setState(HTTP500);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            } catch (...) {
                std::string err = "{\"status\":\"error\",\"message\":\"Storage failed\"}";
                resp.setState(HTTP500);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            // Write to Google Sheet
            // (moved above json_object_put)

            // Send HTML email to participant
            if (Args->smtp && !email.empty()) {
                try {
                    sendHtmlMail(Args->smtp.get(), email.c_str(), subject.c_str(), emailBody, attachments);
                } catch (...) {
                    std::cerr << "survey: email to participant failed" << std::endl;
                }
            }

            // Send HTML email to configured receiver
            if (Args->smtp && !_receiverEmail.empty()) {
                try {
                    sendHtmlMail(Args->smtp.get(), _receiverEmail.c_str(), subject.c_str(), emailBody, attachments);
                } catch (...) {
                    std::cerr << "survey: email to receiver failed" << std::endl;
                }
            }

            std::string ok = "{\"status\":\"ok\",\"message\":\"Response saved\"}";
            resp.setState(HTTP200);
            resp.setContentType("application/json; charset=utf-8");
            resp.setContentLength(ok.size());
            resp.send(req, ok.c_str(), ok.size());
            return true;
        }

        // ----- Render survey HTML form (GET) -----
        bool renderSurvey(const int tid, libhttppp::HttpRequest &req,
                          libhtmlpp::HtmlElement *page, const std::string &sessionid,
                          const SurveyTemplate &tpl) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string submitUrl = Args->config->buildurl("survey/" + tpl.name);
            // +1 for the participant info step
            size_t totalSteps = tpl.questions.size() + 1;

            std::ostringstream html;
            html << "<div id=\"survey_" << tpl.name << "\" class=\"survey-page\">"
                 << "<div class=\"survey-wrap\">"
                 << "<h1>" << tpl.title << "</h1>";

            if (!tpl.description.empty())
                html << "<p class=\"survey-description\">" << tpl.description << "</p>";

            // Progress dots
            html << "<div class=\"wizard-progress\">";
            for (size_t i = 0; i < totalSteps; ++i)
                html << "<div class=\"step-dot" << (i == 0 ? " active" : "") << "\" data-step=\"" << i << "\"></div>";
            html << "</div>";

            html << "<form class=\"surveyForm\">";
            // Step 0: Participant info (always required)
            html << "<div class=\"wizard-step active\" data-step=\"0\" data-required=\"true\">"
                 << "<div class=\"step-counter\">" << tr(_lang, "Step") << " 1 / " << totalSteps << "</div>"
                 << "<div class=\"question\">"
                 << "<div class=\"question-text required\">" << tr(_lang, "Your Information") << "</div>"
                 << "<div style=\"margin-bottom:12px;\"><label style=\"display:block;margin-bottom:4px;font-weight:600;\">" << tr(_lang, "First Name") << " *</label>"
                 << "<input type=\"text\" name=\"_firstname\" required placeholder=\"" << tr(_lang, "First Name") << "\" "
                 << "style=\"width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;\"/></div>"
                 << "<div style=\"margin-bottom:12px;\"><label style=\"display:block;margin-bottom:4px;font-weight:600;\">" << tr(_lang, "Last Name") << " *</label>"
                 << "<input type=\"text\" name=\"_lastname\" required placeholder=\"" << tr(_lang, "Last Name") << "\" "
                 << "style=\"width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;\"/></div>";
            if (_companyRequired) {
                 html << "<div style=\"margin-bottom:12px;\"><label style=\"display:block;margin-bottom:4px;font-weight:600;\">" << tr(_lang, "Company") << " *</label>"
                 << "<input type=\"text\" name=\"_company\" required placeholder=\"" << tr(_lang, "Company") << "\" "
                 << "style=\"width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;\"/></div>";
            } else {
                 html << "<div style=\"margin-bottom:12px;\"><label style=\"display:block;margin-bottom:4px;font-weight:600;\">" << tr(_lang, "Company (optional)") << "</label>"
                 << "<input type=\"text\" name=\"_company\" placeholder=\"" << tr(_lang, "Company (optional)") << "\" "
                 << "style=\"width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;\"/></div>";
            }
            html << "<div style=\"margin-bottom:12px;\"><label style=\"display:block;margin-bottom:4px;font-weight:600;\">Email *</label>"
                 << "<input type=\"email\" name=\"_email\" required placeholder=\"your@email.com\" "
                 << "style=\"width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;\"/></div>"
                 << "</div>" // .question
                 << "<div class=\"step-error\">" << tr(_lang, "Please fill in all fields.") << "</div>"
                 << "<div class=\"wizard-nav\"><span></span>"
                 << "<button type=\"button\" class=\"wizard-btn btn-next\">" << tr(_lang, "Next") << "</button>"
                 << "</div></div>";

            // Sort questions by order for rendering
            std::vector<SurveyQuestion> sortedQuestions = tpl.questions;
            std::sort(sortedQuestions.begin(), sortedQuestions.end(),
                [](const SurveyQuestion &a, const SurveyQuestion &b){ return a.order < b.order; });

            // Question steps (step 1 .. N)
            int stepIdx = 1;
            for (const auto &q : sortedQuestions) {
                html << "<div class=\"wizard-step\" data-step=\"" << stepIdx
                     << "\" data-question-id=\"" << q.id
                     << "\" data-required=\"" << (q.required ? "true" : "false") << "\">"
                     << "<div class=\"step-counter\">" << tr(_lang, "Step") << " " << (stepIdx + 1) << " / " << totalSteps << "</div>"
                     << "<div class=\"question\">"
                     << "<div class=\"question-text" << (q.required ? " required" : "") << "\">" << q.text << "</div>";

                if (q.type == "radio") {
                    for (const auto &opt : q.options) {
                        if (opt.value.empty() && opt.label.empty()) continue;
                        html << "<div class=\"option\"><label><input type=\"radio\" name=\"" << q.id
                             << "\" value=\"" << htmlEscape(opt.value) << "\"";
                        if (!opt.excludes.empty()) html << " data-excludes=\"" << opt.excludes << "\"";
                        html << "/>" << opt.label << "</label></div>";
                    }
                } else if (q.type == "icon") {
                    html << "<div class=\"icon-grid\"" << (q.multiSelect ? " data-multi=\"true\"" : "") << ">";
                    for (const auto &opt : q.options) {
                        if (opt.value.empty() && opt.label.empty()) continue;
                        html << "<label class=\"icon-option\">"
                             << "<input type=\"" << (q.multiSelect ? "checkbox" : "radio") << "\" name=\"" << q.id
                             << "\" value=\"" << htmlEscape(opt.value) << "\"";
                        if (!opt.excludes.empty()) html << " data-excludes=\"" << opt.excludes << "\"";
                        html << "/>";
                        if (!opt.icon.empty())
                            html << "<img src=\"" << htmlEscape(opt.icon) << "\" alt=\"" << htmlEscape(opt.label) << "\"/>";
                        html << "<span>" << opt.label << "</span></label>";
                    }
                    html << "</div>";
                } else if (q.type == "checkbox") {
                    for (const auto &opt : q.options) {
                        if (opt.value.empty() && opt.label.empty()) continue;
                        html << "<div class=\"option\"><label><input type=\"checkbox\" name=\"" << q.id
                             << "\" value=\"" << htmlEscape(opt.value) << "\"";
                        if (!opt.excludes.empty()) html << " data-excludes=\"" << opt.excludes << "\"";
                        html << "/>" << opt.label << "</label></div>";
                    }
                } else if (q.type == "select") {
                    html << "<select name=\"" << q.id << "\"><option value=\"\">-- Select --</option>";
                    for (const auto &opt : q.options) {
                        if (opt.value.empty() && opt.label.empty()) continue;
                        html << "<option value=\"" << htmlEscape(opt.value) << "\"";
                        if (!opt.excludes.empty()) html << " data-excludes=\"" << opt.excludes << "\"";
                        html << ">" << opt.label << "</option>";
                    }
                    html << "</select>";
                } else if (q.type == "rating") {
                    html << "<div class=\"rating-group\">";
                    for (int i = q.ratingMin; i <= q.ratingMax; ++i) {
                        std::string val = std::to_string(i);
                        html << "<label><input type=\"radio\" name=\"" << q.id << "\" value=\"" << val << "\"/>"
                             << "<span>" << val << "</span></label>";
                    }
                    html << "</div>";
                } else if (q.type == "image") {
                    std::string accept = q.acceptTypes.empty() ? "image/*" : q.acceptTypes;
                    html << "<div class=\"file-upload-area\" id=\"upload_" << q.id << "\""
                         << " data-maxsize=\"" << q.maxFileSize << "\""
                         << " data-maxcount=\"" << q.maxFileCount << "\""
                         << " data-accept=\"" << accept << "\">"
                         << "<input type=\"file\" name=\"" << q.id << "\" id=\"file_" << q.id
                         << "\" accept=\"" << accept << "\" multiple/>"
                         << "<div class=\"upload-icon\">&#128247;</div>"
                         << "<div class=\"upload-text\">" << tr(_lang, "Click or drag images here") << "</div>"
                         << "<div class=\"file-hint\">Max " << q.maxFileCount << " images, "
                         << (q.maxFileSize / (1024 * 1024)) << " MB each</div>"
                         << "</div>"
                         << "<div class=\"image-list\" id=\"images_" << q.id << "\"></div>";
                } else { // text
                    html << "<textarea name=\"" << q.id << "\" placeholder=\"" << tr(_lang, "Your answer...") << "\"></textarea>";
                }

                html << "</div>"; // .question
                html << "<div class=\"step-error\">" << tr(_lang, "Please answer this question.") << "</div>";
                html << "<div class=\"wizard-nav\">";
                html << "<button type=\"button\" class=\"wizard-btn btn-prev\">" << tr(_lang, "Back") << "</button>";
                if (stepIdx < (int)totalSteps - 1)
                    html << "<button type=\"button\" class=\"wizard-btn btn-next\">" << tr(_lang, "Next") << "</button>";
                else
                    html << "<button type=\"button\" class=\"wizard-btn btn-submit\">" << tr(_lang, "Submit") << "</button>";
                html << "</div>";
                html << "</div>"; // .wizard-step
                ++stepIdx;
            }

            html << "</form>"
                 << "<div class=\"survey-loading\"><div class=\"survey-spinner\"></div>"
                 << "<div class=\"survey-loading-text\">" << tr(_lang, "Sending...") << "</div></div>"
                 << "<div class=\"surveyMessage\"></div>";

            // Inline CSS — scoped to .survey-page
            html << "<style>"
                 << ".survey-page .survey-wrap{max-width:720px;margin:0 auto;padding:20px;}"
                 << ".survey-page .survey-description{color:var(--blogi-text-muted,#6b6b6b);margin-bottom:20px;}"
                 << ".survey-page .wizard-progress{display:flex;gap:6px;margin-bottom:24px;}"
                 << ".survey-page .step-dot{flex:1;height:6px;border-radius:3px;background:var(--blogi-border,#eaeaea);transition:background .3s;}"
                 << ".survey-page .step-dot.active{background:var(--blogi-accent,#84CB3A);}"
                 << ".survey-page .step-dot.done{background:var(--blogi-accent-dark,#6ab02e);}"
                 << ".survey-page .wizard-step{display:none;}"
                 << ".survey-page .wizard-step.active{display:block;}"
                 << ".survey-page .step-counter{color:var(--blogi-text-muted,#6b6b6b);font-size:.9em;margin-bottom:8px;}"
                 << ".survey-page .question{margin-bottom:20px;padding:16px;border:1px solid var(--blogi-border,#eaeaea);border-radius:8px;background:var(--blogi-bg-alt,#fafafa);}"
                 << ".survey-page .question-text{font-weight:600;margin-bottom:12px;}"
                 << ".survey-page .required::after{content:' *';color:var(--blogi-error,#F36947);}"
                 << ".survey-page .option{margin:6px 0;}.survey-page .option label{cursor:pointer;}"
                 << ".survey-page .option input{margin-right:8px;}"
                 << ".survey-page .question textarea,.survey-page .question select{width:100%;padding:10px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:1em;}"
                 << ".survey-page .question textarea{min-height:80px;resize:vertical;}"
                 << ".survey-page .rating-group{display:flex;gap:8px;flex-wrap:wrap;}"
                 << ".survey-page .rating-group label{padding:8px 16px;border:2px solid var(--blogi-border,#ddd);border-radius:8px;cursor:pointer;transition:.2s;}"
                 << ".survey-page .rating-group input{display:none;}"
                 << ".survey-page .rating-group label:has(input:checked){background:var(--blogi-accent,#84CB3A);color:var(--blogi-surface,#fff);border-color:var(--blogi-accent,#84CB3A);}"
                 << ".survey-page .wizard-nav{display:flex;justify-content:space-between;margin-top:16px;gap:12px;}"
                 << ".survey-page .wizard-btn{padding:10px 24px;border-radius:999px;font-weight:700;cursor:pointer;border:none;transition:.2s;}"
                 << ".survey-page .btn-prev{background:var(--blogi-border,#eaeaea);color:var(--blogi-text,#1a1a1a);}"
                 << ".survey-page .btn-next{background:var(--blogi-accent,#84CB3A);color:var(--blogi-surface,#fff);}"
                 << ".survey-page .btn-submit{background:linear-gradient(135deg,var(--blogi-accent,#84CB3A),var(--blogi-error,#F36947));color:var(--blogi-surface,#fff);}"
                 << ".survey-page .wizard-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.1);}"
                 << ".survey-page .step-error{color:var(--blogi-error,#F36947);font-size:.9em;margin-top:8px;display:none;}"
                 << ".survey-page .step-error.show{display:block;}"
                 << ".survey-page .surveyMessage{padding:16px;border-radius:8px;margin-top:16px;display:none;}"
                 << ".survey-page .surveyMessage.success{background:var(--blogi-success-bg,#e8f5e9);color:var(--blogi-success-text,#2e7d32);display:block;}"
                 << ".survey-page .surveyMessage.error{background:var(--blogi-error-bg,#fbe9e7);color:var(--blogi-error-text,#c62828);display:block;}"
                 << ".survey-page .file-upload-area{border:2px dashed var(--blogi-border,#ccc);border-radius:10px;padding:30px;text-align:center;cursor:pointer;transition:.3s;background:var(--blogi-surface,#fefefe);}"
                 << ".survey-page .file-upload-area:hover,.survey-page .file-upload-area.dragover{border-color:var(--blogi-accent,#84CB3A);background:var(--blogi-bg-alt,#f0f9e8);}"
                 << ".survey-page .file-upload-area input[type=file]{display:none;}"
                 << ".survey-page .upload-icon{font-size:2em;margin-bottom:8px;}"
                 << ".survey-page .upload-text{color:var(--blogi-text-muted,#6b6b6b);}"
                 << ".survey-page .file-hint{margin-top:8px;font-size:.85em;color:var(--blogi-text-muted,#6b6b6b);}"
                 << ".survey-page .image-list{margin-top:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px;}"
                 << ".survey-page .image-item{border:1px solid var(--blogi-border,#eaeaea);border-radius:8px;padding:8px;background:var(--blogi-surface,#fff);}"
                 << ".survey-page .image-item img{width:100%;height:100px;object-fit:cover;border-radius:6px;}"
                 << ".survey-page .image-item .image-name{font-size:.8em;margin-top:6px;color:var(--blogi-text-muted,#444);word-break:break-all;}"
                 << ".survey-page .image-item .file-remove{margin-top:6px;width:100%;background:var(--blogi-error,#F36947);color:var(--blogi-surface,#fff);border:none;padding:6px 10px;border-radius:999px;cursor:pointer;font-size:.85em;}"
                 << ".survey-page .survey-loading{display:none;text-align:center;padding:32px;}"
                 << ".survey-page .survey-loading.active{display:block;}"
                 << ".survey-page .survey-spinner{display:inline-block;width:36px;height:36px;border:4px solid var(--blogi-border,#eaeaea);border-top-color:var(--blogi-accent,#84CB3A);border-radius:50%;animation:survey-spin .8s linear infinite;}"
                 << "@keyframes survey-spin{to{transform:rotate(360deg);}}"
                 << ".survey-page .survey-loading-text{margin-top:12px;color:var(--blogi-text-muted,#6b6b6b);font-size:.95em;}"
                 << ".survey-page .icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px;}"
                 << ".survey-page .icon-option{display:flex;flex-direction:column;align-items:center;padding:12px;border:2px solid var(--blogi-border,#ddd);border-radius:10px;cursor:pointer;transition:.2s;text-align:center;background:var(--blogi-surface,#fff);}"
                 << ".survey-page .icon-option:hover{border-color:var(--blogi-accent,#84CB3A);}"
                 << ".survey-page .icon-option input{display:none;}"
                 << ".survey-page .icon-option img{width:64px;height:64px;object-fit:contain;margin-bottom:6px;}"
                 << ".survey-page .icon-option span{font-size:.85em;color:var(--blogi-text,#333);}"
                 << ".survey-page .icon-option.selected{border-color:var(--blogi-accent,#84CB3A);background:var(--blogi-success-bg,#e8f5e9);box-shadow:0 0 0 3px rgba(132,203,58,.25);}"
                 << "</style>";

            // Inline JS — scoped per survey instance
            html << "<script>"
                 << "(function(){"
                 << "var root=document.getElementById('survey_" << tpl.name << "');"
                 << "var currentStep=0,totalSteps=" << totalSteps << ";"
                 << "var imageData={},imageIdSeq=0;"
                 // exclusion logic
                 << "function getExcludedQuestions(){"
                 << "var excluded={};"
                 << "var checked=root.querySelectorAll('.surveyForm input[type=radio]:checked,.surveyForm input[type=checkbox]:checked');"
                 << "for(var i=0;i<checked.length;i++){var exc=checked[i].getAttribute('data-excludes');"
                 << "if(exc){var ids=exc.split(',');for(var j=0;j<ids.length;j++){var id=ids[j].trim();if(id)excluded[id]=true;}}}"
                 << "var sels=root.querySelectorAll('.surveyForm select');"
                 << "for(var i=0;i<sels.length;i++){var s=sels[i];if(!s.value)continue;"
                 << "var o=s.querySelector('option[value=\"'+s.value+'\"]');"
                 << "if(o){var exc=o.getAttribute('data-excludes');"
                 << "if(exc){var ids=exc.split(',');for(var j=0;j<ids.length;j++){var id=ids[j].trim();if(id)excluded[id]=true;}}}}"
                 << "return excluded;}"
                 << "function isStepExcluded(n){"
                 << "var step=root.querySelectorAll('.wizard-step')[n];"
                 << "if(!step)return false;"
                 << "var qid=step.getAttribute('data-question-id');"
                 << "if(!qid)return false;"
                 << "return !!getExcludedQuestions()[qid];}"
                 << "function nextVisibleStep(from){"
                 << "for(var i=from+1;i<totalSteps;i++){if(!isStepExcluded(i))return i;}return -1;}"
                 << "function prevVisibleStep(from){"
                 << "for(var i=from-1;i>=0;i--){if(!isStepExcluded(i))return i;}return -1;}"
                 << "function updateStepNav(n){"
                 << "var step=root.querySelectorAll('.wizard-step')[n];"
                 << "if(!step)return;var nav=step.querySelector('.wizard-nav');if(!nav)return;"
                 << "var nxt=nextVisibleStep(n);"
                 << "var btnNext=nav.querySelector('.btn-next'),btnSubmit=nav.querySelector('.btn-submit');"
                 << "if(nxt===-1){if(btnNext){btnNext.classList.remove('btn-next');btnNext.classList.add('btn-submit');btnNext.textContent='" << tr(_lang, "Submit") << "';}}"
                 << "else{if(btnSubmit&&!nav.querySelector('.btn-next')){btnSubmit.classList.remove('btn-submit');btnSubmit.classList.add('btn-next');btnSubmit.textContent='" << tr(_lang, "Next") << "';}}}"
                 // showStep
                 << "function showStep(n){"
                 << "var s=root.querySelectorAll('.wizard-step'),d=root.querySelectorAll('.step-dot');"
                 << "for(var i=0;i<s.length;i++){s[i].classList.remove('active');d[i].classList.remove('active');"
                 << "if(i<n)d[i].classList.add('done');else d[i].classList.remove('done');}"
                 << "s[n].classList.add('active');d[n].classList.add('active');currentStep=n;updateStepNav(n);}"
                 // validateStep
                 << "function validateStep(n){"
                 << "var step=root.querySelectorAll('.wizard-step')[n];"
                 << "var req=step.getAttribute('data-required')==='true';"
                 << "var err=step.querySelector('.step-error');"
                 << "if(!req){err.classList.remove('show');return true;}"
                 // check text inputs (participant step)
                 << "var texts=step.querySelectorAll('input[type=text],input[type=email]');"
                 << "for(var i=0;i<texts.length;i++){if(texts[i].value.trim()===''){err.classList.add('show');return false;}}"
                 // check radios
                 << "var r=step.querySelectorAll('input[type=radio]'),c=step.querySelectorAll('input[type=checkbox]'),"
                 << "sel=step.querySelector('select'),ta=step.querySelector('textarea');"
                 << "if(r.length>0){var ok=false;for(var i=0;i<r.length;i++)if(r[i].checked){ok=true;break;}if(!ok){err.classList.add('show');return false;}}"
                 << "if(c.length>0&&r.length===0){var ok=false;for(var i=0;i<c.length;i++)if(c[i].checked){ok=true;break;}if(!ok){err.classList.add('show');return false;}}"
                 << "if(sel&&sel.value===''){err.classList.add('show');return false;}"
                 << "if(ta&&ta.value.trim()===''){err.classList.add('show');return false;}"
                 // check required image
                 << "var fileInput=step.querySelector('input[type=file]');"
                 << "if(fileInput&&r.length===0&&c.length===0&&!sel&&!ta&&texts.length===0){"
                 << "var list=imageData[fileInput.name];if(!list||list.length===0){err.classList.add('show');return false;}}"
                 << "err.classList.remove('show');return true;}"
                 // wizard nav
                 << "function wizardNext(){if(!validateStep(currentStep))return;var nxt=nextVisibleStep(currentStep);if(nxt!==-1)showStep(nxt);}"
                 << "function wizardPrev(){var prv=prevVisibleStep(currentStep);if(prv!==-1)showStep(prv);}"
                 // image upload helpers
                 << "function ensureImageList(name){if(!imageData[name])imageData[name]=[];return imageData[name];}"
                 << "function renderImageList(name){"
                 << "var el=root.querySelector('.image-list[id=\"images_'+name+'\"]');if(!el)return;"
                 << "var list=ensureImageList(name),h='';"
                 << "for(var i=0;i<list.length;i++){"
                 << "h+='<div class=\"image-item\"><img src=\"'+list[i].data+'\" alt=\"\"/>';"
                 << "h+='<div class=\"image-name\">'+list[i].name+'</div>';"
                 << "h+='<button type=\"button\" class=\"file-remove\" data-name=\"'+name+'\" data-imgid=\"'+list[i].id+'\">Remove</button></div>';}"
                 << "el.innerHTML=h;}"
                 << "function handleFileSelect(input){"
                 << "var files=input.files;if(!files||files.length===0)return;"
                 << "var name=input.name,area=input.closest('.file-upload-area');"
                 << "var maxSize=parseInt(area.getAttribute('data-maxsize'))||5242880;"
                 << "var maxCount=parseInt(area.getAttribute('data-maxcount'))||5;"
                 << "var errEl=input.closest('.wizard-step').querySelector('.step-error');"
                 << "var list=ensureImageList(name);"
                 << "if(maxCount>0&&list.length+files.length>maxCount){if(errEl){errEl.textContent='Max '+maxCount+' images';errEl.classList.add('show');}input.value='';return;}"
                 << "for(var i=0;i<files.length;i++){(function(f){"
                 << "if(f.size>maxSize){if(errEl){errEl.textContent='File too large';errEl.classList.add('show');}return;}"
                 << "var reader=new FileReader();reader.onload=function(e){"
                 << "list.push({id:name+'_'+(++imageIdSeq),data:e.target.result,name:f.name});"
                 << "renderImageList(name);if(errEl)errEl.classList.remove('show');};"
                 << "reader.readAsDataURL(f);})(files[i]);}"
                 << "input.value='';}"
                 << "function removeImage(name,imgId){"
                 << "var list=ensureImageList(name);"
                 << "for(var i=list.length-1;i>=0;i--)if(list[i].id===imgId){list.splice(i,1);break;}"
                 << "renderImageList(name);}"
                 // init upload areas
                 << "(function(){"
                 << "var areas=root.querySelectorAll('.file-upload-area');"
                 << "for(var i=0;i<areas.length;i++){(function(area){"
                 << "var input=area.querySelector('input[type=file]');"
                 << "area.addEventListener('click',function(e){if(e.target.tagName!=='BUTTON')input.click();});"
                 << "area.addEventListener('dragover',function(e){e.preventDefault();area.classList.add('dragover');});"
                 << "area.addEventListener('dragleave',function(){area.classList.remove('dragover');});"
                 << "area.addEventListener('drop',function(e){e.preventDefault();area.classList.remove('dragover');"
                 << "if(e.dataTransfer.files.length>0){input.files=e.dataTransfer.files;handleFileSelect(input);}});"
                 << "input.addEventListener('change',function(){handleFileSelect(input);});"
                 << "})(areas[i]);}})();"
                 // submitSurvey
                 << "function submitSurvey(){"
                 << "if(!validateStep(currentStep))return;"
                 << "var form=root.querySelector('.surveyForm'),data={},els=form.elements;"
                 << "for(var i=0;i<els.length;i++){var el=els[i];if(!el.name)continue;"
                 << "if(el.type==='checkbox'){if(el.checked){if(!data[el.name])data[el.name]=[];data[el.name].push(el.value);}}"
                 << "else if(el.type==='radio'){if(el.checked)data[el.name]=el.value;}"
                 << "else if(el.type==='file'){continue;}"
                 << "else if(el.tagName==='SELECT'){if(el.value!=='')data[el.name]=el.value;}"
                 << "else{data[el.name]=el.value;}}"
                 // add images
                 << "for(var key in imageData){if(!imageData.hasOwnProperty(key))continue;"
                 << "var list=imageData[key];if(list&&list.length>0){"
                 << "data[key]=[];for(var i=0;i<list.length;i++)data[key].push(list[i].data);}}"
                 // add excluded question IDs
                 << "var excl=getExcludedQuestions(),exclArr=[];"
                 << "for(var k in excl){if(excl.hasOwnProperty(k))exclArr.push(k);}"
                 << "if(exclArr.length>0)data['_excluded']=exclArr;"
                 << "var xhr=new XMLHttpRequest();"
                 << "var loading=root.querySelector('.survey-loading');"
                 << "form.style.display='none';root.querySelector('.wizard-progress').style.display='none';"
                 << "loading.classList.add('active');"
                 << "xhr.open('POST','" << jsonEscape(submitUrl) << "',true);"
                 << "xhr.setRequestHeader('Content-Type','application/json');"
                 << "xhr.onreadystatechange=function(){"
                 << "if(xhr.readyState===4){loading.classList.remove('active');"
                 << "var m=root.querySelector('.surveyMessage');"
                 << "try{var r=JSON.parse(xhr.responseText);"
                 << "if(xhr.status===200&&r.status==='ok'){m.className='surveyMessage success';m.textContent=r.message||'Thank you!';}"
                 << "else{m.className='surveyMessage error';m.textContent=r.message||'Error';form.style.display='';root.querySelector('.wizard-progress').style.display='';}}"
                 << "catch(e){m.className='surveyMessage error';m.textContent='Server error';form.style.display='';root.querySelector('.wizard-progress').style.display='';}}};"
                 << "xhr.send(JSON.stringify(data));}"
                 // event delegation
                 << "root.addEventListener('click',function(e){"
                 << "var t=e.target.closest?e.target.closest('.wizard-btn'):null;"
                 << "if(t){if(t.classList.contains('btn-next'))wizardNext();"
                 << "else if(t.classList.contains('btn-prev'))wizardPrev();"
                 << "else if(t.classList.contains('btn-submit'))submitSurvey();return;}"
                 << "var ico=e.target.closest?e.target.closest('.icon-option'):null;"
                 << "if(ico){e.preventDefault();var grid=ico.closest('.icon-grid');var multi=grid&&grid.getAttribute('data-multi')==='true';"
                 << "var inp=ico.querySelector('input');if(inp){"
                 << "if(multi){inp.checked=!inp.checked;ico.classList.toggle('selected',inp.checked);}"
                 << "else{inp.checked=true;if(grid){var all=grid.querySelectorAll('.icon-option');"
                 << "for(var i=0;i<all.length;i++)all[i].classList.remove('selected');}ico.classList.add('selected');}"
                 << "inp.dispatchEvent(new Event('change',{bubbles:true}));"
                 << "if(!multi){setTimeout(function(){var nxt=nextVisibleStep(currentStep);if(nxt!==-1)showStep(nxt);else submitSurvey();},350);}}return;}"
                 << "var rm=e.target.closest?e.target.closest('.file-remove'):null;"
                 << "if(rm){var name=rm.getAttribute('data-name'),imgId=rm.getAttribute('data-imgid');"
                 << "if(name&&imgId)removeImage(name,imgId);}});"
                 // update exclusion state when inputs change
                 << "root.addEventListener('change',function(e){"
                 << "var t=e.target;"
                 << "if(t.type==='radio'||t.type==='checkbox'||t.tagName==='SELECT'){"
                 << "for(var i=0;i<totalSteps;i++){updateStepNav(i);}}});"
                 << "})();"
                 << "</script>";

            html << "</div></div>"; // .survey-wrap, .survey-page

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html; charset=utf-8");

            // Build a minimal standalone page (no site header/nav)
            std::ostringstream fullPage;
            fullPage << "<!DOCTYPE html><html><head>"
                     << "<meta charset=\"utf-8\"/>"
                     << "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>"
                     << "<title>" << htmlEscape(tpl.title) << "</title>";

            // Inject theme colors
            int hue = hueFromId(tpl.id.empty() ? tpl.name : tpl.id);
            fullPage << "<style>:root{"
                     << "--blogi-accent:" << hsl(hue, 65, 45) << ";"
                     << "--blogi-accent-dark:" << hsl(hue, 65, 35) << ";"
                     << "--blogi-success-bg:" << hsl(hue, 40, 92) << ";"
                     << "--blogi-success-text:" << hsl(hue, 60, 25) << ";}"
                     << "body{margin:0;font-family:system-ui,-apple-system,sans-serif;"
                     << "background:var(--blogi-bg,#f5f5f5);color:var(--blogi-text,#333);}"
                     << "</style>";

            fullPage << "</head><body>" << html.str() << "</body></html>";

            std::string page_str = fullPage.str();
            sendCompressed(req, resp, page_str.c_str(), page_str.size());
            return true;
        }

        // ====================== SETTINGS ======================

        // ----- Settings: index -----
        json_object* settingsIndex(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            loadTemplates(tid);

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Surveys").c_str()));

            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Name").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Title").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Questions").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Options").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            json_object *jrows = json_object_new_array();
            for (const auto &[name, tpl] : _templates) {
                json_object *jrow = json_object_new_object();
                json_object *jcells = json_object_new_array();
                json_object_array_add(jcells, json_object_new_string(name.c_str()));
                json_object_array_add(jcells, json_object_new_string(tpl.title.c_str()));
                json_object_array_add(jcells, json_object_new_string(std::to_string(tpl.questions.size()).c_str()));
                json_object_object_add(jrow, "cells", jcells);

                json_object *jactions = json_object_new_array();

                json_object *jedit = json_object_new_object();
                json_object_object_add(jedit, "label", json_object_new_string(tr(_lang, "Edit").c_str()));
                json_object_object_add(jedit, "url", json_object_new_string(
                    (Args->config->buildurl("settings/survey/editor?name=") + name).c_str()));
                json_object_array_add(jactions, jedit);

                json_object *jres = json_object_new_object();
                json_object_object_add(jres, "label", json_object_new_string(tr(_lang, "Results").c_str()));
                json_object_object_add(jres, "url", json_object_new_string(
                    (Args->config->buildurl("settings/survey/results?name=") + name).c_str()));
                json_object_array_add(jactions, jres);

                json_object *jexp = json_object_new_object();
                json_object_object_add(jexp, "label", json_object_new_string(tr(_lang, "Export").c_str()));
                json_object_object_add(jexp, "url", json_object_new_string(
                    (Args->config->buildurl("survey/_api/export?name=") + name).c_str()));
                json_object_array_add(jactions, jexp);

                json_object *jdel = json_object_new_object();
                json_object_object_add(jdel, "label", json_object_new_string(tr(_lang, "Remove").c_str()));
                json_object_object_add(jdel, "url", json_object_new_string(
                    (Args->config->buildurl("settings/survey/delete?name=") + name).c_str()));
                json_object_array_add(jactions, jdel);

                json_object_object_add(jrow, "actions", jactions);
                json_object_array_add(jrows, jrow);
            }
            json_object_object_add(jtable, "rows", jrows);
            json_object_object_add(jroot, "table", jtable);

            json_object *jlinks = json_object_new_array();
            json_object *jnewLink = json_object_new_object();
            json_object_object_add(jnewLink, "label", json_object_new_string(tr(_lang, "Add Survey").c_str()));
            json_object_object_add(jnewLink, "url", json_object_new_string(
                Args->config->buildurl("settings/survey/add").c_str()));
            json_object_array_add(jlinks, jnewLink);

            json_object *jimportLink = json_object_new_object();
            json_object_object_add(jimportLink, "label", json_object_new_string(tr(_lang, "Import Survey").c_str()));
            json_object_object_add(jimportLink, "url", json_object_new_string(
                Args->config->buildurl("settings/survey/import").c_str()));
            json_object_array_add(jlinks, jimportLink);

            json_object *jcfgLink = json_object_new_object();
            json_object_object_add(jcfgLink, "label", json_object_new_string(tr(_lang, "Survey Settings").c_str()));
            json_object_object_add(jcfgLink, "url", json_object_new_string(
                Args->config->buildurl("settings/survey/config").c_str()));
            json_object_array_add(jlinks, jcfgLink);

            json_object *jemailLink = json_object_new_object();
            json_object_object_add(jemailLink, "label", json_object_new_string(tr(_lang, "Email Templates").c_str()));
            json_object_object_add(jemailLink, "url", json_object_new_string(
                Args->config->buildurl("settings/survey/emailtemplate").c_str()));
            json_object_array_add(jlinks, jemailLink);

            json_object *jgdriveLink = json_object_new_object();
            json_object_object_add(jgdriveLink, "label", json_object_new_string(tr(_lang, "Google Sheets").c_str()));
            json_object_object_add(jgdriveLink, "url", json_object_new_string(
                Args->config->buildurl("settings/survey/gdrive").c_str()));
            json_object_array_add(jlinks, jgdriveLink);

            json_object_object_add(jroot, "links", jlinks);
            return jroot;
        }

        // ----- Settings: config (receiver email + temp dir) -----
        json_object* settingsConfig(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            bool saved = false;

            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &cur : form.urlData()) {
                if (cur.key == "receiver_email") {
                    setConfig(*Args->database[tid], "SURVEY_RECEIVER", cur.value);
                    _receiverEmail = cur.value;
                    saved = true;
                } else if (cur.key == "company_required") {
                    std::string val = (cur.value == "on" || cur.value == "true" || cur.value == "1") ? "true" : "false";
                    setConfig(*Args->database[tid], "SURVEY_COMPANY_REQUIRED", val);
                    _companyRequired = (val == "true");
                    saved = true;
                }
            }

            // If form was submitted but company_required checkbox was unchecked (not sent)
            if (saved && _companyRequired) {
                // Already set above
            } else if (saved) {
                // Check if receiver_email was submitted but company_required was not in form data
                bool hasCompanyField = false;
                for (const auto &cur : form.urlData())
                    if (cur.key == "company_required") { hasCompanyField = true; break; }
                if (!hasCompanyField) {
                    setConfig(*Args->database[tid], "SURVEY_COMPANY_REQUIRED", std::string("false"));
                    _companyRequired = false;
                }
            }

            if (!saved) {
                try { getConfig(*Args->database[tid], "SURVEY_RECEIVER", _receiverEmail); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_COMPANY_REQUIRED", _companyRequired); } catch (...) {}
            }

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Survey Settings").c_str()));

            if (saved) {
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Saved!").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
            }

            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "action", json_object_new_string(
                Args->config->buildurl("settings/survey/config").c_str()));

            json_object *jfields = json_object_new_array();

            json_object *jf1 = json_object_new_object();
            json_object_object_add(jf1, "name", json_object_new_string("receiver_email"));
            json_object_object_add(jf1, "label", json_object_new_string(tr(_lang, "Receiver Email").c_str()));
            json_object_object_add(jf1, "type", json_object_new_string("text"));
            json_object_object_add(jf1, "value", json_object_new_string(_receiverEmail.c_str()));
            json_object_array_add(jfields, jf1);

            json_object *jf3 = json_object_new_object();
            json_object_object_add(jf3, "name", json_object_new_string("company_required"));
            json_object_object_add(jf3, "label", json_object_new_string(tr(_lang, "Company field enabled (required)").c_str()));
            json_object_object_add(jf3, "type", json_object_new_string("checkbox"));
            json_object_object_add(jf3, "value", json_object_new_string(_companyRequired ? "on" : ""));
            json_object_array_add(jfields, jf3);

            json_object *jf2 = json_object_new_object();
            json_object_object_add(jf2, "name", json_object_new_string("tmp_dir_display"));
            json_object_object_add(jf2, "label", json_object_new_string(tr(_lang, "Temp Directory (config.yaml)").c_str()));
            json_object_object_add(jf2, "type", json_object_new_string("text"));
            json_object_object_add(jf2, "value", json_object_new_string(Args->config->getTmpDir().c_str()));
            json_object_object_add(jf2, "readonly", json_object_new_boolean(1));
            json_object_array_add(jfields, jf2);

            json_object_object_add(jform, "fields", jfields);
            json_object_object_add(jform, "submit", json_object_new_string(tr(_lang, "save").c_str()));
            json_object_object_add(jroot, "form", jform);

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(
                Args->config->buildurl("settings/survey").c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            return jroot;
        }

        // ----- Settings: email templates -----
        json_object* settingsEmailTemplate(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string tplName, tplHtml, deleteReq;
            bool saved = false;

            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &curformdat : form.multipartData()) {
                for (const auto &curctdisp : curformdat.dispositions) {
                    if (curctdisp.key != "name") continue;
                    std::string data;
                    std::copy(curformdat.value.begin(), curformdat.value.end(),
                              std::inserter<std::string>(data, data.begin()));
                    if (curctdisp.value == "tpl_name") tplName = data;
                    else if (curctdisp.value == "tpl_html") tplHtml = data;
                }
            }
            if (tplName.empty() || tplHtml.empty()) {
                for (const auto &cur : form.urlData()) {
                    if (cur.key == "tpl_name") tplName = cur.value;
                    else if (cur.key == "tpl_html") tplHtml = cur.value;
                    else if (cur.key == "delete") deleteReq = cur.value;
                }
            }

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Email Templates").c_str()));

            // Handle delete
            if (!deleteReq.empty()) {
                dbpp::SQL sql;
                dbpp::DBResult res;
                std::vector<char> sbuf;
                sql << "DELETE FROM survey_email_templates WHERE name='"
                    << sql.escaped(sbuf, deleteReq.c_str()) << "'";
                Args->database[tid]->exec(sql, res);
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Deleted successfully").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
            }

            // Handle save
            if (!tplName.empty() && !tplHtml.empty()) {
                dbpp::SQL sql;
                dbpp::DBResult res;
                std::vector<char> sbuf;
                // Upsert: delete + insert
                sql << "DELETE FROM survey_email_templates WHERE name='"
                    << sql.escaped(sbuf, tplName.c_str()) << "'";
                Args->database[tid]->exec(sql, res);
                sql.clear();
                sql << "INSERT INTO survey_email_templates (name, html_body) VALUES('"
                    << sql.escaped(sbuf, tplName.c_str()) << "','"
                    << sql.escaped(sbuf, tplHtml.c_str()) << "')";
                Args->database[tid]->exec(sql, res);
                saved = true;
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Saved!").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
            }

            // List existing templates
            {
                dbpp::SQL sql;
                dbpp::DBResult res;
                sql << "SELECT name, html_body FROM survey_email_templates ORDER BY name";
                int count = Args->database[tid]->exec(sql, res);

                json_object *jtable = json_object_new_object();
                json_object *jheaders = json_object_new_array();
                json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Name").c_str()));
                json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Preview").c_str()));
                json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Options").c_str()));
                json_object_object_add(jtable, "headers", jheaders);

                json_object *jrows = json_object_new_array();
                for (int i = 0; i < count; ++i) {
                    json_object *jrow = json_object_new_object();
                    json_object *jcells = json_object_new_array();
                    json_object_array_add(jcells, json_object_new_string(res[i][0]));
                    std::string preview = res[i][1];
                    if (preview.length() > 100) preview = preview.substr(0, 100) + "...";
                    json_object_array_add(jcells, json_object_new_string(preview.c_str()));
                    json_object_object_add(jrow, "cells", jcells);

                    json_object *jactions = json_object_new_array();
                    json_object *jdel = json_object_new_object();
                    json_object_object_add(jdel, "label", json_object_new_string(tr(_lang, "Remove").c_str()));
                    json_object_object_add(jdel, "url", json_object_new_string(
                        (Args->config->buildurl("settings/survey/emailtemplate?delete=") + std::string(res[i][0])).c_str()));
                    json_object_array_add(jactions, jdel);
                    json_object_object_add(jrow, "actions", jactions);

                    json_object_array_add(jrows, jrow);
                }
                json_object_object_add(jtable, "rows", jrows);
                json_object_object_add(jroot, "table", jtable);
            }

            // Add/edit form
            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "action", json_object_new_string(
                Args->config->buildurl("settings/survey/emailtemplate").c_str()));

            json_object *jfields = json_object_new_array();

            json_object *fname = json_object_new_object();
            json_object_object_add(fname, "name", json_object_new_string("tpl_name"));
            json_object_object_add(fname, "label", json_object_new_string(
                tr(_lang, "Template Name (e.g. survey-slug or 'default')").c_str()));
            json_object_object_add(fname, "type", json_object_new_string("text"));
            json_object_object_add(fname, "placeholder", json_object_new_string("default"));
            json_object_array_add(jfields, fname);

            json_object *fhtml = json_object_new_object();
            json_object_object_add(fhtml, "name", json_object_new_string("tpl_html"));
            json_object_object_add(fhtml, "label", json_object_new_string(
                tr(_lang, "HTML Template").c_str()));
            json_object_object_add(fhtml, "type", json_object_new_string("textarea"));
            json_object_object_add(fhtml, "placeholder", json_object_new_string(
                "<!DOCTYPE html><html><body>\n"
                "<h1>{{SURVEY_TITLE}}</h1>\n"
                "<p>Name: {{FIRSTNAME}} {{LASTNAME}}</p>\n"
                "<p>Company: {{COMPANY}}</p>\n"
                "<p>Email: {{EMAIL}}</p>\n"
                "<table>{{ANSWERS}}</table>\n"
                "</body></html>\n\n"
                "Placeholders: {{SURVEY_TITLE}} {{FIRSTNAME}} {{LASTNAME}} {{COMPANY}} {{EMAIL}} {{ANSWERS}}"));
            json_object_array_add(jfields, fhtml);

            json_object_object_add(jform, "fields", jfields);
            json_object_object_add(jform, "submit", json_object_new_string(tr(_lang, "save").c_str()));
            json_object_object_add(jroot, "form", jform);

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(
                Args->config->buildurl("settings/survey").c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            return jroot;
        }

        // ----- Settings: Google Drive / Sheets -----
        json_object* settingsGDrive(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            bool saved = false;
            std::string statusMsg;

            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &cur : form.urlData()) {
                if (cur.key == "gdrive_enabled") {
                    std::string val = (cur.value == "on" || cur.value == "true" || cur.value == "1") ? "true" : "false";
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_ENABLED", val);
                    _gdriveEnabled = (val == "true");
                    saved = true;
                } else if (cur.key == "gdrive_auth_mode") {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_AUTH_MODE", cur.value);
                    _gdriveAuthMode = cur.value;
                    saved = true;
                } else if (cur.key == "gdrive_sa_file") {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_SA_FILE", cur.value);
                    _gdriveServiceAccountFile = cur.value;
                    saved = true;
                } else if (cur.key == "gdrive_spreadsheet_id") {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_SPREADSHEET_ID", cur.value);
                    _gdriveSpreadsheetId = cur.value;
                    saved = true;
                } else if (cur.key == "gdrive_client_id") {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_CLIENT_ID", cur.value);
                    _gdriveClientId = cur.value;
                    saved = true;
                } else if (cur.key == "gdrive_client_secret") {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_CLIENT_SECRET", cur.value);
                    _gdriveClientSecret = cur.value;
                    saved = true;
                } else if (cur.key == "status") {
                    statusMsg = cur.value;
                }
            }

            // Handle unchecked checkbox
            if (saved) {
                bool hasEnabledField = false;
                for (const auto &cur : form.urlData())
                    if (cur.key == "gdrive_enabled") { hasEnabledField = true; break; }
                if (!hasEnabledField) {
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_ENABLED", std::string("false"));
                    _gdriveEnabled = false;
                }
            }

            if (!saved) {
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_ENABLED", _gdriveEnabled); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_AUTH_MODE", _gdriveAuthMode); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_SA_FILE", _gdriveServiceAccountFile); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_SPREADSHEET_ID", _gdriveSpreadsheetId); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_CLIENT_ID", _gdriveClientId); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_CLIENT_SECRET", _gdriveClientSecret); } catch (...) {}
                try { getConfig(*Args->database[tid], "SURVEY_GDRIVE_REFRESH_TOKEN", _gdriveRefreshToken); } catch (...) {}
            }

            // Check status query param (set by OAuth callback)
            for (const auto &cur : form.urlData()) {
                if (cur.key == "status") statusMsg = cur.value;
            }

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Google Sheets Settings").c_str()));

            if (saved) {
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Saved!").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
            }

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(
                Args->config->buildurl("settings/survey").c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            // Build OAuth redirect URL
            std::string redirectUri = Args->config->getsiteurl() + Args->config->buildurl("survey/_api/gdrive/callback");
            std::string authUrl;
            if (!_gdriveClientId.empty()) {
                authUrl = "https://accounts.google.com/o/oauth2/v2/auth"
                    "?client_id=" + _gdriveClientId
                    + "&redirect_uri=" + redirectUri
                    + "&response_type=code"
                    + "&scope=https://www.googleapis.com/auth/spreadsheets"
                    + "&access_type=offline&prompt=consent";
            }

            bool oauthConnected = (_gdriveAuthMode == "oauth" && !_gdriveRefreshToken.empty());
            std::string formAction = Args->config->buildurl("settings/survey/gdrive");

            // Build Wizard HTML
            std::ostringstream html;
            html << "<style>"
                 << ".gd-wizard{font-family:system-ui,sans-serif;max-width:640px;}"
                 << ".gd-step{border:1px solid var(--blogi-border,#ddd);border-radius:8px;padding:16px;margin:12px 0;"
                 << "background:var(--blogi-card-bg,#fff);}"
                 << ".gd-step h3{margin:0 0 12px;font-size:1.1em;display:flex;align-items:center;gap:8px;}"
                 << ".gd-step-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;"
                 << "border-radius:50%;font-weight:700;font-size:.9em;flex-shrink:0;}"
                 << ".gd-step-num.done{background:var(--blogi-accent,#22c55e);color:#fff;}"
                 << ".gd-step-num.pending{background:var(--blogi-border,#ddd);color:var(--blogi-text-muted,#666);}"
                 << ".gd-step-num.active{background:var(--blogi-nav-active,#3b82f6);color:#fff;}"
                 << ".gd-field{margin:8px 0;}"
                 << ".gd-field label{display:block;font-weight:600;margin-bottom:4px;font-size:.9em;}"
                 << ".gd-field input,.gd-field select{width:100%;padding:8px;border:1px solid var(--blogi-border,#ddd);"
                 << "border-radius:6px;font-size:.95em;box-sizing:border-box;}"
                 << ".gd-btn{display:inline-block;padding:10px 20px;border:none;border-radius:6px;font-size:.95em;"
                 << "cursor:pointer;text-decoration:none;font-weight:600;margin:4px 4px 4px 0;}"
                 << ".gd-btn-primary{background:var(--blogi-nav-active,#3b82f6);color:#fff;}"
                 << ".gd-btn-google{background:#4285f4;color:#fff;}"
                 << ".gd-btn-save{background:var(--blogi-accent,#22c55e);color:#fff;}"
                 << ".gd-status{padding:10px 14px;border-radius:6px;margin:8px 0;font-size:.9em;}"
                 << ".gd-status-ok{background:var(--blogi-success-bg,#dcfce7);color:var(--blogi-success-text,#166534);border:1px solid var(--blogi-accent,#86efac);}"
                 << ".gd-status-warn{background:#fef3c7;color:#92400e;border:1px solid #fcd34d;}"
                 << ".gd-status-err{background:var(--blogi-error-bg,#fef2f2);color:var(--blogi-error-text,#991b1b);border:1px solid var(--blogi-error,#fca5a5);}"
                 << ".gd-row{display:flex;gap:12px;align-items:end;}"
                 << ".gd-row>.gd-field{flex:1;}"
                 << "</style>"
                 << "<div class=\"gd-wizard\">";

            // Status message from OAuth callback
            if (statusMsg == "ok")
                html << "<div class=\"gd-status gd-status-ok\">&#10004; Google-Konto erfolgreich verbunden!</div>";
            else if (statusMsg == "error")
                html << "<div class=\"gd-status gd-status-err\">Verbindung fehlgeschlagen. Bitte erneut versuchen.</div>";

            html << "<form method=\"POST\" action=\"" << htmlEscape(formAction) << "\">";

            // Step 1: Enable + Auth Mode
            int step1State = _gdriveEnabled ? 2 : 1; // 2=done, 1=active
            html << "<div class=\"gd-step\">"
                 << "<h3><span class=\"gd-step-num " << (step1State == 2 ? "done" : "active") << "\">1</span>Aktivieren</h3>"
                 << "<div class=\"gd-row\">"
                 << "<div class=\"gd-field\"><label>Google Sheets aktivieren</label>"
                 << "<input type=\"checkbox\" name=\"gdrive_enabled\" value=\"on\"" << (_gdriveEnabled ? " checked" : "") << " style=\"width:auto;\"/></div>"
                 << "<div class=\"gd-field\"><label>Auth-Methode</label>"
                 << "<select name=\"gdrive_auth_mode\" id=\"gd-mode\">"
                 << "<option value=\"oauth\"" << (_gdriveAuthMode == "oauth" ? " selected" : "") << ">OAuth2 (empfohlen)</option>"
                 << "<option value=\"serviceaccount\"" << (_gdriveAuthMode != "oauth" ? " selected" : "") << ">Service Account</option>"
                 << "</select></div></div></div>";

            // Step 2: Credentials
            bool hasCredentials = (_gdriveAuthMode == "oauth")
                ? (!_gdriveClientId.empty() && !_gdriveClientSecret.empty())
                : (!_gdriveServiceAccountFile.empty());
            int step2State = hasCredentials ? 2 : (_gdriveEnabled ? 1 : 0);

            html << "<div class=\"gd-step\">"
                 << "<h3><span class=\"gd-step-num " << (step2State == 2 ? "done" : step2State == 1 ? "active" : "pending") << "\">2</span>Zugangsdaten</h3>";

            // OAuth fields
            html << "<div id=\"gd-oauth-fields\">"
                 << "<div class=\"gd-field\"><label>Client ID</label>"
                 << "<input type=\"text\" name=\"gdrive_client_id\" value=\"" << htmlEscape(_gdriveClientId) << "\" placeholder=\"aus Google Cloud Console\"/></div>"
                 << "<div class=\"gd-field\"><label>Client Secret</label>"
                 << "<input type=\"password\" name=\"gdrive_client_secret\" value=\"" << htmlEscape(_gdriveClientSecret) << "\" placeholder=\"aus Google Cloud Console\"/></div>"
                 << "<p style=\"font-size:.85em;color:var(--blogi-text-muted,#666);margin:4px 0;\">Redirect URI f&uuml;r Google Cloud Console: <code>"
                 << htmlEscape(redirectUri) << "</code></p>"
                 << "</div>";

            // SA fields
            html << "<div id=\"gd-sa-fields\">"
                 << "<div class=\"gd-field\"><label>Service Account JSON Dateipfad</label>"
                 << "<input type=\"text\" name=\"gdrive_sa_file\" value=\"" << htmlEscape(_gdriveServiceAccountFile) << "\" placeholder=\"/pfad/zur/sa.json\"/></div>"
                 << "</div></div>";

            // Step 3: Google verbinden (OAuth only)
            int step3State = oauthConnected ? 2 : (hasCredentials && _gdriveAuthMode == "oauth" ? 1 : 0);

            html << "<div class=\"gd-step\" id=\"gd-step-connect\">"
                 << "<h3><span class=\"gd-step-num " << (step3State == 2 ? "done" : step3State == 1 ? "active" : "pending") << "\">3</span>Google-Konto verbinden</h3>";

            if (oauthConnected) {
                html << "<div class=\"gd-status gd-status-ok\">&#10004; Verbunden &mdash; Refresh Token vorhanden</div>";
            } else if (_gdriveAuthMode == "oauth" && hasCredentials) {
                html << "<p style=\"font-size:.9em;margin:4px 0;\">Speichere zuerst die Zugangsdaten, dann klicke hier:</p>";
            } else if (_gdriveAuthMode == "oauth") {
                html << "<p style=\"font-size:.9em;color:var(--blogi-text-muted,#666);\">Bitte zuerst Client ID und Secret eingeben und speichern.</p>";
            } else {
                html << "<p style=\"font-size:.9em;color:var(--blogi-text-muted,#666);\">Bei Service Account nicht nötig.</p>";
            }

            if (!authUrl.empty() && _gdriveAuthMode == "oauth")
                html << "<a href=\"" << htmlEscape(authUrl) << "\" class=\"gd-btn gd-btn-google\">&#x1F511; Bei Google anmelden</a>";

            html << "</div>";

            // Step 4: Spreadsheet ID
            int step4State = !_gdriveSpreadsheetId.empty() ? 2 : ((step3State >= 1 || _gdriveAuthMode != "oauth") ? 1 : 0);

            html << "<div class=\"gd-step\">"
                 << "<h3><span class=\"gd-step-num " << (step4State == 2 ? "done" : step4State == 1 ? "active" : "pending") << "\">4</span>Spreadsheet</h3>"
                 << "<div class=\"gd-field\"><label>Spreadsheet ID</label>"
                 << "<input type=\"text\" name=\"gdrive_spreadsheet_id\" value=\"" << htmlEscape(_gdriveSpreadsheetId) << "\" placeholder=\"aus der Spreadsheet-URL\"/></div>"
                 << "<p style=\"font-size:.85em;color:var(--blogi-text-muted,#666);margin:4px 0;\">Die ID steht in der URL: docs.google.com/spreadsheets/d/<b>SPREADSHEET_ID</b>/edit</p>"
                 << "</div>";

            html << "<button type=\"submit\" class=\"gd-btn gd-btn-save\">&#x1F4BE; Speichern</button>"
                 << "</form>";

            // JS: show/hide fields based on auth mode
            html << "<script>"
                 << "(function(){"
                 << "var mode=document.getElementById('gd-mode');"
                 << "var oaF=document.getElementById('gd-oauth-fields');"
                 << "var saF=document.getElementById('gd-sa-fields');"
                 << "var con=document.getElementById('gd-step-connect');"
                 << "function upd(){"
                 << "var o=mode.value==='oauth';"
                 << "oaF.style.display=o?'':'none';"
                 << "saF.style.display=o?'none':'';"
                 << "con.style.display=o?'':'none';}"
                 << "mode.addEventListener('change',upd);upd();"
                 << "})();</script>"
                 << "</div>";

            json_object_object_add(jroot, "html", json_object_new_string(html.str().c_str()));
            return jroot;
        }

        // ----- Settings: add -----
        json_object* settingsAdd(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string sname, sxml;

            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &curformdat : form.multipartData()) {
                for (const auto &curctdisp : curformdat.dispositions) {
                    if (curctdisp.key != "name") continue;
                    std::string data;
                    std::copy(curformdat.value.begin(), curformdat.value.end(),
                              std::inserter<std::string>(data, data.begin()));
                    if (curctdisp.value == "survey_name") sname = data;
                    else if (curctdisp.value == "survey_xml") sxml = data;
                }
            }
            if (sname.empty() || sxml.empty()) {
                for (const auto &cur : form.urlData()) {
                    if (cur.key == "survey_name") sname = cur.value;
                    else if (cur.key == "survey_xml") sxml = cur.value;
                }
            }

            if (!sname.empty()) {
                SurveyTemplate tpl;
                bool parsed = false;

                if (!sxml.empty()) {
                    // Try JSON first, fall back to XML
                    json_object *jdata = json_tokener_parse(sxml.c_str());
                    if (jdata && json_object_is_type(jdata, json_type_object) && templateFromJson(jdata, tpl)) {
                        parsed = true;
                    } else {
                        parsed = parseXml(sxml, tpl);
                    }
                    if (jdata) json_object_put(jdata);

                    if (!parsed) {
                        json_object *jroot = json_object_new_object();
                        json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Add Survey").c_str()));
                        json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Invalid JSON or XML format").c_str()));
                        json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                        return jroot;
                    }
                } else {
                    // Default empty template
                    tpl.id = sname;
                    tpl.title = sname;
                    tpl.description = "";
                    parsed = true;
                }

                // Store as JSON
                json_object *jtpl = templateToJson(tpl);
                const char *jsonStr = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE);

                dbpp::SQL sql;
                dbpp::DBResult res;
                std::vector<char> sbuf;
                sql << "INSERT INTO survey_templates (name, xml_data, title) VALUES('"
                    << sql.escaped(sbuf, sname.c_str()) << "','"
                    << sql.escaped(sbuf, jsonStr) << "','"
                    << sql.escaped(sbuf, tpl.title.c_str()) << "');";
                json_object_put(jtpl);
                try {
                    Args->database[tid]->exec(sql, res);
                } catch (...) {
                    json_object *jroot = json_object_new_object();
                    json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Add Survey").c_str()));
                    json_object_object_add(jroot, "message", json_object_new_string(
                        tr(_lang, "Survey name already exists or DB error").c_str()));
                    json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                    return jroot;
                }

                loadTemplates(tid);

                json_object *jroot = json_object_new_object();
                json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Surveys").c_str()));
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Added successfully!").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
                json_object *jnav = json_object_new_array();
                json_object *jback = json_object_new_object();
                json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
                json_object_object_add(jback, "url", json_object_new_string(
                    Args->config->buildurl("settings/survey").c_str()));
                json_object_array_add(jnav, jback);
                json_object_object_add(jroot, "nav", jnav);
                return jroot;
            }

            // Show form
            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Add Survey").c_str()));

            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "action", json_object_new_string(
                Args->config->buildurl("settings/survey/add").c_str()));
            json_object_object_add(jform, "enctype", json_object_new_string("multipart/form-data"));

            json_object *jfields = json_object_new_array();

            json_object *fname = json_object_new_object();
            json_object_object_add(fname, "name", json_object_new_string("survey_name"));
            json_object_object_add(fname, "label", json_object_new_string(tr(_lang, "Survey Name (URL slug)").c_str()));
            json_object_object_add(fname, "type", json_object_new_string("text"));
            json_object_object_add(fname, "placeholder", json_object_new_string("kundenzufriedenheit"));
            json_object_array_add(jfields, fname);

            json_object *fxml = json_object_new_object();
            json_object_object_add(fxml, "name", json_object_new_string("survey_xml"));
            json_object_object_add(fxml, "label", json_object_new_string(tr(_lang, "Survey JSON").c_str()));
            json_object_object_add(fxml, "type", json_object_new_string("textarea"));
            json_object_object_add(fxml, "placeholder", json_object_new_string(
                "{\n  \"id\": \"example\",\n  \"title\": \"My Survey\",\n"
                "  \"description\": \"Description\",\n"
                "  \"questions\": [{\n"
                "    \"id\": \"q1\", \"type\": \"radio\", \"required\": true,\n"
                "    \"text\": \"Your question\",\n"
                "    \"options\": [{\"value\": \"a\", \"label\": \"Option A\"}]\n"
                "  }]\n}"));
            json_object_array_add(jfields, fxml);

            json_object_object_add(jform, "fields", jfields);
            json_object_object_add(jform, "submit", json_object_new_string(tr(_lang, "save").c_str()));
            json_object_object_add(jroot, "form", jform);
            return jroot;
        }

        // ----- Settings: delete -----
        json_object* settingsDelete(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string curl = req.getRequest();
            std::string name;
            size_t qpos = curl.find("name=");
            if (qpos != std::string::npos) name = curl.substr(qpos + 5);
            // Strip trailing '/' or '&...'
            size_t amp = name.find('&');
            if (amp != std::string::npos) name = name.substr(0, amp);
            if (!name.empty() && name.back() == '/') name.pop_back();

            if (!name.empty()) {
                dbpp::SQL sql;
                dbpp::DBResult res;
                std::vector<char> sbuf;
                sql << "DELETE FROM survey_templates WHERE name='" << sql.escaped(sbuf, name.c_str()) << "';";
                Args->database[tid]->exec(sql, res);
                sql.clear();
                sql << "DELETE FROM survey_results WHERE survey_name='" << sql.escaped(sbuf, name.c_str()) << "';";
                Args->database[tid]->exec(sql, res);

                loadTemplates(tid);

                json_object *jroot = json_object_new_object();
                json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Surveys").c_str()));
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "Deleted successfully").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("success"));
                json_object *jnav = json_object_new_array();
                json_object *jback = json_object_new_object();
                json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
                json_object_object_add(jback, "url", json_object_new_string(
                    Args->config->buildurl("settings/survey").c_str()));
                json_object_array_add(jnav, jback);
                json_object_object_add(jroot, "nav", jnav);
                return jroot;
            }
            return settingsIndex(tid, req);
        }

        // ----- Settings: view results -----
        json_object* settingsResults(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string name;

            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &cur : form.urlData())
                if (cur.key == "name") name = cur.value;

            json_object *jroot = json_object_new_object();

            if (name.empty()) {
                json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Survey Results").c_str()));
                json_object_object_add(jroot, "message", json_object_new_string(tr(_lang, "No survey specified").c_str()));
                json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                return jroot;
            }

            auto it = _templates.find(name);
            std::string titleStr = (it != _templates.end()) ? it->second.title : name;
            json_object_object_add(jroot, "title", json_object_new_string(
                (tr(_lang, "Results") + ": " + titleStr).c_str()));

            dbpp::SQL sql;
            dbpp::DBResult res;
            std::vector<char> sbuf;

            sql << "SELECT id, firstname, lastname, company, email, response, created_at FROM survey_results WHERE survey_name='"
                << sql.escaped(sbuf, name.c_str()) << "' ORDER BY created_at DESC";
            int count = Args->database[tid]->exec(sql, res);

            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Date").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Name").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Company").c_str()));
            json_object_array_add(jheaders, json_object_new_string("Email"));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang, "Response").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            json_object *jrows = json_object_new_array();
            for (int i = 0; i < count; ++i) {
                json_object *jrow = json_object_new_object();
                json_object *jcells = json_object_new_array();
                json_object_array_add(jcells, json_object_new_string(res[i][6])); // created_at
                std::string fullname = std::string(res[i][1]) + " " + std::string(res[i][2]);
                json_object_array_add(jcells, json_object_new_string(fullname.c_str()));
                json_object_array_add(jcells, json_object_new_string(res[i][3])); // company
                json_object_array_add(jcells, json_object_new_string(res[i][4])); // email

                std::string respStr = res[i][5];
                if (respStr.length() > 200)
                    respStr = respStr.substr(0, 200) + "...";
                json_object_array_add(jcells, json_object_new_string(respStr.c_str()));

                json_object_object_add(jrow, "cells", jcells);
                json_object_array_add(jrows, jrow);
            }
            json_object_object_add(jtable, "rows", jrows);
            json_object_object_add(jroot, "table", jtable);

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(
                Args->config->buildurl("settings/survey").c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            return jroot;
        }

        // ----- Convert SurveyTemplate back to XML string -----
        std::string toXml(const SurveyTemplate &tpl) {
            tinyxml2::XMLDocument doc;
            doc.InsertFirstChild(doc.NewDeclaration());
            tinyxml2::XMLElement *surveyEl = doc.NewElement("survey");
            surveyEl->SetAttribute("id", tpl.id.c_str());
            surveyEl->SetAttribute("title", tpl.title.c_str());
            doc.InsertEndChild(surveyEl);

            if (!tpl.description.empty()) {
                tinyxml2::XMLElement *descEl = doc.NewElement("description");
                descEl->SetText(tpl.description.c_str());
                surveyEl->InsertEndChild(descEl);
            }

            for (const auto &q : tpl.questions) {
                tinyxml2::XMLElement *qEl = doc.NewElement("question");
                qEl->SetAttribute("id", q.id.c_str());
                qEl->SetAttribute("type", q.type.c_str());
                qEl->SetAttribute("required", q.required ? "true" : "false");
                if (q.multiSelect) qEl->SetAttribute("multiselect", "true");
                qEl->SetAttribute("order", q.order);
                if (q.type == "rating") {
                    qEl->SetAttribute("min", q.ratingMin);
                    qEl->SetAttribute("max", q.ratingMax);
                }
                if (q.type == "image") {
                    if (!q.acceptTypes.empty()) qEl->SetAttribute("accept", q.acceptTypes.c_str());
                    qEl->SetAttribute("maxsize", q.maxFileSize);
                    qEl->SetAttribute("maxcount", q.maxFileCount);
                }
                tinyxml2::XMLElement *textEl = doc.NewElement("text");
                textEl->SetText(q.text.c_str());
                qEl->InsertEndChild(textEl);

                for (const auto &opt : q.options) {
                    tinyxml2::XMLElement *optEl = doc.NewElement("option");
                    optEl->SetAttribute("value", opt.value.c_str());
                    if (!opt.excludes.empty()) optEl->SetAttribute("excludes", opt.excludes.c_str());
                    if (!opt.icon.empty()) optEl->SetAttribute("icon", opt.icon.c_str());
                    optEl->SetText(opt.label.c_str());
                    qEl->InsertEndChild(optEl);
                }
                surveyEl->InsertEndChild(qEl);
            }

            tinyxml2::XMLPrinter printer;
            doc.Print(&printer);
            return std::string(printer.CStr());
        }

        // ----- Template to JSON -----
        json_object* templateToJson(const SurveyTemplate &tpl) {
            json_object *jtpl = json_object_new_object();
            json_object_object_add(jtpl, "id", json_object_new_string(tpl.id.c_str()));
            json_object_object_add(jtpl, "title", json_object_new_string(tpl.title.c_str()));
            json_object_object_add(jtpl, "description", json_object_new_string(tpl.description.c_str()));

            json_object *jquestions = json_object_new_array();
            for (const auto &q : tpl.questions) {
                json_object *jq = json_object_new_object();
                json_object_object_add(jq, "id", json_object_new_string(q.id.c_str()));
                json_object_object_add(jq, "type", json_object_new_string(q.type.c_str()));
                json_object_object_add(jq, "text", json_object_new_string(q.text.c_str()));
                json_object_object_add(jq, "required", json_object_new_boolean(q.required));
                json_object_object_add(jq, "order", json_object_new_int(q.order));
                if (q.multiSelect)
                    json_object_object_add(jq, "multiSelect", json_object_new_boolean(true));
                if (q.type == "rating") {
                    json_object_object_add(jq, "ratingMin", json_object_new_int(q.ratingMin));
                    json_object_object_add(jq, "ratingMax", json_object_new_int(q.ratingMax));
                }
                if (q.type == "image") {
                    json_object_object_add(jq, "acceptTypes", json_object_new_string(q.acceptTypes.c_str()));
                    json_object_object_add(jq, "maxFileSize", json_object_new_int(q.maxFileSize));
                    json_object_object_add(jq, "maxFileCount", json_object_new_int(q.maxFileCount));
                }
                json_object *jopts = json_object_new_array();
                for (const auto &opt : q.options) {
                    json_object *jo = json_object_new_object();
                    json_object_object_add(jo, "value", json_object_new_string(opt.value.c_str()));
                    json_object_object_add(jo, "label", json_object_new_string(opt.label.c_str()));
                    if (!opt.excludes.empty())
                        json_object_object_add(jo, "excludes", json_object_new_string(opt.excludes.c_str()));
                    if (!opt.icon.empty())
                        json_object_object_add(jo, "icon", json_object_new_string(opt.icon.c_str()));
                    json_object_array_add(jopts, jo);
                }
                json_object_object_add(jq, "options", jopts);
                json_object_array_add(jquestions, jq);
            }
            json_object_object_add(jtpl, "questions", jquestions);
            return jtpl;
        }

        // ----- JSON to SurveyTemplate -----
        bool templateFromJson(json_object *jtpl, SurveyTemplate &tpl) {
            json_object *val = nullptr;
            if (json_object_object_get_ex(jtpl, "id", &val) && json_object_is_type(val, json_type_string))
                tpl.id = json_object_get_string(val);
            if (json_object_object_get_ex(jtpl, "title", &val) && json_object_is_type(val, json_type_string))
                tpl.title = json_object_get_string(val);
            if (json_object_object_get_ex(jtpl, "description", &val) && json_object_is_type(val, json_type_string))
                tpl.description = json_object_get_string(val);

            json_object *jquestions = nullptr;
            if (!json_object_object_get_ex(jtpl, "questions", &jquestions) || !json_object_is_type(jquestions, json_type_array))
                return false;

            tpl.questions.clear();
            size_t qlen = json_object_array_length(jquestions);
            for (size_t i = 0; i < qlen; ++i) {
                json_object *jq = json_object_array_get_idx(jquestions, i);
                if (!jq) continue;
                SurveyQuestion q;
                json_object *v = nullptr;
                if (json_object_object_get_ex(jq, "id", &v) && json_object_is_type(v, json_type_string))
                    q.id = json_object_get_string(v);
                if (json_object_object_get_ex(jq, "type", &v) && json_object_is_type(v, json_type_string))
                    q.type = json_object_get_string(v);
                if (json_object_object_get_ex(jq, "text", &v) && json_object_is_type(v, json_type_string))
                    q.text = json_object_get_string(v);
                if (json_object_object_get_ex(jq, "required", &v))
                    q.required = json_object_get_boolean(v);
                if (json_object_object_get_ex(jq, "multiSelect", &v))
                    q.multiSelect = json_object_get_boolean(v);
                if (json_object_object_get_ex(jq, "order", &v))
                    q.order = json_object_get_int(v);
                if (json_object_object_get_ex(jq, "ratingMin", &v))
                    q.ratingMin = json_object_get_int(v);
                if (json_object_object_get_ex(jq, "ratingMax", &v))
                    q.ratingMax = json_object_get_int(v);
                if (json_object_object_get_ex(jq, "acceptTypes", &v) && json_object_is_type(v, json_type_string))
                    q.acceptTypes = json_object_get_string(v);
                if (json_object_object_get_ex(jq, "maxFileSize", &v))
                    q.maxFileSize = json_object_get_int(v);
                if (json_object_object_get_ex(jq, "maxFileCount", &v))
                    q.maxFileCount = json_object_get_int(v);

                json_object *jopts = nullptr;
                if (json_object_object_get_ex(jq, "options", &jopts) && json_object_is_type(jopts, json_type_array)) {
                    size_t olen = json_object_array_length(jopts);
                    for (size_t j = 0; j < olen; ++j) {
                        json_object *jo = json_object_array_get_idx(jopts, j);
                        if (!jo) continue;
                        SurveyOption opt;
                        json_object *ov = nullptr;
                        if (json_object_object_get_ex(jo, "value", &ov) && json_object_is_type(ov, json_type_string))
                            opt.value = json_object_get_string(ov);
                        if (json_object_object_get_ex(jo, "label", &ov) && json_object_is_type(ov, json_type_string))
                            opt.label = json_object_get_string(ov);
                        if (json_object_object_get_ex(jo, "excludes", &ov) && json_object_is_type(ov, json_type_string))
                            opt.excludes = json_object_get_string(ov);
                        if (json_object_object_get_ex(jo, "icon", &ov) && json_object_is_type(ov, json_type_string))
                            opt.icon = json_object_get_string(ov);
                        q.options.push_back(std::move(opt));
                    }
                }
                tpl.questions.push_back(std::move(q));
            }
            return true;
        }

        // ----- Controller API: save survey template (authenticated, raw JSON) -----
        bool handleApiSave(const int tid, libhttppp::HttpRequest &req,
                           const std::string &sessionid) {
            libhttppp::HttpResponse resp;

            if (!Args->auth->isLoggedIn(tid, req, sessionid)) {
                std::string err = "{\"status\":\"error\",\"message\":\"Unauthorized\"}";
                resp.setState(HTTP403);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            size_t clen = req.getContentLength();
            if (clen == 0 || clen > req.RecvData.size()) {
                std::string err = "{\"status\":\"error\",\"message\":\"Empty request body\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string body(req.RecvData.data(), clen);
            json_object *jreq = json_tokener_parse(body.c_str());
            if (!jreq) {
                std::string err = "{\"status\":\"error\",\"message\":\"Invalid JSON\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            json_object *jname_obj = nullptr, *jsurvey_obj = nullptr, *jorigname_obj = nullptr;
            json_object_object_get_ex(jreq, "name", &jname_obj);
            json_object_object_get_ex(jreq, "survey", &jsurvey_obj);
            json_object_object_get_ex(jreq, "originalName", &jorigname_obj);
            if (!jname_obj || !jsurvey_obj) {
                json_object_put(jreq);
                std::string err = "{\"status\":\"error\",\"message\":\"Missing name or survey\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string sname = json_object_get_string(jname_obj);
            std::string origName = (jorigname_obj && json_object_is_type(jorigname_obj, json_type_string))
                ? json_object_get_string(jorigname_obj) : sname;
            SurveyTemplate tpl;
            if (!templateFromJson(jsurvey_obj, tpl)) {
                json_object_put(jreq);
                std::string err = "{\"status\":\"error\",\"message\":\"Invalid survey data\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            // Store as JSON directly
            json_object *jtpl = templateToJson(tpl);
            const char *jsonStr = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE);

            dbpp::SQL sql;
            dbpp::DBResult res;
            std::vector<char> sbuf;
            // Delete old entry (handles rename)
            sql << "DELETE FROM survey_templates WHERE name='"
                << sql.escaped(sbuf, origName.c_str()) << "';";
            try { Args->database[tid]->exec(sql, res); } catch (...) {}
            // Also delete by new name to avoid duplicates
            if (origName != sname) {
                sql.clear();
                sql << "DELETE FROM survey_templates WHERE name='"
                    << sql.escaped(sbuf, sname.c_str()) << "';";
                try { Args->database[tid]->exec(sql, res); } catch (...) {}
            }

            sql.clear();
            sql << "INSERT INTO survey_templates (name, xml_data, title) VALUES('"
                << sql.escaped(sbuf, sname.c_str()) << "','"
                << sql.escaped(sbuf, jsonStr) << "','"
                << sql.escaped(sbuf, tpl.title.c_str()) << "');";
            json_object_put(jtpl);
            json_object_put(jreq);
            try {
                Args->database[tid]->exec(sql, res);
            } catch (...) {
                std::string err = "{\"status\":\"error\",\"message\":\"Database error\"}";
                resp.setState(HTTP500);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            loadTemplates(tid);
            std::string ok = "{\"status\":\"ok\",\"message\":\"Saved\"}";
            resp.setState(HTTP200);
            resp.setContentType("application/json; charset=utf-8");
            resp.setContentLength(ok.size());
            resp.send(req, ok.c_str(), ok.size());
            return true;
        }

        // ----- Export: download survey as JSON file -----
        bool handleExport(const int tid, libhttppp::HttpRequest &req,
                          const std::string &sessionid) {
            libhttppp::HttpResponse resp;

            if (!Args->auth->isLoggedIn(tid, req, sessionid)) {
                resp.setState(HTTP403);
                resp.send(req, "", 0);
                return true;
            }

            // Parse ?name= from query string
            std::string name;
            std::string fullUrl = req.getRequest();
            size_t qpos = fullUrl.find("name=");
            if (qpos != std::string::npos) {
                name = fullUrl.substr(qpos + 5);
                size_t amp = name.find('&');
                if (amp != std::string::npos) name = name.substr(0, amp);
                if (!name.empty() && name.back() == '/') name.pop_back();
            }

            if (name.empty()) {
                std::string err = "{\"error\":\"missing name parameter\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            loadTemplates(tid);
            auto it = _templates.find(name);
            if (it == _templates.end()) {
                std::string err = "{\"error\":\"survey not found\"}";
                resp.setState(HTTP404);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            // Build export JSON: { name, survey: {...} }
            json_object *jexport = json_object_new_object();
            json_object_object_add(jexport, "name", json_object_new_string(name.c_str()));
            json_object *jtpl = templateToJson(it->second);
            json_object_object_add(jexport, "survey", jtpl);

            std::string body = json_object_to_json_string_ext(jexport, JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_NOSLASHESCAPE);
            json_object_put(jexport);

            resp.setState(HTTP200);
            resp.setContentType("application/json; charset=utf-8");
            std::string disposition = "attachment; filename=\"" + name + ".json\"";
            resp.setHeaderData("Content-Disposition")->push_back(disposition);
            resp.setContentLength(body.size());
            resp.send(req, body.c_str(), body.size());
            return true;
        }

        // ----- Import: receive JSON file and create survey -----
        bool handleImport(const int tid, libhttppp::HttpRequest &req,
                          const std::string &sessionid) {
            libhttppp::HttpResponse resp;

            if (!Args->auth->isLoggedIn(tid, req, sessionid)) {
                resp.setState(HTTP403);
                resp.send(req, "", 0);
                return true;
            }

            size_t clen = req.getContentLength();
            if (clen == 0 || clen > req.RecvData.size()) {
                std::string err = "{\"status\":\"error\",\"message\":\"Empty request body\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string body(req.RecvData.data(), clen);

            json_object *jreq = json_tokener_parse(body.c_str());
            if (!jreq) {
                std::string err = "{\"status\":\"error\",\"message\":\"Invalid JSON\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            json_object *jname_obj = nullptr, *jsurvey_obj = nullptr;
            json_object_object_get_ex(jreq, "name", &jname_obj);
            json_object_object_get_ex(jreq, "survey", &jsurvey_obj);
            if (!jname_obj || !jsurvey_obj) {
                json_object_put(jreq);
                std::string err = "{\"status\":\"error\",\"message\":\"Missing name or survey\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            std::string sname = json_object_get_string(jname_obj);
            SurveyTemplate tpl;
            if (!templateFromJson(jsurvey_obj, tpl)) {
                json_object_put(jreq);
                std::string err = "{\"status\":\"error\",\"message\":\"Invalid survey data\"}";
                resp.setState(HTTP400);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            json_object *jtpl = templateToJson(tpl);
            const char *jsonStr = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE);
            json_object_put(jreq);

            dbpp::SQL sql;
            dbpp::DBResult res;
            std::vector<char> sbuf;
            // Check if name already exists
            sql << "SELECT name FROM survey_templates WHERE name='"
                << sql.escaped(sbuf, sname.c_str()) << "';";
            int existing = 0;
            try { existing = Args->database[tid]->exec(sql, res); } catch (...) {}
            if (existing > 0) {
                json_object_put(jtpl);
                std::string err = "{\"status\":\"error\",\"message\":\"Survey '\" + jsonEscape(sname) + \"' already exists\"}";
                resp.setState(HTTP409);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            sql.clear();
            sql << "INSERT INTO survey_templates (name, xml_data, title) VALUES('"
                << sql.escaped(sbuf, sname.c_str()) << "','"
                << sql.escaped(sbuf, jsonStr) << "','"
                << sql.escaped(sbuf, tpl.title.c_str()) << "');";
            json_object_put(jtpl);
            try {
                Args->database[tid]->exec(sql, res);
            } catch (...) {
                std::string err = "{\"status\":\"error\",\"message\":\"Database error\"}";
                resp.setState(HTTP500);
                resp.setContentType("application/json; charset=utf-8");
                resp.setContentLength(err.size());
                resp.send(req, err.c_str(), err.size());
                return true;
            }

            loadTemplates(tid);
            std::string ok = "{\"status\":\"ok\",\"message\":\"Imported successfully\"}";
            resp.setState(HTTP200);
            resp.setContentType("application/json; charset=utf-8");
            resp.setContentLength(ok.size());
            resp.send(req, ok.c_str(), ok.size());
            return true;
        }

        // ----- OAuth2 callback: exchange auth code for tokens -----
        bool handleGDriveCallback(const int tid, libhttppp::HttpRequest &req,
                                  const std::string &sessionid) {
            // Must be authenticated
            Session sess(*Args->database[tid]);
            std::string authid;
            try { sess.getSessionData(sessionid, "authid", authid); } catch (...) {}
            if (authid.empty()) {
                libhttppp::HttpResponse resp;
                resp.setState(HTTP403);
                std::string body = "Not authenticated";
                resp.setContentLength(body.size());
                resp.send(req, body.c_str(), body.size());
                return true;
            }

            // Parse ?code= from query string
            std::string code;
            libhttppp::HttpForm form;
            form.parse(req);
            for (const auto &qp : form.urlData()) {
                if (qp.key == "code") code = qp.value;
            }

            if (code.empty()) {
                libhttppp::HttpResponse resp;
                resp.setState(HTTP400);
                std::string body = "Missing authorization code";
                resp.setContentLength(body.size());
                resp.send(req, body.c_str(), body.size());
                return true;
            }

            // Exchange code for tokens
            std::string redirectUri = Args->config->getsiteurl() + Args->config->buildurl("survey/_api/gdrive/callback");
            try {
                libhttppp::HttpUrl tokenUrl("https://oauth2.googleapis.com/token");
                libhttppp::HttpClient client(tokenUrl);

                libhttppp::HttpRequest tokenReq;
                tokenReq.setRequestType(POSTREQUEST);
                tokenReq.setRequestURL("/token");
                tokenReq.setHeaderData("host")->push_back("oauth2.googleapis.com");
                tokenReq.setHeaderData("content-type")->push_back("application/x-www-form-urlencoded");

                std::string postBody = "grant_type=authorization_code"
                    "&code=" + code
                    + "&client_id=" + _gdriveClientId
                    + "&client_secret=" + _gdriveClientSecret
                    + "&redirect_uri=" + redirectUri;
                std::vector<char> postData(postBody.begin(), postBody.end());

                std::vector<char> resp = client.Post(tokenReq, postData);
                std::string respStr(resp.begin(), resp.end());

                json_object *respObj = json_tokener_parse(respStr.c_str());
                if (!respObj) {
                    libhttppp::HttpResponse httpResp;
                    httpResp.setState(HTTP500);
                    std::string body = "Invalid token response from Google";
                    httpResp.setContentLength(body.size());
                    httpResp.send(req, body.c_str(), body.size());
                    return true;
                }

                json_object *jToken = nullptr, *jRefresh = nullptr, *jExpIn = nullptr;
                json_object_object_get_ex(respObj, "access_token", &jToken);
                json_object_object_get_ex(respObj, "refresh_token", &jRefresh);
                json_object_object_get_ex(respObj, "expires_in", &jExpIn);

                if (jToken) {
                    _gdriveAccessToken = json_object_get_string(jToken);
                    int expiresIn = jExpIn ? json_object_get_int(jExpIn) : 3600;
                    _gdriveTokenExpiry = time(nullptr) + expiresIn;
                }

                if (jRefresh) {
                    _gdriveRefreshToken = json_object_get_string(jRefresh);
                    setConfig(*Args->database[tid], "SURVEY_GDRIVE_REFRESH_TOKEN", _gdriveRefreshToken);
                }

                json_object_put(respObj);

                // Redirect back to settings
                libhttppp::HttpResponse httpResp;
                httpResp.setState(HTTP302);
                httpResp.setHeaderData("Location")->push_back(
                    Args->config->buildurl("settings/survey/gdrive"));
                httpResp.setContentLength(0);
                httpResp.send(req, "", 0);
                return true;
            } catch (const std::exception &e) {
                std::cerr << "survey: OAuth callback failed: " << e.what() << std::endl;
                libhttppp::HttpResponse httpResp;
                httpResp.setState(HTTP500);
                std::string body = "Token exchange failed: " + std::string(e.what());
                httpResp.setContentLength(body.size());
                httpResp.send(req, body.c_str(), body.size());
                return true;
            }
        }

        // ----- Settings: Import form page -----
        json_object* settingsImportForm(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang, "Import Survey").c_str()));

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(
                Args->config->buildurl("settings/survey").c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            // Inline JS import UI
            std::ostringstream html;
            html << "<div id=\"survey-import\">"
                 << "<p>" << tr(_lang, "Select a .json file exported from another survey instance.") << "</p>"
                 << "<input type=\"file\" id=\"import-file\" accept=\".json,application/json\" />"
                 << "<br/><br/>"
                 << "<button id=\"import-btn\" disabled>" << tr(_lang, "Import") << "</button>"
                 << "<div id=\"import-msg\" style=\"margin-top:10px;\"></div>"
                 << "</div>"
                 << "<script>"
                 << "(function(){"
                 << "var fileInput=document.getElementById('import-file');"
                 << "var btn=document.getElementById('import-btn');"
                 << "var msg=document.getElementById('import-msg');"
                 << "var fileData=null;"
                 << "fileInput.addEventListener('change',function(){"
                 << "var f=fileInput.files[0];if(!f){btn.disabled=true;return;}"
                 << "var reader=new FileReader();"
                 << "reader.onload=function(e){"
                 << "try{fileData=JSON.parse(e.target.result);"
                 << "if(!fileData.name||!fileData.survey){msg.textContent='Invalid file format: missing name or survey';msg.style.color='red';btn.disabled=true;return;}"
                 << "msg.textContent='Ready to import: '+fileData.name;msg.style.color='green';btn.disabled=false;"
                 << "}catch(err){msg.textContent='Invalid JSON: '+err.message;msg.style.color='red';btn.disabled=true;}};"
                 << "reader.readAsText(f);});"
                 << "btn.addEventListener('click',function(){"
                 << "if(!fileData)return;btn.disabled=true;btn.textContent='" << tr(_lang, "Importing...") << "';"
                 << "var xhr=new XMLHttpRequest();"
                 << "xhr.open('POST','" << Args->config->buildurl("survey/_api/import") << "',true);"
                 << "xhr.setRequestHeader('Content-Type','application/json');"
                 << "xhr.onreadystatechange=function(){"
                 << "if(xhr.readyState===4){"
                 << "try{var r=JSON.parse(xhr.responseText);"
                 << "if(r.status==='ok'){msg.textContent=r.message||'Imported!';msg.style.color='green';"
                 << "setTimeout(function(){window.location.href='" << Args->config->buildurl("settings/survey") << "';},1000);}"
                 << "else{msg.textContent=r.message||'Error';msg.style.color='red';}"
                 << "}catch(e){msg.textContent='Network error';msg.style.color='red';}"
                 << "btn.disabled=false;btn.textContent='" << tr(_lang, "Import") << "';}};"
                 << "xhr.send(JSON.stringify(fileData));});"
                 << "})();"
                 << "</script>";

            json_object_object_add(jroot, "html", json_object_new_string(html.str().c_str()));
            return jroot;
        }

        // ----- Settings API: AJAX/JSON CRUD for survey templates (LEGACY) -----
        json_object* settingsApi(const int tid, libhttppp::HttpRequest &req) {
            std::string curl = req.getRequest();
            std::string apiBase = Args->config->buildurl("settings/survey/api/");
            std::string action;
            if (curl.length() > apiBase.length())
                action = curl.substr(apiBase.length());

            json_object *jroot = json_object_new_object();

            if (action == "list") {
                loadTemplates(tid);
                json_object *jarr = json_object_new_array();
                for (const auto &[name, tpl] : _templates) {
                    json_object *jitem = json_object_new_object();
                    json_object_object_add(jitem, "name", json_object_new_string(name.c_str()));
                    json_object_object_add(jitem, "title", json_object_new_string(tpl.title.c_str()));
                    json_object_object_add(jitem, "questionCount", json_object_new_int(tpl.questions.size()));
                    json_object_array_add(jarr, jitem);
                }
                json_object_object_add(jroot, "status", json_object_new_string("ok"));
                json_object_object_add(jroot, "surveys", jarr);
                return jroot;
            }

            if (action.find("get") == 0) {
                // Extract name from query string: api/get?name=xxx
                std::string name;
                size_t qpos = curl.find("name=");
                if (qpos != std::string::npos) name = curl.substr(qpos + 5);
                // URL-decode simple %xx not needed for slug names

                loadTemplates(tid);
                auto it = _templates.find(name);
                if (it == _templates.end()) {
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Survey not found"));
                    return jroot;
                }
                json_object_object_add(jroot, "status", json_object_new_string("ok"));
                json_object_object_add(jroot, "name", json_object_new_string(name.c_str()));
                json_object_object_add(jroot, "survey", templateToJson(it->second));
                return jroot;
            }

            if (action == "save" && req.getRequestType() == POSTREQUEST) {
                size_t clen = req.getContentLength();
                if (clen == 0 || clen > req.RecvData.size()) {
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Empty request body"));
                    return jroot;
                }
                std::string body(req.RecvData.data(), clen);
                json_object *jreq = json_tokener_parse(body.c_str());
                if (!jreq) {
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Invalid JSON"));
                    return jroot;
                }

                json_object *jname_obj = nullptr, *jsurvey_obj = nullptr;
                json_object_object_get_ex(jreq, "name", &jname_obj);
                json_object_object_get_ex(jreq, "survey", &jsurvey_obj);
                if (!jname_obj || !jsurvey_obj) {
                    json_object_put(jreq);
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Missing name or survey"));
                    return jroot;
                }

                std::string sname = json_object_get_string(jname_obj);
                SurveyTemplate tpl;
                if (!templateFromJson(jsurvey_obj, tpl)) {
                    json_object_put(jreq);
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Invalid survey data"));
                    return jroot;
                }

                json_object *jtpl = templateToJson(tpl);
                const char *jsonStr = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN | JSON_C_TO_STRING_NOSLASHESCAPE);
                json_object_put(jreq);

                // Upsert: delete + insert
                dbpp::SQL sql;
                dbpp::DBResult res;
                std::vector<char> sbuf;
                sql << "DELETE FROM survey_templates WHERE name='"
                    << sql.escaped(sbuf, sname.c_str()) << "';";
                try { Args->database[tid]->exec(sql, res); } catch (...) {}

                sql.clear();
                sql << "INSERT INTO survey_templates (name, xml_data, title) VALUES('"
                    << sql.escaped(sbuf, sname.c_str()) << "','"
                    << sql.escaped(sbuf, jsonStr) << "','"
                    << sql.escaped(sbuf, tpl.title.c_str()) << "');";
                json_object_put(jtpl);
                try {
                    Args->database[tid]->exec(sql, res);
                } catch (...) {
                    json_object_object_add(jroot, "status", json_object_new_string("error"));
                    json_object_object_add(jroot, "message", json_object_new_string("Database error"));
                    return jroot;
                }

                loadTemplates(tid);
                json_object_object_add(jroot, "status", json_object_new_string("ok"));
                json_object_object_add(jroot, "message", json_object_new_string("Saved"));
                return jroot;
            }

            json_object_object_add(jroot, "status", json_object_new_string("error"));
            json_object_object_add(jroot, "message", json_object_new_string("Unknown API action"));
            return jroot;
        }

        // ----- Settings: Survey Form Editor (SPA) -----
        json_object* settingsEditor(const int tid, libhttppp::HttpRequest &req) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string curl = req.getRequest();
            std::string surveyName;
            size_t qpos = curl.find("name=");
            if (qpos != std::string::npos) surveyName = curl.substr(qpos + 5);

            std::string saveUrl = Args->config->buildurl("survey/_api/save");
            std::string apiUrl = Args->config->buildurl("api");
            std::string backUrl = Args->config->buildurl("settings/survey");

            // Load survey template data inline
            loadTemplates(tid);
            std::string surveyJson = "{}";
            auto it = _templates.find(surveyName);
            if (it != _templates.end()) {
                json_object *jtpl = templateToJson(it->second);
                surveyJson = json_object_to_json_string_ext(jtpl, JSON_C_TO_STRING_PLAIN);
                json_object_put(jtpl);
            }

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(
                (tr(_lang, "Edit Survey") + ": " + surveyName).c_str()));

            json_object *jnav = json_object_new_array();
            json_object *jback = json_object_new_object();
            json_object_object_add(jback, "label", json_object_new_string(tr(_lang, "Back to list").c_str()));
            json_object_object_add(jback, "url", json_object_new_string(backUrl.c_str()));
            json_object_array_add(jnav, jback);
            json_object_object_add(jroot, "nav", jnav);

            std::ostringstream html;
            html << "<div id=\"survey-editor\" data-name=\"" << htmlEscape(surveyName) << "\" "
                 << "data-save-url=\"" << htmlEscape(saveUrl) << "\" "
                 << "data-api-url=\"" << htmlEscape(apiUrl) << "\" "
                 << "data-survey=\"" << htmlEscape(surveyJson) << "\">"
                 << "</div>";

            // Editor CSS – define theme vars for dark settings modal
            html << "<style>"
                 << "#survey-editor{font-family:system-ui,-apple-system,sans-serif;"
                 << "--blogi-bg:rgb(42,46,50);--blogi-surface:rgb(55,60,65);--blogi-text:#eee;"
                 << "--blogi-text-muted:#aaa;--blogi-border:rgb(75,80,85);--blogi-accent:rgb(58,212,58);"
                 << "--blogi-accent-dark:rgb(40,180,40);--blogi-bg-alt:rgb(48,52,56);"
                 << "--blogi-error:#F36947;--blogi-error-bg:rgb(80,40,35);--blogi-error-text:#f99;"
                 << "--blogi-success-bg:rgb(35,70,40);--blogi-success-text:#8f8;}"
                 << "#se-toolbar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center;}"
                 << "#se-toolbar button,#se-toolbar select{padding:8px 16px;border-radius:6px;border:1px solid var(--blogi-border,#ddd);cursor:pointer;font-size:.9em;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);}"
                 << "#se-toolbar button:hover{background:var(--blogi-accent,#2da82d);color:#fff;border-color:var(--blogi-accent,#2da82d);}"
                 << "#se-save{background:var(--blogi-accent,#2da82d)!important;color:#fff!important;border-color:var(--blogi-accent,#2da82d)!important;font-weight:700;}"
                 << "#se-msg{padding:8px 12px;border-radius:6px;margin-bottom:12px;display:none;}"
                 << "#se-msg.ok{display:block;background:var(--blogi-success-bg,#e8f5e9);color:var(--blogi-success-text,#2e7d32);}"
                 << "#se-msg.err{display:block;background:var(--blogi-error-bg,#fbe9e7);color:var(--blogi-error-text,#c62828);}"
                 << ".se-meta{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;}"
                 << ".se-meta label{display:block;font-weight:600;margin-bottom:4px;color:var(--blogi-text,#333);}"
                 << ".se-meta input,.se-meta textarea{width:100%;padding:8px;border:1px solid var(--blogi-border,#ddd);border-radius:6px;font-size:.95em;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);box-sizing:border-box;}"
                 << ".se-meta input[readonly]{opacity:.7;cursor:default;}"
                 << ".se-meta textarea{grid-column:1/-1;min-height:60px;resize:vertical;}"
                 << ".se-question{border:1px solid var(--blogi-border,#ddd);border-radius:8px;padding:16px;margin-bottom:12px;background:var(--blogi-bg-alt,#fafafa);position:relative;}"
                 << ".se-question:hover{border-color:var(--blogi-accent,#2da82d);}"
                 << ".se-q-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}"
                 << ".se-q-header .se-q-id{font-weight:700;color:var(--blogi-accent,#2da82d);}"
                 << ".se-q-actions button{background:none;border:none;cursor:pointer;font-size:1.1em;padding:2px 6px;color:var(--blogi-text-muted,#888);}"
                 << ".se-q-actions button:hover{color:var(--blogi-error,#F36947);}"
                 << ".se-q-row{display:grid;grid-template-columns:1fr 120px 80px;gap:8px;margin-bottom:8px;align-items:end;}"
                 << ".se-q-row label{display:block;font-size:.85em;font-weight:600;margin-bottom:2px;color:var(--blogi-text-muted,#666);}"
                 << ".se-q-row input,.se-q-row select{width:100%;padding:6px 8px;border:1px solid var(--blogi-border,#ddd);border-radius:4px;font-size:.9em;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);}"
                 << ".se-q-row input[type=checkbox]{width:auto;}"
                 << ".se-opts{margin-top:8px;padding-top:8px;border-top:1px solid var(--blogi-border,#eee);}"
                 << ".se-opts h4{margin:0 0 6px;font-size:.85em;color:var(--blogi-text-muted,#888);}"
                 << ".se-opt-row{display:grid;grid-template-columns:1fr 1fr 1fr 30px;gap:6px;margin-bottom:6px;align-items:start;}"
                 << ".se-opt-row input{padding:4px 6px;border:1px solid var(--blogi-border,#ddd);border-radius:4px;font-size:.85em;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);}"
                 << ".se-opt-row select.se-exc{padding:2px 4px;border:1px solid var(--blogi-border,#ddd);border-radius:4px;font-size:.8em;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);min-height:48px;}"
                 << ".se-opt-row button{background:none;border:none;cursor:pointer;color:var(--blogi-error,#F36947);font-size:1em;}"
                 << ".se-add-opt{background:none;border:1px dashed var(--blogi-border,#ccc);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:.85em;color:var(--blogi-text-muted,#888);margin-top:4px;}"
                 << ".se-add-opt:hover{border-color:var(--blogi-accent,#2da82d);color:var(--blogi-accent,#2da82d);}"
                 << ".se-pick-icon{padding:4px 8px;background:var(--blogi-accent,#2da82d);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:1em;}"
                 << ".se-pick-icon:hover{opacity:.8;}"
                 << ".se-opt-icon{font-size:.85em;}"
                 << ".se-opt-preview{width:32px;height:32px;object-fit:contain;border-radius:4px;border:1px solid var(--blogi-border,#555);}"
                 << ".se-rating-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px;}"
                 << ".se-rating-row label{font-size:.85em;color:var(--blogi-text-muted,#666);}"
                 << ".se-rating-row input{width:70px;padding:4px 6px;border:1px solid var(--blogi-border,#ddd);border-radius:4px;background:var(--blogi-surface,#fff);color:var(--blogi-text,#333);}"
                 << "</style>";

            // Editor JS
            html << "<script>"
                 << "(function(){"
                 << "var el=document.getElementById('survey-editor');"
                 << "var saveUrl=el.getAttribute('data-save-url');"
                 << "var apiUrl=el.getAttribute('data-api-url');"
                 << "var surveyName=el.getAttribute('data-name');"
                 << "var originalName=surveyName;"
                 << "var survey=JSON.parse(el.getAttribute('data-survey'));"
                 << "if(!survey.questions)survey.questions=[];"
                 << ""
                 << "function msg(text,ok){"
                 << "var m=document.getElementById('se-msg');"
                 << "if(!m)return;m.textContent=text;m.className=ok?'ok':'err';"
                 << "if(ok)setTimeout(function(){m.style.display='none';m.className='';},3000);}"
                 << ""
                 << "function save(){"
                 << "readForm();"
                 << "var xhr=new XMLHttpRequest();"
                 << "xhr.open('POST',saveUrl,true);"
                 << "xhr.setRequestHeader('Content-Type','application/json');"
                 << "xhr.onreadystatechange=function(){"
                 << "if(xhr.readyState===4){"
                 << "try{var r=JSON.parse(xhr.responseText);"
                 << "if(r.status==='ok'){msg('Gespeichert!',true);originalName=surveyName;}else msg(r.message||'Fehler',false);"
                 << "}catch(e){msg('Network error',false);}}};"
                 << "xhr.send(JSON.stringify({name:surveyName,originalName:originalName,survey:survey}));}"
                 << ""
                 << "function readForm(){"
                 << "surveyName=document.getElementById('se-sid').value.trim();"
                 << "survey.title=document.getElementById('se-stitle').value;"
                 << "survey.description=document.getElementById('se-sdesc').value;"
                 << "var qEls=el.querySelectorAll('.se-question');"
                 << "survey.questions=[];"
                 << "for(var i=0;i<qEls.length;i++){"
                 << "var qe=qEls[i];"
                 << "var q={id:qe.querySelector('.se-qid').value,"
                 << "type:qe.querySelector('.se-qtype').value,"
                 << "text:qe.querySelector('.se-qtext').value,"
                 << "required:qe.querySelector('.se-qreq').checked,"
                 << "order:parseInt(qe.querySelector('.se-qorder').value)||0,"
                 << "multiSelect:qe.querySelector('.se-qmulti')?qe.querySelector('.se-qmulti').checked:false,"
                 << "options:[]};"
                 << "if(q.type==='rating'){"
                 << "var minI=qe.querySelector('.se-rmin');var maxI=qe.querySelector('.se-rmax');"
                 << "if(minI)q.ratingMin=parseInt(minI.value)||1;"
                 << "if(maxI)q.ratingMax=parseInt(maxI.value)||5;}"
                 << "var optEls=qe.querySelectorAll('.se-opt-row');"
                 << "for(var j=0;j<optEls.length;j++){"
                 << "var inputs=optEls[j].querySelectorAll('input');"
                 << "if(inputs.length>=2){"
                 << "var o={value:inputs[0].value,label:inputs[1].value};"
                 << "var excSel=optEls[j].querySelector('.se-exc');"
                 << "if(excSel){var sv=[];for(var si=0;si<excSel.options.length;si++){if(excSel.options[si].selected)sv.push(excSel.options[si].value);}if(sv.length)o.excludes=sv.join(',');}"
                 << "var iconI=optEls[j].querySelector('.se-opt-icon');"
                 << "if(iconI&&iconI.value)o.icon=iconI.value;"
                 << "q.options.push(o);}}"
                 << "survey.questions.push(q);}}"
                 << ""
                 << "function render(){"
                 << "var h='<div id=\"se-msg\"></div>';"
                 << "h+='<div id=\"se-toolbar\">';"
                 << "h+='<button id=\"se-save\">Speichern</button>';"
                 << "h+='<button id=\"se-add-q\">+ Frage</button>';"
                 << "h+='</div>';"
                 << "h+='<div class=\"se-meta\">';"
                 << "h+='<div><label>Name</label><input id=\"se-sid\" value=\"'+esc(surveyName)+'\"/></div>';"
                 << "h+='<div><label>Title</label><input id=\"se-stitle\" value=\"'+esc(survey.title||'')+'\"/></div>';"
                 << "h+='<label style=\"grid-column:1/-1\">Beschreibung</label>';"
                 << "h+='<textarea id=\"se-sdesc\" style=\"grid-column:1/-1\">'+esc(survey.description||'')+'</textarea>';"
                 << "h+='</div>';"
                 << "h+='<div id=\"se-questions\">';"
                 << "var qs=survey.questions||[];"
                 << "for(var i=0;i<qs.length;i++){h+=renderQuestion(qs[i],i);}"
                 << "h+='</div>';"
                 << "el.innerHTML=h;"
                 << "document.getElementById('se-save').addEventListener('click',save);"
                 << "document.getElementById('se-add-q').addEventListener('click',function(){addQuestion();});"
                 << "bindEvents();}"
                 << ""
                 << "function genUUID(){return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==='x'?r:(r&0x3|0x8)).toString(16);});}"
                 << "function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}"
                 << ""
                 << "function renderQuestion(q,idx){"
                 << "if(!q.id||/^q\\d+$/.test(q.id)){q.id=genUUID();}"
                 << "var h='<div class=\"se-question\" data-idx=\"'+idx+'\">';"
                 << "h+='<div class=\"se-q-header\"><span class=\"se-q-id\">'+esc(q.id.substring(0,8))+'</span>';"
                 << "h+='<div class=\"se-q-actions\">';"
                 << "h+='<button class=\"se-q-up\" title=\"Up\">&uarr;</button>';"
                 << "h+='<button class=\"se-q-down\" title=\"Down\">&darr;</button>';"
                 << "h+='<button class=\"se-q-del\" title=\"Delete\">&times;</button>';"
                 << "h+='</div></div>';"
                 << "h+='<div class=\"se-q-row\">';"
                 << "h+='<div><label>Fragetext</label><input class=\"se-qtext\" value=\"'+esc(q.text||'')+'\"/></div>';"
                 << "h+='<div><label>Typ</label><select class=\"se-qtype\">';"
                 << "var types=['radio','checkbox','text','select','rating','image','icon'];"
                 << "for(var t=0;t<types.length;t++){h+='<option value=\"'+types[t]+'\"'+(q.type===types[t]?' selected':'')+'>'+types[t]+'</option>';}"
                 << "h+='</select></div>';"
                 << "h+='<div><label>Pflicht</label><br/><input type=\"checkbox\" class=\"se-qreq\"'+(q.required?' checked':'')+'/></div>';"
                 << "h+='<div><label>Reihenfolge</label><input type=\"number\" class=\"se-qorder\" value=\"'+(q.order||0)+'\" style=\"width:60px\"/></div>';"
                 << "if(q.type==='icon'){h+='<div><label>Mehrfach</label><br/><input type=\"checkbox\" class=\"se-qmulti\"'+(q.multiSelect?' checked':'')+'/></div>';}"
                 << "h+='</div>';"
                 << "h+='<div style=\"display:none\"><input class=\"se-qid\" value=\"'+esc(q.id)+'\"/></div>';"
                 << "if(q.type==='rating'){"
                 << "h+='<div class=\"se-rating-row\"><div><label>Min</label><input type=\"number\" class=\"se-rmin\" value=\"'+(q.ratingMin||1)+'\"/></div>';"
                 << "h+='<div><label>Max</label><input type=\"number\" class=\"se-rmax\" value=\"'+(q.ratingMax||5)+'\"/></div></div>';}"
                 << "if(q.type==='radio'||q.type==='checkbox'||q.type==='select'||q.type==='icon'){"
                 << "h+='<div class=\"se-opts\"><h4>Optionen</h4>';"
                 << "var opts=q.options||[];"
                 << "for(var j=0;j<opts.length;j++){"
                 << "h+='<div class=\"se-opt-row\">';"
                 << "h+='<input placeholder=\"value\" value=\"'+esc(opts[j].value||'')+'\"/>';"
                 << "h+='<input placeholder=\"label\" value=\"'+esc(opts[j].label||'')+'\"/>';"
                 << "var excArr=(opts[j].excludes||'').split(',').filter(function(s){return s.trim();});"
                 << "h+='<select class=\"se-exc\" multiple title=\"Excludes\">';"
                 << "var allQs=survey.questions||[];"
                 << "for(var k=0;k<allQs.length;k++){"
                 << "if(allQs[k].id===q.id)continue;"
                 << "var sel=excArr.indexOf(allQs[k].id)!==-1?' selected':'';"
                 << "var lbl=allQs[k].id.substring(0,8)+(allQs[k].text?' - '+allQs[k].text.substring(0,20):'');"
                 << "h+='<option value=\"'+esc(allQs[k].id)+'\"'+sel+'>'+esc(lbl)+'</option>';}"
                 << "h+='</select>';"
                 << "if(q.type==='icon'){"
                 << "var icoUrl=opts[j].icon||'';"
                 << "if(icoUrl)h+='<img src=\"'+esc(icoUrl)+'\" class=\"se-opt-preview\" alt=\"\"/>';"
                 << "h+='<input class=\"se-opt-icon\" placeholder=\"icon URL\" value=\"'+esc(icoUrl)+'\" style=\"flex:2\"/>';"
                 << "h+='<button class=\"se-pick-icon\" type=\"button\" title=\"Media\">&#128247;</button>';}"
                 << "h+='<button class=\"se-opt-del\">&times;</button></div>';}"
                 << "h+='<button class=\"se-add-opt\">+ Option</button></div>';}"
                 << "h+='</div>';return h;}"
                 << ""
                 << "function addQuestion(){"
                 << "readForm();"
                 << "var maxOrd=0;for(var i=0;i<survey.questions.length;i++){if(survey.questions[i].order>maxOrd)maxOrd=survey.questions[i].order;}"
                 << "survey.questions.push({id:genUUID(),type:'radio',text:'',required:false,order:maxOrd+1,options:[]});"
                 << "render();}"
                 << ""
                 << "function bindEvents(){"
                 << "el.onclick=function(e){"
                 << "var t=e.target;"
                 << "if(t.classList.contains('se-q-del')){readForm();var qe=t.closest('.se-question');var idx=parseInt(qe.getAttribute('data-idx'));survey.questions.splice(idx,1);render();}"
                 << "else if(t.classList.contains('se-q-up')){readForm();var qe=t.closest('.se-question');var idx=parseInt(qe.getAttribute('data-idx'));if(idx>0){var tmp=survey.questions[idx];survey.questions[idx]=survey.questions[idx-1];survey.questions[idx-1]=tmp;var tmpO=survey.questions[idx].order;survey.questions[idx].order=survey.questions[idx-1].order;survey.questions[idx-1].order=tmpO;render();}}"
                 << "else if(t.classList.contains('se-q-down')){readForm();var qe=t.closest('.se-question');var idx=parseInt(qe.getAttribute('data-idx'));if(idx<survey.questions.length-1){var tmp=survey.questions[idx];survey.questions[idx]=survey.questions[idx+1];survey.questions[idx+1]=tmp;var tmpO=survey.questions[idx].order;survey.questions[idx].order=survey.questions[idx-1].order;survey.questions[idx-1].order=tmpO;render();}}"
                 << "else if(t.classList.contains('se-opt-del')){readForm();var row=t.closest('.se-opt-row');var qe=t.closest('.se-question');var qi=parseInt(qe.getAttribute('data-idx'));var opts=qe.querySelectorAll('.se-opt-row');for(var i=0;i<opts.length;i++){if(opts[i]===row){survey.questions[qi].options.splice(i,1);break;}}render();}"
                 << "else if(t.classList.contains('se-add-opt')){readForm();var qe=t.closest('.se-question');var qi=parseInt(qe.getAttribute('data-idx'));survey.questions[qi].options.push({value:'',label:'',excludes:'',icon:''});render();}"
                 << "else if(t.classList.contains('se-pick-icon')){var inp=t.previousElementSibling;if(inp)openMediaPicker(inp);}};"
                 << "var typeSelects=el.querySelectorAll('.se-qtype');"
                 << "for(var i=0;i<typeSelects.length;i++){"
                 << "typeSelects[i].addEventListener('change',function(){readForm();render();});}}"
                 << ""
                 // media picker for icon type
                 << "function getAuthId(){var m=document.cookie.match(/authid=([^;]+)/);return m?m[1]:null;}"
                 << "function apiCall(cmds,cb){"
                 << "var aid=getAuthId();if(aid)cmds.unshift({authid:aid});"
                 << "var xhr=new XMLHttpRequest();xhr.open('POST',apiUrl,true);"
                 << "xhr.setRequestHeader('Content-Type','application/json');"
                 << "xhr.onreadystatechange=function(){"
                 << "if(xhr.readyState===4){try{var r=JSON.parse(xhr.responseText);"
                 << "if(aid&&Array.isArray(r))r=r.slice(1);cb(null,r);}catch(e){cb(e);}}};"
                 << "xhr.send(JSON.stringify(cmds));}"
                 << ""
                 << "var _sizesRegistered=false;"
                 << "function ensureSizesRegistered(mediaId){"
                 << "if(_sizesRegistered)return;_sizesRegistered=true;"
                 << "apiCall([{command:'media_create_preview',media_id:mediaId,width:100,height:100}],function(){});"
                 << "apiCall([{command:'media_create_preview',media_id:mediaId,width:64,height:64}],function(){});}"
                 << ""
                 << "function openMediaPicker(targetInput){"
                 << "var old=document.getElementById('se-media-modal');if(old)old.remove();"
                 << "var ov=document.createElement('div');ov.id='se-media-modal';"
                 << "ov.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center;';"
                 << "ov.onclick=function(e){if(e.target===ov)ov.remove();};"
                 << "var box=document.createElement('div');"
                 << "box.style.cssText='background:#2a2e32;color:#e0e0e0;border-radius:10px;padding:20px;width:560px;max-height:70vh;display:flex;flex-direction:column;';"
                 << "var navEl=document.createElement('div');"
                 << "var contentEl=document.createElement('div');contentEl.style.cssText='flex:1;overflow-y:auto;min-height:150px;';contentEl.textContent='Laden...';"
                 << "var closeBtn=document.createElement('button');closeBtn.type='button';closeBtn.textContent='Abbrechen';"
                 << "closeBtn.style.cssText='padding:6px 16px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;';"
                 << "closeBtn.onclick=function(){ov.remove();};"
                 << "var h3=document.createElement('h3');h3.style.cssText='margin:0 0 12px';h3.textContent='Media auswählen';"
                 << "var foot=document.createElement('div');foot.style.cssText='margin-top:12px;text-align:right;';foot.appendChild(closeBtn);"
                 << "box.appendChild(h3);box.appendChild(navEl);box.appendChild(contentEl);box.appendChild(foot);"
                 << "ov.appendChild(box);document.body.appendChild(ov);"
                 << "apiCall([{command:'media_list_albums'}],function(err,res){"
                 << "if(err||!res){contentEl.textContent='Fehler';return;}"
                 << "var al=null;for(var i=0;i<res.length;i++){if(res[i]&&res[i].albums){al=res[i].albums;break;}}"
                 << "if(!al){contentEl.textContent='Keine Alben';return;}"
                 << "var h='';for(var i=0;i<al.length;i++){h+='<div class=\"se-mb-album\" data-id=\"'+esc(al[i].id)+'\" data-name=\"'+esc(al[i].name)+'\" style=\"padding:10px;margin:4px 0;background:#3a3e42;border-radius:6px;cursor:pointer;\">'+esc(al[i].name)+' ('+al[i].media_count+')</div>';}"
                 << "contentEl.innerHTML=h;"
                 << "contentEl.onclick=function(ev){"
                 << "var ab=ev.target.closest('.se-mb-album');if(!ab)return;"
                 << "loadAlbumMedia(ab.getAttribute('data-id'),ab.getAttribute('data-name'),targetInput,ov,navEl,contentEl);};});}"
                 << ""
                 << "function loadAlbumMedia(albumId,albumName,targetInput,ov,navEl,contentEl){"
                 << "navEl.innerHTML='<button type=\"button\" style=\"padding:4px 10px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;\">&larr; Zurück</button> <strong>'+esc(albumName)+'</strong>';"
                 << "navEl.querySelector('button').onclick=function(){ov.remove();openMediaPicker(targetInput);};"
                 << "contentEl.textContent='Laden...';"
                 << "apiCall([{command:'media_list_media',album_id:albumId}],function(err,res){"
                 << "if(err||!res){contentEl.textContent='Fehler';return;}"
                 << "var ml=null;for(var i=0;i<res.length;i++){if(res[i]&&res[i].media){ml=res[i].media;break;}}"
                 << "if(!ml||ml.length===0){contentEl.textContent='Leer';return;}"
                 << "var h='<div style=\"display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:8px;\">';"
                 << "for(var i=0;i<ml.length;i++){var m=ml[i];"
                 << "if(i===0)ensureSizesRegistered(m.id);"
                 << "var thumb=m.url+'?w=100&h=100';"
                 << "h+='<div class=\"se-mb-item\" data-url=\"'+esc(m.url)+'\" style=\"cursor:pointer;border:2px solid transparent;border-radius:8px;padding:4px;background:#3a3e42;text-align:center;\">';"
                 << "if(m.kind==='image')h+='<img src=\"'+esc(thumb)+'\" loading=\"lazy\" style=\"width:100%;height:70px;object-fit:cover;border-radius:4px;\"/>';"
                 << "else h+='<div style=\"height:70px;display:flex;align-items:center;justify-content:center;font-size:2em;\">&#128196;</div>';"
                 << "h+='<div style=\"font-size:.7em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\">'+esc(m.filename)+'</div></div>';}"
                 << "h+='</div>';contentEl.innerHTML=h;"
                 << "contentEl.onclick=function(ev){"
                 << "var it=ev.target.closest('.se-mb-item');if(!it)return;"
                 << "var murl=it.getAttribute('data-url');"
                 << "targetInput.value=murl+'?w=64&h=64';"
                 << "ov.remove();};});}"
                 << ""
                 << "render();"
                 << "})();"
                 << "</script>";

            json_object_object_add(jroot, "html", json_object_new_string(html.str().c_str()));
            return jroot;
        }

    };
}

EXPORT blogi::PluginApi* create() {
    return new blogi::Survey();
}

EXPORT void destroy(blogi::PluginApi* p) {
    delete p;
}
