/* SPDX-License-Identifier: GPL-2.0-or-later */

#include "src/core/nm-default-daemon.h"

#include "nm-ppp-mgr.h"

#include <net/if.h>

#include "NetworkManagerUtils.h"
#include "devices/nm-device-utils.h"
#include "nm-act-request.h"
#include "nm-netns.h"
#include "nm-ppp-manager-call.h"
#include "nm-ppp-status.h"

/*****************************************************************************/

struct _NMPppMgr {
    NMPppMgrConfig config;
    NMPPPManager  *ppp_manager;
    GSource       *idle_start;
    GSource       *connect_timeout_source;
    union {
        struct {
            NMPppMgrIPData ip_data_6;
            NMPppMgrIPData ip_data_4;
        };
        NMPppMgrIPData ip_data_x[2];
    };
    int                ifindex;
    NMPppMgrStatsData  stats_data;
    NMPppMgrState      state;
    NMUtilsIPv6IfaceId ipv6_iid;
    bool               ppp_started : 1;
};

/*****************************************************************************/

#define _NMLOG_DOMAIN      LOGD_PPP
#define _NMLOG_PREFIX_NAME "ppp-mgr"
#define _NMLOG(level, ...) \
    __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG_DOMAIN, _NMLOG_PREFIX_NAME, __VA_ARGS__)

/*****************************************************************************/

NM_UTILS_LOOKUP_STR_DEFINE(
    nm_ppp_mgr_state_to_string,
    NMPppMgrState,
    NM_UTILS_LOOKUP_DEFAULT_WARN("???"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_STARTING, "starting"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX, "waiting-for-ifindex"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_HAVE_IFINDEX, "have-ifindex"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_HAVE_IP_CONFIG, "have-ip-config"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_FAILED_TO_START, "failed-to-start"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_FAILED_TO_IFINDEX, "failed-to-ifindex"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_STATE_FAILED, "failed"));

NM_UTILS_LOOKUP_STR_DEFINE(nm_ppp_mgr_callback_type_to_string,
                           NMPppMgrCallbackType,
                           NM_UTILS_LOOKUP_DEFAULT_WARN("???"),
                           NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                                    "state-changed"),
                           NM_UTILS_LOOKUP_STR_ITEM(NM_PPP_MGR_CALLBACK_TYPE_STATS_CHANGED,
                                                    "stats-changed"));

/*****************************************************************************/

#define SELF_TO_USERDATA(self) (&(self)->idle_start)

#define SELF_FROM_USERDATA(user_data) \
    ((NMPppMgr *) (((const char *) (user_data)) - G_STRUCT_OFFSET(NMPppMgr, idle_start)))

/*****************************************************************************/

gboolean
_nm_assert_is_ppp_mgr(const NMPppMgr *self)
{
    nm_assert(G_IS_OBJECT(self->ppp_manager));
    nm_assert(NM_IS_NETNS(self->config.netns));
    nm_assert(self->config.parent_iface);

    return TRUE;
}

/*****************************************************************************/

static void
_callback_emit_with_data(NMPppMgr *self, const NMPppMgrCallbackData *callback_data)
{
    char sbuf_int[30];

    switch (callback_data->callback_type) {
    case NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED:
        _LOGT("emit signal: %s, state=%s%s%s%s%s, reason=%s",
              nm_ppp_mgr_callback_type_to_string(callback_data->callback_type),
              nm_ppp_mgr_state_to_string(callback_data->data.state),
              NM_PRINT_FMT_QUOTED2(callback_data->data.state != callback_data->data.old_state,
                                   ", old-state=",
                                   nm_ppp_mgr_state_to_string(callback_data->data.old_state),
                                   ""),
              NM_PRINT_FMT_QUOTED2(callback_data->data.ifindex > 0,
                                   ", ifindex=",
                                   nm_sprintf_buf(sbuf_int, "%d", callback_data->data.ifindex),
                                   ""),
              callback_data->data.reason_msg);
        break;
    case NM_PPP_MGR_CALLBACK_TYPE_STATS_CHANGED:
        /* This signal might be emitted every 5 seconds. It's too verbose for logging.
         * Be silent. */
        break;
    default:
        nm_assert_not_reached();
    }

    self->config.callback(self, callback_data, self->config.user_data);
}

