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

#include <algorithm>
#include <iostream>
#include <cstring>
#include <fstream>
#include <compare>
#include <string>
#include <filesystem>
#include <map>
#include <iterator>

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

#include <htmlpp/exception.h>

#include "theme.h"
#include "conf.h"
#include "compress.h"
#include "i18n.h"

blogi::Theme::Theme(blogi::ThemeConfig& config,bool formated) : _Config(config){
    auto trimlines = [&] (const std::string in){
        // Lambda function to minify/compress whitespace in text content (CSS/JS/HTML)
        std::string out;
        bool space=false;
        for(auto i : in){
            switch(i){
                case '\r':
                    space=true;
                    continue;
                case '\n':
                    if(!space)
                        out.push_back(' ');
                    space=true;
                    continue;
               case '\t':
                    if(!space)
                        out.push_back(' ');
                    space=true;
                    continue;
                case ' ':
                    if(space)
                        continue;
                    space=true;
                default:
                    space=false;
                    out.push_back(i);
                    break;
            }
        }
        return out;
    };

    _Formated=formated;

    std::string ppath(_Config.Theme);
#ifdef Windows
    ppath+="\\public";
#else
    ppath+="/public";
#endif
    std::filesystem::path public_path = std::filesystem::path(ppath);

    if (std::filesystem::exists(public_path) && std::filesystem::is_directory(public_path)) {
        // [DEBUG] Theme: Found public asset directory: ppath
        for (const auto& entry : std::filesystem::directory_iterator(public_path)) {
            if (std::filesystem::is_regular_file(entry.path())) {

                std::string buf;
                char* tmp = new char[BLOCKSIZE];
                std::ifstream fs_stream;

                try {
                    fs_stream.open(entry.path(), std::ios::in | std::ios::binary);
                }
                catch (const std::exception& e) {
                    std::cerr << "Could not open file: " << entry.path().filename().string() << " - " << e.what() << std::endl; // Enhanced error log
                    delete[] tmp;
                    continue;
                }

                // Read file content into buffer
                while (fs_stream.good()) {
                    fs_stream.read(tmp, BLOCKSIZE);
                    buf.append(tmp, fs_stream.gcount());
                }

                fs_stream.close();
                delete[] tmp;

                ThemeFiles tfile;
                // Path is stored with leading slash for direct matching later
                tfile.Path =  "/theme/public/" + entry.path().filename().string();

                std::string fname = entry.path().filename().string();
                size_t dot_pos = fname.rfind('.');
                size_t end_pos = fname.rfind('?') != std::string::npos ? fname.rfind('?') : fname.length();

                // Determine file extension
                if (dot_pos != std::string::npos && ++dot_pos < end_pos) {
                    tfile.Ending = fname.substr(dot_pos,end_pos-dot_pos);
                } else {
                    tfile.Ending = "";
                }

                // Initialize content with the raw buffer
                tfile.Content = buf;

                if (tfile.Ending == "png" || tfile.Ending == "jpg" || tfile.Ending == "webp" || tfile.Ending == "ico" || tfile.Ending == "svg") {
                    tfile.Type = ThemeFilesTypes::IMAGE;
                }
                else if (tfile.Ending == "css" || tfile.Ending == "html") {
                    tfile.Content = trimlines(buf); // Apply minification
                    compress(tfile.Content, tfile.Compressed); // Compress content
                    tfile.Type = ThemeFilesTypes::TEXT;
                }
                else if (tfile.Ending == "js") {
                    tfile.Content = trimlines(buf); // Apply minification
                    compress(tfile.Content, tfile.Compressed); // Compress content
                    tfile.Type = ThemeFilesTypes::JAVASCRIPT;
                }
                else {
                    tfile.Type = ThemeFilesTypes::GENERIC;
                }

                _PublicFiles.push_back(tfile);
                // [DEBUG] Theme: Successfully loaded asset: tfile.Path (bytes)
            }
        }
    } else {
        std::cerr << "[WARNING] Theme: Public asset directory not found or is not a directory: " << ppath << std::endl; // Warning log
    }
}

blogi::Theme::~Theme(){
    // Destructor implementation
}

std::string blogi::Theme::getLogoUrl(const int tid) const {
    std::string logoUrl;
    try { getConfig(*_Config.TDatabase[tid], "THEME_LOGO_URL", logoUrl); } catch (...) {}
    if (!logoUrl.empty())
        return logoUrl;
    // Fallback to theme default
    return _Config.config.getprefix() + "/theme/public/logo.webp";
}

