#ifdef DYNBH
#include "dynbh.h"

#include <1905_tlvs.h>
#include <easy/if_utils.h>
#include <easy/utils.h>
#include <easymesh.h>
#include <libubox/blob.h>
#include <libubox/blobmsg.h>
#include <libubox/blobmsg_json.h>
#include <libubox/uloop.h>
#include <libubox/utils.h>
#include <libubus.h>
#include <map_module.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <timer.h>
#include <ubusmsg.h>
#include <linux/if.h>

#include "agent.h"
#include "agent_cmdu.h"
#include "agent_map.h"
#include "agent_ubus.h"
#include "agent_tlv.h"
#include "backhaul.h"
#include "1905_ubus.h"
#ifdef PERSIST_CONTROLLER
#include "dynbh_config.h"
#endif
#include "dynbh_nl.h"
#include "dynbh_ubus.h"
#include "../utils/utils.h"


#ifndef BIT
#define BIT(n)	(1U << (n))
#endif

#define HEARTBEAT_PROBE_TIMEOUT 	60
#define APCONF_MAX_RETRIES 		5
#define APCONF_INTERVAL 		2

static struct ethport *dynbh_get_ethport_by_mid(struct dynbh_ctx *ctx, uint16_t mid);

bool dynbh_is_enabled(struct agent *a)
{
	return (a->cfg.cscfg->local == false && a->cfg.dynbh);
}

static bool is_loop(struct ethport *port)
{
	return (port->state == PORT_STATE_LOOP);
}

static bool is_uplink(struct ethport *port)
{
	return (port->state == PORT_STATE_UPLINK);
}

static bool is_connected(struct ethport *port)
{
	return (port->operstate == IF_OPER_UP);
}

const char *port_state_to_str(enum dynbh_port_state state)
{
	switch (state) {
	case PORT_STATE_DISCOVERY:
		return "discovery";
	case PORT_STATE_UPLINK:
		return "uplink";
	case PORT_STATE_LAN:
		return "lan";
	case PORT_STATE_LOOP:
		return "loop";
#ifdef PERSIST_CONTROLLER
	case PORT_STATE_DHCP_SERVER:
		return "dhcp_server";
#endif
	default:
		break;
	}
	return "unknown";
}

const char *dynbh_mode_to_str(enum cntlr_discovery_mode mode)
{
	switch (mode) {
	case DYNAMIC_CNTLR_MODE_AUTO:
		return "auto";
	case DYNAMIC_CNTLR_MODE_DISABLED:
		return "disabled";
	case DYNAMIC_CNTLR_MODE_CONTROLLER:
		return "controller";
	default:
		break;
	}
	return "unknown";
}

enum cntlr_discovery_mode dynbh_str_to_mode(const char *mode)
{
	if (!strncmp(mode, "auto", 4))
		return DYNAMIC_CNTLR_MODE_AUTO;
	else if (!strncmp(mode, "disabled", 8))
		return DYNAMIC_CNTLR_MODE_DISABLED;
	else if (!strncmp(mode, "controller", 10))
		return DYNAMIC_CNTLR_MODE_CONTROLLER;
	return DYNAMIC_CNTLR_MODE_UNKNOWN;
}

static void dynbh_send_topo_discovery(struct agent *a, struct ethport *port)
{
	struct cmdu_buff *cmdu;

	cmdu = agent_gen_topology_discovery(a, a->almac, port->macaddr);
	if (cmdu) {
		uint16_t mid = 0;

		ieee1905_ubus_send_al_cmdu(a->ubus_ctx, cmdu, &mid,
					   port->ifname, a->pvid);
		cmdu_free(cmdu);
	}
}

static void dynbh_start_discovery(struct agent *a, struct ethport *port)
{
	agnt_dbg(LOG_DYNBH, "%s: ===> port:%s\n", __func__, port->ifname);

	if (!is_uplink(port)) {
		br_delif(a->cfg.al_bridge, port->ifname);
		port->state = PORT_STATE_DISCOVERY;
	}

	i1905_addif(a, port);
	dynbh_send_topo_discovery(a, port);
}

static void dynbh_stop_discovery(struct agent *a, struct ethport *port)
{
	agnt_dbg(LOG_DYNBH, "%s: ===> port:%s\n", __func__, port->ifname);

	if (if_isbridge_interface(port->ifname))
		return;

	i1905_delif(a, port);
#ifdef PERSIST_CONTROLLER
	if_setaddr(port->ifname, "0.0.0.0");
#endif
	br_addif(a->cfg.al_bridge, port->ifname);
	dynbh_send_topo_discovery(a, port);
}

