/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2013 Red Hat, Inc.
 */

/**
 * SECTION:nmtui-connect
 * @short_description: nm-applet-like functionality
 *
 * nmtui-connect implements activating and deactivating #NMConnections,
 * including presenting a password dialog if necessary.
 */

#include "libnm-client-aux-extern/nm-default-client.h"

#include <stdlib.h>

#include "libnmt-newt/nmt-newt.h"

#include "nmtui.h"
#include "nmtui-connect.h"
#include "nmt-connect-connection-list.h"
#include "nmt-password-dialog.h"
#include "libnmc-base/nm-secret-agent-simple.h"
#include "libnmc-base/nm-vpn-helpers.h"
#include "libnmc-base/nm-client-utils.h"
#include "nmt-utils.h"

static void
secrets_requested(NMSecretAgentSimple *agent,
                  const char          *request_id,
                  const char          *title,
                  const char          *msg,
                  GPtrArray           *secrets,
                  gpointer             user_data)
{
    NMConnection *connection = NM_CONNECTION(user_data);
    gboolean      success    = FALSE;

    /* Get secrets for OpenConnect VPN */
    if (connection && nm_connection_is_type(connection, NM_SETTING_VPN_SETTING_NAME)) {
        NMSettingVpn *s_vpn = nm_connection_get_setting_vpn(connection);

        if (nm_streq0(nm_setting_vpn_get_service_type(s_vpn),
                      NM_SECRET_AGENT_VPN_TYPE_OPENCONNECT)) {
            gs_free_error GError *error = NULL;

            nmt_newt_message_dialog(_("openconnect will be run to authenticate.\nIt will return to "
                                      "nmtui when completed."));

            newtSuspend();

            success = nm_vpn_openconnect_authenticate_helper(s_vpn, secrets, &error);

            newtResume();

            if (!success)
                nmt_newt_message_dialog(_("Error: openconnect failed: %s"), error->message);
        }
    }

    if (!success) {
        gs_unref_object NmtNewtForm *form = NULL;

        form = nmt_password_dialog_new(request_id, title, msg, secrets);
        nmt_newt_form_run_sync(form);

        success = nmt_password_dialog_succeeded(NMT_PASSWORD_DIALOG(form));
    }

    nm_secret_agent_simple_response(agent, request_id, success ? secrets : NULL);
}

typedef struct {
    NMDevice           *device;
    NMActiveConnection *active;
    NmtSyncOp          *op;
} ActivateConnectionInfo;

static void
connect_cancelled(NmtNewtForm *form, gpointer user_data)
{
    ActivateConnectionInfo *info  = user_data;
    GError                 *error = NULL;

    error = g_error_new_literal(G_IO_ERROR, G_IO_ERROR_CANCELLED, "Cancelled");
    nmt_sync_op_complete_boolean(info->op, FALSE, error);
    g_clear_error(&error);
}

static void
check_activated(ActivateConnectionInfo *info)
{
    NMActiveConnectionState ac_state;
    const char             *reason = NULL;
    gs_free_error GError   *error  = NULL;

    ac_state = nmc_activation_get_effective_state(info->active, info->device, &reason);
    if (!NM_IN_SET(ac_state,
                   NM_ACTIVE_CONNECTION_STATE_ACTIVATED,
                   NM_ACTIVE_CONNECTION_STATE_DEACTIVATED))
        return;

    if (ac_state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATED) {
        nm_assert(reason);
        error = g_error_new(NM_CLIENT_ERROR,
                            NM_CLIENT_ERROR_FAILED,
                            _("Activation failed: %s"),
                            reason);
    }

    nmt_sync_op_complete_boolean(info->op, error == NULL, error);
}

static void
activate_ac_state_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
{
    check_activated(user_data);
}

static void
activate_device_state_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
{
    check_activated(user_data);
}

