/*
 * unasta.c - implements unassociated sta processing and helper functions
 *
 * Copyright (C) 2025 IOPSYS Software Solutions AB. All rights reserved.
 *
 */

#include "unasta.h"

#include "agent.h"
#include "agent_map.h"
#include "agent_cmdu.h"
#include "agent_tlv.h"
#include "utils/debug.h"
#include "utils/utils.h"
#include "timeutils.h"

#include "wifi.h"

static int timestamp_is_unset(struct timespec *ts)
{
	return ts->tv_sec == 0 && ts->tv_nsec == 0;
}

void clear_unassocstalist_in_radio(struct wifi_radio_element *re)
{
	struct wifi_unassoc_sta_element *s = NULL, *tmp;

	list_for_each_entry_safe(s, tmp, &re->unassocstalist, list) {
		list_del(&s->list);
		free(s);
	}
	re->num_unassoc_sta = 0;
}

void clear_unassocstalist(struct agent *a)
{
	struct wifi_radio_element *re = NULL;

	list_for_each_entry(re, &a->radiolist, list)
		clear_unassocstalist_in_radio(re);
}

static int unassoc_sta_meas_requested(struct wifi_unassoc_sta_element *usta)
{
	return !timestamp_is_unset(&usta->last_request_t);
}

static int unassoc_sta_meas_valid(struct wifi_unassoc_sta_element *usta)
{
	if (timestamp_is_unset(&usta->meas.start_time)) {
		/* Measurement not started yet */
		return false;
	}

	if (!usta->meas.rssi) {
		/* RSSI is missing in measurement */
		agnt_dbg(LOG_STA, "%s: RSSI missing in measurement of STA ("MACFMT")\n",
		    __func__, MAC2STR(usta->macaddr));
		return false;
	}

	return true;
}

static int agent_get_monitor_rssi(struct wifi_unassoc_sta_element *usta)
{
	agnt_trace(LOG_STA, "agent: %s: --->\n", __func__);

	struct wifi_monsta monsta = {};
	int ret = 0;

	ret = wifi_get_monitor_sta(usta->mon_ifname, usta->macaddr, &monsta);
	if (!ret) {
		agnt_dbg(LOG_STA, "%s: RSSI measured for " MACFMT ": %d, time delta = %d seconds\n",
		    __func__, MAC2STR(usta->macaddr), monsta.rssi_avg, monsta.last_seen);
		usta->meas.rssi = monsta.rssi_avg;
		usta->meas.rcpi = rssi_to_rcpi(monsta.rssi_avg);
		usta->meas.last_seen_ms = monsta.last_seen * 1000; /* convert to ms */
	} else {
		agnt_warn(LOG_STA, "%s: failed to get RSSI for unassociated STA "MACFMT"\n",
			 __func__, MAC2STR(usta->macaddr));
		usta->meas.rssi = 0;
		usta->meas.rcpi = 255;
		usta->meas.last_seen_ms = 0;
	}

	return ret;
}

int radio_free_unassoc_sta(struct wifi_radio_element *re, uint8_t *sta)
{
	struct wifi_unassoc_sta_element *s = NULL, *tmp;

	list_for_each_entry_safe(s, tmp, &re->unassocstalist, list) {
		agnt_dbg(LOG_STA, "%s: Delete unassociated STA " MACFMT " record\n",
		    __func__, MAC2STR(sta));
		if (!memcmp(s->macaddr, sta, 6)) {
			list_del(&s->list);
			free(s);
			re->num_unassoc_sta--;
			return 0;
		}
	}

	return -1;
}

/* Remove unassociated STA monitoring on given radio */
static int agent_del_unassoc_sta_radio(struct wifi_radio_element *re, uint8_t *macaddr)
{
	struct wifi_unassoc_sta_element *usta;
	int ret = -1;

	if (list_empty(&re->unassocstalist) || re->num_unassoc_sta == 0)
		return -1;

	usta = radio_get_unassoc_sta(re, macaddr);
	if (!usta)
		return -1;

	/* Remove the STA from the list */
	if (!radio_free_unassoc_sta(re, macaddr)) {
		/* Call driver's 'del' api */
		ret = wifi_monitor_sta_del(usta->mon_ifname, macaddr);
	}

	return ret;
}

