#!/opt/lnxall_app/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- Simple UCI-confugured, NetworkManager-based utility
-- 2023/11/02

-- load external Lua modules
local bit = require 'bit'
local uci = require 'uci'
local posix = require 'posix'
local nmcli = require 'nmcli'
local invoker = require 'invoker'

local BDHCPD_SVR   = 'bdhcpd.service'
local HOSTAPD_SVR  = 'lnxhostapd.service'
local BDHCPD_CFG   = "/etc/udhcpd.conf"
local HOSTAPD_CFG  = "/etc/hostapd.conf"

-- rename function
local gfmt = string.format
local g_changed_lan = false

local function setup_env()
	local env_list = { "LANG", "LC_ADDRESS", "LC_IDENTIFICATION", "LC_MEASUREMENT",
		"LC_MONETARY", "LC_NAME", "LC_NUMERIC", "LC_PAPER", "LC_TELEPHONE",
		"LC_TIME", "LC_ALL" }
	for _, envn in ipairs(env_list) do posix.setenv(envn) end
	-- override PATH to call nothup:
	posix.setenv('PATH', '/opt/lnxall_app/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin', 1)

	-- finally, lock a temporary file
	local okay, errmsg = invoker.waitlock('/tmp/.netuci-lock')
	if not okay then
		if type(errmsg) ~= "string" then errmsg = "unknown error" end
		io.stderr:write(gfmt("Error, failed to lock file for netuci: %s\n", errmsg))
		io.stderr:flush()
	end
	return true
end

local function has_netdev(ndev)
	if type(ndev) ~= "string" then return false end
	if string.len(ndev) == 0 then return false end
	return invoker.invoke(invoker.NOSTDIO, "ip", "link", "show", "dev", ndev) == 0
end

local function check_proto(prot)
	if type(prot) ~= "string" then return false end
	if prot == "dhcp" or prot == "static" then return true end
	return false
end

local function check_macaddr(maddr)
	if type(maddr) ~= "string" then return nil end
	if string.len(maddr) ~= 17 then return nil end

	local addrs = {}
	for hex in string.gmatch(maddr, "%x+") do
		local hexnum = tonumber("0x" .. hex)
		if hexnum and hexnum < 256 then
			addrs[#addrs + 1] = hexnum
		end
	end

	if #addrs ~= 6 then return nil end
	return gfmt('%02x:%02x:%02x:%02x:%02x:%02x',
		addrs[1], addrs[2], addrs[3], addrs[4], addrs[5], addrs[6])
end

local function down_all_wlan(w0, w1, w2)
	if w0 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w0, "down") end
	if w1 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w1, "down") end
	if w2 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w2, "down") end
end

local function dhcp_addrp(ipv4, nmask)
	local ips = 0
	for ipe in string.gmatch(ipv4, "%d+") do
		ips = ips * 256 + tonumber(ipe)
	end

	if type(nmask) ~= "number" or nmask == 0 or nmask >= 32 then
		io.stderr:write(gfmt("Error, invalid DHCP server netmask: %s\n", type(nmask)))
		io.stderr:flush()
		return "192.168.1."
	end
	local mask = bit.bnot(bit.lshift(0x1, 32 - nmask) - 0x1)
	ips = bit.band(ips, mask, 0xFFFFFFFF)
	return string.format("%d.%d.%d.",
		bit.band(0xFF, bit.rshift(ips, 24)),
		bit.band(0xFF, bit.rshift(ips, 16)),
		bit.band(0xFF, bit.rshift(ips,  8)))
end

