#pragma once

#include "sdbusplus.hpp"

#include <fmt/format.h>

#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdeventplus/source/signal.hpp>

#include <filesystem>
#include <fstream>

namespace phosphor::fan
{

namespace fs = std::filesystem;
using json = nlohmann::json;
using namespace phosphor::logging;

constexpr auto confOverridePath = "/etc/phosphor-fan-presence";
constexpr auto confBasePath = "/usr/share/phosphor-fan-presence";
constexpr auto confCompatIntf =
    "xyz.openbmc_project.Configuration.IBMCompatibleSystem";
constexpr auto confCompatProp = "Names";

class JsonConfig
{
  public:
    using ConfFileReadyFunc = std::function<void(const std::string&)>;

    JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName,
               const std::string& fileName, ConfFileReadyFunc func) :
        _appName(appName),
        _fileName(fileName), _readyFunc(func)
    {
        _match = std::make_unique<sdbusplus::server::match::match>(
            bus,
            sdbusplus::bus::match::rules::interfacesAdded() +
                sdbusplus::bus::match::rules::sender(
                    "xyz.openbmc_project.EntityManager"),
            std::bind(&JsonConfig::ifacesAddedCallback, this,
                      std::placeholders::_1));
        try
        {
            _confFile = getConfFile(bus, _appName, _fileName);
        }
        catch (const std::runtime_error& e)
        {

        }

        if (!_confFile.empty())
        {
            _match.reset();
            _readyFunc(_confFile);
        }
    }

    void ifacesAddedCallback(sdbusplus::message::message& msg)
    {
        sdbusplus::message::object_path path;
        std::map<std::string,
                 std::map<std::string, std::variant<std::vector<std::string>>>>
            interfaces;

        msg.read(path, interfaces);

        if (interfaces.find(confCompatIntf) == interfaces.end())
        {
            return;
        }

        const auto& properties = interfaces.at(confCompatIntf);
        auto names =
            std::get<std::vector<std::string>>(properties.at(confCompatProp));

        auto it =
            std::find_if(names.begin(), names.end(), [this](auto const& name) {
                auto confFile =
                    fs::path{confBasePath} / _appName / name / _fileName;
                if (fs::exists(confFile))
                {
                    _confFile = confFile;
                    return true;
                }
                return false;
            });

        if (it != names.end())
        {
            _readyFunc(_confFile);
            _match.reset();
        }
        else
        {
            log<level::ERR>(fmt::format("Could not find fan {} conf file {} "
                                        "even after {} iface became available",
                                        _appName, _fileName, confCompatIntf)
                                .c_str());
        }
    }

    static const fs::path getConfFile(sdbusplus::bus::bus& bus,
                                      const std::string& appName,
                                      const std::string& fileName,
                                      bool isOptional = false)
    {

        fs::path confFile = fs::path{confOverridePath} / appName / fileName;
        if (fs::exists(confFile))
        {
            return confFile;
        }

        confFile = fs::path{confBasePath} / appName / fileName;
        if (fs::exists(confFile))
        {
            return confFile;
        }
        confFile.clear();

        auto objects =
            util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
        for (auto& path : objects)
        {
            try
            {

                auto confCompatValue =
                    util::SDBusPlus::getProperty<std::vector<std::string>>(
                        bus, path, confCompatIntf, confCompatProp);

                auto it = std::find_if(
                    confCompatValue.begin(), confCompatValue.end(),
                    [&confFile, &appName, &fileName](auto const& entry) {
                        confFile =
                            fs::path{confBasePath} / appName / entry / fileName;
                        return fs::exists(confFile);
                    });
                if (it != confCompatValue.end())
                {

                    break;
                }
                confFile.clear();
            }
            catch (const util::DBusError&)
            {

            }
        }

        if (!isOptional && confFile.empty() && !objects.empty())
        {
            log<level::ERR>(fmt::format("Could not find fan {} conf file {}",
                                        appName, fileName)
                                .c_str());
        }

        if (confFile.empty() && !isOptional)
        {
            throw std::runtime_error("No JSON config file found");
        }

        return confFile;
    }