static void
activate_callback(GObject *client, GAsyncResult *result, gpointer user_data)
{
    NmtSyncOp          *op = user_data;
    NMActiveConnection *ac;
    GError             *error = NULL;

    ac = nm_client_activate_connection_finish(NM_CLIENT(client), result, &error);
    if (error)
        nmt_sync_op_complete_pointer(op, NULL, error);
    else
        nmt_sync_op_complete_pointer(op, ac, NULL);
}

static void
add_and_activate_callback(GObject *client, GAsyncResult *result, gpointer user_data)
{
    NmtSyncOp          *op = user_data;
    NMActiveConnection *ac;
    GError             *error = NULL;

    ac = nm_client_add_and_activate_connection_finish(NM_CLIENT(client), result, &error);
    if (error)
        nmt_sync_op_complete_pointer(op, NULL, error);
    else
        nmt_sync_op_complete_pointer(op, ac, NULL);
}

static void
deactivate_connection(NMActiveConnection *ac)
{
    GError *error = NULL;

    if (!nm_client_deactivate_connection(nm_client, ac, NULL, &error)) {
        nmt_newt_message_dialog(_("Could not deactivate connection: %s"), error->message);
        g_clear_error(&error);
    }
}

static void
activate_connection(NMConnection *connection, NMDevice *device, NMObject *specific_object)
{
    NmtNewtForm                         *form;
    gs_unref_object NMSecretAgentSimple *agent = NULL;
    NmtNewtWidget                       *label;
    NmtSyncOp                            op;
    const char                          *specific_object_path;
    NMActiveConnection                  *ac;
    GError                              *error = NULL;
    ActivateConnectionInfo               info  = {};

    form  = g_object_new(NMT_TYPE_NEWT_FORM, NULL);
    label = nmt_newt_label_new(_("Connecting..."));
    nmt_newt_form_set_content(form, label);

    agent = nm_secret_agent_simple_new("nmtui");
    if (agent) {
        if (connection) {
            nm_secret_agent_simple_enable(agent, nm_object_get_path(NM_OBJECT(connection)));
        }
        g_signal_connect(agent,
                         NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS,
                         G_CALLBACK(secrets_requested),
                         connection);
    }

    specific_object_path = specific_object ? nm_object_get_path(specific_object) : NULL;

    /* There's no way to cancel an nm_client_activate_connection() /
     * nm_client_add_and_activate_connection() call, so we always let them
     * complete, even if the user hits Esc; they shouldn't normally take long
     * to complete anyway.
     */

    nmt_sync_op_init(&op);
    if (connection) {
        nm_client_activate_connection_async(nm_client,
                                            connection,
                                            device,
                                            specific_object_path,
                                            NULL,
                                            activate_callback,
                                            &op);
    } else {
        nm_client_add_and_activate_connection_async(nm_client,
                                                    NULL,
                                                    device,
                                                    specific_object_path,
                                                    NULL,
                                                    add_and_activate_callback,
                                                    &op);
    }

    nmt_newt_form_show(form);

    ac = nmt_sync_op_wait_pointer(&op, &error);
    if (!ac) {
        nmt_newt_message_dialog(_("Could not activate connection: %s"), error->message);
        g_clear_error(&error);
        goto done;
    } else if (nm_active_connection_get_state(ac) == NM_ACTIVE_CONNECTION_STATE_ACTIVATED) {
        /* Already active */
        goto done;
    } else if (!nmt_newt_widget_get_realized(NMT_NEWT_WIDGET(form))) {
        /* User already hit Esc */
        goto done;
    }

    if (agent && !connection) {
        connection = NM_CONNECTION(nm_active_connection_get_connection(ac));
        if (connection) {
            nm_secret_agent_simple_enable(agent, nm_object_get_path(NM_OBJECT(connection)));
        }
    }

    /* Now wait for the connection to actually reach the ACTIVATED state,
     * allowing the user to cancel if it takes too long.
     */
    nmt_sync_op_init(&op);
    info.active = ac;
    info.device = device;
    info.op     = &op;

    g_signal_connect(form, "quit", G_CALLBACK(connect_cancelled), &info);
    g_signal_connect(ac,
                     "notify::" NM_ACTIVE_CONNECTION_STATE,
                     G_CALLBACK(activate_ac_state_changed),
                     &info);
    if (device) {
        g_signal_connect(device,
                         "notify::" NM_DEVICE_STATE,
                         G_CALLBACK(activate_device_state_changed),
                         &info);
    }

    if (!nmt_sync_op_wait_boolean(&op, &error)) {
        if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            nmt_newt_message_dialog(_("Could not activate connection: %s"), error->message);
        g_clear_error(&error);
    }

    g_signal_handlers_disconnect_by_func(form, G_CALLBACK(connect_cancelled), &info);
    g_signal_handlers_disconnect_by_func(ac, G_CALLBACK(activate_ac_state_changed), &info);
    if (device)
        g_signal_handlers_disconnect_by_func(device,
                                             G_CALLBACK(activate_device_state_changed),
                                             &info);

done:
    if (nmt_newt_widget_get_realized(NMT_NEWT_WIDGET(form)))
        nmt_newt_form_quit(form);
    g_object_unref(form);

    if (agent)
        nm_secret_agent_old_unregister(NM_SECRET_AGENT_OLD(agent), NULL, NULL);
}

