#include <iostream>
#include <stdexcept>
#include <optional>
#include <filesystem>
#include <stimBuffApis/stimBuffApiManager.h>
#include <stimBuffApis/stimBuffApiLib.h>
#include <opts.h>
#include <asynchronousBridge.h>
#include <asynchronousContinuation.h>
#include <asynchronousLoop.h>
#include <callback.h>
#include <user/senseApiDesc.h>
#include <mind.h>
#include <deviceManager/deviceManager.h>
#include <marionette/marionette.h>


namespace fs = std::filesystem;

namespace smo {
namespace stim_buff {

/**
 * @brief Searches for a library in predefined locations
 * @param libraryPath The name or path of the library to find
 * @return Optional containing the full path if found in search paths, nullopt
 *         if not
 *
 * Searches for the library in the following locations in order:
 * 1. Custom path specified by --sense-api-lib-path option (if provided)
 * 2. Current working directory
 * 3. Directory containing the executable
 *
 * If the library is not found in any of these locations, returns nullopt and
 * falls back to system default library search paths (LD_LIBRARY_PATH, etc.)
 */
static std::optional<std::string> searchForLibInSmoSearchPaths(
    const std::string& libraryPath)
{
    std::vector<std::string> searchPaths = {
        fs::current_path().string(),
        fs::path("/proc/self/exe").parent_path().string()
    };

    const auto& options = OptionParser::getOptions();
    if (!options.senseApiLibPath.empty())
    {
        // Insert all stim buff API library paths at the beginning of search paths
        searchPaths.insert(
            searchPaths.begin(),
            options.senseApiLibPath.begin(),
            options.senseApiLibPath.end());
    }

    for (const auto& path : searchPaths)
    {
        fs::path fullPath = fs::path(path) / libraryPath;
        if (fs::exists(fullPath))
        {
            return fullPath.string();
        }
    }

    std::cerr << std::string(__func__) + ": library '"
              + libraryPath + "' isn't in search bespoke search paths: ";
    for (const auto& path : searchPaths) {
        std::cerr << path << " ";
    }
    std::cerr << std::endl;
    std::cerr << "Trying to load " + libraryPath + " from system default "
        "search paths\n";

    return std::nullopt;
}

/* Local static function to wrap ComponentThread::getSelf for SmoCallbacks */
static std::shared_ptr<ComponentThread> ComponentThread_getSelf()
{
	return ComponentThread::getSelf();
}

/* Hooks to be provided to stimBuffApiLibs, enabling them to call into Salmanoff
 * code.
 */
static SmoCallbacks smoCallbacks =
{
	.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPaths,
	.ComponentThread_getSelf = ComponentThread_getSelf
};

/* Static file-scope threading model object for senseApi libraries */
static SmoThreadingModelDesc smoThreadingModelDesc = {
	.componentThread = nullptr
};

std::optional<std::string> StimBuffApiManager::searchForLibInSmoSearchPaths(
    const std::string& libraryPath)
{
    return ::smo::stim_buff::searchForLibInSmoSearchPaths(libraryPath);
}

StimBuffApiLib& StimBuffApiManager::loadStimBuffApiLib(
	const std::string& libraryPath,
	const std::shared_ptr<ComponentThread>& componentThread
	)
{
    std::optional<std::string> fullPath = searchForLibInSmoSearchPaths(
        libraryPath);
    std::string resolvedPath = fullPath.value_or(libraryPath);

    // Clear any existing error
    dlerror();
    auto dlopen_handle = std::unique_ptr<void, StimBuffApiLib::DlCloser>(
        dlopen(resolvedPath.c_str(), RTLD_LAZY));
    if (!dlopen_handle && fullPath.has_value())
    {
        // Fallback to using the supplied libraryPath
        dlerror();
        dlopen_handle.reset(dlopen(libraryPath.c_str(), RTLD_LAZY));
    }

    if (!dlopen_handle)
    {
        const char *dlerr = dlerror();

        std::string error = (dlerr
            ? dlerr
            : "Unknown error while opening shlib");
        throw std::runtime_error(
            std::string(__func__) + ": Cannot load library '"
            + libraryPath + "': "
            + error);
    }

    // Initialize getSenseApiDescriptor
    auto func = reinterpret_cast<SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF *>(
        dlsym(dlopen_handle.get(), SMO_GET_STIM_BUFF_API_DESC_FN_NAME_STR));
    if (!func)
    {
        throw std::runtime_error(
            std::string(__func__) + ": dlsym('"
            SMO_GET_STIM_BUFF_API_DESC_FN_NAME_STR "') failed for library '"
            + libraryPath + "'");
    }

    // Check if the static threading model obj is null and initialize if needed
    if (!smoThreadingModelDesc.componentThread) {
        smoThreadingModelDesc.componentThread = componentThread;
    }

    const StimBuffApiDesc &libApiDesc = func(
		smoCallbacks, smoThreadingModelDesc);

	auto lib = std::make_shared<StimBuffApiLib>(
        libraryPath, dlopen_handle.release(), func);
    lib->setStimBuffApiDesc(libApiDesc);
    stimBuffApiLibs.push_back(lib);
    return *stimBuffApiLibs.back();
}

std::optional<std::shared_ptr<StimBuffApiLib>>
StimBuffApiManager::getStimBuffApiLib(const std::string& libraryPath)
{
    auto it = std::find_if(stimBuffApiLibs.begin(), stimBuffApiLibs.end(),
        [&libPath = libraryPath](const std::shared_ptr<StimBuffApiLib>& lib) {
            return lib->libraryPath == libPath;
        }
    );

    if (it != stimBuffApiLibs.end()) { return *it; }
    return std::nullopt;
}

std::optional<std::shared_ptr<StimBuffApiLib>>
StimBuffApiManager::getStimBuffApiLibByApiName(const std::string& apiName)
{
    auto it = std::find_if(stimBuffApiLibs.begin(), stimBuffApiLibs.end(),
        [&apiName](const std::shared_ptr<StimBuffApiLib>& lib) {
            return lib->stimBuffApiDesc.name == apiName;
        }
    );

    if (it != stimBuffApiLibs.end()) { return *it; }
    return std::nullopt;
}

void StimBuffApiManager::unloadStimBuffApiLib(const std::string& libraryPath)
{
    auto it = std::find_if(stimBuffApiLibs.begin(), stimBuffApiLibs.end(),
        [&lpath = libraryPath](const std::shared_ptr<StimBuffApiLib>& lib) {
            return lib->libraryPath == lpath;
        }
    );

    if (it != stimBuffApiLibs.end())
    {
        stimBuffApiLibs.erase(it);
        return;
    }

    std::cerr << std::string(__func__) + ": Library not found: "
        << libraryPath << '\n';
}

void StimBuffApiManager::unloadAllStimBuffApiLibs(void)
{
    stimBuffApiLibs.clear();
}

void StimBuffApiManager::loadAllStimBuffApiLibsFromOptions(
	const std::shared_ptr<ComponentThread>& componentThread
	)
{
    const auto& options = OptionParser::getOptions();
    for (const auto& libPath : options.senseApiLibs) {
        loadStimBuffApiLib(libPath, componentThread);
    }
}

std::string StimBuffApiManager::stringifyLibs() const
{
    std::string result;
    for (const auto& lib : stimBuffApiLibs) {
        result += lib->stringify() + "\n";
    }
    return result;
}

void StimBuffApiManager::initializeStimBuffApiLib(StimBuffApiLib& lib)
{
	/**	FIXME:
	 * When we eventually make this method async, this method should acquire
	 * the StimBuffApiManager's main CRUD qutex.
	 */
    if (!lib.stimBuffApiDesc.sal_mgmt_libOps.initializeInd)
    {
        throw std::runtime_error(
            std::string(__func__) + ": initializeInd() is NULL for library '"
            + lib.libraryPath + "'");
    }
    lib.stimBuffApiDesc.sal_mgmt_libOps.initializeInd();
}

void StimBuffApiManager::finalizeStimBuffApiLib(StimBuffApiLib& lib)
{
	/**	FIXME:
	 * When we eventually make this method async, this flag should only be set
	 * after acquiring the StimBuffApiManager's main CRUD qutex.
	 */
    lib.isBeingDestroyed.store(true);
    if (!lib.stimBuffApiDesc.sal_mgmt_libOps.finalizeInd)
    {
        throw std::runtime_error(
            std::string(__func__) + ": finalizeInd() is NULL for library '"
            + lib.libraryPath + "'");
    }
    lib.stimBuffApiDesc.sal_mgmt_libOps.finalizeInd();
}

void StimBuffApiManager::initializeAllStimBuffApiLibs(void)
{
    for (auto& lib : stimBuffApiLibs) {
        initializeStimBuffApiLib(*lib);
    }
}

void StimBuffApiManager::finalizeAllStimBuffApiLibs(void)
{
    for (auto& lib : stimBuffApiLibs) {
		finalizeStimBuffApiLib(*lib);
    }
}


} // namespace stim_buff
} // namespace smo