void blogi::Theme::renderPage(const int tid,const char *name,libhtmlpp::HtmlPage& page, libhtmlpp::HtmlElement &index){
    std::string htmlfile=_Config.Theme;
#ifdef Windows
    htmlfile.append("\\");
#else
    htmlfile.append("/");
#endif // Windows
    htmlfile.append(name);

    std::ifstream fhfile(htmlfile);
    std::vector<char> temp,data;

    if (fhfile.is_open()) {
        char ch;
        // Read the entire file character by character
        while (fhfile.get(ch)) {
            temp.push_back(ch);
        }
        fhfile.close();
    } else {
        std::cerr << "Error: Could not open file " << htmlfile << std::endl;
        return;
    }

    auto pos=temp.end(),epos=temp.begin();
    // Map of variables to replace in the HTML template
    std::string _lang = getLang(*_Config.TDatabase[tid]);
    // Logo dimensions
    std::string logoWidth = "100";
    try { getConfig(*_Config.TDatabase[tid], "THEME_LOGO_WIDTH", logoWidth); } catch (...) {}
    if (logoWidth.empty()) logoWidth = "100";

    std::map<std::string, std::string> variable_map = {
        {"PREFIX", _Config.config.getprefix()},
        {"SITEURL", _Config.config.getsiteurl()},
        {"LOGO_URL", getLogoUrl(tid)},
        {"LOGO_WIDTH", logoWidth},
        {"LOGO_HEIGHT", "auto"},
    };

    // Load site title from DB options
    {
        std::string siteTitle;
        try { getConfig(*_Config.TDatabase[tid], "SITE_TITLE", siteTitle); } catch (...) {}
        variable_map["SITE_TITLE"] = std::move(siteTitle);
    }

    // Inline CSS content as template variables to eliminate render-blocking requests
    const std::string prefix_placeholder = "${PREFIX}";
    const std::string& prefix_value = _Config.config.getprefix();
    for(const auto& file : _PublicFiles){
        std::string css_key;
        if(file.Path == "/theme/public/reset.css")
            css_key = "RESET_CSS";
        else if(file.Path == "/theme/public/style.css")
            css_key = "STYLE_CSS";
        else if(file.Path == "/theme/public/mstyle.css")
            css_key = "MSTYLE_CSS";

        if(!css_key.empty()){
            std::string css = file.Content;
            size_t p = 0;
            while((p = css.find(prefix_placeholder, p)) != std::string::npos){
                css.replace(p, prefix_placeholder.size(), prefix_value);
                p += prefix_value.size();
            }
            variable_map[css_key] = std::move(css);
        }
    }

    // Template variable replacement loop (e.g., replacing ${VARIABLE} placeholders)
    do {
        pos = std::find(epos, temp.end(), '$');

        if (pos == temp.end()) {
            std::copy(epos, temp.end(), std::back_inserter(data));
            break;
        }

        std::copy(epos, pos, std::back_inserter(data));

        auto var_start = pos;
        var_start+=2; // Move past "${"

        epos = std::find(var_start, temp.end(), '}');

        if (epos == temp.end()) {
            std::copy(pos, temp.end(), std::back_inserter(data)); // Copy literal "$..." if closing bracket is missing
            break;
        }

        std::string var_name(var_start, epos);

        auto it = variable_map.find(var_name);

        if (it != variable_map.end()) {
            const std::string& replacement = it->second;
            std::copy(replacement.begin(), replacement.end(), std::back_inserter(data));
        } else if (var_name.compare(0, 5, "I18N_") == 0) {
            const std::string& replacement = blogi::tr(_lang, var_name);
            std::copy(replacement.begin(), replacement.end(), std::back_inserter(data));
        } else {
            std::copy(pos, epos+1, std::back_inserter(data)); // Copy original placeholder if variable not found
        }

        ++epos;

    } while (pos != temp.end());

    data.push_back('\0'); // Null-terminate the string for C-style functions

    page.loadString(index,data.data()); // Load processed HTML content
}