static void
listbox_activated(NmtNewtListbox *listbox, gpointer user_data)
{
    NmtConnectConnectionList *list = NMT_CONNECT_CONNECTION_LIST(listbox);
    NMConnection             *connection;
    NMDevice                 *device;
    NMObject                 *specific_object;
    NMActiveConnection       *ac;

    if (!nmt_connect_connection_list_get_selection(list,
                                                   &connection,
                                                   &device,
                                                   &specific_object,
                                                   &ac))
        return;

    if (ac)
        deactivate_connection(ac);
    else
        activate_connection(connection, device, specific_object);
}

static void
activate_clicked(NmtNewtButton *button, gpointer listbox)
{
    listbox_activated(listbox, NULL);
}

static void
listbox_active_changed(GObject *object, GParamSpec *pspec, gpointer button)
{
    NmtConnectConnectionList *list = NMT_CONNECT_CONNECTION_LIST(object);
    static const char        *activate, *deactivate;
    static int                deactivate_padding, activate_padding;
    NMActiveConnection       *ac;
    gboolean                  has_selection;

    if (G_UNLIKELY(activate == NULL)) {
        int activate_width, deactivate_width;

        activate         = _("Activate");
        activate_width   = nmt_newt_text_width(activate);
        deactivate       = _("Deactivate");
        deactivate_width = nmt_newt_text_width(deactivate);

        activate_padding   = NM_MAX(0, deactivate_width - activate_width);
        deactivate_padding = NM_MAX(0, activate_width - deactivate_width);
    }

    has_selection = nmt_connect_connection_list_get_selection(list, NULL, NULL, NULL, &ac);

    nmt_newt_component_set_sensitive(button, has_selection);
    if (has_selection && ac) {
        nmt_newt_button_set_label(button, deactivate);
        nmt_newt_widget_set_padding(button, 0, 0, deactivate_padding, 0);
    } else {
        nmt_newt_button_set_label(button, activate);
        nmt_newt_widget_set_padding(button, 0, 0, activate_padding, 0);
    }
}

/* Contains both the UI and batch data for wifi-rescans */
typedef struct {
    NmtNewtWidget *activate_button;
    NmtNewtWidget *listbox;
    NmtNewtForm   *rescan_form;
    NmtNewtWidget *rescan_button;
    int            pending;
    GCancellable  *cancellable;
    gboolean       cancelled;
    gboolean       has_error;
    gboolean       timeout_occurred;
} RescanBatch;

typedef struct {
    NMDeviceWifi *wifi_device;
    GSource      *timeout_id;
    gulong        signal_id;
    RescanBatch  *batch;
} RescanData;

