#include <internal/facts/linux/os_linux.hpp>
#include <internal/facts/resolvers/operating_system_resolver.hpp>
#include <facter/facts/os.hpp>
#include <facter/facts/os_family.hpp>
#include <leatherman/execution/execution.hpp>
#include <leatherman/file_util/file.hpp>
#include <leatherman/util/regex.hpp>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <vector>

using namespace std;
using namespace leatherman::execution;
using namespace boost::filesystem;
using namespace leatherman::util;

namespace bs = boost::system;
namespace lth_file = leatherman::file_util;

namespace facter { namespace facts { namespace linux {

    // Return contents of the os-release file
    // http://www.freedesktop.org/software/systemd/man/os-release.html
    map<string, string> os_linux::key_value_file(string file, set<string> const& items)
    {
        map<string, string> values;
        bs::error_code ec;
        if (!items.empty() && is_regular_file(file, ec)) {
            string key, value;
            lth_file::each_line(file, [&](string& line) {
                if (re_search(line, boost::regex("(?m)^(\\w+)=[\"']?(.+?)[\"']?$"), &key, &value)) {
                    if (items.count(key)) {
                        values.insert(make_pair(key, value));
                    }
                }
                return items.size() != values.size();
            });
        }
        return values;
    }

    os_linux::os_linux(std::set<std::string> items, std::string file) :
            _release_info(key_value_file(file, items)) {}

    static string check_debian_linux(string const& distro_id)
    {
        // Check for Debian variants
        bs::error_code ec;
        if (is_regular_file(release_file::huawei, ec)) {
          return os::huawei;
        }

        if (is_regular_file(release_file::devuan, ec)) {
          return os::devuan;
        }

        if (is_regular_file(release_file::debian, ec)) {
            if (distro_id == os::ubuntu || distro_id == os::linux_mint) {
                return distro_id;
            }
            return os::debian;
        }
        return {};
    }

    static string check_oracle_linux()
    {
        bs::error_code ec;
        if (is_regular_file(release_file::oracle_enterprise_linux, ec)) {
            if (is_regular_file(release_file::oracle_vm_linux, ec)) {
                return os::oracle_vm_linux;
            }
            return os::oracle_enterprise_linux;
        }
        return {};
    }

    static string check_redhat_linux()
    {
        bs::error_code ec;
        if (is_regular_file(release_file::redhat, ec)) {
            static vector<tuple<boost::regex, string>> const regexs {
                make_tuple(boost::regex("(?i)centos"),                        string(os::centos)),
                make_tuple(boost::regex("(?i)scientific linux CERN"),         string(os::scientific_cern)),
                make_tuple(boost::regex("(?i)scientific linux release"),      string(os::scientific)),
                make_tuple(boost::regex("(?im)^cloudlinux"),                  string(os::cloud_linux)),
                make_tuple(boost::regex("(?im)^virtuozzo linux"),             string(os::virtuozzo_linux)),
                make_tuple(boost::regex("(?i)Ascendos"),                      string(os::ascendos)),
                make_tuple(boost::regex("(?im)^XenServer"),                   string(os::xen_server)),
                make_tuple(boost::regex("(?im)^XCP-ng"),                      string(os::xcp_ng)),
                make_tuple(boost::regex("XCP"),                               string(os::zen_cloud_platform)),
                make_tuple(boost::regex("(?im)^Parallels Server Bare Metal"), string(os::psbm)),
                make_tuple(boost::regex("(?m)^Fedora release"),               string(os::fedora)),
            };

            string contents = lth_file::read(release_file::redhat);
            boost::trim(contents);
            for (auto const& regex : regexs) {
                if (re_search(contents, get<0>(regex))) {
                    return get<1>(regex);
                }
            }
            return os::redhat;
        }
        return {};
    }

    static string check_photon_linux()
    {
        string contents = lth_file::read(release_file::lsb);
        boost::trim(contents);
        if (re_search(contents, boost::regex("VMware Photon"))) {
            return string(os::photon_os);
        }
        return {};
    }

    static string check_suse_linux()
    {
        bs::error_code ec;
        if (is_regular_file(release_file::suse, ec)) {
            static vector<tuple<boost::regex, string>> const regexs {
                make_tuple(boost::regex("(?im)^SUSE LINUX Enterprise Server"),  string(os::suse_enterprise_server)),
                make_tuple(boost::regex("(?im)^SUSE LINUX Enterprise Desktop"), string(os::suse_enterprise_desktop)),
                make_tuple(boost::regex("(?im)^openSUSE"),                      string(os::open_suse)),
            };

            string contents = lth_file::read(release_file::suse);
            boost::trim(contents);
            for (auto const& regex : regexs) {
                if (re_search(contents, get<0>(regex))) {
                    return get<1>(regex);
                }
            }
            return os::suse;
        }
        return {};
    }