/* Remove unassociated STA monitoring on all radios */
int agent_del_unassoc_sta(struct agent *a, uint8_t *macaddr)
{
	struct wifi_radio_element *re = NULL;
	int num_deleted = 0;

	list_for_each_entry(re, &a->radiolist, list) {
		if (agent_del_unassoc_sta_radio(re, macaddr))
			continue;
		num_deleted++;
	}

	return num_deleted ? 0 : -1;
}

static struct wifi_unassoc_sta_element *unassoc_sta_get_oldest(struct wifi_radio_element *re)
{
	struct wifi_unassoc_sta_element *s = NULL, *oldest = NULL;
	struct timespec oldest_t = {0};

	if (list_empty(&re->unassocstalist) || re->num_unassoc_sta == 0)
		return NULL;

	list_for_each_entry(s, &re->unassocstalist, list) {
		struct timespec request_t = s->last_request_t;

		if (!unassoc_sta_meas_requested(s)) {
			/* Not requested yet, substitute with start_time */
			if (timestamp_is_unset(&s->meas.start_time))
				continue;
			request_t = s->meas.start_time;
		}

		if (timestamp_is_unset(&oldest_t) ||
		    timestamp_less_than(&request_t, &oldest_t)) {
			oldest = s;
			oldest_t = request_t;
		}
	}

	return oldest;
}

static struct wifi_unassoc_sta_element *unassoc_sta_list_add(uint8_t *macaddr,
			struct wifi_radio_element *re, uint8_t opclass, uint8_t channel)
{
	struct wifi_unassoc_sta_element *usta = NULL;
	struct netif_ap *ap;

	if (re->num_unassoc_sta >= MAX_UNASSOC_STA) {
		struct wifi_unassoc_sta_element *oldest = NULL;

		agnt_dbg(LOG_STA, "%s: max number of unassociated STAs reached\n", __func__);
		oldest = unassoc_sta_get_oldest(re);
		if (oldest) {
			agnt_dbg(LOG_STA, "%s: removing oldest requested unassociated STA "MACFMT"\n",
			    __func__, MAC2STR(oldest->macaddr));
			if (agent_del_unassoc_sta_radio(re, oldest->macaddr))
				return NULL;
		} else {
			agnt_err(LOG_STA, "%s: failed to add unassociated sta "MACFMT"\n",
			    __func__, MAC2STR(macaddr));
			return NULL;
		}
	}

	/* Measure over one interface per radio only */
	/* TODO: use interface with least associated STAs */
	ap = list_first_entry(&re->aplist, struct netif_ap, list);
	if (!ap) {
		agnt_err(LOG_STA, "%s: no AP found on radio %s\n", __func__, re->name);
		return NULL;
	}

	usta = calloc(1, sizeof(*usta));
	if (!usta) {
		agnt_err(LOG_STA, "%s: failed to allocate unassoc sta "MACFMT"\n",
		    __func__, MAC2STR(macaddr));
		return NULL;
	}

	agnt_dbg(LOG_STA, "%s: allocated unassociated sta " MACFMT "\n",
	    __func__, MAC2STR(macaddr));

	memcpy(usta->macaddr, macaddr, sizeof(usta->macaddr));
	usta->meas.opclass = opclass;
	usta->meas.channel = channel;
	strncpy(usta->mon_ifname, ap->ifname, sizeof(usta->mon_ifname));
	list_add_tail(&usta->list, &re->unassocstalist);
	re->num_unassoc_sta++;

	return usta;
}

struct wifi_unassoc_sta_element *radio_get_unassoc_sta(struct wifi_radio_element *re, uint8_t *sta)
{
	struct wifi_unassoc_sta_element *s = NULL;

	if (list_empty(&re->unassocstalist) || re->num_unassoc_sta == 0)
		return NULL;

	list_for_each_entry(s, &re->unassocstalist, list) {
		if (!memcmp(s->macaddr, sta, 6))
			return s;
	}

	return NULL;
}

