/*
 * rate.c - Controller initiated STA steering based on est-rate achievable from target-BSS.
 *
 * Copyright (C) 2025 Genexis Sweden AB.
 *
 * See LICENSE file for license related information.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/time.h>
#include <time.h>
#include <libubox/list.h>
#include <libubox/blobmsg.h>
#include <uci.h>
#include <easy/easy.h>
#include <wifiutils.h>

#include <cmdu.h>
#include <1905_tlvs.h>
#include <easymesh.h>

#include <map-controller/wifi_dataelements.h>
#include <map-controller/wifi_opclass.h>
#include <map-controller/cntlr_commands.h>
#include <map-controller/cntlr_apis.h>
#include <map-controller/steer_module.h>
#include <map-controller/timer.h>
#include <map-controller/timer_impl.h>
#include <map-controller/utils/debug.h>

#ifndef MAC_ADDR_HASH
#define MAC_ADDR_HASH(a)	(a[0] ^ a[1] ^ a[2] ^ a[3] ^ a[4] ^ a[5])
#endif

#ifndef blobmsg_add_macaddr
#define blobmsg_add_macaddr(b, f, v)	\
do {					\
	char _vstr[18] = {0};		\
	hwaddr_ntoa(v, _vstr);		\
	blobmsg_add_string(b, f, _vstr);\
} while (0)
#endif

#ifndef cntlr_dbg
#define cntlr_dbg(feat, fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)  /* Flawfinder: ignore */
#endif

#define STA_MAX_NUM		128
#define sta_table_hash(m)	MAC_ADDR_HASH(m) & (STA_MAX_NUM - 1)
#define STA_INTERVAL_MAX	1000000

enum {
	STA_TIMER_NOP,
	STA_TIMER_ADD,
	STA_TIMER_DEL,
};

#define RATE_PLUGIN_NAME             "rate"

#define DEFAULT_RATE_THRESHOLD       40        /* % of maxrate */
#define DEFAULT_RATE_BOOST           30        /* in MBps */

#define DEFAULT_RATE_LOW_CNT         3
#define DEFAULT_RATE_HIGH_CNT        4

#define USTA_METRICS_AGEOUT          10        /* in secs */
#define USTA_RESPONSE_TIMEOUT        20        /* in secs */


#define DEFAULT_STEER_INT            180       /* 180s */

#define NUM_RATE_SAMPLES             10

#define MAX_SNR                      70        /* arbitrary high value */
#define MIN_ANPI                     40        /* = -90dBm */

enum rate_trigger_type {
	RATE_TRIGGER_NONE = 0,
	RATE_TRIGGER_LOW = -1,
	RATE_TRIGGER_HIGH = 1,
};

enum rate_sta_state {
	RATE_STA_CONNECTED,
	RATE_STA_NOP,
	RATE_STA_USTA_CHECK,
	RATE_STA_NOSTEER,
	RATE_STA_DISCONNECTED,
};

/* per-sta steer context data */
struct rate_steer_sta_data {
	uint8_t macaddr[6];
	time_t t0, tprev;
	struct {
		uint32_t dl, ul;
		uint32_t est_dl;
		uint32_t tx, rx;
		uint8_t dt;
	} rate[NUM_RATE_SAMPLES];
	uint8_t t;
	uint8_t rate_low_trigger_cnt;
	uint8_t rate_high_trigger_cnt;
	uint8_t boost;

	enum rate_sta_state state;

	uint32_t maxrate;	/* from caps */
	enum wifi_bw bw;
	uint8_t nss;

	uint32_t est_rate;
	uint8_t rate_threshold;
	time_t lowrate_tstart;
	time_t lowrate_tend;
	bool lowrate_epoch;

	time_t nop_tstart;
	time_t usta_query_time;

	bool usta_query_sent;
	bool usta_metrics_processed;

	uint8_t connected;
	bool cleanup;

	uint32_t interval;	/* in ms */
	struct timeval tm;

	/* num attempts in a connection session */
	uint32_t num_steer_attempt;

	bool steer_attempt_pending;

	struct hlist_node hlist;
	void *priv;	/* struct rate_steer_control */
	void *sta;	/* struct steer_sta */
};

/* rate plugin private config options.
 *
 * config sta-steer 'rate'
 *	option enabled '1              # rate plugin is enabled or not
 *	option steer_int '180'         # steer interval in seconds
 *	option threshold '40'          # percentage of maxrate
 *	option boost '20'              # target-AP should be better by this amount (in Mbps)
 *	option bandsteer '0            # steer to different band
 *	option max_steer_attempt '-1'  # max steer attempts in a connection session
 *
 */

/* rate private context */
struct rate_steer_control {
	bool enabled;
	uint8_t rate_threshold;
	uint8_t boost;
	bool bandsteer;
	int max_btm_attempt;
	uint32_t steer_int;
	uint32_t sta_count;
	struct hlist_head sta_table[STA_MAX_NUM];

	atimer_t t;
	struct timeval next_tmo;
	void *self;	/* our steer_control struct */
};

static int rate_steer_init(void **priv);
static int rate_steer_configure(void *priv, void *config_section);
static int rate_steer_exit(void *priv);

static void rate_reset_steer_sta_data(struct rate_steer_sta_data *st);

//TODO: remove; get from libwifiutils.so
enum wifi_bw wifi_bw_to_enum_bw(uint16_t bw)
{
	switch (bw) {
	case 40:
		return BW40;
	case 80:
		return BW80;
	case 160:
		return BW160;
	case 8080:
		return BW8080;
	case 320:
		return BW320;
	default:
		break;
	}

	return BW20;
}