bool blogi::Theme::Controller(const int tid,libhttppp::HttpRequest &req){
    // Helper lambda for converting a string to lowercase
    auto to_lower = [](const std::string& str) {
        std::string data = str;
        std::transform(data.begin(), data.end(), data.begin(),
                        [](unsigned char c){ return std::tolower(c); });
        return data;
    };

    std::string full_request_url = req.getRequestURL();

    // Strip the query string (e.g., ?v=17) from the requested URL
    size_t query_pos = full_request_url.find('?');
    if (query_pos != std::string::npos) {
        full_request_url = full_request_url.substr(0, query_pos);
    }

    // Convert the cleaned URL and prefix to lowercase for case-insensitive matching
    std::string publicf_lower = to_lower(full_request_url);
    std::string prefix_lower = to_lower(_Config.config.getprefix());

    // Standardize prefix by removing trailing slash if present (e.g., "/blog/" -> "/blog")
    if (prefix_lower.length() > 0 && prefix_lower.back() == '/') {
        prefix_lower.pop_back();
    }

    // Loop through all loaded public files and attempt an exact match
    for(auto curfile=_PublicFiles.begin(); curfile!=_PublicFiles.end(); curfile++){

        std::string curfile_path_lower = to_lower(curfile->Path); // e.g., /theme/public/style.css

        // Construct the full expected request path: /prefix/theme/public/style.css
        std::string expected_full_path = prefix_lower + curfile_path_lower;

        // Check if the request URL matches the expected full path exactly (case-insensitive)
        if (publicf_lower.length() == expected_full_path.length() &&
            publicf_lower.compare(0, publicf_lower.length(), expected_full_path) == 0)
        {

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setHeaderData("cache-control")->push_back("max-age=31536000");

            // Set Content-Type based on file ending
            if(curfile->Type==ThemeFilesTypes::IMAGE){
                if(curfile->Ending=="svg")
                    resp.setContentType(std::string("image/svg+xml").c_str());
                else if(curfile->Ending=="ico")
                    resp.setContentType(std::string("image/x-icon").c_str());
                else
                    resp.setContentType(std::string("image/").append(curfile->Ending).c_str());
            }else if(curfile->Type==ThemeFilesTypes::TEXT){
                resp.setContentType(std::string("text/").append(curfile->Ending).c_str());
            }else if(curfile->Type==ThemeFilesTypes::JAVASCRIPT){
                resp.setContentType("application/javascript");
            }else if(curfile->Type==ThemeFilesTypes::GENERIC){
                resp.setContentType("application/octet-stream");
            }

            bool compressed = false;
            libhttppp::HttpHeader::HeaderData *acph=req.getHeaderData("accept-encoding");

            // Check if client supports Brotli ("br")
            if(acph){
                for(libhttppp::HttpHeader::HeaderData::Values *cdat=acph->getfirstValue();
                    cdat; cdat=cdat->nextvalue()){
                    if(to_lower(cdat->getvalue()).find("br")!=std::string::npos){
                        compressed=true;
                        break;
                    }
                }
            }

            // Final Header Setup: Set Content-Encoding and Content-Length
            if( curfile->Compressed.length() > 0 && compressed ){
                resp.setHeaderData("content-encoding")->push_back("br");
                resp.setContentLength(curfile->Compressed.length());
                resp.send(req,"",-1);
            }else{
                // Send uncompressed/minified content
                resp.setContentLength(curfile->Content.length());
                resp.send(req,"",-1);
            }

            req.SendData.pos=0; // Reset position for streaming in the Response function
            return true;
        }
    }

    // If no match was found after checking all assets
    return false;
}

bool blogi::Theme::Response(const int tid, libhttppp::HttpRequest &req){

    auto to_lower = [](const std::string& str) {
        std::string data = str;
        std::transform(data.begin(), data.end(), data.begin(),
                        [](unsigned char c){ return std::tolower(c); });
        return data;
    };

    std::string full_request_url = req.getRequestURL();

    // Strip the query string (?v=17) from the requested URL
    size_t query_pos = full_request_url.find('?');
    if (query_pos != std::string::npos) {
        full_request_url = full_request_url.substr(0, query_pos);
    }

    // Convert the cleaned URL and prefix to lowercase
    std::string publicf = to_lower(full_request_url);
    std::string prefix_lower = to_lower(_Config.config.getprefix());

    // Standardize prefix by removing trailing slash if present
    if (prefix_lower.length() > 0 && prefix_lower.back() == '/') {
        prefix_lower.pop_back();
    }

    // --- Compression Acceptance Check ---
    bool acpt_bz = false;
    libhttppp::HttpHeader::HeaderData *acph = req.getHeaderData("accept-encoding");

    // Check if client supports Brotli ("br")
    if (acph) {
        for(libhttppp::HttpHeader::HeaderData::Values *cdat = acph->getfirstValue();
            cdat; cdat = cdat->nextvalue()){
            if(to_lower(cdat->getvalue()).find("br") != std::string::npos){
                acpt_bz = true;
                break;
            }
        }
    }
    // ------------------------------------

    // Loop through all loaded public files and attempt an exact match
    for(auto curfile=_PublicFiles.begin(); curfile!=_PublicFiles.end(); curfile++){

        std::string curfile_path_lower = to_lower(curfile->Path);

        // Construct the full expected request path: /prefix/theme/public/style.css
        std::string expected_full_path = prefix_lower + curfile_path_lower;

        // Check if the request URL matches the expected full path exactly (case-insensitive)
        if (publicf.length() == expected_full_path.length() &&
            publicf.compare(0, publicf.length(), expected_full_path) == 0)
        {
            // If there is more data to send (streaming response)
            if(req.SendData.pos < curfile->Content.length() ){
                if( curfile->Compressed.length() > 0 && acpt_bz){
                    size_t len = BLOCKSIZE < curfile->Compressed.length()-req.SendData.pos ?
                                BLOCKSIZE : curfile->Compressed.length()-req.SendData.pos;
                    req.SendData.append(curfile->Compressed.substr(req.SendData.pos,len).c_str(),len);
                    req.SendData.pos+=len;
                }else{
                    size_t len = BLOCKSIZE < curfile->Content.length()-req.SendData.pos ?
                                BLOCKSIZE : curfile->Content.length()-req.SendData.pos;
                    req.SendData.append(curfile->Content.substr(req.SendData.pos,len).c_str(),len);
                    req.SendData.pos+=len;
                }
                return true;
            }
        }
    }
    return false;
}