static void dynbh_unset_eth_uplink(struct agent *a)
{
	agnt_dbg(LOG_DYNBH, "%s: ===>\n", __func__);
	agent_exec_platform_scripts("unset_uplink eth");
#ifdef AGENT_ISLAND_PREVENTION
	if (a->cfg.island_prevention)
		wifiagent_toggle_fh(a, false, "all", true);
#endif
	dynbh_bsta_scan_on_enabled(a);
}

static void dynbh_set_eth_uplink(struct agent *a, struct ethport *port)
{
	char fmt[64] = {0};

	agnt_info(LOG_DYNBH, "%s: port:%s is uplink\n", __func__, port->ifname);

	br_addif(a->cfg.al_bridge, port->ifname);

	agent_exec_platform_scripts("bstas_down");

	snprintf(fmt, sizeof(fmt), "set_uplink eth %s " MACFMT, port->ifname, MAC2STR(port->macaddr)); /* Flawfinder: ignore */
	agent_exec_platform_scripts(fmt);

#ifdef AGENT_ISLAND_PREVENTION
	if (a->cfg.island_prevention)
		wifiagent_toggle_fh(a, true, "all", true);
#endif
	agnt_dbg(LOG_DYNBH, "%s: Scheduling next ACS in 1 second\n", __func__);
	timer_set(&a->autocfg_dispatcher, 1 * 1000);
	timestamp_update(&a->eth_connect_t);
	port->state = PORT_STATE_UPLINK;
}

static void dynbh_set_loop(struct agent *a, struct ethport *port)
{
	if (if_isbridge_interface(port->ifname))
		br_delif(a->cfg.al_bridge, port->ifname);

	i1905_delif(a, port);
	port->state = PORT_STATE_LOOP;
}


#ifdef PERSIST_CONTROLLER
static void controller_discovery_persist(struct agent *a)
{
	agent_exec_platform_scripts("persist_cntlr");
	dynbh_config_set_controller_select(DYNAMIC_CNTLR_MODE_CONTROLLER, true, true);

	agent_config_reload(a);
#ifdef DYNBH
#ifdef PERSIST_CONTROLLER
	if (a->cfg.cscfg->discovery_mode == DYNAMIC_CNTLR_MODE_CONTROLLER) {
		if (!is_local_cntlr_running() &&
		    is_local_cntlr_available())
			agent_reload_local_cntlr(a, true);

		if (!is_process_running("decollector") &&
		    is_package_available("decollector"))
			runCmd("/etc/init.d/decollector reload");
	}
#endif
#endif
}

static void controller_discovery_disable(struct agent *a)
{
	dynbh_config_set_controller_select(DYNAMIC_CNTLR_MODE_DISABLED, false, false);
	agent_config_reload(a);
}

static void controller_discovery_send_dhcp(const char *ifname)
{
	char fmt[64] = {0};

	snprintf(fmt, sizeof(fmt), "dhcp_discovery %s", ifname);
	agent_exec_platform_scripts(fmt);
}
#endif

static uint16_t ethport_send_autoconf_search(struct agent *a, struct ethport *port)
{
	struct cmdu_buff *cmdu;
	uint16_t mid = 0;

	agnt_dbg(LOG_DYNBH, "%s: sending autoconf search (num:%d) for port:%s\n",
		 __func__, (port->num_mid+1), port->ifname);
	i1905_addif(a, port);

	cmdu = agent_gen_ap_autoconfig_search(a, 0x01, a->cfg.map_profile);
	if (!cmdu)
		return 0xffff;

	ieee1905_ubus_send_al_cmdu(a->ubus_ctx, cmdu, &mid, port->ifname, a->pvid);
	cmdu_free(cmdu);

	port->mid[port->num_mid] = mid;
	if (port->num_mid < 31)
		port->num_mid++;

	return mid;
}

#ifdef PERSIST_CONTROLLER
static bool has_dhcp_ip4(struct agent *a, const char *ifname)
{
	struct ip_address ips[32] = {0};
        int num_ipaddrs = 32;
        int ret, i;

        ret = if_getaddrs(ifname, ips, &num_ipaddrs);
        if (ret || num_ipaddrs <= 0)
		return false;
	for (i = 0; i < num_ipaddrs; i++) {
		if (ips[i].family == AF_INET)
			return true;
	}

	return false;
}

