/**
 * Copyright © 2018 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 "config.h"

#include "manager.hpp"

#include "callout.hpp"
#include "policy_find.hpp"

#include <phosphor-logging/log.hpp>

namespace ibm
{
namespace logging
{

namespace fs = std::experimental::filesystem;
using namespace phosphor::logging;

Manager::Manager(sdbusplus::bus::bus& bus) :
    bus(bus),
    addMatch(bus,
             sdbusplus::bus::match::rules::interfacesAdded() +
                 sdbusplus::bus::match::rules::path_namespace(LOGGING_PATH),
             std::bind(std::mem_fn(&Manager::interfaceAdded), this,
                       std::placeholders::_1)),
    removeMatch(bus,
                sdbusplus::bus::match::rules::interfacesRemoved() +
                    sdbusplus::bus::match::rules::path_namespace(LOGGING_PATH),
                std::bind(std::mem_fn(&Manager::interfaceRemoved), this,
                          std::placeholders::_1))
#ifdef USE_POLICY_INTERFACE
    ,
    policies(POLICY_JSON_PATH)
#endif
{
    createAll();
}

void Manager::createAll()
{
    try
    {
        auto objects = getManagedObjects(bus, LOGGING_BUSNAME, LOGGING_PATH);

        for (const auto& object : objects)
        {
            const auto& interfaces = object.second;

            auto propertyMap = interfaces.find(LOGGING_IFACE);

            if (propertyMap != interfaces.end())
            {
                createWithRestore(object.first, interfaces);
            }
        }
    }
    catch (const sdbusplus::exception::exception& e)
    {
        log<level::ERR>("sdbusplus error getting logging managed objects",
                        entry("ERROR=%s", e.what()));
    }
}

void Manager::createWithRestore(const std::string& objectPath,
                                const DbusInterfaceMap& interfaces)
{
    createObject(objectPath, interfaces);

    restoreCalloutObjects(objectPath, interfaces);
}

void Manager::create(const std::string& objectPath,
                     const DbusInterfaceMap& interfaces)
{
    createObject(objectPath, interfaces);

    createCalloutObjects(objectPath, interfaces);
}

void Manager::createObject(const std::string& objectPath,
                           const DbusInterfaceMap& interfaces)
{
#ifdef USE_POLICY_INTERFACE
    auto logInterface = interfaces.find(LOGGING_IFACE);
    createPolicyInterface(objectPath, logInterface->second);
#endif
}

void Manager::erase(EntryID id)
{
    fs::remove_all(getSaveDir(id));
    childEntries.erase(id);
    entries.erase(id);
}

void Manager::addInterface(const std::string& objectPath, InterfaceType type,
                           std::any& object)
{
    auto id = getEntryID(objectPath);
    auto entry = entries.find(id);

    if (entry == entries.end())
    {
        InterfaceMap interfaces;
        interfaces.emplace(type, object);
        entries.emplace(id, std::move(interfaces));
    }
    else
    {
        entry->second.emplace(type, object);
    }
}

void Manager::addChildInterface(const std::string& objectPath,
                                InterfaceType type, std::any& object)
{
    auto id = getEntryID(objectPath);
    auto entry = childEntries.find(id);

    // childEntries is:
    // A map of error log entry IDs to:
    //  a map of interface types to:
    //    a vector of interface objects

    if (entry == childEntries.end())
    {
        ObjectList objects{object};
        InterfaceMapMulti interfaces;
        interfaces.emplace(type, std::move(objects));
        childEntries.emplace(id, std::move(interfaces));
    }
    else
    {
        auto i = entry->second.find(type);
        if (i == entry->second.end())
        {
            ObjectList objects{objects};
            entry->second.emplace(type, objects);
        }
        else
        {
            i->second.emplace_back(object);
        }
    }
}

#ifdef USE_POLICY_INTERFACE
void Manager::createPolicyInterface(const std::string& objectPath,
                                    const DbusPropertyMap& properties)
{
    auto values = policy::find(policies, properties);

    auto object = std::make_shared<PolicyObject>(bus, objectPath.c_str(), true);

    object->eventID(std::get<policy::EIDField>(values));
    object->description(std::get<policy::MsgField>(values));

    object->emit_object_added();

    std::any anyObject = object;

    addInterface(objectPath, InterfaceType::POLICY, anyObject);
}
#endif

void Manager::createCalloutObjects(const std::string& objectPath,
                                   const DbusInterfaceMap& interfaces)
{
    // Use the associations property in the org.openbmc.Associations
    // interface to find any callouts.  Then grab all properties on
    // the Asset interface for that object in the inventory to use
    // in our callout objects.

    auto associations = interfaces.find(ASSOC_IFACE);
    if (associations == interfaces.end())
    {
        return;
    }

    const auto& properties = associations->second;
    auto assocProperty = properties.find("Associations");
    auto assocValue = std::get<AssociationsPropertyType>(assocProperty->second);

    auto id = getEntryID(objectPath);
    auto calloutNum = 0;
    DbusSubtree subtree;

    for (const auto& association : assocValue)
    {
        try
        {
            if (std::get<forwardPos>(association) != "callout")
            {
                continue;
            }

            auto callout = std::get<endpointPos>(association);

            if (subtree.empty())
            {
                subtree = getSubtree(bus, "/", 0, ASSET_IFACE);
                if (subtree.empty())
                {
                    break;
                }
            }

            auto service = getService(callout, ASSET_IFACE, subtree);
            if (service.empty())
            {
                continue;
            }

            auto properties =
                getAllProperties(bus, service, callout, ASSET_IFACE);
            if (properties.empty())
            {
                continue;
            }

            auto calloutPath = getCalloutObjectPath(objectPath, calloutNum);

            auto object = std::make_shared<Callout>(
                bus, calloutPath, callout, calloutNum,
                getLogTimestamp(interfaces), properties);

            auto dir = getCalloutSaveDir(id);
            if (!fs::exists(dir))
            {
                fs::create_directories(dir);
            }
            object->serialize(dir);

            std::any anyObject = object;
            addChildInterface(objectPath, InterfaceType::CALLOUT, anyObject);
            calloutNum++;
        }
        catch (const sdbusplus::exception::exception& e)
        {
            log<level::ERR>("sdbusplus exception", entry("ERROR=%s", e.what()));
        }
    }
}

void Manager::restoreCalloutObjects(const std::string& objectPath,
                                    const DbusInterfaceMap& interfaces)
{
    auto saveDir = getCalloutSaveDir(getEntryID(objectPath));

    if (!fs::exists(saveDir))
    {
        return;
    }

    size_t id;
    for (auto& f : fs::directory_iterator(saveDir))
    {
        try
        {
            id = std::stoul(f.path().filename());
        }
        catch (const std::exception& e)
        {
            log<level::ERR>("Invalid IBM logging callout save file. Deleting",
                            entry("FILE=%s", f.path().c_str()));
            fs::remove(f.path());
            continue;
        }

        auto path = getCalloutObjectPath(objectPath, id);
        auto callout = std::make_shared<Callout>(bus, path, id,
                                                 getLogTimestamp(interfaces));
        if (callout->deserialize(saveDir))
        {
            callout->emit_object_added();
            std::any anyObject = callout;
            addChildInterface(objectPath, InterfaceType::CALLOUT, anyObject);
        }
    }
}

void Manager::interfaceAdded(sdbusplus::message::message& msg)
{
    sdbusplus::message::object_path path;
    DbusInterfaceMap interfaces;

    msg.read(path, interfaces);

    // Find the Logging.Entry interface with all of its properties
    // to pass to create().
    if (interfaces.find(LOGGING_IFACE) != interfaces.end())
    {
        create(path, interfaces);
    }
}

uint64_t Manager::getLogTimestamp(const DbusInterfaceMap& interfaces)
{
    auto interface = interfaces.find(LOGGING_IFACE);
    if (interface != interfaces.end())
    {
        auto property = interface->second.find("Timestamp");
        if (property != interface->second.end())
        {
            return std::get<uint64_t>(property->second);
        }
    }

    return 0;
}

fs::path Manager::getSaveDir(EntryID id)
{
    return fs::path{ERRLOG_PERSIST_PATH} / std::to_string(id);
}

fs::path Manager::getCalloutSaveDir(EntryID id)
{
    return getSaveDir(id) / "callouts";
}

std::string Manager::getCalloutObjectPath(const std::string& objectPath,
                                          uint32_t calloutNum)
{
    return fs::path{objectPath} / "callouts" / std::to_string(calloutNum);
}

void Manager::interfaceRemoved(sdbusplus::message::message& msg)
{
    sdbusplus::message::object_path path;
    DbusInterfaceList interfaces;

    msg.read(path, interfaces);

    // If the Logging.Entry interface was removed, then remove
    // our object

    auto i = std::find(interfaces.begin(), interfaces.end(), LOGGING_IFACE);

    if (i != interfaces.end())
    {
        erase(getEntryID(path));
    }
}
} // namespace logging
} // namespace ibm