/**
 * Copyright © 2021 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 "power_control.hpp"

#include "config_file_parser.hpp"
#include "format_utils.hpp"
#include "types.hpp"
#include "ucd90160_device.hpp"
#include "ucd90320_device.hpp"
#include "utility.hpp"

#include <exception>
#include <format>
#include <functional>
#include <map>
#include <span>
#include <stdexcept>
#include <thread>
#include <utility>

namespace phosphor::power::sequencer
{

const std::string powerOnTimeoutError =
    "xyz.openbmc_project.Power.Error.PowerOnTimeout";

const std::string powerOffTimeoutError =
    "xyz.openbmc_project.Power.Error.PowerOffTimeout";

const std::string shutdownError = "xyz.openbmc_project.Power.Error.Shutdown";

PowerControl::PowerControl(sdbusplus::bus_t& bus,
                           const sdeventplus::Event& event) :
    PowerObject{bus, POWER_OBJ_PATH, PowerObject::action::defer_emit}, bus{bus},
    services{bus},
    pgoodWaitTimer{event, std::bind(&PowerControl::onFailureCallback, this)},
    powerOnAllowedTime{std::chrono::steady_clock::now() + minimumColdStartTime},
    timer{event, std::bind(&PowerControl::pollPgood, this), pollInterval}
{
    // Obtain dbus service name
    bus.request_name(POWER_IFACE);

    compatSysTypesFinder = std::make_unique<util::CompatibleSystemTypesFinder>(
        bus, std::bind_front(&PowerControl::compatibleSystemTypesFound, this));

    deviceFinder = std::make_unique<DeviceFinder>(
        bus, std::bind_front(&PowerControl::deviceFound, this));

    setUpGpio();
}

int PowerControl::getPgood() const
{
    return pgood;
}

int PowerControl::getPgoodTimeout() const
{
    return timeout.count();
}

int PowerControl::getState() const
{
    return state;
}

void PowerControl::onFailureCallback()
{
    services.logInfoMsg("After onFailure wait");

    onFailure(false);

    // Power good has failed, call for chassis hard power off
    auto method = bus.new_method_call(util::SYSTEMD_SERVICE, util::SYSTEMD_ROOT,
                                      util::SYSTEMD_INTERFACE, "StartUnit");
    method.append(util::POWEROFF_TARGET);
    method.append("replace");
    bus.call_noreply(method);
}

void PowerControl::onFailure(bool wasTimeOut)
{
    std::string error;
    std::map<std::string, std::string> additionalData{};

    // Check if pgood fault occurred on rail monitored by power sequencer device
    if (device)
    {
        try
        {
            error = device->findPgoodFault(services, powerSupplyError,
                                           additionalData);
        }
        catch (const std::exception& e)
        {
            services.logErrorMsg(e.what());
            additionalData.emplace("ERROR", e.what());
        }
    }

    // If fault was not isolated to a voltage rail, select a more generic error
    if (error.empty())
    {
        if (!powerSupplyError.empty())
        {
            error = powerSupplyError;
        }
        else if (wasTimeOut)
        {
            error = powerOnTimeoutError;
        }
        else
        {
            error = shutdownError;
        }
    }

    services.logError(error, Entry::Level::Critical, additionalData);

    if (!wasTimeOut)
    {
        services.createBMCDump();
    }
}

void PowerControl::pollPgood()
{
    if (inStateTransition)
    {
        // In transition between power on and off, check for timeout
        const auto now = std::chrono::steady_clock::now();
        if (now > pgoodTimeoutTime)
        {
            services.logErrorMsg(std::format(
                "Power state transition timeout, state: {}", state));
            inStateTransition = false;

            if (state)
            {
                // Time out powering on
                onFailure(true);
            }
            else
            {
                // Time out powering off
                std::map<std::string, std::string> additionalData{};
                services.logError(powerOffTimeoutError, Entry::Level::Critical,
                                  additionalData);
            }

            failureFound = true;
            return;
        }
    }

    int pgoodState = pgoodLine.get_value();
    if (pgoodState != pgood)
    {
        // Power good has changed since last read
        pgood = pgoodState;
        if (pgoodState == 0)
        {
            emitPowerLostSignal();
        }
        else
        {
            emitPowerGoodSignal();
            // Clear any errors on the transition to power on
            powerSupplyError.clear();
            failureFound = false;
        }
        emitPropertyChangedSignal("pgood");
    }
    if (pgoodState == state)
    {
        // Power good matches requested state
        inStateTransition = false;
    }
    else if (!inStateTransition && (pgoodState == 0) && !failureFound)
    {
        // Not in power off state, not changing state, and power good is off
        services.logErrorMsg("Chassis pgood failure");
        pgoodWaitTimer.restartOnce(std::chrono::seconds(7));
        failureFound = true;
    }
}

void PowerControl::setPgoodTimeout(int t)
{
    if (timeout.count() != t)
    {
        timeout = std::chrono::seconds(t);
        emitPropertyChangedSignal("pgood_timeout");
    }
}

void PowerControl::setPowerSupplyError(const std::string& error)
{
    powerSupplyError = error;
}

void PowerControl::setState(int s)
{
    if (state == s)
    {
        services.logInfoMsg(
            std::format("Power already at requested state: {}", state));
        return;
    }
    if (s == 0)
    {
        // Wait for two seconds when powering down. This is to allow host and
        // other BMC applications time to complete power off processing
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
    else
    {
        // If minimum power off time has not passed, wait
        if (powerOnAllowedTime > std::chrono::steady_clock::now())
        {
            services.logInfoMsg(std::format(
                "Waiting {} seconds until power on allowed",
                std::chrono::duration_cast<std::chrono::seconds>(
                    powerOnAllowedTime - std::chrono::steady_clock::now())
                    .count()));
        }
        std::this_thread::sleep_until(powerOnAllowedTime);
    }

    services.logInfoMsg(std::format("setState: {}", s));
    services.logInfoMsg(std::format("Powering chassis {}", (s ? "on" : "off")));
    powerControlLine.request(
        {"phosphor-power-control", gpiod::line_request::DIRECTION_OUTPUT, 0});
    powerControlLine.set_value(s);
    powerControlLine.release();

    if (s == 0)
    {
        // Set a minimum amount of time to wait before next power on
        powerOnAllowedTime =
            std::chrono::steady_clock::now() + minimumPowerOffTime;
    }

    pgoodTimeoutTime = std::chrono::steady_clock::now() + timeout;
    inStateTransition = true;
    state = s;
    emitPropertyChangedSignal("state");
}

void PowerControl::compatibleSystemTypesFound(
    const std::vector<std::string>& types)
{
    // If we don't already have compatible system types
    if (compatibleSystemTypes.empty())
    {
        std::string typesStr = format_utils::toString(std::span{types});
        services.logInfoMsg(
            std::format("Compatible system types found: {}", typesStr));

        // Store compatible system types
        compatibleSystemTypes = types;

        // Load config file and create device object if possible
        loadConfigFileAndCreateDevice();
    }
}

void PowerControl::deviceFound(const DeviceProperties& properties)
{
    // If we don't already have device properties
    if (!deviceProperties)
    {
        services.logInfoMsg(std::format(
            "Power sequencer device found: type={}, name={}, bus={:d}, address={:#02x}",
            properties.type, properties.name, properties.bus,
            properties.address));

        // Store device properties
        deviceProperties = properties;

        // Load config file and create device object if possible
        loadConfigFileAndCreateDevice();
    }
}

void PowerControl::setUpGpio()
{
    const std::string powerControlLineName = "power-chassis-control";
    const std::string pgoodLineName = "power-chassis-good";

    pgoodLine = gpiod::find_line(pgoodLineName);
    if (!pgoodLine)
    {
        std::string errorString{"GPIO line name not found: " + pgoodLineName};
        services.logErrorMsg(errorString);
        throw std::runtime_error(errorString);
    }
    powerControlLine = gpiod::find_line(powerControlLineName);
    if (!powerControlLine)
    {
        std::string errorString{
            "GPIO line name not found: " + powerControlLineName};
        services.logErrorMsg(errorString);
        throw std::runtime_error(errorString);
    }

    pgoodLine.request(
        {"phosphor-power-control", gpiod::line_request::DIRECTION_INPUT, 0});
    int pgoodState = pgoodLine.get_value();
    pgood = pgoodState;
    state = pgoodState;
    services.logInfoMsg(std::format("Pgood state: {}", pgoodState));
}

void PowerControl::loadConfigFileAndCreateDevice()
{
    // If compatible system types and device properties have been found
    if (!compatibleSystemTypes.empty() && deviceProperties)
    {
        // Find the JSON configuration file
        std::filesystem::path configFile = findConfigFile();
        if (!configFile.empty())
        {
            // Parse the JSON configuration file
            std::vector<std::unique_ptr<Rail>> rails;
            if (parseConfigFile(configFile, rails))
            {
                // Create the power sequencer device object
                createDevice(std::move(rails));
            }
        }
    }
}

std::filesystem::path PowerControl::findConfigFile()
{
    // Find config file for current system based on compatible system types
    std::filesystem::path configFile;
    if (!compatibleSystemTypes.empty())
    {
        try
        {
            configFile = config_file_parser::find(compatibleSystemTypes);
            if (!configFile.empty())
            {
                services.logInfoMsg(std::format(
                    "JSON configuration file found: {}", configFile.string()));
            }
        }
        catch (const std::exception& e)
        {
            services.logErrorMsg(std::format(
                "Unable to find JSON configuration file: {}", e.what()));
        }
    }
    return configFile;
}

bool PowerControl::parseConfigFile(const std::filesystem::path& configFile,
                                   std::vector<std::unique_ptr<Rail>>& rails)
{
    // Parse JSON configuration file
    bool wasParsed{false};
    try
    {
        rails = config_file_parser::parse(configFile);
        wasParsed = true;
    }
    catch (const std::exception& e)
    {
        services.logErrorMsg(std::format(
            "Unable to parse JSON configuration file: {}", e.what()));
    }
    return wasParsed;
}

void PowerControl::createDevice(std::vector<std::unique_ptr<Rail>> rails)
{
    // Create power sequencer device based on device properties
    if (deviceProperties)
    {
        try
        {
            if (deviceProperties->type == UCD90160Device::deviceName)
            {
                device = std::make_unique<UCD90160Device>(
                    std::move(rails), services, deviceProperties->bus,
                    deviceProperties->address);
            }
            else if (deviceProperties->type == UCD90320Device::deviceName)
            {
                device = std::make_unique<UCD90320Device>(
                    std::move(rails), services, deviceProperties->bus,
                    deviceProperties->address);
            }
            else
            {
                throw std::runtime_error{std::format(
                    "Unsupported device type: {}", deviceProperties->type)};
            }
            services.logInfoMsg(std::format(
                "Power sequencer device created: {}", device->getName()));
        }
        catch (const std::exception& e)
        {
            services.logErrorMsg(
                std::format("Unable to create device object: {}", e.what()));
        }
    }
}

} // namespace phosphor::power::sequencer