static GPtrArray *
active_wifi_devices(void)
{
    const GPtrArray *devices;
    GPtrArray       *active_wifi_devices;

    devices = nm_client_get_devices(nm_client);

    if (!devices)
        return NULL;

    active_wifi_devices = g_ptr_array_new_with_free_func(g_object_unref);

    for (guint i = 0; i < devices->len; i++) {
        NMDevice *dev = g_ptr_array_index((GPtrArray *) devices, i);

        if (!NM_IS_DEVICE_WIFI(dev))
            continue;
        if (NM_IN_SET(nm_device_get_state(dev),
                      NM_DEVICE_STATE_UNAVAILABLE,
                      NM_DEVICE_STATE_UNMANAGED,
                      NM_DEVICE_STATE_FAILED))
            continue;

        g_ptr_array_add(active_wifi_devices, g_object_ref(dev));
    }
    return active_wifi_devices;
}

static void
on_rescan_cancel(NmtNewtForm *form, gpointer user_data)
{
    RescanBatch *batch_data = user_data;

    if (!batch_data || batch_data->cancelled)
        return;

    batch_data->cancelled = TRUE;

    /* Cancel the async operations using nm_clear_g_cancellable */
    if (batch_data->cancellable)
        nm_clear_g_cancellable(&batch_data->cancellable);

    g_object_unref(batch_data->rescan_form);
    batch_data->rescan_form = NULL;
}

/* creates the wifi-rescan form and manages other ui properties while rescanning */
static void
wifi_rescan_form(RescanBatch *batch_data)
{
    NmtNewtForm   *rescan_form;
    NmtNewtWidget *label;

    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->listbox), FALSE);
    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->rescan_button), FALSE);
    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->activate_button), FALSE);

    /* open the scanning form*/
    rescan_form = g_object_new(NMT_TYPE_NEWT_FORM, NULL);
    label       = nmt_newt_label_new(_("Rescanning Wi-Fi devices..."));
    nmt_newt_form_set_content(rescan_form, label);

    /* connect the quit signal to rescan cancel */
    g_signal_connect(rescan_form, "quit", G_CALLBACK(on_rescan_cancel), batch_data);

    nmt_newt_form_show(rescan_form);
    batch_data->rescan_form = rescan_form;
}

/* rebuilds the connection list and changes back the
 * state for various UI components which were
 * changed during rescanning */
static void
on_rescan_complete(gpointer data)
{
    NmtConnectConnectionList *list;
    RescanData               *rescan_data;
    RescanBatch              *batch_data;
    NMDeviceWifi             *wifi_device;

    rescan_data = data;
    batch_data  = rescan_data->batch;
    wifi_device = rescan_data->wifi_device;

    g_free(rescan_data);
    rescan_data = NULL;

    if (batch_data->has_error) {
        nmt_newt_message_dialog(_("Wi-Fi scan failed for device : %s"),
                                nm_device_get_iface(NM_DEVICE(wifi_device)));
        batch_data->has_error = FALSE;
    }

    if (batch_data->timeout_occurred) {
        nmt_newt_message_dialog(_("Wi-Fi scan timed out for device : %s"),
                                nm_device_get_iface(NM_DEVICE(wifi_device)));
        batch_data->timeout_occurred = FALSE;
    }

    /* If the scans are not complete then simply wait for them to complete
     * This also ensures that UI is only enabled either after all
     * scans complete (Either complete or cancel ). Preventing any
     * overlapping scans caused due to the user pressing rescan multiple times
     */

    if (--batch_data->pending > 0)
        return;

    /* If the scan is not cancelled quit normally */
    if (!batch_data->cancelled) {
        if (nmt_newt_widget_get_realized(NMT_NEWT_WIDGET(batch_data->rescan_form))) {
            nmt_newt_form_quit(batch_data->rescan_form);
            nm_clear_g_object(&batch_data->rescan_form);
            batch_data->rescan_form = NULL;
        }
        list = NMT_CONNECT_CONNECTION_LIST(batch_data->listbox);
        nmt_newt_listbox_clear(NMT_NEWT_LISTBOX(list));

        g_object_notify(G_OBJECT(nm_client), NM_CLIENT_CONNECTIONS);
    }

    /* The following cleanup  is required regardless of any cancellation */
    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->listbox), TRUE);
    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->rescan_button), TRUE);
    nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(batch_data->activate_button), TRUE);

    g_clear_object(&batch_data->cancellable);
}

