// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2024, Richard Acayan. All rights reserved.
 */

#include <gio/gio.h>
#include <glib.h>
#include <libmm-glib.h>
#include <libqrtr.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>

#include "qmi_imsd.h"
#include "qvd-message.h"
#include "qvd-modem.h"
#include "qvd-server.h"

#define IMSD_START_CONNECTION 0x20
#define IMSD_STOP_CONNECTION 0x21

#define IMSD_CONNECTION_CHANGED 0x20

enum imsd_address_family {
	IMSD_AF_IPV4 = 0,
	IMSD_AF_IPV6 = 1,
};

enum imsd_profiles {
	IMSD_PROFILES_3GPP2 = 0,
	IMSD_PROFILES_3GPP = 1,
	IMSD_PROFILES_3GPP_3GPP2 = 2,
};

struct qvd_connect_data {
	QVDServer *server;
	QVDConnection *conn;
	QVDMessage *msg;
	guint32 connection_id;
	guint32 subscription_id;
};

struct _QVDServer {
	GObject parent_instance;

	GSocket *sock;
	GSource *source;
	GHashTable *clients;
	GPtrArray *conns;

	QVDModem *modem;
	gulong modem_sig;

	struct qrtr_packet outpkt;
	char *outbuf;

	guint16 ind_txn;
};

static void qvd_server_initable_iface_init(GInitableIface *iface);
static gboolean qvd_server_initable_init(GInitable *initable,
					 GCancellable *cancellable,
					 GError **out_err);
static void qvd_server_dispose(GObject *obj);

static gboolean receive_message(GSocket *sock, GIOCondition cond, gpointer data);

static void connection_address_changed(QVDModem *modem,
				       QVDConnection *conn,
				       MMBearerIpFamily ip_type,
				       const gchar *addr,
				       gpointer data);

G_DEFINE_TYPE_WITH_CODE(QVDServer, qvd_server, G_TYPE_OBJECT,
			G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
					      qvd_server_initable_iface_init))

static void qvd_server_class_init(QVDServerClass *c)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(c);

	obj_class->dispose = qvd_server_dispose;
}

static void qvd_server_initable_iface_init(GInitableIface *iface)
{
	iface->init = qvd_server_initable_init;
}

static void qvd_server_init(QVDServer *self)
{
}

static gboolean qvd_server_initable_init(GInitable *initable,
					 GCancellable *cancellable,
					 GError **out_err)
{
	QVDServer *server = QVD_SERVER(initable);
	GError *err = NULL;
	int fd, ret;

	server->ind_txn = 1;

	server->outbuf = g_malloc(65536);

	fd = qrtr_open(0);
	if (fd == -1) {
		g_set_error(out_err, G_FILE_ERROR, G_FILE_ERROR_FAILED, "QRTR open failed");
		return FALSE;
	}

	server->sock = g_socket_new_from_fd(fd, &err);
	if (server->sock == NULL) {
		g_propagate_error(out_err, err);
		qrtr_close(fd);
		return FALSE;
	}

	server->source = g_socket_create_source(server->sock, G_IO_IN, cancellable);
	g_source_set_callback(server->source, G_SOURCE_FUNC(receive_message), server, NULL);
	g_source_attach(server->source, NULL);

	server->conns = g_ptr_array_new_full(0, g_object_unref);
	server->clients = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, NULL);

	ret = qrtr_new_server(fd, 770, 1, 0);
	if (ret) {
		// NOPUSH
		g_set_error(out_err, G_FILE_ERROR, G_FILE_ERROR_FAILED, "QRTR server publish failed");
		return FALSE;
	}

	return TRUE;
}

static void qvd_server_dispose(GObject *obj)
{
	QVDServer *server = QVD_SERVER(obj);
	GSource *src;

	if (server->clients != NULL)
		g_hash_table_destroy(g_steal_pointer(&server->clients));

	if (server->conns != NULL)
		g_ptr_array_unref(g_steal_pointer(&server->conns));

	if (server->source != NULL) {
		src = g_steal_pointer(&server->source);
		g_source_destroy(src);
		g_source_unref(src);
	}

	if (server->outbuf != NULL)
		g_free(g_steal_pointer(&server->outbuf));

	// Close the socket and decrease the modem reference counter.
	g_clear_object(&server->sock);
	g_clear_object(&server->modem);
}

QVDServer *qvd_server_new(QVDModem *modem, GError **out_err)
{
	QVDServer *server;
	GError *err = NULL;

	g_assert(modem != NULL);

	server = g_initable_new(QVD_TYPE_SERVER, NULL, &err, NULL);
	if (err != NULL) {
		g_propagate_error(out_err, err);
		return NULL;
	}

	server->modem = g_object_ref(modem);
	server->modem_sig = g_signal_connect(server->modem, "qvd-connection-address-changed",
					     G_CALLBACK(connection_address_changed), server);

	return server;
}

static void encode_error(QVDMessage *msg, guint16 code)
{
	struct imsd_no_op_resp resp;
	GError *err = NULL;

	resp.res.err_status = 1;
	resp.res.err_code = code;

	qvd_message_respond(msg, &resp, imsd_no_op_resp_ei, &err);
	if (err != NULL)
		g_printerr("IMSD Error: failed to send error response: %s\n",
			   err->message);
}

