/*******************************************************************************
 * Copyright (c) 2026, 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 "plugin.h"

#include <iostream>
#include <filesystem>
#include <cstring>
#include <netplus/exception.h>

#ifdef Windows
#  include <windows.h>
#else
#  include <dlfcn.h>
#endif

namespace authdb {

/*
 * Platform helpers – thin wrappers around dlopen/LoadLibrary so the
 * rest of the code stays platform-neutral.
 */
#ifdef Windows
static constexpr const char *PLUGIN_EXT = ".dll";

static void *pluginOpen(const char *path) {
    HMODULE h = LoadLibraryA(path);
    return reinterpret_cast<void*>(h);
}

static void *pluginSym(void *handle, const char *sym) {
    return reinterpret_cast<void*>(
        GetProcAddress(reinterpret_cast<HMODULE>(handle), sym));
}

static void pluginClose(void *handle) {
    FreeLibrary(reinterpret_cast<HMODULE>(handle));
}

static std::string pluginError() {
    DWORD err = GetLastError();
    char buf[256];
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, err, 0,
                   buf, sizeof(buf), nullptr);
    return std::string(buf);
}
#else  /* POSIX */
static constexpr const char *PLUGIN_EXT = ".so";

static void *pluginOpen(const char *path) {
    return dlopen(path, RTLD_NOW);
}

static void *pluginSym(void *handle, const char *sym) {
    return dlsym(handle, sym);
}

static void pluginClose(void *handle) {
    dlclose(handle);
}

static std::string pluginError() {
    const char *e = dlerror();
    return e ? e : "";
}
#endif

PluginManager::~PluginManager() {
    for (auto &[name, lp] : _Plugins) {
        if (lp.instance && lp.destroy)
            lp.destroy(lp.instance);
        if (lp.handle)
            pluginClose(lp.handle);
    }
    _Plugins.clear();
}

/*
 * Scan `directory` for shared libraries (*.so on Linux, *.dll on Windows).
 * Each .so that exports authdb_plugin_create / authdb_plugin_destroy is
 * instantiated and stored by name().
 *
 * On Linux, versioned sonames like libfoo.so.0.1 are skipped – only the
 * unversioned symlink libfoo.so is loaded so each plugin is loaded once.
 */
int PluginManager::loadAll(const std::string &directory,
                           AuthBackend &backend,
                           const char *configJson) {
    int loaded = 0;
    namespace fs = std::filesystem;

    if (!fs::is_directory(directory)) {
        std::cerr << "Plugin directory not found: " << directory << std::endl;
        return 0;
    }

    for (auto &entry : fs::directory_iterator(directory)) {
        /* Follow symlinks (the .so is typically a symlink to .so.0.1) */
        if (entry.is_directory())
            continue;

        std::string fname = entry.path().filename().string();

        /* On Linux accept files ending exactly in ".so" (skip .so.0, .so.0.1).
         * On Windows accept files ending in ".dll".                          */
        bool accepted = false;
        if (fname.size() > strlen(PLUGIN_EXT)
            && fname.compare(fname.size() - strlen(PLUGIN_EXT),
                             strlen(PLUGIN_EXT), PLUGIN_EXT) == 0) {
            /* Make sure nothing follows the extension (rules out .so.0) */
            accepted = true;
        }

        if (!accepted)
            continue;

        /* Resolve the real path so dlopen gets the actual file */
        std::string path = fs::canonical(entry.path()).string();

        void *handle = pluginOpen(path.c_str());
        if (!handle) {
            std::cerr << "Plugin load failed: " << path << ": "
                      << pluginError() << std::endl;
            continue;
        }

        auto createFn = reinterpret_cast<PluginCreateFn>(
            pluginSym(handle, "authdb_plugin_create"));
        auto destroyFn = reinterpret_cast<PluginDestroyFn>(
            pluginSym(handle, "authdb_plugin_destroy"));

        if (!createFn || !destroyFn) {
            std::cerr << "Plugin missing symbols: " << fname << std::endl;
            pluginClose(handle);
            continue;
        }

        Plugin *instance = nullptr;
        try {
            instance = createFn(backend, configJson);
        } catch (const netplus::NetException &e) {
            std::cerr << "Plugin create failed: " << fname << ": "
                      << e.what() << std::endl;
            pluginClose(handle);
            continue;
        } catch (const std::exception &e) {
            std::cerr << "Plugin create failed: " << fname << ": "
                      << e.what() << std::endl;
            pluginClose(handle);
            continue;
        }

        if (!instance) {
            pluginClose(handle);
            continue;
        }

        std::string pluginName = instance->name();
        std::cerr << "Plugin loaded: " << pluginName
                  << " (" << fname << ")" << std::endl;

        LoadedPlugin lp;
        lp.handle   = handle;
        lp.instance = instance;
        lp.destroy  = destroyFn;
        _Plugins[pluginName] = lp;
        ++loaded;
    }

    return loaded;
}

void PluginManager::initAll() {
    for (auto &[name, lp] : _Plugins) {
        if (lp.instance) {
            try {
                lp.instance->init();
            } catch (const netplus::NetException &e) {
                std::cerr << "Plugin init failed: " << name << ": "
                          << e.what() << std::endl;
            } catch (const std::exception &e) {
                std::cerr << "Plugin init failed: " << name << ": "
                          << e.what() << std::endl;
            }
        }
    }
}

} /* namespace authdb */