    static string check_other_linux()
    {
        static vector<tuple<string, string>> const files {
            make_tuple(string(release_file::arista_eos),     string(os::arista_eos)),
            make_tuple(string(release_file::gentoo),         string(os::gentoo)),
            make_tuple(string(release_file::mageia),         string(os::mageia)),
            make_tuple(string(release_file::mandriva),       string(os::mandriva)),
            make_tuple(string(release_file::mandrake),       string(os::mandrake)),
            make_tuple(string(release_file::meego),          string(os::meego)),
            make_tuple(string(release_file::archlinux),      string(os::archlinux)),
            make_tuple(string(release_file::manjarolinux),   string(os::manjarolinux)),
            make_tuple(string(release_file::oracle_linux),   string(os::oracle_linux)),
            make_tuple(string(release_file::openwrt),        string(os::openwrt)),
            make_tuple(string(release_file::alpine),         string(os::alpine)),
            make_tuple(string(release_file::vmware_esx),     string(os::vmware_esx)),
            make_tuple(string(release_file::slackware),      string(os::slackware)),
        };

        for (auto const& file : files) {
            bs::error_code ec;
            if (is_regular_file(get<0>(file), ec)) {
                return get<1>(file);
            }
        }
        return {};
    }

    static string check_amazon()
    {
        bs::error_code ec;
        if (is_regular_file(release_file::amazon, ec)) {
            return os::amazon;
        }
        return {};
    }

    string os_linux::get_name(string const& distro_id) const
    {
        // Check for Debian first; this happens after AristaEOS in Facter 2.x
        // but that platform is not a Debian so shouldn't matter.
        auto value = check_debian_linux(distro_id);
        if (!value.empty()) {
            return value;
        }

        // Check for specialized distributions next
        value = check_other_linux();
        if (!value.empty()) {
            return value;
        }

        // Check for Oracle Enterprise Linux next
        value = check_oracle_linux();
        if (!value.empty()) {
            return value;
        }

        // Check for RedHat next
        value = check_redhat_linux();
        if (!value.empty()) {
            return value;
        }

        // Check for SuSE next
        value = check_suse_linux();
        if (!value.empty()) {
            return value;
        }

        value = check_photon_linux();
        if (!value.empty()) {
            return value;
        }
        // This should happen after everything else because it's a relatively broad match
        return check_amazon();
    }

    string os_linux::get_family(string const& name) const
    {
        static map<string, string> const systems = {
            { string(os::redhat),                   string(os_family::redhat) },
            { string(os::fedora),                   string(os_family::redhat) },
            { string(os::centos),                   string(os_family::redhat) },
            { string(os::scientific),               string(os_family::redhat) },
            { string(os::scientific_cern),          string(os_family::redhat) },
            { string(os::ascendos),                 string(os_family::redhat) },
            { string(os::cloud_linux),              string(os_family::redhat) },
            { string(os::psbm),                     string(os_family::redhat) },
            { string(os::oracle_linux),             string(os_family::redhat) },
            { string(os::oracle_vm_linux),          string(os_family::redhat) },
            { string(os::oracle_enterprise_linux),  string(os_family::redhat) },
            { string(os::amazon),                   string(os_family::redhat) },
            { string(os::xen_server),               string(os_family::redhat) },
            { string(os::xcp_ng),                   string(os_family::redhat) },
            { string(os::photon_os),                string(os_family::redhat) },
            { string(os::huawei),                   string(os_family::debian) },
            { string(os::linux_mint),               string(os_family::debian) },
            { string(os::ubuntu),                   string(os_family::debian) },
            { string(os::debian),                   string(os_family::debian) },
            { string(os::devuan),                   string(os_family::debian) },
            { string(os::suse_enterprise_server),   string(os_family::suse) },
            { string(os::suse_enterprise_desktop),  string(os_family::suse) },
            { string(os::open_suse),                string(os_family::suse) },
            { string(os::suse),                     string(os_family::suse) },
            { string(os::gentoo),                   string(os_family::gentoo) },
            { string(os::archlinux),                string(os_family::archlinux) },
            { string(os::manjarolinux),             string(os_family::archlinux) },
            { string(os::mandrake),                 string(os_family::mandrake) },
            { string(os::mandriva),                 string(os_family::mandrake) },
            { string(os::mageia),                   string(os_family::mandrake) },
        };
        auto const& it = systems.find(name);
        if (it != systems.end()) {
            return it->second;
        }
        return {};
    }