void blogi::Theme::printSite(const int tid,libhtmlpp::HtmlString &output,libhtmlpp::HtmlElement *index,const std::string &crrurl,bool login,const char *meta,const char *title){
    try{

        libhtmlpp::HtmlElement *head,*footernav;
        head=index->getElementbyTag("head");

        // Update <title> tag: use plugin-provided title or fall back to SITE_TITLE setting
        if(head){
            libhtmlpp::HtmlElement *titleEl = head->getElementbyTag("title");
            if(titleEl){
                if(title && title[0] != '\0'){
                    titleEl->appendChild(libhtmlpp::TextElement(" - " + std::string(title)));
                }
            }
        }

        std::string hostpath;

        if(meta && head){
            // Add meta description
            libhtmlpp::HtmlElement des("meta");
            des.setAttribute("name","description");
            des.setAttribute("content",meta);
            head->appendChild(&des);

            // Add Open Graph image meta tag
            libhtmlpp::HtmlElement thumbnail("meta");
            thumbnail.setAttribute("property","og:image");
            thumbnail.setAttribute("content",std::string(_Config.config.getsiteurl()).append(_Config.config.buildurl("theme/public/thumbnail.webp")).c_str());
            head->appendChild(&thumbnail);

            // Add Open Graph URL meta tag
            libhtmlpp::HtmlElement baseurl("meta");
            baseurl.setAttribute("property","og:url");
            baseurl.setAttribute("content",std::string(_Config.config.getsiteurl()).append(crrurl).c_str());
            head->appendChild(&baseurl);
        }

        // Add footer navigation links
        footernav = index->getElementbyID("footernav");
        std::string _lang = getLang(*_Config.TDatabase[tid]);
        libhtmlpp::HtmlString footerancor;
        footerancor << "<div>";
        if(!login){
            footerancor << "<a class=\"footer\" href=\"" << _Config.config.buildurl("login") << "\">" << tr(_lang,"Login") << "</a>";
        }else{
            footerancor << "<a class=\"footer\" href=\"" << _Config.config.buildurl("logout") << "\">" << tr(_lang,"Logout") << "</a>"
            << "<a class=\"footer\" href=\"" << _Config.config.buildurl("editor") << "\">" << tr(_lang,"Editor") << "</a>"
            << "<a class=\"footer\" href=\"" << _Config.config.buildurl("settings") << "\">" << tr(_lang,"Settings") << "</a>";
        }
        footerancor << "</div>";

        if(footernav)
            footernav->insertChild(footerancor.parse());

        // Inject theme color CSS variables into <head>
        injectThemeColors(tid, index);

        // Print the final HTML structure to the output stream
        libhtmlpp::print(*index,output,_Formated);
    }catch(libhtmlpp::HTMLException &e){
        libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << e.what();
        throw excep;
    }
}

// Normalize a color value read from DB — fix bare hex (missing '#')
static void normalizeColor(std::string &val){
    if(val.empty()) return;
    // If it looks like 6 hex digits without '#', prepend it
    if(val.size() == 6 && val[0] != '#' &&
       val.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos){
        val.insert(0, "#");
    }
}

