#pragma once

#include <phosphor-logging/elog-errors.hpp>
#include <phosphor-logging/elog.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/bus/match.hpp>
#include <sdbusplus/message.hpp>
#include <xyz/openbmc_project/Common/error.hpp>

#include <format>

namespace phosphor
{
namespace fan
{
namespace util
{
namespace detail
{
namespace errors = sdbusplus::xyz::openbmc_project::Common::Error;
} // namespace detail

/**
 * @class DBusError
 *
 * The base class for the exceptions thrown on fails in the various
 * SDBusPlus calls.  Used so that a single catch statement can catch
 * any type of these exceptions.
 *
 * None of these exceptions will log anything when they are created,
 * it is up to the handler to do that if desired.
 */
class DBusError : public std::runtime_error
{
  public:
    explicit DBusError(const std::string& msg) : std::runtime_error(msg) {}
};

/**
 * @class DBusMethodError
 *
 * Thrown on a DBus Method call failure
 */
class DBusMethodError : public DBusError
{
  public:
    DBusMethodError(const std::string& busName, const std::string& path,
                    const std::string& interface, const std::string& method) :
        DBusError(std::format("DBus method failed: {} {} {} {}", busName, path,
                              interface, method)),
        busName(busName), path(path), interface(interface), method(method)
    {}

    const std::string busName;
    const std::string path;
    const std::string interface;
    const std::string method;
};

/**
 * @class DBusServiceError
 *
 * Thrown when a service lookup fails.  Usually this points to
 * the object path not being present in D-Bus.
 */
class DBusServiceError : public DBusError
{
  public:
    DBusServiceError(const std::string& path, const std::string& interface) :
        DBusError(
            std::format("DBus service lookup failed: {} {}", path, interface)),
        path(path), interface(interface)
    {}

    const std::string path;
    const std::string interface;
};

/**
 * @class DBusPropertyError
 *
 * Thrown when a set/get property fails.
 */
class DBusPropertyError : public DBusError
{
  public:
    DBusPropertyError(const std::string& msg, const std::string& busName,
                      const std::string& path, const std::string& interface,
                      const std::string& property) :
        DBusError(msg + std::format(": {} {} {} {}", busName, path, interface,
                                    property)),
        busName(busName), path(path), interface(interface), property(property)
    {}

    const std::string busName;
    const std::string path;
    const std::string interface;
    const std::string property;
};

/** @brief Alias for PropertiesChanged signal callbacks. */
template <typename... T>
using Properties = std::map<std::string, std::variant<T...>>;

/** @class SDBusPlus
 *  @brief DBus access delegate implementation for sdbusplus.
 */
class SDBusPlus
{
  public:
    /** @brief Get the bus connection. */
    static auto& getBus() __attribute__((pure))
    {
        static auto bus = sdbusplus::bus::new_default();
        return bus;
    }

    /** @brief Invoke a method. */
    template <typename... Args>
    static auto callMethod(sdbusplus::bus_t& bus, const std::string& busName,
                           const std::string& path,
                           const std::string& interface,
                           const std::string& method, Args&&... args)
    {
        auto reqMsg = bus.new_method_call(busName.c_str(), path.c_str(),
                                          interface.c_str(), method.c_str());
        reqMsg.append(std::forward<Args>(args)...);
        try
        {
            auto respMsg = bus.call(reqMsg);
            if (respMsg.is_method_error())
            {
                throw DBusMethodError{busName, path, interface, method};
            }
            return respMsg;
        }
        catch (const sdbusplus::exception_t&)
        {
            throw DBusMethodError{busName, path, interface, method};
        }
    }

    /** @brief Invoke a method. */
    template <typename... Args>
    static auto callMethod(const std::string& busName, const std::string& path,
                           const std::string& interface,
                           const std::string& method, Args&&... args)
    {
        return callMethod(getBus(), busName, path, interface, method,
                          std::forward<Args>(args)...);
    }

    /** @brief Invoke a method and read the response. */
    template <typename Ret, typename... Args>
    static auto
        callMethodAndRead(sdbusplus::bus_t& bus, const std::string& busName,
                          const std::string& path, const std::string& interface,
                          const std::string& method, Args&&... args)
    {
        sdbusplus::message_t respMsg = callMethod<Args...>(
            bus, busName, path, interface, method, std::forward<Args>(args)...);
        Ret resp;
        respMsg.read(resp);
        return resp;
    }