static const char *rate_sta_state_string(enum rate_sta_state s)
{
	switch (s) {
	case RATE_STA_CONNECTED:
		return "Connected";
	case RATE_STA_DISCONNECTED:
		return "Disconnected";
	case RATE_STA_USTA_CHECK:
		return "Unassoc-STA metrics check";
	case RATE_STA_NOP:
		return "Idle";
	case RATE_STA_NOSTEER:
		return "No Steer";
	default:
		break;
	}

	return "Unknown";
}

static int timeradd_msecs(struct timeval *a, unsigned long msecs,
			  struct timeval *res)
{
	if (res) {
		struct timeval t = { 0 };

		if (msecs > 1000) {
			t.tv_sec += msecs / 1000;
			t.tv_usec = (msecs % 1000) * 1000;
		} else {
			t.tv_usec = msecs * 1000;
		}

		timeradd(a, &t, res);
		return 0;
	}

	return -1;
}

static int getcurrtime(struct timeval *out)
{
	struct timespec nowts = { 0 };
	struct timeval now = { 0 };
	int ret;

	ret = clock_gettime(CLOCK_MONOTONIC_RAW, &nowts);
	if (!ret) {
		now.tv_sec = nowts.tv_sec;
		now.tv_usec = nowts.tv_nsec / 1000;
	} else {
		ret = gettimeofday(&now, NULL);
	}

	now.tv_usec = (now.tv_usec / 1000) * 1000;
	out->tv_sec = now.tv_sec;
	out->tv_usec = now.tv_usec;

	return ret;
}

static int is_meas_old(time_t now, time_t t, uint32_t age)
{
	return (difftime(now, t) > age) ? 1 : 0;
}

int rate_sta_table_entry_process(struct rate_steer_control *sc,
				 struct rate_steer_sta_data *st)
{
	struct wifi_sta_element *sta;
	int action = STA_TIMER_NOP;
	time_t now = time(NULL);
	struct steer_sta *s;
	void *cntlr;

	cntlr = ((struct steer_control *)(sc->self))->controller;
	sta = cntlr_get_sta_element(cntlr, st->macaddr);
	if (!sta)
		return STA_TIMER_NOP;

	/* if action = STA_TIMER_ADD, set 'st->interval' as desired.
	 * Otherwise, the existing 'st->interval' will be used.
	 */

	s = cntlr_get_sta_steer_data(cntlr, st->macaddr);
	if (!s || s != st->sta) {
		cntlr_dbg(LOG_STEER, "%s: "MACFMT" updating steer_sta = %p\n",
				__func__, MAC2STR(st->macaddr), s);
		st->sta = s;
	}

	if (st->cleanup) {
		cntlr_dbg(LOG_STEER, "STA " MACFMT ": Reset steer data\n",
			  MAC2STR(st->macaddr));

		rate_reset_steer_sta_data(st);
		return STA_TIMER_NOP;
	}

	switch (st->state) {
	case RATE_STA_USTA_CHECK:
		if (st->usta_query_sent && difftime(now, st->usta_query_time) >= USTA_RESPONSE_TIMEOUT) {
			st->state = RATE_STA_NOP;
			cntlr_dbg(LOG_STEER,
				  "rate_sta_timer_cb: STA " MACFMT ": Unassoc-STA meas not available yet\n",
				  MAC2STR(st->macaddr));

			/* enter 'steer_int' nop interval */
			time(&st->nop_tstart);
			st->interval = sc->steer_int * 1000;
			action = STA_TIMER_ADD;
		}
		break;
	case RATE_STA_NOP:
		if (difftime(now, st->nop_tstart) >= sc->steer_int) {
			st->state = RATE_STA_CONNECTED;
			st->nop_tstart = 0;
		}
		break;
	default:
		break;
	}

	return action;
}

void rate_sta_table_flush(struct hlist_head *table)
{
	struct rate_steer_sta_data *e = NULL;
	struct hlist_node *tmp;
	int i;

	for (i = 0; i < STA_MAX_NUM; i++) {
		if (hlist_empty(&table[i]))
			continue;

		hlist_for_each_entry_safe(e, tmp, &table[i], hlist) {
			hlist_del(&e->hlist, &table[i]);
			e->priv = NULL;
			e->sta = NULL;
			free(e);
		}
	}
}

void rate_sta_timer_set(struct rate_steer_control *sc,
			struct rate_steer_sta_data *st,
			uint32_t interval)
{
	struct timeval now = { 0 };

	getcurrtime(&now);
	st->interval = interval;
	timeradd_msecs(&now, st->interval, &st->tm);
	st->tm.tv_usec = (st->tm.tv_usec / 1000) * 1000;

	if (timer_pending(&sc->t)) {
		if (timercmp(&sc->next_tmo, &st->tm, >)) {
			sc->next_tmo.tv_sec = st->tm.tv_sec;
			sc->next_tmo.tv_usec = st->tm.tv_usec;
			timer_set(&sc->t, st->interval);
		}
	} else {
		sc->next_tmo.tv_sec = st->tm.tv_sec;
		sc->next_tmo.tv_usec = st->tm.tv_usec;
		timer_set(&sc->t, st->interval);
	}
}

void rate_sta_timer_reset(struct rate_steer_control *sc,
			  struct rate_steer_sta_data *st)
{
	rate_sta_timer_set(sc, st, STA_INTERVAL_MAX * 1000);
}

#if 0
void rate_sta_timer_del(struct rate_steer_control *sc,
			struct rate_steer_sta_data *st)
{
	struct timeval now = { 0 };

	getcurrtime(&now);
	st->interval = STA_INTERVAL_MAX;
	timeradd_msecs(&now, st->interval, &st->tm);
	st->tm.tv_usec = (st->tm.tv_usec / 1000) * 1000;
}
#endif

static void rate_sta_timer_cb(struct rate_steer_control *priv,
			      struct hlist_head *head,
			      struct timeval *min_next_tmo)
{
	struct rate_steer_sta_data *e = NULL;
	struct timeval now = { 0 };
	struct hlist_node *tmp;


