/********************************************************************************
 * Copyright (c) 2025-2026 ZF Friedrichshafen AG
 *
 * This program and the accompanying materials are made available under the
 * terms of the Apache License Version 2.0 which is available at
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contributors:
 *   Erik Verhoeven - initial API and implementation
 ********************************************************************************/

#include "module_control.h"
#include <algorithm>
#include "../../global/exec_dir_helper.h"
#include "toml_parser/parser_toml.h"
#include "toml_parser//parser_node_toml.h"
#include "app_settings.h"
#include "app_config.h"

/// @cond DOXYGEN_IGNORE
#ifdef _WIN32
#include <Shlobj.h>
#elif defined __unix__
#include <ctime>
#include <utime.h>
#else
#error OS is not supported!
#endif
/// @endcond

// GetModuleControl might be redirected for unit tests.
#ifndef GetModuleControl 
CModuleControl& GetModuleControl()
{
    static CModuleControl module_control;
    return module_control;
}
#endif // !defined GetModuleControl

CModuleControl::~CModuleControl()
{
    try
    {
        UnloadAll(std::vector<sdv::core::TModuleID>());
    }
    catch (const sdv::XSysExcept&)
    {
    }
    catch (const std::exception&)
    {
    }

}

bool CModuleControl::AddModuleSearchDir(const sdv::u8string& ssDir)
{
    // Add initial paths if not done so already
    AddCurrentPath();

    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);

    std::filesystem::path pathDir(ssDir.c_str());

    // Relative paths are always relative to the executable
    if (pathDir.is_relative())
        pathDir = GetExecDirectory() / ssDir.c_str();

    // Remove any indirections
    pathDir = pathDir.lexically_normal();

    // If the current path is not a directory, it cannot be added
    if (!std::filesystem::is_directory(pathDir)) return false;

    // Check whether the path is already in the list
    if (std::find(m_lstSearchPaths.begin(), m_lstSearchPaths.end(), pathDir) != m_lstSearchPaths.end())
        return true; // This is not an error

    // Add the path
    m_lstSearchPaths.push_back(pathDir);

    return true;
}

sdv::sequence<sdv::u8string> CModuleControl::GetModuleSearchDirs() const
{
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    sdv::sequence<sdv::u8string> seqSearchDirs;
    for (const std::filesystem::path& rpathSearchDir : m_lstSearchPaths)
        seqSearchDirs.push_back(rpathSearchDir.generic_u8string());
    return seqSearchDirs;
}

sdv::sequence<sdv::core::SModuleInfo> CModuleControl::GetModuleList() const
{
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    sdv::sequence<sdv::core::SModuleInfo> seqModuleInfos;

    // Build the module information sequence
    for (const std::shared_ptr<CModuleInst>& ptrModule : m_lstModules)
    {
        if (!ptrModule) continue;
        seqModuleInfos.push_back(ptrModule->GetModuleInfo());
    }
    return seqModuleInfos;
}

sdv::sequence<sdv::SClassInfo> CModuleControl::GetClassList(/*in*/ sdv::core::TModuleID tModuleID) const
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    sdv::sequence<sdv::SClassInfo> seqClassInfos;
    auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr<CModuleInst>& ptrModule)
        {
            return ptrModule && ptrModule->GetModuleID() == tModuleID;
        });
    if (itModule == m_lstModules.end()) return seqClassInfos; // Cannot find the module.

    // Request the class infos from the module instance.
    std::vector<std::string> vecClasses = (*itModule)->GetAvailableClasses();
    for (const std::string& rssClass : vecClasses)
    {
        auto optClassInfo = (*itModule)->GetClassInfo(rssClass);
        if (!optClassInfo) continue;
        seqClassInfos.push_back(*optClassInfo);
    }
    return seqClassInfos;
}

