/*******************************************************************************
 * 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 <cstring>
#include <filesystem>

#include <httppp/exception.h>
#include <authdb/client.h>
#include <uuidp.h>

#ifdef Windows
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#else
#include <dlfcn.h>
#endif
#include "plugin.h"
#include "conf.h"

blogi::SearchRet::SearchRet(){
    _firstData=nullptr;
    _lastData=nullptr;
}

blogi::SearchRet::~SearchRet(){
    clear();
}

blogi::SearchRet::Data * blogi::SearchRet::getfirstData(){
    return _firstData.get();
}

void blogi::SearchRet::clear(){
    _firstData.reset();
}

void blogi::SearchRet::push_back(const blogi::SearchRet::Data& dat){
    if(!_firstData){
        _firstData=std::make_unique<Data>(dat);
        _lastData=_firstData.get();
    }else{
        _lastData->_next=std::make_unique<Data>(dat);
        _lastData=_lastData->_next.get();
    }
}

blogi::SearchRet::Data::Data(){
    _next=nullptr;
}

blogi::SearchRet::Data::Data(const blogi::SearchRet::Data& dat){
     title=dat.title;
     anchor=dat.anchor;
     std::copy(dat.text.begin(),dat.text.end(),std::back_inserter(text));
     _next=nullptr;
}


blogi::SearchRet::Data::~Data(){
}

blogi::SearchRet::Data* blogi::SearchRet::Data::nextData(){
    return _next.get();
}


//musl hack
blogi::PluginApi::~PluginApi(){
}

void blogi::PluginApi::initPlugin(){
    return;
}

void blogi::PluginApi::registerGPO(const char *gpoid, const char *name, const char *desc, bool gpodefault){
    if(Args->config->getAuthSourceCount() < 1)
        return;
    for(size_t i = 0; i < Args->config->getAuthSourceCount(); ++i){
        try {
            const AuthSource &src = Args->config->getAuthSource(i);
            authdb::client::ClientConnection authcon;
            authcon.setUrl(src.url);
            authcon.setClientName(src.clientName);
            authcon.setClientSecret(src.clientSecret);
            authdb::client::Client clt(authcon);
            if(clt.ClientAuth()){
                uuid::uuid gid(gpoid);
                if(!clt.GPOexits(gid))
                    clt.GPOadd(gid, name, desc, gpodefault, {});
            }
        } catch(const std::exception &e) {
            std::cerr << "GPO registration failed for " << name << ": " << e.what() << std::endl;
        } catch(...) {
            std::cerr << "GPO registration failed for " << name << std::endl;
        }
    }
}

bool blogi::PluginApi::haveSettings(){
    return false;
}

json_object* blogi::PluginApi::getSettingsTabs(){
    return nullptr;
}

void blogi::PluginApi::Rendering(const int tid,libhttppp::HttpRequest &req,libhtmlpp::HtmlElement *curpage,int type,const std::string &sessionid){
    return;
}

json_object* blogi::PluginApi::Settings(const int tid,libhttppp::HttpRequest &req,const std::string &sessionid){
    return nullptr;
};

bool blogi::PluginApi::Controller(const int tid,libhttppp::HttpRequest &req,libhtmlpp::HtmlElement *page,const std::string &sessionid){
    return false;
}

bool blogi::PluginApi::Response(const int tid,libhttppp::HttpRequest& req){
    return false;
}

void blogi::PluginApi::Search(const int tid, const char* word, blogi::SearchRet &result, const std::string &sessionid){
    return;
}

void blogi::PluginApi::GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std::chrono::year_month_day> &sitemap){
    libhttppp::HttpUrl url= Args->config->getsiteurl()+ "/"+getName();
    auto zero_date = std::chrono::year(1970) / std::chrono::month(1) / std::chrono::day(1);  sitemap.emplace(url,zero_date);
    return;
}

void blogi::PluginApi::JsonApi(const int tid,json_object* request, json_object* response,const uuid::uuid &authid){
    return;
}

bool blogi::PluginApi::canPublish(){
    return false;
}

json_object* blogi::PluginApi::getPublishForm(const int tid){
    return json_object_new_array();
}

void blogi::PluginApi::Publish(const int tid, const std::string &html, json_object *params, json_object *response){
    json_object_object_add(response, "status", json_object_new_string("error"));
    json_object_object_add(response, "message", json_object_new_string("Plugin does not support publishing."));
}

void blogi::PluginApi::RenderPublished(const int /*tid*/, const std::string &html,
                                       json_object * /*params*/, libhtmlpp::HtmlElement &page){
    libhtmlpp::HtmlElement *main = page.getElementbyID("main");
    if (main) {
        libhtmlpp::HtmlString buf;
        buf << html;
        try {
            main->appendChild(buf.parse());
        } catch (...) {}
    }
}

void blogi::PluginApi::sharePost(const int /*tid*/, SharePostType /*type*/, const std::string &/*url*/,
                                 const std::string &/*title*/, const std::string &/*description*/){
}


void blogi::PluginApi::setArgs(blogi::PluginArgs* args){
    Args=args;
}