    /** @brief Invoke a method and read the response. */
    template <typename Ret, typename... Args>
    static auto callMethodAndRead(
        const std::string& busName, const std::string& path,
        const std::string& interface, const std::string& method, Args&&... args)
    {
        return callMethodAndRead<Ret>(getBus(), busName, path, interface,
                                      method, std::forward<Args>(args)...);
    }

    /** @brief Get subtree from the mapper without checking response. */
    static auto getSubTreeRaw(sdbusplus::bus_t& bus, const std::string& path,
                              const std::string& interface, int32_t depth)
    {
        using namespace std::literals::string_literals;

        using Path = std::string;
        using Intf = std::string;
        using Serv = std::string;
        using Intfs = std::vector<Intf>;
        using Objects = std::map<Path, std::map<Serv, Intfs>>;
        Intfs intfs = {interface};

        return callMethodAndRead<Objects>(
            bus, "xyz.openbmc_project.ObjectMapper"s,
            "/xyz/openbmc_project/object_mapper"s,
            "xyz.openbmc_project.ObjectMapper"s, "GetSubTree"s, path, depth,
            intfs);
    }

    /** @brief Get subtree from the mapper without checking response,
     * (multiple interfaces version). */
    static auto getSubTreeRaw(sdbusplus::bus_t& bus, const std::string& path,
                              const std::vector<std::string>& intfs,
                              int32_t depth)
    {
        using namespace std::literals::string_literals;

        using Path = std::string;
        using Intf = std::string;
        using Serv = std::string;
        using Intfs = std::vector<Intf>;
        using Objects = std::map<Path, std::map<Serv, Intfs>>;

        return callMethodAndRead<Objects>(
            bus, "xyz.openbmc_project.ObjectMapper"s,
            "/xyz/openbmc_project/object_mapper"s,
            "xyz.openbmc_project.ObjectMapper"s, "GetSubTree"s, path, depth,
            intfs);
    }

    /** @brief Get subtree from the mapper. */
    static auto getSubTree(sdbusplus::bus_t& bus, const std::string& path,
                           const std::string& interface, int32_t depth)
    {
        auto mapperResp = getSubTreeRaw(bus, path, interface, depth);
        if (mapperResp.empty())
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                "Empty response from mapper GetSubTree",
                phosphor::logging::entry("SUBTREE=%s", path.c_str()),
                phosphor::logging::entry("INTERFACE=%s", interface.c_str()),
                phosphor::logging::entry("DEPTH=%u", depth));
            phosphor::logging::elog<detail::errors::InternalFailure>();
        }
        return mapperResp;
    }

    /** @brief Get subtree paths from the mapper without checking response. */
    static auto getSubTreePathsRaw(sdbusplus::bus_t& bus,
                                   const std::string& path,
                                   const std::string& interface, int32_t depth)
    {
        using namespace std::literals::string_literals;

        using Path = std::string;
        using Intf = std::string;
        using Intfs = std::vector<Intf>;
        using ObjectPaths = std::vector<Path>;
        Intfs intfs = {interface};

        return callMethodAndRead<ObjectPaths>(
            bus, "xyz.openbmc_project.ObjectMapper"s,
            "/xyz/openbmc_project/object_mapper"s,
            "xyz.openbmc_project.ObjectMapper"s, "GetSubTreePaths"s, path,
            depth, intfs);
    }

    /** @brief Get subtree paths from the mapper. */
    static auto getSubTreePaths(sdbusplus::bus_t& bus, const std::string& path,
                                const std::string& interface, int32_t depth)
    {
        auto mapperResp = getSubTreePathsRaw(bus, path, interface, depth);
        if (mapperResp.empty())
        {
            phosphor::logging::log<phosphor::logging::level::ERR>(
                "Empty response from mapper GetSubTreePaths",
                phosphor::logging::entry("SUBTREE=%s", path.c_str()),
                phosphor::logging::entry("INTERFACE=%s", interface.c_str()),
                phosphor::logging::entry("DEPTH=%u", depth));
            phosphor::logging::elog<detail::errors::InternalFailure>();
        }
        return mapperResp;
    }

    /** @brief Get service from the mapper without checking response. */
    static auto getServiceRaw(sdbusplus::bus_t& bus, const std::string& path,
                              const std::string& interface)
    {
        using namespace std::literals::string_literals;
        using GetObject = std::map<std::string, std::vector<std::string>>;

        return callMethodAndRead<GetObject>(
            bus, "xyz.openbmc_project.ObjectMapper"s,
            "/xyz/openbmc_project/object_mapper"s,
            "xyz.openbmc_project.ObjectMapper"s, "GetObject"s, path,
            GetObject::mapped_type{interface});
    }