    static const json load(const fs::path& confFile)
    {
        std::ifstream file;
        json jsonConf;

        if (!confFile.empty() && fs::exists(confFile))
        {
            log<level::INFO>(
                fmt::format("Loading configuration from {}", confFile.string())
                    .c_str());
            file.open(confFile);
            try
            {
                jsonConf = json::parse(file);
            }
            catch (std::exception& e)
            {
                log<level::ERR>(
                    fmt::format(
                        "Failed to parse JSON config file: {}, error: {}",
                        confFile.string(), e.what())
                        .c_str());
                throw std::runtime_error(
                    fmt::format(
                        "Failed to parse JSON config file: {}, error: {}",
                        confFile.string(), e.what())
                        .c_str());
            }
        }
        else
        {
            log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
                                        confFile.string())
                                .c_str());
            throw std::runtime_error(
                fmt::format("Unable to open JSON config file: {}",
                            confFile.string())
                    .c_str());
        }

        return jsonConf;
    }

  private:

    const std::string _appName;

    const std::string _fileName;

    ConfFileReadyFunc _readyFunc;

    fs::path _confFile;

    std::unique_ptr<sdbusplus::server::match::match> _match;
};

}
/**
 * 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.
 */
#pragma once

#include "sdbusplus.hpp"

#include <fmt/format.h>

#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdeventplus/source/signal.hpp>

#include <filesystem>
#include <fstream>

namespace phosphor::fan
{

namespace fs = std::filesystem;
using json = nlohmann::json;
using namespace phosphor::logging;

constexpr auto confOverridePath = "/etc/phosphor-fan-presence";
constexpr auto confBasePath = "/usr/share/phosphor-fan-presence";
constexpr auto confCompatIntf =
    "xyz.openbmc_project.Configuration.IBMCompatibleSystem";
constexpr auto confCompatProp = "Names";

class JsonConfig
{
  public:
    using ConfFileReadyFunc = std::function<void(const std::string&)>;

    /**
     * @brief Constructor
     *
     * Looks for the JSON config file.  If it can't find one, then it
     * will watch entity-manager for the IBMCompatibleSystem interface
     * to show up and then use that data to try again.  If the config
     * file is initially present, the callback function is executed
     * with the path to the file.
     *
     * @param[in] bus - The dbus bus object
     * @param[in] appName - The appName portion of the config file path
     * @param[in] fileName - Application's configuration file's name
     * @param[in] func - The function to call when the config file
     *                   is found.
     */
    JsonConfig(sdbusplus::bus::bus& bus, const std::string& appName,
               const std::string& fileName, ConfFileReadyFunc func) :
        _appName(appName),
        _fileName(fileName), _readyFunc(func)
    {
        _match = std::make_unique<sdbusplus::server::match::match>(
            bus,
            sdbusplus::bus::match::rules::interfacesAdded() +
                sdbusplus::bus::match::rules::sender(
                    "xyz.openbmc_project.EntityManager"),
            std::bind(&JsonConfig::ifacesAddedCallback, this,
                      std::placeholders::_1));
        try
        {
            _confFile = getConfFile(bus, _appName, _fileName);
        }
        catch (const std::runtime_error& e)
        {
            // No conf file found, so let the interfacesAdded
            // match callback handle finding it.
        }

        if (!_confFile.empty())
        {
            _match.reset();
            _readyFunc(_confFile);
        }
    }

    /**
     * @brief The interfacesAdded callback function that looks for
     *        the IBMCompatibleSystem interface.  If it finds it,
     *        it uses the Names property in the interface to find
     *        the JSON config file to use.  If it finds one, it calls
     *        the _readyFunc function with the config file path.
     *
     * @param[in] msg - The D-Bus message contents
     */
    void ifacesAddedCallback(sdbusplus::message::message& msg)
    {
        sdbusplus::message::object_path path;
        std::map<std::string,
                 std::map<std::string, std::variant<std::vector<std::string>>>>
            interfaces;

        msg.read(path, interfaces);

        if (interfaces.find(confCompatIntf) == interfaces.end())
        {
            return;
        }

        const auto& properties = interfaces.at(confCompatIntf);
        auto names =
            std::get<std::vector<std::string>>(properties.at(confCompatProp));

        auto it =
            std::find_if(names.begin(), names.end(), [this](auto const& name) {
                auto confFile =
                    fs::path{confBasePath} / _appName / name / _fileName;
                if (fs::exists(confFile))
                {
                    _confFile = confFile;
                    return true;
                }
                return false;
            });

        if (it != names.end())
        {
            _readyFunc(_confFile);
            _match.reset();
        }
        else
        {
            log<level::ERR>(fmt::format("Could not find fan {} conf file {} "
                                        "even after {} iface became available",
                                        _appName, _fileName, confCompatIntf)
                                .c_str());
        }
    }