sdv::core::TModuleID CModuleControl::Load(const sdv::u8string& ssModulePath)
{
    if(ssModulePath.empty())
        return 0;

    // Add initial paths if not done so already
    AddCurrentPath();

    // Core services bypass.
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    std::filesystem::path pathModule;
    if (ssModulePath == "core_services.sdv")
        pathModule = static_cast<std::string>(ssModulePath);
    else
    {
        if (GetAppSettings().IsMainApplication() || GetAppSettings().IsIsolatedApplication())
        {
            // Check the installation for the module.
            pathModule = GetAppConfig().FindInstalledModule(static_cast<std::string>(ssModulePath));
        }
        else
        {
            try
            {
                // Get the search paths if the module path is relative
                std::list<std::filesystem::path> lstSearchPaths;
                std::filesystem::path pathSupplied(static_cast<std::string>(ssModulePath));
                if (pathSupplied.is_relative())
                    lstSearchPaths = m_lstSearchPaths;

                // Add an empty path to allow the OS to search when our own search paths could not find the module.
                lstSearchPaths.push_back(std::filesystem::path());

                // Run through the search paths and try to find the module.
                for (const std::filesystem::path& rpathDirectory : lstSearchPaths)
                {
                    // Compose the path
                    std::filesystem::path pathModuleTemp;
                    if (rpathDirectory.is_absolute())
                        pathModuleTemp = (rpathDirectory / pathSupplied).lexically_normal();
                    else
                    {
                        if (rpathDirectory.empty())
                            pathModuleTemp = pathSupplied.lexically_normal();
                        else
                            pathModuleTemp = (GetExecDirectory() / rpathDirectory / pathSupplied).lexically_normal();
                    }

                    if (std::filesystem::exists(pathModuleTemp))
                    {
                        pathModule = pathModuleTemp;
                        break;
                    }
                }
            }
            catch (const std::exception& re)
            {
                SDV_LOG_ERROR("Supplied path to module load is not valid \"", ssModulePath, "\": ", re.what());
            }
        }
    }

    // Check for an exisiting module. If existing, return the existing module.
    for (const std::shared_ptr<CModuleInst>& rptrModule : m_lstModules)
    {
        if (rptrModule && rptrModule->GetModulePath() == pathModule)
            return rptrModule->GetModuleID();
    }

    // Create a new instance (even if the module could not be found).
    std::shared_ptr<CModuleInst> ptrModule = std::make_shared<CModuleInst>(static_cast<std::string>(ssModulePath), pathModule);
    m_lstModules.push_back(ptrModule);
    if (!ptrModule->IsValid())
    {
        SDV_LOG_ERROR("The module was not found \"", ssModulePath, "\"");
        return 0;   // Do not return the module ID of not loadable modules.
    }
    return ptrModule->GetModuleID();
}

bool CModuleControl::Unload(sdv::core::TModuleID tModuleID)
{
    // Cannot unload the core services
    if (tModuleID == sdv::core::tCoreLibModule)
        return true;

    return ContextUnload(tModuleID, false);
}

bool CModuleControl::HasActiveObjects(sdv::core::TModuleID tModuleID) const
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr<CModuleInst>& ptrModule)
        {
            return ptrModule && ptrModule->GetModuleID() == tModuleID;
        });
    if (itModule == m_lstModules.end()) return false; // Cannot find the module; no active objects known.

    // Redirect the call to the module instance.
    return (*itModule)->HasActiveObjects();
}

std::shared_ptr<CModuleInst> CModuleControl::FindModuleByClass(const std::string& rssClass)
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    for (const std::shared_ptr<CModuleInst>& ptrModule : m_lstModules)
    {
        if (!ptrModule) continue;
        std::vector<std::string> vecClasses = ptrModule->GetAvailableClasses();
        for (const std::string& rssClassLocal : vecClasses)
        {
            if (rssClassLocal == rssClass) return ptrModule;
            auto optClassInfo = ptrModule->GetClassInfo(rssClass);
            if (!optClassInfo) continue;
            if (std::find(optClassInfo->seqClassAliases.begin(), optClassInfo->seqClassAliases.end(), rssClass) !=
                optClassInfo->seqClassAliases.end())
                return ptrModule;
        }
    }

    // For main, isolated and maintenance applications, check whether the module is in one of the installation manifests.
    auto optManifest = GetAppConfig().FindInstalledComponent(rssClass);
    if (!optManifest) return nullptr;
    std::filesystem::path pathModule = std::filesystem::u8path(static_cast<std::string>(optManifest->ssModulePath));
    auto ssManifest = GetAppConfig().FindInstalledModuleManifest(pathModule);
    if (ssManifest.empty()) return nullptr;
    lock.unlock();

    // Load the module
    return GetModule(ContextLoad(pathModule, ssManifest));
}

