//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

// Find default gateways on Linux using ip route command

#ifndef OPENVPN_NETCONF_LINUX_GW_H
#define OPENVPN_NETCONF_LINUX_GW_H

#include <string>
#include <limits>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/common/splitlines.hpp>
#include <openvpn/common/process.hpp>
#include <openvpn/addr/route.hpp>

namespace openvpn {
class LinuxGW
{
  public:
    OPENVPN_EXCEPTION(linux_gw_error);

    LinuxGW(const std::string &ip_route_show_txt, const bool ignore_errors)
    {
        int best_metric = std::numeric_limits<int>::max();

        SplitLines sl(ip_route_show_txt);
        while (sl())
        {
            const std::string &line = sl.line_ref();

            try
            {
                // parse an output line generated by "ip [-6] route show"
                const std::vector<std::string> v = Split::by_space<std::vector<std::string>, NullLex, SpaceMatch, Split::NullLimit>(line);

                // blank line?
                if (v.empty())
                    continue;

                // only interested in default routes
                if (v[0] != "default")
                    continue;

                // parse out route information
                enum RouteInfo
                {
                    INITIAL,
                    VIA,
                    DEV,
                    METRIC,
                };

                std::string d;
                IP::Addr a;
                int m = std::numeric_limits<int>::max();

                RouteInfo ri = INITIAL;
                for (const auto &term : v)
                {
                    switch (ri)
                    {
                    case INITIAL:
                        if (term == "via")
                            ri = VIA;
                        else if (term == "dev")
                            ri = DEV;
                        else if (term == "metric")
                            ri = METRIC;
                        else
                            ri = INITIAL;
                        break;
                    case VIA:
                        a = IP::Addr(term, "via");
                        ri = INITIAL;
                        break;
                    case DEV:
                        d = validate_dev(term);
                        ri = INITIAL;
                        break;
                    case METRIC:
                        m = parse_number_throw<int>(term, "bad metric");
                        ri = INITIAL;
                        break;
                    }
                }

                // best metric?
                if (m < best_metric || best_metric == std::numeric_limits<int>::max())
                {
                    best_metric = m;
                    dev_ = std::move(d);
                    addr_ = a;
                }
            }
            catch (const std::exception &e)
            {
                if (!ignore_errors)
                    OPENVPN_THROW(linux_gw_error, "error parsing line: " << line << " : " << e.what());
            }
        }
    }

    static std::string ip_route_show(const bool ipv6)
    {
        RedirectPipe::InOut pipe;
        Argv argv;
        argv.emplace_back("/sbin/ip");
        if (ipv6)
            argv.emplace_back("-6");
        argv.emplace_back("route");
        argv.emplace_back("show");
        const int status = system_cmd(argv[0], argv, nullptr, pipe, 0, nullptr);
        if (status != 0)
            OPENVPN_THROW(linux_gw_error, "command returned error status " << status << " : " << argv.to_string());
        return pipe.out;
    }

    const std::string &dev() const
    {
        return dev_;
    }

    const IP::Addr &addr() const
    {
        return addr_;
    }

    bool defined() const
    {
        return !dev_.empty() && addr_.defined();
    }

    std::string to_string() const
    {
        return dev_ + '/' + addr_.to_string();
    }

  private:
    std::string validate_dev(const std::string &dev)
    {
        if (dev.empty())
            OPENVPN_THROW_EXCEPTION("dev is empty");
        return dev;
    }

    std::string dev_;
    IP::Addr addr_;
};

struct LinuxGW46
{
    LinuxGW46(const bool ignore_errors)
        : v4(LinuxGW::ip_route_show(false), ignore_errors),
          v6(LinuxGW::ip_route_show(true), ignore_errors)
    {
    }

    std::string to_string() const
    {
        std::string ret = "[";
        if (v4.defined())
        {
            ret += "4:";
            ret += v4.to_string();
        }
        if (v6.defined())
        {
            if (v4.defined())
                ret += ' ';
            ret += "6:";
            ret += v6.to_string();
        }
        ret += "]";
        return ret;
    }

    std::string dev() const
    {
        if (v4.defined())
            return v4.dev();
        else if (v6.defined())
            return v6.dev();
        else
            throw LinuxGW::linux_gw_error("cannot determine gateway interface");
    }

    LinuxGW v4;
    LinuxGW v6;
};
} // namespace openvpn

#endif
