/**
 * Copyright © 2020 IBM 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 "event.hpp"

#include "action.hpp"
#include "config_base.hpp"
#include "group.hpp"
#include "manager.hpp"
#include "sdbusplus.hpp"
#include "trigger.hpp"

#include <fmt/format.h>

#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>

#include <algorithm>
#include <optional>

namespace phosphor::fan::control::json
{

using json = nlohmann::json;
using namespace phosphor::logging;

std::map<configKey, std::unique_ptr<Group>> Event::allGroups;

Event::Event(const json& jsonObj, Manager* mgr,
             std::map<configKey, std::unique_ptr<Zone>>& zones) :
    ConfigBase(jsonObj),
    _bus(util::SDBusPlus::getBus()), _manager(mgr), _zones(zones)
{
    // Event groups are optional
    if (jsonObj.contains("groups"))
    {
        setGroups(jsonObj, _profiles, _groups);
    }
    // Event actions are optional
    if (jsonObj.contains("actions"))
    {
        setActions(jsonObj);
    }
    setTriggers(jsonObj);
}

void Event::enable()
{
    for (const auto& [type, trigger] : _triggers)
    {
        // Don't call the powerOn or powerOff triggers
        if (type.find("power") == std::string::npos)
        {
            trigger(getName(), _manager, _groups, _actions);
        }
    }
}

void Event::powerOn()
{
    for (const auto& [type, trigger] : _triggers)
    {
        if (type == "poweron")
        {
            trigger(getName(), _manager, _groups, _actions);
        }
    }
}

void Event::powerOff()
{
    for (const auto& [type, trigger] : _triggers)
    {
        if (type == "poweroff")
        {
            trigger(getName(), _manager, _groups, _actions);
        }
    }
}

std::map<configKey, std::unique_ptr<Group>>&
    Event::getAllGroups(bool loadGroups)
{
    if (allGroups.empty() && loadGroups)
    {
        allGroups = Manager::getConfig<Group>(true);
    }

    return allGroups;
}

void Event::configGroup(Group& group, const json& jsonObj)
{
    if (!jsonObj.contains("interface") || !jsonObj.contains("property") ||
        !jsonObj["property"].contains("name"))
    {
        log<level::ERR>("Missing required group attribute",
                        entry("JSON=%s", jsonObj.dump().c_str()));
        throw std::runtime_error("Missing required group attribute");
    }

    // Get the group members' interface
    auto intf = jsonObj["interface"].get<std::string>();
    group.setInterface(intf);

    // Get the group members' property name
    auto prop = jsonObj["property"]["name"].get<std::string>();
    group.setProperty(prop);

    // Get the group members' data type
    if (jsonObj["property"].contains("type"))
    {
        std::optional<std::string> type =
            jsonObj["property"]["type"].get<std::string>();
        group.setType(type);
    }

    // Get the group members' expected value
    if (jsonObj["property"].contains("value"))
    {
        std::optional<PropertyVariantType> value =
            getJsonValue(jsonObj["property"]["value"]);
        group.setValue(value);
    }
}

void Event::setGroups(const json& jsonObj,
                      const std::vector<std::string>& profiles,
                      std::vector<Group>& groups)
{
    if (jsonObj.contains("groups"))
    {
        auto& availGroups = getAllGroups();
        for (const auto& jsonGrp : jsonObj["groups"])
        {
            if (!jsonGrp.contains("name"))
            {
                auto msg = fmt::format("Missing required group name attribute");
                log<level::ERR>(msg.c_str(),
                                entry("JSON=%s", jsonGrp.dump().c_str()));
                throw std::runtime_error(msg.c_str());
            }

            configKey eventProfile =
                std::make_pair(jsonGrp["name"].get<std::string>(), profiles);
            auto grpEntry = std::find_if(availGroups.begin(), availGroups.end(),
                                         [&eventProfile](const auto& grp) {
                                             return Manager::inConfig(
                                                 grp.first, eventProfile);
                                         });
            if (grpEntry != availGroups.end())
            {
                auto group = Group(*grpEntry->second);
                configGroup(group, jsonGrp);
                groups.emplace_back(group);
            }
        }
    }
}

void Event::setActions(const json& jsonObj)
{
    for (const auto& jsonAct : jsonObj["actions"])
    {
        if (!jsonAct.contains("name"))
        {
            log<level::ERR>("Missing required event action name",
                            entry("JSON=%s", jsonAct.dump().c_str()));
            throw std::runtime_error("Missing required event action name");
        }

        // Determine list of zones action should be run against
        std::vector<std::reference_wrapper<Zone>> actionZones;
        if (!jsonAct.contains("zones"))
        {
            // No zones configured on the action results in the action running
            // against all zones matching the event's active profiles
            for (const auto& zone : _zones)
            {
                configKey eventProfile =
                    std::make_pair(zone.second->getName(), _profiles);
                auto zoneEntry = std::find_if(_zones.begin(), _zones.end(),
                                              [&eventProfile](const auto& z) {
                                                  return Manager::inConfig(
                                                      z.first, eventProfile);
                                              });
                if (zoneEntry != _zones.end())
                {
                    actionZones.emplace_back(*zoneEntry->second);
                }
            }
        }
        else
        {
            // Zones configured on the action result in the action only running
            // against those zones if they match the event's active profiles
            for (const auto& jsonZone : jsonAct["zones"])
            {
                configKey eventProfile =
                    std::make_pair(jsonZone.get<std::string>(), _profiles);
                auto zoneEntry = std::find_if(_zones.begin(), _zones.end(),
                                              [&eventProfile](const auto& z) {
                                                  return Manager::inConfig(
                                                      z.first, eventProfile);
                                              });
                if (zoneEntry != _zones.end())
                {
                    actionZones.emplace_back(*zoneEntry->second);
                }
            }
        }
        if (actionZones.empty())
        {
            log<level::DEBUG>(
                fmt::format("No zones configured for event {}'s action {} "
                            "based on the active profile(s)",
                            getName(), jsonAct["name"].get<std::string>())
                    .c_str());
        }

        // Action specific groups, if any given, will override the use of event
        // groups in the action(s)
        std::vector<Group> actionGroups;
        setGroups(jsonAct, _profiles, actionGroups);
        if (!actionGroups.empty())
        {
            // Create the action for the event using the action's groups
            auto actObj = ActionFactory::getAction(
                jsonAct["name"].get<std::string>(), jsonAct,
                std::move(actionGroups), std::move(actionZones));
            if (actObj)
            {
                actObj->setEventName(_name);
                _actions.emplace_back(std::move(actObj));
            }
        }
        else
        {
            // Create the action for the event using the event's groups
            auto actObj = ActionFactory::getAction(
                jsonAct["name"].get<std::string>(), jsonAct, _groups,
                std::move(actionZones));
            if (actObj)
            {
                actObj->setEventName(_name);
                _actions.emplace_back(std::move(actObj));
            }
        }

        if (actionGroups.empty() && _groups.empty())
        {
            log<level::DEBUG>(
                fmt::format("No groups configured for event {}'s action {} "
                            "based on the active profile(s)",
                            getName(), jsonAct["name"].get<std::string>())
                    .c_str());
        }
    }
}

void Event::setTriggers(const json& jsonObj)
{
    if (!jsonObj.contains("triggers"))
    {
        log<level::ERR>("Missing required event triggers list",
                        entry("JSON=%s", jsonObj.dump().c_str()));
        throw std::runtime_error("Missing required event triggers list");
    }
    for (const auto& jsonTrig : jsonObj["triggers"])
    {
        if (!jsonTrig.contains("class"))
        {
            log<level::ERR>("Missing required event trigger class",
                            entry("JSON=%s", jsonTrig.dump().c_str()));
            throw std::runtime_error("Missing required event trigger class");
        }
        // The class of trigger used to run the event actions
        auto tClass = jsonTrig["class"].get<std::string>();
        std::transform(tClass.begin(), tClass.end(), tClass.begin(), tolower);
        auto trigFunc = trigger::triggers.find(tClass);
        if (trigFunc != trigger::triggers.end())
        {
            _triggers.emplace_back(
                trigFunc->first,
                trigFunc->second(jsonTrig, getName(), _actions));
        }
        else
        {
            // Construct list of available triggers
            auto availTrigs = std::accumulate(
                std::next(trigger::triggers.begin()), trigger::triggers.end(),
                trigger::triggers.begin()->first, [](auto list, auto trig) {
                    return std::move(list) + ", " + trig.first;
                });
            log<level::ERR>(
                fmt::format("Trigger '{}' is not recognized", tClass).c_str(),
                entry("AVAILABLE_TRIGGERS=%s", availTrigs.c_str()));
            throw std::runtime_error("Unsupported trigger class name given");
        }
    }
}

json Event::dump() const
{
    json actionData;
    std::for_each(_actions.begin(), _actions.end(),
                  [&actionData](const auto& action) {
                      actionData[action->getUniqueName()] = action->dump();
                  });

    std::vector<std::string> groupData;
    std::for_each(_groups.begin(), _groups.end(),
                  [&groupData](const auto& group) {
                      groupData.push_back(group.getName());
                  });

    json eventData;
    eventData["groups"] = groupData;
    eventData["actions"] = actionData;

    return eventData;
}

} // namespace phosphor::fan::control::json