static void process_no_op(unsigned int msg_id, QVDMessage *msg)
{
	struct imsd_no_op_req req;
	struct imsd_no_op_resp resp = {
		.res = { .err_status = 0, .err_code = 0, },
	};
	GError *err = NULL;

	qvd_message_decode(msg, &req, imsd_no_op_req_ei, &err);
	if (err != NULL) {
		g_printerr("IMSD No Operation: failed to decode request: %s\n",
			   err->message);
	}

	qvd_message_respond(msg, &resp, imsd_no_op_resp_ei, &err);
	if (err != NULL)
		g_printerr("IMSD No Operation: failed to send response: %s\n",
			   err->message);

	g_object_unref(msg);
}

static void register_connection(QVDServer *server, QVDConnection *conn,
				guint32 connection, guint32 subscription)
{
	guint i;

	for (i = 0; i < server->conns->len; i++) {
		if (server->conns->pdata[i] == NULL) {
			server->conns->pdata[i] = conn;
			break;
		}
	}

	if (i == server->conns->len)
		g_ptr_array_add(server->conns, conn);

	qvd_connection_set_handles(conn, connection, subscription, i);
}

static void on_connection_opened(GObject *obj, GAsyncResult *res, gpointer data)
{
	GError *err = NULL;
	struct qvd_connect_data *connect_data = data;
	struct imsd_start_connection_resp resp;
	guint8 local_id;
	gboolean success;

	success = qvd_modem_get_connection_finish(QVD_MODEM(obj), res, &err);
	if (!success) {
		g_printerr("IMSD Start Connection: failed to create bearer: %s\n", err->message);
		encode_error(connect_data->msg, 1);
		goto err;
	}

	qvd_connection_connect(connect_data->conn);

	register_connection(connect_data->server,
			    connect_data->conn,
			    connect_data->connection_id,
			    connect_data->subscription_id);

	qvd_connection_get_handles(connect_data->conn, NULL, NULL, &local_id);

	resp.res.err_status = 0;
	resp.res.err_code = 0;
	resp.connection = local_id;
	resp.orig_conn_id = connect_data->connection_id;
	resp.subscription = connect_data->subscription_id;

	qvd_message_respond(connect_data->msg, &resp, imsd_start_connection_resp_ei, &err);
	if (err != NULL) {
		g_printerr("IMSD Start Connection: failed to send response: %s\n",
			   err->message);
		g_clear_error(&err);
	}

err:
	g_object_unref(connect_data->server);
	g_object_unref(connect_data->msg);
	g_free(connect_data);
}

static void process_start_connection(QVDServer *server, QVDMessage *msg)
{
	MMBearerIpFamily ip_family;
	QVDConnection *conn;
	GError *err = NULL;
	struct imsd_start_connection_req req;
	struct qvd_connect_data *connect_data;
	gint *client;
	guint node, port;

	qvd_message_decode(msg, &req, imsd_start_connection_req_ei, &err);
	if (err != NULL) {
		g_printerr("IMSD Start Connection: failed to decode request: %s\n",
			   err->message);
		g_clear_error(&err);
		encode_error(msg, 1);
		return;
	}

	if (req.conn_params.ip_family == IMSD_AF_IPV4) {
		ip_family = MM_BEARER_IP_FAMILY_IPV4;
	} else if (req.conn_params.ip_family == IMSD_AF_IPV6) {
		ip_family = MM_BEARER_IP_FAMILY_IPV6;
	} else {
		g_printerr("IMSD Start Connection: unknown IP family: %u\n",
			   req.conn_params.ip_family);
		g_clear_error(&err);
		encode_error(msg, 1);
		return;
	}

	// Register the client for indications.
	g_object_get(msg, "peer-node", &node, "peer-port", &port, NULL);
	client = g_malloc(sizeof(gint));
	*client = (node << 16) | port;
	g_hash_table_add(server->clients, client);

	connect_data = g_malloc(sizeof(*connect_data));
	connect_data->msg = msg;
	connect_data->server = g_object_ref(server);

	conn = qvd_modem_get_connection(server->modem, req.conn_params.apn, ip_family,
					NULL, on_connection_opened, connect_data);
	if (conn == NULL) {
		g_printerr("IMSD Start Connection: modem not present\n");
		g_clear_error(&err);
		encode_error(msg, 1);
		g_free(connect_data);
		return;
	}

	connect_data->conn = conn;
	connect_data->connection_id = req.connection;
	connect_data->subscription_id = req.subscription;
}