static gboolean
scan_timeout_callback(gpointer user_data)
{
    RescanData *data = user_data;

    nm_clear_g_source_inst(&data->timeout_id);

    /* Timeout reached - scan took too long */
    if (data->signal_id) {
        g_signal_handler_disconnect(data->wifi_device, data->signal_id);
        data->signal_id = 0;
    }

    data->batch->timeout_occurred = TRUE;

    on_rescan_complete(data);
    return G_SOURCE_REMOVE;
}

static void
on_last_scan_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
{
    RescanData *data = user_data;

    /* Scan completed successfully */
    nm_clear_g_source_inst(&data->timeout_id);
    if (data->signal_id) {
        g_signal_handler_disconnect(data->wifi_device, data->signal_id);
        data->signal_id = 0;
    }

    on_rescan_complete(data);
}

static void
wifi_rescan_callback(GObject *source_object, GAsyncResult *result, gpointer rescan_data)
{
    RescanData *data = rescan_data;

    if (!nm_device_wifi_request_scan_finish(data->wifi_device, result, NULL)) {
        data->batch->has_error = TRUE;
        on_rescan_complete(data);
        return;
    }

    /* Listen for last-scan property changes */
    data->signal_id = g_signal_connect(data->wifi_device,
                                       "notify::last-scan",
                                       G_CALLBACK(on_last_scan_changed),
                                       data);

    /* Set a 10-second timeout in case scan doesn't complete */
    data->timeout_id = nm_g_timeout_add_source(RESCAN_TIMEOUT_MS, scan_timeout_callback, data);
}

static void
wifi_rescan(NmtNewtButton *button, gpointer data_batch)
{
    gs_unref_ptrarray GPtrArray *devices = active_wifi_devices();
    RescanData                  *data;
    RescanBatch                 *batch_data = data_batch;

    if (!devices || devices->len == 0) {
        nmt_newt_message_dialog(_("No active Wi-Fi devices found"));
        return;
    }

    /* create a shared batch for all the devices*/
    batch_data->pending     = devices->len;
    batch_data->cancelled   = FALSE;
    batch_data->cancellable = g_cancellable_new();

    wifi_rescan_form(batch_data);

    for (guint i = 0; i < devices->len; i++) {
        NMDevice *dev = g_ptr_array_index(devices, i);

        /* per-device data */
        data              = g_new0(RescanData, 1);
        data->wifi_device = NM_DEVICE_WIFI(dev);
        data->batch       = batch_data;

        nm_device_wifi_request_scan_async(NM_DEVICE_WIFI(dev),
                                          batch_data->cancellable,
                                          wifi_rescan_callback,
                                          data);
    }
}

static void
connect_search_apply(gpointer list, const char *text)
{
    nmt_connect_connection_list_set_filter_text(NMT_CONNECT_CONNECTION_LIST(list), text);
}

static int
connect_search_count(gpointer list)
{
    return nmt_connect_connection_list_get_match_count(NMT_CONNECT_CONNECTION_LIST(list));
}

