/*
// Copyright (c) 2019 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
*/

#include "callback_manager.hpp"

#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/container/flat_map.hpp>
#include <sdbusplus/asio/connection.hpp>
#include <sdbusplus/asio/object_server.hpp>

#include <variant>

constexpr const char* fatalLedPath =
    "/xyz/openbmc_project/led/groups/status_critical";
constexpr const char* criticalLedPath =
    "/xyz/openbmc_project/led/groups/status_non_critical";
constexpr const char* warningLedPath =
    "/xyz/openbmc_project/led/groups/status_degraded";
constexpr const char* okLedPath = "/xyz/openbmc_project/led/groups/status_ok";

constexpr const char* ledIface = "xyz.openbmc_project.Led.Group";
constexpr const char* ledAssertProp = "Asserted";
constexpr const char* ledManagerBusname =
    "xyz.openbmc_project.LED.GroupManager";

std::unique_ptr<AssociationManager> associationManager;

enum class StatusSetting
{
    none,
    ok,
    warn,
    critical,
    fatal
};

constexpr const bool debug = false;

// final led state tracking
StatusSetting currentPriority = StatusSetting::none;

// maps of <object-path, <property, asserted>>
boost::container::flat_map<std::string,
                           boost::container::flat_map<std::string, bool>>
    fatalAssertMap;
boost::container::flat_map<std::string,
                           boost::container::flat_map<std::string, bool>>
    criticalAssertMap;
boost::container::flat_map<std::string,
                           boost::container::flat_map<std::string, bool>>
    warningAssertMap;

std::vector<std::string> assertedInMap(
    const boost::container::flat_map<
        std::string, boost::container::flat_map<std::string, bool>>& map)
{
    std::vector<std::string> ret;
    // if any of the properties are true, return true
    for (const auto& pair : map)
    {
        for (const auto& item : pair.second)
        {
            if (item.second)
            {
                ret.push_back(pair.first);
            }
        }
    }
    return ret;
}

void updateLedStatus(std::shared_ptr<sdbusplus::asio::connection>& conn,
                     bool forceRefresh = false)
{
    std::vector<std::string> fatalVector = assertedInMap(fatalAssertMap);
    bool fatal = fatalVector.size();

    std::vector<std::string> criticalVector = assertedInMap(criticalAssertMap);
    bool critical = criticalVector.size();

    std::vector<std::string> warningVector = assertedInMap(warningAssertMap);
    bool warn = warningVector.size();

    associationManager->setLocalAssociations(fatalVector, criticalVector,
                                             warningVector);

    StatusSetting last = currentPriority;
    std::vector<std::pair<std::string, std::variant<bool>>> ledsToSet;
    if (forceRefresh)
    {
        ledsToSet.push_back(std::make_pair(fatalLedPath, false));
        ledsToSet.push_back(std::make_pair(criticalLedPath, false));
        ledsToSet.push_back(std::make_pair(warningLedPath, false));
        ledsToSet.push_back(std::make_pair(okLedPath, false));
        for (const auto& ledPair : ledsToSet)
        {
            conn->async_method_call(
                [ledPair](const boost::system::error_code ec) {
                    if (ec)
                    {
                        std::cerr << "Cannot set " << ledPair.first << " to "
                                  << std::boolalpha
                                  << std::get<bool>(ledPair.second) << "\n";
                    }
                    if constexpr (debug)
                    {
                        std::cerr << "Set " << ledPair.first << " to "
                                  << std::boolalpha
                                  << std::get<bool>(ledPair.second) << "\n";
                    }
                },
                ledManagerBusname, ledPair.first,
                "org.freedesktop.DBus.Properties", "Set", ledIface,
                ledAssertProp, ledPair.second);
        }
    }
    if (fatal)
    {
        currentPriority = StatusSetting::fatal;
    }
    else if (critical)
    {
        currentPriority = StatusSetting::critical;
    }
    else if (warn)
    {
        currentPriority = StatusSetting::warn;
    }
    else
    {
        currentPriority = StatusSetting::ok;
    }

    if (last != currentPriority || forceRefresh)
    {
        switch (currentPriority)
        {
            case (StatusSetting::fatal):
            {
                ledsToSet.push_back(std::make_pair(fatalLedPath, true));
                ledsToSet.push_back(std::make_pair(criticalLedPath, false));
                ledsToSet.push_back(std::make_pair(warningLedPath, false));
                ledsToSet.push_back(std::make_pair(okLedPath, false));
                break;
            }
            case (StatusSetting::critical):
            {
                ledsToSet.push_back(std::make_pair(fatalLedPath, false));
                ledsToSet.push_back(std::make_pair(criticalLedPath, true));
                ledsToSet.push_back(std::make_pair(warningLedPath, false));
                ledsToSet.push_back(std::make_pair(okLedPath, false));
                break;
            }
            case (StatusSetting::warn):
            {
                ledsToSet.push_back(std::make_pair(fatalLedPath, false));
                ledsToSet.push_back(std::make_pair(criticalLedPath, false));
                ledsToSet.push_back(std::make_pair(warningLedPath, true));
                ledsToSet.push_back(std::make_pair(okLedPath, false));
                break;
            }
            case (StatusSetting::ok):
            {
                ledsToSet.push_back(std::make_pair(fatalLedPath, false));
                ledsToSet.push_back(std::make_pair(criticalLedPath, false));
                ledsToSet.push_back(std::make_pair(warningLedPath, false));
                ledsToSet.push_back(std::make_pair(okLedPath, true));
                break;
            }
        }
    }

    for (const auto& ledPair : ledsToSet)
    {
        conn->async_method_call(
            [ledPair](const boost::system::error_code ec) {
                if (ec)
                {
                    std::cerr << "Cannot set " << ledPair.first << " to "
                              << std::boolalpha
                              << std::get<bool>(ledPair.second) << "\n";
                }
                if constexpr (debug)
                {
                    std::cerr << "Set " << ledPair.first << " to "
                              << std::boolalpha
                              << std::get<bool>(ledPair.second) << "\n";
                }
            },
            ledManagerBusname, ledPair.first, "org.freedesktop.DBus.Properties",
            "Set", ledIface, ledAssertProp, ledPair.second);
    }
}