void blogi::Theme::injectThemeColors(const int tid, libhtmlpp::HtmlElement *index){
    libhtmlpp::HtmlElement *head = index->getElementbyTag("head");
    if(!head)
        return;

    struct ColorVar { const char *dbSuffix; const char *cssVar; const char *darkDefault; const char *lightDefault; };
    ColorVar vars[] = {
        {"BG",           "--blogi-bg",            "#232627",  "rgba(235,237,240,1)"},
        {"SURFACE",      "--blogi-surface",       "#31363b",  "rgba(245,246,248,1)"},
        {"SURFACE_A",    "--blogi-surface-a",     "rgba(49,54,59,0.7)", "rgba(245,246,248,0.88)"},
        {"TEXT",         "--blogi-text",           "#eeeeee",  "rgba(44,62,80,1)"},
        {"TEXT_MUTED",   "--blogi-text-muted",     "#aaaaaa",  "rgba(127,140,141,1)"},
        {"ACCENT",       "--blogi-accent",        "#3ad43a",  "rgba(39,148,94,1)"},
        {"ACCENT_DARK",  "--blogi-accent-dark",   "#2ea82e",  "rgba(30,122,75,1)"},
        {"BORDER",       "--blogi-border",        "#3a3f44",  "rgba(220,225,230,1)"},
        {"BG_ALT",       "--blogi-bg-alt",        "#2a2e32",  "rgba(232,236,240,1)"},
        {"ERROR",        "--blogi-error",         "#F36947",  "rgba(231,76,60,1)"},
        {"ERROR_BG",     "--blogi-error-bg",      "#3d1f1a",  "rgba(253,236,234,1)"},
        {"ERROR_TEXT",   "--blogi-error-text",    "#ff9a8a",  "rgba(192,57,43,1)"},
        {"SUCCESS_BG",   "--blogi-success-bg",    "#1a3d1f",  "rgba(234,250,241,1)"},
        {"SUCCESS_TEXT", "--blogi-success-text",   "#8aff9a",  "rgba(39,174,96,1)"},
        {"NAV_ACTIVE",   "--blogi-nav-active",    "#3daee9",  "rgba(41,128,185,1)"},
        {"NAV_HOVER",    "--blogi-nav-hover",     "#232629",  "rgba(232,236,240,1)"},
        {"NAV_BORDER",   "--blogi-nav-border",    "#222529",  "rgba(189,195,199,1)"},
    };

    // Build dark values
    std::string darkPrefix = "THEME_DARK_";
    libhtmlpp::HtmlString css;
    css << "<style>";

    // Dark scheme (default)
    css << ":root{";
    for(auto &v : vars){
        std::string val;
        try { getConfig(*_Config.TDatabase[tid], darkPrefix + v.dbSuffix, val); } catch(...) {}
        normalizeColor(val);
        if(val.empty()) val = v.darkDefault;
        css << v.cssVar << ":" << val.c_str() << ";";
    }
    css << "color-scheme:dark;}";

    // Light scheme — explicit data-theme="light"
    std::string lightPrefix = "THEME_LIGHT_";
    css << ":root[data-theme=\"light\"]{";
    for(auto &v : vars){
        std::string val;
        try { getConfig(*_Config.TDatabase[tid], lightPrefix + v.dbSuffix, val); } catch(...) {}
        normalizeColor(val);
        if(val.empty()) val = v.lightDefault;
        css << v.cssVar << ":" << val.c_str() << ";";
    }
    css << "color-scheme:light;}";

    // Light scheme — OS preference (auto mode)
    css << "@media(prefers-color-scheme:light){:root:not([data-theme=\"dark\"]){";
    for(auto &v : vars){
        std::string val;
        try { getConfig(*_Config.TDatabase[tid], lightPrefix + v.dbSuffix, val); } catch(...) {}
        normalizeColor(val);
        if(val.empty()) val = v.lightDefault;
        css << v.cssVar << ":" << val.c_str() << ";";
    }
    css << "color-scheme:light;}}";

    css << "</style>";

    head->appendChild(css.parse());
}

json_object* blogi::Theme::getThemeColors(const int tid){
    struct ColorVar { const char *dbSuffix; const char *cssVar; const char *darkDefault; const char *lightDefault; };
    ColorVar vars[] = {
        {"BG",           "--blogi-bg",            "#232627",  "rgba(235,237,240,1)"},
        {"SURFACE",      "--blogi-surface",       "#31363b",  "rgba(245,246,248,1)"},
        {"SURFACE_A",    "--blogi-surface-a",     "rgba(49,54,59,0.7)", "rgba(245,246,248,0.88)"},
        {"TEXT",         "--blogi-text",           "#eeeeee",  "rgba(44,62,80,1)"},
        {"TEXT_MUTED",   "--blogi-text-muted",     "#aaaaaa",  "rgba(127,140,141,1)"},
        {"ACCENT",       "--blogi-accent",        "#3ad43a",  "rgba(39,148,94,1)"},
        {"ACCENT_DARK",  "--blogi-accent-dark",   "#2ea82e",  "rgba(30,122,75,1)"},
        {"BORDER",       "--blogi-border",        "#3a3f44",  "rgba(220,225,230,1)"},
        {"BG_ALT",       "--blogi-bg-alt",        "#2a2e32",  "rgba(232,236,240,1)"},
        {"ERROR",        "--blogi-error",         "#F36947",  "rgba(231,76,60,1)"},
        {"ERROR_BG",     "--blogi-error-bg",      "#3d1f1a",  "rgba(253,236,234,1)"},
        {"ERROR_TEXT",   "--blogi-error-text",    "#ff9a8a",  "rgba(192,57,43,1)"},
        {"SUCCESS_BG",   "--blogi-success-bg",    "#1a3d1f",  "rgba(234,250,241,1)"},
        {"SUCCESS_TEXT", "--blogi-success-text",   "#8aff9a",  "rgba(39,174,96,1)"},
        {"NAV_ACTIVE",   "--blogi-nav-active",    "#3daee9",  "rgba(41,128,185,1)"},
        {"NAV_HOVER",    "--blogi-nav-hover",     "#232629",  "rgba(232,236,240,1)"},
        {"NAV_BORDER",   "--blogi-nav-border",    "#222529",  "rgba(189,195,199,1)"},
    };

    json_object *root = json_object_new_object();
    json_object *dark = json_object_new_object();
    json_object *light = json_object_new_object();

    for(auto &v : vars){
        std::string dval;
        try { getConfig(*_Config.TDatabase[tid], std::string("THEME_DARK_") + v.dbSuffix, dval); } catch(...) {}
        normalizeColor(dval);
        if(dval.empty()) dval = v.darkDefault;
        json_object_object_add(dark, v.cssVar, json_object_new_string(dval.c_str()));

        std::string lval;
        try { getConfig(*_Config.TDatabase[tid], std::string("THEME_LIGHT_") + v.dbSuffix, lval); } catch(...) {}
        normalizeColor(lval);
        if(lval.empty()) lval = v.lightDefault;
        json_object_object_add(light, v.cssVar, json_object_new_string(lval.c_str()));
    }

    json_object_object_add(root, "dark", dark);
    json_object_object_add(root, "light", light);
    return root;
}

