#include "fru-fault-monitor.hpp"

#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/lg2.hpp>
#include <sdbusplus/exception.hpp>
#include <xyz/openbmc_project/Common/error.hpp>

namespace phosphor
{
namespace led
{
namespace fru
{
namespace fault
{
namespace monitor
{

using namespace phosphor::logging;

static constexpr auto mapperBusName = "xyz.openbmc_project.ObjectMapper";
static constexpr auto mapperObjPath = "/xyz/openbmc_project/object_mapper";
static constexpr auto mapperIntf = "xyz.openbmc_project.ObjectMapper";
static constexpr auto objMgrIntf = "org.freedesktop.DBus.ObjectManager";
static constexpr auto ledGroups = "/xyz/openbmc_project/led/groups/";
static constexpr auto logIntf = "xyz.openbmc_project.Logging.Entry";

using AssociationList =
    std::vector<std::tuple<std::string, std::string, std::string>>;
using Attributes = std::variant<bool, AssociationList>;
using PropertyName = std::string;
using PropertyMap = std::unordered_map<PropertyName, Attributes>;
using InterfaceName = std::string;
using InterfaceMap = std::unordered_map<InterfaceName, PropertyMap>;

using Service = std::string;
using Path = std::string;
using Interface = std::string;
using Interfaces = std::vector<Interface>;
using MapperResponseType =
    std::unordered_map<Path, std::unordered_map<Service, Interfaces>>;

using ResourceNotFoundErr =
    sdbusplus::xyz::openbmc_project::Common::Error::ResourceNotFound;
using InvalidArgumentErr =
    sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument;

std::string getService(sdbusplus::bus_t& bus, const std::string& path)
{
    auto mapper = bus.new_method_call(mapperBusName, mapperObjPath, mapperIntf,
                                      "GetObject");
    mapper.append(path.c_str(), std::vector<std::string>({objMgrIntf}));

    std::unordered_map<std::string, std::vector<std::string>> mapperResponse;
    try
    {
        auto mapperResponseMsg = bus.call(mapper);
        mapperResponseMsg.read(mapperResponse);
    }
    catch (const sdbusplus::exception_t& e)
    {
        lg2::error(
            "Failed to parse getService mapper response, ERROR = {ERROR}",
            "ERROR", e);
        using namespace xyz::openbmc_project::Common;
        elog<ResourceNotFoundErr>(ResourceNotFound::RESOURCE(path.c_str()));
    }
    if (mapperResponse.empty())
    {
        using namespace xyz::openbmc_project::Common;
        elog<ResourceNotFoundErr>(ResourceNotFound::RESOURCE(path.c_str()));
        return {};
    }

    return mapperResponse.cbegin()->first;
}

void action(sdbusplus::bus_t& bus, const std::string& path, bool assert)
{
    std::string service;
    try
    {
        std::string groups{ledGroups};
        groups.pop_back();
        service = getService(bus, groups);
    }
    catch (const ResourceNotFoundErr& e)
    {
        commit<ResourceNotFoundErr>();
        return;
    }

    auto pos = path.rfind('/');
    if (pos == std::string::npos)
    {
        using namespace xyz::openbmc_project::Common;
        report<InvalidArgumentErr>(
            InvalidArgument::ARGUMENT_NAME("path"),
            InvalidArgument::ARGUMENT_VALUE(path.c_str()));
        return;
    }
    auto unit = path.substr(pos + 1);

    std::string ledPath = ledGroups + unit + '_' + LED_FAULT;

    auto method = bus.new_method_call(service.c_str(), ledPath.c_str(),
                                      "org.freedesktop.DBus.Properties", "Set");
    method.append("xyz.openbmc_project.Led.Group");
    method.append("Asserted");

    method.append(std::variant<bool>(assert));

    try
    {
        bus.call_noreply(method);
    }
    catch (const sdbusplus::exception_t& e)
    {
        // Log an info message, system may not have all the LED Groups defined
        lg2::info("Failed to Assert LED Group, ERROR = {ERROR}", "ERROR", e);
    }

    return;
}

void Add::created(sdbusplus::message_t& msg)
{
    auto bus = msg.get_bus();

    sdbusplus::message::object_path objectPath;
    InterfaceMap interfaces;
    try
    {
        msg.read(objectPath, interfaces);
    }
    catch (const sdbusplus::exception_t& e)
    {
        lg2::error("Failed to parse created message, ERROR = {ERROR}", "ERROR",
                   e);
        return;
    }

    std::size_t found = objectPath.str.find(ELOG_ENTRY);
    if (found == std::string::npos)
    {
        // Not a new error entry skip
        return;
    }
    auto iter = interfaces.find("xyz.openbmc_project.Association.Definitions");
    if (iter == interfaces.end())
    {
        return;
    }

    // Nothing else shows when a specific error log
    // has been created. Do it here.
    lg2::info("{PATH} created", "PATH", objectPath);

    auto attr = iter->second.find("Associations");
    if (attr == iter->second.end())
    {
        return;
    }

    auto& assocs = std::get<AssociationList>(attr->second);

    for (const auto& item : assocs)
    {
        if (std::get<1>(item) == CALLOUT_REV_ASSOCIATION)
        {
            removeWatches.emplace_back(
                std::make_unique<Remove>(bus, std::get<2>(item)));
            action(bus, std::get<2>(item), true);
        }
    }

    return;
}

void getLoggingSubTree(sdbusplus::bus_t& bus, MapperResponseType& subtree)
{
    auto depth = 0;
    auto mapperCall = bus.new_method_call(mapperBusName, mapperObjPath,
                                          mapperIntf, "GetSubTree");
    mapperCall.append("/");
    mapperCall.append(depth);
    mapperCall.append(std::vector<Interface>({logIntf}));

    try
    {
        auto mapperResponseMsg = bus.call(mapperCall);
        mapperResponseMsg.read(subtree);
    }
    catch (const sdbusplus::exception_t& e)
    {
        lg2::error(
            "Failed to parse existing callouts subtree message, ERROR = {ERROR}",
            "ERROR", e);
    }
}

void Add::processExistingCallouts(sdbusplus::bus_t& bus)
{
    MapperResponseType mapperResponse;

    getLoggingSubTree(bus, mapperResponse);
    if (mapperResponse.empty())
    {
        // No errors to process.
        return;
    }

    for (const auto& elem : mapperResponse)
    {
        auto method = bus.new_method_call(
            elem.second.begin()->first.c_str(), elem.first.c_str(),
            "org.freedesktop.DBus.Properties", "Get");
        method.append("xyz.openbmc_project.Association.Definitions");
        method.append("Associations");
        auto reply = bus.call(method);
        if (reply.is_method_error())
        {
            // do not stop, continue with next elog
            lg2::error("Error in getting associations");
            continue;
        }

        std::variant<AssociationList> assoc;
        try
        {
            reply.read(assoc);
        }
        catch (const sdbusplus::exception_t& e)
        {
            lg2::error(
                "Failed to parse existing callouts associations message, ERROR = {ERROR}",
                "ERROR", e);
            continue;
        }
        auto& assocs = std::get<AssociationList>(assoc);

        for (const auto& item : assocs)
        {
            if (std::get<1>(item) == CALLOUT_REV_ASSOCIATION)
            {
                removeWatches.emplace_back(
                    std::make_unique<Remove>(bus, std::get<2>(item)));
                action(bus, std::get<2>(item), true);
            }
        }
    }
}

void Remove::removed(sdbusplus::message_t& msg)
{
    auto bus = msg.get_bus();

    action(bus, inventoryPath, false);
    return;
}

} // namespace monitor
} // namespace fault
} // namespace fru
} // namespace led
} // namespace phosphor