/**
 * Copyright © 2024 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 "standard_device.hpp"

#include "format_utils.hpp"

#include <exception>
#include <format>
#include <span>
#include <stdexcept>

namespace phosphor::power::sequencer
{

std::string StandardDevice::findPgoodFault(
    Services& services, const std::string& powerSupplyError,
    std::map<std::string, std::string>& additionalData)
{
    std::string error{};
    try
    {
        prepareForPgoodFaultDetection(services);

        // Get all GPIO values (if possible) from device.  They may be slow to
        // obtain, so obtain them once and then pass values to each Rail object.
        std::vector<int> gpioValues = getGPIOValuesIfPossible(services);

        // Try to find a voltage rail where a pgood fault occurred
        Rail* rail =
            findRailWithPgoodFault(services, gpioValues, additionalData);
        if (rail != nullptr)
        {
            services.logErrorMsg(std::format(
                "Pgood fault found in rail monitored by device {}", name));

            // If this is a PSU rail and a PSU error was previously detected
            if (rail->isPowerSupplyRail() && !powerSupplyError.empty())
            {
                // Return power supply error as root cause
                error = powerSupplyError;
            }
            else
            {
                // Return pgood fault as root cause
                error =
                    "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault";
            }

            storePgoodFaultDebugData(services, gpioValues, additionalData);
        }
    }
    catch (const std::exception& e)
    {
        throw std::runtime_error{std::format(
            "Unable to determine if a pgood fault occurred in device {}: {}",
            name, e.what())};
    }
    return error;
}

std::vector<int> StandardDevice::getGPIOValuesIfPossible(Services& services)
{
    std::vector<int> values{};
    try
    {
        values = getGPIOValues(services);
    }
    catch (...)
    {}
    return values;
}

Rail* StandardDevice::findRailWithPgoodFault(
    Services& services, const std::vector<int>& gpioValues,
    std::map<std::string, std::string>& additionalData)
{
    // Look for the first rail in the power on sequence with a pgood fault based
    // on STATUS_VOUT.  This is usually the most accurate method.  For example,
    // if a pgood fault occurs, the power sequencer device may automatically
    // shut off related rails.  Ideally the device will only set fault bits in
    // STATUS_VOUT for the rail with the pgood fault.  However, all the related
    // rails will likely appear to be faulted by the other methods.
    for (std::unique_ptr<Rail>& rail : rails)
    {
        if (rail->hasPgoodFaultStatusVout(*this, services, additionalData))
        {
            return rail.get();
        }
    }

    // Look for the first rail in the power on sequence with a pgood fault based
    // on either a GPIO or the output voltage.  Both methods check if the rail
    // is powered off.  If a pgood fault occurs during the power on sequence,
    // the power sequencer device may stop powering on rails.  As a result, all
    // rails after the faulted one in the sequence may also be powered off.
    for (std::unique_ptr<Rail>& rail : rails)
    {
        if (rail->hasPgoodFaultGPIO(*this, services, gpioValues,
                                    additionalData) ||
            rail->hasPgoodFaultOutputVoltage(*this, services, additionalData))
        {
            return rail.get();
        }
    }

    // No rail with pgood fault found
    return nullptr;
}

void StandardDevice::storePgoodFaultDebugData(
    Services& services, const std::vector<int>& gpioValues,
    std::map<std::string, std::string>& additionalData)
{
    try
    {
        additionalData.emplace("DEVICE_NAME", name);
        storeGPIOValues(services, gpioValues, additionalData);
    }
    catch (...)
    {}
}

void StandardDevice::storeGPIOValues(
    Services& services, const std::vector<int>& values,
    std::map<std::string, std::string>& additionalData)
{
    if (!values.empty())
    {
        std::string valuesStr = format_utils::toString(std::span(values));
        services.logInfoMsg(
            std::format("Device {} GPIO values: {}", name, valuesStr));
        additionalData.emplace("GPIO_VALUES", valuesStr);
    }
}

} // namespace phosphor::power::sequencer