/* callback to do DHCP discovery */
static int ethport_send_dhcp_discovery(struct agent *a, struct ethport *port)
{
	int rc = 0;

	if (agent_is_bsta_connected(a)) {
		controller_discovery_disable(a);
		return -1;
	}

	agnt_dbg(LOG_DYNBH, "%s: sending dhcp discovery for port:%s\n",
		 __func__, port->ifname);
	controller_discovery_send_dhcp(port->ifname);
	return rc;
}

static void ethport_set_dhcp_server(struct agent *a, struct ethport *port)
{
	port->state = PORT_STATE_DHCP_SERVER;

	agnt_info(LOG_DYNBH, "%s: port:%s has dhcp server but no controller\n",
		 __func__, port->ifname);

	i1905_delif(a, port);
	if_setaddr(port->ifname, "0.0.0.0");
	br_addif(a->cfg.al_bridge, port->ifname);
	controller_discovery_persist(a);
}
#endif

static void ethport_set_lan(struct agent *a, struct ethport *port)
{
	if (is_uplink(port))
		dynbh_unset_eth_uplink(a);
	port->state = PORT_STATE_LAN;

	agnt_info(LOG_DYNBH, "%s: port:%s is lan\n", __func__, port->ifname);

	dynbh_stop_discovery(a, port);

	port->num_mid = 0;
	port->discovery_attempts++;
}

/* callback to send ap autoconfig search */
static void ethport_discovery_cb(atimer_t *t)
{
	struct ethport *port = container_of(t, struct ethport, port_discovery);
	struct agent *a = container_of(port->ctx, struct agent, dbh);
	float timeout = 15 * port->discovery_attempts;

	if (timeout > HEARTBEAT_PROBE_TIMEOUT) {
		/* trigger keep-alive heartbeat at least once per minute */
		timeout = HEARTBEAT_PROBE_TIMEOUT;
	}

	if (port->num_mid < APCONF_MAX_RETRIES) {
		/* trigger autoconf search to detect controller */
		ethport_send_autoconf_search(a, port);

		timeout = APCONF_INTERVAL;
#ifdef PERSIST_CONTROLLER
		if (port->state == PORT_STATE_DISCOVERY &&
		    !has_dhcp_ip4(a, port->ifname))
			ethport_send_dhcp_discovery(a, port);
#endif
	}
#ifdef PERSIST_CONTROLLER
	else if (a->cfg.cscfg->discovery_mode == DYNAMIC_CNTLR_MODE_AUTO &&
		 has_dhcp_ip4(a, port->ifname)) {
		ethport_set_dhcp_server(a, port);
		controller_discovery_send_dhcp(a->cfg.al_bridge);
	}
#endif
	else {
		/* port did not have controller (or DHCP server) */
		if (port->state == PORT_STATE_DISCOVERY)
			ethport_set_lan(a, port);
		else {
			port->num_mid = 0;
			port->discovery_attempts++;
		}
	}

	timer_set(&port->port_discovery, timeout * 1000);
}

void dynbh_port_disconnected(struct agent *a, struct ethport *port)
{
	struct dynbh_ctx *priv = &a->dbh;

	agnt_dbg(LOG_DYNBH, "%s: port:%s disconnected (is_uplink:%d)\n", __func__,
		 port->ifname, is_uplink(port));

	timer_del(&port->port_discovery);

	if (!if_isbridge_interface(port->ifname))
		dynbh_stop_discovery(a, port);

	if (is_uplink(port)) {
		struct ethport *p = NULL;
		bool found = false;

		list_for_each_entry(p, &priv->ethportlist, list) {
			if (!is_loop(p))
				continue;

			dynbh_set_eth_uplink(a, p);
			dynbh_stop_discovery(a, p);
			found = true;
		}

		if (!found)
			dynbh_unset_eth_uplink(a);
	}
	port->state = PORT_STATE_UNKNOWN;
}

void dynbh_port_connected(struct agent *a, struct ethport *port)
{
	agnt_trace(LOG_DYNBH, "%s: ===>\n", __func__);

	port->discovery_attempts = 1;
	port->num_mid = 0;
	/* immediately send apconf search */
	timer_set(&port->port_discovery, 0);

	dynbh_start_discovery(a, port);
}

static void dynbh_cntlr_found(struct agent *a, struct cmdu_buff *cmdu,
			      struct ethport *port)
{
	int timeout = HEARTBEAT_PROBE_TIMEOUT;
	const char *ifname = port->ifname;

	if (is_uplink(port) || is_loop(port)) {
		agnt_trace(LOG_DYNBH, "port %s already known to be connected"\
				      "to a controller\n", ifname);
		goto out;
	} else
		agnt_info(LOG_DYNBH, "port %s is not known to have a controller\n",
			  ifname);

