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

#include <locale.h>

#include "sd-bus.h"
#include "sd-event.h"
#include "sd-json.h"

#include "alloc-util.h"
#include "build.h"
#include "bus-error.h"
#include "bus-label.h"
#include "bus-locator.h"
#include "bus-map-properties.h"
#include "bus-util.h"
#include "errno-util.h"
#include "format-table.h"
#include "hashmap.h"
#include "json-util.h"
#include "main-func.h"
#include "options.h"
#include "pager.h"
#include "polkit-agent.h"
#include "pretty-print.h"
#include "runtime-scope.h"
#include "string-util.h"
#include "strv.h"
#include "sysupdate-update-set-flags.h"
#include "sysupdate-util.h"
#include "terminal-util.h"
#include "verbs.h"

static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static bool arg_reboot = false;
static bool arg_offline = false;
static bool arg_now = false;
static bool arg_ask_password = true;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
static const char *arg_host = NULL;

#define SYSUPDATE_HOST_PATH "/org/freedesktop/sysupdate1/target/host"
#define SYSUPDATE_TARGET_INTERFACE "org.freedesktop.sysupdate1.Target"

typedef struct Version {
        char *version;
        UpdateSetFlags flags;
        char **changelog;
        char *contents_json;
} Version;

static void version_done(Version *v) {
        assert(v);

        v->version = mfree(v->version);
        v->changelog = strv_free(v->changelog);
        v->flags = 0;
        v->contents_json = mfree(v->contents_json);
}

typedef struct Operation {
        void *userdata;

        sd_bus *bus;
        sd_event *event;
        unsigned *remaining;

        const char *target_path;
        const char *target_id;

        uint64_t job_id;
        char *job_path;
        sd_event_source *job_interrupt_source;
        sd_bus_slot *job_properties_slot;
        sd_bus_slot *job_finished_slot;

        /* Only used for Acquire()/Install() operations: */
        char *acquired_version;
        /* The version the user requested, possibly empty */
        char *requested_version;
} Operation;

static Operation* operation_free(Operation *p) {
        if (!p)
                return NULL;

        assert(*p->remaining > 0);
        *p->remaining -= 1;
        if (*p->remaining == 0)
                /* We want to crash the program if we can't exit the loop
                 * cleanly, otherwise it will just hang */
                assert_se(sd_event_exit(p->event, 0) >= 0);

        free(p->job_path);
        free(p->acquired_version);
        free(p->requested_version);

        sd_event_source_disable_unref(p->job_interrupt_source);
        sd_bus_slot_unref(p->job_properties_slot);
        sd_bus_slot_unref(p->job_finished_slot);

        return mfree(p);
}

DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_free);

static Operation* operation_new(
                void *userdata,
                sd_bus *bus,
                unsigned *remaining,
                const char *target_path,
                const char *target_id) {

        _cleanup_(operation_freep) Operation *o = NULL;

        o = new(Operation, 1);
        if (!o)
                return NULL;

        *o = (Operation) {
                .userdata = userdata,
                .bus = bus,
                .event = sd_bus_get_event(bus),
                .remaining = remaining,
                .target_path = target_path,
                .target_id = target_id,
        };
        return TAKE_PTR(o);
}

static int ensure_targets(sd_bus *bus, char **argv, char ***ret_targets) {
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_strv_free_ char **targets = NULL;
        int r;

        assert(bus);
        assert(ret_targets);

        if (strv_isempty(argv)) {
                const char *class, *name, *path;

                r = bus_call_method(bus, bus_sysupdate_mgr, "ListTargets", &error, &reply, NULL);
                if (r < 0)
                        return log_error_errno(r, "Failed to call ListTargets: %s", bus_error_message(&error, r));

                r = sd_bus_message_enter_container(reply, 'a', "(sso)");
                if (r < 0)
                        return bus_log_parse_error(r);

                while ((r = sd_bus_message_read(reply, "(sso)", &class, &name, &path)) > 0) {
                        _cleanup_free_ char *id = NULL;

                        if (streq(class, "host"))
                                id = strdup("host");
                        else
                                id = strjoin(class, ":", name);
                        if (!id)
                                return log_oom();

                        r = strv_consume(&targets, TAKE_PTR(id));
                        if (r < 0)
                                return log_oom();
                }
                if (r < 0)
                        return bus_log_parse_error(r);

                r = sd_bus_message_exit_container(reply);
                if (r < 0)
                        return bus_log_parse_error(r);
        } else {
                r = strv_extend_strv(&targets, argv, true);
                if (r < 0)
                        return log_oom();
        }

        *ret_targets = TAKE_PTR(targets);
        return 0;
}

static int parse_target(
                const char *in,
                char **ret_bus_path,
                char **ret_version) {
        _cleanup_free_ char *id = NULL, *version = NULL, *escaped = NULL, *objpath = NULL;
        const char *s;

        /*
         * Parses the TARGET[@VERSION] syntax from the command line into
         * a bus object path and an optional version number.
         */

        assert(in);
        assert(ret_bus_path);
        assert(ret_version);

        s = strrchr(in, '@');
        if (s) {
                version = strdup(s + 1);
                if (!version)
                        return -ENOMEM;
                id = strndup(in, s - in);
        } else
                id = strdup(in);
        if (!id)
                return -ENOMEM;

        escaped = bus_label_escape(id);
        if (!escaped)
                return -ENOMEM;

        objpath = strjoin("/org/freedesktop/sysupdate1/target/", escaped);
        if (!objpath)
                return -ENOMEM;

        *ret_bus_path = TAKE_PTR(objpath);
        *ret_version = TAKE_PTR(version);
        return 0;
}

static int parse_targets(
                char **targets,
                size_t *ret_n,
                char ***ret_bus_paths,
                char ***ret_versions) {
        _cleanup_strv_free_ char **bus_paths = NULL;
        _cleanup_strv_free_ char **versions = NULL;
        size_t n = 0;
        int r;

        assert(ret_bus_paths);
        assert(ret_n);

        if (strv_isempty(targets))
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No targets found.");

        STRV_FOREACH(id, targets) {
                _cleanup_free_ char *bus_path = NULL, *version = NULL;

                r = parse_target(*id, &bus_path, &version);
                if (r < 0)
                        return log_oom();

                if (version && !ret_versions)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                               "Unexpected version specifier in target: %s",
                                               *id);

                r = strv_extend(&bus_paths, strempty(bus_path));
                if (r < 0)
                        return log_oom();

                r = strv_extend(&versions, strempty(version));
                if (r < 0)
                        return log_oom();

                n++;
        }

        *ret_n = n;
        *ret_bus_paths = TAKE_PTR(bus_paths);
        if (ret_versions)
                *ret_versions = TAKE_PTR(versions);
        return 0;
}