std::shared_ptr<CModuleInst> CModuleControl::GetModule(sdv::core::TModuleID tModuleID) const
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr<CModuleInst>& ptrModule)
        {
            return ptrModule && ptrModule->GetModuleID() == tModuleID;
        });
    if (itModule == m_lstModules.end()) return nullptr; // Cannot find the module.
    return *itModule;
}

void CModuleControl::UnloadAll(const std::vector<sdv::core::TModuleID>& rvecIgnoreModules)
{
    // Force unloading all modules in reverse order of their loading
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    std::list<std::shared_ptr<CModuleInst>> lstCopy = m_lstModules;
    lock.unlock();
    while (lstCopy.size())
    {
        std::shared_ptr<CModuleInst> ptrModule = std::move(lstCopy.back());
        lstCopy.pop_back();
        if (!ptrModule) continue;

        // On the ignore list?
        if (std::find(rvecIgnoreModules.begin(), rvecIgnoreModules.end(), ptrModule->GetModuleID()) != rvecIgnoreModules.end())
            continue;

        // Unload... -> call the module control function to remove it from the list.
        ContextUnload(ptrModule->GetModuleID(), true);
    }
}

void CModuleControl::ResetConfigBaseline()
{
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    m_setConfigModules.clear();
}

sdv::core::EConfigProcessResult CModuleControl::LoadModulesFromConfig(const CAppConfigFile& rconfig, bool bAllowPartialLoad)
{
    // TODO EVE: Extract the parameters from the configuration and store them at the class info.

    switch (GetAppSettings().GetContextType())
    {
    case sdv::app::EAppContext::main:
    case sdv::app::EAppContext::isolated:
    case sdv::app::EAppContext::maintenance:
        return sdv::core::EConfigProcessResult::successful; // Do not load anything
    default:
        break;
    }

    // First load the modules in the module list
    auto vecModules = rconfig.GetModuleList();
    size_t nSuccess = 0, nFail = 0;
    for (const auto& rsModule : vecModules)
    {
        sdv::core::TModuleID tModuleID = Load(rsModule.pathModule.generic_u8string());
        if (tModuleID)
            ++nSuccess;
        else
            ++nFail;
    }

    // Load all the modules from the class list
    auto vecClasses = rconfig.GetClassList();
    for (const auto& rsClass : vecClasses)
    {
        if (rsClass.ssModulePath.empty()) continue;
        sdv::core::TModuleID tModuleID = Load(rsClass.ssModulePath);
        if (tModuleID)
            ++nSuccess;
        else
            ++nFail;
    }

    // Load all the modules from the component list
    auto vecComponents = rconfig.GetComponentList();
    for (const auto& rsComponent : vecComponents)
    {
        if (rsComponent.pathModule.empty()) continue;
        sdv::core::TModuleID tModuleID = Load(rsComponent.pathModule.generic_u8string());
        if (tModuleID)
            ++nSuccess;
        else
            ++nFail;
    }

    if (!bAllowPartialLoad && nFail) return sdv::core::EConfigProcessResult::failed;
    if (!nFail) return sdv::core::EConfigProcessResult::successful;
    if (nSuccess) return sdv::core::EConfigProcessResult::partially_successful;
    return sdv::core::EConfigProcessResult::failed;
}