    string os_linux::get_release(string const& name, string const& distro_release) const
    {
        // Map of release files that contain a "release X.X.X" on the first line
        static map<string, string> const release_files = {
                { string(os::amazon),                   string(release_file::amazon) },
                { string(os::centos),                   string(release_file::redhat) },
                { string(os::redhat),                   string(release_file::redhat) },
                { string(os::scientific),               string(release_file::redhat) },
                { string(os::scientific_cern),          string(release_file::redhat) },
                { string(os::ascendos),                 string(release_file::redhat) },
                { string(os::cloud_linux),              string(release_file::redhat) },
                { string(os::psbm),                     string(release_file::redhat) },
                { string(os::xen_server),               string(release_file::redhat) },
                { string(os::xcp_ng),                   string(release_file::redhat) },
                { string(os::fedora),                   string(release_file::fedora) },
                { string(os::meego),                    string(release_file::meego) },
                { string(os::oracle_linux),             string(release_file::oracle_linux) },
                { string(os::oracle_enterprise_linux),  string(release_file::oracle_enterprise_linux) },
                { string(os::oracle_vm_linux),          string(release_file::oracle_vm_linux) },
                { string(os::arista_eos),               string(release_file::arista_eos) },
                { string(os::gentoo),                   string(release_file::gentoo) },
        };

        string value;
        auto it = release_files.find(name);
        if (it != release_files.end()) {
            string contents;
            if (lth_file::each_line(it->second, [&](string& line) {
                // We only need the first line
                contents = move(line);
                return false;
            })) {
                if (boost::ends_with(contents, "(Rawhide)")) {
                    value = "Rawhide";
                } else if (contents.find("release") != string::npos) {
                    re_search(contents, boost::regex("release (\\d[\\d.]*)"), &value);
                } else {
                    re_search(contents, boost::regex("Amazon Linux (\\d+)"), &value);
                }
            }
        }

        // Debian uses the entire contents of the release file as the version
        if (value.empty() && name == os::debian) {
            value = lth_file::read(release_file::debian);
            boost::trim_right(value);
        }

        // Devuan uses the entire contents of the release file as the version
        if (value.empty() && name == os::devuan) {
            value = lth_file::read(release_file::devuan);
            boost::trim_right(value);
        }

        // Alpine uses the entire contents of the release file as the version
        if (value.empty() && name == os::alpine) {
            value = lth_file::read(release_file::alpine);
            boost::trim_right(value);
        }

        // HuaweiOS uses the entire contents of the release file as the version
        if (value.empty() && name == os::huawei) {
            value = lth_file::read(release_file::huawei);
            boost::trim_right(value);
        }

        // Check for SuSE related distros, read the release file
        if (value.empty() && (
                name == os::suse ||
                name == os::suse_enterprise_server ||
                name == os::suse_enterprise_desktop ||
                name == os::open_suse)) {
            string contents = lth_file::read(release_file::suse);
            string major;
            string minor;
            if (re_search(contents, boost::regex("(?m)^VERSION\\s*=\\s*(\\d+)\\.?(\\d+)?"), &major, &minor)) {
                // Check that we have a minor version; if not, use the patch level
                if (minor.empty()) {
                    if (!re_search(contents, boost::regex("(?m)^PATCHLEVEL\\s*=\\s*(\\d+)"), &minor)) {
                        minor = "0";
                    }
                }
                value = major + "." + minor;
            } else {
                value = "unknown";
            }
        }
        if (value.empty() && name == os::photon_os) {
            string major, minor;
            string contents = lth_file::read(release_file::lsb);
            string pattern = "DISTRIB_RELEASE=\"(\\d+)\\.(\\d+)( ([a-zA-Z]+\\d+))?\"";
            if (re_search(contents, boost::regex(pattern), &major, &minor)) {
                value = major + "." + minor;
            }
        }

        // Read version files of particular operating systems
        if (value.empty()) {
            const char* file = nullptr;
            boost::regex pattern;
            if (name == os::ubuntu) {
                file = release_file::lsb;
                pattern = "(?m)^DISTRIB_RELEASE=(\\d+\\.\\d+)(?:\\.\\d+)*";
            } else if (name == os::slackware) {
                file = release_file::slackware;
                pattern = "Slackware ([0-9.]+)";
            } else if (name == os::mageia) {
                file = release_file::mageia;
                pattern = "Mageia release ([0-9.]+)";
            } else if (name == os::linux_mint) {
                file = release_file::linux_mint_info;
                pattern = "(?m)^RELEASE=(\\d+)";
            } else if (name == os::openwrt) {
                file = release_file::openwrt_version;
                pattern = "(?m)^(\\d+\\.\\d+.*)";
            } else if (name == os::arista_eos) {
                file = release_file::arista_eos;
                pattern = "Arista Networks EOS (\\d+\\.\\d+\\.\\d+[A-M]?)";
            }
            if (file) {
                string contents = lth_file::read(file);
                re_search(contents, pattern, &value);
            }
        }

        // For VMware ESX, execute the vmware tool
        if (value.empty() && name == os::vmware_esx) {
            auto exec = execute("vmware", { "-v" });
            if (exec.success) {
                re_search(exec.output, boost::regex("VMware ESX .*?(\\d.*)"), &value);
            }
        }

        return value;
    }

    tuple<string, string> os_linux::parse_release(string const& name, string const& release) const
    {
        return facter::facts::resolvers::operating_system_resolver::parse_distro(name, release);
    }

}}}  // namespace facter::facts::linux