	getcurrtime(&now);
	hlist_for_each_entry_safe(e, tmp, head, hlist) {
		int action = STA_TIMER_ADD;
		struct timeval new_next_tmo = { 0 };

		if (!timercmp(&e->tm, &now, >)) {
			/* cntlr_dbg(LOG_STEER,
				  "%s: process timer for STA " MACFMT "\n",
				  __func__, MAC2STR(e->macaddr)); */

			action = rate_sta_table_entry_process(priv, e);
			if (action == STA_TIMER_ADD)
				timeradd_msecs(&now, e->interval, &e->tm);
		}

		switch (action) {
		case STA_TIMER_DEL:
			cntlr_dbg(LOG_STEER, "Remove timer for " MACFMT "\n", MAC2STR(e->macaddr));
			//rate_sta_timer_reset(priv, e);	//TODO: remove
			break;
		case STA_TIMER_ADD:
			timersub(&e->tm, &now, &new_next_tmo);
			if (!timercmp(min_next_tmo, &new_next_tmo, <)) {
				min_next_tmo->tv_sec = new_next_tmo.tv_sec;
				min_next_tmo->tv_usec = new_next_tmo.tv_usec;
				/* cntlr_dbg(LOG_STEER,
					  "Adjusted next-tmo = (%jd.%jd)\n",
					  (uintmax_t) min_next_tmo->tv_sec,
					  (uintmax_t) min_next_tmo->tv_usec); */
			}
			break;
		}
	}
}

void rate_timer_cb(atimer_t *t)
{
	struct rate_steer_control *p = container_of(t, struct rate_steer_control, t);
	struct timeval min_next_tmo = { .tv_sec = STA_INTERVAL_MAX };
	struct timeval now;
	int remain_cnt = 0;
	int i;

	getcurrtime(&now);
	for (i = 0; i < STA_MAX_NUM; i++) {
		if (hlist_empty(&p->sta_table[i]))
			continue;

		rate_sta_timer_cb(p, &p->sta_table[i], &min_next_tmo);
	}

	remain_cnt = p->sta_count;
	timeradd(&now, &min_next_tmo, &p->next_tmo);

	/*
	cntlr_dbg(LOG_STEER,
		  "Next timer: when = %jd.%jd, after = %jd.%jd, sta-cnt = %d\n",
		  (uintmax_t) p->next_tmo.tv_sec, (uintmax_t) p->next_tmo.tv_usec,
		  (uintmax_t) min_next_tmo.tv_sec, (uintmax_t) min_next_tmo.tv_usec,
		  remain_cnt);
	*/

	if (remain_cnt) {
		uint32_t tmo_msecs =
			min_next_tmo.tv_sec * 1000 + min_next_tmo.tv_usec / 1000;

		if (tmo_msecs > 0) {
			/*
			cntlr_dbg(LOG_STEER,
				  "Next timer: set after %u ms, sta-cnt = %d\n",
				  tmo_msecs, remain_cnt);
			*/

			timer_set(&p->t, tmo_msecs);
		}
	}
}

bool is_bandsteer_allowed(void *priv)
{
	struct rate_steer_control *sc = (struct rate_steer_control *)priv;

	cntlr_dbg(LOG_STEER, "%s: bandsteer is %sallowed\n", __func__,
		  sc->bandsteer ? "" : "not ");

	return sc->bandsteer ? true : false;
}

struct rate_steer_sta_data *rate_lookup_sta(struct rate_steer_control *ctl,
					    uint8_t *macaddr)
{
	struct rate_steer_sta_data *st = NULL;
	int idx = sta_table_hash(macaddr);

	hlist_for_each_entry(st, &ctl->sta_table[idx], hlist) {
		if (!memcmp(st->macaddr, macaddr, 6)) {
			return st;
		}
	}

	return NULL;
}

struct steer_sta_target_bss *nbrlist_lookup_bssid(struct steer_sta *s, uint8_t *bssid)
{
	for (int i = 0; i < s->num_nbr; i++) {
		if (!memcmp(s->nbrlist[i].bssid, bssid, 6))
			return &s->nbrlist[i];
	}

	return NULL;
}

void print_nbrlist(struct steer_sta *s)
{
	for (int i = 0; i < s->num_nbr; i++) {
		cntlr_dbg(LOG_STEER, "NBR " MACFMT ": Channel = %3d Opclass = %3d\n",
			  MAC2STR(s->nbrlist[i].bssid),
			  s->nbrlist[i].channel,
			  s->nbrlist[i].opclass);
	}
}

