/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <getopt.h>
#include <sys/stat.h>

#include "sd-event.h"

#include "alloc-util.h"
#include "build.h"
#include "daemon-util.h"
#include "hashmap.h"
#include "log.h"
#include "main-func.h"
#include "parse-argument.h"
#include "pretty-print.h"
#include "socket-util.h"
#include "strv.h"
#include "time-util.h"
#include "wait-online-manager.h"

static bool arg_quiet = false;
static usec_t arg_timeout = 120 * USEC_PER_SEC;
static Hashmap *arg_interfaces = NULL;
static char **arg_ignore = NULL;
static LinkOperationalStateRange arg_required_operstate = LINK_OPERSTATE_RANGE_INVALID;
static AddressFamily arg_required_family = ADDRESS_FAMILY_NO;
static bool arg_any = false;
static bool arg_requires_dns = false;

STATIC_DESTRUCTOR_REGISTER(arg_interfaces, hashmap_freep);
STATIC_DESTRUCTOR_REGISTER(arg_ignore, strv_freep);

static int help(void) {
        _cleanup_free_ char *link = NULL;
        int r;

        r = terminal_urlify_man("systemd-networkd-wait-online.service", "8", &link);
        if (r < 0)
                return log_oom();

        printf("%s [OPTIONS...]\n\n"
               "Block until network is configured.\n\n"
               "  -h --help                 Show this help\n"
               "     --version              Print version string\n"
               "  -q --quiet                Do not show status information\n"
               "  -i --interface=INTERFACE[:MIN_OPERSTATE[:MAX_OPERSTATE]]\n"
               "                            Block until at least these interfaces have appeared\n"
               "     --ignore=INTERFACE     Don't take these interfaces into account\n"
               "  -o --operational-state=MIN_OPERSTATE[:MAX_OPERSTATE]\n"
               "                            Required operational state\n"
               "  -4 --ipv4                 Requires at least one IPv4 address\n"
               "  -6 --ipv6                 Requires at least one IPv6 address\n"
               "     --any                  Wait until at least one of the interfaces is online\n"
               "     --timeout=SECS         Maximum time to wait for network connectivity\n"
               "     --dns                  Requires at least one DNS server to be accessible\n"
               "\nSee the %s for details.\n",
               program_invocation_short_name,
               link);

        return 0;
}

static int parse_interface_with_operstate_range(const char *str) {
        _cleanup_free_ char *ifname = NULL;
        _cleanup_free_ LinkOperationalStateRange *range = NULL;
        const char *p;
        int r;

        assert(str);

        range = new(LinkOperationalStateRange, 1);
        if (!range)
                return log_oom();

        p = strchr(str, ':');
        if (p) {
                r = parse_operational_state_range(p + 1, range);
                if (r < 0)
                        return log_error_errno(r, "Invalid operational state range: %s", p + 1);

                ifname = strndup(optarg, p - optarg);
        } else {
                *range = LINK_OPERSTATE_RANGE_INVALID;
                ifname = strdup(str);
        }
        if (!ifname)
                return log_oom();

        if (!ifname_valid(ifname))
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Invalid interface name: %s", ifname);

        r = hashmap_ensure_put(&arg_interfaces, &string_hash_ops_free_free, ifname, range);
        if (r == -ENOMEM)
                return log_oom();
        if (r < 0)
                return log_error_errno(r, "Failed to store interface name: %m");
        if (r == 0)
                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
                                       "Interface name %s is already specified.", ifname);

        TAKE_PTR(ifname);
        TAKE_PTR(range);
        return 0;
}

static int parse_argv(int argc, char *argv[]) {

        enum {
                ARG_VERSION = 0x100,
                ARG_IGNORE,
                ARG_ANY,
                ARG_TIMEOUT,
                ARG_DNS,
        };

        static const struct option options[] = {
                { "help",              no_argument,       NULL, 'h'         },
                { "version",           no_argument,       NULL, ARG_VERSION },
                { "quiet",             no_argument,       NULL, 'q'         },
                { "interface",         required_argument, NULL, 'i'         },
                { "ignore",            required_argument, NULL, ARG_IGNORE  },
                { "operational-state", required_argument, NULL, 'o'         },
                { "ipv4",              no_argument,       NULL, '4'         },
                { "ipv6",              no_argument,       NULL, '6'         },
                { "any",               no_argument,       NULL, ARG_ANY     },
                { "timeout",           required_argument, NULL, ARG_TIMEOUT },
                { "dns",               optional_argument, NULL, ARG_DNS     },
                {}
        };

        int c, r;

        assert(argc >= 0);
        assert(argv);

        while ((c = getopt_long(argc, argv, "hi:qo:46", options, NULL)) >= 0)

                switch (c) {

                case 'h':
                        help();
                        return 0;

                case 'q':
                        arg_quiet = true;
                        break;

                case ARG_VERSION:
                        return version();

                case 'i':
                        r = parse_interface_with_operstate_range(optarg);
                        if (r < 0)
                                return r;
                        break;

                case ARG_IGNORE:
                        if (strv_extend(&arg_ignore, optarg) < 0)
                                return log_oom();

                        break;

                case 'o':
                        r = parse_operational_state_range(optarg, &arg_required_operstate);
                        if (r < 0)
                                return log_error_errno(r, "Invalid operational state range '%s'", optarg);
                        break;

                case '4':
                        arg_required_family |= ADDRESS_FAMILY_IPV4;
                        break;

                case '6':
                        arg_required_family |= ADDRESS_FAMILY_IPV6;
                        break;

                case ARG_ANY:
                        arg_any = true;
                        break;

                case ARG_TIMEOUT:
                        r = parse_sec(optarg, &arg_timeout);
                        if (r < 0)
                                return r;
                        break;

                case ARG_DNS:
                        r = parse_boolean_argument("--dns", optarg, &arg_requires_dns);
                        if (r < 0)
                                return r;
                        break;

                case '?':
                        return -EINVAL;

                default:
                        assert_not_reached();
                }

        return 1;
}

static int run(int argc, char *argv[]) {
        _cleanup_(manager_freep) Manager *m = NULL;
        _unused_ _cleanup_(notify_on_cleanup) const char *notify_message = NULL;
        int r;

        log_setup();

        umask(0022);

        r = parse_argv(argc, argv);
        if (r <= 0)
                return r;

        if (arg_quiet)
                log_set_max_level(LOG_ERR);

        r = manager_new(&m,
                        arg_interfaces,
                        arg_ignore,
                        arg_required_operstate,
                        arg_required_family,
                        arg_any,
                        arg_timeout,
                        arg_requires_dns);
        if (r < 0)
                return log_error_errno(r, "Could not create manager: %m");

        if (manager_configured(m))
                goto success;

        notify_message = notify_start("READY=1\n"
                                      "STATUS=Waiting for network connections...",
                                      "STATUS=Failed to wait for network connectivity...");

        r = sd_event_loop(m->event);
        if (r == -ETIMEDOUT)
                return log_error_errno(r, "Timeout occurred while waiting for network connectivity.");
        if (r < 0)
                return log_error_errno(r, "Event loop failed: %m");

success:
        notify_message = "STATUS=All interfaces configured...";

        return 0;
}

DEFINE_MAIN_FUNCTION(run);