#define MAX_UNASSOC_STA_METRICS 32
int unassoc_sta_send_link_metrics_response(struct wifi_radio_element *re)
{
	struct wifi_unassoc_sta_element *usta = NULL;
	struct wifi_unassoc_sta_element metrics[MAX_UNASSOC_STA_METRICS] = {};
	uint8_t num_metrics = 0;
	uint8_t remaining = 0;
	uint8_t opclass = re->current_opclass;

	agnt_dbg(LOG_STA, "agent: %s: --->\n", __func__);

	if (!re) {
		agnt_dbg(LOG_STA, "%s: radio element not found\n", __func__);
		return -1;
	}

	if (list_empty(&re->unassocstalist) || re->num_unassoc_sta == 0) {
		agnt_dbg(LOG_STA, "%s: no unassociated STAs under measurement\n", __func__);
		return -1;
	}

	list_for_each_entry(usta, &re->unassocstalist, list) {
		bool reset = false;
		int ret = 0;

		if (!unassoc_sta_meas_requested(usta))
			/* Not requested yet */
			continue;

		if (usta->is_monitored != true)
			/* Not monitored atm */
			continue;

		opclass = usta->meas.opclass;

		/* Get most recent RSSI from driver */
		ret = agent_get_monitor_rssi(usta);
		if (ret || !usta->meas.rssi) {
			/* No RSSI: reenable monitoring in driver */
			if (!wifi_monitor_sta_add(usta->mon_ifname, usta->macaddr)) {
				agnt_dbg(LOG_STA, "%s: restarted monitoring of unassociated STA " MACFMT "\n",
					__func__, MAC2STR(usta->macaddr));
				timestamp_update(&usta->meas.start_time);
			} else {
				agnt_warn(LOG_STA, "%s: failed to restart monitoring of unassoc sta "MACFMT"!\n",
					 __func__, MAC2STR(usta->macaddr));
				timestamp_reset(&usta->meas.start_time);
				usta->is_monitored = false;
			}
		}

		/* Check if currently stored measurement is valid */
		if (unassoc_sta_meas_valid(usta)) {
			memcpy(&metrics[num_metrics], usta, sizeof(*usta));
			num_metrics += 1;
			reset = true;
		} else {
			/* Measurement not available yet, retry */
			agnt_dbg(LOG_STA, "%s: measurement for "MACFMT" unavailable\n",
				 __func__, MAC2STR(usta->macaddr));
			if (usta->meas.num_tries >= UNASSOC_STA_MEAS_MAXTRIES) {
				agnt_dbg(LOG_STA, "%s: exceeded max number of tries to get metrics for " MACFMT "\n",
				    __func__, MAC2STR(usta->macaddr));
				reset = true;
			} else {
				remaining++;
				usta->meas.num_tries++;
			}
		}

		if (reset) {
			/* Added to send list, stop monitoring and clean measurement data */
#ifndef UNASSOC_STA_CONT_MONITOR
			wifi_monitor_sta_del(usta->mon_ifname, usta->macaddr);
			usta->is_monitored = false;
			memset(&usta->meas, 0, sizeof(struct wifi_sta_measurement));
#endif
			timestamp_reset(&usta->last_request_t);
		}
	}

	if (num_metrics > MAX_UNASSOC_STA_METRICS) {
		agnt_dbg(LOG_STA, "%s: too many measurements (%d) to send\n",
		    __func__, num_metrics);
		return -1;
	}

	/* TODO:
	 * If the Multi-AP Agent cannot obtain any RCPI measurements on all of the STAs specified
	 * in the Unassociated STA Link Metrics Query message after some implementation-specific
	 * timeout, the Multi-AP Agent shall set the number of STAs included field in the Unassociated
	 * STA Link Metrics Response message to zero.
	 */
	if (num_metrics > 0 || !remaining) {
		/* TODO: offchannel */
		agnt_dbg(LOG_STA, "%s: sending measurement from %d stations (opclass = %d)\n",
		    __func__, num_metrics, opclass);
		send_unassoc_sta_link_metrics_response(re->agent, num_metrics, metrics, opclass);
	}

	if (remaining) {
		agnt_dbg(LOG_STA, "%s: awaiting measurement for %d client(s) - retry in %dsec\n",
		    __func__, remaining, UNASSOC_STA_MEAS_RETRY_TIME / 1000);
		timer_set(&re->unassoc_sta_meas_timer, UNASSOC_STA_MEAS_RETRY_TIME);
	}

	return 0;
}