static int log_bus_error(int r, const sd_bus_error *error, const char *target, const char *action) {
        assert(action);

        if (r == 0) {
                assert(sd_bus_error_is_set(error));
                r = sd_bus_error_get_errno(error);
        }

        if (sd_bus_error_has_name(error, SD_BUS_ERROR_UNKNOWN_OBJECT)) {
                if (target)
                        return log_error_errno(r, "Invalid target: %s", target);
                return log_error_errno(r, "Invalid target");
        }

        if (target)
                return log_error_errno(r, "Failed to %s for '%s': %s", action, target,
                                       bus_error_message(error, r));
        return log_error_errno(r, "Failed to %s: %s", action, bus_error_message(error, r));
}

static int list_targets(sd_bus *bus) {
        _cleanup_(table_unrefp) Table *table = NULL;
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
        size_t n;
        int r;

        assert(bus);

        r = ensure_targets(bus, /* argv= */ NULL, &targets);
        if (r < 0)
                return log_error_errno(r, "Failed to find targets: %m");

        r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse targets: %m");

        table = table_new("target", "version", "path");
        if (!table)
                return log_oom();

        for (size_t i = 0; i < n; i++) {
                char *version = NULL;
                _cleanup_free_ char *path = NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;

                r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination,
                                       target_paths[i], SYSUPDATE_TARGET_INTERFACE,
                                       "GetVersion", &error, &reply, NULL);
                if (r < 0)
                        return log_bus_error(r, &error, targets[i], "get current version");
                r = sd_bus_message_read_basic(reply, 's', &version);
                if (r < 0)
                        return bus_log_parse_error(r);

                r = sd_bus_get_property_string(bus, bus_sysupdate_mgr->destination,
                                               target_paths[i], SYSUPDATE_TARGET_INTERFACE,
                                               "Path", &error, &path);
                if (r < 0)
                        return log_bus_error(r, &error, targets[i], "get target bus path");

                r = table_add_many(table,
                                   TABLE_STRING, targets[i],
                                   TABLE_STRING, empty_to_dash(version),
                                   TABLE_STRING, path);
                if (r < 0)
                        return table_log_add_error(r);
        }

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}

typedef struct DescribeParams {
        Version v;
        sd_json_variant *contents_json;
        bool newest;
        bool available;
        bool installed;
        bool partial;
        bool pending;
        bool obsolete;
        bool protected;
        bool incomplete;
} DescribeParams;

static void describe_params_done(DescribeParams *p) {
        assert(p);

        version_done(&p->v);
        sd_json_variant_unref(p->contents_json);
}

static int parse_describe(sd_bus_message *reply, Version *ret) {
        char *version_json = NULL;
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
        int r;

        assert(reply);
        assert(ret);

        r = sd_bus_message_read_basic(reply, 's', &version_json);
        if (r < 0)
                return bus_log_parse_error(r);

        r = sd_json_parse(version_json, 0, &json, NULL, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse JSON: %m");

        assert(sd_json_variant_is_object(json));

        static const sd_json_dispatch_field dispatch_table[] = {
                { "version",       SD_JSON_VARIANT_STRING,  json_dispatch_version,    offsetof(DescribeParams, v.version),     0 },
                { "newest",        SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, newest),        0 },
                { "available",     SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, available),     0 },
                { "installed",     SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, installed),     0 },
                { "partial",       SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, partial),       0 },
                { "pending",       SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, pending),       0 },
                { "obsolete",      SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, obsolete),      0 },
                { "protected",     SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, protected),     0 },
                { "incomplete",    SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DescribeParams, incomplete),    0 },
                { "changelogUrls", SD_JSON_VARIANT_ARRAY,   sd_json_dispatch_strv,    offsetof(DescribeParams, v.changelog),   0 },
                { "contents",      SD_JSON_VARIANT_ARRAY,   sd_json_dispatch_variant, offsetof(DescribeParams, contents_json), 0 },
                {},
        };

        _cleanup_(describe_params_done) DescribeParams p = {};

        r = sd_json_dispatch(json, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
        if (r < 0)
                return log_error_errno(r, "Failed to parse JSON: %m");

        SET_FLAG(p.v.flags, UPDATE_NEWEST, p.newest);
        SET_FLAG(p.v.flags, UPDATE_AVAILABLE, p.available);
        SET_FLAG(p.v.flags, UPDATE_INSTALLED, p.installed);
        SET_FLAG(p.v.flags, UPDATE_PARTIAL, p.partial);
        SET_FLAG(p.v.flags, UPDATE_PENDING, p.pending);
        SET_FLAG(p.v.flags, UPDATE_OBSOLETE, p.obsolete);
        SET_FLAG(p.v.flags, UPDATE_PROTECTED, p.protected);
        SET_FLAG(p.v.flags, UPDATE_INCOMPLETE, p.incomplete);

        r = sd_json_variant_format(p.contents_json, 0, &p.v.contents_json);
        if (r < 0)
                return log_error_errno(r, "Failed to format JSON for contents: %m");

        *ret = TAKE_STRUCT(p.v);
        return 0;
}

static int list_versions_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        Table *table = ASSERT_PTR(op->userdata);
        const sd_bus_error *e;
        _cleanup_(version_done) Version v = {};
        _cleanup_free_ char *version_link = NULL;
        const char *color;
        int r;

        assert(reply);

        e = sd_bus_message_get_error(reply);
        if (e)
                return log_bus_error(0, e, NULL, "call Describe");

        r = parse_describe(reply, &v);
        if (r < 0)
                return log_error_errno(r, "Failed to parse Describe output: %m");

        color = update_set_flags_to_color(v.flags);

        if (urlify_enabled() && !strv_isempty(v.changelog)) {
                version_link = strjoin(v.version, glyph(GLYPH_EXTERNAL_LINK));
                if (!version_link)
                        return log_oom();
        }

        r = table_add_many(table,
                           TABLE_STRING,    update_set_flags_to_glyph(v.flags),
                           TABLE_SET_COLOR, color,
                           TABLE_STRING,    version_link ?: v.version,
                           TABLE_SET_COLOR, color,
                           TABLE_SET_URL,   strv_isempty(v.changelog) ? NULL : v.changelog[0],
                           TABLE_STRING,    update_set_flags_to_string(v.flags),
                           TABLE_SET_COLOR, color);
        if (r < 0)
                return table_log_add_error(r);

        return 0;
}

