/**
 * 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 "config.h"

#include "manager.hpp"

#include "action.hpp"
#include "event.hpp"
#include "fan.hpp"
#include "group.hpp"
#include "json_config.hpp"
#include "power_state.hpp"
#include "profile.hpp"
#include "sdbusplus.hpp"
#include "utils/flight_recorder.hpp"
#include "zone.hpp"

#include <systemd/sd-bus.h>

#include <nlohmann/json.hpp>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/server/manager.hpp>
#include <sdeventplus/event.hpp>
#include <sdeventplus/utility/timer.hpp>

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
#include <tuple>
#include <utility>
#include <vector>

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

using json = nlohmann::json;

std::vector<std::string> Manager::_activeProfiles;
std::map<std::string,
         std::map<std::string, std::pair<bool, std::vector<std::string>>>>
    Manager::_servTree;
std::map<std::string,
         std::map<std::string, std::map<std::string, PropertyVariantType>>>
    Manager::_objects;
std::unordered_map<std::string, PropertyVariantType> Manager::_parameters;
std::unordered_map<std::string, TriggerActions> Manager::_parameterTriggers;

const std::string Manager::dumpFile = "/tmp/fan_control_dump.json";

Manager::Manager(const sdeventplus::Event& event) :
    _bus(util::SDBusPlus::getBus()), _event(event),
    _mgr(util::SDBusPlus::getBus(), CONTROL_OBJPATH), _loadAllowed(true),
    _powerState(std::make_unique<PGoodState>(
        util::SDBusPlus::getBus(),
        std::bind(std::mem_fn(&Manager::powerStateChanged), this,
                  std::placeholders::_1)))
{}

void Manager::sighupHandler(sdeventplus::source::Signal&,
                            const struct signalfd_siginfo*)
{
    FlightRecorder::instance().log("main", "SIGHUP received");
    // Save current set of available and active profiles
    std::map<configKey, std::unique_ptr<Profile>> profiles;
    profiles.swap(_profiles);
    std::vector<std::string> activeProfiles;
    activeProfiles.swap(_activeProfiles);

    try
    {
        _loadAllowed = true;
        load();
    }
    catch (const std::runtime_error& re)
    {
        // Restore saved available and active profiles
        _loadAllowed = false;
        _profiles.swap(profiles);
        _activeProfiles.swap(activeProfiles);
        log<level::ERR>("Error reloading configs, no changes made",
                        entry("LOAD_ERROR=%s", re.what()));
        FlightRecorder::instance().log(
            "main", fmt::format("Error reloading configs, no changes made: {}",
                                re.what()));
    }
}

void Manager::sigUsr1Handler(sdeventplus::source::Signal&,
                             const struct signalfd_siginfo*)
{
    debugDumpEventSource = std::make_unique<sdeventplus::source::Defer>(
        _event, std::bind(std::mem_fn(&Manager::dumpDebugData), this,
                          std::placeholders::_1));
}

void Manager::dumpDebugData(sdeventplus::source::EventBase& /*source*/)
{
    json data;
    FlightRecorder::instance().dump(data);
    dumpCache(data);

    std::for_each(_zones.begin(), _zones.end(), [&data](const auto& zone) {
        data["zones"][zone.second->getName()] = zone.second->dump();
    });

    std::ofstream file{Manager::dumpFile};
    if (!file)
    {
        log<level::ERR>("Could not open file for fan dump");
        return;
    }

    file << std::setw(4) << data;

    debugDumpEventSource.reset();
}

void Manager::dumpCache(json& data)
{
    auto& objects = data["objects"];
    for (const auto& [path, interfaces] : _objects)
    {
        auto& interfaceJSON = objects[path];

        for (const auto& [interface, properties] : interfaces)
        {
            auto& propertyJSON = interfaceJSON[interface];
            for (const auto& [propName, propValue] : properties)
            {
                std::visit(
                    [&obj = propertyJSON[propName]](auto&& val) { obj = val; },
                    propValue);
            }
        }
    }

    auto& parameters = data["parameters"];
    for (const auto& [name, value] : _parameters)
    {
        std::visit([&obj = parameters[name]](auto&& val) { obj = val; }, value);
    }

    data["services"] = _servTree;
}