blogi::Plugin::Plugin(){
}


void blogi::Plugin::loadPlugins(const std::string &path, PluginArgs *args){
    try {
        if (std::filesystem::is_directory(path)) {
            for (const auto& entry : std::filesystem::directory_iterator(path)) {
                 std::unique_ptr<PluginData> ldplg = std::make_unique<PluginData>();
#ifdef Windows
                if (entry.path().extension() == ".dll") {
                    try {
                        std::string ppath = entry.path().string();
                        ldplg->pldata = LoadLibraryA(ppath.c_str());

                        if (ldplg->pldata == nullptr) {
                            DWORD lastError = GetLastError();
                            throw std::runtime_error("Cannot load library. Error code: " + std::to_string(lastError));
                        }

                        create_t *create_plugin = (create_t*)GetProcAddress(ldplg->pldata, "create");

                        if (create_plugin == nullptr) {
                            DWORD lastError = GetLastError();
                            FreeLibrary(ldplg->pldata);
                            throw std::runtime_error("Cannot load symbol 'create'. Error code: " + std::to_string(lastError));
                        }

                        destroy_t* destroy_plugin = (destroy_t*)GetProcAddress(ldplg->pldata, "destroy");
                        if (destroy_plugin == nullptr) {
                            DWORD lastError = GetLastError();
                            FreeLibrary(ldplg->pldata);
                            throw std::runtime_error("Cannot load symbol 'destroy'. Error code: " + std::to_string(lastError));
                        }

#else // Linux/Unix
                if (entry.path().extension() == ".so" || entry.path().extension() == ".dylib") {
                    try{


                        ldplg->pldata = dlopen(entry.path().c_str(), RTLD_LAZY);

                        const char * dlsym_error = nullptr;

                        if (!ldplg->pldata) { // FIX: Check ldplg->pldata, not !ldplg->pldata && dlsym_error
                            dlsym_error = dlerror();
                            libhttppp::HTTPException err;
                            err[libhttppp::HTTPException::Error] << "Cannot load library: " << (dlsym_error ? dlsym_error : "Unknown error");
                            throw err;
                        }

                        // Get the address of the 'create' function
                        create_t* create_plugin= (create_t*) dlsym(ldplg->pldata, "create");

                        if (create_plugin == nullptr) {
                            dlsym_error = dlerror();
                            libhttppp::HTTPException err;
                            // The custom deleter will call dlclose when ldplg goes out of scope/is destroyed
                            err[libhttppp::HTTPException::Error] << "Cannot load symbol create: " << (dlsym_error ? dlsym_error : "Unknown error") << '\n';
                            throw err;
                        }
                        destroy_t* destroy_plugin = (destroy_t*) dlsym(ldplg->pldata, "destroy");
#endif
                        PluginApi *plg=create_plugin();
                        plg->setArgs(args);
                        plg->initPlugin();
                        ldplg->ins=std::unique_ptr<PluginApi, PluginApiDeleter>(plg, PluginApiDeleter(destroy_plugin,ldplg->pldata));

                        if(_lastPlugin){
                            _lastPlugin->next=std::move(ldplg);
                            _lastPlugin = _lastPlugin->next.get();
                        }else{
                            _firstPlugin=std::move(ldplg);
                            _lastPlugin = _firstPlugin.get();
                        }
                    }
                    catch (const std::exception& e) {
                        std::cerr << "Error processing file " << entry.path().filename().string() << ": " << e.what() << std::endl;
                    }catch(libhttppp::HTTPException &e){
                         std::cerr << "Error processing file " << e.what() << ": " << e.what() << std::endl;
                    }
                }
            }
        }
    }catch (const std::exception& e) {
        std::cerr << "Error loading plugins directory: " << e.what() << std::endl;
    }
}

blogi::Plugin::~Plugin(){
}

const blogi::Plugin::PluginData* blogi::Plugin::getFirstPlugin() const{
    return _firstPlugin.get();
}

blogi::Plugin::PluginData::~PluginData(){
}

blogi::Plugin::PluginData::PluginData(){
    pldata = nullptr;
    next=nullptr;
}

blogi::PluginApi * blogi::Plugin::PluginData::getInstace() const{
    return ins.get();
}

const blogi::Plugin::PluginData *blogi::Plugin::PluginData::getNextPlg() const{
    return next.get();
}

blogi::webedit::EditPluginLoad::EditPluginLoad(const std::vector<std::string> &paths){
    for(auto path : paths) {
        try {
            if (std::filesystem::is_directory(path)) {
                for (const auto& entry : std::filesystem::directory_iterator(path)) {
                    std::unique_ptr<PluginData> ldplg = std::make_unique<PluginData>();
#ifdef Windows
                    if (entry.path().extension() == ".dll") {
                        try {
                            std::string ppath = entry.path().string();
                            ldplg->pldata = LoadLibraryA(ppath.c_str());

                            if (ldplg->pldata == nullptr) {
                                DWORD lastError = GetLastError();
                                throw std::runtime_error("Cannot load library. Error code: " + std::to_string(lastError));
                            }

                            create_t *create_plugin = (create_t*)GetProcAddress(ldplg->pldata, "create");

                            if (create_plugin == nullptr) {
                                DWORD lastError = GetLastError();
                                FreeLibrary(ldplg->pldata);
                                throw std::runtime_error("Cannot load symbol 'create'. Error code: " + std::to_string(lastError));
                            }

                            destroy_t* destroy_plugin = (destroy_t*)GetProcAddress(ldplg->pldata, "destroy");
                            if (destroy_plugin == nullptr) {
                                DWORD lastError = GetLastError();
                                FreeLibrary(ldplg->pldata);
                                throw std::runtime_error("Cannot load symbol 'destroy'. Error code: " + std::to_string(lastError));
                            }
                            
                            ldplg->create_func = create_plugin;
                            ldplg->destroy_func = destroy_plugin;
                        } catch (const std::exception& e) {
                            std::cerr << "Windows dll error processing file " << entry.path().filename().string() << ": " << e.what() << std::endl;
                            continue;
                        }
                    }

#else // Linux/Unix
                    if (entry.path().extension() == ".so" || entry.path().extension() == ".dylib") {
                        try{


                            ldplg->pldata = dlopen(entry.path().c_str(), RTLD_LAZY);

                            const char * dlsym_error = nullptr;

                            if (!ldplg->pldata) { // FIX: Check ldplg->pldata, not !ldplg->pldata && dlsym_error
                                dlsym_error = dlerror();
                                libhttppp::HTTPException err;
                                err[libhttppp::HTTPException::Error] << "Cannot load library: " << (dlsym_error ? dlsym_error : "Unknown error");
                                throw err;
                            }

                            // Get the address of the 'create' function
                            create_t* create_plugin= (create_t*) dlsym(ldplg->pldata, "create");

                            if (create_plugin == nullptr) {
                                dlsym_error = dlerror();
                                libhttppp::HTTPException err;
                                // The custom deleter will call dlclose when ldplg goes out of scope/is destroyed
                                err[libhttppp::HTTPException::Error] << "Cannot load symbol create: " << (dlsym_error ? dlsym_error : "Unknown error") << '\n';
                                throw err;
                            }
                            destroy_t* destroy_plugin = (destroy_t*) dlsym(ldplg->pldata, "destroy");
                            
                            ldplg->create_func = create_plugin;
                            ldplg->destroy_func = destroy_plugin;
                        } catch (const std::exception& e) {
                            std::cerr << "Linux so error processing file " << entry.path().filename().string() << ": " << e.what() << std::endl;
                            continue;
                        } catch(libhttppp::HTTPException &e){
                            std::cerr << "Linux so error processing file " << entry.path().filename().string() << ": " << e.what() << std::endl;
                            continue;
                        }
                    }
#endif
                    if (ldplg->create_func && ldplg->destroy_func) {
                        try {
                            EditPlugin *plg = ldplg->create_func();
                            ldplg->ins=std::unique_ptr<EditPlugin, EditPluginDeleter>(plg, EditPluginDeleter(ldplg->destroy_func,ldplg->pldata));

                            if(_lastPlugin){
                                _lastPlugin->next=std::move(ldplg);
                                _lastPlugin = _lastPlugin->next.get();
                            }else{
                                _firstPlugin=std::move(ldplg);
                                _lastPlugin = _firstPlugin.get();
                            }
                        } catch (...) {
                           std::cerr << "Failed to initialize." << std::endl; 
                        }
                    }
                }
            }
        }catch (const std::exception& e) {
            std::cerr << "Error loading plugins directory: " << e.what() << std::endl;
        }
    }
}

blogi::webedit::EditPluginLoad::~EditPluginLoad(){
}

const blogi::webedit::EditPluginLoad::PluginData* blogi::webedit::EditPluginLoad::EditPluginLoad::getFirstPlugin() const{
    return _firstPlugin.get();
}

const blogi::webedit::EditPluginLoad::PluginData *blogi::webedit::EditPluginLoad::PluginData::getNextPlg() const{
    return next.get();
}

blogi::webedit::EditPlugin *blogi::webedit::EditPluginLoad::PluginData::getInstace() const{
    return ins.get();
}

blogi::webedit::EditPlugin* blogi::webedit::EditPluginLoad::PluginData::createNewInstance() const {
    if (create_func) {
        return create_func();
    }
    return nullptr;
}

const blogi::webedit::EditPlugin* blogi::webedit::EditPluginLoad::getPluginPrototype(const uuid::uuid& typeId) const {
    auto it = _pluginIndex.find(typeId);
    if (it != _pluginIndex.end()) {
        return it->second;
    }
    return nullptr;
}

void blogi::webedit::EditPluginLoad::buildPluginIndex() {
    _pluginIndex.clear();
    const PluginData* current = _firstPlugin.get();
    while (current) {
        if (current->getInstace()) {
            // Index the plugin prototype by its Type UUID
            _pluginIndex[current->getTypeId()] = current->getInstace();
        }
        current = current->getNextPlg();
    }
}