static int list_versions(sd_bus *bus, const char *target_path) {
        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_strv_free_ char **versions = NULL;
        unsigned remaining = 0;
        int r;

        r = sd_bus_call_method(
                        bus,
                        bus_sysupdate_mgr->destination,
                        target_path,
                        SYSUPDATE_TARGET_INTERFACE,
                        "List",
                        &error,
                        &reply,
                        "t",
                        arg_offline ? SD_SYSUPDATE_OFFLINE : 0);
        if (r < 0)
                return log_bus_error(r, &error, NULL, "call List");

        r = sd_bus_message_read_strv(reply, &versions);
        if (r < 0)
                return bus_log_parse_error(r);

        table = table_new("", "version", "status");
        if (!table)
                return log_oom();

        (void) table_set_sort(table, 1);
        (void) table_set_reverse(table, 1, true);

        r = sd_event_default(&event);
        if (r < 0)
                return log_error_errno(r, "Failed to get event loop: %m");

        r = sd_bus_attach_event(bus, event, 0);
        if (r < 0)
                return log_error_errno(r, "Failed to attach bus to event loop: %m");

        r = sd_event_set_signal_exit(event, true);
        if (r < 0)
                return log_error_errno(r, "Failed to set up interrupt handler: %m");

        STRV_FOREACH(version, versions) {
                _cleanup_(operation_freep) Operation *op = NULL;
                op = operation_new(table, bus, &remaining, NULL, NULL);
                if (!op)
                        return log_oom();

                r = sd_bus_call_method_async(bus,
                                             NULL,
                                             bus_sysupdate_mgr->destination,
                                             target_path,
                                             SYSUPDATE_TARGET_INTERFACE,
                                             "Describe",
                                             list_versions_finished,
                                             op,
                                             "st",
                                             *version,
                                             arg_offline ? SD_SYSUPDATE_OFFLINE : 0);
                if (r < 0)
                        return log_error_errno(r, "Failed to call Describe: %m");
                TAKE_PTR(op);

                remaining++;
        }

        r = sd_event_loop(event);
        if (r < 0)
                return log_error_errno(r, "Failed to start event loop: %m");

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}

static int describe(sd_bus *bus, const char *target_path, const char *version) {
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
        _cleanup_(version_done) Version v = {};
        sd_json_variant *entry;
        const char *color;
        int r;

        r = sd_bus_call_method(
                        bus,
                        bus_sysupdate_mgr->destination,
                        target_path,
                        SYSUPDATE_TARGET_INTERFACE,
                        "Describe",
                        &error,
                        &reply,
                        "st",
                        version,
                        arg_offline ? SD_SYSUPDATE_OFFLINE : 0);
        if (r < 0)
                return log_bus_error(r, &error, NULL, "call Describe");

        r = parse_describe(reply, &v);
        if (r < 0)
                return log_error_errno(r, "Failed to parse Describe output: %m");

        color = strempty(update_set_flags_to_color(v.flags));

        printf("%s%s%s Version: %s\n"
               "    State: %s%s%s\n",
               color,
               update_set_flags_to_glyph(v.flags),
               ansi_normal(),
               v.version,
               color,
               update_set_flags_to_string(v.flags),
               ansi_normal());

        STRV_FOREACH(url, v.changelog) {
                _cleanup_free_ char *changelog_link = NULL;

                r = terminal_urlify(*url, NULL, &changelog_link);
                if (r < 0)
                        return log_error_errno(r, "Could not urlify link to change-log: %m");

                printf("ChangeLog: %s\n", strna(changelog_link));
        }
        printf("\n");

        r = sd_json_parse(v.contents_json, 0, &json, NULL, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse JSON: %m");

        assert(sd_json_variant_is_array(json));

        JSON_VARIANT_ARRAY_FOREACH(entry, json) {
                assert(sd_json_variant_is_object(entry));
                const char *key;
                sd_json_variant *value;

                if (!table) {
                        table = table_new_raw(sd_json_variant_elements(entry) / 2);
                        if (!table)
                                return log_oom();

                        JSON_VARIANT_OBJECT_FOREACH(key, value, entry) {

                                r = table_add_cell(table, NULL, TABLE_HEADER, key);
                                if (r < 0)
                                        return table_log_add_error(r);
                        }
                }

                JSON_VARIANT_OBJECT_FOREACH(key, value, entry) {
                        TableDataType type;
                        uint64_t number;
                        bool boolean;
                        const void *data;

                        if (sd_json_variant_is_string(value)) {
                                type = TABLE_STRING;
                                assert_se(data = sd_json_variant_string(value));
                        } else if (sd_json_variant_is_unsigned(value)) {
                                type = TABLE_UINT64;
                                number = sd_json_variant_unsigned(value);
                                data = &number;
                        } else if (sd_json_variant_is_boolean(value)) {
                                type = TABLE_BOOLEAN;
                                boolean = sd_json_variant_boolean(value);
                                data = &boolean;
                        } else if (sd_json_variant_is_null(value)) {
                                type = TABLE_EMPTY;
                                data = NULL;
                        } else
                                assert_not_reached();

                        if (streq(key, "ptflags"))
                                type = TABLE_UINT64_HEX;
                        else if (streq(key, "size"))
                                type = TABLE_SIZE;
                        else if (streq(key, "mode"))
                                type = TABLE_MODE;
                        else if (streq(key, "mtime"))
                                type = TABLE_TIMESTAMP;

                        r = table_add_cell(table, NULL, type, data);
                        if (r < 0)
                                return table_log_add_error(r);
                }
        }

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}

VERB(verb_list, "list", "[TARGET[@VERSION]]", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY,
     "List available targets and versions");
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        _cleanup_free_ char *target_path = NULL, *version = NULL;
        int r;

        if (argc == 1)
                return list_targets(bus);

        r = parse_target(argv[1], &target_path, &version);
        if (r < 0)
                return log_oom();

        if (version)
                return describe(bus, target_path, version);
        else
                return list_versions(bus, target_path);
}