void Manager::load()
{
    if (_loadAllowed)
    {
        // Load the available profiles and which are active
        setProfiles();

        // Load the zone configurations
        auto zones = getConfig<Zone>(false, _event, this);
        // Load the fan configurations and move each fan into its zone
        auto fans = getConfig<Fan>(false);
        for (auto& fan : fans)
        {
            configKey fanProfile =
                std::make_pair(fan.second->getZone(), fan.first.second);
            auto itZone = std::find_if(
                zones.begin(), zones.end(), [&fanProfile](const auto& zone) {
                    return Manager::inConfig(fanProfile, zone.first);
                });
            if (itZone != zones.end())
            {
                if (itZone->second->getTarget() != fan.second->getTarget() &&
                    fan.second->getTarget() != 0)
                {
                    // Update zone target to current target of the fan in the
                    // zone
                    itZone->second->setTarget(fan.second->getTarget());
                }
                itZone->second->addFan(std::move(fan.second));
            }
        }

        // Save all currently available groups, if any, then clear for reloading
        auto groups = std::move(Event::getAllGroups(false));
        Event::clearAllGroups();

        std::map<configKey, std::unique_ptr<Event>> events;
        try
        {
            // Load any events configured, including all the groups
            events = getConfig<Event>(true, this, zones);
        }
        catch (const std::runtime_error& re)
        {
            // Restore saved set of all available groups for current events
            Event::setAllGroups(std::move(groups));
            throw re;
        }

        // Enable zones
        _zones = std::move(zones);
        std::for_each(_zones.begin(), _zones.end(),
                      [](const auto& entry) { entry.second->enable(); });

        // Clear current timers and signal subscriptions before enabling events
        // To save reloading services and/or objects into cache, do not clear
        // cache
        _timers.clear();
        _signals.clear();

        // Enable events
        _events = std::move(events);
        std::for_each(_events.begin(), _events.end(),
                      [](const auto& entry) { entry.second->enable(); });

        _loadAllowed = false;
    }
}

void Manager::powerStateChanged(bool powerStateOn)
{
    if (powerStateOn)
    {
        if (_zones.empty())
        {
            throw std::runtime_error("No configured zones found at poweron");
        }
        std::for_each(_zones.begin(), _zones.end(), [](const auto& entry) {
            entry.second->setTarget(entry.second->getPoweronTarget());
        });

        // Tell events to run their power on triggers
        std::for_each(_events.begin(), _events.end(),
                      [](const auto& entry) { entry.second->powerOn(); });
    }
    else
    {
        // Tell events to run their power off triggers
        std::for_each(_events.begin(), _events.end(),
                      [](const auto& entry) { entry.second->powerOff(); });
    }
}

const std::vector<std::string>& Manager::getActiveProfiles()
{
    return _activeProfiles;
}

bool Manager::inConfig(const configKey& input, const configKey& comp)
{
    // Config names dont match, do not include in config
    if (input.first != comp.first)
    {
        return false;
    }
    // No profiles specified by input config, can be used in any config
    if (input.second.empty())
    {
        return true;
    }
    else
    {
        // Profiles must have one match in the other's profiles(and they must be
        // an active profile) to be used in the config
        return std::any_of(
            input.second.begin(), input.second.end(),
            [&comp](const auto& lProfile) {
                return std::any_of(
                    comp.second.begin(), comp.second.end(),
                    [&lProfile](const auto& rProfile) {
                        if (lProfile != rProfile)
                        {
                            return false;
                        }
                        auto activeProfs = getActiveProfiles();
                        return std::find(activeProfs.begin(), activeProfs.end(),
                                         lProfile) != activeProfs.end();
                    });
            });
    }
}

bool Manager::hasOwner(const std::string& path, const std::string& intf)
{
    auto itServ = _servTree.find(path);
    if (itServ == _servTree.end())
    {
        // Path not found in cache, therefore owner missing
        return false;
    }
    for (const auto& service : itServ->second)
    {
        auto itIntf = std::find_if(
            service.second.second.begin(), service.second.second.end(),
            [&intf](const auto& interface) { return intf == interface; });
        if (itIntf != std::end(service.second.second))
        {
            // Service found, return owner state
            return service.second.first;
        }
    }
    // Interface not found in cache, therefore owner missing
    return false;
}

void Manager::setOwner(const std::string& serv, bool hasOwner)
{
    // Update owner state on all entries of `serv`
    for (auto& itPath : _servTree)
    {
        auto itServ = itPath.second.find(serv);
        if (itServ != itPath.second.end())
        {
            itServ->second.first = hasOwner;

            // Remove associated interfaces from object cache when service no
            // longer has an owner
            if (!hasOwner && _objects.find(itPath.first) != _objects.end())
            {
                for (auto& intf : itServ->second.second)
                {
                    _objects[itPath.first].erase(intf);
                }
            }
        }
    }
}

