#include <internal/facts/linux/networking_resolver.hpp>
#include <internal/util/posix/scoped_descriptor.hpp>
#include <leatherman/execution/execution.hpp>
#include <leatherman/file_util/file.hpp>
#include <leatherman/logging/logging.hpp>
#include <boost/algorithm/string.hpp>
#include <algorithm>
#include <cstring>
#include <unordered_set>
#include <netpacket/packet.h>
#include <net/if.h>
#include <sys/ioctl.h>

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

namespace lth_file = leatherman::file_util;
namespace lth_exe  = leatherman::execution;

namespace facter { namespace facts { namespace linux {

    networking_resolver::data networking_resolver::collect_data(collection& facts)
    {
        read_routing_table();
        data result = bsd::networking_resolver::collect_data(facts);
        populate_from_routing_table(result);

        // On linux, the macaddress of bonded interfaces is reported
        // as the address of the bonding master. We want to report the
        // original HW address, so we dig it out of /proc
        for (auto& interface : result.interfaces) {
            // For each interface we check if we're part of a bond,
            // and update the `macaddress` fact if we are
            auto bond_master = get_bond_master(interface.name);
            if (!bond_master.empty()) {
                bool in_our_block = false;
                lth_file::each_line("/proc/net/bonding/"+bond_master, [&](string& line) {
                    // /proc/net/bonding files are organized into chunks for each slave
                    // interface. We want to grab the mac address for the block we're in.
                    if (line == "Slave Interface: " + interface.name) {
                        in_our_block = true;
                    } else if (line.find("Slave Interface") != string::npos) {
                        in_our_block = false;
                    }

                    // If we're in the block for our iface, we can grab the HW address
                    if (in_our_block && line.find("Permanent HW addr: ") != string::npos) {
                        auto split = line.find(':') + 2;
                        interface.macaddress = line.substr(split, string::npos);
                        return false;
                    }
                    return true;
                });
            }
        }
        return result;
    }

    bool networking_resolver::is_link_address(sockaddr const* addr) const
    {
        return addr && addr->sa_family == AF_PACKET;
    }

    uint8_t const* networking_resolver::get_link_address_bytes(sockaddr const* addr) const
    {
        if (!is_link_address(addr)) {
            return nullptr;
        }
        sockaddr_ll const* link_addr = reinterpret_cast<sockaddr_ll const*>(addr);
        if (link_addr->sll_halen != 6 && link_addr->sll_halen != 20) {
            return nullptr;
        }
        return reinterpret_cast<uint8_t const*>(link_addr->sll_addr);
    }

    uint8_t networking_resolver::get_link_address_length(sockaddr const* addr) const
    {
        if (!is_link_address(addr)) {
            return 0;
        }
        sockaddr_ll const* link_addr = reinterpret_cast<sockaddr_ll const*>(addr);
        return link_addr->sll_halen;
    }

    boost::optional<uint64_t> networking_resolver::get_link_mtu(string const& interface, void* data) const
    {
        // Unfortunately in Linux, the data points at interface statistics
        // Nothing useful for us, so we need to use ioctl to query the MTU
        ifreq req;
        memset(&req, 0, sizeof(req));
        strncpy(req.ifr_name, interface.c_str(), sizeof(req.ifr_name));

        scoped_descriptor sock(socket(AF_INET, SOCK_DGRAM, 0));
        if (static_cast<int>(sock) < 0) {
            LOG_WARNING("socket failed: {1} ({2}): interface MTU fact is unavailable for interface {3}.", strerror(errno), errno, interface);
            return boost::none;
        }

        if (ioctl(sock, SIOCGIFMTU, &req) == -1) {
            LOG_WARNING("ioctl failed: {1} ({2}): interface MTU fact is unavailable for interface {3}.", strerror(errno), errno, interface);
            return boost::none;
        }
        return req.ifr_mtu;
    }

    string networking_resolver::get_primary_interface() const
    {
        // If we have a list of routes, then we'll determine the
        // primary interface from that later on when we are processing
        // them.
        if (routes4.size()) {
             return {};
        }

        // Read /proc/net/route to determine the primary interface
        // We consider the primary interface to be the one that has 0.0.0.0 as the
        // routing destination.
        string interface;
        lth_file::each_line("/proc/net/route", [&interface](string& line) {
            vector<boost::iterator_range<string::iterator>> parts;
            boost::split(parts, line, boost::is_space(), boost::token_compress_on);
            if (parts.size() > 7 && parts[1] == boost::as_literal("00000000")
                                 && parts[7] == boost::as_literal("00000000")) {
                interface.assign(parts[0].begin(), parts[0].end());
                return false;
            }
            return true;
        });
        return interface;
    }