	if (!agent_is_backhaul_type_eth()) {
		/* if there is no active eth backhaul use this link as backhaul */
		dynbh_set_eth_uplink(a, port);
		dynbh_stop_discovery(a, port);
#ifdef PERSIST_CONTROLLER
		if (a->cfg.cscfg->discovery_mode == DYNAMIC_CNTLR_MODE_AUTO) {
			if_setaddr(port->ifname, "0.0.0.0");
			controller_discovery_disable(a);
			controller_discovery_send_dhcp(a->cfg.al_bridge);
		}
#endif
	} else {
		agnt_info(LOG_DYNBH, "%s: active controller additionally found on port:%s, keep out of bridge to prevent a loop\n",
			  __func__, port->ifname);
		dynbh_set_loop(a, port);
	}

out:
	port->discovery_attempts = port->num_mid = 0;
	port->discovery_attempts++;
	timer_set(&port->port_discovery, timeout * 1000);
}

int dynbh_handle_autoconfig_response(struct agent *a, struct cmdu_buff *cmdu)
{
	struct tlv_policy a_policy[] = {
		[0] = {
			.type = TLV_TYPE_SUPPORTED_ROLE,
			.present = TLV_PRESENT_ONE
		},
		[1] = {
			.type = MAP_TLV_SUPPORTED_SERVICE,
			.present = TLV_PRESENT_OPTIONAL_ONE
		},
		[2] = {
			.type = MAP_TLV_MULTIAP_PROFILE,
			.present = TLV_PRESENT_ONE
		},
		[3] = {
			.type = TLV_TYPE_SUPPORTED_FREQ_BAND,
			.present = TLV_PRESENT_ONE
		}
	};
	struct tlv *tv[4][TLV_MAXNUM] = {0};
	char *ifname = cmdu->dev_ifname;
	struct dynbh_ctx *priv = &a->dbh;
	struct ethport *port;
	uint16_t mid;
	int ret;

	mid = cmdu_get_mid(cmdu);
	port = dynbh_get_ethport_by_mid(priv, mid);
	if (!port) {
		agnt_trace(LOG_DYNBH, "%s: no port with mid:%u found\n",
			   __func__, mid);

		port = dynbh_get_ethport(priv, ifname);
		if (!port) {
			agnt_trace(LOG_DYNBH, "%s: no port with ifname:%s found\n",
				   __func__, ifname);
			return -1;
		}
	}

	if (!is_connected(port)) {
		agnt_info(LOG_DYNBH, "%s: port %s not yet known to be connected\n",
			  __func__, port->ifname);
	}

	ret = cmdu_parse_tlvs(cmdu, tv, a_policy, 4);
	if (ret) {
		agnt_err(LOG_DYNBH, "%s: parse_tlv failed\n", __func__);
		return -1;
	}

	if (!tv[1][0]) {
		agnt_dbg(LOG_DYNBH, "%s: supported service tlv not included\n",
			 __func__);
		return -1;
	}

	if (!agent_tlv_supported_service_is_controller(tv[1][0])) {
		agnt_trace(LOG_DYNBH, "%s: Response did not support controller!\n",
			   __func__);
		return -1;
	}

	dynbh_cntlr_found(a, cmdu, port);
	return 0;
}