static void
_callback_emit_state_change(NMPppMgr            *self,
                            NMPppMgrCallbackType callback_type,
                            NMPppMgrState        old_state,
                            gboolean             ip_changed_4,
                            gboolean             ip_changed_6,
                            NMDeviceStateReason  reason,
                            const char          *reason_msg)
{
    const NMPppMgrCallbackData callback_data = {
        .callback_type = callback_type,
        .data =
            {
                .old_state    = old_state,
                .state        = self->state,
                .ifindex      = self->ifindex,
                .reason       = reason,
                .reason_msg   = reason_msg,
                .ip_data_4    = &self->ip_data_4,
                .ip_data_6    = &self->ip_data_6,
                .stats_data   = &self->stats_data,
                .ip_changed_4 = ip_changed_4,
                .ip_changed_6 = ip_changed_6,
            },
    };

    _callback_emit_with_data(self, &callback_data);
}

/*****************************************************************************/

NMPppMgrState
nm_ppp_mgr_get_state(const NMPppMgr *self)
{
    g_return_val_if_fail(NM_IS_PPP_MGR(self), NM_PPP_MGR_STATE_FAILED);

    return self->state;
}

int
nm_ppp_mgr_get_ifindex(const NMPppMgr *self)
{
    g_return_val_if_fail(NM_IS_PPP_MGR(self), 0);

    return self->ifindex;
}

const NMPppMgrIPData *
nm_ppp_mgr_get_ip_data(const NMPppMgr *self, int addr_family)
{
    const int IS_IPv4 = NM_IS_IPv4(addr_family);

    g_return_val_if_fail(NM_IS_PPP_MGR(self), NULL);

    return &self->ip_data_x[IS_IPv4];
}

const NMPppMgrStatsData *
nm_ppp_mgr_get_stats(const NMPppMgr *self)
{
    g_return_val_if_fail(NM_IS_PPP_MGR(self), NULL);

    return &self->stats_data;
}

/*****************************************************************************/

static void
_set_state(NMPppMgr *self, NMPppMgrState state, NMPppMgrState *out_old_state)
{
    if (state >= NM_PPP_MGR_STATE_HAVE_IP_CONFIG)
        nm_clear_g_source_inst(&self->connect_timeout_source);

    NM_SET_OUT(out_old_state, self->state);
    if (self->state != state) {
        _LOGT("set state: %s (was %s)",
              nm_ppp_mgr_state_to_string(state),
              nm_ppp_mgr_state_to_string(self->state));
        self->state = state;
    }
}

static void
_set_state_failed(NMPppMgr *self, NMPppMgrState state, NMPppMgrState *out_old_state)
{
    nm_assert(state >= _NM_PPP_MGR_STATE_FAILED_START);
    nm_assert(self->state < _NM_PPP_MGR_STATE_FAILED_START);

    _set_state(self, state, out_old_state);

    self->ifindex = 0;
    nm_clear_l3cd(&self->ip_data_4.l3cd);
    nm_clear_l3cd(&self->ip_data_6.l3cd);
    self->ip_data_4 = (NMPppMgrIPData){
        .ip_received = FALSE,
        .ip_enabled  = FALSE,
    };
    self->ip_data_6 = (NMPppMgrIPData){
        .ip_received = FALSE,
        .ip_enabled  = FALSE,
    };

    if (self->ppp_manager) {
        g_signal_handlers_disconnect_by_data(self->ppp_manager, SELF_TO_USERDATA(self));
        if (self->ppp_started) {
            self->ppp_started = FALSE;
            nm_ppp_manager_stop(self->ppp_manager, NULL, NULL, NULL);
        }
        g_object_unref(self->ppp_manager);
    }
}

static gboolean
_state_ready_for_have_ifindex(NMPppMgr *self)
{
    if (self->ip_data_4.ip_received) {
        /* once we receive an IPv4 config, we consider this as ready.
         *
         * The problem is that we don't know when we can expect an IPv6 config
         * too, so we cannot just keep waiting. I don't know how to solve this, but
         * it means IPv4+IPv6 together doesn't work well (because we would not
         * wait for IPv6, once IPv4 config is received. */
        return TRUE;
    }

    return ((!self->ip_data_4.ip_enabled || self->ip_data_4.ip_received)
            && (!self->ip_data_6.ip_enabled || self->ip_data_6.ip_received));
}

/*****************************************************************************/