    void networking_resolver::read_routing_table()
    {
        auto ip_command = lth_exe::which("ip");
        if (ip_command.empty()) {
            LOG_DEBUG("Could not find the 'ip' command. Network bindings will not be populated from routing table");
            return;
        }

        unordered_set<string> known_route_types {
            "unicast",
            "broadcast",
            "local",
            "nat",
            "unreachable",
            "prohibit",
            "blackhole",
            "throw"
        };

        auto parse_route_line = [&known_route_types](string& line, int family, std::vector<route>& routes) {
            vector<boost::iterator_range<string::iterator>> parts;
            boost::split(parts, line, boost::is_space(), boost::token_compress_on);

            // skip links that are linkdown
            if (std::find_if(parts.cbegin(), parts.cend(), [](const boost::iterator_range<string::iterator>& range) {
                return std::string(range.begin(), range.end()) == "linkdown";
            }) != parts.cend()) {
                return true;
            }

            // remove trailing "onlink" or "pervasive" flags
            while (parts.size() > 0) {
                std::string last_token(parts.back().begin(), parts.back().end());
                if (last_token == "onlink" || last_token == "pervasive")
                    parts.pop_back();
                else
                    break;
            }

            size_t dst_idx = 0;
            if (parts.size() % 2 == 0) {
                std::string route_type(parts[0].begin(), parts[0].end());
                if (known_route_types.find(route_type) == known_route_types.end()) {
                    LOG_WARNING("Could not process routing table entry: Expected a destination followed by key/value pairs, got '{1}'", line);
                    return true;
                } else {
                    dst_idx = 1;
                }
            }

            route r;
            r.destination.assign(parts[dst_idx].begin(), parts[dst_idx].end());

            // Check if we queried for the IPV6 routing tables. If yes, then check if our
            // destination address is missing a ':'. If yes, then IPV6 is disabled since
            // IPV6 addresses have a ':' in them. Our ip command has mistakenly outputted IPV4
            // information. This is bogus data that we want to flush.
            //
            // See FACT-1475 for more details.
            if (family == AF_INET6 && r.destination.find(':') == string::npos) {
              routes = {};
              return false;
            }

            // Iterate over key/value pairs and add the ones we care
            // about to our routes entries
            for (size_t i = dst_idx+1; i < parts.size(); i += 2) {
                std::string key(parts[i].begin(), parts[i].end());
                if (key == "dev") {
                    r.interface.assign(parts[i+1].begin(), parts[i+1].end());
                }
                if (key == "src") {
                    r.source.assign(parts[i+1].begin(), parts[i+1].end());
                }
            }
            routes.push_back(r);
            return true;
        };

        lth_exe::each_line(ip_command, { "route", "show" }, [this, &parse_route_line](string& line) {
            return parse_route_line(line, AF_INET, this->routes4);
        });
        lth_exe::each_line(ip_command, { "-6", "route", "show" }, [this, &parse_route_line](string& line) {
            return parse_route_line(line, AF_INET6, this->routes6);
        });
    }

    void networking_resolver::populate_from_routing_table(networking_resolver::data& result) const
    {
        for (const auto& r : routes4) {
            if (r.destination == "default" && result.primary_interface.empty()) {
                 result.primary_interface = r.interface;
            }
            associate_src_with_iface(r, result, [](interface& iface) -> vector<binding>& {
                return iface.ipv4_bindings;
            });
        }

        for (const auto& r : routes6) {
            associate_src_with_iface(r, result, [](interface& iface) -> vector<binding>& {
                return iface.ipv6_bindings;
            });
        }
    }

    template<typename F>
    void networking_resolver::associate_src_with_iface(const networking_resolver::route& r, networking_resolver::data& result, F get_bindings) const {
        if (!r.source.empty()) {
            auto iface = find_if(result.interfaces.begin(), result.interfaces.end(), [&](const interface& iface) {
                return iface.name == r.interface;
            });
            if (iface != result.interfaces.end()) {
                auto& bindings = get_bindings(*iface);
                auto existing_binding = find_if(bindings.begin(), bindings.end(), [&](const binding& b) {
                    return b.address == r.source;
                });
                if (existing_binding == bindings.end()) {
                    binding b = { r.source, "", "" };
                    bindings.emplace_back(move(b));
                }
            }
        }
    }

    string networking_resolver::get_bond_master(const std::string& name) const {
        static bool have_logged_about_bonding = false;
        auto ip_command = lth_exe::which("ip");
        if (ip_command.empty()) {
            if (!have_logged_about_bonding) {
                 LOG_DEBUG("Could not find the 'ip' command. Physical macaddress for bonded interfaces will be incorrect.");
                 have_logged_about_bonding = true;
            }
            return {};
        }

        string bonding_master;

        lth_exe::each_line(ip_command, {"link", "show", name}, [&bonding_master](string& line) {
            if (line.find("SLAVE") != string::npos) {
                vector<boost::iterator_range<string::iterator>> parts;
                boost::split(parts, line, boost::is_space(), boost::token_compress_on);

                // We have to use find_if here since a boost::iterator_range doesn't compare properly to a string.
                auto master = find_if(parts.begin(), parts.end(), [](boost::iterator_range<string::iterator>& part){
                    string p {part.begin(), part.end()};
                    return p == "master";
                });

                // the actual master interface is in the output as
                // "master <iface>". Once we've found the master
                // string above, we get the next token and return that
                // as our interface device.
                if (master != parts.end()) {
                    auto master_iface = master + 1;
                    if (master_iface != parts.end()) {
                        bonding_master.assign(master_iface->begin(), master_iface->end());
                        return false;
                    }
                }
            }
            return true;
        });
        return bonding_master;
    }
}}}  // namespace facter::facts::linux