    /** @brief Get service from the mapper. */
    static auto getService(sdbusplus::bus_t& bus, const std::string& path,
                           const std::string& interface)
    {
        try
        {
            auto mapperResp = getServiceRaw(bus, path, interface);

            if (mapperResp.empty())
            {
                // Should never happen.  A missing object would fail
                // in callMethodAndRead()
                phosphor::logging::log<phosphor::logging::level::ERR>(
                    "Empty mapper response on service lookup");
                throw DBusServiceError{path, interface};
            }
            return mapperResp.begin()->first;
        }
        catch (const DBusMethodError& e)
        {
            throw DBusServiceError{path, interface};
        }
    }

    /** @brief Get service from the mapper. */
    static auto getService(const std::string& path,
                           const std::string& interface)
    {
        return getService(getBus(), path, interface);
    }

    /** @brief Get managed objects. */
    template <typename Variant>
    static auto getManagedObjects(sdbusplus::bus_t& bus,
                                  const std::string& service,
                                  const std::string& path)
    {
        using namespace std::literals::string_literals;

        using Path = sdbusplus::message::object_path;
        using Intf = std::string;
        using Prop = std::string;
        using GetManagedObjects =
            std::map<Path, std::map<Intf, std::map<Prop, Variant>>>;

        return callMethodAndRead<GetManagedObjects>(
            bus, service, path, "org.freedesktop.DBus.ObjectManager"s,
            "GetManagedObjects"s);
    }

