/*
 * Copyright 2024 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "lua_common.h"
#include "libserver/maps/map_helpers.h"

/***
 * @module rspamd_ip
 * `rspamd_ip` is a helper module to simplify IP addresses manipulations.
 * @example
local print_octets = function(ip)
        print('Normal order octets:')
        for _,o in ipairs(ip:str_octets()) do
                print(o)
        end
        print('Reversed order octets:')
        for _,o in ipairs(ip:inversed_str_octets()) do
                print(o)
        end
        print('Numeric octets:')
        for _,o in ipairs(ip:to_table()) do
                print(o)
        end
end

local rspamd_ip = require "rspamd_ip"
-- Create ipv4
local ip4 = rspamd_ip.from_string('127.0.0.1')
-- Implicit conversion to string
print(ip4)
-- Numeric version
print(ip4:get_version())
print_octets(ip4)

-- Create a sample ipv6 address
local ip6 = rspamd_ip.from_string('2001:41d0:8:dd9a::100')
print(ip6)
print(ip6:get_version())
print_octets(ip6)
 */

/***
 * @method ip:to_string([pretty=false])
 * Converts valid IP address to string
 * @param {bool} pretty print IP address with port and braces (for IPv6)
 * @return {string or nil} string representation of IP or `nil` if IP is invalid
 */
LUA_FUNCTION_DEF(ip, to_string);
/***
 * @method ip:to_number()
 * Converts valid IP address to number or list of numbers in case of IPv6
 * @return {integer(s) or nil} numeric representation of IP in *host* byte order or `nil` if IP is invalid
 */
LUA_FUNCTION_DEF(ip, to_number);

/***
 * @method ip:to_table()
 * Converts valid IP address to the table of numeric octets
 * @return {table or nil} numeric octets of IP address or `nil` if IP is invalid
 * @example
local ip = rspamd_ip.from_string('127.0.0.1')
for _,o in ipairs(ip:to_table()) do
    print(o)
end
-- Output:
-- 127
-- 0
-- 0
-- 1
 */
LUA_FUNCTION_DEF(ip, to_table);
/***
 * @method ip:str_octets()
 * Converts valid IP address to the table of string octets. The difference from
 * @see ip:to_table() is that this method returns just hex strings for ipv6
 * addresses.
 * @return {table or nil} string octets of IP address or `nil` if IP is invalid
 * @example
local ip = rspamd_ip.from_string('fe80::11')
print(table.concat(ip:str_octets(), "."))
-- Output:
-- f.e.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.1
 */
LUA_FUNCTION_DEF(ip, str_octets);
/***
 * @method ip:inversed_str_octets()
 * Converts valid IP address to the table of string octets in reversed order. The difference from
 * @see ip:to_table() is that this method returns just hex strings for ipv6
 * addresses in reversed order.
 * @return {table or nil} string octets of IP address or `nil` if IP is invalid
 * @example
local ip = rspamd_ip.from_string('fe80::11')
print(table.concat(ip:inversed_str_octets(), "."))
-- Output:
-- 1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f
 */
LUA_FUNCTION_DEF(ip, inversed_str_octets);
/***
 * @function rspamd_ip.from_string(line)
 * Create IP address from its string representation.
 * @param {string} line valid IP address string (either ipv4 or ipv6)
 * @return {ip} new ip object or `nil` if input is invalid
 */
LUA_FUNCTION_DEF(ip, from_string);
/***
 * @method ip:__gc()
 * Automatically destroys IP object.
 */
LUA_FUNCTION_DEF(ip, destroy);
/***
 * @method ip:get_version()
 * Gets numeric version of ip address
 * @return {number} `4` for IPv4 and `6` for IPv6
 */
LUA_FUNCTION_DEF(ip, get_version);
/***
 * @method ip:is_valid()
 * Checks if an IP object is a valid IP address.
 * @return {boolean} `true` if IP is valid and `false` otherwise
 */
LUA_FUNCTION_DEF(ip, is_valid);
/***
 * @method ip:apply_mask(mask)
 * Applies mask to IP address, resetting up to `mask` least significant bits to zero.
 * @param {integer} mask how many bits to reset
 * @return {ip} new IP object with `mask` bits reset
 */
LUA_FUNCTION_DEF(ip, apply_mask);
/***
 * @method ip:__eq(other)
 * Compares two IP addresses
 * @param {ip} other IP to compare
 * @return {boolean} `true` if two objects are the same
 */
LUA_FUNCTION_DEF(ip, equal);
/***
 * @method ip:copy()
 * Performs deep copy of IP address.
 * @return {ip} a fresh copy of IP address
 */
LUA_FUNCTION_DEF(ip, copy);

/**
 * @method ip:get_port()
 * Returns associated port for this IP address
 * @return {number} port number or nil
 */
LUA_FUNCTION_DEF(ip, get_port);
/***
 * @method ip:is_local()
 * Returns true if address is local one
 * @return {boolean} `true` if address is local
 */