void Manager::setOwner(const std::string& path, const std::string& serv,
                       const std::string& intf, bool isOwned)
{
    // Set owner state for specific object given
    auto& ownIntf = _servTree[path][serv];
    ownIntf.first = isOwned;
    auto itIntf = std::find_if(
        ownIntf.second.begin(), ownIntf.second.end(),
        [&intf](const auto& interface) { return intf == interface; });
    if (itIntf == std::end(ownIntf.second))
    {
        ownIntf.second.emplace_back(intf);
    }

    // Update owner state on all entries of the same `serv` & `intf`
    for (auto& itPath : _servTree)
    {
        if (itPath.first == path)
        {
            // Already set/updated owner on this path for `serv` & `intf`
            continue;
        }
        for (auto& itServ : itPath.second)
        {
            if (itServ.first != serv)
            {
                continue;
            }
            auto itIntf = std::find_if(
                itServ.second.second.begin(), itServ.second.second.end(),
                [&intf](const auto& interface) { return intf == interface; });
            if (itIntf != std::end(itServ.second.second))
            {
                itServ.second.first = isOwned;
            }
        }
    }
}

const std::string& Manager::findService(const std::string& path,
                                        const std::string& intf)
{
    static const std::string empty = "";

    auto itServ = _servTree.find(path);
    if (itServ != _servTree.end())
    {
        for (const auto& service : itServ->second)
        {
            auto itIntf = std::find_if(
                service.second.second.begin(), service.second.second.end(),
                [&intf](const auto& interface) { return intf == interface; });
            if (itIntf != std::end(service.second.second))
            {
                // Service found, return service name
                return service.first;
            }
        }
    }

    return empty;
}

void Manager::addServices(const std::string& intf, int32_t depth)
{
    // Get all subtree objects for the given interface
    auto objects = util::SDBusPlus::getSubTreeRaw(util::SDBusPlus::getBus(),
                                                  "/", intf, depth);
    // Add what's returned to the cache of path->services
    for (auto& itPath : objects)
    {
        auto pathIter = _servTree.find(itPath.first);
        if (pathIter != _servTree.end())
        {
            // Path found in cache
            for (auto& itServ : itPath.second)
            {
                auto servIter = pathIter->second.find(itServ.first);
                if (servIter != pathIter->second.end())
                {
                    if (std::find(servIter->second.second.begin(),
                                  servIter->second.second.end(),
                                  intf) == servIter->second.second.end())
                    {
                        // Add interface to cache
                        servIter->second.second.emplace_back(intf);
                    }
                }
                else
                {
                    // Service not found in cache
                    auto intfs = {intf};
                    pathIter->second[itServ.first] =
                        std::make_pair(true, intfs);
                }
            }
        }
        else
        {
            // Path not found in cache
            auto intfs = {intf};
            for (const auto& [servName, servIntfs] : itPath.second)
            {
                _servTree[itPath.first][servName] = std::make_pair(true, intfs);
            }
        }
    }
}

const std::string& Manager::getService(const std::string& path,
                                       const std::string& intf)
{
    // Retrieve service from cache
    const auto& serviceName = findService(path, intf);
    if (serviceName.empty())
    {
        addServices(intf, 0);
        return findService(path, intf);
    }

    return serviceName;
}

std::vector<std::string> Manager::findPaths(const std::string& serv,
                                            const std::string& intf)
{
    std::vector<std::string> paths;

    for (const auto& path : _servTree)
    {
        auto itServ = path.second.find(serv);
        if (itServ != path.second.end())
        {
            if (std::find(itServ->second.second.begin(),
                          itServ->second.second.end(),
                          intf) != itServ->second.second.end())
            {
                if (std::find(paths.begin(), paths.end(), path.first) ==
                    paths.end())
                {
                    paths.push_back(path.first);
                }
            }
        }
    }

    return paths;
}

std::vector<std::string> Manager::getPaths(const std::string& serv,
                                           const std::string& intf)
{
    auto paths = findPaths(serv, intf);
    if (paths.empty())
    {
        addServices(intf, 0);
        return findPaths(serv, intf);
    }

    return paths;
}