local function start_hostapd_server(wlan)
	local cfgs = {}
	cfgs[#cfgs + 1] = "interface=" .. wlan['netdev']
	cfgs[#cfgs + 1] = "bridge=br-lan"
	cfgs[#cfgs + 1] = "driver=nl80211"
	cfgs[#cfgs + 1] = "logger_syslog=-1"
	cfgs[#cfgs + 1] = "logger_syslog_level=3"
	local ctrl_i = "/var/run/hostapd"
	cfgs[#cfgs + 1] = "ctrl_interface=" .. ctrl_i
	cfgs[#cfgs + 1] = "ctrl_interface_group=0"
	cfgs[#cfgs + 1] = "hw_mode=g"
	cfgs[#cfgs + 1] = "channel=" .. tostring(wlan['channel'])
	if wlan['passwd'] then
		cfgs[#cfgs + 1] = "wpa=2"
		cfgs[#cfgs + 1] = "auth_algs=3"
		cfgs[#cfgs + 1] = "wpa_key_mgmt=WPA-PSK"
		cfgs[#cfgs + 1] = "wpa_passphrase=" .. wlan['passwd']
	else
		cfgs[#cfgs + 1] = "auth_algs=1"
	end
	local ssid = wlan['ssid']
	if ssid == 'lnxall_def_SN' then
		local fini = invoker.readfile("/app/config/fac.ini")
		if type(fini) == "string" then
			local gwsn = string.match(fini, "sn%s*=%s*([^\r\n%s]+)")
			if gwsn then
				ssid = 'lnxall_def_' .. gwsn
			end
		end
	end
	cfgs[#cfgs + 1] = 'ssid=' .. ssid
	cfgs[#cfgs + 1] = "\n"
	cfgs = table.concat(cfgs, "\n")

	local oldcfg = invoker.readfile(HOSTAPD_CFG)
	if oldcfg ~= cfgs then
		local filp = io.open(HOSTAPD_CFG, "wb")
		if filp then
			filp:write(cfgs)
			filp:close(); filp = nil
		end
		invoker.invoke(invoker.NOSTDIO, "rm", "-rf", string.format("%s/*", ctrl_i))
		invoker.invoke(invoker.NOSTDIO, "systemctl", "restart", HOSTAPD_SVR)
	else
		invoker.invoke(invoker.NOSTDIO, "systemctl", g_changed_lan and "restart" or "start", HOSTAPD_SVR)
	end
	invoker.invoke(invoker.NOSTDIO, "systemctl", "enable", HOSTAPD_SVR)
end

local function stop_hostapd_server()
	invoker.invoke(invoker.NOSTDIO, "systemctl", "stop", HOSTAPD_SVR)
	invoker.invoke(invoker.NOSTDIO, "systemctl", "disable", HOSTAPD_SVR)
end

local function start_dhcp_server(lcfg)
	local lfile = '/run/udhcpd.leases'
	if posix.access(lfile) ~= 0 then invoker.invoke(invoker.NOSTDIO, "touch", lfile) end
	local ucfg, addrp = {}, dhcp_addrp(lcfg.ipaddr, lcfg.netmask)
	ucfg[#ucfg + 1] = gfmt("start %s150", addrp)
	ucfg[#ucfg + 1] = gfmt("end %s200", addrp)
	ucfg[#ucfg + 1] = "interface br-lan"
	ucfg[#ucfg + 1] = gfmt("opt dns %s %s",
		lcfg.dns1 or '8.8.8.8', lcfg.dns2 or '223.5.5.5')
	ucfg[#ucfg + 1] = gfmt("option subnet %s0", dhcp_addrp('255.255.255.255', lcfg.netmask))
	ucfg[#ucfg + 1] = gfmt("opt router %s", lcfg.ipaddr)
	ucfg[#ucfg + 1] = "option lease 7200 # default: 2 hours"
	ucfg[#ucfg + 1] = "pidfile /run/udhcpd.pid"
	ucfg[#ucfg + 1] = gfmt("lease_file %s", lfile)
	ucfg[#ucfg + 1] = "\n"

	ucfg = table.concat(ucfg, "\n")
	if ucfg == invoker.readfile(BDHCPD_CFG) then
		io.stdout:write("INFO: busybox based DHCP server configuration not changed.\n")
		io.stdout:flush()
		invoker.invoke(invoker.NOSTDIO, 'systemctl', g_changed_lan and 'restart' or 'start', BDHCPD_SVR)
		invoker.invoke(invoker.NOSTDIO, 'systemctl', 'enable', BDHCPD_SVR)
		return true
	end

	local filp = io.open(BDHCPD_CFG, "wb")
	if not filp then
		io.stderr:write("Error, failed to open DHCP server config!\n")
		io.stderr:flush()
		return false
	end

	filp:write(ucfg)
	filp:close(); filp = nil

	-- restart DHCP service
	invoker.invoke(invoker.NOSTDIO, 'systemctl', 'enable', BDHCPD_SVR)
	invoker.invoke(invoker.NOSTDIO, 'systemctl', 'restart', BDHCPD_SVR)
	return true
end

local function stop_dhcp_server()
	invoker.invoke(invoker.NOSTDIO, 'systemctl', 'stop', BDHCPD_SVR)
	invoker.invoke(invoker.NOSTDIO, 'systemctl', 'disable', BDHCPD_SVR)
	posix.unlink(BDHCPD_CFG); posix.sync()
	return true
end

-- check whether there is a network device used more than once
local function filter_netdev(wcfgs, lcfg)
	local nlist, okay = {}, true
	for _, wcfg in pairs(wcfgs) do
		local netdev = wcfg["netdev"]
		if type(netdev) ~= "string" then
			io.stderr:write(gfmt("Error, invalid type of network device: %s\n",
				type(netdev)))
			io.stderr:flush()
		elseif not wcfg['disabled'] and nlist[netdev] then
			okay = false
			io.stderr:write(gfmt("Error, network device used twice: %s\n", netdev))
			io.stderr:flush()
		elseif not wcfg['disabled'] then
			nlist[netdev] = true -- wcfg["iface"]
		end
	end

	local lndevs = type(lcfg) == "table" and lcfg["netdevs"] or nil
	if type(lndevs) == "table" then
		for _, ndev in ipairs(lndevs) do
			local wcfg = wcfgs[ndev]
			if nlist[ndev] and type(wcfg) == "table" and not wcfg['disabled'] then
				okay = false
				io.stderr:write(gfmt("Error, network device used many times: %s\n", ndev))
				io.stderr:flush()
			else nlist[ndev] = true end
		end
	end

	nlist = nil
	return okay
end

local function load_wancfg(uhdl, name)
	local iface = uhdl:get('network', name)
	if type(iface) ~= "string" or iface ~= "interface" then
		-- io.stderr:write(gfmt("INFO: WAN interface not found: %s\n", name))
		-- io.stderr:flush()
		return nil
	end

	local netdev = uhdl:get('network', name, 'device')
	if not has_netdev(netdev) then
		io.stderr:write(gfmt("Warning: WAN device not found for %s\n", name))
		io.stderr:flush()
		return nil
	end

	local proto = uhdl:get('network', name, 'proto')
	if not check_proto(proto) then
		io.stderr:write(gfmt("ERROR: invalid protocol for %s/%s\n", name, netdev))
		io.stderr:flush()
		return nil
	end

	local is_dhcpv6 = false
	local enable = uhdl:get('network', name, 'dhcpv6')
	if type(enable) == "number" and enable == 1 then
		is_dhcpv6 = true
	elseif type(enable) == "string" and enable == "1" then
		is_dhcpv6 = true
	end

	local disabled = false -- interface disabled ?
	local tmpval = uhdl:get('network', name, 'disabled')
	if type(tmpval) == "number" and tmpval == 1 then
		disabled = true
	elseif type(tmpval) == "string" and tmpval == "1" then
		disabled = true
	end

	tmpval = uhdl:get('network', name, 'macaddr')
	local macaddr = check_macaddr(tmpval)
	if tmpval and not macaddr then
		io.stderr:write(gfmt("ERROR: invalid macaddr for %s/%s", name, netdev))
		io.stderr:flush()
		return nil
	end

	local metric = 0 -- if not specified, default to zero
	tmpval = uhdl:get('network', name, 'metric')
	if type(tmpval) == "string" then tmpval = tonumber(tmpval) end
	if type(tmpval) == "number" and tmpval >= 0 then
		metric = math.floor(tmpval)
	end

	local dns1, dns2 = nil, nil
	local dns = uhdl:get('network', name, 'dns')
	if type(dns) == "table" then
		if invoker.isipv4(dns[1]) then dns1 = dns[1] end
		if invoker.isipv4(dns[2]) then dns2 = dns[2] end
	end

	local wcfg = { ["iface"] = name, ["netdev"] = netdev, ["macaddr"] = macaddr,
		["proto"] = proto, ["disabled"] = disabled, ["metric"] = metric,
		dns1 = dns1, dns2 = dns2, ["dhcpv6"] = is_dhcpv6 }
	if proto == "dhcp" then return wcfg end

	local ipaddr = uhdl:get('network', name, 'ipaddr')
	if type(ipaddr) ~= "string" or string.len(ipaddr) == 0 then
		io.stderr:write(gfmt("ERROR: invalid static IP address for %s/%s\n", name, netdev))
		io.stderr:flush()
		return nil
	end
	if not invoker.isipv4(ipaddr) then
		io.stderr:write(gfmt("ERROR: invalid static IP address: %s\n", ipaddr))
		io.stderr:flush()
		return nil
	end

	local netmask = uhdl:get('network', name, 'netmask')
	netmask = invoker.ismaskv4(netmask)
	if not netmask then
		io.stderr:write(gfmt("Warning, invalid netmask specified for %s/%s, using default 255.255.255.0\n", name, netdev))
		io.stderr:flush()
		netmask = invoker.ismaskv4('255.255.255.0')
	end

	local gwip = uhdl:get('network', name, 'gateway')
	if type(gwip) ~= "string" then
		gwip = nil
	elseif not invoker.isipv4(gwip) then
		io.stderr:write(gfmt("Error, invalid gateway IP address for %s/%s\n", name, netdev))
		io.stderr:flush()
		return nil
	end

	wcfg["ipaddr"] = ipaddr
	wcfg["netmask"] = netmask
	wcfg["gateway"] = gwip
	return wcfg
end

local function load_wancfgs(path)
	if not path then path = '/etc/config' end

	local xuci = uci.cursor(path)
	local ifaces = { 'wan', 'wan1', 'wan2', 'wan3', 'wan4', 'wwan' }

	local wcfgs, count = {}, 0
	for _, iface in ipairs(ifaces) do
		local cfg = load_wancfg(xuci, iface)
		if cfg then
			count = count + 1
			wcfgs[iface] = cfg
		end
	end

	xuci:close()
	xuci = nil
	if count > 0 then return wcfgs end
	return nil
end

local function load_lancfg(path)
	if not path then path = '/etc/config' end

	local landev = '@device[0]'
	local xuci = uci.cursor(path)
	local ltype = xuci:get('network', landev, 'type')
	if ltype ~= "bridge" then
		local l_type = type(ltype)
		if l_type ~= "string" then ltype = l_type end
		io.stderr:write(gfmt("Error, invalid br-lan type: %s\n", ltype))
		io.stderr:flush()
		xuci:close(); xuci = nil
		return nil
	end

	local ports = xuci:get('network', landev, 'ports')
	if type(ports) ~= "table" or #ports == 0 then
		ports = {}
		io.stderr:write("Warning, no ports specified for br-lan\n")
		io.stderr:flush()
		-- xuci:close(); xuci = nil
		-- return nil
	end

	local ndevs, idx = {}, 0
	while true do
		idx = idx + 1
		local nports = ports[idx]
		if type(nports) ~= "string" then break end
		for port in string.gmatch(nports, "[^%s]+") do
			if has_netdev(port) then
				ndevs[#ndevs + 1] = port
			else
				io.stderr:write(gfmt("Error, network device not found for br-lan: %s\n", port))
				io.stderr:flush()
			end
		end
	end

	if #ndevs == 0 then
		io.stderr:write("Warning, no valid network device found for br-lan.\n")
		io.stderr:flush()
		-- xuci:close(); xuci = nil
		-- return nil
	end

	local tmpval = xuci:get('network', 'lan', 'device')
	if tmpval ~= "br-lan" then
		io.stderr:write("Error, invalid device specified for lan.\n")
		io.stderr:flush()
		xuci:close(); xuci = nil
		return nil
	end

	tmpval = xuci:get('network', 'lan', 'proto')
	if tmpval ~= "static" then
		io.stderr:write("Error, invalid protocol for lan, must be static.\n")
		io.stderr:flush()
		xuci:close(); xuci = nil
		return nil
	end

	local ipaddr = xuci:get('network', 'lan', 'ipaddr')
	if not invoker.isipv4(ipaddr) then
		io.stderr:write("Error, invalid IPv4 address specified for lan.\n")
		io.stderr:flush()
		xuci:close(); xuci = nil
		return nil
	end

	local netmask = xuci:get('network', 'lan', 'netmask')
	netmask = invoker.ismaskv4(netmask)
	if not netmask or netmask > 24 then
		io.stderr:write("Error, invalid netmask specified for lan.\n")
		io.stderr:flush()
		xuci:close(); xuci = nil
		return nil
	end

	local dns1, dns2 = nil, nil
	local dns = xuci:get('network', 'lan', 'dns')
	if type(dns) == "table" then
		if invoker.isipv4(dns[1]) then dns1 = dns[1] end
		if invoker.isipv4(dns[2]) then dns2 = dns[2] end
	end

	local disabled = xuci:get('network', 'lan', 'disabled')
	if disabled == "1" or disabled == 1 then disabled = true else disabled = false end

	local langw, lanmtr = xuci:get('network', 'lan', 'gateway'), xuci:get('network', 'lan', 'metric')
	if invoker.isipv4(langw) then
		if type(lanmtr) == "string" and #lanmtr > 0 then lanmtr = tonumber(lanmtr) end
		if type(lanmtr) == "number" and lanmtr >= 0 then
			lanmtr = math.floor(lanmtr)
		else
			lanmtr = 200
		end
	else
		langw, lanmtr = nil, 999
	end

	local tmpval = xuci:get('network', 'lan', 'macaddr')
	local macaddr = check_macaddr(tmpval)
	if tmpval and not macaddr then
		io.stderr:write(gfmt("ERROR: invalid macaddr for br-lan: %s\n", tmpval))
		io.stderr:flush()
	end

	xuci:close(); xuci = nil
	return { ["ipaddr"] = ipaddr, ["netmask"] = netmask, dns1 = dns1, dns2 = dns2,
		["netdevs"] = ndevs, ["disabled"] = disabled, ["gateway"] = langw, ["metric"] = lanmtr, ["macaddr"] = macaddr }
end

local function load_wificfg(xuci, iface)
	local dis = xuci:get('wireless', iface, 'disabled')
	if dis == "1" or dis == 1 then
		dis = true
	else
		dis = false
	end

	local netw = xuci:get('wireless', iface, 'network')
	if netw ~= 'lan' and netw ~= 'wifiwan' then
		if type(netw) == "string" then
			io.stderr:write(gfmt("Error, invalid network option for wireless '%s'\n", iface))
			io.stderr:flush()
		end
		return nil
	end

	local mode = xuci:get('wireless', iface, 'mode')
	if (netw == 'lan' and mode ~= 'ap') or (netw == 'wifiwan' and mode ~= 'sta') then
		io.stderr:write(gfmt("Error, unsupported mode for wireless '%s'\n", iface))
		io.stderr:flush()
		return nil
	end

	local ssid = xuci:get('wireless', iface, 'ssid')
	if type(ssid) ~= "string" or string.len(ssid) == 0 then
		io.stderr:write(gfmt("Error, SSID option not specified for '%s'\n", iface))
		io.stderr:flush()
		return nil
	end

	local enc = xuci:get('wireless', iface, 'encryption')
	if type(enc) ~= "string" then
		io.stderr:write(gfmt("Error, encryption not specified for SSID '%s'\n", ssid))
		io.stderr:flush()
		return nil
	end

	local chan = xuci:get('wireless', iface, 'channel')
	if type(chan) == "string" then
		if chan == "auto" then chan = 0 else chan = tonumber(chan) end
	end
	if type(chan) ~= "number" then chan = 11 end

	local passwd = nil
	if enc ~= 'none' then
		passwd = xuci:get('wireless', iface, 'key')
		if type(passwd) ~= "string" or #passwd == 0 then
			io.stderr:write(gfmt("Error, invalid password specified for SSID '%s'\n", ssid))
			io.stderr:flush()
			return nil
		end
	end

	return { ['network'] = netw, ['mode'] = mode, ssid = ssid,
		['wifienc'] = enc, ['passwd'] = passwd, ['disabled'] = dis, ['channel'] = chan }
end

local function load_wificfgs(path)
	if not path then path = '/etc/config' end
	local wuci = uci.cursor(path)
	local ifaces = { 'radio0', 'radio1' }

	local wcfgs, count = {}, 0
	for _, iface in ipairs(ifaces) do
		local cfg = load_wificfg(wuci, iface)
		if cfg then
			count = count + 1
			wcfgs[cfg['network']] = cfg
		end
	end

	wuci:close(); wuci = nil
	if count > 0 then return wcfgs end
	return nil
end

local function dump_wancfgs(wcfgs)
	for _, wcfg in pairs(wcfgs) do
		io.stdout:write(gfmt("Interface:      %s =>\n", wcfg.iface))
		io.stdout:write(gfmt("    device:     %s\n", wcfg.netdev))
		io.stdout:write(gfmt("    protocol:   %s\n", wcfg.proto))
		io.stdout:write(gfmt("    disabled:   %s\n", wcfg.disabled and 'true' or 'false'))
		io.stdout:write(gfmt("    metric:     %d\n", wcfg.metric))
		io.stdout:write(gfmt("    macaddr:    %s\n", wcfg.macaddr or ''))
		io.stdout:write(gfmt("    disabled:   %s\n", wcfg.disabled and '1' or '0'))
		if wcfg.proto == 'static' then
			io.stdout:write(gfmt("    ipaddr:     %s\n", wcfg.ipaddr))
			io.stdout:write(gfmt("    netmask:    %d\n", wcfg.netmask))
			io.stdout:write(gfmt("    gateway:    %s\n", wcfg.gateway or ''))
		end
		io.stdout:write("==============================\n")
		io.stdout:flush()
	end
end

local function config_changed(ucicfg, nmcfg)
	local fields = { "netdev", "macaddr", "proto", "disabled",
		"metric", "dns1", "dns2", "ipaddr", "netmask", "gateway", "dhcpv6" }

	for _, field in ipairs(fields) do
		local uval, nval = ucicfg[field], nmcfg[field]
		if uval ~= nval then
			io.stderr:write(gfmt("INFO: %s has changed for %s: %s <=> %s\n", field,
				ucicfg["iface"], tostring(uval), tostring(nval)))
			io.stderr:flush()
			return true
		end
	end

	if ucicfg['proto'] == 'dhcp' and type(nmcfg['_ipmask']) == "string" then
		return true
	end
	return false
end

local function wifi_bringup(wcfgs, nmcfgs)
	-- load wifiwan configuration from `/etc/config/network
	local nuci = uci.cursor('/etc/config')
	local wanen, wifiwan = true, 'wifiwan'
	local wdev, wdevok = nuci:get('network', wifiwan, 'device'), true
	if type(wdev) ~= "string" or not has_netdev(wdev) then
		if type(wdev) == "string" then
			io.stderr:write(gfmt("Error, wifiwan device not found: %s\n", wdev))
			io.stderr:flush()
		end
		wdevok = false -- wireless network device is not available
	end
	local disabled = nuci:get('network', wifiwan, 'disabled')
	if disabled == "1" or disabled == 1 then
		wanen = false -- wifiwan is disabled
	end

	if wanen and wdevok then -- okay, wifiwan is enabled, we have work to do.
		local metric = nuci:get('network', wifiwan, 'metric')
		if metric == nil then metric = 0 end
		if type(metric) == "string" then metric = tonumber(metric) end
		if type(metric) ~= "number" then
			metric = 30
			io.stderr:write(gfmt("Error, invalid metric value for '%s'\n", wifiwan))
			io.stderr:flush()
		end

		-- proto should be dhcp for wifiwan
		local proto = nuci:get('network', wifiwan, 'proto')
		if proto ~= 'dhcp' then
			io.stderr:write("Error, only dhcp protocol is supported by wifiwan\n")
			io.stderr:flush()
		end

		local wan = wcfgs['wifiwan']
		if type(wan) == "table" and not wan['disabled'] then
			wan['netdev'] = wdev
			wan['metric'] = metric
			nmcli.wifiwan(wan, nmcfgs)
			stop_hostapd_server()
			return true
		end
	end

	if not wdevok then
		stop_hostapd_server()
		down_all_wlan("wlan0", "wlan1", "wlan2")
		return false
	end
	local nmcon = nmcli.BRPFX .. wdev -- connection name of wlan
	local lancfg = nmcfgs[nmcon] -- realtime connection state
	-- setup wireless AP configuration
	local wlan = wcfgs['lan']
	if type(wlan) ~= 'table' or wlan['disabled'] then
		if lancfg then -- previous wlan connection exists
			nmcli.delete(nmcon) -- delete wlan connection
		end
		stop_hostapd_server()
		down_all_wlan("wlan0", "wlan1", "wlan2")
		return true
	end

	wlan['netdev'] = wdev -- set WiFi network device
	-- call nmcli module method for the dirty work
	nmcli.wlan(nmcon, wlan, nmcfgs)
	start_hostapd_server(wlan)
	return true
end

local function mainfunc()
	local wancfgs = load_wancfgs()
	if not wancfgs then
		io.stderr:write("Error, no valid configurations found.\n")
		io.stderr:flush()
		return 1
	end

	local lancfg = load_lancfg()
	local wificfgs = load_wificfgs();
	if not lancfg then
		io.stderr:write("Warning: no br-lan configuration found, disabled\n")
		io.stderr:flush()
	end
	if not filter_netdev(wancfgs, lancfg) then return 2 end
	dump_wancfgs(wancfgs)

	print('***************************************************')
	local nmcfgs = nmcli.getall()
	-- if nmcfgs then dump_wancfgs(nmcfgs) end
	if not nmcfgs then nmcfgs = {} end -- empty list

	-- compare the configurations to detect which one has changed
	for concfg, ccfg in pairs(wancfgs) do
		local changed = true
		local nmcfg, changed = nmcfgs[concfg], true
		if type(nmcfg) == "table" then
			changed = config_changed(ccfg, nmcfg)
		end
		if changed then nmcli.update(ccfg, nmcfg) end
		nmcfgs[concfg] = nil
	end

	-- check and carry out LAN configuration
	if lancfg and not lancfg['disabled'] then
		local _, changed = nmcli.updatelan(lancfg, nmcfgs)
		if changed then g_changed_lan = true end
		start_dhcp_server(lancfg)
	else
		stop_dhcp_server()
	end

	-- determine and setup wifiwan
	if wificfgs then
		wifi_bringup(wificfgs, nmcfgs)
	else
		stop_hostapd_server()
		down_all_wlan("wlan0", "wlan1", "wlan2")
	end

	-- for any extra connections, just DELETION.
	-- this means that you SHOULD delete any connection
	-- you want to keep from `nmcfgs
	for concfg, _ in pairs(nmcfgs) do nmcli.delete(concfg) end
	return 0
end

local function delay_msec(mdelay)
	local tnow = invoker.uptime()
	local hnow = tnow + math.floor(mdelay / 1000) + 1
	while tnow < hnow do
		invoker.waitsec(hnow - tnow)
		tnow = invoker.uptime()
	end
	posix.sync()
end

setup_env()
local rval = mainfunc()
if type(rval) ~= "number" then rval = 1 end
delay_msec(3000)
os.exit(rval)