static bool unassoc_sta_update_params(struct wifi_unassoc_sta_element *usta,
		uint8_t opclass, uint8_t channel)
{
	bool updated = false;

	if (usta->meas.opclass != opclass) {
		agnt_dbg(LOG_STA, "%s: STA ("MACFMT"): opc(meas)=%d, opc(req)=%d\n",
		    __func__, MAC2STR(usta->macaddr),
		    usta->meas.opclass, opclass);
		usta->meas.opclass = opclass;
		updated = true;
	}

	if (usta->meas.channel != channel) {
		agnt_dbg(LOG_STA, "%s: STA ("MACFMT"): ch(meas)=%d, ch(req)=%d\n",
		    __func__, MAC2STR(usta->macaddr),
		    usta->meas.channel, channel);
		usta->meas.channel = channel;
		updated = true;
	}

	return updated;
}

struct wifi_unassoc_sta_element *radio_monitor_unassoc_sta(struct wifi_radio_element *re,
			uint8_t *sta_mac, uint8_t opclass, uint8_t channel, bool *param_change)
{
	struct wifi_unassoc_sta_element *usta = NULL;

	*param_change = true;

	/* Check if STA is already on the list */
	usta = radio_get_unassoc_sta(re, sta_mac);
	if (!usta) {
		/* Not on the list, add */
		usta = unassoc_sta_list_add(sta_mac, re, opclass, channel);
		if (!usta) {
			agnt_warn(LOG_STA, "%s: failed to add unassoc sta "MACFMT"\n",
				 __func__, MAC2STR(sta_mac));
			return NULL;
		}
		agnt_dbg(LOG_STA, "%s: added unassociated STA " MACFMT " to the list\n",
			__func__, MAC2STR(sta_mac));
	} else {
		/* Already on the list, updated params(?) */
		if (usta->is_monitored) {
			/* Already monitored */
			agnt_dbg(LOG_STA, "%s: unassociated STA " MACFMT " already monitored\n",
			    __func__, MAC2STR(sta_mac));
		}
		/* New request, reset tries */
		usta->meas.num_tries = 0;

		if (!unassoc_sta_update_params(usta, opclass, channel)) {
			/* Params not changed */
			*param_change = false;
		}
	}

	if (usta->is_monitored && *param_change == false) {
		/* Already monitored & request for same channel */
		goto out;
	}

	/* new params and/or sta not monitored yet, setup monitor */
	if (wifi_monitor_sta_add(usta->mon_ifname, sta_mac)) {
		agnt_warn(LOG_STA, "%s: monitoring of unassoc sta "MACFMT" failed!\n",
			 __func__, MAC2STR(sta_mac));
		timestamp_reset(&usta->meas.start_time);
		usta->is_monitored = false;
		goto out;
	}

	agnt_dbg(LOG_STA, "%s: (re)started monitoring of unassociated STA " MACFMT " on %s\n",
		__func__, MAC2STR(sta_mac), usta->mon_ifname);

	timestamp_update(&usta->meas.start_time);
	usta->is_monitored = true;

out:
	timestamp_update(&usta->last_request_t);

	return usta;
}

static int unassoc_sta_stop_radio_monitoring(struct wifi_radio_element *re)
{
	struct wifi_unassoc_sta_element *s = NULL;
	int num = 0;

	agnt_dbg(LOG_STA, "%s: stop monitoring of all clients on radio " MACFMT "\n",
	    __func__, MAC2STR(re->macaddr));

	if (list_empty(&re->unassocstalist) || re->num_unassoc_sta == 0)
		return -1;

	list_for_each_entry(s, &re->unassocstalist, list) {
		if (s->is_monitored) {
			if (!wifi_monitor_sta_del(s->mon_ifname, s->macaddr)) {
				s->is_monitored = false;
				num++;
			}
		}
	}

	clear_unassocstalist_in_radio(re);

	agnt_dbg(LOG_STA, "%s: stopped monitoring of %d clients\n", __func__, num);

	return 0;
}