std::string CModuleControl::SaveConfig(const std::set<std::filesystem::path>& /*rsetIgnoreModule*/)
{
    std::stringstream sstream;
    //std::unique_lock<std::recursive_mutex> lock(m_mtxModules);

    //// Add all the loaded modules
    //for (const std::shared_ptr<CModuleInst>& rptrModule : m_lstModules)
    //{
    //    if (m_setConfigModules.find(rptrModule->GetModuleID()) != m_setConfigModules.end() &&
    //        rsetIgnoreModule.find(rptrModule->GetModuleConfigPath()) != rsetIgnoreModule.end())
    //    {
    //        sstream << std::endl;
    //        sstream << "[[Module]]" << std::endl;
    //        sstream << "Path = \"" << rptrModule->GetModuleConfigPath().generic_u8string() << "\"" << std::endl;
    //    }
    //}
    return sstream.str();
}

sdv::core::TModuleID CModuleControl::ContextLoad(const std::filesystem::path& rpathModule, const std::string& rssManifest)
{
    // Run through the manifest and check for complex services, applications and utilities.
    // TODO EVE: Temporary suppression of cppcheck warning.
    // cppcheck-suppress variableScope
    [[maybe_unused]] bool bIsolationNeeded = false;
    try
    {
        size_t nIndex = 0;
        size_t nUnsupportedObjectCount = 0;
        toml_parser::CParser parser(rssManifest);
        do
        {
            std::shared_ptr<toml_parser::CNode> ptrComponentType =
                parser.Root().Direct(std::string("Component[") + std::to_string(nIndex++) + "].Type");
            if (!ptrComponentType) break;
            std::string ssType = static_cast<std::string>(ptrComponentType->GetValue());
            if (ssType == "Device") continue; // Okay
            if (ssType == "BasicService") continue; // Okay
            if (ssType == "Proxy") continue; // Okay
            if (ssType == "Stub") continue; // Okay
            if (ssType == "ComplexService" || ssType == "Application" || ssType == "Utility")
            {
                bIsolationNeeded = true;
                continue; // Okay
            }

            // Not okay
            nUnsupportedObjectCount++;
        } while (true);
        if (nIndex == nUnsupportedObjectCount) return 0;    // No object to load.
    }
    catch (const sdv::toml::XTOMLParseException& /*rexcept*/)
    {
        return 0;
    }

    // TODO: If the module to load needs isolation...
    // If this is a main application: isolate modules that should be isolated
    // If this is an isolated application: check for another loaded module that needs isolation already. If this do not allow
    // this module to load.

    // Load the module
    return Load(rpathModule.generic_u8string());
}

bool CModuleControl::ContextUnload(sdv::core::TModuleID tModuleID, bool bForce)
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr<CModuleInst>& ptrModule)
        {
            return ptrModule && ptrModule->GetModuleID() == tModuleID;
        });
    if (itModule == m_lstModules.end()) return true; // Cannot find the module to unload. This is not an error!

    // Check whether it is possible to unload.
    bool bSuccess = (*itModule)->Unload(bForce);
    if (bSuccess)
        m_lstModules.erase(itModule);

    return bSuccess;
}

std::shared_ptr<CModuleInst> CModuleControl::FindModuleByPath(const std::filesystem::path& rpathModule) const
{
    // Find the module instance
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);
    auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr<CModuleInst>& ptrModule)
        {
            return ptrModule && ptrModule->GetModulePath() == rpathModule;
        });
    if (itModule == m_lstModules.end()) return nullptr; // Cannot find the module
    return *itModule;
}

void CModuleControl::AddCurrentPath()
{
    std::unique_lock<std::recursive_mutex> lock(m_mtxModules);

    // Do this only once.
    if (!m_lstSearchPaths.empty()) return;

    // Add the core directory
    std::filesystem::path pathCoreDir = GetAppSettings().GetFrameworkDir().lexically_normal();
    m_lstSearchPaths.push_back(pathCoreDir);

    // Add the exe dir
    std::filesystem::path pathExeDir = GetExecDirectory().lexically_normal();
    if (pathExeDir != pathCoreDir) m_lstSearchPaths.push_back(pathExeDir);
}

bool CModuleControlService::EnableModuleControlAccess()
{
    return GetAppSettings().IsStandaloneApplication() ||
        GetAppSettings().IsEssentialApplication();
}