static void
_ppp_signal_state_changed(NMPPPManager *ppp_manager, guint ppp_state_u, gpointer user_data)
{
    NMPppMgr           *self      = SELF_FROM_USERDATA(user_data);
    NMPPPStatus         ppp_state = ppp_state_u;
    NMPppMgrState       state;
    NMPppMgrState       old_state;
    NMDeviceStateReason reason;
    const char         *reason_msg;

    if ((guint) ppp_state != ppp_state_u)
        ppp_state = NM_PPP_STATUS_UNKNOWN;

    switch (ppp_state) {
    case NM_PPP_STATUS_DISCONNECT:
        state      = NM_PPP_MGR_STATE_FAILED;
        reason     = NM_DEVICE_STATE_REASON_PPP_DISCONNECT;
        reason_msg = "ppp signals disconnect";
        break;
    case NM_PPP_STATUS_DEAD:
    case NM_PPP_STATUS_INTERN_DEAD:
        state      = NM_PPP_MGR_STATE_FAILED;
        reason     = NM_DEVICE_STATE_REASON_PPP_FAILED;
        reason_msg = "ppp signals disconnect";
        break;
    default:
        _LOGT("ppp signal about state changed: #%u signal (ignored)", ppp_state_u);
        return;
    }

    _LOGT("ppp signal about state changed: #%u signal, new-state %s, state-reason=%s, %s",
          ppp_state_u,
          nm_ppp_mgr_state_to_string(state),
          nm_device_state_reason_to_string(reason),
          reason_msg);

    _set_state_failed(self, state, &old_state);
    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                old_state,
                                FALSE,
                                FALSE,
                                reason,
                                reason_msg);
}

static void
_ppp_signal_ifindex_set(NMPPPManager *ppp_manager,
                        int           ifindex,
                        const char   *ifname,
                        gpointer      user_data)
{
    NMPppMgr     *self = SELF_FROM_USERDATA(user_data);
    NMPppMgrState old_state;
    NMPppMgrState new_state;
    gboolean      ip_changed_4;
    gboolean      ip_changed_6;

    if (ifindex <= 0) {
        nm_assert(self->state == NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX);
        _LOGT("ifindex set: invalid ifindex received");
        _set_state_failed(self, NM_PPP_MGR_STATE_FAILED_TO_IFINDEX, &old_state);
        _callback_emit_state_change(self,
                                    NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                    old_state,
                                    FALSE,
                                    FALSE,
                                    NM_DEVICE_STATE_REASON_PPP_FAILED,
                                    "invalid ifindex provided by ppp plugin");
        return;
    }

    if (self->ifindex > 0) {
        nm_assert(self->state > NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX);
        nm_assert(self->state < _NM_PPP_MGR_STATE_FAILED_START);

        _LOGT("ifindex set: ignore ifindex %d, already set to %d", ifindex, self->ifindex);
        return;
    }

    nm_assert(self->state == NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX);
    self->ifindex = ifindex;

    if (_state_ready_for_have_ifindex(self)) {
        new_state    = NM_PPP_MGR_STATE_HAVE_IP_CONFIG;
        ip_changed_4 = self->ip_data_4.ip_received;
        ip_changed_6 = self->ip_data_6.ip_received;
    } else {
        new_state    = NM_PPP_MGR_STATE_HAVE_IFINDEX;
        ip_changed_4 = FALSE;
        ip_changed_6 = FALSE;
    }

    _LOGT("ifindex set: ifindex %d", ifindex);

    _set_state(self, new_state, &old_state);
    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                old_state,
                                ip_changed_4,
                                ip_changed_6,
                                NM_DEVICE_STATE_REASON_PPP_FAILED,
                                "invalid ifindex provided by ppp plugin");
}