static NmtNewtForm *
nmt_connect_connection_list(gboolean is_top)
{
    NmtNewtForm                 *form;
    NmtNewtWidget               *list, *activate, *quit, *bbox, *grid, *rescan;
    NmtNewtWidget               *search_row, *search_label, *search_entry;
    NmtSearch                   *search;
    RescanBatch                 *batch_data;
    gs_unref_ptrarray GPtrArray *all_active_wifi_devices = NULL;

    form = g_object_new(NMT_TYPE_NEWT_FORM, "fullscreen-vertical", TRUE, NULL);

    grid = nmt_newt_grid_new();

    list = nmt_connect_connection_list_new();
    nmt_newt_grid_add(NMT_NEWT_GRID(grid), list, 0, 0);
    nmt_newt_grid_set_flags(NMT_NEWT_GRID(grid),
                            list,
                            NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_FILL_Y | NMT_NEWT_GRID_EXPAND_X
                                | NMT_NEWT_GRID_EXPAND_Y);
    g_signal_connect(list, "activated", G_CALLBACK(listbox_activated), NULL);

    bbox = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_VERTICAL);
    nmt_newt_grid_add(NMT_NEWT_GRID(grid), bbox, 1, 0);
    nmt_newt_widget_set_padding(bbox, 1, 1, 0, 1);

    activate = nmt_newt_button_box_add_start(NMT_NEWT_BUTTON_BOX(bbox), _("Activate"));
    g_signal_connect(list, "notify::active", G_CALLBACK(listbox_active_changed), activate);
    listbox_active_changed(G_OBJECT(list), NULL, activate);
    g_signal_connect(activate, "clicked", G_CALLBACK(activate_clicked), list);

    all_active_wifi_devices = active_wifi_devices();
    if (all_active_wifi_devices && all_active_wifi_devices->len > 0) {
        rescan = nmt_newt_button_box_add_start(NMT_NEWT_BUTTON_BOX(bbox), _("Rescan Wi-Fi"));

        batch_data                  = g_new0(RescanBatch, 1);
        batch_data->activate_button = activate;
        batch_data->listbox         = list;
        batch_data->rescan_button   = rescan;

        /* Bind the lifecycle of batch_data to the rescan button.
         * The data will be freed automatically when the button is destroyed i.e form quits. */
        g_object_set_data_full(G_OBJECT(rescan), "rescan-batch-data", batch_data, g_free);

        g_signal_connect(rescan, "clicked", G_CALLBACK(wifi_rescan), batch_data);
    }

    quit = nmt_newt_button_box_add_end(NMT_NEWT_BUTTON_BOX(bbox), is_top ? _("Quit") : _("Back"));
    nmt_newt_widget_set_exit_on_activate(quit, TRUE);

    /* Search row below the list. The row is always present so revealing the
     * entry does not resize the form; vim-style '/' shows the entry, and once a
     * filter is applied the label reports it ("Matching '...' (N)"). */
    search_row = nmt_newt_grid_new();
    nmt_newt_grid_add(NMT_NEWT_GRID(grid), search_row, 0, 1);
    nmt_newt_grid_set_flags(NMT_NEWT_GRID(grid),
                            search_row,
                            NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_EXPAND_X);

    search_label = nmt_newt_label_new("");
    nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_label, 0, 0);

    search_entry = nmt_newt_entry_new(NMT_SEARCH_ENTRY_WIDTH, 0);
    nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_entry, 1, 0);
    nmt_newt_widget_set_padding(search_entry, 1, 0, 0, 0);

    search = nmt_search_new(NMT_NEWT_ENTRY(search_entry),
                            NMT_NEWT_LABEL(search_label),
                            list,
                            connect_search_apply,
                            connect_search_count,
                            list);
    nmt_search_bind_form(search, form);
    g_object_set_data_full(G_OBJECT(form), "search-data", search, g_free);

    nmt_newt_form_set_content(form, grid);
    return form;
}

static NmtNewtForm *
nmt_connect_connection(const char *identifier)
{
    NmtNewtWidget      *list;
    NMConnection       *connection;
    NMDevice           *device;
    NMObject           *specific_object;
    NMActiveConnection *ac;

    list = nmt_connect_connection_list_new();
    if (!nmt_connect_connection_list_get_connection(NMT_CONNECT_CONNECTION_LIST(list),
                                                    identifier,
                                                    &connection,
                                                    &device,
                                                    &specific_object,
                                                    &ac))
        nmt_newt_message_dialog(_("No such connection '%s'"), identifier);
    else if (ac)
        nmt_newt_message_dialog(_("Connection is already active"));
    else
        activate_connection(connection, device, specific_object);
    g_object_unref(list);

    return NULL;
}

NmtNewtForm *
nmtui_connect(gboolean is_top, int argc, char **argv)
{
    if (argc == 2)
        return nmt_connect_connection(argv[1]);
    else
        return nmt_connect_connection_list(is_top);
}