int rate_query_unassoc_sta_metrics(struct rate_steer_control *sc, struct rate_steer_sta_data *st)
{
	void *cntlr = ((struct steer_control *)(sc->self))->controller;
	struct steer_sta *s = ((struct steer_sta *)st->sta);
	int ret = 0;

	cntlr_dbg(LOG_STEER, "%s: Enter\n", __func__);

	if (s->num_nbr == 0)
		return -1;

	/* Request unassoc-sta-metrics reports from all neighbor fBSSs */
	for (int i = 0; i < s->num_nbr; i++) {
		struct steer_sta_target_bss *nbr = &s->nbrlist[i];
		struct blob_buf bi = { 0 };
		struct blob_buf bo = { 0 };
		void *arr;


		/* Skip querying STA's current Agent as it will return error */
		if (!memcmp(s->bss.agent, nbr->agent, 6))
			continue;

		/* also skip querying for different band if bandsteer disabled */
		if (!sc->bandsteer &&
		    wifi_opclass_get_band(nbr->opclass) != wifi_opclass_get_band(s->bss.opclass)) {
			continue;
		}

		blob_buf_init(&bi, 0);
		blob_buf_init(&bo, 0);

		blobmsg_add_macaddr(&bi, "agent", nbr->agent);

		//if (wifi_opclass_get_bw(nbr->opclass) < wifi_opclass_get_bw(s->bss->opclass))

		blobmsg_add_u32(&bi, "opclass", s->bss.opclass);
		blobmsg_add_u32(&bi, "channel", s->bss.channel);
		arr = blobmsg_open_array(&bi, "stalist");
		blobmsg_add_macaddr(&bi, "", s->macaddr);
		blobmsg_close_array(&bi, arr);

		cntlr_dbg(LOG_STEER, "%s: STA " MACFMT ": Query Unassoc STA metrics - Opclass = %u, Ch = %u ========>\n",
			  __func__, MAC2STR(s->macaddr), s->bss.opclass, s->bss.channel);

		ret = COMMAND(query_unassoc_sta_metrics)(cntlr, bi.head, &bo);
		if (!ret) {
			struct blob_attr *tb[2];
			const struct blobmsg_policy rattr[2] = {
				[0] = { .name = "mid", .type = BLOBMSG_TYPE_INT32 },
				[1] = { .name = "status", .type = BLOBMSG_TYPE_STRING }
			};

			blobmsg_parse(rattr, 2, tb, blob_data(bo.head), blob_len(bo.head));

			if (tb[0]) {
				uint16_t req_mid = blobmsg_get_u32(tb[0]);

				cntlr_dbg(LOG_STEER,
					  "%s: STA " MACFMT ": Request Unassoc-STA metrics from " MACFMT " cmdu mid = %hu\n",
					  __func__, MAC2STR(s->macaddr),
					  MAC2STR(nbr->agent), req_mid);

				time(&st->usta_query_time);
				st->usta_query_sent = true;
				st->usta_metrics_processed = 0;
			}
		} else {
			cntlr_dbg(LOG_STEER,
				  "%s: failed to request Unassoc-STA metrics\n", __func__);
		}

		blob_buf_free(&bi);
		blob_buf_free(&bo);
	}

	cntlr_dbg(LOG_STEER, "%s: Exit\n", __func__);
	return ret;
}

void rate_reset_steer_sta_data(struct rate_steer_sta_data *st)
{
	struct steer_sta *sta = (struct steer_sta *)st->sta;

	st->t = 0;
	memset(st->rate, 0, sizeof(st->rate));
	st->tprev = 0;

	if (sta)
		st->connected = sta->bss.connected;

	st->state = st->connected ? RATE_STA_CONNECTED : RATE_STA_DISCONNECTED;
	st->maxrate = 0;
	st->est_rate = 0;
	st->nss = 0;
	st->bw = 0;
	st->usta_query_sent = false;
	st->usta_metrics_processed = false;
	st->lowrate_epoch = 0;
	st->cleanup = false;
	st->steer_attempt_pending = false;
	st->num_steer_attempt = 0;
}

void rate_print_steer_sta_data(struct rate_steer_sta_data *st)
{
#define NUM_USTA_METRICS	16
#define NUM_BCN_METRICS		16
	cntlr_dbg(LOG_STEER, "STA = " MACFMT "\n", MAC2STR(st->macaddr));
	cntlr_dbg(LOG_STEER, "BSSID = " MACFMT "\n", MAC2STR(((struct steer_sta *)st->sta)->bss.bssid));
	cntlr_dbg(LOG_STEER, "state = %s\n", rate_sta_state_string(st->state));
	cntlr_dbg(LOG_STEER, "lowrate = %d\n", st->lowrate_epoch);
	cntlr_dbg(LOG_STEER, "max-rate = %d\n", st->maxrate);
	cntlr_dbg(LOG_STEER, "est-current-rate = %d\n", st->est_rate);
	cntlr_dbg(LOG_STEER, "usta-query-sent = %d\n", st->usta_query_sent);
	cntlr_dbg(LOG_STEER, "usta-metrics-processed = %d\n", st->usta_metrics_processed);
	cntlr_dbg(LOG_STEER, "config: rate-threshold = %u%% (%uM)\n", st->rate_threshold, (st->maxrate * st->rate_threshold) / 100);
	cntlr_dbg(LOG_STEER, "config: rate-boost = %uM\n", st->boost);
	cntlr_dbg(LOG_STEER, "config: rate-low-trigger-cnt = %u\n", st->rate_low_trigger_cnt);
	cntlr_dbg(LOG_STEER, "config: rate-high-trigger-cnt = %u\n", st->rate_high_trigger_cnt);

	if (st->t != 0) {
		char rbuf[NUM_RATE_SAMPLES * 10 + 64] = {0};
		char buf[2048] = {0};
		int comma = 0;
		int start = 0;
		int end = 0;

		end = st->t < NUM_RATE_SAMPLES ? st->t : st->t % NUM_RATE_SAMPLES;
		if (st->t >= NUM_RATE_SAMPLES)
			start = st->t % NUM_RATE_SAMPLES;

		snprintf(buf, sizeof(buf) - 1, "rate = [");

		do {
			snprintf(rbuf + strlen(rbuf),
				 sizeof(rbuf) - 1 - strlen(rbuf), "(%u | %u,%u | %u,%u),",
				 st->rate[start].dt, st->rate[start].dl, st->rate[start].ul,
				 st->rate[start].tx, st->rate[start].rx);

			start = (start + 1) % NUM_RATE_SAMPLES;
			comma = 1;
		} while (start != end);

		snprintf(buf + strlen(buf), sizeof(buf) - 1 - strlen(buf), "%s", rbuf);
		snprintf(buf + strlen(buf) -comma , sizeof(buf) - 1 - strlen(buf), "%s", "]");

		cntlr_dbg(LOG_STEER, "%s\n", buf);
	}

	if (list_empty(((struct steer_sta *)st->sta)->unassoc_metriclist)) {
		cntlr_dbg(LOG_STEER, "usta-metrics-list = []\n");
	} else {
		char ubuf[25 * NUM_USTA_METRICS] = {0};
		struct unassoc_sta_metrics *u = NULL;
		char buf[1024] = {0};
		int comma = 1;
		int cnt = 0;

		snprintf(buf, sizeof(buf) - 1, "usta-metrics-list = [");
		list_for_each_entry(u, ((struct steer_sta *)st->sta)->unassoc_metriclist, list) {
			snprintf(ubuf + strlen(ubuf),
				 sizeof(ubuf) - 1 - strlen(ubuf), "("MACFMT",%u),",
				 MAC2STR(u->bssid), u->ul_rcpi);

			cnt++;
			if (cnt >= NUM_USTA_METRICS)
				break;
		}
		snprintf(buf + strlen(buf), sizeof(buf) - 1 - strlen(buf), "%s", ubuf);
		snprintf(buf + strlen(buf) - comma , sizeof(buf) - 1 - strlen(buf), "%s", "]");
		cntlr_dbg(LOG_STEER, "%s\n", buf);
	}
}

