#include <internal/facts/solaris/networking_resolver.hpp>
#include <internal/util/posix/scoped_descriptor.hpp>
#include <facter/util/string.hpp>
#include <leatherman/execution/execution.hpp>
#include <leatherman/logging/logging.hpp>
#include <boost/algorithm/string.hpp>
#include <sys/sockio.h>
#include <net/if_arp.h>

using namespace std;
using namespace facter::util::posix;
using namespace facter::util;
using namespace leatherman::execution;

namespace facter { namespace facts { namespace solaris {

    networking_resolver::data networking_resolver::collect_data(collection& facts)
    {
        auto data = posix::networking_resolver::collect_data(facts);

        scoped_descriptor ctl(socket(AF_INET, SOCK_DGRAM, 0));
        if (static_cast<int>(ctl) == -1) {
            LOG_DEBUG("socket failed {1} ({2}): interface information is unavailable.", strerror(errno), errno);
            return data;
        }

        // (patterned on bsd impl)
        lifnum ifnr{AF_UNSPEC, 0, 0};
        if (ioctl(ctl, SIOCGLIFNUM, &ifnr) == -1) {
            LOG_DEBUG("ioctl with SIOCGLIFNUM failed: {1} ({2}): interface information is unavailable.", strerror(errno), errno);
            return data;
        }

        vector<lifreq> buffer(ifnr.lifn_count);
        lifconf lifc = {AF_UNSPEC, 0, static_cast<int>(buffer.size() * sizeof(lifreq)), reinterpret_cast<caddr_t>(buffer.data())};
        if (ioctl(ctl, SIOCGLIFCONF, &lifc) == -1) {
            LOG_DEBUG("ioctl with SIOCGLIFCONF failed: {1} ({2}): interface information is unavailable.", strerror(errno), errno);
            return data;
        }

        // put them in a multimap so that similar address can be
        // grouped together.
        multimap<string, const lifreq*> interface_map;
        for (lifreq const& lreq : buffer) {
            interface_map.insert({lreq.lifr_name, &lreq});
        }

        data.primary_interface = get_primary_interface();

        // Walk the interfaces
        decltype(interface_map.begin()) it = interface_map.begin();
        while (it != interface_map.end()) {
            string const& name = it->first;

            interface iface;
            iface.name = name;

            // Populate the MAC address and MTU once per interface
            populate_macaddress(iface, it->second);
            populate_mtu(iface, it->second);

            // Walk the addresses for this interface
            do {
                populate_binding(iface, it->second);
                ++it;
            } while (it != interface_map.end() && it->first == name);

            // Find the DCHP server for the interface
            iface.dhcp_server = find_dhcp_server(name);

            data.interfaces.emplace_back(move(iface));
        }
        return data;
    }

    void networking_resolver::populate_binding(interface& iface, lifreq const* addr) const
    {
        // Populate the correct bindings list
        vector<binding>* bindings = nullptr;
        if (addr->lifr_addr.ss_family == AF_INET) {
            bindings = &iface.ipv4_bindings;
        } else if (addr->lifr_addr.ss_family == AF_INET) {
            bindings = &iface.ipv6_bindings;
        }

        if (!bindings) {
            return;
        }

        // Create a binding
        binding b;
        b.address = address_to_string(reinterpret_cast<sockaddr const*>(&addr->lifr_addr));

        // Get the netmask
        scoped_descriptor ctl(socket(addr->lifr_addr.ss_family, SOCK_DGRAM, 0));
        if (static_cast<int>(ctl) == -1) {
            LOG_DEBUG("socket failed: {1} ({2}): netmask and network for interface {3} are unavailable.", strerror(errno), errno, addr->lifr_name);
        } else {
            lifreq netmask_addr = *addr;
            if (ioctl(ctl, SIOCGLIFNETMASK, &netmask_addr) == -1) {
                LOG_DEBUG("ioctl with SIOCGLIFNETMASK failed: {1} ({2}): netmask and network for interface {3} are unavailable.", strerror(errno), errno, addr->lifr_name);
            } else {
                b.netmask = address_to_string(reinterpret_cast<sockaddr const*>(&netmask_addr.lifr_addr));
                b.network = address_to_string(reinterpret_cast<sockaddr const*>(&addr->lifr_addr), reinterpret_cast<sockaddr const*>(&netmask_addr.lifr_addr));
            }
        }

        bindings->emplace_back(std::move(b));
    }

    void networking_resolver::populate_macaddress(interface& iface, lifreq const* addr) const
    {
        scoped_descriptor ctl(socket(addr->lifr_addr.ss_family, SOCK_DGRAM, 0));
        if (static_cast<int>(ctl) == -1) {
            LOG_DEBUG("socket failed: {1} ({2}): link level address for interface {3} is unavailable.", strerror(errno), errno, addr->lifr_name);
            return;
        }

        arpreq arp;
        sockaddr_in* arp_addr = reinterpret_cast<sockaddr_in*>(&arp.arp_pa);
        arp_addr->sin_addr.s_addr = reinterpret_cast<sockaddr_in const*>(&addr->lifr_addr)->sin_addr.s_addr;
        if (ioctl(ctl, SIOCGARP, &arp) == -1) {
            LOG_DEBUG("ioctl with SIOCGARP failed: {1} ({2}): link level address for {3} is unavailable.", strerror(errno), errno, addr->lifr_name);
            return;
        }

        iface.macaddress = macaddress_to_string(reinterpret_cast<uint8_t const*>(arp.arp_ha.sa_data));
    }

    void networking_resolver::populate_mtu(interface& iface, lifreq const* addr) const
    {
        scoped_descriptor ctl(socket(addr->lifr_addr.ss_family, SOCK_DGRAM, 0));
        if (static_cast<int>(ctl) == -1) {
            LOG_DEBUG("socket failed: {1} ({2}): MTU for interface {3} is unavailable.", strerror(errno), errno, addr->lifr_name);
            return;
        }

        lifreq mtu = *addr;
        if (ioctl(ctl, SIOCGLIFMTU, &mtu) == -1) {
            LOG_DEBUG("ioctl with SIOCGLIFMTU failed: {1} ({2}): MTU for interface {3} is unavailable.", strerror(errno), errno, addr->lifr_name);
            return;
        }

        iface.mtu = mtu.lifr_metric;
    }

    string networking_resolver::get_primary_interface() const
    {
        string value;
        each_line("netstat", { "-rn"}, [&value](string& line) {
            boost::trim(line);
            if (boost::starts_with(line, "default")) {
                vector<string> fields;
                boost::split(fields, line, boost::is_space(), boost::token_compress_on);
                value = fields.size() < 6 ? "" : fields[5];
                return false;
            }
            return true;
        });
        return value;
    }

    bool networking_resolver::is_link_address(const sockaddr* addr) const
    {
        // We explicitly populate the MAC address; we don't need address_to_string to support link layer addresses
        return false;
    }

    uint8_t const* networking_resolver::get_link_address_bytes(const sockaddr * addr) const
    {
        return nullptr;
    }

    string networking_resolver::find_dhcp_server(string const& interface) const
    {
        auto exec = execute("dhcpinfo", { "-i", interface, "ServerID" });
        if (!exec.success) {
            return {};
        }
        return exec.output;
    }

}}}  // namespace facter::facts::solaris