    /** @brief Get a property with mapper lookup. */
    template <typename Property>
    static auto getProperty(sdbusplus::bus_t& bus, const std::string& path,
                            const std::string& interface,
                            const std::string& property)
    {
        using namespace std::literals::string_literals;

        auto service = getService(bus, path, interface);
        auto msg =
            callMethod(bus, service, path, "org.freedesktop.DBus.Properties"s,
                       "Get"s, interface, property);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus get property failed", service, path,
                                    interface, property};
        }
        std::variant<Property> value;
        msg.read(value);
        return std::get<Property>(value);
    }

    /** @brief Get a property with mapper lookup. */
    template <typename Property>
    static auto getProperty(const std::string& path,
                            const std::string& interface,
                            const std::string& property)
    {
        return getProperty<Property>(getBus(), path, interface, property);
    }

    /** @brief Get a property variant with mapper lookup. */
    template <typename Variant>
    static auto getPropertyVariant(
        sdbusplus::bus_t& bus, const std::string& path,
        const std::string& interface, const std::string& property)
    {
        using namespace std::literals::string_literals;

        auto service = getService(bus, path, interface);
        auto msg =
            callMethod(bus, service, path, "org.freedesktop.DBus.Properties"s,
                       "Get"s, interface, property);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus get property variant failed", service,
                                    path, interface, property};
        }
        Variant value;
        msg.read(value);
        return value;
    }

    /** @brief Get a property variant with mapper lookup. */
    template <typename Variant>
    static auto getPropertyVariant(const std::string& path,
                                   const std::string& interface,
                                   const std::string& property)
    {
        return getPropertyVariant<Variant>(getBus(), path, interface, property);
    }

    /** @brief Invoke a method and return without checking for error. */
    template <typename... Args>
    static auto callMethodAndReturn(
        sdbusplus::bus_t& bus, const std::string& busName,
        const std::string& path, const std::string& interface,
        const std::string& method, Args&&... args)
    {
        auto reqMsg = bus.new_method_call(busName.c_str(), path.c_str(),
                                          interface.c_str(), method.c_str());
        reqMsg.append(std::forward<Args>(args)...);
        auto respMsg = bus.call(reqMsg);

        return respMsg;
    }

    /** @brief Get a property without mapper lookup. */
    template <typename Property>
    static auto getProperty(sdbusplus::bus_t& bus, const std::string& service,
                            const std::string& path,
                            const std::string& interface,
                            const std::string& property)
    {
        using namespace std::literals::string_literals;

        auto msg = callMethodAndReturn(bus, service, path,
                                       "org.freedesktop.DBus.Properties"s,
                                       "Get"s, interface, property);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus get property failed", service, path,
                                    interface, property};
        }
        std::variant<Property> value;
        msg.read(value);
        return std::get<Property>(value);
    }

    /** @brief Get a property without mapper lookup. */
    template <typename Property>
    static auto getProperty(const std::string& service, const std::string& path,
                            const std::string& interface,
                            const std::string& property)
    {
        return getProperty<Property>(getBus(), service, path, interface,
                                     property);
    }

    /** @brief Get a property variant without mapper lookup. */
    template <typename Variant>
    static auto getPropertyVariant(
        sdbusplus::bus_t& bus, const std::string& service,
        const std::string& path, const std::string& interface,
        const std::string& property)
    {
        using namespace std::literals::string_literals;

        auto msg = callMethodAndReturn(bus, service, path,
                                       "org.freedesktop.DBus.Properties"s,
                                       "Get"s, interface, property);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus get property variant failed", service,
                                    path, interface, property};
        }
        Variant value;
        msg.read(value);
        return value;
    }

    /** @brief Get a property variant without mapper lookup. */
    template <typename Variant>
    static auto getPropertyVariant(
        const std::string& service, const std::string& path,
        const std::string& interface, const std::string& property)
    {
        return getPropertyVariant<Variant>(getBus(), service, path, interface,
                                           property);
    }

    /** @brief Set a property with mapper lookup. */
    template <typename Property>
    static void setProperty(sdbusplus::bus_t& bus, const std::string& path,
                            const std::string& interface,
                            const std::string& property, Property&& value)
    {
        using namespace std::literals::string_literals;

        std::variant<Property> varValue(std::forward<Property>(value));

        auto service = getService(bus, path, interface);
        auto msg = callMethodAndReturn(bus, service, path,
                                       "org.freedesktop.DBus.Properties"s,
                                       "Set"s, interface, property, varValue);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus set property failed", service, path,
                                    interface, property};
        }
    }

    /** @brief Set a property with mapper lookup. */
    template <typename Property>
    static void setProperty(const std::string& path,
                            const std::string& interface,
                            const std::string& property, Property&& value)
    {
        return setProperty(getBus(), path, interface, property,
                           std::forward<Property>(value));
    }

    /** @brief Set a property without mapper lookup. */
    template <typename Property>
    static void setProperty(sdbusplus::bus_t& bus, const std::string& service,
                            const std::string& path,
                            const std::string& interface,
                            const std::string& property, Property&& value)
    {
        using namespace std::literals::string_literals;

        std::variant<Property> varValue(std::forward<Property>(value));

        auto msg = callMethodAndReturn(bus, service, path,
                                       "org.freedesktop.DBus.Properties"s,
                                       "Set"s, interface, property, varValue);
        if (msg.is_method_error())
        {
            throw DBusPropertyError{"DBus set property failed", service, path,
                                    interface, property};
        }
    }

    /** @brief Set a property without mapper lookup. */
    template <typename Property>
    static void setProperty(const std::string& service, const std::string& path,
                            const std::string& interface,
                            const std::string& property, Property&& value)
    {
        return setProperty(getBus(), service, path, interface, property,
                           std::forward<Property>(value));
    }

    /** @brief Invoke method with mapper lookup. */
    template <typename... Args>
    static auto lookupAndCallMethod(
        sdbusplus::bus_t& bus, const std::string& path,
        const std::string& interface, const std::string& method, Args&&... args)
    {
        return callMethod(bus, getService(bus, path, interface), path,
                          interface, method, std::forward<Args>(args)...);
    }

    /** @brief Invoke method with mapper lookup. */
    template <typename... Args>
    static auto lookupAndCallMethod(const std::string& path,
                                    const std::string& interface,
                                    const std::string& method, Args&&... args)
    {
        return lookupAndCallMethod(getBus(), path, interface, method,
                                   std::forward<Args>(args)...);
    }

    /** @brief Invoke method and read with mapper lookup. */
    template <typename Ret, typename... Args>
    static auto lookupCallMethodAndRead(
        sdbusplus::bus_t& bus, const std::string& path,
        const std::string& interface, const std::string& method, Args&&... args)
    {
        return callMethodAndRead(bus, getService(bus, path, interface), path,
                                 interface, method,
                                 std::forward<Args>(args)...);
    }

    /** @brief Invoke method and read with mapper lookup. */
    template <typename Ret, typename... Args>
    static auto lookupCallMethodAndRead(
        const std::string& path, const std::string& interface,
        const std::string& method, Args&&... args)
    {
        return lookupCallMethodAndRead<Ret>(getBus(), path, interface, method,
                                            std::forward<Args>(args)...);
    }
};

} // namespace util
} // namespace fan
} // namespace phosphor