void rate_dump_sta_table(void *priv, uint8_t *macaddr)
{
	struct rate_steer_control *ctl = (struct rate_steer_control *)priv;
	struct rate_steer_sta_data *st = NULL;
	int i;

	cntlr_dbg(LOG_STEER, "-------- STA Table ---------\n");
	for (i = 0; i < STA_MAX_NUM; i++) {
		if (hlist_empty(&ctl->sta_table[i]))
			continue;

		hlist_for_each_entry(st, &ctl->sta_table[i], hlist) {
			if (!macaddr || !memcmp(macaddr, st->macaddr, 6)) {
				rate_print_steer_sta_data(st);
			}
		}
	}
	cntlr_dbg(LOG_STEER, "----------------------------\n");
}

static int estimate_sta_thput(void *priv, struct steer_sta *s, bool curr)
{
	struct rate_steer_control *sc = (struct rate_steer_control *)priv;
	struct rate_steer_sta_data *st = rate_lookup_sta(sc, s->macaddr);
	void *cntlr = ((struct steer_control *)(sc->self))->controller;
	struct wifi_radio_element *rel;
	struct wifi_sta_element *sta;
	struct wifi_caps caps = {0};
	enum wifi_bw bw = BW20;
	enum wifi_bw apbw;
	size_t ieslen;
	uint8_t *ies;
	uint8_t anpi;
	int snr;


	sta = cntlr_get_sta_element(cntlr, s->macaddr);
	if (!sta)
		return -1;

	if (sta->reassoc_framelen < 4 || !sta->reassoc_frame)
		return -1;

	ieslen = sta->reassoc_framelen - 4;
	ies = sta->reassoc_frame + 4;

	if (wifi_find_ie_ext(ies, ieslen, IE_EXT_EHT_CAP)) {
		caps.valid |= WIFI_CAP_EHT_VALID;
		bw = BW160;
	} else if (wifi_find_ie_ext(ies, ieslen, IE_EXT_HE_CAP)) {
		caps.valid |= WIFI_CAP_HE_VALID;
		bw = BW80;
	} else if (wifi_find_ie(ies, ieslen, IE_VHT_CAP)) {
		caps.valid |= WIFI_CAP_VHT_VALID;
		bw = BW40;
	} else if (wifi_find_ie(ies, ieslen, IE_HT_CAP)) {
		caps.valid |= WIFI_CAP_HT_VALID;
	}

	apbw = wifi_bw_to_enum_bw(wifi_opclass_get_bw(s->bss.opclass));
	bw = min(apbw, bw);

	rel = cntlr_get_radio_element_by_bssid(cntlr, s->bss.bssid);
	anpi =  rel ? rel->anpi : MIN_ANPI;
	if (anpi == 0)
		anpi = MIN_ANPI;

	snr = curr ? (sta->rcpi - anpi) / 2 : MAX_SNR;
	wifi_nss_from_ies(ies, ieslen, &st->nss);

	return wifi_get_estimated_throughput(snr, bw, &caps, st->nss, 0);
}

static int estimate_sta_thput_with_nbr(void *priv, struct steer_sta *s,
				       struct steer_sta_target_bss *tbss,
				       uint8_t rcpi)
{
	struct rate_steer_control *sc = (struct rate_steer_control *)priv;
	struct rate_steer_sta_data *st = rate_lookup_sta(sc, s->macaddr);
	void *cntlr = ((struct steer_control *)(sc->self))->controller;
	struct wifi_radio_element *rel;
	struct wifi_sta_element *sta;
	struct wifi_caps caps = {0};
	enum wifi_bw bw = BW20;
	enum wifi_bw apbw;
	//uint8_t ch_busy;
	size_t ieslen;
	uint8_t *ies;
	uint8_t anpi;
	int snr;


	//ch_busy = (tbss->ch_util / 255) * 100;	/* in %ge */

	sta = cntlr_get_sta_element(cntlr, s->macaddr);
	if (!sta)
		return -1;

	if (sta->reassoc_framelen < 4 || !sta->reassoc_frame)
		return -1;

	ieslen = sta->reassoc_framelen - 4;
	ies = sta->reassoc_frame + 4;

	if (wifi_find_ie_ext(ies, ieslen, IE_EXT_EHT_CAP)) {
		caps.valid |= WIFI_CAP_EHT_VALID;
		bw = BW160;
	} else if (wifi_find_ie_ext(ies, ieslen, IE_EXT_HE_CAP)) {
		caps.valid |= WIFI_CAP_HE_VALID;
		bw = BW80;
	} else if (wifi_find_ie(ies, ieslen, IE_VHT_CAP)) {
		caps.valid |= WIFI_CAP_VHT_VALID;
		bw = BW40;
	} else if (wifi_find_ie(ies, ieslen, IE_HT_CAP)) {
		caps.valid |= WIFI_CAP_HT_VALID;
	}

	apbw = wifi_bw_to_enum_bw(wifi_opclass_get_bw(tbss->opclass));
	bw = min(apbw, bw);

	rel = cntlr_get_radio_element_by_bssid(cntlr, tbss->bssid);
	anpi =  rel ? rel->anpi : MIN_ANPI;
	if (anpi == 0)
		anpi = MIN_ANPI;

	snr = (rcpi - anpi) / 2;
	return wifi_get_estimated_throughput(snr, bw, &caps, st->nss, 0);
}