void Manager::insertFilteredObjects(ManagedObjects& ref)
{
    // Filter out objects that aren't part of a group
    const auto& allGroupMembers = Group::getAllMembers();
    auto it = ref.begin();

    while (it != ref.end())
    {
        if (allGroupMembers.find(it->first) == allGroupMembers.end())
        {
            it = ref.erase(it);
        }
        else
        {
            it++;
        }
    }

    for (auto& [path, pathMap] : ref)
    {
        for (auto& [intf, intfMap] : pathMap)
        {
            // for each property on this path+interface
            for (auto& [prop, value] : intfMap)
            {
                setProperty(path, intf, prop, value);
            }
        }
    }
}

void Manager::addObjects(const std::string& path, const std::string& intf,
                         const std::string& prop)
{
    auto service = getService(path, intf);
    if (service.empty())
    {
        // Log service not found for object
        log<level::DEBUG>(
            fmt::format("Unable to get service name for path {}, interface {}",
                        path, intf)
                .c_str());
        return;
    }

    auto objMgrPaths = getPaths(service, "org.freedesktop.DBus.ObjectManager");
    if (objMgrPaths.empty())
    {
        // No object manager interface provided by service?
        // Attempt to retrieve property directly
        auto value = util::SDBusPlus::getPropertyVariant<PropertyVariantType>(
            _bus, service, path, intf, prop);

        setProperty(path, intf, prop, value);
        return;
    }

    for (const auto& objMgrPath : objMgrPaths)
    {
        // Get all managed objects of service
        auto objects = util::SDBusPlus::getManagedObjects<PropertyVariantType>(
            _bus, service, objMgrPath);

        // insert all objects that are in groups but remove any NaN values
        insertFilteredObjects(objects);
    }
}

const std::optional<PropertyVariantType>
    Manager::getProperty(const std::string& path, const std::string& intf,
                         const std::string& prop)
{
    // TODO Objects hosted by fan control (i.e. ThermalMode) are required to
    // update the cache upon being set/updated
    auto itPath = _objects.find(path);
    if (itPath != _objects.end())
    {
        auto itIntf = itPath->second.find(intf);
        if (itIntf != itPath->second.end())
        {
            auto itProp = itIntf->second.find(prop);
            if (itProp != itIntf->second.end())
            {
                return itProp->second;
            }
        }
    }

    return std::nullopt;
}

void Manager::setProperty(const std::string& path, const std::string& intf,
                          const std::string& prop, PropertyVariantType value)
{
    // filter NaNs out of the cache
    if (PropertyContainsNan(value))
    {
        // dont use operator [] if paths dont exist
        if (_objects.find(path) != _objects.end() &&
            _objects[path].find(intf) != _objects[path].end())
        {
            _objects[path][intf].erase(prop);
        }
    }
    else
    {
        _objects[path][intf][prop] = std::move(value);
    }
}

void Manager::addTimer(const TimerType type,
                       const std::chrono::microseconds interval,
                       std::unique_ptr<TimerPkg> pkg)
{
    auto dataPtr =
        std::make_unique<TimerData>(std::make_pair(type, std::move(*pkg)));
    Timer timer(_event,
                std::bind(&Manager::timerExpired, this, std::ref(*dataPtr)));
    if (type == TimerType::repeating)
    {
        timer.restart(interval);
    }
    else if (type == TimerType::oneshot)
    {
        timer.restartOnce(interval);
    }
    else
    {
        throw std::invalid_argument("Invalid Timer Type");
    }
    _timers.emplace_back(std::move(dataPtr), std::move(timer));
}