static int check_describe_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        Table *table = ASSERT_PTR(op->userdata);
        _cleanup_(version_done) Version v = {};
        _cleanup_free_ char *update = NULL;
        const sd_bus_error *e;
        sd_bus_error error = {};
        const char *lnk = NULL;
        char *current;
        int r;

        assert(reply);

        e = sd_bus_message_get_error(reply);
        if (e)
                return log_bus_error(0, e, NULL, "call Describe");

        r = parse_describe(reply, &v);
        if (r < 0)
                return log_error_errno(r, "Failed to parse output of Describe: %m");

        r = sd_bus_call_method(
                        op->bus,
                        bus_sysupdate_mgr->destination,
                        op->target_path,
                        SYSUPDATE_TARGET_INTERFACE,
                        "GetVersion",
                        &error,
                        &reply,
                        NULL);
        if (r < 0)
                return log_bus_error(r, &error, op->target_id, "get current version");

        r = sd_bus_message_read_basic(reply, 's', &current);
        if (r < 0)
                return bus_log_parse_error(r);

        if (urlify_enabled() && !strv_isempty(v.changelog))
                lnk = glyph(GLYPH_EXTERNAL_LINK);

        update = strjoin(empty_to_dash(current), " ",
                         glyph(GLYPH_ARROW_RIGHT), " ",
                         v.version, strempty(lnk));
        if (!update)
                return log_oom();

        r = table_add_many(table,
                           TABLE_STRING,  op->target_id,
                           TABLE_STRING,  update,
                           TABLE_SET_URL, strv_isempty(v.changelog) ? NULL : v.changelog[0]);
        if (r < 0)
                return table_log_add_error(r);

        return 0;
}

static int check_finished(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        const sd_bus_error *e;
        const char *new_version = NULL;
        int r;

        assert(reply);

        e = sd_bus_message_get_error(reply);
        if (e)
                return log_bus_error(0, e, op->target_id, "call CheckNew");

        r = sd_bus_message_read(reply, "s", &new_version);
        if (r < 0)
                return bus_log_parse_error(r);

        if (isempty(new_version))
                return 0;

        r = sd_bus_call_method_async(op->bus,
                                     NULL,
                                     bus_sysupdate_mgr->destination,
                                     op->target_path,
                                     SYSUPDATE_TARGET_INTERFACE,
                                     "Describe",
                                     check_describe_finished,
                                     op,
                                     "st",
                                     new_version,
                                     arg_offline ? SD_SYSUPDATE_OFFLINE : 0);
        if (r < 0)
                return log_error_errno(r, "Failed to call Describe: %m");
        TAKE_PTR(op);

        return 0;
}

VERB(verb_check, "check", "[TARGET...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY,
     "Check for updates");
static int verb_check(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        _cleanup_(table_unrefp) Table *table = NULL;
        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
        _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
        size_t n;
        unsigned remaining = 0;
        int r;

        r = ensure_targets(bus, argv + 1, &targets);
        if (r < 0)
                return log_error_errno(r, "Failed to find targets: %m");

        r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse targets: %m");

        table = table_new("target", "update");
        if (!table)
                return log_oom();

        (void) table_set_sort(table, 0);

        r = sd_event_default(&event);
        if (r < 0)
                return log_error_errno(r, "Failed to get event loop: %m");

        r = sd_bus_attach_event(bus, event, 0);
        if (r < 0)
                return log_error_errno(r, "Failed to attach bus to event loop: %m");

        r = sd_event_set_signal_exit(event, true);
        if (r < 0)
                return log_error_errno(r, "Failed to set up interrupt handler: %m");

        for (size_t i = 0; i < n; i++) {
                _cleanup_(operation_freep) Operation *op = NULL;
                op = operation_new(table, bus, &remaining, target_paths[i], targets[i]);
                if (!op)
                        return log_oom();

                r = sd_bus_call_method_async(bus, NULL, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "CheckNew", check_finished, op, NULL);
                if (r < 0)
                        return log_error_errno(r, "Failed to call CheckNew for target %s: %m", targets[i]);
                TAKE_PTR(op);

                remaining++;
        }

        r = sd_event_loop(event);
        if (r < 0)
                return log_error_errno(r, "Failed to start event loop: %m");

        if (table_isempty(table)) {
                log_info("No updates available.");
                return 0;
        }

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}

#define UPDATE_PROGRESS_FAILED INT_MIN
#define UPDATE_PROGRESS_ACQUIRED (INT_MAX - 1)
#define UPDATE_PROGRESS_DONE INT_MAX
/* Make sure it doesn't overlap w/ errno values */
assert_cc(UPDATE_PROGRESS_FAILED < -ERRNO_MAX);

static int update_render_progress(sd_event_source *source, void *userdata) {
        OrderedHashmap *map = ASSERT_PTR(userdata);
        const char *target;
        void *p;
        unsigned total;
        size_t n;
        bool exiting;

        exiting = sd_event_get_state(sd_event_source_get_event(source)) == SD_EVENT_EXITING;

        total = 0;
        n = ordered_hashmap_size(map);

        if (n == 0)
                return 0;

        /* We're outputting lots of small strings to STDERR, which is unbuffered by default. So let's turn
         * on full buffering, so we pass this all to the TTY in one go, to make things more efficient */
        WITH_BUFFERED_STDERR;

        if (!terminal_is_dumb()) {
                for (size_t i = 0; i <= n; i++)
                        fputs("\n", stderr); /* Possibly scroll the terminal to make room (including total) */

                fprintf(stderr, "\e[%zuF", n+1); /* Go back */

                fputs("\e7", stderr); /* Save cursor position */
                fputs("\e[?25l", stderr); /* Hide cursor */
        }

        ORDERED_HASHMAP_FOREACH_KEY(p, target, map) {
                int progress = PTR_TO_INT(p);

                if (progress == UPDATE_PROGRESS_FAILED) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s Unknown failure\n", target, RED_CROSS_MARK());
                        total += 100;
                } else if (progress == -EALREADY) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s Already up-to-date\n", target, GREEN_CHECK_MARK());
                        n--; /* Don't consider this target in the total */
                } else if (progress == -EUCLEAN) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s Update is already acquired and partially installed. Vacuum it to try installing again.\n", target, RED_CROSS_MARK());
                        total += 100;
                } else if (progress < 0) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s %s\n", target, RED_CROSS_MARK(), STRERROR(progress));
                        total += 100;
                } else if (progress == UPDATE_PROGRESS_ACQUIRED) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s Installing\n", target, glyph(GLYPH_DOWNLOAD));
                        total += 100;
                } else if (progress == UPDATE_PROGRESS_DONE) {
                        clear_progress_bar_unbuffered(target);
                        fprintf(stderr, "%s: %s Done\n", target, GREEN_CHECK_MARK());
                        total += 100;
                } else {
                        draw_progress_bar_unbuffered(target, progress);
                        fputs("\n", stderr);
                        total += progress;
                }
        }

        if (n > 1) {
                if (exiting)
                        clear_progress_bar_unbuffered(target);
                else {
                        draw_progress_bar_unbuffered("Total", (double) total / n);
                        if (terminal_is_dumb())
                                fputs("\n", stderr);
                }
        }

        if (!terminal_is_dumb()) {
                if (exiting)
                        fputs("\e[?25h", stderr); /* Show cursor again */
                else
                        fputs("\e8", stderr); /* Restore cursor position */
        } else if (!exiting)
                fputs("------\n", stderr);

        return 0;
}

