/**
 * Copyright © 2022 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 "zone.hpp"

#include "../utils/flight_recorder.hpp"
#include "dbus_zone.hpp"
#include "fan.hpp"
#include "sdbusplus.hpp"

#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdeventplus/event.hpp>

#include <algorithm>
#include <chrono>
#include <iterator>
#include <map>
#include <memory>
#include <numeric>
#include <utility>
#include <vector>

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

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

const std::map<
    std::string,
    std::map<std::string, std::function<std::function<void(DBusZone&, Zone&)>(
                              const json&, bool)>>>
    Zone::_intfPropHandlers = {
        {DBusZone::thermalModeIntf,
         {{DBusZone::supportedProp, zone::property::supported},
          {DBusZone::currentProp, zone::property::current}}}};

Zone::Zone(const json& jsonObj, const sdeventplus::Event& event, Manager* mgr) :
    ConfigBase(jsonObj), _dbusZone{}, _manager(mgr), _defaultFloor(0),
    _incDelay(0), _decInterval(0), _floor(0), _target(0), _incDelta(0),
    _decDelta(0), _requestTargetBase(0), _isActive(true),
    _incTimer(event, std::bind(&Zone::incTimerExpired, this)),
    _decTimer(event, std::bind(&Zone::decTimerExpired, this))
{
    // Increase delay is optional, defaults to 0
    if (jsonObj.contains("increase_delay"))
    {
        _incDelay =
            std::chrono::seconds(jsonObj["increase_delay"].get<uint64_t>());
    }

    // Poweron target is required
    setPowerOnTarget(jsonObj);

    // Default ceiling is optional, defaults to poweron target
    _defaultCeiling = _poweronTarget;
    if (jsonObj.contains("default_ceiling"))
    {
        _defaultCeiling = jsonObj["default_ceiling"].get<uint64_t>();
    }
    // Start with the current ceiling set as the default ceiling
    _ceiling = _defaultCeiling;

    // Default floor is optional, defaults to 0
    if (jsonObj.contains("default_floor"))
    {
        _defaultFloor = jsonObj["default_floor"].get<uint64_t>();
        if (_defaultFloor > _ceiling)
        {
            log<level::ERR>(
                fmt::format("Configured default_floor({}) above ceiling({}), "
                            "setting default floor to ceiling",
                            _defaultFloor, _ceiling)
                    .c_str());
            _defaultFloor = _ceiling;
        }
        // Start with the current floor set as the default
        _floor = _defaultFloor;
    }

    // Decrease interval is optional, defaults to 0
    // A decrease interval of 0sec disables the decrease timer
    if (jsonObj.contains("decrease_interval"))
    {
        _decInterval =
            std::chrono::seconds(jsonObj["decrease_interval"].get<uint64_t>());
    }

    // Setting properties on interfaces to be served are optional
    if (jsonObj.contains("interfaces"))
    {
        setInterfaces(jsonObj);
    }
}

void Zone::enable()
{
    // Create thermal control dbus object
    _dbusZone = std::make_unique<DBusZone>(*this);

    // Init all configured dbus interfaces' property states
    for (const auto& func : _propInitFunctions)
    {
        // Only call non-null init property functions
        if (func)
        {
            func(*_dbusZone, *this);
        }
    }

    // TODO - Restore any persisted properties in init function
    // Restore thermal control current mode state, if exists
    _dbusZone->restoreCurrentMode();

    // Emit object added for this zone's associated dbus object
    _dbusZone->emit_object_added();

    // A decrease interval of 0sec disables the decrease timer
    if (_decInterval != std::chrono::seconds::zero())
    {
        // Start timer for fan target decreases
        _decTimer.restart(_decInterval);
    }
}

void Zone::addFan(std::unique_ptr<Fan> fan)
{
    _fans.emplace_back(std::move(fan));
}

void Zone::setTarget(uint64_t target)
{
    if (_isActive)
    {
        if (_target != target)
        {
            FlightRecorder::instance().log(
                "zone-set-target" + getName(),
                fmt::format("Set target {} (from {})", target, _target));
        }
        _target = target;
        for (auto& fan : _fans)
        {
            fan->setTarget(_target);
        }
    }
}

void Zone::lockFanTarget(const std::string& fname, uint64_t target)
{
    auto fanItr =
        std::find_if(_fans.begin(), _fans.end(), [&fname](const auto& fan) {
            return fan->getName() == fname;
        });

    if (_fans.end() != fanItr)
    {
        (*fanItr)->lockTarget(target);
    }
    else
    {
        log<level::DEBUG>(
            fmt::format("Configured fan {} not found in zone {} to lock target",
                        fname, getName())
                .c_str());
    }
}

void Zone::unlockFanTarget(const std::string& fname, uint64_t target)
{
    auto fanItr =
        std::find_if(_fans.begin(), _fans.end(), [&fname](const auto& fan) {
            return fan->getName() == fname;
        });

    if (_fans.end() != fanItr)
    {
        (*fanItr)->unlockTarget(target);

        // attempt to resume Zone target on fan
        (*fanItr)->setTarget(getTarget());
    }
    else
    {
        log<level::DEBUG>(
            fmt::format(
                "Configured fan {} not found in zone {} to unlock target",
                fname, getName())
                .c_str());
    }
}

void Zone::setTargetHold(const std::string& ident, uint64_t target, bool hold)
{
    using namespace std::string_literals;

    if (!hold)
    {
        size_t removed = _targetHolds.erase(ident);
        if (removed)
        {
            FlightRecorder::instance().log(
                "zone-target"s + getName(),
                fmt::format("{} is removing target hold", ident));
        }
    }
    else
    {
        if (!((_targetHolds.find(ident) != _targetHolds.end()) &&
              (_targetHolds[ident] == target)))
        {
            FlightRecorder::instance().log(
                "zone-target"s + getName(),
                fmt::format("{} is setting target hold to {}", ident, target));
        }
        _targetHolds[ident] = target;
        _isActive = false;
    }

    auto itHoldMax = std::max_element(_targetHolds.begin(), _targetHolds.end(),
                                      [](const auto& aHold, const auto& bHold) {
                                          return aHold.second < bHold.second;
                                      });
    if (itHoldMax == _targetHolds.end())
    {
        _isActive = true;
    }
    else
    {
        if (_target != itHoldMax->second)
        {
            FlightRecorder::instance().log(
                "zone-target"s + getName(),
                fmt::format("Settings fans to target hold of {}",
                            itHoldMax->second));
        }

        _target = itHoldMax->second;
        for (auto& fan : _fans)
        {
            fan->setTarget(_target);
        }
    }
}

void Zone::setFloorHold(const std::string& ident, uint64_t target, bool hold)
{
    using namespace std::string_literals;

    if (target > _ceiling)
    {
        target = _ceiling;
    }

    if (!hold)
    {
        size_t removed = _floorHolds.erase(ident);
        if (removed)
        {
            FlightRecorder::instance().log(
                "zone-floor"s + getName(),
                fmt::format("{} is removing floor hold", ident));
        }
    }
    else
    {
        if (!((_floorHolds.find(ident) != _floorHolds.end()) &&
              (_floorHolds[ident] == target)))
        {
            FlightRecorder::instance().log(
                "zone-floor"s + getName(),
                fmt::format("{} is setting floor hold to {}", ident, target));
        }
        _floorHolds[ident] = target;
    }

    if (!std::all_of(_floorChange.begin(), _floorChange.end(),
                     [](const auto& entry) { return entry.second; }))
    {
        return;
    }

    auto itHoldMax = std::max_element(_floorHolds.begin(), _floorHolds.end(),
                                      [](const auto& aHold, const auto& bHold) {
                                          return aHold.second < bHold.second;
                                      });
    if (itHoldMax == _floorHolds.end())
    {
        if (_floor != _defaultFloor)
        {
            FlightRecorder::instance().log(
                "zone-floor"s + getName(),
                fmt::format("No set floor exists, using default floor",
                            _defaultFloor));
        }
        _floor = _defaultFloor;
    }
    else
    {
        if (_floor != itHoldMax->second)
        {
            FlightRecorder::instance().log(
                "zone-floor"s + getName(),
                fmt::format("Setting new floor to {}", itHoldMax->second));
        }
        _floor = itHoldMax->second;
    }

    // Floor above target, update target to floor
    if (_target < _floor)
    {
        requestIncrease(_floor - _target);
    }
}

void Zone::setFloor(uint64_t target)
{
    // Check all entries are set to allow floor to be set
    auto pred = [](const auto& entry) { return entry.second; };
    if (std::all_of(_floorChange.begin(), _floorChange.end(), pred))
    {
        _floor = (target > _ceiling) ? _ceiling : target;
        // Floor above target, update target to floor
        if (_target < _floor)
        {
            requestIncrease(_floor - _target);
        }
    }
}

void Zone::requestIncrease(uint64_t targetDelta)
{
    // Only increase when delta is higher than the current increase delta for
    // the zone and currently under ceiling
    if (targetDelta > _incDelta && _target < _ceiling)
    {
        auto requestTarget = getRequestTargetBase();
        requestTarget = (targetDelta - _incDelta) + requestTarget;
        _incDelta = targetDelta;
        // Target can not go above a current ceiling
        if (requestTarget > _ceiling)
        {
            requestTarget = _ceiling;
        }
        setTarget(requestTarget);
        // Restart timer countdown for fan target increase
        _incTimer.restartOnce(_incDelay);
    }
}

void Zone::incTimerExpired()
{
    // Clear increase delta when timer expires allowing additional target
    // increase requests or target decreases to occur
    _incDelta = 0;
}

void Zone::requestDecrease(uint64_t targetDelta)
{
    // Only decrease the lowest target delta requested
    if (_decDelta == 0 || targetDelta < _decDelta)
    {
        _decDelta = targetDelta;
    }
}

void Zone::decTimerExpired()
{
    // Check all entries are set to allow a decrease
    auto pred = [](auto const& entry) { return entry.second; };
    auto decAllowed = std::all_of(_decAllowed.begin(), _decAllowed.end(), pred);

    // Only decrease targets when allowed, a requested decrease target delta
    // exists, where no requested increases exist and the increase timer is not
    // running (i.e. not in the middle of increasing)
    if (decAllowed && _decDelta != 0 && _incDelta == 0 &&
        !_incTimer.isEnabled())
    {
        auto requestTarget = getRequestTargetBase();
        // Request target should not start above ceiling
        if (requestTarget > _ceiling)
        {
            requestTarget = _ceiling;
        }
        // Target can not go below the defined floor
        if ((requestTarget < _decDelta) || (requestTarget - _decDelta < _floor))
        {
            requestTarget = _floor;
        }
        else
        {
            requestTarget = requestTarget - _decDelta;
        }
        setTarget(requestTarget);
    }
    // Clear decrease delta when timer expires
    _decDelta = 0;
    // Decrease timer is restarted since its repeating
}

void Zone::setPersisted(const std::string& intf, const std::string& prop)
{
    if (std::find_if(_propsPersisted[intf].begin(), _propsPersisted[intf].end(),
                     [&prop](const auto& p) { return prop == p; }) ==
        _propsPersisted[intf].end())
    {
        _propsPersisted[intf].emplace_back(prop);
    }
}

bool Zone::isPersisted(const std::string& intf, const std::string& prop) const
{
    auto it = _propsPersisted.find(intf);
    if (it == _propsPersisted.end())
    {
        return false;
    }

    return std::any_of(it->second.begin(), it->second.end(),
                       [&prop](const auto& p) { return prop == p; });
}

void Zone::setPowerOnTarget(const json& jsonObj)
{
    if (!jsonObj.contains("poweron_target"))
    {
        auto msg = "Missing required zone's poweron target";
        log<level::ERR>(msg, entry("JSON=%s", jsonObj.dump().c_str()));
        throw std::runtime_error(msg);
    }
    _poweronTarget = jsonObj["poweron_target"].get<uint64_t>();
}

void Zone::setInterfaces(const json& jsonObj)
{
    for (const auto& interface : jsonObj["interfaces"])
    {
        if (!interface.contains("name") || !interface.contains("properties"))
        {
            log<level::ERR>("Missing required zone interface attributes",
                            entry("JSON=%s", interface.dump().c_str()));
            throw std::runtime_error(
                "Missing required zone interface attributes");
        }
        auto propFuncs =
            _intfPropHandlers.find(interface["name"].get<std::string>());
        if (propFuncs == _intfPropHandlers.end())
        {
            // Construct list of available configurable interfaces
            auto intfs = std::accumulate(
                std::next(_intfPropHandlers.begin()), _intfPropHandlers.end(),
                _intfPropHandlers.begin()->first, [](auto list, auto intf) {
                    return std::move(list) + ", " + intf.first;
                });
            log<level::ERR>("Configured interface not available",
                            entry("JSON=%s", interface.dump().c_str()),
                            entry("AVAILABLE_INTFS=%s", intfs.c_str()));
            throw std::runtime_error("Configured interface not available");
        }

        for (const auto& property : interface["properties"])
        {
            if (!property.contains("name"))
            {
                log<level::ERR>(
                    "Missing required interface property attributes",
                    entry("JSON=%s", property.dump().c_str()));
                throw std::runtime_error(
                    "Missing required interface property attributes");
            }
            // Attribute "persist" is optional, defaults to `false`
            auto persist = false;
            if (property.contains("persist"))
            {
                persist = property["persist"].get<bool>();
            }
            // Property name from JSON must exactly match supported
            // index names to functions in property namespace
            auto propFunc =
                propFuncs->second.find(property["name"].get<std::string>());
            if (propFunc == propFuncs->second.end())
            {
                // Construct list of available configurable properties
                auto props = std::accumulate(
                    std::next(propFuncs->second.begin()),
                    propFuncs->second.end(), propFuncs->second.begin()->first,
                    [](auto list, auto prop) {
                        return std::move(list) + ", " + prop.first;
                    });
                log<level::ERR>("Configured property not available",
                                entry("JSON=%s", property.dump().c_str()),
                                entry("AVAILABLE_PROPS=%s", props.c_str()));
                throw std::runtime_error(
                    "Configured property function not available");
            }

            _propInitFunctions.emplace_back(
                propFunc->second(property, persist));
        }
    }
}

json Zone::dump() const
{
    json output;

    output["active"] = _isActive;
    output["floor"] = _floor;
    output["ceiling"] = _ceiling;
    output["target"] = _target;
    output["increase_delta"] = _incDelta;
    output["decrease_delta"] = _decDelta;
    output["power_on_target"] = _poweronTarget;
    output["default_ceiling"] = _defaultCeiling;
    output["default_floor"] = _defaultFloor;
    output["increase_delay"] = _incDelay.count();
    output["decrease_interval"] = _decInterval.count();
    output["requested_target_base"] = _requestTargetBase;
    output["floor_change"] = _floorChange;
    output["decrease_allowed"] = _decAllowed;
    output["persisted_props"] = _propsPersisted;
    output["target_holds"] = _targetHolds;
    output["floor_holds"] = _floorHolds;

    return output;
}

/**
 * Properties of interfaces supported by the zone configuration that return
 * a handler function that sets the zone's property value(s) and persist
 * state.
 */