void Manager::addGroups(const std::vector<Group>& groups)
{
    std::string lastServ;
    std::vector<std::string> objMgrPaths;
    std::set<std::string> services;
    for (const auto& group : groups)
    {
        for (const auto& member : group.getMembers())
        {
            try
            {
                auto service = group.getService();
                if (service.empty())
                {
                    service = getService(member, group.getInterface());
                }

                if (!service.empty())
                {
                    if (lastServ != service)
                    {
                        objMgrPaths = getPaths(
                            service, "org.freedesktop.DBus.ObjectManager");
                        lastServ = service;
                    }

                    // Look for the ObjectManager as an ancestor from the
                    // member.
                    auto hasObjMgr = std::any_of(
                        objMgrPaths.begin(), objMgrPaths.end(),
                        [&member](const auto& path) {
                            return member.find(path) != std::string::npos;
                        });

                    if (!hasObjMgr)
                    {
                        // No object manager interface provided for group member
                        // Attempt to retrieve group member property directly
                        auto value = util::SDBusPlus::getPropertyVariant<
                            PropertyVariantType>(_bus, service, member,
                                                 group.getInterface(),
                                                 group.getProperty());

                        setProperty(member, group.getInterface(),
                                    group.getProperty(), value);
                        continue;
                    }

                    if (services.find(service) == services.end())
                    {
                        services.insert(service);
                        for (const auto& objMgrPath : objMgrPaths)
                        {
                            // Get all managed objects from the service
                            auto objects = util::SDBusPlus::getManagedObjects<
                                PropertyVariantType>(_bus, service, objMgrPath);

                            // Insert objects into cache
                            insertFilteredObjects(objects);
                        }
                    }
                }
            }
            catch (const util::DBusError&)
            {
                // No service or property found for group member with the
                // group's configured interface
                continue;
            }
        }
    }
}

void Manager::timerExpired(TimerData& data)
{
    if (std::get<bool>(data.second))
    {
        addGroups(std::get<const std::vector<Group>&>(data.second));
    }

    auto& actions =
        std::get<std::vector<std::unique_ptr<ActionBase>>&>(data.second);
    // Perform the actions in the timer data
    std::for_each(actions.begin(), actions.end(),
                  [](auto& action) { action->run(); });

    // Remove oneshot timers after they expired
    if (data.first == TimerType::oneshot)
    {
        auto itTimer = std::find_if(
            _timers.begin(), _timers.end(), [&data](const auto& timer) {
                return (data.first == timer.first->first &&
                        (std::get<std::string>(data.second) ==
                         std::get<std::string>(timer.first->second)));
            });
        if (itTimer != std::end(_timers))
        {
            _timers.erase(itTimer);
        }
    }
}

void Manager::handleSignal(sdbusplus::message::message& msg,
                           const std::vector<SignalPkg>* pkgs)
{
    for (auto& pkg : *pkgs)
    {
        // Handle the signal callback and only run the actions if the handler
        // updated the cache for the given SignalObject
        if (std::get<SignalHandler>(pkg)(msg, std::get<SignalObject>(pkg),
                                         *this))
        {
            // Perform the actions in the handler package
            auto& actions = std::get<TriggerActions>(pkg);
            std::for_each(actions.begin(), actions.end(), [](auto& action) {
                if (action.get())
                {
                    action.get()->run();
                }
            });
        }
        // Only rewind message when not last package
        if (&pkg != &pkgs->back())
        {
            sd_bus_message_rewind(msg.get(), true);
        }
    }
}

void Manager::setProfiles()
{
    // Profiles JSON config file is optional
    auto confFile = fan::JsonConfig::getConfFile(_bus, confAppName,
                                                 Profile::confFileName, true);

    _profiles.clear();
    if (!confFile.empty())
    {
        for (const auto& entry : fan::JsonConfig::load(confFile))
        {
            auto obj = std::make_unique<Profile>(entry);
            _profiles.emplace(
                std::make_pair(obj->getName(), obj->getProfiles()),
                std::move(obj));
        }
    }

    // Ensure all configurations use the same set of active profiles
    // (In case a profile's active state changes during configuration)
    _activeProfiles.clear();
    for (const auto& profile : _profiles)
    {
        if (profile.second->isActive())
        {
            _activeProfiles.emplace_back(profile.first.first);
        }
    }
}

void Manager::addParameterTrigger(
    const std::string& name, std::vector<std::unique_ptr<ActionBase>>& actions)
{
    auto it = _parameterTriggers.find(name);
    if (it != _parameterTriggers.end())
    {
        std::for_each(actions.begin(), actions.end(),
                      [&actList = it->second](auto& action) {
                          actList.emplace_back(std::ref(action));
                      });
    }
    else
    {
        TriggerActions triggerActions;
        std::for_each(actions.begin(), actions.end(),
                      [&triggerActions](auto& action) {
                          triggerActions.emplace_back(std::ref(action));
                      });
        _parameterTriggers[name] = std::move(triggerActions);
    }
}

void Manager::runParameterActions(const std::string& name)
{
    auto it = _parameterTriggers.find(name);
    if (it != _parameterTriggers.end())
    {
        std::for_each(it->second.begin(), it->second.end(),
                      [](auto& action) { action.get()->run(); });
    }
}

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