static int update_properties_changed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
        Operation *op = ASSERT_PTR(userdata);
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        const char *interface;
        uint32_t progress = UINT32_MAX;
        static const struct bus_properties_map prop_map[] = {
                { "Progress", "u", NULL, 0 },
                {}
        };
        int r;

        assert(m);

        r = sd_bus_message_read(m, "s", &interface);
        if (r < 0) {
                bus_log_parse_error_debug(r);
                return 0;
        }

        if (!streq(interface, "org.freedesktop.sysupdate1.Job"))
                return 0;

        r = bus_message_map_all_properties(m, prop_map, /* flags= */ 0, error, &progress);
        if (r < 0)
                return 0; /* map_all_properties does the debug logging internally... */

        if (progress == UINT_MAX)
                return 0;

        r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR((int) progress));
        if (r < 0)
                log_debug_errno(r, "Failed to update hashmap: %m");
        return 0;
}

static int update_install_started(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        const sd_bus_error *e;
        const char *job_path;
        int r;

        assert(reply);

        e = sd_bus_message_get_error(reply);
        if (e) {
                r = -sd_bus_error_get_errno(e);

                r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(r));
                if (r < 0)
                        log_debug_errno(r, "Failed to update hashmap: %m");

                return 0;
        }

        r = sd_bus_message_read(reply, "sto", NULL, &op->job_id, &job_path);
        if (r < 0)
                return bus_log_parse_error(r);
        r = free_and_strdup_warn(&op->job_path, job_path);
        if (r < 0)
                return r;

        /* Update this job in the hashmap. */
        r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(UPDATE_PROGRESS_ACQUIRED));
        if (r < 0)
                log_debug_errno(r, "Failed to update hashmap: %m");

        /* Register for progress notifications for this Install() D-Bus call; previously
         * op->job_properties_slot was registered for progress notifications for the Acquire() D-Bus call. */
        sd_bus_slot_unref(TAKE_PTR(op->job_properties_slot));
        r = sd_bus_match_signal_async(
                        op->bus,
                        &op->job_properties_slot,
                        bus_sysupdate_mgr->destination,
                        job_path,
                        "org.freedesktop.DBus.Properties",
                        "PropertiesChanged",
                        update_properties_changed,
                        NULL,
                        op);
        if (r < 0)
                return log_bus_error(r, NULL, op->target_id, "listen for PropertiesChanged");

        TAKE_PTR(op); /* update_install_finished/update_interrupted take ownership of the data */

        return 0;
}

static int update_install_finished(sd_bus_message *m, void *userdata, sd_bus_error *error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        uint64_t id;
        int r, status;

        assert(m);

        r = sd_bus_message_read(m, "toi", &id, NULL, &status);
        if (r < 0) {
                bus_log_parse_error_debug(r);
                return 0;
        }

        if (id != op->job_id) {
                TAKE_PTR(op);
                return 0;
        }

        if (status == 0) /* success */
                status = UPDATE_PROGRESS_DONE;
        else if (status > 0) /* exit status without errno */
                status = UPDATE_PROGRESS_FAILED; /* i.e. EXIT_FAILURE */
        /* else errno */

        r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(status));
        if (r < 0)
                log_debug_errno(r, "Failed to update hashmap: %m");
        return 0;
}

static int update_acquire_finished(sd_bus_message *m, void *userdata, sd_bus_error *error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        uint64_t id;
        int r, status;

        assert(m);

        r = sd_bus_message_read(m, "toi", &id, NULL, &status);
        if (r < 0) {
                bus_log_parse_error_debug(r);
                return 0;
        }

        if (id != op->job_id) {
                TAKE_PTR(op);
                return 0;
        }

        if (status == 0) /* success */
                status = UPDATE_PROGRESS_ACQUIRED;
        else if (status > 0) /* exit status without errno */
                status = UPDATE_PROGRESS_FAILED; /* i.e. EXIT_FAILURE */
        /* else errno */

        r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(status));
        if (r < 0)
                log_debug_errno(r, "Failed to update hashmap: %m");

        /* Renew the JobRemoved notification for the Install() call instead. */
        sd_bus_slot_unref(op->job_finished_slot);
        r = bus_match_signal_async(
                        op->bus, &op->job_finished_slot, bus_sysupdate_mgr, "JobRemoved", update_install_finished, NULL, op);
        if (r < 0)
                return log_bus_error(r, NULL, op->target_id, "listen for JobRemoved");

        /* With the Acquire() call finished, immediately call Install() to deploy the downloaded update.
         * This reuses the same Operation struct so the progress reporting continues to be done in the same
         * slot in the terminal. */
        r = sd_bus_call_method_async(
                        op->bus,
                        NULL,
                        bus_sysupdate_mgr->destination,
                        op->target_path,
                        SYSUPDATE_TARGET_INTERFACE,
                        "Install",
                        update_install_started,
                        op,
                        "st",
                        op->requested_version,
                        0LU);
        if (r < 0)
                return log_bus_error(r, NULL, op->target_id, "call Install");
        TAKE_PTR(op);

        return 0;
}

static int update_interrupted(sd_event_source *source, void *userdata) {
        /* Since the event loop is exiting, we will never receive the JobRemoved
         * signal. So, we must free the userdata here. */
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        int r;

        /* This call should work regardless of whether we’re cancelling the Acquire() call or the Install()
         * call. */
        r = sd_bus_call_method(op->bus,
                               bus_sysupdate_mgr->destination,
                               op->job_path,
                               "org.freedesktop.sysupdate1.Job",
                               "Cancel",
                               &error,
                               /* ret_reply= */ NULL,
                               NULL);
        if (r < 0)
                return log_bus_error(r, &error, NULL, "call Cancel");

        r = ordered_hashmap_replace(map, op->target_id, INT_TO_PTR(-ECANCELED));
        if (r < 0)
                log_debug_errno(r, "Failed to update hashmap: %m");

        return 0;
}