json_object* blogi::Theme::SettingsTheme(const int tid, libhttppp::HttpRequest &req, const std::string &sessionid){
    bool saved = false;
    bool reset = false;

    // Handle form POST — save dark and light color fields
    libhttppp::HttpForm form;
    form.parse(req);

    const char *suffixes[] = {"BG","SURFACE","SURFACE_A","TEXT","TEXT_MUTED","ACCENT","ACCENT_DARK","BORDER","BG_ALT","ERROR","ERROR_BG","ERROR_TEXT","SUCCESS_BG","SUCCESS_TEXT","NAV_ACTIVE","NAV_HOVER","NAV_BORDER"};
    const char *profiles[] = {"dark", "light"};

    // Check for reset / logo URL / logo width / site title
    for(const auto &cur : form.urlData()){
        if(cur.key == "theme_reset" && cur.value == "1"){
            reset = true;
        }
        if(cur.key == "site_title"){
            setConfig(*_Config.TDatabase[tid], "SITE_TITLE", cur.value);
            saved = true;
        }
        if(cur.key == "logo_url"){
            setConfig(*_Config.TDatabase[tid], "THEME_LOGO_URL", cur.value);
            saved = true;
        }
        if(cur.key == "logo_width" && !cur.value.empty()){
            int w = std::atoi(cur.value.c_str());
            if (w > 0 && w <= 4096) {
                setConfig(*_Config.TDatabase[tid], "THEME_LOGO_WIDTH", std::to_string(w));
                // Register size in media preview allowed sizes table
                std::string sizesJson;
                try { getConfig(*_Config.TDatabase[tid], "MEDIA_ALLOWED_SIZES", sizesJson); } catch (...) {}
                json_object *arr = nullptr;
                if (!sizesJson.empty())
                    arr = json_tokener_parse(sizesJson.c_str());
                if (!arr || !json_object_is_type(arr, json_type_array)) {
                    if (arr) json_object_put(arr);
                    arr = json_object_new_array();
                }
                bool found = false;
                size_t alen = json_object_array_length(arr);
                for (size_t i = 0; i < alen; ++i) {
                    json_object *item = json_object_array_get_idx(arr, i);
                    json_object *jw = nullptr, *jh = nullptr;
                    if (json_object_object_get_ex(item, "w", &jw) &&
                        json_object_object_get_ex(item, "h", &jh)) {
                        if (json_object_get_int(jw) == w && json_object_get_int(jh) == 0) {
                            found = true;
                            break;
                        }
                    }
                }
                if (!found) {
                    json_object *item = json_object_new_object();
                    json_object_object_add(item, "w", json_object_new_int(w));
                    json_object_object_add(item, "h", json_object_new_int(0));
                    json_object_array_add(arr, item);
                    setConfig(*_Config.TDatabase[tid], "MEDIA_ALLOWED_SIZES",
                             json_object_to_json_string_ext(arr, JSON_C_TO_STRING_NOSLASHESCAPE));
                }
                json_object_put(arr);
                saved = true;
            }
        }
    }

    struct ColorDef { const char *suffix; const char *label; const char *darkDef; const char *lightDef; };
    ColorDef colors[] = {
        {"BG",           "Background",        "#232627",  "rgba(235,237,240,1)"},
        {"SURFACE",      "Surface",           "#31363b",  "rgba(245,246,248,1)"},
        {"SURFACE_A",    "Surface (alpha)",   "rgba(49,54,59,0.7)", "rgba(245,246,248,0.88)"},
        {"TEXT",         "Text",              "#eeeeee",  "rgba(44,62,80,1)"},
        {"TEXT_MUTED",   "Text Muted",        "#aaaaaa",  "rgba(127,140,141,1)"},
        {"ACCENT",       "Accent / Links",    "#3ad43a",  "rgba(39,148,94,1)"},
        {"ACCENT_DARK",  "Accent Dark",       "#2ea82e",  "rgba(30,122,75,1)"},
        {"BORDER",       "Border",            "#3a3f44",  "rgba(220,225,230,1)"},
        {"BG_ALT",       "Background Alt",    "#2a2e32",  "rgba(232,236,240,1)"},
        {"ERROR",        "Error",             "#F36947",  "rgba(231,76,60,1)"},
        {"ERROR_BG",     "Error Background",  "#3d1f1a",  "rgba(253,236,234,1)"},
        {"ERROR_TEXT",   "Error Text",        "#ff9a8a",  "rgba(192,57,43,1)"},
        {"SUCCESS_BG",   "Success Background","#1a3d1f",  "rgba(234,250,241,1)"},
        {"SUCCESS_TEXT", "Success Text",       "#8aff9a",  "rgba(39,174,96,1)"},
        {"NAV_ACTIVE",   "Nav Active",        "#3daee9",  "rgba(41,128,185,1)"},
        {"NAV_HOVER",    "Nav Hover",         "#232629",  "rgba(232,236,240,1)"},
        {"NAV_BORDER",   "Nav Border",        "#222529",  "rgba(189,195,199,1)"},
    };

    if(reset){
        // Batch reset: delete all theme color keys and re-insert defaults in one SQL
        dbpp::SQL rsql;
        dbpp::DBResult rres;
        std::vector<char> buf;
        rsql << "DELETE FROM options WHERE key LIKE 'THEME_DARK_%' OR key LIKE 'THEME_LIGHT_%';";
        for(auto &c : colors){
            for(int p = 0; p < 2; ++p){
                const char *prefix = p == 0 ? "THEME_DARK_" : "THEME_LIGHT_";
                const char *val    = p == 0 ? c.darkDef : c.lightDef;
                uuid::uuid id;
                id.generate();
                rsql << "INSERT INTO options (id,key,value) VALUES ('"
                     << id.c_str() << "','"
                     << dbpp::SQL::escaped(buf, (std::string(prefix) + c.suffix).c_str())
                     << "','"
                     << dbpp::SQL::escaped(buf, val) << "');";
            }
        }
        _Config.TDatabase[tid]->exec(rsql, rres);
    } else {
        for(auto &profile : profiles){
            std::string pfx = std::string("THEME_") + (std::string(profile) == "light" ? "LIGHT" : "DARK") + "_";
            for(auto &s : suffixes){
                // Form key: theme_dark_bg, theme_light_bg, etc.
                std::string formKey = std::string("theme_") + profile + "_" + s;
                std::transform(formKey.begin(), formKey.end(), formKey.begin(),
                    [](unsigned char c){ return std::tolower(c); });
                for(const auto &cur : form.urlData()){
                    if(cur.key == formKey){
                        setConfig(*_Config.TDatabase[tid], pfx + s, cur.value);
                        saved = true;
                    }
                }
            }
        }
    }

    // Build JSON form
    json_object *jroot = json_object_new_object();
    json_object_object_add(jroot, "title", json_object_new_string("Theme Settings"));

    if(saved){
        json_object_object_add(jroot, "message", json_object_new_string("Saved!"));
        json_object_object_add(jroot, "message_type", json_object_new_string("success"));
    }
    if(reset){
        json_object_object_add(jroot, "message", json_object_new_string("Reset to defaults!"));
        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(req.getRequestURL().c_str()));

    json_object *jtabs = json_object_new_array();

    // --- Tab 1: Logo ---
    {
        json_object *jtab = json_object_new_object();
        json_object_object_add(jtab, "label", json_object_new_string("Logo"));
        json_object *jfields = json_object_new_array();

        // Site Title
        {
            std::string curTitle;
            try { getConfig(*_Config.TDatabase[tid], "SITE_TITLE", curTitle); } catch (...) {}

            json_object *jf = json_object_new_object();
            json_object_object_add(jf, "name", json_object_new_string("site_title"));
            json_object_object_add(jf, "type", json_object_new_string("text"));
            json_object_object_add(jf, "label", json_object_new_string("Site Title &mdash; shown in browser tab, overridable by plugins"));
            json_object_object_add(jf, "value", json_object_new_string(curTitle.c_str()));
            json_object_array_add(jfields, jf);
        }

        // Logo heading with preview
        {
            std::string logoUrl = getLogoUrl(tid);
            std::string curUrl;
            try { getConfig(*_Config.TDatabase[tid], "THEME_LOGO_URL", curUrl); } catch (...) {}

            std::string logoLabel = "Logo";
            logoLabel += " &mdash; <img src=\"" + logoUrl + "\" style=\"max-height:60px;max-width:200px;vertical-align:middle;margin-left:8px;\" />";
            if (curUrl.empty())
                logoLabel += " <small>(theme default)</small>";

            json_object *jf = json_object_new_object();
            json_object_object_add(jf, "type", json_object_new_string("heading"));
            json_object_object_add(jf, "label", json_object_new_string(logoLabel.c_str()));
            json_object_array_add(jfields, jf);
        }
        // Logo URL
        {
            std::string curUrl;
            try { getConfig(*_Config.TDatabase[tid], "THEME_LOGO_URL", curUrl); } catch (...) {}

            json_object *jf = json_object_new_object();
            json_object_object_add(jf, "name", json_object_new_string("logo_url"));
            json_object_object_add(jf, "type", json_object_new_string("media_url"));
            json_object_object_add(jf, "label", json_object_new_string("Logo Image &mdash; leave empty for theme default"));
            json_object_object_add(jf, "value", json_object_new_string(curUrl.c_str()));
            json_object_array_add(jfields, jf);
        }
        // Logo width
        {
            std::string curWidth = "100";
            try { getConfig(*_Config.TDatabase[tid], "THEME_LOGO_WIDTH", curWidth); } catch (...) {}
            if (curWidth.empty()) curWidth = "100";

            json_object *jf = json_object_new_object();
            json_object_object_add(jf, "name", json_object_new_string("logo_width"));
            json_object_object_add(jf, "type", json_object_new_string("number"));
            json_object_object_add(jf, "label", json_object_new_string("Logo Width (px) &mdash; height auto-calculated"));
            json_object_object_add(jf, "value", json_object_new_string(curWidth.c_str()));
            json_object_array_add(jfields, jf);
        }

        json_object_object_add(jtab, "fields", jfields);
        json_object_array_add(jtabs, jtab);
    }

    // --- Tab 2: Dark Mode Colors ---
    {
        json_object *jtab = json_object_new_object();
        json_object_object_add(jtab, "label", json_object_new_string("Dark Colors"));
        json_object *jfields = json_object_new_array();

        for(auto &c : colors){
            std::string val;
            try { getConfig(*_Config.TDatabase[tid], std::string("THEME_DARK_") + c.suffix, val); } catch(...) {}
            normalizeColor(val);
            if(val.empty()) val = c.darkDef;

            json_object *jf = json_object_new_object();
            std::string name = "theme_dark_";
            name += c.suffix;
            std::transform(name.begin(), name.end(), name.begin(),
                [](unsigned char ch){ return std::tolower(ch); });
            json_object_object_add(jf, "name", json_object_new_string(name.c_str()));
            json_object_object_add(jf, "label", json_object_new_string(c.label));
            json_object_object_add(jf, "type", json_object_new_string("color"));
            json_object_object_add(jf, "value", json_object_new_string(val.c_str()));
            json_object_array_add(jfields, jf);
        }

        json_object_object_add(jtab, "fields", jfields);
        json_object_array_add(jtabs, jtab);
    }

    // --- Tab 3: Light Mode Colors ---
    {
        json_object *jtab = json_object_new_object();
        json_object_object_add(jtab, "label", json_object_new_string("Light Colors"));
        json_object *jfields = json_object_new_array();

        for(auto &c : colors){
            std::string val;
            try { getConfig(*_Config.TDatabase[tid], std::string("THEME_LIGHT_") + c.suffix, val); } catch(...) {}
            normalizeColor(val);
            if(val.empty()) val = c.lightDef;

            json_object *jf = json_object_new_object();
            std::string name = "theme_light_";
            name += c.suffix;
            std::transform(name.begin(), name.end(), name.begin(),
                [](unsigned char ch){ return std::tolower(ch); });
            json_object_object_add(jf, "name", json_object_new_string(name.c_str()));
            json_object_object_add(jf, "label", json_object_new_string(c.label));
            json_object_object_add(jf, "type", json_object_new_string("color"));
            json_object_object_add(jf, "value", json_object_new_string(val.c_str()));
            json_object_array_add(jfields, jf);
        }

        json_object_object_add(jtab, "fields", jfields);
        json_object_array_add(jtabs, jtab);
    }

    json_object_object_add(jform, "tabs", jtabs);
    json_object_object_add(jform, "submit", json_object_new_string("save"));
    json_object_object_add(jform, "reset_name", json_object_new_string("theme_reset"));
    json_object_object_add(jform, "reset_label", json_object_new_string("Reset to Defaults"));
    json_object_object_add(jroot, "form", jform);

    return jroot;
}