LUA_FUNCTION_DEF(ip, is_local);

/***
 * @method ip:less_than(other)
 * Returns true if address is less than other
 * @return {boolean}
 */
LUA_FUNCTION_DEF(ip, less_than);

static const struct luaL_reg iplib_m[] = {
	LUA_INTERFACE_DEF(ip, to_string),
	LUA_INTERFACE_DEF(ip, to_table),
	LUA_INTERFACE_DEF(ip, to_number),
	LUA_INTERFACE_DEF(ip, str_octets),
	LUA_INTERFACE_DEF(ip, inversed_str_octets),
	LUA_INTERFACE_DEF(ip, get_version),
	LUA_INTERFACE_DEF(ip, get_port),
	LUA_INTERFACE_DEF(ip, is_valid),
	LUA_INTERFACE_DEF(ip, apply_mask),
	LUA_INTERFACE_DEF(ip, copy),
	LUA_INTERFACE_DEF(ip, is_local),
	{"tostring", lua_ip_to_string},
	{"totable", lua_ip_to_table},
	{"tonumber", lua_ip_to_number},
	{"__tostring", lua_ip_to_string},
	{"__eq", lua_ip_equal},
	{"__gc", lua_ip_destroy},
	{"__lt", lua_ip_less_than},
	{NULL, NULL}};

static const struct luaL_reg iplib_f[] = {
	LUA_INTERFACE_DEF(ip, from_string),
	{"fromstring", lua_ip_from_string},
	{"fromip", lua_ip_copy},
	{"from_ip", lua_ip_copy},
	{NULL, NULL}};

static struct rspamd_lua_ip *
lua_ip_new(lua_State *L, struct rspamd_lua_ip *old)
{
	struct rspamd_lua_ip *ip, **pip;

	ip = g_malloc0(sizeof(*ip));

	if (old != NULL && old->addr != NULL) {
		ip->addr = rspamd_inet_address_copy(old->addr, NULL);
	}

	pip = lua_newuserdata(L, sizeof(struct rspamd_lua_ip *));
	rspamd_lua_setclass(L, rspamd_ip_classname, -1);
	*pip = ip;


	return ip;
}

struct rspamd_lua_ip *
lua_check_ip(lua_State *L, int pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, rspamd_ip_classname);

	luaL_argcheck(L, ud != NULL, pos, "'ip' expected");
	return ud ? *((struct rspamd_lua_ip **) ud) : NULL;
}