/* Function returns
 * -1 when rate < rate_threshold for consecutive rate_low_trigger_cnt,
 * +1 when rate >= rate_threshold for consecutive rate_high_trigger_cnt,
 * 0 otherwise.
 */
int rate_sta_trigger_check(struct rate_steer_sta_data *st)
{
	int highrate_cnt = 0;
	int lowrate_cnt = 0;
	int start = 0;
	int end = 0;

	if (st->t < st->rate_low_trigger_cnt)
		return RATE_TRIGGER_NONE;

	end = st->t < NUM_RATE_SAMPLES ? st->t : st->t % NUM_RATE_SAMPLES;
	if (st->t >= NUM_RATE_SAMPLES)
		start = st->t % NUM_RATE_SAMPLES;

	cntlr_trace(LOG_STEER, "Check rate-trigger type\n");
	do {
		end--;
		if (end < 0)
			end = NUM_RATE_SAMPLES - 1;

		cntlr_trace(LOG_STEER, "[%d] = (dt = %u | dl = %u, ul = %u, est-dl = %u)\n",
			    end, st->rate[end].dt, st->rate[end].dl, st->rate[end].ul, st->rate[end].est_dl);

		if (st->rate[end].est_dl < (st->maxrate * st->rate_threshold) / 100) {
			highrate_cnt = 0;
			lowrate_cnt++;
			if (lowrate_cnt >= st->rate_low_trigger_cnt) {
				if (!st->lowrate_epoch) {
					time(&st->lowrate_tstart);
					st->lowrate_epoch = 1;
					return RATE_TRIGGER_LOW;
				}

				return RATE_TRIGGER_NONE;
			}
		} else {
			lowrate_cnt = 0;
			highrate_cnt++;
			if (highrate_cnt >= st->rate_high_trigger_cnt) {
				if (st->lowrate_epoch) {
					time(&st->lowrate_tend);
					st->lowrate_epoch = 0;
					return RATE_TRIGGER_HIGH;
				}

				return RATE_TRIGGER_NONE;
			}
		}
	} while (end != start);

	return RATE_TRIGGER_NONE;
}

static int rate_steer_ok(struct rate_steer_sta_data *st, uint32_t best_rate)
{
	struct steer_sta *s = ((struct steer_sta *)st->sta);

	s->reason = STEER_REASON_LOW_THPUT;
	s->verdict = STEER_VERDICT_OK;
	st->num_steer_attempt++;
	st->steer_attempt_pending = true;

	cntlr_info(LOG_STEER,
		  "rate_steer: decision is steer STA " MACFMT " to new AP " MACFMT " with est DL-RATE %u\n",
		  MAC2STR(s->macaddr),
		  MAC2STR(s->target.bssid),
		  best_rate);

	return 0;
}