static int update_acquire_started(sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) {
        _cleanup_(operation_freep) Operation *op = ASSERT_PTR(userdata);
        OrderedHashmap *map = ASSERT_PTR(op->userdata);
        const sd_bus_error *e;
        _cleanup_free_ char *key = NULL;
        const char *new_version, *job_path;
        int r;

        assert(reply);

        e = sd_bus_message_get_error(reply);
        if (e) {
                r = -sd_bus_error_get_errno(e);

                key = strdup(op->target_id);
                if (!key)
                        return log_oom();
                r = ordered_hashmap_put(map, key, INT_TO_PTR(r));
                if (r < 0)
                        return log_error_errno(r, "Failed to update hashmap: %m");
                TAKE_PTR(key);

                return 0;
        }

        r = sd_bus_message_read(reply, "sto", &new_version, &op->job_id, &job_path);
        if (r < 0)
                return bus_log_parse_error(r);
        op->job_path = strdup(job_path);
        if (!op->job_path)
                return log_oom();

        /* Store the version for the subsequent Install() call */
        op->acquired_version = strdup(new_version);
        if (!op->acquired_version)
                return log_oom();

        if (isempty(new_version))
                new_version = "latest";

        /* Register this job into the hashmap. This will give it a progress bar */
        if (strchr(op->target_id, '@'))
                key = strdup(op->target_id);
        else
                key = strjoin(op->target_id, "@", new_version);
        if (!key)
                return log_oom();
        r = ordered_hashmap_put(map, key, INT_TO_PTR(0)); /* takes ownership of key */
        if (r < 0)
                return log_error_errno(r, "Failed to add target to tracking map: %m");
        op->target_id = TAKE_PTR(key); /* just borrowing */

        /* Cancel the job if the event loop exits */
        r = sd_event_add_exit(op->event, &op->job_interrupt_source, update_interrupted, op);
        if (r < 0)
                return log_error_errno(r, "Failed to set up interrupt handler: %m");

        /* We need to cancel the job before the final iteration of the renderer runs */
        r = sd_event_source_set_priority(op->job_interrupt_source, SD_EVENT_PRIORITY_IMPORTANT);
        if (r < 0)
                return log_error_errno(r, "Failed to set interrupt priority: %m");

        /* Register for progress notifications */
        r = sd_bus_match_signal_async(
                        op->bus,
                        &op->job_properties_slot,
                        bus_sysupdate_mgr->destination,
                        job_path,
                        "org.freedesktop.DBus.Properties",
                        "PropertiesChanged",
                        update_properties_changed,
                        NULL,
                        op);
        if (r < 0)
                return log_bus_error(r, NULL, op->target_id, "listen for PropertiesChanged");

        TAKE_PTR(op); /* update_acquire_finished/update_interrupted take ownership of the data */

        return 0;
}

static int do_update(sd_bus *bus, char **targets) {
        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
        _cleanup_(sd_event_source_unrefp) sd_event_source *render_exit = NULL;
        _cleanup_ordered_hashmap_free_ OrderedHashmap *map = NULL;
        _cleanup_strv_free_ char **versions = NULL, **target_paths = NULL;
        size_t n;
        unsigned remaining = 0;
        void *p;
        bool did_anything = false;
        int r;

        assert(bus);
        assert(targets);

        r = parse_targets(targets, &n, &target_paths, &versions);
        if (r < 0)
                return log_error_errno(r, "Could not parse targets: %m");

        map = ordered_hashmap_new(&string_hash_ops_free);
        if (!map)
                return log_oom();

        r = sd_event_default(&event);
        if (r < 0)
                return log_error_errno(r, "Failed to get event loop: %m");

        r = sd_bus_attach_event(bus, event, 0);
        if (r < 0)
                return log_error_errno(r, "Failed to attach bus to event loop: %m");

        r = sd_event_set_signal_exit(event, true);
        if (r < 0)
                return log_error_errno(r, "Failed to set up interrupt handler: %m");

        for (size_t i = 0; i < n; i++) {
                _cleanup_(operation_freep) Operation *op = NULL;
                op = operation_new(map, bus, &remaining, target_paths[i], targets[i]);
                if (!op)
                        return log_oom();

                /* Sign up for notification when the associated job finishes */
                r = bus_match_signal_async(
                                op->bus, &op->job_finished_slot, bus_sysupdate_mgr, "JobRemoved", update_acquire_finished, NULL, op);
                if (r < 0)
                        return log_bus_error(r, NULL, op->target_id, "listen for JobRemoved");

                r = sd_bus_call_method_async(
                                bus,
                                NULL,
                                bus_sysupdate_mgr->destination,
                                target_paths[i],
                                SYSUPDATE_TARGET_INTERFACE,
                                "Acquire",
                                update_acquire_started,
                                op,
                                "st",
                                versions[i],
                                0LU);
                if (r < 0)
                        return log_bus_error(r, NULL, targets[i], "call Acquire");

                op->requested_version = strdup(versions[i]);
                if (!op->requested_version)
                        return log_oom();

                TAKE_PTR(op);

                remaining++;
        }

        /* Set up the rendering */
        r = sd_event_add_post(event, NULL, update_render_progress, map);
        if (r < 0)
                return log_error_errno(r, "Failed to add progress rendering callback: %m");

        r = sd_event_add_exit(event, &render_exit, update_render_progress, map);
        if (r < 0)
                return log_error_errno(r, "Failed to add exit callback: %m");

        r = sd_event_source_set_priority(render_exit, SD_EVENT_PRIORITY_IDLE);
        if (r < 0)
                return log_error_errno(r, "Failed to set priority of update job");

        r = sd_event_loop(event);
        if (r < 0)
                return log_error_errno(r, "Failed to start event loop");

        ORDERED_HASHMAP_FOREACH(p, map) {
                r = PTR_TO_INT(p);
                if (r == -EALREADY)
                        continue;
                if (r == UPDATE_PROGRESS_FAILED)
                        return EXIT_FAILURE;
                if (r < 0)
                        return r;

                did_anything = true;
        }

        return did_anything ? 1 : 0;
}