int dynbh_handle_autoconfig_search(struct agent *a, struct cmdu_buff *cmdu)
{
	struct tlv_policy a_policy[] = {
		[0] = { .type = TLV_TYPE_AL_MAC_ADDRESS_TYPE,
			.present = TLV_PRESENT_ONE,
			.len = 6, /* macaddr */
		},
		[1] = { .type = TLV_TYPE_SEARCHED_ROLE,
			.present = TLV_PRESENT_ONE,
			.len = 1, /* tlv_searched_role */
		},
		[2] = { .type = TLV_TYPE_AUTOCONFIG_FREQ_BAND,
			.present = TLV_PRESENT_ONE,
			.len = 1, /* tlv_autoconfig_band */
		},
		[3] = { .type = MAP_TLV_SUPPORTED_SERVICE,
			.present = TLV_PRESENT_OPTIONAL_ONE,
			.minlen = 1, /* num of services */
		},
		[4] = { .type = MAP_TLV_SEARCHED_SERVICE,
			.present = TLV_PRESENT_OPTIONAL_ONE,
			.minlen = 1, /* num of services */
		},
		[5] = { .type = MAP_TLV_MULTIAP_PROFILE,
			.present = TLV_PRESENT_ONE,
			.len = 1, /* tlv_map_profile */
		},
	};
	struct tlv *tv[6][TLV_MAXNUM] = {0};
	char *ifname = cmdu->dev_ifname;
	struct dynbh_ctx *priv = &a->dbh;
	struct ethport *port;
	uint16_t mid;
	int ret;

	mid = cmdu_get_mid(cmdu);
	port = dynbh_get_ethport_by_mid(priv, mid);
	if (!port) {
		agnt_trace(LOG_DYNBH, "%s: no port with mid:%u found\n",
			   __func__, mid);

		port = dynbh_get_ethport(priv, ifname);
		if (!port) {
			agnt_trace(LOG_DYNBH, "%s: no port with ifname:%s found\n",
				   __func__, ifname);
			return -1;
		}
	}

	if (!is_connected(port)) {
		agnt_info(LOG_DYNBH, "%s: port %s not yet known to be connected\n",
			  __func__, port->ifname);
	}

	ret = cmdu_parse_tlvs(cmdu, tv, a_policy, 6);
	if (ret) {
		agnt_err(LOG_DYNBH, "%s: parse_tlv failed\n", __func__);
		return -1;
	}

	if (!tv[3][0]) {
		agnt_dbg(LOG_DYNBH, "%s: supported service tlv not included\n",
			 __func__);
		return -1;
	}

	if (!agent_tlv_supported_service_is_controller(tv[3][0])) {
		agnt_trace(LOG_DYNBH, "Response did not support controller!\n");
		return -1;
	}

	dynbh_cntlr_found(a, cmdu, port);
	return 0;
}


struct ethport *dynbh_get_ethport(struct dynbh_ctx *ctx,
		const char *ifname)
{
	struct ethport *port = NULL;

	list_for_each_entry(port, &ctx->ethportlist, list) {
		if (!strncmp(port->ifname, ifname, sizeof(port->ifname)))
			return port;
	}

	return NULL;
}

static struct ethport *dynbh_get_ethport_by_mid(struct dynbh_ctx *ctx, uint16_t mid)
{
	struct ethport *port = NULL;

	list_for_each_entry(port, &ctx->ethportlist, list) {
		int i;

		for (i = 0; i < port->num_mid; i++) {
			if (port->mid[i] == mid)
				return port;
		}
	}

	return NULL;
}

static struct ethport *dynbh_alloc_ethport(struct agent *a,
					   struct dynbh_ctx *priv,
					   char *ifname)
{
	struct ethport *port = NULL;

	port = calloc(1, sizeof(struct ethport));
	if (!port)
		return NULL;

	port->state = PORT_STATE_UNKNOWN;
	port->ctx = priv;
	strncpy(port->ifname, ifname, sizeof(port->ifname) - 1);

	timer_init(&port->port_discovery, ethport_discovery_cb);
	if_gethwaddr(port->ifname, port->macaddr);
	list_add_tail(&port->list, &priv->ethportlist);

	if_getoperstate(ifname, &port->operstate);
	if (is_connected(port))
		dynbh_port_connected(a, port);
	return port;
}

int (* const dbh_ftable[])(struct agent *a, struct cmdu_buff *cmdu) = {
	[0x00] = NULL,
	[0x07] = dynbh_handle_autoconfig_search,
	[0x08] = dynbh_handle_autoconfig_response,
	[LAST_1905_CMDU] = NULL,

};

static void port_free(struct ethport *port)
{
	timer_del(&port->port_discovery);
	free(port);
}

void dynbh_free(struct agent *a)
{
	struct ethport *port = NULL, *tmp;
	struct dynbh_ctx *ctx = &a->dbh;

	list_for_each_entry_safe(port, tmp, &ctx->ethportlist, list) {
		list_del(&port->list);
		port_free(port);
	}
}

void dynbh_init(struct agent *a)
{
	struct dynbh_ctx *ctx = &a->dbh;
	char bh_ifname[16] = {0};
	char ifs[16][16] = {0};
	int num = 0;
	int i;

	INIT_LIST_HEAD(&ctx->ethportlist);

	get_ethportslist(&num, ifs);
	for (i = 0; i < num; i++)
		dynbh_alloc_ethport(a, ctx, ifs[i]);

	if (agent_get_backhaul_ifname(a, bh_ifname)) {
		struct ethport *port;

		port = dynbh_get_ethport(ctx, bh_ifname);
		if (port && is_connected(port)) {
			controller_discovery_disable(a);
			dynbh_set_eth_uplink(a, port);
		} else if (port)
			dynbh_unset_eth_uplink(a);
	}
}
#endif