void createThresholdMatch(std::shared_ptr<sdbusplus::asio::connection>& conn)
{

    static sdbusplus::bus::match_t match(
        static_cast<sdbusplus::bus_t&>(*conn),
        "type='signal',member='ThresholdAsserted'",
        [&conn](sdbusplus::message_t& message) {
            std::string sensorName;
            std::string thresholdInterface;
            std::string event;
            bool assert;
            double assertValue;

            try
            {
                message.read(sensorName, thresholdInterface, event, assert,
                             assertValue);
            }
            catch (sdbusplus::exception_t&)
            {
                return;
            }
            if constexpr (debug)
            {
                std::cerr << "Threshold callback: SensorName = " << sensorName
                          << ", Event = " << event << ", Asserted = " << assert
                          << "\n";
            }

            if (event == "CriticalAlarmLow")
            {
                criticalAssertMap[message.get_path()]["low"] = assert;
            }
            else if (event == "CriticalAlarmHigh")
            {
                criticalAssertMap[message.get_path()]["high"] = assert;
            }
            else if (event == "WarningAlarmLow")
            {
                warningAssertMap[message.get_path()]["low"] = assert;
            }
            else if (event == "WarningAlarmHigh")
            {
                warningAssertMap[message.get_path()]["high"] = assert;
            }

            associationManager->setSensorAssociations(
                assertedInMap(criticalAssertMap),
                assertedInMap(warningAssertMap));

            updateLedStatus(conn);
        });
}

void createAssociationMatch(std::shared_ptr<sdbusplus::asio::connection>& conn)
{
    static sdbusplus::bus::match_t match(
        static_cast<sdbusplus::bus_t&>(*conn),
        "type='signal',interface='org.freedesktop.DBus.Properties',"
        "arg0namespace='" +
            std::string(associationIface) + "'",
        [&conn](sdbusplus::message_t& message) {
            if (message.get_path() == rootPath)
            {
                return; // it's us
            }
            std::string objectName;
            boost::container::flat_map<std::string,
                                       std::variant<std::vector<Association>>>
                values;
            try
            {
                message.read(objectName, values);
            }
            catch (sdbusplus::exception_t&)
            {
                return;
            }

            if constexpr (debug)
            {
                std::cerr << "Association callback " << message.get_path()
                          << "\n";
            }

            auto findAssociations = values.find("Associations");
            if (findAssociations == values.end())
            {
                return;
            }
            const std::vector<Association>* associations =
                std::get_if<std::vector<Association>>(
                    &findAssociations->second);

            if (associations == nullptr)
            {
                std::cerr << "Illegal Association on " << message.get_path()
                          << "\n";
                return;
            }

            bool localWarning = false;
            bool localCritical = false;
            bool globalWarning = false;
            bool globalCritical = false;

            for (const auto& [forward, reverse, path] : *associations)
            {
                if (path == rootPath)
                {
                    globalWarning = globalWarning ? true : reverse == "warning";
                    globalCritical =
                        globalCritical ? true : reverse == "critical";

                    if constexpr (1)
                    {
                        std::cerr << "got global ";
                    }
                }
                else
                {
                    localWarning = localWarning ? true : reverse == "warning";
                    localCritical =
                        localCritical ? true : reverse == "critical";
                }
                if (globalCritical && localCritical)
                {
                    break;
                }
            }

            bool fatal = globalCritical && localCritical;
            bool critical = globalWarning && localCritical;
            bool warning = globalWarning && !critical;

            fatalAssertMap[message.get_path()]["association"] = fatal;
            criticalAssertMap[message.get_path()]["association"] = critical;
            warningAssertMap[message.get_path()]["association"] = warning;

            updateLedStatus(conn);
        });
}

int main(int argc, char** argv)
{
    boost::asio::io_context io;
    auto conn = std::make_shared<sdbusplus::asio::connection>(io);
    conn->request_name("xyz.openbmc_project.CallbackManager");
    sdbusplus::asio::object_server objServer(conn);
    std::shared_ptr<sdbusplus::asio::dbus_interface> rootIface =
        objServer.add_interface(rootPath,
                                "xyz.openbmc_project.CallbackManager");
    rootIface->register_method("RetriggerLEDUpdate",
                               [&conn]() { updateLedStatus(conn, true); });
    rootIface->initialize();

    std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
        objServer.add_interface(rootPath, globalInventoryIface);
    inventoryIface->initialize();

    associationManager = std::make_unique<AssociationManager>(objServer, conn);

    createThresholdMatch(conn);
    createAssociationMatch(conn);
    updateLedStatus(conn);

    io.run();

    return 0;
}