VERB(verb_update, "update", "[TARGET[@VERSION]...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY,
     "Install updates");
static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        _cleanup_strv_free_ char **targets = NULL;
        bool did_anything = false;
        int r;

        r = ensure_targets(bus, argv + 1, &targets);
        if (r < 0)
                return log_error_errno(r, "Could not find targets: %m");

        r = do_update(bus, targets);
        if (r < 0)
                return r;
        if (r > 0)
                did_anything = true;

        if (!arg_reboot)
                return 0;

        if (did_anything)
                return reboot_now();

        log_info("Nothing was updated... skipping reboot.");
        return 0;
}

static int do_vacuum(sd_bus *bus, const char *target, const char *path) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        uint32_t count, disabled;
        int r;

        r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, path, SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
        if (r < 0)
                return log_bus_error(r, &error, target, "call Vacuum");

        r = sd_bus_message_read(reply, "uu", &count, &disabled);
        if (r < 0)
                return bus_log_parse_error(r);

        if (count > 0 && disabled > 0)
                log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
                         count, disabled, target);
        else if (count > 0)
                log_info("Deleted %u instance(s) of %s.", count, target);
        else if (disabled > 0)
                log_info("Deleted %u disabled transfer(s) of %s.", disabled, target);
        else
                log_info("Found nothing to delete for %s.", target);

        return count + disabled > 0 ? 1 : 0;
}

VERB(verb_vacuum, "vacuum", "[TARGET...]", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY,
     "Clean up old updates");
static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        _cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
        size_t n;
        int r;

        r = ensure_targets(bus, argv + 1, &targets);
        if (r < 0)
                return log_error_errno(r, "Failed to find targets: %m");

        r = parse_targets(targets, &n, &target_paths, /* ret_versions= */ NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse targets: %m");

        for (size_t i = 0; i < n; i++) {
                r = do_vacuum(bus, targets[i], target_paths[i]);
                if (r < 0)
                        return r;
        }
        return 0;
}

typedef struct Feature {
        char *name;
        char *description;
        bool enabled;
        char *documentation;
        char **transfers;
} Feature;

static void feature_done(Feature *f) {
        assert(f);
        f->name = mfree(f->name);
        f->description = mfree(f->description);
        f->documentation = mfree(f->documentation);
        f->transfers = strv_free(f->transfers);
}

static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
        _cleanup_(feature_done) Feature f = {};
        char *json;
        int r;

        static const sd_json_dispatch_field dispatch_table[] = {
                { "name",             SD_JSON_VARIANT_STRING,  sd_json_dispatch_string,  offsetof(Feature, name),          SD_JSON_MANDATORY },
                { "description",      SD_JSON_VARIANT_STRING,  sd_json_dispatch_string,  offsetof(Feature, description),   0                 },
                { "enabled",          SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled),       SD_JSON_MANDATORY },
                { "documentationUrl", SD_JSON_VARIANT_STRING,  sd_json_dispatch_string,  offsetof(Feature, documentation), 0                 },
                { "transfers",        SD_JSON_VARIANT_ARRAY,   sd_json_dispatch_strv,    offsetof(Feature, transfers),     0                 },
                {}
        };

        assert(bus);
        assert(feature);
        assert(ret);

        r = sd_bus_call_method(bus,
                               bus_sysupdate_mgr->destination,
                               SYSUPDATE_HOST_PATH,
                               SYSUPDATE_TARGET_INTERFACE,
                               "DescribeFeature",
                               &error,
                               &reply,
                               "st",
                               feature,
                               UINT64_C(0));
        if (r < 0)
                return log_bus_error(r, &error, "host", "lookup feature");

        r = sd_bus_message_read_basic(reply, 's', &json);
        if (r < 0)
                return bus_log_parse_error(r);

        r = sd_json_parse(json, 0, &v, NULL, NULL);
        if (r < 0)
                return log_error_errno(r, "Failed to parse JSON: %m");

        r = sd_json_dispatch(v, dispatch_table, 0, &f);
        if (r < 0)
                return log_error_errno(r, "Failed to dispatch JSON: %m");

        *ret = TAKE_STRUCT(f);
        return 0;
}

static int list_features(sd_bus *bus) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
        _cleanup_strv_free_ char **features = NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        int r;

        assert(bus);

        table = table_new("", "feature", "description");
        if (!table)
                return log_oom();

        r = sd_bus_call_method(bus,
                               bus_sysupdate_mgr->destination,
                               SYSUPDATE_HOST_PATH,
                               SYSUPDATE_TARGET_INTERFACE,
                               "ListFeatures",
                               &error,
                               &reply,
                               "t",
                               UINT64_C(0));
        if (r < 0)
                return log_bus_error(r, &error, "host", "lookup feature");

        r = sd_bus_message_read_strv(reply, &features);
        if (r < 0)
                return bus_log_parse_error(r);

        STRV_FOREACH(feature, features) {
                _cleanup_(feature_done) Feature f = {};
                _cleanup_free_ char *name_link = NULL;

                r = describe_feature(bus, *feature, &f);
                if (r < 0)
                        return r;

                if (urlify_enabled() && f.documentation) {
                        name_link = strjoin(f.name, glyph(GLYPH_EXTERNAL_LINK));
                        if (!name_link)
                                return log_oom();
                }

                r = table_add_many(table,
                                   TABLE_BOOLEAN_CHECKMARK, f.enabled,
                                   TABLE_SET_COLOR, ansi_highlight_green_red(f.enabled),
                                   TABLE_STRING, name_link ?: f.name,
                                   TABLE_SET_URL, f.documentation,
                                   TABLE_STRING, f.description);
                if (r < 0)
                        return table_log_add_error(r);
        }

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
}

VERB(verb_features, "features", "[FEATURE]", VERB_ANY, 2, VERB_ONLINE_ONLY,
     "List and inspect optional features on host OS");
static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        _cleanup_(table_unrefp) Table *table = NULL;
        _cleanup_(feature_done) Feature f = {};
        int r;

        if (argc == 1)
                return list_features(bus);

        table = table_new_vertical();
        if (!table)
                return log_oom();

        r = describe_feature(bus, argv[1], &f);
        if (r < 0)
                return r;

        r = table_add_many(table,
                           TABLE_FIELD, "Name",
                           TABLE_STRING, f.name,
                           TABLE_FIELD, "Enabled",
                           TABLE_BOOLEAN, f.enabled);
        if (r < 0)
                return table_log_add_error(r);

        if (f.description) {
                r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f.description);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (f.documentation) {
                r = table_add_many(table,
                                   TABLE_FIELD, "Documentation",
                                   TABLE_STRING, f.documentation,
                                   TABLE_SET_URL, f.documentation);
                if (r < 0)
                        return table_log_add_error(r);
        }

        if (!strv_isempty(f.transfers)) {
                r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, f.transfers);
                if (r < 0)
                        return table_log_add_error(r);
        }

        return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, false);
}