    /**
     * Get the json configuration file. The first location found to contain
     * the json config file for the given fan application is used from the
     * following locations in order.
     * 1.) From the confOverridePath location
     * 2.) From config file found using an entry from a list obtained from an
     * interface's property as a relative path extension on the base path where:
     *     interface = Interface set in confCompatIntf with the property
     *     property = Property set in confCompatProp containing a list of
     *                subdirectories in priority order to find a config
     * 3.) *DEFAULT* - From the confBasePath location
     *
     * @brief Get the configuration file to be used
     *
     * @param[in] bus - The dbus bus object
     * @param[in] appName - The phosphor-fan-presence application name
     * @param[in] fileName - Application's configuration file's name
     * @param[in] isOptional - Config file is optional, default to 'false'
     *
     * @return filesystem path
     *     The filesystem path to the configuration file to use
     */
    static const fs::path getConfFile(sdbusplus::bus::bus& bus,
                                      const std::string& appName,
                                      const std::string& fileName,
                                      bool isOptional = false)
    {
        // Check override location
        fs::path confFile = fs::path{confOverridePath} / appName / fileName;
        if (fs::exists(confFile))
        {
            return confFile;
        }

        // If the default file is there, use it
        confFile = fs::path{confBasePath} / appName / fileName;
        if (fs::exists(confFile))
        {
            return confFile;
        }
        confFile.clear();

        // Get all objects implementing the compatible interface
        auto objects =
            util::SDBusPlus::getSubTreePathsRaw(bus, "/", confCompatIntf, 0);
        for (auto& path : objects)
        {
            try
            {
                // Retrieve json config compatible relative path locations
                auto confCompatValue =
                    util::SDBusPlus::getProperty<std::vector<std::string>>(
                        bus, path, confCompatIntf, confCompatProp);
                // Look for a config file at each entry relative to the base
                // path and use the first one found
                auto it = std::find_if(
                    confCompatValue.begin(), confCompatValue.end(),
                    [&confFile, &appName, &fileName](auto const& entry) {
                        confFile =
                            fs::path{confBasePath} / appName / entry / fileName;
                        return fs::exists(confFile);
                    });
                if (it != confCompatValue.end())
                {
                    // Use the first config file found at a listed location
                    break;
                }
                confFile.clear();
            }
            catch (const util::DBusError&)
            {
                // Property unavailable on object.
            }
        }

        if (!isOptional && confFile.empty() && !objects.empty())
        {
            log<level::ERR>(fmt::format("Could not find fan {} conf file {}",
                                        appName, fileName)
                                .c_str());
        }

        if (confFile.empty() && !isOptional)
        {
            throw std::runtime_error("No JSON config file found");
        }

        return confFile;
    }

    /**
     * @brief Load the JSON config file
     *
     * @param[in] confFile - File system path of the configuration file to load
     *
     * @return Parsed JSON object
     *     The parsed JSON configuration file object
     */
    static const json load(const fs::path& confFile)
    {
        std::ifstream file;
        json jsonConf;

        if (!confFile.empty() && fs::exists(confFile))
        {
            log<level::INFO>(
                fmt::format("Loading configuration from {}", confFile.string())
                    .c_str());
            file.open(confFile);
            try
            {
                jsonConf = json::parse(file);
            }
            catch (std::exception& e)
            {
                log<level::ERR>(
                    fmt::format(
                        "Failed to parse JSON config file: {}, error: {}",
                        confFile.string(), e.what())
                        .c_str());
                throw std::runtime_error(
                    fmt::format(
                        "Failed to parse JSON config file: {}, error: {}",
                        confFile.string(), e.what())
                        .c_str());
            }
        }
        else
        {
            log<level::ERR>(fmt::format("Unable to open JSON config file: {}",
                                        confFile.string())
                                .c_str());
            throw std::runtime_error(
                fmt::format("Unable to open JSON config file: {}",
                            confFile.string())
                    .c_str());
        }

        return jsonConf;
    }

  private:
    /**
     * @brief The 'appName' portion of the config file path.
     */
    const std::string _appName;

    /**
     * @brief The config file name.
     */
    const std::string _fileName;

    /**
     * @brief The function to call when the config file is available.
     */
    ConfFileReadyFunc _readyFunc;

    /**
     * @brief The JSON config file
     */
    fs::path _confFile;

    /**
     * @brief The interfacesAdded match that is used to wait
     *        for the IBMCompatibleSystem interface to show up.
     */
    std::unique_ptr<sdbusplus::server::match::match> _match;
};

} // namespace phosphor::fan