static int
lua_ip_to_table(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);
	unsigned int max, i;
	uint8_t *ptr;

	if (ip != NULL && ip->addr) {
		ptr = rspamd_inet_address_get_hash_key(ip->addr, &max);
		lua_createtable(L, max, 0);

		for (i = 1; i <= max; i++, ptr++) {
			lua_pushinteger(L, *ptr);
			lua_rawseti(L, -2, i);
		}
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_str_octets(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);
	unsigned int max, i;
	uint8_t *ptr;
	int af;
	char numbuf[8];

	if (ip != NULL && ip->addr) {
		af = rspamd_inet_address_get_af(ip->addr);
		ptr = rspamd_inet_address_get_hash_key(ip->addr, &max);
		lua_createtable(L, max * 2, 0);

		for (i = 1; i <= max; i++, ptr++) {
			if (af == AF_INET) {
				rspamd_snprintf(numbuf, sizeof(numbuf), "%d", *ptr);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i);
			}
			else {
				rspamd_snprintf(numbuf,
								sizeof(numbuf),
								"%xd",
								(*ptr & 0xf0) >> 4);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i * 2 - 1);
				rspamd_snprintf(numbuf, sizeof(numbuf), "%xd", *ptr & 0x0f);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i * 2);
			}
		}
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_inversed_str_octets(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);
	unsigned int max, i;
	uint8_t *ptr;
	char numbuf[4];
	int af;

	if (ip != NULL && ip->addr) {
		ptr = rspamd_inet_address_get_hash_key(ip->addr, &max);
		af = rspamd_inet_address_get_af(ip->addr);
		lua_createtable(L, max * 2, 0);

		ptr += max - 1;
		for (i = 1; i <= max; i++, ptr--) {
			if (af == AF_INET) {
				rspamd_snprintf(numbuf, sizeof(numbuf), "%d", *ptr);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i);
			}
			else {
				rspamd_snprintf(numbuf, sizeof(numbuf), "%xd", *ptr & 0x0f);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i * 2 - 1);
				rspamd_snprintf(numbuf,
								sizeof(numbuf),
								"%xd",
								(*ptr & 0xf0) >> 4);
				lua_pushstring(L, numbuf);
				lua_rawseti(L, -2, i * 2);
			}
		}
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_to_string(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip != NULL && ip->addr) {
		if (lua_isboolean(L, 2) && lua_toboolean(L, 2) == true) {
			lua_pushstring(L, rspamd_inet_address_to_string_pretty(ip->addr));
		}
		else {
			lua_pushstring(L, rspamd_inet_address_to_string(ip->addr));
		}
	}
	else {
		luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_ip_get_port(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip != NULL && ip->addr) {
		lua_pushinteger(L, rspamd_inet_address_get_port(ip->addr));
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_from_string(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip;
	const char *ip_str;
	gsize len;

	ip_str = luaL_checklstring(L, 1, &len);
	if (ip_str) {
		ip = lua_ip_new(L, NULL);

		if (!rspamd_parse_inet_address(&ip->addr,
									   ip_str, len, RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
			msg_warn("cannot parse ip: %*s", (int) len, ip_str);
			ip->addr = NULL;
		}
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_to_number(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);
	uint32_t c;
	unsigned int max, i;
	unsigned char *ptr;

	if (ip != NULL && ip->addr) {
		ptr = rspamd_inet_address_get_hash_key(ip->addr, &max);

		for (i = 0; i < max / sizeof(c); i++) {
			memcpy(&c, ptr + i * sizeof(c), sizeof(c));
			lua_pushinteger(L, ntohl(c));
		}

		return max / sizeof(c);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}


static int
lua_ip_destroy(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip) {
		if (ip->addr) {
			rspamd_inet_address_free(ip->addr);
		}
		g_free(ip);
	}

	return 0;
}

static int
lua_ip_get_version(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip && ip->addr) {
		lua_pushinteger(L, rspamd_inet_address_get_af(ip->addr) == AF_INET6 ? 6 : 4);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_is_valid(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip) {
		lua_pushboolean(L, ip->addr != NULL);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_apply_mask(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1), *nip;
	int mask;

	mask = lua_tonumber(L, 2);
	if (mask > 0 && ip != NULL && ip->addr) {
		nip = lua_ip_new(L, ip);
		rspamd_inet_address_apply_mask(nip->addr, mask);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_equal(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip1 = lua_check_ip(L, 1),
						 *ip2 = lua_check_ip(L, 2);
	gboolean res = FALSE;

	if (ip1 && ip2 && ip1->addr && ip2->addr) {
		res = rspamd_inet_address_compare(ip1->addr, ip2->addr, TRUE) == 0;
	}

	lua_pushboolean(L, res);

	return 1;
}

static int
lua_ip_copy(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);

	if (ip) {
		lua_ip_new(L, ip);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_is_local(lua_State *L)
{
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1);
	gboolean check_laddrs = TRUE;

	if (ip && ip->addr) {

		if (lua_type(L, 2) == LUA_TBOOLEAN) {
			check_laddrs = lua_toboolean(L, 2);
		}

		if (rspamd_inet_address_is_local(ip->addr)) {
			lua_pushboolean(L, true);

			return 1;
		}
		else if (check_laddrs) {
			struct rspamd_radix_map_helper *local_addrs =
				rspamd_inet_library_get_lib_ctx();
			if (local_addrs) {
				if (rspamd_match_radix_map_addr(local_addrs, ip->addr) != NULL) {
					lua_pushboolean(L, true);

					return 1;
				}
			}
		}

		lua_pushboolean(L, false);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_ip_less_than(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_ip *ip = lua_check_ip(L, 1),
						 *other = lua_check_ip(L, 2);

	if (ip && other) {
		lua_pushboolean(L,
						rspamd_inet_address_compare(ip->addr, other->addr, true) < 0);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

void rspamd_lua_ip_push(lua_State *L, rspamd_inet_addr_t *addr)
{
	struct rspamd_lua_ip *ip, **pip;

	if (addr) {
		ip = g_malloc0(sizeof(struct rspamd_lua_ip));
		ip->addr = rspamd_inet_address_copy(addr, NULL);
		pip = lua_newuserdata(L, sizeof(struct rspamd_lua_ip *));
		rspamd_lua_setclass(L, rspamd_ip_classname, -1);
		*pip = ip;
	}
	else {
		lua_pushnil(L);
	}
}

void rspamd_lua_ip_push_fromstring(lua_State *L, const char *ip_str)
{
	struct rspamd_lua_ip *ip, **pip;

	if (ip_str == NULL) {
		lua_pushnil(L);
	}
	else {
		ip = g_malloc0(sizeof(struct rspamd_lua_ip));

		if (rspamd_parse_inet_address(&ip->addr,
									  ip_str, strlen(ip_str), RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {

			pip = lua_newuserdata(L, sizeof(struct rspamd_lua_ip *));
			rspamd_lua_setclass(L, rspamd_ip_classname, -1);
			*pip = ip;
		}
		else {
			g_free(ip);
			lua_pushnil(L);
		}
	}
}

static int
lua_load_ip(lua_State *L)
{
	lua_newtable(L);
	luaL_register(L, NULL, iplib_f);

	return 1;
}

void luaopen_ip(lua_State *L)
{
	rspamd_lua_new_class(L, rspamd_ip_classname, iplib_m);
	lua_pop(L, 1);
	rspamd_lua_add_preload(L, "rspamd_ip", lua_load_ip);
}