static void
_ppp_signal_new_config(NMPPPManager             *ppp_manager,
                       int                       addr_family,
                       const NML3ConfigData     *l3cd,
                       const NMUtilsIPv6IfaceId *iid,
                       gpointer                  user_data)
{
    NMPppMgr     *self    = SELF_FROM_USERDATA(user_data);
    const int     IS_IPv4 = NM_IS_IPv4(addr_family);
    NMPppMgrState old_state;
    gboolean      ip_changed_4;
    gboolean      ip_changed_6;

    nm_assert(self->state >= NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX);
    nm_assert(self->state < _NM_PPP_MGR_STATE_FAILED_START);

    if (nm_l3_config_data_equal(self->ip_data_x[IS_IPv4].l3cd, l3cd))
        l3cd = self->ip_data_x[IS_IPv4].l3cd;

    nm_l3_config_data_reset(&self->ip_data_x[IS_IPv4].l3cd, l3cd);
    if (!IS_IPv4) {
        if (iid)
            self->ipv6_iid = *iid;
        else
            self->ipv6_iid = (NMUtilsIPv6IfaceId) NM_UTILS_IPV6_IFACE_ID_INIT;
        self->ip_data_6.ipv6_iid =
            nm_utils_memeqzero(&self->ipv6_iid, sizeof(self->ipv6_iid)) ? NULL : &self->ipv6_iid;
    }
    self->ip_data_x[IS_IPv4].ip_received = TRUE;

    if (self->state == NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX) {
        /* we still wait for the ifindex. We just cache the IP configuration,
         * but leave the state unchanged. */
        _LOGT("ip-config v%c received (still waiting for ifindex)",
              nm_utils_addr_family_to_char(addr_family));
        old_state = NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX;
    } else {
        NMPppMgrState new_state;

        _LOGT("ip-config v%c received", nm_utils_addr_family_to_char(addr_family));
        new_state = _state_ready_for_have_ifindex(self) ? NM_PPP_MGR_STATE_HAVE_IP_CONFIG
                                                        : NM_PPP_MGR_STATE_HAVE_IFINDEX;
        nm_assert((self->state == NM_PPP_MGR_STATE_HAVE_IFINDEX)
                  || (self->state == NM_PPP_MGR_STATE_HAVE_IP_CONFIG
                      && new_state == NM_PPP_MGR_STATE_HAVE_IP_CONFIG));
        _set_state(self, new_state, &old_state);
    }

    ip_changed_4 = IS_IPv4;
    ip_changed_6 = !IS_IPv4;

    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                old_state,
                                ip_changed_4,
                                ip_changed_6,
                                NM_DEVICE_STATE_REASON_NONE,
                                "ip config received");
}

static void
_ppp_signal_stats(NMPPPManager *ppp_manager, guint in_bytes, guint out_bytes, gpointer user_data)
{
    NMPppMgr *self = SELF_FROM_USERDATA(user_data);

    if (self->stats_data.in_bytes == in_bytes && self->stats_data.out_bytes == out_bytes)
        return;

    self->stats_data.in_bytes  = in_bytes;
    self->stats_data.out_bytes = out_bytes;

    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATS_CHANGED,
                                self->state,
                                FALSE,
                                FALSE,
                                NM_DEVICE_STATE_REASON_NONE,
                                "stats update");
}

/*****************************************************************************/

static gboolean
_ifindex_timeout_cb(gpointer user_data)
{
    NMPppMgr     *self = user_data;
    NMPppMgrState old_state;

    nm_clear_g_source_inst(&self->connect_timeout_source);

    _set_state_failed(self, NM_PPP_MGR_STATE_FAILED_TO_IFINDEX, &old_state);
    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                old_state,
                                FALSE,
                                FALSE,
                                NM_DEVICE_STATE_REASON_PPP_FAILED,
                                "timeout connecting");
    return G_SOURCE_CONTINUE;
}

/*****************************************************************************/