VERB(verb_enable, "enable", "FEATURE...", 2, VERB_ANY, VERB_ONLINE_ONLY,
     "Enable optional feature on host OS");
VERB(verb_enable, "disable", "FEATURE...", 2, VERB_ANY, VERB_ONLINE_ONLY,
     "Disable optional feature on host OS");
static int verb_enable(int argc, char *argv[], uintptr_t _data, void *userdata) {
        sd_bus *bus = ASSERT_PTR(userdata);
        bool did_anything = false, enable;
        char **features;
        int r;

        enable = streq(argv[0], "enable");
        features = strv_skip(argv, 1);

        STRV_FOREACH(feature, features) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;

                r = sd_bus_call_method(bus,
                                       bus_sysupdate_mgr->destination,
                                       SYSUPDATE_HOST_PATH,
                                       SYSUPDATE_TARGET_INTERFACE,
                                       "SetFeatureEnabled",
                                       &error,
                                       /* ret_reply= */ NULL,
                                       "sit",
                                       *feature,
                                       (int) enable,
                                       UINT64_C(0));
                if (r < 0)
                        return log_bus_error(r, &error, "host", "call SetFeatureEnabled");
        }

        if (!arg_now) /* We weren't asked to apply the changes, so we're done! */
                return 0;

        if (enable) {
                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
                _cleanup_free_ char *target = NULL;
                char *version = NULL;

                /* We're downloading the new feature into the "current" version, which is either going to be
                 * the currently booted version or it's going to be a pending update that has already been
                 * installed and is just waiting for us to reboot into it. */

                r = sd_bus_call_method(bus,
                                       bus_sysupdate_mgr->destination,
                                       SYSUPDATE_HOST_PATH,
                                       SYSUPDATE_TARGET_INTERFACE,
                                       "GetVersion",
                                       &error,
                                       &reply,
                                       NULL);
                if (r < 0)
                        return log_bus_error(r, &error, "host", "get current version");

                r = sd_bus_message_read_basic(reply, 's', &version);
                if (r < 0)
                        return bus_log_parse_error(r);

                target = strjoin("host@", version);
                if (!target)
                        return log_oom();

                r = do_update(bus, STRV_MAKE(target));
        } else
                r = do_vacuum(bus, "host", SYSUPDATE_HOST_PATH);
        if (r < 0)
                return r;
        if (r > 0)
                did_anything = true;

        if (arg_reboot && did_anything)
                return reboot_now();
        else if (did_anything)
                log_info("Feature(s) %s.", enable ? "downloaded" : "deleted");
        else
                log_info("Nothing %s%s.",
                         enable ? "downloaded" : "deleted",
                         arg_reboot ? ", skipping reboot" :"");

        return 0;
}

static int help(void) {
        _cleanup_free_ char *link = NULL;
        _cleanup_(table_unrefp) Table *verbs = NULL, *verbs2 = NULL, *options = NULL;
        int r;

        r = terminal_urlify_man("updatectl", "1", &link);
        if (r < 0)
                return log_oom();

        r = verbs_get_help_table(&verbs);
        if (r < 0)
                return r;

        r = option_parser_get_help_table_group("Verbs", &verbs2);
        if (r < 0)
                return r;

        r = option_parser_get_help_table(&options);
        if (r < 0)
                return r;

        (void) table_sync_column_widths(0, verbs, verbs2, options);

        printf("%s [OPTIONS...] [VERSION]\n"
               "\n%sManage system updates.%s\n"
               "\n%sCommands:%s\n",
               program_invocation_short_name,
               ansi_highlight(),
               ansi_normal(),
               ansi_underline(),
               ansi_normal());

        r = table_print_or_warn(verbs);
        if (r < 0)
                return r;
        r = table_print_or_warn(verbs2);
        if (r < 0)
                return r;

        printf("\n%sOptions:%s\n",
               ansi_underline(),
               ansi_normal());

        r = table_print_or_warn(options);
        if (r < 0)
                return r;

        printf("\nSee the %s for details.\n", link);
        return 0;
}

VERB_COMMON_HELP_HIDDEN(help);

static int parse_argv(int argc, char *argv[], char ***ret_args) {
        assert(argc >= 0);
        assert(argv);

        OptionParser opts = { argc, argv };

        FOREACH_OPTION_OR_RETURN(c, &opts)
                switch (c) {

                OPTION_LONG("reboot", NULL, "Reboot after updating to newer version"):
                        arg_reboot = true;
                        break;

                OPTION_LONG("offline", NULL, "Do not fetch metadata from the network"):
                        arg_offline = true;
                        break;

                OPTION_LONG("now", NULL, "Download/delete resources immediately"):
                        arg_now = true;
                        break;

                OPTION_COMMON_HOST:
                        arg_transport = BUS_TRANSPORT_REMOTE;
                        arg_host = opts.arg;
                        break;

                OPTION_COMMON_NO_PAGER:
                        arg_pager_flags |= PAGER_DISABLE;
                        break;

                OPTION_COMMON_NO_LEGEND:
                        arg_legend = false;
                        break;

                OPTION_GROUP("Verbs"): {}

                OPTION_COMMON_NO_ASK_PASSWORD:
                        arg_ask_password = false;
                        break;

                OPTION_COMMON_HELP:
                        return help();

                OPTION_COMMON_VERSION:
                        return version();
                }

        *ret_args = option_parser_get_args(&opts);
        return 1;
}

static int run(int argc, char *argv[]) {
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
        int r;

        setlocale(LC_ALL, "");
        log_setup();

        (void) signal(SIGWINCH, columns_lines_cache_reset);

        char **args = NULL;
        r = parse_argv(argc, argv, &args);
        if (r <= 0)
                return r;

        r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, &bus);
        if (r < 0)
                return bus_log_connect_error(r, arg_transport, RUNTIME_SCOPE_SYSTEM);

        (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);

        (void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password);

        return dispatch_verb(args, bus);
}

DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