int rate_steer_check(void *priv, struct steer_sta *s, uint16_t rxcmdu_type)
{
	struct rate_steer_control *sc = (struct rate_steer_control *)priv;
	struct rate_steer_sta_data *st = NULL;
	int rate_trigger = RATE_TRIGGER_NONE;
	struct wifi_sta_element *sta;
	bool best_found = false;
	time_t now = time(NULL);
	uint8_t best_channel;
	uint8_t *best_agent;
	uint8_t *best_bssid;
	uint8_t best_rate;
	void *cntlr;


	if (!sc || !sc->enabled || !s)
		return 0;

	if (hwaddr_is_zero(s->bss.bssid) || hwaddr_is_zero(s->bss.agent) ||
	    s->bss.channel == 0 || s->bss.opclass == 0) {
		cntlr_dbg(LOG_STEER, "%s: s->bss.bssid = "MACFMT", s->bss.agent = " MACFMT
			  ", channel = %d, opclass = %d\n",
			  __func__, MAC2STR(s->bss.bssid), MAC2STR(s->bss.agent),
			  s->bss.channel, s->bss.opclass);
		cntlr_dbg(LOG_STEER, "%s: Incomplete/invalid STA data. Exit\n", __func__);

		return 0;
	}

	cntlr = ((struct steer_control *)(sc->self))->controller;
	if (cntlr_is_sta_bsta(cntlr, s->macaddr)) {
		cntlr_dbg(LOG_STEER, "%s: " MACFMT " is bSTA; Exit\n", __func__, MAC2STR(s->macaddr));
		return 0;
	}

	sta = cntlr_get_sta_element(cntlr, s->macaddr);
	if (!sta)
		return 0;

	cntlr_dbg(LOG_STEER, "%s: STA " MACFMT ", rate (dl = %uMbps, ul = %uMbps) --------->\n",
		  __func__, MAC2STR(s->macaddr),
		  sta->dl_rate / 1000, sta->ul_rate / 1000);

	print_nbrlist(s);
	best_channel = s->bss.channel;
	best_agent = s->bss.agent;
	best_bssid = s->bss.bssid;
	s->verdict = STEER_VERDICT_UNDECIDED;
	s->reason = STEER_REASON_UNDEFINED;

	st = rate_lookup_sta(sc, s->macaddr);
	if (!st) {
		int idx = sta_table_hash(s->macaddr);

		/* initialize steer state data for the STA */
		st = calloc(1, sizeof(*st));
		if (!st) {
			cntlr_dbg(LOG_STEER, "%s: -ENOMEM\n", __func__);
			return -1;
		}

		st->priv = sc;
		st->sta = s;
		memcpy(st->macaddr, s->macaddr, 6);
		st->t = 0;
		time(&st->t0);
		st->rate_threshold = sc->rate_threshold;
		st->boost = sc->boost;
		st->rate_low_trigger_cnt = DEFAULT_RATE_LOW_CNT;
		st->rate_high_trigger_cnt = DEFAULT_RATE_HIGH_CNT;

		sc->sta_count++;
		hlist_add_head(&st->hlist, &sc->sta_table[idx]);

		s->verdict = STEER_VERDICT_UNDECIDED;
		s->reason = STEER_REASON_UNDEFINED;

		rate_sta_timer_reset(sc, st);
	}

	/* shouldn't happen */
	if (WARN_ON(st->sta != s))
		st->sta = s;

	if (rxcmdu_type == CMDU_TYPE_TOPOLOGY_NOTIFICATION) {
		if (s->bss.connected != st->connected) {
			cntlr_dbg(LOG_STEER,
				  "%s: STA " MACFMT " %s " MACFMT "\n", __func__,
				  MAC2STR(s->macaddr),
				  s->bss.connected == 0 ? "disconnected from" : "connected to",
				  MAC2STR(s->bss.bssid));

			rate_reset_steer_sta_data(st);
			rate_sta_timer_reset(sc, st);
			return 0;
		}
	}

	if (st->maxrate <= 0) {
		st->maxrate = estimate_sta_thput(priv, s, 0);
		if (st->maxrate < 0)
			return 0;
	}

	st->est_rate = estimate_sta_thput(priv, s, 1);
	if (st->est_rate < 0)
		return 0;

	best_rate = st->est_rate;

	/* update STA's latest samples */
	if (rxcmdu_type == CMDU_ASSOC_STA_LINK_METRICS_RESPONSE ||
	    rxcmdu_type == CMDU_AP_METRICS_RESPONSE) {
		st->rate[st->t % NUM_RATE_SAMPLES].dl = sta->dl_rate / 1000;
		st->rate[st->t % NUM_RATE_SAMPLES].ul = sta->ul_rate / 1000;
		st->rate[st->t % NUM_RATE_SAMPLES].est_dl = st->est_rate;
		st->rate[st->t % NUM_RATE_SAMPLES].tx = sta->tx_bytes;
		st->rate[st->t % NUM_RATE_SAMPLES].rx = sta->rx_bytes;
		st->rate[st->t % NUM_RATE_SAMPLES].dt = st->tprev ? difftime(now, st->tprev) : 0;
		st->t++;
		st->tprev = now;

		rate_dump_sta_table(sc, s->macaddr);

		if (st->maxrate == 0)
			return 0;

		rate_trigger = rate_sta_trigger_check(st);
		cntlr_dbg(LOG_STEER, "%s: rate-trigger = %s\n", __func__,
			  rate_trigger == RATE_TRIGGER_LOW ? "LOW" :
			  rate_trigger == RATE_TRIGGER_HIGH ? "HIGH" : "NONE");
	}

	if (rxcmdu_type == CMDU_CLIENT_STEERING_BTM_REPORT) {
		if (st->steer_attempt_pending)
			st->steer_attempt_pending = 0;

		return 0;
	}

	if (sc->max_btm_attempt != DEFAULT_MAX_STEER_ATTEMPT &&
	    st->num_steer_attempt > sc->max_btm_attempt) {
		cntlr_dbg(LOG_STEER,
			  "%s: Skip STA " MACFMT " steer check. "
			  "Num-attempts (%d) > max-attempts (%d)\n",
			  __func__, MAC2STR(s->macaddr),
			  st->num_steer_attempt, sc->max_btm_attempt);
		return 0;
	}

	if (st->lowrate_epoch) {
		/* return if the last steer attempt is pending */
		if (st->steer_attempt_pending)
			return 0;

		if (st->usta_query_sent) {
			struct unassoc_sta_metrics *u = NULL;

			list_for_each_entry(u, s->unassoc_metriclist, list) {
				struct steer_sta_target_bss *tbss = NULL;
				int tbss_est_rate_dl;
				int est_rate_dl;
				time_t meas_time;

				st->usta_metrics_processed = 1;
				meas_time = u->rpt_time - (u->time_delta / 1000);
				if (is_meas_old(now, meas_time, USTA_METRICS_AGEOUT))
					continue;

				if (!sc->bandsteer && u->channel != s->bss.channel)
					continue;

				if (!memcmp(u->bssid, s->bss.bssid, 6))
					continue;

				tbss = nbrlist_lookup_bssid(s, u->bssid);
				if (!tbss)
					continue;

				est_rate_dl = estimate_sta_thput_with_nbr(priv, s, tbss, u->ul_rcpi);
				if (est_rate_dl < 0)
					continue;

				tbss_est_rate_dl = cntlr_get_max_thput_estimate_for_node(cntlr, tbss->agent);
				if (tbss_est_rate_dl < 0)
					continue;

				cntlr_dbg(LOG_STEER, "%s: STA " MACFMT ": estimated target fh-rate = %u, bh-rate = %u\n",
					  __func__, MAC2STR(s->macaddr), est_rate_dl, tbss_est_rate_dl);

				est_rate_dl = min(est_rate_dl, tbss_est_rate_dl);
				if (est_rate_dl >= best_rate + st->boost) {
					best_found = true;
					best_rate = tbss_est_rate_dl;
					best_agent = tbss->agent;
					best_bssid = tbss->bssid;
					best_channel = tbss->channel;
				}
			}
		}

		if (best_found) {
			if (st->est_rate <= (st->maxrate * st->rate_threshold) / 100) {
				memcpy(s->target.bssid, best_bssid, 6);
				memcpy(s->target.agent, best_agent, 6);
				s->target.channel = best_channel;

				return rate_steer_ok(st, best_rate);
			}

			return 0;
		}

		if (!st->usta_query_sent) {
			int ret = rate_query_unassoc_sta_metrics(sc, st);

			if (!ret && st->usta_query_sent) {
				st->state = RATE_STA_USTA_CHECK;
				st->usta_metrics_processed = 0;
				rate_sta_timer_set(sc, st, USTA_RESPONSE_TIMEOUT * 1000);
			}
		}

		return 0;
	}

	/* curr-rate >= rate_threshold */
	if (rate_trigger == RATE_TRIGGER_HIGH) {
		rate_reset_steer_sta_data(st);
	}

	return 0;
}