static gboolean
_idle_start_cb(gpointer user_data)
{
    NMPppMgr             *self  = user_data;
    gs_free_error GError *error = NULL;
    NMPppMgrState         old_state;
    gboolean              ip4_enabled;
    gboolean              ip6_enabled;
    NMPlatform           *platform;
    const NMPlatformLink *plink;

    nm_clear_g_source_inst(&self->idle_start);

    /* We only evaluate ipx_enabled here. That is because nm_ppp_manager_start()
     * will evaluate it based on act_req's applied connection (like we do now),
     * but as the applied connection can be reapplied, let's do it at the
     * same time to be sure we agree.
     *
     * This should be nicer solved by NMPPPManager not accessing the NMConnection
     * or make little/no use of NMActRequest. */
    nm_utils_ppp_ip_methods_enabled(nm_act_request_get_applied_connection(self->config.act_req),
                                    &ip4_enabled,
                                    &ip6_enabled);
    self->ip_data_4.ip_enabled = !!ip4_enabled;
    self->ip_data_6.ip_enabled = !!ip6_enabled;

    g_signal_connect(self->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
                     G_CALLBACK(_ppp_signal_state_changed),
                     SELF_TO_USERDATA(self));
    g_signal_connect(self->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
                     G_CALLBACK(_ppp_signal_ifindex_set),
                     SELF_TO_USERDATA(self));
    g_signal_connect(self->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_NEW_CONFIG,
                     G_CALLBACK(_ppp_signal_new_config),
                     SELF_TO_USERDATA(self));
    g_signal_connect(self->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_STATS,
                     G_CALLBACK(_ppp_signal_stats),
                     SELF_TO_USERDATA(self));

    platform = nm_netns_get_platform(self->config.netns);
    plink    = nm_platform_link_get_by_ifname(platform, self->config.parent_iface);
    if (plink && !NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP)) {
        nm_platform_link_change_flags(platform, plink->ifindex, IFF_UP, TRUE);
    }

    self->ppp_started = TRUE;
    if (!nm_ppp_manager_start(self->ppp_manager,
                              self->config.act_req,
                              self->config.ppp_username,
                              0,
                              self->config.baud_override,
                              &error)) {
        gs_free char *reason_msg = NULL;

        _set_state_failed(self, NM_PPP_MGR_STATE_FAILED_TO_START, &old_state);
        reason_msg = g_strdup_printf("failed to start: %s", error->message);
        _callback_emit_state_change(self,
                                    NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                    old_state,
                                    FALSE,
                                    FALSE,
                                    NM_DEVICE_STATE_REASON_PPP_START_FAILED,
                                    reason_msg);
        return G_SOURCE_CONTINUE;
    }

    nm_assert(self->state == NM_PPP_MGR_STATE_STARTING);
    _set_state(self, NM_PPP_MGR_STATE_WAITING_FOR_IFINDEX, &old_state);

    self->connect_timeout_source =
        nm_g_timeout_add_seconds_source(self->config.timeout_secs, _ifindex_timeout_cb, self);

    _callback_emit_state_change(self,
                                NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED,
                                old_state,
                                FALSE,
                                FALSE,
                                NM_DEVICE_STATE_REASON_NONE,
                                "pppd is starting");

    return G_SOURCE_CONTINUE;
}

/*****************************************************************************/

NMPppMgr *
nm_ppp_mgr_start(const NMPppMgrConfig *config, GError **error)
{
    NMPppMgr     *self;
    NMPPPManager *ppp_manager;

    g_return_val_if_fail(config, NULL);
    g_return_val_if_fail(NM_IS_NETNS(config->netns), NULL);
    g_return_val_if_fail(config->parent_iface, NULL);
    g_return_val_if_fail(NM_IS_ACT_REQUEST(config->act_req), NULL);
    g_return_val_if_fail(config->callback, NULL);
    g_return_val_if_fail(!error || !*error, NULL);

    ppp_manager = nm_ppp_manager_create(config->parent_iface, error);

    if (!ppp_manager)
        return NULL;

    self = g_slice_new(NMPppMgr);

    *self = (NMPppMgr){
        .config      = *config,
        .ppp_manager = ppp_manager,
        .idle_start  = nm_g_idle_add_source(_idle_start_cb, self),
        .state       = NM_PPP_MGR_STATE_STARTING,
        .ip_data_4 =
            {
                .ip_received = FALSE,
                .ip_enabled  = NM_OPTION_BOOL_DEFAULT,
            },
        .ip_data_6 =
            {
                .ip_received = FALSE,
                .ip_enabled  = NM_OPTION_BOOL_DEFAULT,
            },
        .stats_data =
            {
                .in_bytes  = 0,
                .out_bytes = 0,
            },
    };

    g_object_ref(self->config.act_req);
    g_object_ref(self->config.netns);
    self->config.parent_iface = g_strdup(self->config.parent_iface);
    self->config.ppp_username = g_strdup(self->config.ppp_username);

    _LOGD("created");

    return self;
}

void
nm_ppp_mgr_destroy(NMPppMgr *self)
{
    if (!self)
        return;

    _LOGD("destroying");

    if (self->state < _NM_PPP_MGR_STATE_FAILED_START)
        _set_state_failed(self, NM_PPP_MGR_STATE_FAILED, NULL);

    nm_clear_g_source_inst(&self->idle_start);
    nm_clear_g_source_inst(&self->connect_timeout_source);

    g_object_unref(self->config.act_req);

    g_free((char *) self->config.parent_iface);
    g_free((char *) self->config.ppp_username);

    g_object_unref(self->config.netns);

    nm_g_slice_free(self);
}
