/*******************************************************************************
 * 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 <iostream>
#include <vector>
#include <iterator>
#include <map>
#include <set>
#include <mutex>
#include <shared_mutex>
#include <atomic>
#include <memory>
#include <algorithm>
#include <cstdint>
#include <cstring>

#include <uuidp.h>

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

#include <mediadb/client.h>

#include <plugin.h>
#include <i18n.h>

#include "icon.webp.h"

namespace blogi {

    struct MediaType {
        std::string ext;   // file extension (e.g. "jpg", "mp4")
        std::string kind;  // "image" or "video"
    };

    class MediaDetect {
    public:
        static bool matchBytes(const unsigned char* data, size_t datalen,
                               const unsigned char* sig, size_t siglen, size_t offset = 0) {
            if (datalen < offset + siglen) return false;
            return std::memcmp(data + offset, sig, siglen) == 0;
        }

        static MediaType DetectMediaType(const unsigned char* data, size_t datalen) {
            if (datalen < 12) return {};

            // JPEG: FF D8 FF
            if (data[0]==0xFF && data[1]==0xD8 && data[2]==0xFF)
                return {"jpg", "image"};

            // PNG: 89 50 4E 47 0D 0A 1A 0A
            static const unsigned char png[] = {0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
            if (matchBytes(data, datalen, png, sizeof(png)))
                return {"png", "image"};

            // GIF: GIF87a or GIF89a
            if (data[0]=='G' && data[1]=='I' && data[2]=='F' && data[3]=='8'
                && (data[4]=='7' || data[4]=='9') && data[5]=='a')
                return {"gif", "image"};

            // WebP: RIFF....WEBP
            if (data[0]=='R' && data[1]=='I' && data[2]=='F' && data[3]=='F'
                && data[8]=='W' && data[9]=='E' && data[10]=='B' && data[11]=='P')
                return {"webp", "image"};

            // BMP: BM
            if (data[0]=='B' && data[1]=='M')
                return {"bmp", "image"};

            // TIFF: II*\0 or MM\0*
            if ((data[0]==0x49 && data[1]==0x49 && data[2]==0x2A && data[3]==0x00)
             || (data[0]==0x4D && data[1]==0x4D && data[2]==0x00 && data[3]==0x2A))
                return {"tiff", "image"};

            // HEIF/HEIC/AVIF/MP4/MOV/3GP: ftyp box
            if (data[4]=='f' && data[5]=='t' && data[6]=='y' && data[7]=='p') {
                if (matchBytes(data, datalen, (const unsigned char*)"heic", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"heix", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"mif1", 4, 8))
                    return {"heic", "image"};
                if (matchBytes(data, datalen, (const unsigned char*)"avif", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"avis", 4, 8))
                    return {"avif", "image"};
                if (matchBytes(data, datalen, (const unsigned char*)"isom", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"iso2", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"mp41", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"mp42", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"M4V ", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"MSNV", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"dash", 4, 8)
                 || matchBytes(data, datalen, (const unsigned char*)"mp71", 4, 8))
                    return {"mp4", "video"};
                if (matchBytes(data, datalen, (const unsigned char*)"qt  ", 4, 8))
                    return {"mov", "video"};
                if (matchBytes(data, datalen, (const unsigned char*)"3gp", 3, 8))
                    return {"3gp", "video"};
            }

            // AVI: RIFF....AVI
            if (data[0]=='R' && data[1]=='I' && data[2]=='F' && data[3]=='F'
                && data[8]=='A' && data[9]=='V' && data[10]=='I' && data[11]==' ')
                return {"avi", "video"};

            // MKV/WebM: EBML header 1A 45 DF A3
            static const unsigned char ebml[] = {0x1A,0x45,0xDF,0xA3};
            if (matchBytes(data, datalen, ebml, sizeof(ebml)))
                return {"mkv", "video"};

            // OGG: OggS
            if (data[0]=='O' && data[1]=='g' && data[2]=='g' && data[3]=='S')
                return {"ogv", "video"};

            // FLV: FLV\1
            if (data[0]=='F' && data[1]=='L' && data[2]=='V' && data[3]==0x01)
                return {"flv", "video"};

            return {};
        }
    };

    class Media : public PluginApi {
    public:
        Media(){
        }

        ~Media(){
        }

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

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

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

        bool haveSettings(){
            return true;
        }

        json_object* newAlbum(const int tid,libhttppp::HttpRequest &req,const std::string &sessionid){
            std::string _lang = getLang(*Args->database[tid]);
            std::string albumname;
            std::string selectedStore;
            bool makePublic = false;

            libhttppp::HttpForm form;
            form.parse(req);

            for(const auto &curdat : form.urlData()){
                if(curdat.key=="albumname")
                    albumname=curdat.value;
                else if(curdat.key=="store_name")
                    selectedStore=curdat.value;
                else if(curdat.key=="album_public")
                    makePublic = (curdat.value=="on" || curdat.value=="1" || curdat.value=="true");
            }

            if(!albumname.empty() && !selectedStore.empty()){
                std::string token = _getSessionToken(tid, sessionid);
                _ensureMdbReady(tid, token);
                std::string storeId = _getStoreId(tid, selectedStore);
                auto album = _mdb[tid]->create_album(storeId, albumname);
                if(makePublic){
                    _mdb[tid]->set_album_public(album.id, true);
                }
            }

            // Return a form JSON for creating new album
            json_object *jroot = json_object_new_object();
            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "action", json_object_new_string(Args->config->buildurl("settings/media").c_str()));

            json_object *jfields = json_object_new_array();

            // Store selection dropdown
            json_object *jfStore = json_object_new_object();
            json_object_object_add(jfStore, "name", json_object_new_string("store_name"));
            json_object_object_add(jfStore, "label", json_object_new_string(tr(_lang,"Store").c_str()));
            json_object_object_add(jfStore, "type", json_object_new_string("select"));
            json_object *jStoreOptions = json_object_new_array();
            for(const auto &sn : _mdbStores){
                json_object *jopt = json_object_new_object();
                json_object_object_add(jopt, "value", json_object_new_string(sn.c_str()));
                json_object_object_add(jopt, "label", json_object_new_string(sn.c_str()));
                json_object_array_add(jStoreOptions, jopt);
            }
            json_object_object_add(jfStore, "options", jStoreOptions);
            json_object_array_add(jfields, jfStore);

            json_object *jfAlbum = json_object_new_object();
            json_object_object_add(jfAlbum, "name", json_object_new_string("albumname"));
            json_object_object_add(jfAlbum, "label", json_object_new_string(tr(_lang,"Album name").c_str()));
            json_object_object_add(jfAlbum, "type", json_object_new_string("text"));
            json_object_object_add(jfAlbum, "value", json_object_new_string(""));
            json_object_array_add(jfields, jfAlbum);

            json_object *jfPublic = json_object_new_object();
            json_object_object_add(jfPublic, "name", json_object_new_string("album_public"));
            json_object_object_add(jfPublic, "label", json_object_new_string(tr(_lang,"Public").c_str()));
            json_object_object_add(jfPublic, "type", json_object_new_string("checkbox"));
            json_object_array_add(jfields, jfPublic);

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

            return jroot;
        }

        json_object* uploadMedia(const int tid,const std::string &albumId,libhttppp::HttpRequest &req,const std::string &sessionid){
            std::string _lang = getLang(*Args->database[tid]);

            std::string token = _getSessionToken(tid, sessionid);
            try {
                _ensureMdbReady(tid, token);
            } catch(std::exception &e) {
                std::cerr << "media: uploadMedia _ensureMdbReady failed: " << e.what() << std::endl;
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "message", json_object_new_string(e.what()));
                json_object_object_add(jerr, "message_type", json_object_new_string("error"));
                return jerr;
            } catch(...) {
                std::cerr << "media: uploadMedia _ensureMdbReady failed" << std::endl;
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "message", json_object_new_string(tr(_lang,"MediaDB connection failed").c_str()));
                json_object_object_add(jerr, "message_type", json_object_new_string("error"));
                return jerr;
            }

            // Fetch existing media to detect duplicates
            std::vector<mediadb::MediaInfo> mediaList;
            try {
                mediaList = _mdb[tid]->list_media(albumId);
            } catch(std::exception &e) {
                std::cerr << "media: list_media failed for album " << albumId << ": " << e.what() << std::endl;
            } catch(...) {
                std::cerr << "media: list_media failed for album " << albumId << std::endl;
            }

            std::set<std::string> existingNames;
            for(const auto &mi : mediaList)
                existingNames.insert(mi.filename);

            // Parse multipart upload
            libhttppp::HttpForm form;
            form.parse(req);

            for (const auto &curformdat : form.multipartData()) {
                std::string filename;
                for(const auto &curctdisp : curformdat.dispositions){
                    if(curctdisp.key=="filename" && !curctdisp.value.empty()){
                        filename = curctdisp.value;
                    }
                }
                if(!filename.empty() && !curformdat.value.empty()){
                    // Skip duplicate filenames
                    if(existingNames.count(filename)){
                        std::cerr << "media: skipping duplicate upload '" << filename << "' in album " << albumId << std::endl;
                        continue;
                    }
                    std::vector<std::uint8_t> payload(
                        reinterpret_cast<const std::uint8_t*>(curformdat.value.data()),
                        reinterpret_cast<const std::uint8_t*>(curformdat.value.data()) + curformdat.value.size());
                    try {
                        _mdb[tid]->upload(albumId, filename, payload);
                        existingNames.insert(filename);
                    } catch(std::exception &e) {
                        std::cerr << "media: upload failed for '" << filename << "': " << e.what() << std::endl;
                    } catch(...) {
                        std::cerr << "media: upload failed for '" << filename << "'" << std::endl;
                    }
                }
            }

            // Refresh media list after uploads
            try {
                mediaList = _mdb[tid]->list_media(albumId);
            } catch(std::exception &e) {
                std::cerr << "media: list_media refresh failed: " << e.what() << std::endl;
            } catch(...) {
                std::cerr << "media: list_media refresh failed" << std::endl;
            }

            libhtmlpp::HtmlString preview;
            preview << "<div id=\"viewmedia\"><ul>";

            for(const auto &mi : mediaList){
                std::string ext = "webp";
                size_t edot = mi.filename.rfind('.');
                if(edot != std::string::npos) ext = mi.filename.substr(edot + 1);

                if(mi.kind == "image"){
                    preview << "<li class=\"upreview\" ><a><img src=\""
                            << Args->config->buildurl("media/getimage/").c_str()
                            << mi.id.c_str() << "." << ext.c_str() << "\"></a></li>";
                } else if(mi.kind == "video"){
                    preview << "<li class=\"upreview\" ><a><video> <source src=\""
                            << Args->config->buildurl("media/getimage/").c_str()
                            << mi.id.c_str() << "." << ext.c_str() << "\""
                            << " type=\"" << mi.content_type.c_str()
                            << "\"></video></a></li>";
                }
            }

            preview << "</ul></div><br>";

            json_object *jroot = json_object_new_object();
            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"Upload Media").c_str()));
            json_object_object_add(jroot, "html", json_object_new_string(preview.c_str()));

            // Upload form
            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "enctype", json_object_new_string("multipart/form-data"));
            std::string formAction = Args->config->buildurl("settings/media/viewalbum?albumid=") + albumId;
            json_object_object_add(jform, "action", json_object_new_string(formAction.c_str()));

            json_object *jfields = json_object_new_array();

            json_object *jfHidden = json_object_new_object();
            json_object_object_add(jfHidden, "name", json_object_new_string("albumid"));
            json_object_object_add(jfHidden, "type", json_object_new_string("hidden"));
            json_object_object_add(jfHidden, "value", json_object_new_string(albumId.c_str()));
            json_object_array_add(jfields, jfHidden);

            json_object *jfFile = json_object_new_object();
            json_object_object_add(jfFile, "name", json_object_new_string("mediafile"));
            json_object_object_add(jfFile, "label", json_object_new_string(tr(_lang,"Upload Media").c_str()));
            json_object_object_add(jfFile, "type", json_object_new_string("file"));
            json_object_array_add(jfields, jfFile);

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

            return jroot;
        }

        json_object* viewAlbum(const int tid,libhttppp::HttpRequest &req,const std::string &sessionid){
            std::string _lang = getLang(*Args->database[tid]);
            std::string albumId;
            libhttppp::HttpForm form;
            form.parse(req);

            for(const auto &curdat : form.urlData()){
                if(curdat.key=="albumid")
                    albumId=curdat.value;
            }

            for (const auto &curformdat : form.multipartData()) {
                for(const auto &curctdisp : curformdat.dispositions){
                    if(curctdisp.key=="name" && curctdisp.value=="albumid"){
                        albumId.assign(curformdat.value.begin(), curformdat.value.end());
                    }
                }
            }

            if(albumId.empty()){
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "title", json_object_new_string(tr(_lang,"View media library").c_str()));
                json_object_object_add(jerr, "message", json_object_new_string(tr(_lang,"no album id found!").c_str()));
                json_object_object_add(jerr, "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/media").c_str()));
                json_object_array_add(jnav, jback);
                json_object_object_add(jerr, "nav", jnav);
                return jerr;
            }

            json_object *jroot = uploadMedia(tid,albumId,req,sessionid);

            json_object_object_add(jroot, "title", json_object_new_string(tr(_lang,"View media library").c_str()));

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

            return jroot;
        }

        json_object* settingsConfig(const int tid, libhttppp::HttpRequest &req){
            std::string _lang = getLang(*Args->database[tid]);
            bool saved = false;
            std::string action, newStoreName;

            libhttppp::HttpForm form;
            form.parse(req);

            for(const auto &cur : form.urlData()){
                if(cur.key=="action"){
                    action = cur.value;
                }else if(cur.key=="store_name"){
                    newStoreName = cur.value;
                }
            }

            if(action == "add_store" && !newStoreName.empty()){
                _addStore(tid, newStoreName);
                saved = true;
            } else if(action == "remove_store" && !newStoreName.empty()){
                _removeStore(tid, newStoreName);
                saved = true;
            }

            if(saved){
                _mdbStoreIds.clear();
                for(auto &c : _mdb) c.reset();
            }

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

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

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

            // Stores table with remove actions
            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Store Name").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Actions").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            json_object *jrows = json_object_new_array();
            for(const auto &storeName : _mdbStores){
                json_object *jrow = json_object_new_object();
                json_object *jcells = json_object_new_array();
                json_object_array_add(jcells, json_object_new_string(storeName.c_str()));
                json_object_object_add(jrow, "cells", jcells);

                json_object *jactions = json_object_new_array();
                json_object *jremove = json_object_new_object();
                json_object_object_add(jremove, "label", json_object_new_string(tr(_lang,"Remove").c_str()));
                std::string removeUrl = Args->config->buildurl("settings/media/config?action=remove_store&store_name=")
                                      + storeName;
                json_object_object_add(jremove, "url", json_object_new_string(removeUrl.c_str()));
                json_object_array_add(jactions, jremove);
                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);

            // Display MediaDB URL (read-only, from node config)
            json_object *jinfo = json_object_new_object();
            json_object_object_add(jinfo, "label", json_object_new_string(tr(_lang,"MediaDB URL").c_str()));
            json_object_object_add(jinfo, "value", json_object_new_string(_mdbUrl.c_str()));
            json_object_object_add(jroot, "info", jinfo);

            // Combined form: add store
            json_object *jform = json_object_new_object();
            json_object_object_add(jform, "method", json_object_new_string("POST"));
            json_object_object_add(jform, "action", json_object_new_string(
                Args->config->buildurl("settings/media/config").c_str()));

            json_object *jfields = json_object_new_array();

            json_object *jfAction = json_object_new_object();
            json_object_object_add(jfAction, "name", json_object_new_string("action"));
            json_object_object_add(jfAction, "type", json_object_new_string("hidden"));
            json_object_object_add(jfAction, "value", json_object_new_string("add_store"));
            json_object_array_add(jfields, jfAction);

            json_object *jfStore = json_object_new_object();
            json_object_object_add(jfStore, "name", json_object_new_string("store_name"));
            json_object_object_add(jfStore, "label", json_object_new_string(tr(_lang,"New Store Name").c_str()));
            json_object_object_add(jfStore, "type", json_object_new_string("text"));
            json_object_object_add(jfStore, "value", json_object_new_string(""));
            json_object_array_add(jfields, jfStore);

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

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

            libhttppp::HttpForm form;
            form.parse(req);

            std::string action;
            int w = 0, h = 0;
            for(const auto &cur : form.urlData()){
                if(cur.key == "action") action = cur.value;
                else if(cur.key == "width"){ try { w = std::stoi(cur.value); } catch(...) {} }
                else if(cur.key == "height"){ try { h = std::stoi(cur.value); } catch(...) {} }
            }

            bool saved = false;
            if(action == "add" && w > 0 && h > 0){
                _addAllowedSize(tid, w, h);
                saved = true;
            } else if(action == "remove" && w > 0 && h > 0){
                _removeAllowedSize(tid, w, h);
                saved = true;
            }

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

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

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

            // Table of allowed sizes
            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Width").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Height").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Actions").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            json_object *jrows = json_object_new_array();
            {
                std::lock_guard<std::mutex> lock(_sizesMutex);
                for(const auto &s : _allowedSizes){
                    json_object *jrow = json_object_new_object();
                    json_object *jcells = json_object_new_array();
                    json_object_array_add(jcells, json_object_new_string(std::to_string(s.w).c_str()));
                    json_object_array_add(jcells, json_object_new_string(std::to_string(s.h).c_str()));
                    json_object_object_add(jrow, "cells", jcells);

                    json_object *jactions = json_object_new_array();
                    json_object *jremove = json_object_new_object();
                    json_object_object_add(jremove, "label", json_object_new_string(tr(_lang,"Remove").c_str()));
                    std::string removeUrl = Args->config->buildurl("settings/media/previews?action=remove&width=")
                                          + std::to_string(s.w) + "&height=" + std::to_string(s.h);
                    json_object_object_add(jremove, "url", json_object_new_string(removeUrl.c_str()));
                    json_object_array_add(jactions, jremove);
                    json_object_object_add(jrow, "actions", jactions);

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

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

            json_object *jfields = json_object_new_array();

            json_object *jfAction = json_object_new_object();
            json_object_object_add(jfAction, "name", json_object_new_string("action"));
            json_object_object_add(jfAction, "type", json_object_new_string("hidden"));
            json_object_object_add(jfAction, "value", json_object_new_string("add"));
            json_object_array_add(jfields, jfAction);

            json_object *jfW = json_object_new_object();
            json_object_object_add(jfW, "name", json_object_new_string("width"));
            json_object_object_add(jfW, "label", json_object_new_string(tr(_lang,"Width (px)").c_str()));
            json_object_object_add(jfW, "type", json_object_new_string("number"));
            json_object_object_add(jfW, "value", json_object_new_string(""));
            json_object_array_add(jfields, jfW);

            json_object *jfH = json_object_new_object();
            json_object_object_add(jfH, "name", json_object_new_string("height"));
            json_object_object_add(jfH, "label", json_object_new_string(tr(_lang,"Height (px)").c_str()));
            json_object_object_add(jfH, "type", json_object_new_string("number"));
            json_object_object_add(jfH, "value", json_object_new_string(""));
            json_object_array_add(jfields, jfH);

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

            return jroot;
        }

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

            std::string suburl;

            size_t pos=Args->config->buildurl("settings/media/").length();

            if(req.getRequestURL().length()>pos ){
                suburl=req.getRequestURL().substr(pos, req.getRequestURL().length()-pos);
                size_t delimter_url=suburl.find("?");
                if(delimter_url!=std::string::npos){
                    suburl=suburl.substr(0,delimter_url);
                }
            }

            if(suburl=="viewalbum"){
                return viewAlbum(tid,req,sessionid);
            }

            if(suburl=="delalbum"){
                std::string albumId;
                libhttppp::HttpForm form;
                form.parse(req);
                for(const auto &curdat : form.urlData()){
                    if(curdat.key=="albumid")
                        albumId=curdat.value;
                }
                if(!albumId.empty()){
                    try {
                        std::string _sessionToken = _getSessionToken(tid, sessionid);
                        _ensureMdbReady(tid, _sessionToken);
                        _mdb[tid]->delete_album(albumId);
                    } catch(std::exception &e) {
                        std::cerr << "media: delalbum failed: " << e.what() << std::endl;
                    } catch(...) {
                        std::cerr << "media: delalbum failed" << std::endl;
                    }
                }
            }

            if(suburl=="togglepublic"){
                std::string albumId;
                libhttppp::HttpForm form;
                form.parse(req);
                for(const auto &curdat : form.urlData()){
                    if(curdat.key=="albumid")
                        albumId=curdat.value;
                }
                if(!albumId.empty()){
                    try {
                        std::string _sessionToken = _getSessionToken(tid, sessionid);
                        _ensureMdbReady(tid, _sessionToken);
                        auto album = _mdb[tid]->get_album(albumId);
                        _mdb[tid]->set_album_public(albumId, !album.is_public);
                    } catch(std::exception &e) {
                        std::cerr << "media: togglepublic failed: " << e.what() << std::endl;
                    } catch(...) {
                        std::cerr << "media: togglepublic failed" << std::endl;
                    }
                }
            }

            if(suburl=="config"){
                return settingsConfig(tid,req);
            }

            if(suburl=="previews"){
                return settingsPreviews(tid,req);
            }

            std::string _sessionToken = _getSessionToken(tid, sessionid);

            try {
                _ensureMdbReady(tid, _sessionToken);
            } catch(std::exception &e) {
                std::cerr << "media plugin: mediadb connection failed: " << e.what() << std::endl;
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "title", json_object_new_string(tr(_lang,"media library").c_str()));
                json_object_object_add(jerr, "message", json_object_new_string(e.what()));
                json_object_object_add(jerr, "message_type", json_object_new_string("error"));
                // Add config link so user can fix settings
                json_object *jnav = json_object_new_array();
                json_object *jcfg = json_object_new_object();
                json_object_object_add(jcfg, "label", json_object_new_string(tr(_lang,"Configuration").c_str()));
                json_object_object_add(jcfg, "url", json_object_new_string(Args->config->buildurl("settings/media/config").c_str()));
                json_object_array_add(jnav, jcfg);
                json_object_object_add(jerr, "nav", jnav);
                return jerr;
            } catch(libhttppp::HTTPException &e) {
                std::cerr << "media plugin: mediadb connection failed: " << e.what() << std::endl;
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "title", json_object_new_string(tr(_lang,"media library").c_str()));
                json_object_object_add(jerr, "message", json_object_new_string(e.what().c_str()));
                json_object_object_add(jerr, "message_type", json_object_new_string("error"));
                json_object *jnav = json_object_new_array();
                json_object *jcfg = json_object_new_object();
                json_object_object_add(jcfg, "label", json_object_new_string(tr(_lang,"Configuration").c_str()));
                json_object_object_add(jcfg, "url", json_object_new_string(Args->config->buildurl("settings/media/config").c_str()));
                json_object_array_add(jnav, jcfg);
                json_object_object_add(jerr, "nav", jnav);
                return jerr;
            } catch(...) {
                std::cerr << "media plugin: mediadb connection failed (unknown exception)" << std::endl;
                json_object *jerr = json_object_new_object();
                json_object_object_add(jerr, "title", json_object_new_string(tr(_lang,"media library").c_str()));
                json_object_object_add(jerr, "message", json_object_new_string(tr(_lang,"MediaDB connection failed").c_str()));
                json_object_object_add(jerr, "message_type", json_object_new_string("error"));
                json_object *jnav = json_object_new_array();
                json_object *jcfg = json_object_new_object();
                json_object_object_add(jcfg, "label", json_object_new_string(tr(_lang,"Configuration").c_str()));
                json_object_object_add(jcfg, "url", json_object_new_string(Args->config->buildurl("settings/media/config").c_str()));
                json_object_array_add(jnav, jcfg);
                json_object_object_add(jerr, "nav", jnav);
                return jerr;
            }

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

            json_object *jnav = json_object_new_array();
            json_object *jcfg = json_object_new_object();
            json_object_object_add(jcfg, "label", json_object_new_string(tr(_lang,"Configuration").c_str()));
            json_object_object_add(jcfg, "url", json_object_new_string(Args->config->buildurl("settings/media/config").c_str()));
            json_object_array_add(jnav, jcfg);

            json_object *jprev = json_object_new_object();
            json_object_object_add(jprev, "label", json_object_new_string(tr(_lang,"Preview Sizes").c_str()));
            json_object_object_add(jprev, "url", json_object_new_string(Args->config->buildurl("settings/media/previews").c_str()));
            json_object_array_add(jnav, jprev);

            json_object_object_add(jroot, "nav", jnav);

            // Albums table — list albums from all stores
            json_object *jtable = json_object_new_object();
            json_object *jheaders = json_object_new_array();
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Store").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Album name").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Media count").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Visibility").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Created").c_str()));
            json_object_array_add(jheaders, json_object_new_string(tr(_lang,"Actions").c_str()));
            json_object_object_add(jtable, "headers", jheaders);

            json_object *jrows = json_object_new_array();
            for(const auto &storeName : _mdbStores){
                std::string storeId;
                std::vector<mediadb::AlbumInfo> albums;
                try {
                    storeId = _getStoreId(tid, storeName);
                    albums = _mdb[tid]->list_albums(storeId);
                } catch(std::exception &e) {
                    std::cerr << "media plugin: list_albums failed for store '" << storeName << "': " << e.what() << std::endl;
                    continue;
                } catch(...) {
                    std::cerr << "media plugin: list_albums failed for store '" << storeName << "' (unknown exception)" << std::endl;
                    continue;
                }

                for(const auto &album : albums){
                    json_object *jrow = json_object_new_object();
                    json_object *jcells = json_object_new_array();
                    json_object_array_add(jcells, json_object_new_string(storeName.c_str()));
                    json_object_array_add(jcells, json_object_new_string(album.name.c_str()));
                    json_object_array_add(jcells, json_object_new_string(std::to_string(album.media_count).c_str()));
                    json_object_array_add(jcells, json_object_new_string(
                        album.is_public ? tr(_lang,"Public").c_str() : tr(_lang,"Private").c_str()));
                    json_object_array_add(jcells, json_object_new_string(album.created_at.c_str()));
                    json_object_object_add(jrow, "cells", jcells);

                    json_object *jactions = json_object_new_array();

                    json_object *jview = json_object_new_object();
                    json_object_object_add(jview, "label", json_object_new_string(tr(_lang,"View").c_str()));
                    std::string viewUrl = Args->config->buildurl("settings/media/viewalbum?") + "albumid=" + album.id;
                    json_object_object_add(jview, "url", json_object_new_string(viewUrl.c_str()));
                    json_object_array_add(jactions, jview);

                    json_object *jtoggle = json_object_new_object();
                    json_object_object_add(jtoggle, "label", json_object_new_string(
                        album.is_public ? tr(_lang,"Make Private").c_str() : tr(_lang,"Make Public").c_str()));
                    std::string toggleUrl = Args->config->buildurl("settings/media/togglepublic?") + "albumid=" + album.id;
                    json_object_object_add(jtoggle, "url", json_object_new_string(toggleUrl.c_str()));
                    json_object_array_add(jactions, jtoggle);

                    json_object *jremove = json_object_new_object();
                    json_object_object_add(jremove, "label", json_object_new_string(tr(_lang,"Remove").c_str()));
                    std::string removeUrl = Args->config->buildurl("settings/media/delalbum?") + "albumid=" + album.id;
                    json_object_object_add(jremove, "url", json_object_new_string(removeUrl.c_str()));
                    json_object_array_add(jactions, jremove);

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

            // New album form
            json_object *jnewAlbum = newAlbum(tid, req, sessionid);
            json_object *jnewForm = nullptr;
            if(json_object_object_get_ex(jnewAlbum, "form", &jnewForm)){
                json_object_get(jnewForm);
                json_object_object_add(jroot, "form", jnewForm);
            }
            json_object_put(jnewAlbum);

            return jroot;
        }

        void initPlugin(){
            _mdbUrl = Args->config->getMediaDBUrl();

            _loadStores(0);

            _mdb.resize(Args->maxthreads);
            _mdbHttpUrl = std::make_unique<libhttppp::HttpUrl>(_mdbUrl);
            _mdbClientPool.resize(Args->maxthreads);
            _loadAllowedSizes(0);
        }

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

            if (req.getRequestURL().compare(0,Args->config->buildurl("media/getimage/").length(),
                Args->config->buildurl("media/getimage/"))==0){

                size_t plen=Args->config->buildurl("media/getimage/").length();
                size_t mlen=req.getRequestURL().length();

                if( mlen <= plen )
                    return true;

                // Extract media ID and extension from URL path
                // Format: <uuid>.<ext>  with query params ?w=<W>&h=<H>
                std::string mediaId = req.getRequestURL().substr(plen);
                std::string fmt;
                int reqWidth = 0, reqHeight = 0;
                size_t dotPos = mediaId.rfind('.');
                if(dotPos != std::string::npos){
                    fmt = mediaId.substr(dotPos + 1);
                    mediaId = mediaId.substr(0, dotPos);
                }

                // Parse w= and h= from query string via HttpForm
                {
                    libhttppp::HttpForm qform;
                    qform.parse(req);
                    for(const auto &qp : qform.urlData()){
                        std::cerr << "media: query param key='" << qp.key << "' value='" << qp.value << "'" << std::endl;
                        if(qp.key == "w"){ try { reqWidth = std::stoi(qp.value); } catch(...) {} }
                        else if(qp.key == "h"){ try { reqHeight = std::stoi(qp.value); } catch(...) {} }
                    }
                }
                std::cerr << "media: mediaId=" << mediaId << " fmt=" << fmt
                          << " reqWidth=" << reqWidth << " reqHeight=" << reqHeight << std::endl;

                // Validate against allowed preview sizes
                if((reqWidth > 0 || reqHeight > 0) && !_isSizeAllowed(reqWidth, reqHeight)){
                    libhttppp::HttpResponse curres;
                    curres.setState(HTTP403);
                    curres.send(req, "", 0);
                    return true;
                }

                std::string _sessionToken = _getSessionToken(tid, sessionid);
                _ensureMdbReady(tid, _sessionToken);

                // Look up media info from mediadb
                mediadb::MediaInfo mi;
                try {
                    mi = _mdb[tid]->get_media(mediaId);
                } catch(std::exception &e) {
                    std::cerr << "media: get_media failed for " << mediaId << ": " << e.what() << std::endl;
                    libhttppp::HttpResponse curres;
                    curres.setState(HTTP404);
                    curres.send(req,"",0);
                    return true;
                } catch(...) {
                    std::cerr << "media: get_media failed for " << mediaId << ": unknown error" << std::endl;
                    libhttppp::HttpResponse curres;
                    curres.setState(HTTP404);
                    curres.send(req,"",0);
                    return true;
                }

                std::cerr << "media: kind=" << mi.kind << " content_type=" << mi.content_type << std::endl;

                bool isVideo = (mi.kind == "video")
                            || (mi.content_type.compare(0, 6, "video/") == 0);

                // Parse Range header
                bool hasRange = false;
                size_t rangeStart = 0, rangeEnd = 0;
                {
                    libhttppp::HttpHeader::HeaderData *rangeHdr = req.getHeaderData("Range");
                    if (rangeHdr && rangeHdr->getfirstValue()) {
                        hasRange = true;
                        std::string rangeVal = rangeHdr->getfirstValue()->getvalue();
                        if (rangeVal.compare(0, 6, "bytes=") == 0) {
                            std::string range = rangeVal.substr(6);
                            size_t dashPos = range.find('-');
                            if (dashPos != std::string::npos) {
                                rangeStart = std::stoull(range.substr(0, dashPos));
                                if (dashPos + 1 < range.size())
                                    rangeEnd = std::stoull(range.substr(dashPos + 1));
                            }
                        }
                    }
                }

                // Stream from mediadb via GetStream
                // Videos use /raw endpoint; images use /preview with optional resize params
                std::string rawUrl;
                if (isVideo) {
                    rawUrl = "/media/" + mediaId + "/raw";
                } else {
                    rawUrl = "/media/" + mediaId + "/preview";
                    std::string sep = "?";
                    if(!fmt.empty()){ rawUrl += sep + "fmt=" + fmt; sep = "&"; }
                    if(reqWidth > 0){ rawUrl += sep + "w=" + std::to_string(reqWidth); sep = "&"; }
                    if(reqHeight > 0){ rawUrl += sep + "h=" + std::to_string(reqHeight); sep = "&"; }
                }
                std::cerr << "media: upstream rawUrl=" << rawUrl << std::endl;
                libhttppp::HttpRequest dataReq;
                dataReq.setRequestURL(rawUrl);
                if (!_sessionToken.empty())
                    dataReq.setHeaderData("Authorization")->push_back("Bearer " + _sessionToken);

                if (hasRange) {
                    std::string upstreamRange = "bytes=" + std::to_string(rangeStart) + "-";
                    if (rangeEnd > 0)
                        upstreamRange += std::to_string(rangeEnd);
                    dataReq.setHeaderData("Range")->push_back(upstreamRange);
                }

                auto streamClient = _acquireMdbClient(tid);
                std::unique_ptr<libhttppp::HttpResponse> dataResPtr;
                try {
                    dataResPtr = std::make_unique<libhttppp::HttpResponse>(streamClient->GetStream(dataReq));
                } catch(...) {
                    // Pooled connection may be stale — retry with a fresh one
                    std::cerr << "media: upstream GetStream failed, retrying with fresh connection" << std::endl;
                    streamClient = std::make_unique<libhttppp::HttpClient>(*_mdbHttpUrl, true);
                    try {
                        dataResPtr = std::make_unique<libhttppp::HttpResponse>(streamClient->GetStream(dataReq));
                    } catch(std::exception &e2) {
                        std::cerr << "media: upstream retry failed: " << e2.what() << std::endl;
                    } catch(...) {
                        std::cerr << "media: upstream retry failed: unknown error" << std::endl;
                    }
                }

                if (!dataResPtr) {
                    libhttppp::HttpResponse curres;
                    curres.setState(HTTP502);
                    curres.send(req, "", 0);
                    return true;
                }

                libhttppp::HttpResponse &dataRes = *dataResPtr;
                int dataStatus = dataRes.getStatusCode();
                size_t dataContentLen = dataRes.getContentLength();

                size_t totalSize = 0;
                size_t sendSize = 0;

                libhttppp::HttpHeader::HeaderData *crHdr = dataRes.getHeaderData("Content-Range");
                if (crHdr && crHdr->getfirstValue()) {
                    std::string crVal = crHdr->getfirstValue()->getvalue();
                    size_t slashPos = crVal.find('/');
                    if (slashPos != std::string::npos && slashPos + 1 < crVal.size())
                        totalSize = std::stoull(crVal.substr(slashPos + 1));
                    size_t bytesPos = crVal.find("bytes ");
                    if (bytesPos != std::string::npos) {
                        std::string rangePart = crVal.substr(bytesPos + 6, slashPos - bytesPos - 6);
                        size_t dashPos = rangePart.find('-');
                        if (dashPos != std::string::npos) {
                            rangeStart = std::stoull(rangePart.substr(0, dashPos));
                            rangeEnd = std::stoull(rangePart.substr(dashPos + 1));
                        }
                    }
                    sendSize = dataContentLen > 0 ? dataContentLen : (rangeEnd - rangeStart + 1);
                } else {
                    totalSize = dataContentLen;
                    sendSize = dataContentLen;
                    if (hasRange && totalSize > 0) {
                        if (rangeEnd == 0)
                            rangeEnd = totalSize - 1;
                        sendSize = rangeEnd - rangeStart + 1;
                    }
                }

                if (totalSize == 0 || sendSize == 0) {
                    char drain[4096];
                    size_t n;
                    while ((n = streamClient->readBodyChunkNonBlocking(drain, sizeof(drain))) != 0
                           && n != static_cast<size_t>(-1)) {}
                    libhttppp::HttpResponse curres;
                    curres.setState(HTTP404);
                    curres.send(req,"",0);
                    return true;
                }

                // Store streaming state per-request
                req.RecvData.pos = 0;
                _storeStreamState(req, std::move(streamClient), sendSize, tid);

                libhttppp::HttpResponse curres;
                // Set content type based on requested format
                if(fmt == "webp") curres.setContentType("image/webp");
                else if(fmt == "png") curres.setContentType("image/png");
                else if(fmt == "jpeg" || fmt == "jpg") curres.setContentType("image/jpeg");
                else if(fmt == "avif") curres.setContentType("image/avif");
                else curres.setContentType(mi.content_type.c_str());
                curres.setHeaderData("Accept-Ranges")->push_back("bytes");
                curres.setHeaderData("Cache-Control")->push_back("public, max-age=86400");

                if (hasRange || dataStatus == 206) {
                    std::string crHeader = "bytes " + std::to_string(rangeStart) + "-"
                                         + std::to_string(rangeEnd) + "/" + std::to_string(totalSize);
                    curres.setHeaderData("Content-Range")->push_back(crHeader);
                    curres.setState(HTTP206);
                } else {
                    curres.setState(HTTP200);
                }

                curres.setContentLength(sendSize);
                curres.send(req, "", -1);
                return true;
            }

            if (req.getRequestURL().compare(0,Args->config->buildurl("media/upload").length(),
                Args->config->buildurl("media/upload"))==0){

                if(req.getRequestType()==PUTREQUEST){
                    // Raw PUT upload: body is the file bytes, metadata via headers
                    const char* rawData = req.RecvData.data();
                    size_t rawSize = req.RecvData.size();

                    if (rawSize < 12) {
                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP400);
                        curres.setContentType("application/json");
                        const char *err = "{\"status\":\"error\",\"message\":\"Upload body too small\"}";
                        curres.send(req, err, strlen(err));
                        return true;
                    }

                    // Detect media type from magic bytes
                    MediaType mt = MediaDetect::DetectMediaType(
                                    (const unsigned char*)rawData, rawSize);
                    if (mt.ext.empty()) {
                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP400);
                        curres.setContentType("application/json");
                        const char *err = "{\"status\":\"error\",\"message\":\"Upload format not supported\"}";
                        curres.send(req, err, strlen(err));
                        return true;
                    }

                    // Get album ID from X-Album-Id header
                    std::string albumId;
                    {
                        libhttppp::HttpHeader::HeaderData *hdr = req.getHeaderData("X-Album-Id");
                        if(hdr && hdr->getfirstValue())
                            albumId = hdr->getfirstValue()->getvalue();
                    }

                    if(albumId.empty()){
                        // Parse from query string
                        libhttppp::HttpForm qform;
                        qform.parse(req);
                        for(const auto &qp : qform.urlData()){
                            if(qp.key == "albumid")
                                albumId = qp.value;
                        }
                    }

                    if(albumId.empty()){
                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP400);
                        curres.setContentType("application/json");
                        const char *err = "{\"status\":\"error\",\"message\":\"X-Album-Id header or albumid query param required\"}";
                        curres.send(req, err, strlen(err));
                        return true;
                    }

                    // Generate UUID filename like bolli
                    uuid::uuid mediauid;
                    mediauid.generate();
                    const std::string filename = std::string(mediauid.c_str()) + "." + mt.ext;

                    std::string _sessionToken = _getSessionToken(tid, sessionid);
                    _ensureMdbReady(tid, _sessionToken);

                    std::vector<std::uint8_t> payload(
                        reinterpret_cast<const std::uint8_t*>(rawData),
                        reinterpret_cast<const std::uint8_t*>(rawData) + rawSize);

                    try {
                        auto uploadInfo = _mdb[tid]->upload(albumId, filename, payload);

                        // Build JSON response with upload result
                        json_object *jroot = json_object_new_object();
                        json_object_object_add(jroot, "status", json_object_new_string("success"));
                        json_object *jm = json_object_new_object();
                        json_object_object_add(jm, "id", json_object_new_string(uploadInfo.id.c_str()));
                        json_object_object_add(jm, "filename", json_object_new_string(uploadInfo.filename.c_str()));
                        json_object_object_add(jm, "kind", json_object_new_string(uploadInfo.kind.c_str()));
                        json_object_object_add(jm, "content_type", json_object_new_string(uploadInfo.content_type.c_str()));
                        std::string ext = mt.ext;
                        std::string url = Args->config->buildurl("media/getimage/") + uploadInfo.id + "." + ext;
                        json_object_object_add(jm, "url", json_object_new_string(url.c_str()));
                        json_object_object_add(jroot, "media", jm);

                        const char *jsonStr = json_object_to_json_string_ext(jroot, JSON_C_TO_STRING_NOSLASHESCAPE);
                        size_t jsonLen = strlen(jsonStr);

                        // Store response for streaming via ResponseEvent
                        _storeUploadResponse(req, jsonStr, jsonLen);

                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP200);
                        curres.setContentType("application/json");
                        curres.setContentLength(jsonLen);
                        curres.send(req, "", -1);

                        json_object_put(jroot);
                    } catch(std::exception &e) {
                        std::cerr << "media: upload failed: " << e.what() << std::endl;
                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP502);
                        curres.setContentType("application/json");
                        json_object *jerr = json_object_new_object();
                        json_object_object_add(jerr, "status", json_object_new_string("error"));
                        json_object_object_add(jerr, "message", json_object_new_string(e.what()));
                        const char *errStr = json_object_to_json_string_ext(jerr, JSON_C_TO_STRING_NOSLASHESCAPE);
                        curres.send(req, errStr, strlen(errStr));
                        json_object_put(jerr);
                    } catch(...) {
                        std::cerr << "media: upload failed: unknown error" << std::endl;
                        libhttppp::HttpResponse curres;
                        curres.setState(HTTP502);
                        curres.setContentType("application/json");
                        const char *err = "{\"status\":\"error\",\"message\":\"Unknown upload error\"}";
                        curres.send(req, err, strlen(err));
                    }
                    return true;
                }

                // GET: return empty 200 (upload form is handled by frontend)
                libhttppp::HttpResponse curres;
                curres.setState(HTTP200);
                curres.setContentType("text/html");
                curres.send(req, "", 0);
                return true;
            }

            return false;
        }

        bool Response(const int tid,libhttppp::HttpRequest &req){
            // Stream upload response
            if (req.getRequestURL().compare(0,Args->config->buildurl("media/upload").length(),
                Args->config->buildurl("media/upload"))==0){

                auto *ur = _getUploadResponse(req);
                if(!ur || ur->data.empty())
                    return false;

                if(ur->produced < ur->data.size()){
                    size_t remaining = ur->data.size() - ur->produced;
                    size_t toSend = remaining < BLOCKSIZE ? remaining : BLOCKSIZE;
                    req.SendData.append(ur->data.data() + ur->produced, toSend);
                    ur->produced += toSend;
                }

                if(ur->produced >= ur->data.size()){
                    _removeUploadResponse(req);
                    return false;
                }
                return true;
            }

            // Stream getimage response
            if (req.getRequestURL().compare(0,Args->config->buildurl("media/getimage/").length(),
                Args->config->buildurl("media/getimage/"))==0){

                auto *ss = _getStreamState(req);
                if(!ss || !ss->client)
                    return false;

                if(ss->produced < ss->sendSize){
                    static constexpr size_t STREAM_CHUNK = 65536;
                    static constexpr size_t MAX_FILL = STREAM_CHUNK * 32;
                    std::vector<char> chunk(STREAM_CHUNK);
                    size_t filled = 0;
                    while(filled < MAX_FILL && ss->produced < ss->sendSize){
                        const size_t remaining = ss->sendSize - ss->produced;
                        const size_t toRead = remaining < STREAM_CHUNK ? remaining : STREAM_CHUNK;
                        size_t n;
                        try {
                            n = ss->client->readBodyChunkNonBlocking(chunk.data(), toRead);
                        } catch(...) {
                            n = static_cast<size_t>(-1);
                        }
                        if(n == static_cast<size_t>(-1)){
                            ss->client.reset();
                            ss->sendSize = ss->produced;
                            return false;
                        }
                        if(n > 0){
                            if(n > remaining) n = remaining;
                            req.SendData.append(chunk.data(), n);
                            ss->produced += n;
                            filled += n;
                        } else {
                            if(filled == 0)
                                req.slots[0].ResponsePending.store(true, std::memory_order_release);
                            break;
                        }
                    }
                }

                if(ss->produced >= ss->sendSize){
                    ss->client.reset();
                    return false;
                }
                return true;
            }
            return false;
        }

        void GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std::chrono::year_month_day> &sitemap){
            return;
        }

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

            std::string token;
            const char *authStr = authid.c_str();
            if(authStr && authStr[0] != '\0')
                token = authStr;

            if(strcmp(command, "media_list_albums") == 0){
                try {
                    _ensureMdbReady(tid, token);
                    json_object *jarr = json_object_new_array();

                    // Optional store_name filter; if absent, list from all stores
                    json_object *jsn = nullptr;
                    std::vector<std::string> storesToQuery;
                    if(json_object_object_get_ex(request, "store_name", &jsn)){
                        const char *sn = json_object_get_string(jsn);
                        if(sn && sn[0] != '\0') storesToQuery.push_back(sn);
                    }
                    if(storesToQuery.empty())
                        storesToQuery = _mdbStores;

                    for(const auto &storeName : storesToQuery){
                        std::string storeId = _getStoreId(tid, storeName);
                        auto albums = _mdb[tid]->list_albums(storeId);
                        for(const auto &a : albums){
                            json_object *jal = json_object_new_object();
                            json_object_object_add(jal, "id", json_object_new_string(a.id.c_str()));
                            json_object_object_add(jal, "name", json_object_new_string(a.name.c_str()));
                            json_object_object_add(jal, "store", json_object_new_string(storeName.c_str()));
                            json_object_object_add(jal, "media_count", json_object_new_int(a.media_count));
                            json_object_array_add(jarr, jal);
                        }
                    }
                    json_object_object_add(response, "status", json_object_new_string("success"));
                    json_object_object_add(response, "albums", jarr);
                } 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("Unknown error"));
                }
            } else if(strcmp(command, "media_list_media") == 0){
                json_object *jaid = nullptr;
                if(!json_object_object_get_ex(request, "album_id", &jaid)){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("album_id required"));
                    return;
                }
                const char *albumId = json_object_get_string(jaid);
                try {
                    _ensureMdbReady(tid, token);
                    auto mediaList = _mdb[tid]->list_media(albumId);
                    json_object *jarr = json_object_new_array();
                    for(const auto &mi : mediaList){
                        json_object *jm = json_object_new_object();
                        json_object_object_add(jm, "id", json_object_new_string(mi.id.c_str()));
                        json_object_object_add(jm, "filename", json_object_new_string(mi.filename.c_str()));
                        json_object_object_add(jm, "kind", json_object_new_string(mi.kind.c_str()));
                        json_object_object_add(jm, "content_type", json_object_new_string(mi.content_type.c_str()));
                        std::string ext = "webp";
                        size_t dp = mi.filename.rfind('.');
                        if(dp != std::string::npos) ext = mi.filename.substr(dp + 1);
                        std::string url = Args->config->buildurl("media/getimage/") + mi.id + "." + ext;
                        json_object_object_add(jm, "url", json_object_new_string(url.c_str()));
                        json_object_array_add(jarr, jm);
                    }
                    json_object_object_add(response, "status", json_object_new_string("success"));
                    json_object_object_add(response, "media", jarr);
                } 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("Unknown error"));
                }
            } else if(strcmp(command, "media_create_preview") == 0){
                json_object *jmid = nullptr, *jw = nullptr, *jh = nullptr;
                if(!json_object_object_get_ex(request, "media_id", &jmid)){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("media_id required"));
                    return;
                }
                const char *mediaId = json_object_get_string(jmid);
                int width = 0, height = 0;
                if(json_object_object_get_ex(request, "width", &jw)) width = json_object_get_int(jw);
                if(json_object_object_get_ex(request, "height", &jh)) height = json_object_get_int(jh);
                if(width <= 0 || height <= 0){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("width and height must be > 0"));
                    return;
                }
                try {
                    _ensureMdbReady(tid, token);
                    auto mi = _mdb[tid]->get_media(mediaId);
                    std::string ext = "webp";
                    size_t dp = mi.filename.rfind('.');
                    if(dp != std::string::npos) ext = mi.filename.substr(dp + 1);
                    mediadb::PreviewOptions opts;
                    opts.width = width;
                    opts.height = height;
                    opts.fmt = ext;
                    std::string ct;
                    _mdb[tid]->get_preview(mediaId, opts, &ct);
                    _addAllowedSize(tid, width, height);
                    std::string url = Args->config->buildurl("media/getimage/") + mi.id + "." + ext
                                     + "?w=" + std::to_string(width) + "&h=" + std::to_string(height);
                    json_object_object_add(response, "status", json_object_new_string("success"));
                    json_object_object_add(response, "url", json_object_new_string(url.c_str()));
                    json_object_object_add(response, "width", json_object_new_int(width));
                    json_object_object_add(response, "height", json_object_new_int(height));
                } 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("Unknown error"));
                }
            } else if(strcmp(command, "media_get_allowed_sizes") == 0){
                json_object *jarr = json_object_new_array();
                {
                    std::lock_guard<std::mutex> lock(_sizesMutex);
                    for(const auto &s : _allowedSizes){
                        json_object *item = json_object_new_object();
                        json_object_object_add(item, "w", json_object_new_int(s.w));
                        json_object_object_add(item, "h", json_object_new_int(s.h));
                        json_object_array_add(jarr, item);
                    }
                }
                json_object_object_add(response, "status", json_object_new_string("success"));
                json_object_object_add(response, "sizes", jarr);
            } else if(strcmp(command, "media_remove_size") == 0){
                json_object *jw = nullptr, *jh = nullptr;
                int w = 0, h = 0;
                if(json_object_object_get_ex(request, "width", &jw)) w = json_object_get_int(jw);
                if(json_object_object_get_ex(request, "height", &jh)) h = json_object_get_int(jh);
                if(_removeAllowedSize(tid, w, h)){
                    json_object_object_add(response, "status", json_object_new_string("success"));
                } else {
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("Size not found"));
                }
            } else if(strcmp(command, "media_list_stores") == 0){
                json_object *jarr = json_object_new_array();
                for(const auto &s : _mdbStores){
                    json_object_array_add(jarr, json_object_new_string(s.c_str()));
                }
                json_object_object_add(response, "status", json_object_new_string("success"));
                json_object_object_add(response, "stores", jarr);
            } else if(strcmp(command, "media_create_album") == 0){
                json_object *jsn = nullptr, *jan = nullptr;
                if(!json_object_object_get_ex(request, "store_name", &jsn) ||
                   !json_object_object_get_ex(request, "album_name", &jan)){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("store_name and album_name required"));
                    return;
                }
                const char *storeName = json_object_get_string(jsn);
                const char *albumName = json_object_get_string(jan);
                if(!storeName || !albumName || storeName[0]=='\0' || albumName[0]=='\0'){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("store_name and album_name must not be empty"));
                    return;
                }
                try {
                    _ensureMdbReady(tid, token);
                    std::string storeId = _getStoreId(tid, storeName);
                    auto album = _mdb[tid]->create_album(storeId, albumName);
                    json_object *jpub = nullptr;
                    if(json_object_object_get_ex(request, "is_public", &jpub) && json_object_get_boolean(jpub)){
                        _mdb[tid]->set_album_public(album.id, true);
                        album.is_public = true;
                    }
                    json_object_object_add(response, "status", json_object_new_string("success"));
                    json_object *jal = json_object_new_object();
                    json_object_object_add(jal, "id", json_object_new_string(album.id.c_str()));
                    json_object_object_add(jal, "name", json_object_new_string(album.name.c_str()));
                    json_object_object_add(jal, "store", json_object_new_string(storeName));
                    json_object_object_add(jal, "media_count", json_object_new_int(album.media_count));
                    json_object_object_add(jal, "is_public", json_object_new_boolean(album.is_public));
                    json_object_object_add(response, "album", jal);
                } 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("Unknown error"));
                }
            } else if(strcmp(command, "media_upload") == 0){
                json_object *jaid = nullptr, *jfn = nullptr, *jdata = nullptr;
                if(!json_object_object_get_ex(request, "album_id", &jaid) ||
                   !json_object_object_get_ex(request, "filename", &jfn) ||
                   !json_object_object_get_ex(request, "data", &jdata)){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("album_id, filename, and data required"));
                    return;
                }
                const char *albumId = json_object_get_string(jaid);
                const char *filename = json_object_get_string(jfn);
                const char *b64data = json_object_get_string(jdata);
                if(!albumId || !filename || !b64data){
                    json_object_object_add(response, "status", json_object_new_string("error"));
                    json_object_object_add(response, "message", json_object_new_string("invalid parameters"));
                    return;
                }
                try {
                    // Base64 decode
                    static const std::string chars =
                        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
                    int T[256];
                    std::fill(T, T + 256, -1);
                    for(int i = 0; i < 64; i++) T[(unsigned char)chars[i]] = i;
                    std::vector<std::uint8_t> payload;
                    std::string encoded(b64data);
                    // Strip data URI prefix if present
                    size_t commaPos = encoded.find(',');
                    if(commaPos != std::string::npos && commaPos < 100)
                        encoded = encoded.substr(commaPos + 1);
                    payload.reserve(encoded.size() * 3 / 4);
                    int val = 0, valb = -8;
                    for(unsigned char c : encoded){
                        if(T[c] == -1) continue;
                        val = (val << 6) + T[c];
                        valb += 6;
                        if(valb >= 0){ payload.push_back(static_cast<std::uint8_t>((val >> valb) & 0xFF)); valb -= 8; }
                    }

                    _ensureMdbReady(tid, token);

                    // Check for duplicate filename in album
                    auto existingMedia = _mdb[tid]->list_media(albumId);
                    for(const auto &em : existingMedia){
                        if(em.filename == filename){
                            json_object_object_add(response, "status", json_object_new_string("error"));
                            json_object_object_add(response, "message", json_object_new_string("File with this name already exists in album"));
                            return;
                        }
                    }

                    auto mi = _mdb[tid]->upload(albumId, filename, payload);
                    std::string ext = "webp";
                    size_t dp = mi.filename.rfind('.');
                    if(dp != std::string::npos) ext = mi.filename.substr(dp + 1);
                    std::string url = Args->config->buildurl("media/getimage/") + mi.id + "." + ext;

                    json_object_object_add(response, "status", json_object_new_string("success"));
                    json_object *jm = json_object_new_object();
                    json_object_object_add(jm, "id", json_object_new_string(mi.id.c_str()));
                    json_object_object_add(jm, "filename", json_object_new_string(mi.filename.c_str()));
                    json_object_object_add(jm, "kind", json_object_new_string(mi.kind.c_str()));
                    json_object_object_add(jm, "content_type", json_object_new_string(mi.content_type.c_str()));
                    json_object_object_add(jm, "url", json_object_new_string(url.c_str()));
                    json_object_object_add(response, "media", jm);
                } 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("Unknown error"));
                }
            }
        }

    private:
        // ---- mediadb client management ----

        std::string _getSessionToken(const int tid, const std::string &sessionid){
            if(sessionid.empty())
                return "";
            std::string authid;
            try {
                Session sess(*Args->database[tid]);
                sess.getSessionData(sessionid, "authid", authid);
            } catch(...) {}
            return authid;
        }

        void _ensureMdbReady(int tid, const std::string &token = ""){
            if(!_mdb[tid]){
                _mdb[tid] = std::make_unique<mediadb::Client>(_mdbUrl, false, true);
            }
            if(!token.empty()){
                try {
                    _mdb[tid]->set_token(token);
                } catch(...) {
                    // Connection broken — recreate client and retry
                    _mdb[tid] = std::make_unique<mediadb::Client>(_mdbUrl, false, true);
                    _mdb[tid]->set_token(token);
                }
            }
        }

        std::string _getStoreId(int tid, const std::string &storeName){
            {
                std::lock_guard<std::mutex> guard(_mdbSetupMutex);
                auto it = _mdbStoreIds.find(storeName);
                if(it != _mdbStoreIds.end())
                    return it->second;
            }

            auto stores = _mdb[tid]->list_stores();

            std::lock_guard<std::mutex> guard(_mdbSetupMutex);
            // Double-check after network call
            auto it = _mdbStoreIds.find(storeName);
            if(it != _mdbStoreIds.end())
                return it->second;

            for(const auto &s : stores){
                if(s.name == storeName){
                    _mdbStoreIds[storeName] = s.id;
                    return s.id;
                }
            }
            // Store not found, create one
            std::string newId = _mdb[tid]->create_store(storeName).id;
            _mdbStoreIds[storeName] = newId;
            return newId;
        }

        const std::vector<std::string>& _getStoreNames() const {
            return _mdbStores;
        }

        void _loadStores(int tid){
            std::string json;
            try { getConfig(*Args->database[tid], "MEDIADB_STORES", json); } catch(...) {}
            _mdbStores.clear();
            if(!json.empty()){
                json_object *arr = json_tokener_parse(json.c_str());
                if(arr && json_object_is_type(arr, json_type_array)){
                    size_t len = json_object_array_length(arr);
                    for(size_t i = 0; i < len; ++i){
                        json_object *item = json_object_array_get_idx(arr, i);
                        const char *s = json_object_get_string(item);
                        if(s && s[0] != '\0')
                            _mdbStores.push_back(s);
                    }
                }
                if(arr) json_object_put(arr);
            }
            // Backward compat: if MEDIADB_STORES is empty, try single MEDIADB_STORE
            if(_mdbStores.empty()){
                std::string singleStore;
                try { getConfig(*Args->database[tid], "MEDIADB_STORE", singleStore); } catch(...) {}
                if(singleStore.empty())
                    singleStore = "blogi";
                _mdbStores.push_back(singleStore);
            }
        }

        void _saveStores(int tid){
            json_object *arr = json_object_new_array();
            for(const auto &s : _mdbStores){
                json_object_array_add(arr, json_object_new_string(s.c_str()));
            }
            setConfig(*Args->database[tid], "MEDIADB_STORES",
                      json_object_to_json_string_ext(arr, JSON_C_TO_STRING_NOSLASHESCAPE));
            json_object_put(arr);
        }

        bool _addStore(int tid, const std::string &storeName){
            for(const auto &s : _mdbStores){
                if(s == storeName) return false;
            }
            _mdbStores.push_back(storeName);
            _saveStores(tid);
            return true;
        }

        bool _removeStore(int tid, const std::string &storeName){
            auto it = std::find(_mdbStores.begin(), _mdbStores.end(), storeName);
            if(it == _mdbStores.end()) return false;
            _mdbStores.erase(it);
            _mdbStoreIds.erase(storeName);
            _saveStores(tid);
            return true;
        }

        // ---- upload response state per-request ----

        struct UploadResponseState {
            std::string data;
            size_t produced = 0;
        };

        void _storeUploadResponse(libhttppp::HttpRequest &req, const char *json, size_t len){
            std::unique_lock lock(_uploadRespMutex);
            auto &ur = _uploadResponses[reinterpret_cast<uintptr_t>(&req)];
            ur.data.assign(json, len);
            ur.produced = 0;
        }

        UploadResponseState* _getUploadResponse(libhttppp::HttpRequest &req){
            std::unique_lock lock(_uploadRespMutex);
            auto it = _uploadResponses.find(reinterpret_cast<uintptr_t>(&req));
            if(it == _uploadResponses.end()) return nullptr;
            return &it->second;
        }

        void _removeUploadResponse(libhttppp::HttpRequest &req){
            std::unique_lock lock(_uploadRespMutex);
            _uploadResponses.erase(reinterpret_cast<uintptr_t>(&req));
        }

        std::mutex _uploadRespMutex;
        std::map<uintptr_t, UploadResponseState> _uploadResponses;

        // ---- streaming state per-request ----

        struct StreamState {
            std::unique_ptr<libhttppp::HttpClient> client;
            size_t sendSize = 0;
            size_t produced = 0;
            int tid = 0;
        };

        void _storeStreamState(libhttppp::HttpRequest &req,
                               std::unique_ptr<libhttppp::HttpClient> client,
                               size_t sendSize, int tid){
            std::unique_lock lock(_streamMutex);
            auto &ss = _streams[reinterpret_cast<uintptr_t>(&req)];
            ss.client = std::move(client);
            ss.sendSize = sendSize;
            ss.produced = 0;
            ss.tid = tid;
        }

        StreamState* _getStreamState(libhttppp::HttpRequest &req){
            std::unique_lock lock(_streamMutex);
            auto it = _streams.find(reinterpret_cast<uintptr_t>(&req));
            if(it == _streams.end()) return nullptr;
            auto *ptr = &it->second;
            if(ptr->produced >= ptr->sendSize || !ptr->client){
                _streams.erase(it);
                return nullptr;
            }
            return ptr;
        }

        // ---- HttpClient pool for mediadb streaming ----

        std::unique_ptr<libhttppp::HttpClient> _acquireMdbClient(int tid){
            auto &pool = _mdbClientPool[tid];
            if(!pool.empty()){
                auto client = std::move(pool.back());
                pool.pop_back();
                return client;
            }
            return std::make_unique<libhttppp::HttpClient>(*_mdbHttpUrl, true);
        }

        // ---- member state ----

        std::string _mdbUrl;
        std::vector<std::string> _mdbStores;
        std::map<std::string, std::string> _mdbStoreIds;
        std::vector<std::unique_ptr<mediadb::Client>> _mdb;
        std::unique_ptr<libhttppp::HttpUrl> _mdbHttpUrl;
        std::mutex _mdbSetupMutex;
        std::vector<std::vector<std::unique_ptr<libhttppp::HttpClient>>> _mdbClientPool;
        std::mutex _streamMutex;
        std::map<uintptr_t, StreamState> _streams;

        // ---- allowed preview sizes ----

        struct PreviewSize {
            int w = 0;
            int h = 0;
            bool operator==(const PreviewSize &o) const { return w == o.w && h == o.h; }
        };

        std::vector<PreviewSize> _allowedSizes;
        std::mutex _sizesMutex;

        void _loadAllowedSizes(int tid){
            std::string json;
            try { getConfig(*Args->database[tid], "MEDIA_ALLOWED_SIZES", json); } catch(...) {}
            std::lock_guard<std::mutex> lock(_sizesMutex);
            _allowedSizes.clear();
            if(json.empty()) return;
            json_object *arr = json_tokener_parse(json.c_str());
            if(!arr || !json_object_is_type(arr, json_type_array)){
                if(arr) json_object_put(arr);
                return;
            }
            size_t len = json_object_array_length(arr);
            for(size_t i = 0; i < len; ++i){
                json_object *item = json_object_array_get_idx(arr, i);
                json_object *jw = nullptr, *jh = nullptr;
                if(json_object_object_get_ex(item, "w", &jw) &&
                   json_object_object_get_ex(item, "h", &jh)){
                    _allowedSizes.push_back({json_object_get_int(jw), json_object_get_int(jh)});
                }
            }
            json_object_put(arr);
        }

        void _saveAllowedSizes(int tid){
            json_object *arr = json_object_new_array();
            {
                std::lock_guard<std::mutex> lock(_sizesMutex);
                for(const auto &s : _allowedSizes){
                    json_object *item = json_object_new_object();
                    json_object_object_add(item, "w", json_object_new_int(s.w));
                    json_object_object_add(item, "h", json_object_new_int(s.h));
                    json_object_array_add(arr, item);
                }
            }
            setConfig(*Args->database[tid], "MEDIA_ALLOWED_SIZES",
                      json_object_to_json_string_ext(arr, JSON_C_TO_STRING_NOSLASHESCAPE));
            json_object_put(arr);
        }

        bool _addAllowedSize(int tid, int w, int h){
            PreviewSize ps{w, h};
            {
                std::lock_guard<std::mutex> lock(_sizesMutex);
                for(const auto &s : _allowedSizes){
                    if(s == ps) return false;
                }
                _allowedSizes.push_back(ps);
            }
            _saveAllowedSizes(tid);
            return true;
        }

        bool _removeAllowedSize(int tid, int w, int h){
            PreviewSize ps{w, h};
            {
                std::lock_guard<std::mutex> lock(_sizesMutex);
                auto it = std::find(_allowedSizes.begin(), _allowedSizes.end(), ps);
                if(it == _allowedSizes.end()) return false;
                _allowedSizes.erase(it);
            }
            _saveAllowedSizes(tid);
            return true;
        }

        bool _isSizeAllowed(int w, int h){
            if(w == 0 && h == 0) return true;
            // Always allow small thumbnail sizes used by the internal browser
            if(w > 0 && w <= 200 && h >= 0 && h <= 200) return true;
            std::lock_guard<std::mutex> lock(_sizesMutex);
            for(const auto &s : _allowedSizes){
                if(w > 0 && h > 0){
                    if(s.w == w && s.h == h) return true;
                } else if(w > 0){
                    if(s.w == w) return true;
                } else if(h > 0){
                    if(s.h == h) return true;
                }
            }
            return false;
        }
    };
};

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

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