namespace zone::property
{
// Get a set property handler function for the configured values of the
// "Supported" property
std::function<void(DBusZone&, Zone&)> supported(const json& jsonObj,
                                                bool persist)
{
    std::vector<std::string> values;
    if (!jsonObj.contains("values"))
    {
        log<level::ERR>("No 'values' found for \"Supported\" property, "
                        "using an empty list",
                        entry("JSON=%s", jsonObj.dump().c_str()));
    }
    else
    {
        for (const auto& value : jsonObj["values"])
        {
            if (!value.contains("value"))
            {
                log<level::ERR>("No 'value' found for \"Supported\" property "
                                "entry, skipping",
                                entry("JSON=%s", value.dump().c_str()));
            }
            else
            {
                values.emplace_back(value["value"].get<std::string>());
            }
        }
    }

    return Zone::setProperty<std::vector<std::string>>(
        DBusZone::thermalModeIntf, DBusZone::supportedProp,
        &DBusZone::supported, std::move(values), persist);
}

// Get a set property handler function for a configured value of the
// "Current" property
std::function<void(DBusZone&, Zone&)> current(const json& jsonObj, bool persist)
{
    // Use default value for "Current" property if no "value" entry given
    if (!jsonObj.contains("value"))
    {
        log<level::INFO>("No 'value' found for \"Current\" property, "
                         "using default",
                         entry("JSON=%s", jsonObj.dump().c_str()));
        // Set persist state of property
        return Zone::setPropertyPersist(DBusZone::thermalModeIntf,
                                        DBusZone::currentProp, persist);
    }

    return Zone::setProperty<std::string>(
        DBusZone::thermalModeIntf, DBusZone::currentProp, &DBusZone::current,
        jsonObj["value"].get<std::string>(), persist);
}

} // namespace zone::property

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