int agent_process_unassoc_sta_lm_query_tlv(struct agent *a,
		struct tlv_unassoc_sta_link_metrics_query *query,
		struct cmdu_buff *cmdu)
{
	agnt_trace(LOG_STA, "agent: %s: --->\n", __func__);

	enum wifi_band band = BAND_UNKNOWN;
	struct wifi_radio_element *re;
	struct wifi_radio_opclass_entry *opc_entry;
	int num_sta_monitor = 0;
	int num_sta_new_or_updated = 0;
	int i;

	agnt_dbg(LOG_STA, "%s: query opc = %d\n", __func__, query->opclass);

	band = wifi_opclass_get_band(query->opclass);
	if (band == BAND_UNKNOWN) {
		agnt_dbg(LOG_STA, "%s: invalid opclass %d\n", __func__, query->opclass);
		return -1;
	}

	re = agent_get_radio_by_band(a, band);
	if (!re) {
		agnt_dbg(LOG_STA, "%s: radio not found for band %d\n", __func__, band);
		return -1;
	}

	opc_entry = wifi_opclass_find_entry(&re->opclass, query->opclass);
	if (!opc_entry) {
		agnt_dbg(LOG_STA, "%s: opclass %d not supported\n", __func__, query->opclass);
		return -1;
	}

	for (i = 0; i < query->num_channel; i++) {
		int j;
		struct wifi_radio_opclass_channel *chan;

		chan = wifi_opclass_find_ctrl_channel(opc_entry, query->ch[i].channel);
		if (!chan) {
			agnt_dbg(LOG_STA, "%s: channel %d not supported for opclass %d\n",
				__func__, query->ch[i].channel, query->opclass);
			continue;
		}

		if (a->cfg.off_channel_monitor == false &&
			re->current_channel != query->ch[i].channel) {
			agnt_dbg(LOG_STA, "%s channel[%d]=%d differs from current (%d)\n",
			    __func__, i, query->ch[i].channel, re->current_channel);
			continue;
		}

		/* On channel */

		if (query->ch[i].num_sta == 0) {
			/* Stop any monitoring on this radio */
			agnt_dbg(LOG_STA, "%s: num_sta = 0 for channel %d, stop monitring radio " MACFMT "\n",
			    __func__, query->ch[i].channel, MAC2STR(re->macaddr));
			unassoc_sta_stop_radio_monitoring(re);
			timer_del(&re->unassoc_sta_meas_timer);
			break;
		}

		for (j = 0; j < query->ch[i].num_sta; j++) {
			struct wifi_unassoc_sta_element *usta = NULL;
			bool param_change = false;

			if (agent_get_ap_with_sta(a, query->ch[i].sta[j].macaddr)) {
				/* Do not monitor associated STAs */
				agnt_dbg(LOG_STA, "%s skip monitoring of associated sta " MACFMT "\n",
				    __func__, MAC2STR(query->ch[i].sta[j].macaddr));
				continue;
			}

			usta = radio_monitor_unassoc_sta(re, query->ch[i].sta[j].macaddr,
					query->opclass, query->ch[i].channel, &param_change);
			if (!usta)
				continue;

			if (usta->is_monitored)
				num_sta_monitor++;

			if (param_change)
				num_sta_new_or_updated++;
		}
	}

	if (num_sta_monitor) {
		agnt_dbg(LOG_STA, "%s: %d STAs monitored, %d new/updated\n",
		    __func__, num_sta_monitor, num_sta_new_or_updated);

		/* Do not decrease timer if already waiting for retry on given radio */
		if (!timer_pending(&re->unassoc_sta_meas_timer)) {
			agnt_dbg(LOG_STA, "%s: getting measurements in 1 sec\n", __func__);
			timer_set(&re->unassoc_sta_meas_timer, 1);
		}
	}

	return 0;
}