int rate_cmdu_notifier(void *priv, struct cmdu_buff *cmdu)
{
	cntlr_dbg(LOG_STEER, "%s: received CMDU 0x%02X\n", __func__, cmdu_get_type(cmdu));

	//TODO

	return 0;
}

uint16_t cmdu_notifylist[] = {
	CMDU_ASSOC_STA_LINK_METRICS_RESPONSE,
	CMDU_BEACON_METRICS_RESPONSE,
	CMDU_UNASSOC_STA_LINK_METRIC_RESPONSE,
	CMDU_TYPE_TOPOLOGY_NOTIFICATION,
	/* CMDU_ASSOCIATION_STATUS_NOTIFICATION, */
	CMDU_CLIENT_STEERING_BTM_REPORT,
	CMDU_STEERING_COMPLETED,
};

struct steer_control rate = {
	.name = RATE_PLUGIN_NAME,
	.init = rate_steer_init,
	.config = rate_steer_configure,
	.exit = rate_steer_exit,
	.steer = rate_steer_check,
	.cmdu_notifier = rate_cmdu_notifier,
	.num_cmdu = ARRAY_SIZE(cmdu_notifylist),
	.cmdulist = cmdu_notifylist,
};

static int rate_steer_init(void **priv)
{
	struct rate_steer_control *p;

	p = calloc(1, sizeof(struct rate_steer_control));
	if (!p)
		return -1;

	*priv = p;
	p->self = &rate;
	p->enabled = true;
	p->rate_threshold = DEFAULT_RATE_THRESHOLD;
	p->boost = DEFAULT_RATE_BOOST;
	p->max_btm_attempt = DEFAULT_MAX_STEER_ATTEMPT;
	p->steer_int = DEFAULT_STEER_INT;

	timer_init(&p->t, rate_timer_cb);

	cntlr_info(LOG_STEER, "%s", "Estimated-rate in target-BSS based STA steering initalized");
	return 0;
}

static int rate_steer_configure(void *priv, void *config_section)
{
	enum {
		RATE_ENABLED,
		RATE_ALLOW_BANDSTEER,
		RATE_BOOST,
		RATE_THRESHOLD,
		//RATE_STEER_RETRY_INT,
		RATE_STEER_INT,
		//RATE_STEER_DISABLE_INT,
		NUM_RATE_ATTRS,
	};
	const struct uci_parse_option opts[] = {
		[RATE_ENABLED] = { .name = "enabled", .type = UCI_TYPE_STRING },
		[RATE_ALLOW_BANDSTEER] = { .name = "bandsteer", .type = UCI_TYPE_STRING },
		[RATE_BOOST] = { .name = "boost", .type = UCI_TYPE_STRING },
		[RATE_THRESHOLD] = { .name = "threshold", .type = UCI_TYPE_STRING },
		[RATE_STEER_INT] = { .name = "steer_int", .type = UCI_TYPE_STRING },
	};
	struct rate_steer_control *p = (struct rate_steer_control *)priv;
	struct uci_section *s = (struct uci_section *)config_section;
	struct uci_option *tb[NUM_RATE_ATTRS] = {0};


	cntlr_dbg(LOG_STEER, "%s: Enter\n", __func__);
	if (!p)
		return -1;

	uci_parse_section(s, opts, NUM_RATE_ATTRS, tb);

	if (strncmp(s->e.name, RATE_PLUGIN_NAME, strlen(s->e.name)))
		return 0;	/* not our config */

	if (tb[RATE_ENABLED]) {
		const char *val = tb[RATE_ENABLED]->v.string;

		p->enabled = atoi(val) == 1 ? true : false;
	}

	if (tb[RATE_ALLOW_BANDSTEER]) {
		const char *val = tb[RATE_ALLOW_BANDSTEER]->v.string;

		p->bandsteer = atoi(val) == 1 ? true : false;
	}

	if (tb[RATE_THRESHOLD]) {
		const char *val = tb[RATE_THRESHOLD]->v.string;
		int thresh = atoi(val);

		if (thresh > 0)
			p->rate_threshold = thresh;
	}

	if (tb[RATE_BOOST]) {
		const char *val = tb[RATE_BOOST]->v.string;
		int boost = atoi(val);

		if (boost > 0)
			p->boost = boost;
	}

	if (tb[RATE_STEER_INT]) {
		const char *val = tb[RATE_STEER_INT]->v.string;
		int ival = atoi(val);

		if (ival > 0)
			p->steer_int = ival;
	}

	cntlr_dbg(LOG_STEER, "%s: Exit\n", __func__);
	return 0;
}

static int rate_steer_exit(void *priv)
{
	struct rate_steer_control *p = (struct rate_steer_control *)priv;

	if (p) {
		timer_del(&p->t);
		rate_sta_table_flush(p->sta_table);
		free(p);
	}

	cntlr_dbg(LOG_STEER, "%s: Exit\n", __func__);
	return 0;
}

