/*******************************************************************************
 * Copyright (c) 2023, 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 <chrono>
#ifdef FreeBSD
#include <date/date.h>
#include <date/tz.h>
#endif
#include <iostream>
#include <algorithm>
#include <compare>
#include <random>

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

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

#include <dbpp/database.h>

#include <auth.h>
#include <conf.h>
#include <plugin.h>
#include <session.h>
#include <i18n.h>
#include <compress.h>
#include <mail.h>

#include <cctype>

#define SITELIMIT 10

#define GPO_CONTENT_EDIT_OWN      "c3eaf8d4-9d5f-6a7c-b123-4f6d8e0e2c34"
#define GPO_CONTENT_EDIT_OTHERS   "d4fba9e5-0e6d-7b8d-c234-5e7e9f1e3d45"
#define GPO_CONTENT_DELETE_OWN    "a1c8f6e2-7b3d-4e5a-9f01-2d4b6c8e0a12"
#define GPO_CONTENT_DELETE_OTHERS "b2d9e7f3-8c4e-5f6b-a012-3e5c7d9f1b23"
#define GPO_CONTENT_COMMENT       "e5a1b2c3-4d6e-7f8a-9b01-c2d3e4f5a6b7"
#define GPO_CONTENT_MODERATE      "f6b2c3d4-5e7f-8a9b-0c12-d3e4f5a6b7c8"

namespace blogi {
    class Content : public PluginApi{
    public:
        Content(){
        };

        ~Content(){
        };

        static std::string makeSlug(const char *title) {
            std::string slug;
            for (const char *p = title; *p; ++p) {
                unsigned char c = static_cast<unsigned char>(*p);
                if (std::isalnum(c))
                    slug += static_cast<char>(std::tolower(c));
                else if (c == ' ' || c == '\t')
                    slug += '_';
                // skip other characters
            }
            // collapse consecutive underscores
            std::string out;
            for (char c : slug) {
                if (c == '_' && !out.empty() && out.back() == '_')
                    continue;
                out += c;
            }
            // trim trailing underscores
            while (!out.empty() && out.back() == '_')
                out.pop_back();
            return out;
        }

        void GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std::chrono::year_month_day> &sitemap){
            dbpp::SQL sql;
            dbpp::DBResult res;
            sql="SELECT content.slug,content.created FROM content;";
            int ncount=Args->database[tid]->exec(sql,res);
            for(int i=0; i<ncount; ++i){
#ifndef FreeBSD
                std::chrono::year_month_day cdate;
                std::stringstream ss(res[i][1]);
                std::chrono::from_stream(ss, "%Y-%m-%d", cdate);
#else
		date::year_month_day cdate;
                std::stringstream ss(res[i][1]);
                date::from_stream(ss, "%Y-%m-%d", cdate);
#endif
                if (ss.fail()) {
                    std::cerr << "Parsing failed for date: " << res[i][1] << std::endl;
                    continue; // Skip this entry
                }

                sitemap.emplace(
                    Args->config->getsiteurl() + "/content/read/" + res[i][0],
                                cdate
                );
            }
        }

        void contentIdxPage(const int tid,libhttppp::HttpRequest &curreq,libhtmlpp::HtmlElement *page,const std::string &tag,int start,int end,const std::string &sessionid){            std::string _lang = getLang(*Args->database[tid]);            libhttppp::HTTPException excep;
            libhtmlpp::HtmlString condat;

            condat << "<div id=\"contentidx\">";

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

            int ncount=0;

            if (!tag.empty()) {
                sql << "SELECT id FROM tags where name='" << dbpp::SQL::escaped(sbuf,tag.c_str()) << "' LIMIT 1";
                if(Args->database[tid]->exec(sql,res)<1){
                    excep[libhttppp::HTTPException::Critical] << "no tag data found for this name!";
                    throw excep;
                }else {
                    sql = "SELECT content.id,content.title,content.descrition,users.displayname,content.created,content.slug FROM content ";
                    sql <<"LEFT JOIN users ON content.author=users.id LEFT JOIN tags_content ON tags_content.content_id=content.id where tags_content.tag_id='"
                    << res[0][0]
                    <<"' ORDER BY content.id DESC LIMIT '" << end << "' OFFSET " << start;
                    ncount=Args->database[tid]->exec(sql,res);
                }
            } else {
                sql="SELECT content.id,content.title,content.descrition,users.displayname,content.created,content.slug FROM content LEFT JOIN users ON content.author=users.id ORDER BY content.id DESC";
                sql << " LIMIT '" << end << "' OFFSET " << start;
                ncount=Args->database[tid]->exec(sql,res);
            }

            std::string meta;



            if (ncount<1) {
                condat << " </div>";
                libhtmlpp::HtmlString out;
                page->getElementbyID("main")->insertChild(condat.parse());
                Args->theme->printSite(tid,out,page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid),meta.c_str());

                libhttppp::HttpResponse resp;
                resp.setState(HTTP200);
                resp.setContentType("text/html");
                sendCompressed(curreq,resp,out.c_str(),out.length());

                return;
            }

            for (int i = 0; i < ncount; i++) {
                condat << "<div class=\"blog_entry\">"
                << "<span class=\"title\">" << res[i][1] << "</span>"
                << "<div class=\"blog_entry_idx_content\" ><div  class=\"entry_text\">" << res[i][2] << "</div>"
                << "<span class=\"readmore\"><a href=\""<< Args->config->buildurl("content/read/") << res[i][5] << "\">"+tr(_lang,"read more ...")+"</a></span>"
                << "<span class=\"author\">"+tr(_lang,"written by")+" " << res[i][3] << " "+tr(_lang,"on")+" "<< res[i][4]  <<" </span></div></div>";
                meta.append(res[i][1]);
                meta.append(" author: ");
                meta.append(res[i][3]);
                meta.append(" date: ");
                meta.append(res[i][4]);
                meta.append(" ");
            }

            bool showBack = (start - end) >= 0;
            bool showNext = (ncount - 10) >= 0;

            if (showBack || showNext) {
                condat << "<div id=\"pager\">";

                if(showBack)
                    condat << "<a href=\"" << curreq.getRequestURL() << "?start=" << (start- end) <<"\" > "+tr(_lang,"Back")+" </a>";

                if(showNext)
                    condat << "<a href=\"" << curreq.getRequestURL() << "?start=" << start+10 <<"\" > "+tr(_lang,"Next")+" </a>";

                condat << "</div>";
            }

            condat << "</div>";

            sql="SELECT name,id FROM tags";

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


            if (tcount>0) {
                condat << "<div id=\"idxtags\"><ul>";

                for (int i = 0; i < tcount; i++) {
                    sql="select tag_id FROM tags_content where tag_id = '";
                    sql << res[i][1] << "'";
                    std::string url;
                    libhttppp::HttpForm::urlEncode(res[i][0],url);
                    dbpp::DBResult rescnt;
                    condat << "<li><a href=\""
                    << Args->config->buildurl("content/tag/")+url << "\">" << res[i][0] << "(" << Args->database[tid]->exec(sql,rescnt) << ")" << "</a></li>";

                }

                condat << "</ul></div></div>";
            }

            libhtmlpp::HtmlString out;

            try{
                if(page->getElementbyID("main"))
                    page->getElementbyID("main")->insertChild(condat.parse());
            }catch(libhtmlpp::HTMLException &e){
                std::cerr << e.what() << std::endl;
                return;
            }

            libhtmlpp::HtmlElement canon("link");
            std::stringstream href;
            if(tag.empty()){
                href <<  Args->config->getsiteurl() << "/content/index" << "?start="<< start;
            }else{
                href << Args->config->getsiteurl() + "/content/tag/" + tag << "?start=" << start;
            }
            canon.setAttribute("rel","canonical");
            canon.setAttribute("href",href.str());
            if(page->getElementbyTag("head"))
                    page->getElementbyTag("head")->appendChild(canon);

            Args->theme->printSite(tid,out,page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid),meta.c_str());

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq,resp,out.c_str(),out.length());
        };

        void contentPage(const int tid,libhttppp::HttpRequest& curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid) {
            std::string _lang = getLang(*Args->database[tid]);
            libhttppp::HTTPException excep;

            // Extract slug from URL: /content/read/<slug>
            std::string prefix = Args->config->buildurl("content/read/");
            std::string slug;
            std::string requrl = curreq.getRequestURL();
            if (requrl.length() > prefix.length())
                slug = requrl.substr(prefix.length());
            // URL-decode the slug
            std::string decodedSlug;
            libhttppp::HttpForm::urlDecode(slug, decodedSlug);

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

            sql << "select content.id,content.title,content.text,users.displayname,content.created,content.author,content.slug from content LEFT JOIN users ON content.author=users.id WHERE content.slug='" << dbpp::SQL::escaped(sbuf, decodedSlug.c_str()) <<"' LIMIT 1";

            dbpp::DBResult res;

            if(Args->database[tid]->exec(sql,res)<1){
                // Fallback: try as numeric ID for old links
                int cid = 0;
                try { cid = std::stoi(decodedSlug); } catch (...) {}
                if (cid > 0) {
                    sql.clear();
                    sql << "select content.id,content.title,content.text,users.displayname,content.created,content.author,content.slug from content LEFT JOIN users ON content.author=users.id WHERE content.id='" << cid <<"' LIMIT 1";
                    if(Args->database[tid]->exec(sql,res) >= 1 && res[0][6] && res[0][6][0] != '\0') {
                        // Redirect old numeric URL to slug URL
                        libhttppp::HttpResponse resp;
                        resp.setState(HTTP301);
                        resp.setContentType("text/html");
                        std::string loc = Args->config->getsiteurl() + "/" + Args->config->getprefix() + "/content/read/" + res[0][6];
                        resp.setHeaderData("location")->push_back(loc);
                        resp.send(curreq,"",0);
                        return;
                    }
                }
                excep[libhttppp::HTTPException::Critical] << "No entry found for: " << decodedSlug;
                throw excep;
            }

            int cid = atoi(res[0][0]);
            std::string contentSlug = res[0][6] ? res[0][6] : "";
            std::string contentTitle = res[0][1] ? res[0][1] : "";

            // Admin menu with GPO-based edit/delete visibility
            if (Args->auth->isLoggedIn(tid,curreq,sessionid)) {
                bool canEdit = false;
                bool canDelete = false;
                try {
                    std::string authid;
                    Session sess(*Args->database[tid]);
                    sess.getSessionData(sessionid,"authid",authid);
                    if(!authid.empty()){
                        std::string uid;
                        sess.getSessionData(sessionid,"uid",uid);
                        dbpp::SQL asql;
                        dbpp::DBResult ares;
                        asql << "SELECT id FROM users WHERE sid='" << uid.c_str() << "' LIMIT 1;";
                        int currentAuthor = -1;
                        if(Args->database[tid]->exec(asql,ares) >= 1)
                            currentAuthor = atoi(ares[0][0]);

                        int postAuthor = res[0][5] ? atoi(res[0][5]) : -2;

                        for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                            const AuthSource &src = Args->config->getAuthSource(si);
                            try {
                                authdb::client::ClientConnection authcon;
                                authcon.setUrl(src.url);
                                authcon.setClientName(src.clientName);
                                authcon.setClientSecret(src.clientSecret);
                                authcon.setSessionID(uuid::uuid(authid.c_str()));
                                authdb::client::Client clt(authcon);
                                if(clt.ClientAuth()){
                                    bool isOwn = (currentAuthor == postAuthor);
                                    if(!canEdit){
                                        if(isOwn && clt.GPOcheck(uuid::uuid(GPO_CONTENT_EDIT_OWN)))
                                            canEdit = true;
                                        else if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_EDIT_OTHERS)))
                                            canEdit = true;
                                    }
                                    if(!canDelete){
                                        if(isOwn && clt.GPOcheck(uuid::uuid(GPO_CONTENT_DELETE_OWN)))
                                            canDelete = true;
                                        else if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_DELETE_OTHERS)))
                                            canDelete = true;
                                    }
                                }
                            
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                            if(canEdit && canDelete) break;
                        }
                    }
                } catch(...) {}

                if(canEdit || canDelete){
                    libhtmlpp::HtmlString admindat;
                    admindat << "<div id=\"content\"><div class=\"blog_adminmenu\">"
                    << "<ul>";
                    if(canEdit){
                        admindat << "<li><a href=\"" << Args->config->buildurl("content/edit/") << cid << "\">"+tr(_lang,"Edit")+"</a></li>";
                    }
                    if(canDelete){
                        admindat << "<li><a href=\"" << Args->config->buildurl("content/del/") << cid << "\">"+tr(_lang,"Delete")+"</a></li>";
                    }
                    admindat << "</ul>"
                    << "</div></div>";
                    libhtmlpp::HtmlElement *main = page.getElementbyID("main");
                    if (main) {
                        try { main->insertChild(admindat.parse()); } catch (...) {}
                    }
                }
            }

            // Use RenderPublished for the article wrapper
            json_object *params = json_object_new_object();
            json_object_object_add(params, "title", json_object_new_string(res[0][1] ? res[0][1] : ""));
            json_object_object_add(params, "author", json_object_new_string(res[0][3] ? res[0][3] : ""));
            json_object_object_add(params, "date", json_object_new_string(res[0][4] ? res[0][4] : ""));
            std::string text = res[0][2] ? res[0][2] : "";
            RenderPublished(tid, text, params, page);
            json_object_put(params);

            // --- Comments Section ---
            {
                libhtmlpp::HtmlString cmtdat;
                cmtdat << "<div class=\"blog_comments\">"
                       << "<h3>" << tr(_lang,"Comments") << "</h3>";

                // Show approved comments
                dbpp::SQL csql; dbpp::DBResult cres;
                csql << "SELECT id,author_name,text,created FROM comments WHERE content_id="
                     << cid << " AND approved=true ORDER BY created ASC;";
                int ccount = Args->database[tid]->exec(csql, cres);
                if(ccount > 0){
                    for(int ci = 0; ci < ccount; ++ci){
                        std::vector<char> esbuf;
                        cmtdat << "<div class=\"blog_comment\">"
                               << "<span class=\"comment_author\">"
                               << dbpp::SQL::escaped(esbuf, cres[ci][1] ? cres[ci][1] : "?") << "</span>"
                               << " <span class=\"comment_date\">" << (cres[ci][3] ? cres[ci][3] : "") << "</span>"
                               << "<div class=\"comment_text\">" << (cres[ci][2] ? cres[ci][2] : "") << "</div>"
                               << "</div>";
                    }
                } else {
                    cmtdat << "<p class=\"no_comments\">" << tr(_lang,"No comments yet.") << "</p>";
                }

                // Comment form
                bool loggedIn = Args->auth->isLoggedIn(tid, curreq, sessionid);
                bool canComment = false;
                std::string displayName;

                if(loggedIn){
                    try {
                        std::string authid;
                        Session sess(*Args->database[tid]);
                        sess.getSessionData(sessionid, "authid", authid);
                        if(!authid.empty()){
                            for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                                const AuthSource &src = Args->config->getAuthSource(si);
                                try {
                                    authdb::client::ClientConnection authcon;
                                    authcon.setUrl(src.url);
                                    authcon.setClientName(src.clientName);
                                    authcon.setClientSecret(src.clientSecret);
                                    authcon.setSessionID(uuid::uuid(authid.c_str()));
                                    authdb::client::Client clt(authcon);
                                    if(clt.ClientAuth()){
                                        // Get display name for all logged-in users
                                        authdb::SessionData sdat;
                                        clt.SessionInfo(sdat);
                                        uuid::uuid uid;
                                        sdat.getUid(uid);
                                        class authdb::UserData ud(uid);
                                        clt.UserInfo(ud);
                                        displayName = ud.getFirstname() + " " + ud.getLastname();
                                        // GPO controls auto-approve
                                        if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_COMMENT)))
                                            canComment = true;
                                    }
                                
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                                if(!displayName.empty()) break;
                            }
                        }
                    } catch(...) {}
                }

                cmtdat << "<div class=\"comment_form_wrap\">"
                       << "<h4>" << tr(_lang,"Leave a comment") << "</h4>"
                       << "<form method=\"POST\" action=\"" << Args->config->buildurl("content/comment/") << cid << "\">";

                if(loggedIn && !displayName.empty()){
                    // Logged in: name from session, no email needed
                    cmtdat << "<input type=\"hidden\" name=\"comment_author\" value=\"" << displayName << "\" />"
                           << "<p>" << tr(_lang,"Commenting as") << " <strong>" << displayName << "</strong></p>";
                } else {
                    // Guest: require name + email, needs moderation
                    cmtdat << "<div class=\"comment_field\">"
                           << "<label>" << tr(_lang,"Name") << "</label>"
                           << "<input type=\"text\" name=\"comment_author\" required />"
                           << "</div>"
                           << "<div class=\"comment_field\">"
                           << "<label>" << tr(_lang,"Email") << "</label>"
                           << "<input type=\"email\" name=\"comment_email\" required />"
                           << "</div>";
                }

                cmtdat << "<div class=\"comment_field\">"
                       << "<label>" << tr(_lang,"Comment") << "</label>"
                       << "<textarea name=\"comment_text\" required></textarea>"
                       << "</div>"
                       << "<button type=\"submit\">" << tr(_lang,"Submit") << "</button>"
                       << "</form></div></div>";

                libhtmlpp::HtmlElement *mainCmt = page.getElementbyID("main");
                if(mainCmt){
                    try { mainCmt->appendChild(cmtdat.parse()); } catch(...) {}
                }
            }

            // Back link
            libhtmlpp::HtmlString backdat;
            backdat << "<a class=\"back_content\" href=\"#\" onclick=\"history.back()\">"+tr(_lang,"Back")+"</a>";
            libhtmlpp::HtmlElement *main2 = page.getElementbyID("main");
            if (main2) {
                try { main2->appendChild(backdat.parse()); } catch (...) {}
            }

            libhtmlpp::HtmlElement canon("link");
            std::stringstream href;
            href << Args->config->getsiteurl() << "/content/read/" << contentSlug;
            canon.setAttribute("rel","canonical");
            canon.setAttribute("href",href.str());
            if(page.getElementbyTag("head"))
                    page.getElementbyTag("head")->appendChild(canon);

            libhtmlpp::HtmlString out;
            Args->theme->printSite(tid,out,&page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid),nullptr,contentTitle.c_str());

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq,resp,out.c_str(),out.length());
        };

        void commentPage(const int tid,libhttppp::HttpRequest& curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid) {
            std::string _lang = getLang(*Args->database[tid]);
            std::vector<char> sbuf;

            int cid = -1;
            sscanf(curreq.getRequestURL().c_str(), Args->config->buildurl("content/comment/%d").c_str(), &cid);

            if(cid <= 0){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Invalid post ID for comment!";
                throw exp;
            }

            // Verify post exists and get slug
            dbpp::SQL vsql; dbpp::DBResult vres;
            vsql << "SELECT id,slug FROM content WHERE id=" << cid << " LIMIT 1;";
            if(Args->database[tid]->exec(vsql, vres) < 1){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Post not found!";
                throw exp;
            }

            // Parse form
            std::string authorName, email, commentText;
            libhttppp::HttpForm form;
            form.parse(curreq);
            if(!form.getBoundary().empty()){
                for(const auto &curformdat : form.multipartData()){
                    std::string field;
                    for(const auto &curctdisp : curformdat.dispositions){
                        if(curctdisp.key=="name"){ field = curctdisp.value; break; }
                    }
                    if(field == "comment_author") authorName.append(curformdat.value.data(), curformdat.value.size());
                    else if(field == "comment_email") email.append(curformdat.value.data(), curformdat.value.size());
                    else if(field == "comment_text") commentText.append(curformdat.value.data(), curformdat.value.size());
                }
            } else {
                for(const auto &cur : form.urlData()){
                    if(cur.key == "comment_author") authorName = cur.value;
                    else if(cur.key == "comment_email") email = cur.value;
                    else if(cur.key == "comment_text") commentText = cur.value;
                }
            }

            if(commentText.empty()){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << tr(_lang,"Comment text is required!");
                throw exp;
            }

            // Determine if logged-in user → pull name/email from session
            bool autoApprove = false;
            int userId = 0;
            bool loggedIn = Args->auth->isLoggedIn(tid, curreq, sessionid);
            if(loggedIn){
                try {
                    std::string authid;
                    Session sess(*Args->database[tid]);
                    sess.getSessionData(sessionid, "authid", authid);
                    std::string uid;
                    sess.getSessionData(sessionid, "uid", uid);
                    if(!uid.empty()){
                        dbpp::SQL asql; dbpp::DBResult ares;
                        asql << "SELECT id FROM users WHERE sid='" << uid.c_str() << "' LIMIT 1;";
                        if(Args->database[tid]->exec(asql, ares) >= 1)
                            userId = atoi(ares[0][0]);
                    }
                    if(!authid.empty()){
                        for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                            const AuthSource &src = Args->config->getAuthSource(si);
                            try {
                                authdb::client::ClientConnection authcon;
                                authcon.setUrl(src.url);
                                authcon.setClientName(src.clientName);
                                authcon.setClientSecret(src.clientSecret);
                                authcon.setSessionID(uuid::uuid(authid.c_str()));
                                authdb::client::Client clt(authcon);
                                if(clt.ClientAuth()){
                                    // Pull name + email from authdb
                                    authdb::SessionData sdat;
                                    clt.SessionInfo(sdat);
                                    uuid::uuid auid;
                                    sdat.getUid(auid);
                                    class authdb::UserData ud(auid);
                                    clt.UserInfo(ud);
                                    authorName = ud.getFirstname() + " " + ud.getLastname();
                                    email = ud.getMail();
                                    // GPO controls auto-approve
                                    if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_COMMENT))
                                       || clt.GPOcheck(uuid::uuid(GPO_CONTENT_MODERATE)))
                                        autoApprove = true;
                                }
                            
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                            if(!authorName.empty()) break;
                        }
                    }
                } catch(...) {}
            }

            if(!loggedIn && (authorName.empty() || email.empty())){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << tr(_lang,"Name and email are required for guest comments!");
                throw exp;
            }

            // Generate verify token for guest comments
            std::string verifyToken;
            if(!autoApprove && !loggedIn && !email.empty()){
                static const char alphanum[] = "0123456789abcdef";
                std::random_device rd;
                std::mt19937 gen(rd());
                std::uniform_int_distribution<> dis(0, sizeof(alphanum) - 2);
                verifyToken.reserve(32);
                for(int ti = 0; ti < 32; ++ti)
                    verifyToken += alphanum[dis(gen)];
            }

            // Insert comment
            dbpp::SQL isql; dbpp::DBResult ires;
            isql << "INSERT INTO comments (content_id,user_id,author_name,email,text,approved,email_verified,verify_token) VALUES ("
                 << cid << ",";
            if(userId > 0)
                isql << userId;
            else
                isql << "NULL";
            isql << ",'"
                 << dbpp::SQL::escaped(sbuf, authorName.c_str()) << "','"
                 << dbpp::SQL::escaped(sbuf, email.c_str()) << "','"
                 << dbpp::SQL::escaped(sbuf, commentText.c_str()) << "',";
            if(autoApprove)
                isql << "true";
            else
                isql << "false";
            isql << ",";
            if(autoApprove || loggedIn)
                isql << "true";
            else
                isql << "false";
            isql << ",";
            if(!verifyToken.empty())
                isql << "'" << verifyToken.c_str() << "'";
            else
                isql << "NULL";
            isql << ");";
            Args->database[tid]->exec(isql, ires);

            // Send verification email for guest comments
            if(!verifyToken.empty() && Args->smtp){
                try {
                    std::string vurl = Args->config->getsiteurl() + Args->config->buildurl("content/comment/verify") + "?token=" + verifyToken;
                    std::string subject = "Verify your comment";
                    std::string body = "Please verify your comment by clicking this link:\n\n" + vurl + "\n";
                    sendMail(Args->smtp.get(), email.c_str(), subject.c_str(), body);
                } catch(...) {}
            }

            // Show confirmation and redirect back
            libhtmlpp::HtmlString condat;
            if(autoApprove){
                condat << "<div id=\"content\"><p class=\"comment_success\">"
                       << tr(_lang,"Comment posted!") << "</p></div>";
            } else if(!verifyToken.empty()) {
                condat << "<div id=\"content\"><p class=\"comment_pending\">"
                       << tr(_lang,"A verification email has been sent. Please check your inbox.") << "</p></div>";
            } else {
                condat << "<div id=\"content\"><p class=\"comment_pending\">"
                       << tr(_lang,"Comment submitted. It will appear after moderation.") << "</p></div>";
            }
            condat << "<a class=\"back_content\" href=\"" << Args->config->buildurl("content/read/") << vres[0][1] << "\">"
                   << tr(_lang,"Back to post") << "</a>";

            libhtmlpp::HtmlElement *main = page.getElementbyID("main");
            if(main){
                try { main->insertChild(condat.parse()); } catch(...) {}
            }

            libhtmlpp::HtmlString out;
            Args->theme->printSite(tid, out, &page, curreq.getRequestURL(), loggedIn);

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq, resp, out.c_str(), out.length());
        }

        void verifyCommentPage(const int tid,libhttppp::HttpRequest& curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid) {
            std::string _lang = getLang(*Args->database[tid]);
            std::vector<char> sbuf;

            std::string token;
            libhttppp::HttpForm form;
            form.parse(curreq);
            for(const auto &cur : form.urlData()){
                if(cur.key == "token") { token = cur.value; break; }
            }

            libhtmlpp::HtmlString condat;

            if(token.empty() || token.length() != 32){
                condat << "<div id=\"content\"><p class=\"comment_error\">"
                       << tr(_lang,"Invalid verification link.") << "</p></div>";
            } else {
                dbpp::SQL vsql; dbpp::DBResult vres;
                vsql << "SELECT comments.id,comments.content_id,content.slug FROM comments JOIN content ON content.id=comments.content_id WHERE comments.verify_token='" << dbpp::SQL::escaped(sbuf, token.c_str()) << "' AND comments.email_verified=false LIMIT 1;";
                int vcount = Args->database[tid]->exec(vsql, vres);
                if(vcount > 0){
                    int cmtId = atoi(vres[0][0]);
                    std::string contentSlug = vres[0][2];
                    dbpp::SQL usql; dbpp::DBResult ures;
                    usql << "UPDATE comments SET email_verified=true, verify_token=NULL WHERE id=" << cmtId << ";";
                    Args->database[tid]->exec(usql, ures);
                    condat << "<div id=\"content\"><p class=\"comment_success\">"
                           << tr(_lang,"Email verified! Your comment will be published after moderation.") << "</p></div>"
                           << "<a class=\"back_content\" href=\"" << Args->config->buildurl("content/read/") << contentSlug << "\">" << tr(_lang,"Back to post") << "</a>";
                } else {
                    condat << "<div id=\"content\"><p class=\"comment_error\">"
                           << tr(_lang,"Verification link is invalid or has already been used.") << "</p></div>";
                }
            }

            libhtmlpp::HtmlElement *main = page.getElementbyID("main");
            if(main){
                try { main->insertChild(condat.parse()); } catch(...) {}
            }

            libhtmlpp::HtmlString out;
            Args->theme->printSite(tid, out, &page, curreq.getRequestURL(), false);

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq, resp, out.c_str(), out.length());
        }

        void addPostPage(const int tid,libhttppp::HttpRequest &curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid){
            std::string _lang = getLang(*Args->database[tid]);
            char url[512];
            std::vector<char> sbuf;

            if(!Args->auth->isLoggedIn(tid,curreq,sessionid)){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Please login before!";
                throw exp;
            }


            libhttppp::HTTPException excep;

            libhttppp::HttpForm curform;
            curform.parse(curreq);

            std::vector<char>  title;
            std::vector<char>  text;
            std::vector<char>  tags;
            std::vector<char> description;

            if(!curform.getBoundary().empty()){
                for(const auto &curformdat : curform.multipartData()){
                    std::string field;
                    for(const auto &curctdisp : curformdat.dispositions){
                        if(curctdisp.key=="name"){
                            field = curctdisp.value;
                            break;
                        }
                    }
                    if (field == "blog_title") {
                        std::copy(curformdat.value.begin(),curformdat.value.end(),std::back_inserter(title));
                    }else if(field == "blog_description"){
                        std::copy(curformdat.value.begin(),curformdat.value.end(),std::back_inserter(description));
                    }else if (field == "blog_tags") {
                        std::copy(curformdat.value.begin(),curformdat.value.end(),std::back_inserter(tags));
                    }else if(field == "blog_content"){
                        std::copy(curformdat.value.begin(),curformdat.value.end(),std::back_inserter(text));
                    }
                }

                int content_id = -1;

                if(text.empty() || title.empty() || description.empty()){
                    excep[libhttppp::HTTPException::Critical] << "not all fields filled !";
                    throw excep;
                }

                dbpp::SQL asql,sql;
                dbpp::DBResult ares,res;

                std::string uid;
                Session sess(*Args->database[tid]);
                sess.getSessionData(sessionid,"uid",uid);

                asql << "SELECT id from users WHERE sid='"<< uid.c_str() <<"' LIMIT 1;";

                if (Args->database[tid]->exec(asql,ares) < 1) {
                    excep[libhttppp::HTTPException::Error] << "User with SID not found!";
                    throw excep;
                }

                int author=atoi(ares[0][0]);

                std::string time_str;

                try {
                    auto now = std::chrono::system_clock::now();
#ifdef FreeBSD
                    auto local_now = date::zoned_time{date::current_zone(), now};
#else
                    auto local_now = std::chrono::zoned_time{std::chrono::current_zone(), now};
#endif
                    try {
                        time_str = std::format("{:%a %b %d %H:%M:%S %Y %z}", now);
                    } catch (...) {
                        throw excep[libhttppp::HTTPException::Error] << "Cannot get Time (std::format/put_time failed)!";
                    }

                    if (time_str.empty()) {
                        throw excep[libhttppp::HTTPException::Error] << "Cannot get Time (Time structure failure)!";
                    }
                } catch (const libhttppp::HTTPException& excep) {
                    throw excep;
                }
                std::string slug = makeSlug(title.data());
                sql << "INSERT INTO content (title,slug,descrition,text,author,created) VALUES ('" <<
                dbpp::SQL::escaped(sbuf,title.data()) << "','" <<
                dbpp::SQL::escaped(sbuf,slug.c_str()) << "','" <<
                dbpp::SQL::escaped(sbuf,description.data()) <<"','" <<
                dbpp::SQL::escaped(sbuf,text.data()) <<"','"<< author <<"','" << time_str.c_str() << "') RETURNING id;";

                try {
                    Args->database[tid]->exec(sql,res);
                    content_id=atoi(res[0][0]);
                }catch(libhttppp::HTTPException &e){
                    throw e;
                }catch(const char *e){
                    libhttppp::HTTPException re;
                    re[libhttppp::HTTPException::Error] << e;
                    throw re;
                }


                size_t tgsstart=0;
                if (!tags.empty() && content_id != -1) {
                    for (size_t i = 0; i <= tags.size(); ++i) {
                        if (tags[i] == ' ' || i == tags.size()) {
                            if((i - tgsstart) >0 ){
                                std::string tag;
                                std::copy(tags.begin()+tgsstart,tags.begin()+(i-tgsstart),std::back_inserter(tag));
                                tgsstart = i+1;
                                int tries = 0;
                                TAGNAMECHECK:
                                dbpp::SQL sql;
                                dbpp::DBResult res;
                                if (tries>5) {
                                    excep[libhttppp::HTTPException::Critical] << "tag doesn't exists and could not created!";
                                    throw excep;
                                }

                                sql = "select id,name from tags where name='";
                                sql << dbpp::SQL::escaped(sbuf,tag.c_str()) << "' LIMIT 1;";

                                if (Args->database[tid]->exec(sql,res) != 1) {
                                    sql = "insert into tags (name) VALUES ('";
                                    sql << dbpp::SQL::escaped(sbuf,tag.c_str()) <<"');";
                                    Args->database[tid]->exec(sql,res);
                                    ++tries;
                                    goto TAGNAMECHECK;
                                }

                                sql =  "insert into tags_content (content_id,tag_id) VALUES ('"; sql << content_id <<"','" << res[0][0] <<"');";
                                Args->database[tid]->exec(sql,res);
                            }
                        }
                    }
                }

                std::stringstream ccurl;
                ccurl << Args->config->getprefix() << "/content/read/" << slug;

                libhttppp::HttpResponse resp;
                resp.setState(HTTP307);
                resp.setHeaderData("location")->push_back(ccurl.str());
                resp.setContentType("text/html");
                resp.send(curreq,"",0);
                return;
            }

            libhtmlpp::HtmlString condat;
            condat << "<div id=\"content\">"
            << "<form action=\""<< Args->config->buildurl("content/addpost") << "\" method=\"post\" enctype=\"multipart/form-data\" >"
            << "<span>"+tr(_lang,"Title:")+"</span><input maxlength=\"255\" name=\"blog_title\" type=\"text\" style=\"width: 100%;\" /><br/>"
            << "<span>"+tr(_lang,"Short description:")+"</span><input maxlength=\"255\" name=\"blog_description\" type=\"text\"  style=\"width: 100%;\" /><br/>"
            << "<span>"+tr(_lang,"Tags:")+"</span><input name=\"blog_tags\" type=\"text\"  style=\"width: 100%;\" /><br/>"
            << "<span>"+tr(_lang,"Text:")+"</span><textarea name=\"blog_content\" type=\"text\" style=\"width: 100%; height:480px;\"> </textarea>"
            << "<input type=\"submit\" value=\""+tr(_lang,"Post blog entry")+"\"/>"
            << "</form>"
            << "</div>";

            libhtmlpp::HtmlString out;

            page.getElementbyID("main")->insertChild(condat.parse());

            Args->theme->printSite(tid,out,&page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid));

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq,resp,out.c_str(),out.length());
        }

        void delPostPage(const int tid,libhttppp::HttpRequest& curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid) {
            std::string _lang = getLang(*Args->database[tid]);
            std::string sid;
            char url[512];

            if(!Args->auth->isLoggedIn(tid,curreq,sessionid)){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Please login before!";
                throw exp;
            }

            libhttppp::HTTPException excep;

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

            int cid = -1;

            sscanf(curreq.getRequestURL().c_str(), Args->config->buildurl("content/del/%d").c_str(), &cid);

            // Get the post's author
            dbpp::SQL psql;
            dbpp::DBResult pres;
            psql << "SELECT author FROM content WHERE id='" << cid << "' LIMIT 1;";
            if(Args->database[tid]->exec(psql,pres) < 1){
                excep[libhttppp::HTTPException::Error] << "Post not found!";
                throw excep;
            }
            int postAuthor = atoi(pres[0][0]);

            // Get current user's author id
            std::string uid;
            Session sess(*Args->database[tid]);
            sess.getSessionData(sessionid,"uid",uid);

            dbpp::SQL asql;
            dbpp::DBResult ares;
            asql << "SELECT id FROM users WHERE sid='" << uid.c_str() << "' LIMIT 1;";
            if(Args->database[tid]->exec(asql,ares) < 1){
                excep[libhttppp::HTTPException::Error] << "User not found!";
                throw excep;
            }
            int currentAuthor = atoi(ares[0][0]);

            // Check GPO permissions via authdb
            bool allowed = false;
            std::string authid;
            sess.getSessionData(sessionid,"authid",authid);
            if(!authid.empty()){
                for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                    const AuthSource &src = Args->config->getAuthSource(si);
                    try {
                        authdb::client::ClientConnection authcon;
                        authcon.setUrl(src.url);
                        authcon.setClientName(src.clientName);
                        authcon.setClientSecret(src.clientSecret);
                        authcon.setSessionID(uuid::uuid(authid.c_str()));
                        authdb::client::Client clt(authcon);
                        if(clt.ClientAuth()){
                            if(currentAuthor == postAuthor && clt.GPOcheck(uuid::uuid(GPO_CONTENT_DELETE_OWN))){
                                allowed = true;
                            } else if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_DELETE_OTHERS))){
                                allowed = true;
                            }
                        }
                    
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                    if(allowed) break;
                }
            }

            if(!allowed){
                excep[libhttppp::HTTPException::Error] << "Permission denied: insufficient rights to delete this post!";
                throw excep;
            }

            sql ="DELETE FROM tags_content WHERE content_id='";
            sql << cid << "';";
            try { Args->database[tid]->exec(sql,res); } catch(...) {}

            sql.clear();
            sql << "DELETE FROM content WHERE id='" << cid << "';";
            if(Args->database[tid]->exec(sql,res)<0){
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Can'T content sql data!";
                throw exp;
            }

            cleartags(tid);

            libhtmlpp::HtmlString condat;

            condat << "<span>"+tr(_lang,"Post deleted")+"</span>";

            libhtmlpp::HtmlString out;

            try{
                page.getElementbyID("main")->insertChild(condat.parse());
            }catch(libhtmlpp::HTMLException &e){
                excep[libhttppp::HTTPException::Error] << e.what();
                throw excep;
            }

            Args->theme->printSite(tid,out,&page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid));

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq,resp,out.c_str(),out.length());

        }

        void cleartags(const int tid) {

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

            sql="SELECT name,id FROM tags;";

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

            libhttppp::HTTPException excep;


            for (int i = 0; i < n; i++) {

                sql="SELECT COUNT(*) FROM tags_content WHERE tag_id='"; sql << res[i][1] << "';";

                dbpp::DBResult res2;

                if(Args->database[tid]->exec(sql,res2)<0){
                    libhttppp::HTTPException excep;
                    excep[libhttppp::HTTPException::Critical] << "clear tags count failed this shouldn't happend!";
                    throw excep;
                }

                if( atoi(res2[0][0])>0)
                    continue;

                sql = "DELETE FROM tags WHERE id='"; sql << res[i][1] << "';";

                if(Args->database[tid]->exec(sql,res2)<0){
                    libhttppp::HTTPException excep;
                    excep[libhttppp::HTTPException::Critical] << "clear tags deletd failed this shouldn't happend!";
                    throw excep;
                }

            }
        }

        void editPostPage(const int tid,libhttppp::HttpRequest& curreq,libhtmlpp::HtmlElement &page,const std::string &sessionid) {
            std::string _lang = getLang(*Args->database[tid]);
            char url[512];
            if (!Args->auth->isLoggedIn(tid,curreq,sessionid)) {
                libhttppp::HTTPException exp;
                exp[libhttppp::HTTPException::Error] << "Please login before!";
                throw exp;
            }

            libhttppp::HTTPException excep;

            int cid = -1;

            sscanf(curreq.getRequestURL().c_str(), Args->config->buildurl("content/edit/%d").c_str(), &cid);

            // GPO permission check for editing
            {
                dbpp::SQL psql;
                dbpp::DBResult pres;
                psql << "SELECT author FROM content WHERE id='" << cid << "' LIMIT 1;";
                if(Args->database[tid]->exec(psql,pres) < 1){
                    excep[libhttppp::HTTPException::Error] << "Post not found!";
                    throw excep;
                }
                int postAuthor = atoi(pres[0][0]);

                std::string uid;
                Session sess(*Args->database[tid]);
                sess.getSessionData(sessionid,"uid",uid);

                dbpp::SQL asql;
                dbpp::DBResult ares;
                asql << "SELECT id FROM users WHERE sid='" << uid.c_str() << "' LIMIT 1;";
                if(Args->database[tid]->exec(asql,ares) < 1){
                    excep[libhttppp::HTTPException::Error] << "User not found!";
                    throw excep;
                }
                int currentAuthor = atoi(ares[0][0]);

                bool allowed = false;
                std::string authid;
                sess.getSessionData(sessionid,"authid",authid);
                if(!authid.empty()){
                    for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                        const AuthSource &src = Args->config->getAuthSource(si);
                        try {
                            authdb::client::ClientConnection authcon;
                            authcon.setUrl(src.url);
                            authcon.setClientName(src.clientName);
                            authcon.setClientSecret(src.clientSecret);
                            authcon.setSessionID(uuid::uuid(authid.c_str()));
                            authdb::client::Client clt(authcon);
                            if(clt.ClientAuth()){
                                if(currentAuthor == postAuthor && clt.GPOcheck(uuid::uuid(GPO_CONTENT_EDIT_OWN))){
                                    allowed = true;
                                } else if(clt.GPOcheck(uuid::uuid(GPO_CONTENT_EDIT_OTHERS))){
                                    allowed = true;
                                }
                            }
                        
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                        if(allowed) break;
                    }
                }

                if(!allowed){
                    excep[libhttppp::HTTPException::Error] << "Permission denied: insufficient rights to edit this post!";
                    throw excep;
                }
            }

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

            libhttppp::HttpForm curform;
            curform.parse(curreq);

            std::string title;
            std::string text;
            std::string tags;
            std::string descrition;

            if (!curform.getBoundary().empty()) {
                for (const auto &curformdat : curform.multipartData()) {
                    std::string field;
                    for(const auto &curctdisp : curformdat.dispositions){
                        if(curctdisp.key=="name"){
                            field = curctdisp.value;
                            break;
                        }
                    }
                    if (field == "blog_title") {
                        title.append(curformdat.value.data(),curformdat.value.size());
                    }
                    else if (field == "blog_descrition") {
                        descrition.append(curformdat.value.data(), curformdat.value.size());
                    }
                    else if (field == "blog_tags") {
                        tags.append(curformdat.value.data(), curformdat.value.size());
                    }
                    else if (field == "blog_content") {
                        text.append(curformdat.value.data(), curformdat.value.size());
                    }
                }

                if (!text.empty() && !title.empty() && !descrition.empty()) {
                    std::string slug = makeSlug(title.c_str());
                    dbpp::SQL sqltext;
                    sqltext << "UPDATE content SET title='" <<
                    dbpp::SQL::escaped(sbuf,title.c_str()) << "',slug='" <<
                    dbpp::SQL::escaped(sbuf,slug.c_str()) << "',descrition='" <<
                    dbpp::SQL::escaped(sbuf,descrition.c_str()) << "',text='" <<
                    dbpp::SQL::escaped(sbuf,text.c_str())  << "' where id='" << cid << "';";

                    dbpp::DBResult textres;

                    if (Args->database[tid]->exec(sqltext, textres)<0) {
                        excep[libhttppp::HTTPException::Critical] << "can't update updare content with id: " << cid << " !";
                        throw excep;
                    }
                }

                sql <<  "DELETE FROM tags_content WHERE content_id='" << cid <<"';";

                if (Args->database[tid]->exec(sql,res) < 0) {
                    excep[libhttppp::HTTPException::Critical] << "can't delete old tags id from tags_id!";
                    throw excep;
                }

                cleartags(tid);

                size_t tgsstart = 0;
                if (!tags.empty() && cid != -1) {
                    for (size_t i = 0; i <= tags.length(); ++i) {
                        if (tags[i] == ' ' || i == tags.length()) {
                            if ((i - tgsstart) > 0) {
                                std::string tag = tags.substr(tgsstart, (i - tgsstart));
                                tgsstart = i + 1;
                                int tries = 0;

                                TAGNAMECHECK:
                                if (tries > 5) {
                                    excep[libhttppp::HTTPException::Critical] << "tag doesn't exists and could not created!";
                                    throw excep;
                                }
                                sql="select id,name from tags where name='";
                                sql << dbpp::SQL::escaped(sbuf,tag.c_str()) << "' LIMIT 1;";

                                int tamount;

                                if ((tamount=Args->database[tid]->exec(sql,res)) < 0) {
                                    excep[libhttppp::HTTPException::Critical] << "can't find existing tags !";
                                    throw excep;
                                }

                                if (tamount != 1) {
                                    sql="insert into tags (name) VALUES ('";
                                    sql << dbpp::SQL::escaped(sbuf,tag.c_str()) << "');";
                                    Args->database[tid]->exec(sql,res);
                                    ++tries;
                                    goto TAGNAMECHECK;
                                }

                                sql= "insert into tags_content (content_id,tag_id) VALUES ('"; sql << cid << "','" << res[0][0] << "');";

                                if (Args->database[tid]->exec(sql,res) < 0) {
                                    excep[libhttppp::HTTPException::Critical] << "can't create link between tag and content !";
                                    throw excep;
                                }

                            }
                        }
                    }
                }
            }

            sql = "SELECT title,descrition,text,tags.name FROM content ";
            sql << "LEFT JOIN tags_content ON tags_content.content_id = content.id "
                << "LEFT JOIN tags ON tags.id = tags_content.tag_id where content.id = '" << cid << "'";

            libhtmlpp::HtmlString condat;

            int ccamount;

            if ((ccamount=Args->database[tid]->exec(sql,res)) < 0) {
                excep[libhttppp::HTTPException::Error] << "can't find tages for this content id !";
                throw excep;
            }

            if (ccamount > 0) {
                condat << "<div id=\"content\">"
                << "<form action=\"" << Args->config->buildurl("content/edit/") << cid << "\" method=\"post\" enctype=\"multipart/form-data\" >"
                << "<span>"+tr(_lang,"Title:")+"</span><input maxlength=\"255\" name=\"blog_title\" type=\"text\" style=\"width: 100%;\" value=\"" << res[0][0] << "\" ><br>"
                << "<span>"+tr(_lang,"Short description:")+"</span><input maxlength=\"255\" name=\"blog_descrition\" type=\"text\"  style=\"width: 100%;\" value=\"" << res[0][1] << "\" ><br>"
                << "<span>"+tr(_lang,"Tags:")+"</span><input name=\"blog_tags\" type=\"text\"  style=\"width: 100%;\" value=\"";
                for (int i = 0; i < ccamount; ++i) {
                    condat << res[i][3];
                    if ((ccamount - 1) != i)
                        condat << " ";
                }
                condat << "\" ><br>"
                << "<span>"+tr(_lang,"Text:")+"</span><textarea name=\"blog_content\" type=\"text\" style=\"width: 100%; height:480px;\" >" << res[0][2] << "</textarea>"
                << "<input type=\"submit\" value=\""+tr(_lang,"Post blog entry")+"\">"
                << "</form>"
                << "</div>";
            };
            libhtmlpp::HtmlString out;

            try{
                page.getElementbyID("main")->insertChild(condat.parse());
            }catch(libhtmlpp::HTMLException &e){
                excep[libhttppp::HTTPException::Error] << e.what();
                throw excep;
            }
            Args->theme->printSite(tid,out,&page,curreq.getRequestURL(),Args->auth->isLoggedIn(tid,curreq,sessionid));

            libhttppp::HttpResponse resp;
            resp.setState(HTTP200);
            resp.setContentType("text/html");
            sendCompressed(curreq,resp,out.c_str(),out.length());
        }

        const std::string getName(){
            return "content";
        }

        const std::string getVersion(){
            return "0.2alpha";
        }

        const std::string getAuthor(){
            return "Jan Koester";
        }

        bool haveSettings(){
            return true;
        }

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

            // Helper to build page_tabs array
            auto makePageTabs = [&](bool postsActive) -> json_object* {
                json_object *tabs = json_object_new_array();
                json_object *t1 = json_object_new_object();
                json_object_object_add(t1, "label", json_object_new_string(tr(_lang,"Posts").c_str()));
                json_object_object_add(t1, "url", json_object_new_string(Args->config->buildurl("settings/content").c_str()));
                json_object_object_add(t1, "active", json_object_new_boolean(postsActive));
                json_object_array_add(tabs, t1);
                json_object *t2 = json_object_new_object();
                json_object_object_add(t2, "label", json_object_new_string(tr(_lang,"Comments").c_str()));
                json_object_object_add(t2, "url", json_object_new_string(Args->config->buildurl("settings/content/comments").c_str()));
                json_object_object_add(t2, "active", json_object_new_boolean(!postsActive));
                json_object_array_add(tabs, t2);
                return tabs;
            };

            // Resolve current user for GPO checks
            int currentAuthor = -1;
            std::string authid;
            try {
                Session sess(*Args->database[tid]);
                sess.getSessionData(sessionid,"authid",authid);
                std::string uid;
                sess.getSessionData(sessionid,"uid",uid);
                if(!uid.empty()){
                    dbpp::SQL asql;
                    dbpp::DBResult ares;
                    asql << "SELECT id FROM users WHERE sid='" << uid.c_str() << "' LIMIT 1;";
                    if(Args->database[tid]->exec(asql,ares) >= 1)
                        currentAuthor = atoi(ares[0][0]);
                }
            } catch(...) {}

            auto checkGPO = [&](const char *gpoid) -> bool {
                if(authid.empty()) return false;
                for(size_t si = 0; si < Args->config->getAuthSourceCount(); ++si){
                    const AuthSource &src = Args->config->getAuthSource(si);
                    try {
                        authdb::client::ClientConnection authcon;
                        authcon.setUrl(src.url);
                        authcon.setClientName(src.clientName);
                        authcon.setClientSecret(src.clientSecret);
                        authcon.setSessionID(uuid::uuid(authid.c_str()));
                        authdb::client::Client clt(authcon);
                        if(clt.ClientAuth() && clt.GPOcheck(uuid::uuid(gpoid)))
                            return true;
                    
} catch(authdb::AuthBackendError &e) { std::cerr << "AuthBackendError: " << e.what() << std::endl; continue; }
  catch(std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; continue; }
  catch(...) { std::cerr << "Unknown exception" << std::endl; continue; }
                }
                return false;
            };

            bool canEditOwn    = checkGPO(GPO_CONTENT_EDIT_OWN);
            bool canEditOthers = checkGPO(GPO_CONTENT_EDIT_OTHERS);
            bool canDelOwn     = checkGPO(GPO_CONTENT_DELETE_OWN);
            
bool canDelOthers  = checkGPO(GPO_CONTENT_DELETE_OTHERS);
std::cerr << "DEBUG: authid=" << authid << " currentAuthor=" << currentAuthor << " canEditOwn=" << canEditOwn << " canEditOthers=" << canEditOthers << " canDelOwn=" << canDelOwn << " canDelOthers=" << canDelOthers << std::endl;

            // Sub-route: delete
            if(curl.length() > base.length()){
                std::string sub = curl.substr(base.length());

                if(sub.find("delete") == 0){
                    int cid = -1;
                    bool confirmed = false;
                    libhttppp::HttpForm delform;
                    delform.parse(req);
                    for(const auto &cur : delform.urlData()){
                        if(cur.key == "id") cid = atoi(cur.value.c_str());
                        if(cur.key == "confirm" && cur.value == "yes") confirmed = true;
                    }

                    if(cid > 0){
                        // Check permission
                        dbpp::SQL psql; dbpp::DBResult pres;
                        psql << "SELECT author,title FROM content WHERE id='" << cid << "' LIMIT 1;";
                        bool allowed = false;
                        std::string postTitle;
                        if(Args->database[tid]->exec(psql,pres) >= 1){
                            int postAuthor = atoi(pres[0][0]);
                            if(pres[0][1]) postTitle = pres[0][1];
                            if(currentAuthor == postAuthor && canDelOwn) allowed = true;
                            else if(canDelOthers) allowed = true;
                        }
                        json_object *jroot = json_object_new_object();
                        json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Content").c_str()));

                        if(!allowed){
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Permission denied").c_str()));
                            json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                        } else if(!confirmed){
                            // Show confirmation dialog
                            json_object *jconfirm = json_object_new_object();
                            std::string msg = tr(_lang,"Are you sure you want to delete") + " \"" + postTitle + "\"?";
                            json_object_object_add(jconfirm, "message", json_object_new_string(msg.c_str()));
                            std::string yesUrl = Args->config->buildurl("settings/content/delete?id=")
                                + std::to_string(cid) + "&confirm=yes";
                            json_object_object_add(jconfirm, "yes_url", json_object_new_string(yesUrl.c_str()));
                            json_object_object_add(jconfirm, "no_url",
                                json_object_new_string(Args->config->buildurl("settings/content").c_str()));
                            json_object_object_add(jroot, "confirm", jconfirm);
                        } else {
                            dbpp::SQL dsql; dbpp::DBResult dres;
                            dsql << "DELETE FROM tags_content WHERE content_id='" << cid << "';";
                            try { Args->database[tid]->exec(dsql,dres); } catch(...) {}
                            dsql.clear();
                            dsql << "DELETE FROM content WHERE id='" << cid << "';";
                            Args->database[tid]->exec(dsql,dres);
                            cleartags(tid);
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Post deleted").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/content").c_str()));
                        json_object_array_add(jnav, jback);
                        json_object_object_add(jroot, "nav", jnav);
                        return jroot;
                    }
                }

                // Sub-route: edit
                if(sub.find("edit") == 0){
                    int cid = -1;
                    libhttppp::HttpForm editform;
                    editform.parse(req);
                    for(const auto &cur : editform.urlData()){
                        if(cur.key == "id") { cid = atoi(cur.value.c_str()); break; }
                    }

                    if(cid > 0){
                        // Check permission
                        dbpp::SQL psql; dbpp::DBResult pres;
                        psql << "SELECT author,title,descrition,text FROM content WHERE id='" << cid << "' LIMIT 1;";
                        bool allowed = false;
                        bool found = false;
                        int postAuthor = -1;
                        if(Args->database[tid]->exec(psql,pres) >= 1){
                            found = true;
                            postAuthor = atoi(pres[0][0]);
                            if(currentAuthor == postAuthor && canEditOwn) allowed = true;
                            else if(canEditOthers) allowed = true;
                        }

                        if(!found || !allowed){
                            json_object *jroot = json_object_new_object();
                            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Content").c_str()));
                            json_object_object_add(jroot, "message", json_object_new_string(
                                found ? tr(_lang,"Permission denied").c_str() : tr(_lang,"Post not found").c_str()));
                            json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                            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/content").c_str()));
                            json_object_array_add(jnav, jback);
                            json_object_object_add(jroot, "nav", jnav);
                            return jroot;
                        }

                        // Handle POST save
                        libhttppp::HttpForm form;
                        form.parse(req);
                        std::string newTitle, newDesc, newText;
                        bool saving = false;
                        for(const auto &cur : form.urlData()){
                            if(cur.key == "blog_title") { newTitle = cur.value; saving = true; }
                            else if(cur.key == "blog_description") newDesc = cur.value;
                            else if(cur.key == "blog_content") newText = cur.value;
                        }

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

                        if(saving && !newTitle.empty()){
                            std::vector<char> sbuf;
                            dbpp::SQL usql; dbpp::DBResult ures;
                            std::string slug = makeSlug(newTitle.c_str());
                            usql << "UPDATE content SET title='" << dbpp::SQL::escaped(sbuf,newTitle.c_str())
                                 << "',slug='" << dbpp::SQL::escaped(sbuf,slug.c_str())
                                 << "',descrition='" << dbpp::SQL::escaped(sbuf,newDesc.c_str())
                                 << "',text='" << dbpp::SQL::escaped(sbuf,newText.c_str())
                                 << "' WHERE id='" << cid << "';";
                            Args->database[tid]->exec(usql,ures);
                            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"));
                            // Re-read updated values
                            psql.clear();
                            psql << "SELECT author,title,descrition,text FROM content WHERE id='" << cid << "' LIMIT 1;";
                            Args->database[tid]->exec(psql,pres);
                        }

                        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/content").c_str()));
                        json_object_array_add(jnav, jback);
                        json_object_object_add(jroot, "nav", jnav);

                        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(curl.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("blog_title"));
                        json_object_object_add(jf1, "type", json_object_new_string("text"));
                        json_object_object_add(jf1, "label", json_object_new_string(tr(_lang,"Title").c_str()));
                        json_object_object_add(jf1, "value", json_object_new_string(pres[0][1] ? pres[0][1] : ""));
                        json_object_array_add(jfields, jf1);

                        json_object *jf2 = json_object_new_object();
                        json_object_object_add(jf2, "name", json_object_new_string("blog_description"));
                        json_object_object_add(jf2, "type", json_object_new_string("text"));
                        json_object_object_add(jf2, "label", json_object_new_string(tr(_lang,"Short description").c_str()));
                        json_object_object_add(jf2, "value", json_object_new_string(pres[0][2] ? pres[0][2] : ""));
                        json_object_array_add(jfields, jf2);

                        json_object *jf3 = json_object_new_object();
                        json_object_object_add(jf3, "name", json_object_new_string("blog_content"));
                        json_object_object_add(jf3, "type", json_object_new_string("textarea"));
                        json_object_object_add(jf3, "label", json_object_new_string(tr(_lang,"Text").c_str()));
                        json_object_object_add(jf3, "value", json_object_new_string(pres[0][3] ? pres[0][3] : ""));
                        json_object_array_add(jfields, jf3);

                        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;
                    }
                }

                // Sub-route: comments (moderation)
                if(sub.find("comments") == 0){
                    bool canModerate = checkGPO(GPO_CONTENT_MODERATE);

                    // Handle approve/delete actions (require GPO)
                    if(sub.find("comments/approve") == 0){
                        if(!canModerate){
                            json_object *jroot = json_object_new_object();
                            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Comments").c_str()));
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Permission denied").c_str()));
                            json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                            return jroot;
                        }
                        int cmtId = -1;
                        libhttppp::HttpForm cmtform;
                        cmtform.parse(req);
                        for(const auto &cur : cmtform.urlData()){
                            if(cur.key == "id") { cmtId = atoi(cur.value.c_str()); break; }
                        }
                        json_object *jroot = json_object_new_object();
                        json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Comments").c_str()));
                        if(cmtId > 0){
                            dbpp::SQL asql; dbpp::DBResult ares;
                            asql << "UPDATE comments SET approved=true WHERE id=" << cmtId << ";";
                            Args->database[tid]->exec(asql, ares);
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Comment approved").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 comments").c_str()));
                        json_object_object_add(jback, "url", json_object_new_string(Args->config->buildurl("settings/content/comments").c_str()));
                        json_object_array_add(jnav, jback);
                        json_object_object_add(jroot, "nav", jnav);
                        return jroot;
                    }

                    if(sub.find("comments/delete") == 0){
                        if(!canModerate){
                            json_object *jroot = json_object_new_object();
                            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Comments").c_str()));
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Permission denied").c_str()));
                            json_object_object_add(jroot, "message_type", json_object_new_string("error"));
                            return jroot;
                        }
                        int cmtId = -1;
                        bool confirmed = false;
                        libhttppp::HttpForm cmtform;
                        cmtform.parse(req);
                        for(const auto &cur : cmtform.urlData()){
                            if(cur.key == "id") cmtId = atoi(cur.value.c_str());
                            if(cur.key == "confirm" && cur.value == "yes") confirmed = true;
                        }
                        json_object *jroot = json_object_new_object();
                        json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Comments").c_str()));
                        if(cmtId > 0 && !confirmed){
                            json_object *jconfirm = json_object_new_object();
                            json_object_object_add(jconfirm, "message",
                                json_object_new_string(tr(_lang,"Are you sure you want to delete this comment?").c_str()));
                            std::string yesUrl = Args->config->buildurl("settings/content/comments/delete?id=")
                                + std::to_string(cmtId) + "&confirm=yes";
                            json_object_object_add(jconfirm, "yes_url", json_object_new_string(yesUrl.c_str()));
                            json_object_object_add(jconfirm, "no_url",
                                json_object_new_string(Args->config->buildurl("settings/content/comments").c_str()));
                            json_object_object_add(jroot, "confirm", jconfirm);
                        } else if(cmtId > 0 && confirmed){
                            dbpp::SQL dsql; dbpp::DBResult dres;
                            dsql << "DELETE FROM comments WHERE id=" << cmtId << ";";
                            Args->database[tid]->exec(dsql, dres);
                            json_object_object_add(jroot, "message", json_object_new_string(tr(_lang,"Comment deleted").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 comments").c_str()));
                        json_object_object_add(jback, "url", json_object_new_string(Args->config->buildurl("settings/content/comments").c_str()));
                        json_object_array_add(jnav, jback);
                        json_object_object_add(jroot, "nav", jnav);
                        return jroot;
                    }

                    // Default: list all comments
                    json_object *jroot = json_object_new_object();
                    json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Content").c_str()));
                    json_object_object_add(jroot, "page_tabs", makePageTabs(false));

                    json_object *jtable = json_object_new_object();
                    json_object *jheaders = json_object_new_array();
                    json_object_array_add(jheaders, json_object_new_string("ID"));
                    json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Author").c_str()));
                    json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Email").c_str()));
                    json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Comment").c_str()));
                    json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Post").c_str()));
                    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,"Status").c_str()));
                    json_object_object_add(jtable, "headers", jheaders);

                    dbpp::SQL csql; dbpp::DBResult cres;
                    csql << "SELECT comments.id,comments.author_name,comments.email,"
                         << "substring(comments.text from 1 for 80),content.title,comments.created,"
                         << "comments.approved,comments.email_verified "
                         << "FROM comments LEFT JOIN content ON comments.content_id=content.id "
                         << "ORDER BY comments.approved ASC, comments.created DESC LIMIT 100;";
                    int ccount = Args->database[tid]->exec(csql, cres);

                    json_object *jrows = json_object_new_array();
                    for(int i = 0; i < ccount; ++i){
                        json_object *jrow = json_object_new_object();
                        json_object *jcells = json_object_new_array();
                        json_object_array_add(jcells, json_object_new_string(cres[i][0] ? cres[i][0] : ""));
                        json_object_array_add(jcells, json_object_new_string(cres[i][1] ? cres[i][1] : ""));
                        json_object_array_add(jcells, json_object_new_string(cres[i][2] ? cres[i][2] : ""));
                        json_object_array_add(jcells, json_object_new_string(cres[i][3] ? cres[i][3] : ""));
                        json_object_array_add(jcells, json_object_new_string(cres[i][4] ? cres[i][4] : ""));
                        json_object_array_add(jcells, json_object_new_string(cres[i][5] ? cres[i][5] : ""));

                        bool isApproved = cres[i][6] && std::string(cres[i][6]) == "t";
                        bool isVerified = cres[i][7] && std::string(cres[i][7]) == "t";
                        std::string status;
                        if(isApproved)
                            status = tr(_lang,"Approved");
                        else if(isVerified)
                            status = tr(_lang,"Email verified");
                        else
                            status = tr(_lang,"Pending verification");
                        json_object_array_add(jcells, json_object_new_string(status.c_str()));

                        json_object_object_add(jrow, "cells", jcells);

                        json_object *jactions = json_object_new_array();
                        if(canModerate){
                            if(!isApproved && isVerified){
                                json_object *japprove = json_object_new_object();
                                json_object_object_add(japprove, "label", json_object_new_string(tr(_lang,"Approve").c_str()));
                                json_object_object_add(japprove, "url", json_object_new_string(
                                    (Args->config->buildurl("settings/content/comments/approve?id=") + std::string(cres[i][0])).c_str()));
                                json_object_array_add(jactions, japprove);
                            }

                            json_object *jdel = json_object_new_object();
                            json_object_object_add(jdel, "label", json_object_new_string(tr(_lang,"Delete").c_str()));
                            json_object_object_add(jdel, "url", json_object_new_string(
                                (Args->config->buildurl("settings/content/comments/delete?id=") + std::string(cres[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);
                    return jroot;
                }
            }

            // Default: post list with edit/delete actions
            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Content").c_str()));
            json_object_object_add(jroot, "page_tabs", makePageTabs(true));

            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string("ID"));
            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,"Author").c_str()));
            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,"Actions").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            dbpp::SQL sql; dbpp::DBResult res;
            sql << "SELECT content.id,content.title,users.displayname,content.created,content.author "
                << "FROM content LEFT JOIN users ON content.author=users.id ORDER BY content.id DESC LIMIT 100";
            int ncount = Args->database[tid]->exec(sql,res);

            json_object *jrows = json_object_new_array();
            for(int i = 0; i < ncount; ++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] ? res[i][0] : ""));
                json_object_array_add(jcells, json_object_new_string(res[i][1] ? res[i][1] : ""));
                json_object_array_add(jcells, json_object_new_string(res[i][2] ? res[i][2] : ""));
                json_object_array_add(jcells, json_object_new_string(res[i][3] ? res[i][3] : ""));
                json_object_object_add(jrow, "cells", jcells);

                int postAuthor = res[i][4] ? atoi(res[i][4]) : -2;
                bool isOwn = (currentAuthor == postAuthor);

                json_object *jactions = json_object_new_array();

                if((isOwn && canEditOwn) || canEditOthers){
                    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("editor?content_id=") + std::string(res[i][0]) + "&content_title=" + std::string(res[i][1] ? res[i][1] : "")).c_str()));
                    json_object_array_add(jactions, jedit);
                }

                if((isOwn && canDelOwn) || canDelOthers){
                    json_object *jdel = json_object_new_object();
                    json_object_object_add(jdel, "label", json_object_new_string(tr(_lang,"Delete").c_str()));
                    json_object_object_add(jdel, "url", json_object_new_string(
                        (Args->config->buildurl("settings/content/delete?id=") + 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);

            return jroot;
        }

        void initPlugin(){
            registerGPO("35f21cbf-66d7-4d5f-8d09-3b8fd2d67295",
                        "Blogi: Manage Content",
                        "Controls access to content management",
                        true);

            registerGPO(GPO_CONTENT_EDIT_OWN,
                        "Blogi: Edit Own Posts",
                        "Allows users to edit their own content posts",
                        true);

            registerGPO(GPO_CONTENT_EDIT_OTHERS,
                        "Blogi: Edit Others Posts",
                        "Allows users to edit content posts from other authors",
                        false);

            registerGPO(GPO_CONTENT_DELETE_OWN,
                        "Blogi: Delete Own Posts",
                        "Allows users to delete their own content posts",
                        true);

            registerGPO(GPO_CONTENT_DELETE_OTHERS,
                        "Blogi: Delete Others Posts",
                        "Allows users to delete content posts from other authors",
                        false);

            registerGPO(GPO_CONTENT_COMMENT,
                        "Blogi: Comment on Posts",
                        "Allows logged-in users to post comments without moderation",
                        true);

            registerGPO(GPO_CONTENT_MODERATE,
                        "Blogi: Moderate Comments",
                        "Allows users to approve or delete guest comments",
                        true);

            dbpp::SQL sql;
            dbpp::DBResult res;
            sql << "CREATE TABLE IF NOT EXISTS content ("
                <<   "id integer PRIMARY KEY " << Args->database[0]->autoincrement(sql) << ","
                <<     "title character varying(255) NOT NULL UNIQUE,"
                <<     "slug character varying(255) NOT NULL DEFAULT '',"
                <<     "text text NOT NULL,"
                <<     "descrition character varying(255) NOT NULL,"
                <<     "author integer ,"
                <<     "created date NOT NULL,"
                <<     "FOREIGN KEY (author) REFERENCES users(id)"
                <<   "); "
                << "CREATE TABLE IF NOT EXISTS tags ("
                <<   "id integer PRIMARY KEY " << Args->database[0]->autoincrement(sql) << ","
                <<      "name character varying(255) NOT NULL UNIQUE"
                << "); "
                << "CREATE TABLE IF NOT EXISTS tags_content ("
                <<   "tag_id integer NOT NULL,"
                <<   "content_id integer NOT NULL,"
                <<   "FOREIGN KEY (content_id) REFERENCES content (id),"
                <<   "FOREIGN KEY (tag_id) REFERENCES tags (id)"
                << "); "
                << "CREATE TABLE IF NOT EXISTS comments ("
                <<   "id integer PRIMARY KEY " << Args->database[0]->autoincrement(sql) << ","
                <<   "content_id integer NOT NULL,"
                <<   "user_id integer,"
                <<   "author_name character varying(255) NOT NULL,"
                <<   "email character varying(255),"
                <<   "text text NOT NULL,"
                <<   "created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                <<   "approved boolean NOT NULL DEFAULT false,"
                <<   "email_verified boolean NOT NULL DEFAULT false,"
                <<   "verify_token character varying(64),"
                <<   "FOREIGN KEY (content_id) REFERENCES content (id),"
                <<   "FOREIGN KEY (user_id) REFERENCES users (id)"
                << ");";
            Args->database[0]->exec(sql,res);

            // Migration: add slug column if missing, populate empty slugs
            try {
                sql.clear();
                sql << "ALTER TABLE content ADD COLUMN slug character varying(255) NOT NULL DEFAULT '';";
                Args->database[0]->exec(sql, res);
            } catch (...) { /* column already exists */ }
            try {
                sql.clear();
                sql << "SELECT id,title FROM content WHERE slug='' OR slug IS NULL;";
                int migCount = Args->database[0]->exec(sql, res);
                std::vector<char> mbuf;
                for (int i = 0; i < migCount; ++i) {
                    std::string s = makeSlug(res[i][1]);
                    dbpp::SQL usql;
                    dbpp::DBResult ures;
                    usql << "UPDATE content SET slug='" << dbpp::SQL::escaped(mbuf, s.c_str())
                         << "' WHERE id=" << res[i][0] << ";";
                    Args->database[0]->exec(usql, ures);
                }
            } catch (...) {}

            // Migration: add UNIQUE constraint on title if missing
            try {
                sql.clear();
                sql << "ALTER TABLE content ADD CONSTRAINT content_title_unique UNIQUE (title);";
                Args->database[0]->exec(sql, res);
            } catch (...) { /* constraint already exists */ }

            return;
        }

        bool Controller(const int tid,libhttppp::HttpRequest &req,libhtmlpp::HtmlElement *page,const std::string &sessionid){

            if(req.getRequestURL().compare(0,Args->config->buildurl("content").length(),Args->config->buildurl("content"))!=0){
                return false;
            }

            std::string curl;

            std::copy(req.getRequestURL().begin(),req.getRequestURL().end(),std::back_inserter(curl));

            if (curl.compare(0,Args->config->buildurl("content/addpost").length(),Args->config->buildurl("content/addpost")) == 0){
                    addPostPage(tid,req,*page,sessionid);
            }else if (curl.compare(0,Args->config->buildurl("content/comment/verify").length(), Args->config->buildurl("content/comment/verify")) == 0) {
                    verifyCommentPage(tid,req,*page,sessionid);
            }else if (curl.compare(0,Args->config->buildurl("content/comment").length(), Args->config->buildurl("content/comment")) == 0) {
                    commentPage(tid,req,*page,sessionid);
            }else if (curl.compare(0,Args->config->buildurl("content/read").length(), Args->config->buildurl("content/read")) == 0) {
                    contentPage(tid,req,*page,sessionid);
            }else if (curl.compare(0,Args->config->buildurl("content/edit").length(), Args->config->buildurl("content/edit")) == 0) {
                    editPostPage(tid,req,*page,sessionid);
            }else if (curl.compare(0,Args->config->buildurl("content/del").length(), Args->config->buildurl("content/del")) == 0) {
                    delPostPage(tid,req,*page,sessionid);
            }else{
                int startpos = 0;
                libhttppp::HttpForm start;
                start.parse(req);
                for (const auto &cururlform : start.urlData()){
                    if(cururlform.key=="start")
                        startpos=atoi(cururlform.value.c_str());
                }

                const auto tagPrefix = Args->config->buildurl("content/tag/");
                if (curl.compare(0, tagPrefix.length(), tagPrefix) == 0) {
                    std::string dtag = curl.substr(tagPrefix.length());
                    std::string tag;
                    libhttppp::HttpForm::urlDecode(dtag, tag);
                    contentIdxPage(tid, req, page, tag, startpos, SITELIMIT, sessionid);
                } else {
                    contentIdxPage(tid, req, page, "", startpos, SITELIMIT, sessionid);
                }
            }
            return true;
        }

        void Search(const int tid, const char* word, blogi::SearchRet& result, const std::string &sessionid){
            std::vector<char> buf;
            dbpp::SQL sql;
            dbpp::DBResult res;

            sql << "SELECT title,descrition,slug FROM content WHERE title @@ to_tsquery('"
                  << sql.escaped(buf,word)
                  <<"') OR descrition @@ to_tsquery('"
                  << sql.escaped(buf,word)
                  <<"')  OR text @@ to_tsquery('"
                  << sql.escaped(buf,word) <<"');";

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

            for (int i=0; i < count; ++i) {
                SearchRet::Data data;
                data.title=res[i][0];

                std::copy(res[i][1],res[i][1]+strlen(res[i][1]),std::back_inserter(data.text));

                std::string surl=Args->config->buildurl("content/read/");

                surl+=res[i][2];

                data.anchor=surl;

                result.push_back(data);

            }

        }

        // --- Publish Interface ---
        bool canPublish() override {
            return true;
        }

        void RenderPublished(const int tid, const std::string &html,
                             json_object *params, libhtmlpp::HtmlElement &page) override {
            std::string _lang = getLang(*Args->database[tid]);
            std::string title, description, author, date;
            json_object *val_obj = nullptr;
            if (params) {
                if (json_object_object_get_ex(params, "title", &val_obj))
                    title = json_object_get_string(val_obj);
                if (json_object_object_get_ex(params, "description", &val_obj))
                    description = json_object_get_string(val_obj);
                if (json_object_object_get_ex(params, "author", &val_obj))
                    author = json_object_get_string(val_obj);
                if (json_object_object_get_ex(params, "date", &val_obj))
                    date = json_object_get_string(val_obj);
            }

            libhtmlpp::HtmlString condat;
            condat << "<div id=\"content\">"
                   << "<div class=\"blog_entry\">"
                   << "<span class=\"title\">" << (title.empty() ? tr(_lang,"Untitled") : title) << "</span>"
                   << "<div class=\"blog_entry_idx_content\"><div class=\"text\">" << html << "</div>";
            if (!author.empty() || !date.empty()) {
                condat << "<span class=\"author\">" << tr(_lang,"written by") << " "
                       << (author.empty() ? "?" : author);
                if (!date.empty())
                    condat << " " << tr(_lang,"on") << " " << date;
                condat << "</span>";
            }
            condat << "</div></div></div>";

            libhtmlpp::HtmlElement *main = page.getElementbyID("main");
            if (main) {
                try {
                    main->insertChild(condat.parse());
                } catch (libhtmlpp::HTMLException &e) {
                    std::cerr << "content: RenderPublished HTML error: " << e.what() << std::endl;
                }
            }
        }

        json_object* getPublishForm(const int tid) override {
            std::string _lang = getLang(*Args->database[tid]);
            json_object *fields = json_object_new_array();

            json_object *f_title = json_object_new_object();
            json_object_object_add(f_title, "key", json_object_new_string("title"));
            json_object_object_add(f_title, "label", json_object_new_string(tr(_lang,"Title").c_str()));
            json_object_object_add(f_title, "type", json_object_new_string("text"));
            json_object_object_add(f_title, "required", json_object_new_boolean(1));
            json_object_object_add(f_title, "placeholder", json_object_new_string(tr(_lang,"Post title").c_str()));
            json_object_array_add(fields, f_title);

            json_object *f_desc = json_object_new_object();
            json_object_object_add(f_desc, "key", json_object_new_string("description"));
            json_object_object_add(f_desc, "label", json_object_new_string(tr(_lang,"Description").c_str()));
            json_object_object_add(f_desc, "type", json_object_new_string("text"));
            json_object_object_add(f_desc, "required", json_object_new_boolean(1));
            json_object_object_add(f_desc, "placeholder", json_object_new_string(tr(_lang,"Short description").c_str()));
            json_object_array_add(fields, f_desc);

            json_object *f_tags = json_object_new_object();
            json_object_object_add(f_tags, "key", json_object_new_string("tags"));
            json_object_object_add(f_tags, "label", json_object_new_string(tr(_lang,"Tags").c_str()));
            json_object_object_add(f_tags, "type", json_object_new_string("text"));
            json_object_object_add(f_tags, "placeholder", json_object_new_string(tr(_lang,"tag1 tag2 tag3").c_str()));
            json_object_array_add(fields, f_tags);

            return fields;
        }

        void Publish(const int tid, const std::string &html, json_object *params, json_object *response) override {
            std::string _lang = getLang(*Args->database[tid]);
            std::vector<char> sbuf;
            json_object *val_obj = nullptr;

            std::string title, description, tags;

            if (params) {
                if (json_object_object_get_ex(params, "title", &val_obj)) {
                    const char *v = json_object_get_string(val_obj);
                    if (v) title = v;
                }
                if (json_object_object_get_ex(params, "description", &val_obj)) {
                    const char *v = json_object_get_string(val_obj);
                    if (v) description = v;
                }
                if (json_object_object_get_ex(params, "tags", &val_obj)) {
                    const char *v = json_object_get_string(val_obj);
                    if (v) tags = v;
                }
            }

            if (title.empty() || description.empty()) {
                json_object_object_add(response, "status", json_object_new_string("error"));
                json_object_object_add(response, "message", json_object_new_string(tr(_lang,"Title and description are required.").c_str()));
                return;
            }

            try {
                std::string time_str;
                auto now = std::chrono::system_clock::now();
                try {
                    time_str = std::format("{:%Y-%m-%d}", now);
                } catch (...) {
                    time_str = "1970-01-01";
                }

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

                // Resolve author: use author_id from params (set by editor from authdb)
                int authorId = 0;
                if (params) {
                    json_object *aid_obj = nullptr;
                    if (json_object_object_get_ex(params, "author_id", &aid_obj)) {
                        authorId = json_object_get_int(aid_obj);
                    }
                }

                if (authorId <= 0) {
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string(tr(_lang,"No valid author found. Please ensure a user exists.").c_str()));
                    return;
                }

                // Check for existing post with same title
                bool overwrite = false;
                if (params) {
                    json_object *ow_obj = nullptr;
                    if (json_object_object_get_ex(params, "overwrite", &ow_obj)) {
                        overwrite = json_object_get_boolean(ow_obj);
                    }
                }

                sql << "SELECT id FROM content WHERE title='" << dbpp::SQL::escaped(sbuf, title.c_str()) << "' LIMIT 1;";
                int dupCount = Args->database[tid]->exec(sql, res);

                int content_id = 0;
                std::string slug = makeSlug(title.c_str());

                if (dupCount > 0 && !overwrite) {
                    // Duplicate found — ask user to confirm overwrite
                    json_object_object_add(response, "status", json_object_new_string("duplicate"));
                    json_object_object_add(response, "message",
                        json_object_new_string(tr(_lang,"A post with this title already exists. Overwrite?").c_str()));
                    json_object_object_add(response, "existing_id", json_object_new_int(atoi(res[0][0])));
                    return;
                } else if (dupCount > 0 && overwrite) {
                    // Overwrite existing post
                    content_id = atoi(res[0][0]);
                    sql.clear();
                    sql << "UPDATE content SET slug='" << dbpp::SQL::escaped(sbuf, slug.c_str())
                        << "', descrition='" << dbpp::SQL::escaped(sbuf, description.c_str())
                        << "', text='" << dbpp::SQL::escaped(sbuf, html.c_str())
                        << "', author='" << authorId
                        << "', created='" << time_str.c_str()
                        << "' WHERE id=" << content_id << ";";
                    Args->database[tid]->exec(sql, res);

                    // Remove old tag links
                    sql.clear();
                    sql << "DELETE FROM tags_content WHERE content_id=" << content_id << ";";
                    try { Args->database[tid]->exec(sql, res); } catch (...) {}
                } else {
                    // New post
                    sql.clear();
                    sql << "INSERT INTO content (title,slug,descrition,text,author,created) VALUES ('"
                        << dbpp::SQL::escaped(sbuf, title.c_str()) << "','"
                        << dbpp::SQL::escaped(sbuf, slug.c_str()) << "','"
                        << dbpp::SQL::escaped(sbuf, description.c_str()) << "','"
                        << dbpp::SQL::escaped(sbuf, html.c_str()) << "','" << authorId << "','"
                        << time_str.c_str() << "') RETURNING id;";

                    Args->database[tid]->exec(sql, res);
                    content_id = atoi(res[0][0]);
                }

                // Process tags
                if (!tags.empty()) {
                    size_t start = 0;
                    for (size_t i = 0; i <= tags.size(); ++i) {
                        if (i == tags.size() || tags[i] == ' ') {
                            if (i > start) {
                                std::string tag = tags.substr(start, i - start);
                                // Find or create tag
                                sql.clear();
                                sql << "SELECT id FROM tags WHERE name='" << dbpp::SQL::escaped(sbuf, tag.c_str()) << "' LIMIT 1;";
                                int tc = Args->database[tid]->exec(sql, res);
                                if (tc < 1) {
                                    sql.clear();
                                    sql << "INSERT INTO tags (name) VALUES ('" << dbpp::SQL::escaped(sbuf, tag.c_str()) << "') RETURNING id;";
                                    Args->database[tid]->exec(sql, res);
                                }
                                // Link tag to content
                                std::string tag_id = res[0][0];
                                sql.clear();
                                sql << "INSERT INTO tags_content (content_id,tag_id) VALUES ('" << content_id << "','" << tag_id.c_str() << "');";
                                try { Args->database[tid]->exec(sql, res); } catch (...) {}
                            }
                            start = i + 1;
                        }
                    }
                }

                json_object_object_add(response, "status", json_object_new_string("success"));
                json_object_object_add(response, "message", json_object_new_string(tr(_lang,"Content published successfully.").c_str()));
                std::string url = Args->config->getprefix() + "/content/read/" + slug;
                json_object_object_add(response, "url", json_object_new_string(url.c_str()));
            } catch (std::exception &e) {
                json_object_object_add(response, "status", json_object_new_string("error"));
                json_object_object_add(response, "message", json_object_new_string(e.what()));
            } catch (...) {
                json_object_object_add(response, "status", json_object_new_string("error"));
                json_object_object_add(response, "message", json_object_new_string(tr(_lang,"Unknown error during content publish.").c_str()));
            }
        }

    };
};

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

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