static void process_stop_connection(QVDServer *server, QVDMessage *msg)
{
	QVDConnection *conn;
	GError *err = NULL;
	struct imsd_stop_connection_req req;
	struct imsd_stop_connection_resp resp;
	gint *client;
	guint node, port;

	qvd_message_decode(msg, &req, imsd_stop_connection_req_ei, &err);
	if (err != NULL) {
		g_printerr("IMSD Stop Connection: failed to decode request: %s\n",
			   err->message);
		g_clear_error(&err);
		encode_error(msg, 1);
		goto err;
	}

	if (req.connection >= server->conns->len
	 || server->conns->pdata[req.connection] == NULL) {
		g_printerr("IMSD Stop Connection: unknown connection ID %u\n",
			   req.connection);
		encode_error(msg, 9);
		goto err;
	}

	// Register the client for indications.
	g_object_get(msg, "peer-node", &node, "peer-port", &port, NULL);
	client = g_malloc(sizeof(gint));
	*client = (node << 16) | port;
	g_hash_table_add(server->clients, client);

	conn = g_steal_pointer(&server->conns->pdata[req.connection]);
	qvd_connection_disconnect(conn);
	g_object_unref(conn);

	resp.res.err_status = 0;
	resp.res.err_code = 0;
	resp.unkfield_FA = 0xFA;
	resp.unkecho_01 = req.unkecho_01;

	qvd_message_respond(msg, &resp, imsd_stop_connection_resp_ei, &err);
	if (err != NULL) {
		g_printerr("IMSD Stop Connection: failed to send response: %s\n",
			   err->message);
		g_clear_error(&err);
	}

err:
	g_object_unref(msg);
}

static void process_message(QVDServer *server, QVDMessage *msg)
{
	guint msg_id;

	msg_id = qvd_message_get_msg_id(msg);

	if (msg_id == IMSD_START_CONNECTION)
		process_start_connection(server, msg);
	else if (msg_id == IMSD_STOP_CONNECTION)
		process_stop_connection(server, msg);
	else
		process_no_op(msg_id, msg);
}

static gboolean receive_message(GSocket *sock, GIOCondition cond, gpointer data)
{
	QVDServer *server = QVD_SERVER(data);
	QVDMessage *msg;
	struct sockaddr_qrtr peer = { .sq_family = AF_QIPCRTR, };
	struct qrtr_packet *req;
	gint *client;
	int fd, inlen;

	fd = g_socket_get_fd(sock);

	req = g_malloc(sizeof(*req));
	req->data_len = 65536;
	req->data = g_malloc(req->data_len);

	inlen = qrtr_recvfrom(fd, req->data, 65536, &peer.sq_node, &peer.sq_port);
	if (inlen < 0)
		return TRUE;

	qrtr_decode(req, req->data, inlen, &peer);

	if (req->type == QRTR_TYPE_DATA) {
		msg = qvd_message_new(g_object_ref(server->sock),
				      req,
				      peer.sq_node, peer.sq_port);

		process_message(server, msg);
	} else if (req->type == QRTR_TYPE_DEL_CLIENT) {
		client = g_malloc(sizeof(gint));
		*client = (req->node << 16) | req->port;
		g_hash_table_remove(server->clients, client);
		g_free(client);

		g_free(req->data);
		g_free(req);
	}

	return TRUE;
}

static void send_staged_message(gpointer k, gpointer v, gpointer data)
{
	QVDServer *server = QVD_SERVER(data);
	guint *addr = k;
	uint16_t node = *addr >> 16;
	uint16_t port = *addr & 0xFFFF;
	int fd;

	// The MSG_NOSIGNAL flag is not supported by the QRTR kernel driver.
	fd = g_socket_get_fd(server->sock);
	qrtr_sendto(fd, node, port, server->outpkt.data, server->outpkt.data_len);
}

static void connection_address_changed(QVDModem *modem,
				       QVDConnection *conn,
				       MMBearerIpFamily ip_type,
				       const gchar *addr,
				       gpointer data)
{
	QVDServer *server = QVD_SERVER(data);
	struct imsd_connection_changed_ind ind;
	guint32 conn_id, sub_id;
	guint8 local_id;

	g_assert(ip_type == MM_BEARER_IP_FAMILY_IPV4
	      || ip_type == MM_BEARER_IP_FAMILY_IPV6);

	qvd_connection_get_handles(conn, &conn_id, &sub_id, &local_id);

	server->outpkt.data = server->outbuf;
	server->outpkt.data_len = 65536;

	ind.res.err_status = 0;
	ind.connection = local_id;
	ind.orig_conn_id = conn_id;
	ind.subscription = sub_id;

	if (addr != NULL) {
		ind.res.err_code = 0;
		ind.ip_addr_valid = true;

		strncpy(ind.ip_addr.addr, addr, 255);
		ind.ip_addr.addr[255] = '\0';

		if (ip_type == MM_BEARER_IP_FAMILY_IPV4)
			ind.ip_addr.family = IMSD_AF_IPV4;
		else if (ip_type == MM_BEARER_IP_FAMILY_IPV6)
			ind.ip_addr.family = IMSD_AF_IPV6;
	} else {
		ind.res.err_code = 13;
		ind.ip_addr_valid = false;
	}

	qmi_encode_message(&server->outpkt,
			   QMI_INDICATION, IMSD_CONNECTION_CHANGED, server->ind_txn,
			   &ind, imsd_connection_changed_ind_ei);
	server->ind_txn++;

	g_hash_table_foreach(server->clients, send_staged_message, server);
}
