--
-- (C) 2020-24 - ntop.org
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/pools/?.lua;" .. package.path

require "ntop_utils"
local json = require("dkjson")

-- ##############################################

local radius_handler = {}
local session_id_length = 32
local redis_accounting_key = "ntopng.radius.accounting.%s"

local function get_first_ip(mac)
    -- Get the first ip used by the mac
    local first_host = {}
    local mac_hosts = interface.getMacHosts(mac) or {}
    if table.len(mac_hosts) > 0 then
        for _, h in pairsByKeys(mac_hosts, asc) do
            first_host = h
            break
        end
    end

    local ip_address = first_host["ip"]
    if isEmptyString(ip_address) then
        ip_address = nil
    end

    return ip_address
end

-- ##############################################

---@brief Handles the Radius accounting start request
---@param name string, used to identify the the user logged in
---@param username string, used to login the account to radius
---@param password string, used to login the account to radius
---@return boolean, true if the accounting start went well, false otherwise
function radius_handler.accountingStart(name, username, password)
    if not radius_handler.isAccountingEnabled() then
        return true
    end
    name = string.upper(name)

    -- Check if the user is already saved on redis
    local is_accounting_on = radius_handler.isAccountingRequested(name)

    -- In case the info are already on redis, means that the  system is restarted
    -- or the same request has been done twice, so just skip this
    if is_accounting_on then
        return true
    end

    traceError(TRACE_NORMAL, TRACE_CONSOLE,
        string.format("Accounting start requested for MAC [%s] with username [%s]", name, username))

    local session_id = tostring(math.random(100000000000000000, 999999999999999999))
    local ip_address = get_first_ip(name)
    local current_time = os.time()
    math.randomseed(current_time)
    local accounting_started = interface.radiusAccountingStart(username --[[ Username ]] , name --[[ MAC Address ]] ,
        session_id, ip_address --[[ First IP Address ]] , current_time)
    if accounting_started then
        local key = string.format(redis_accounting_key, name)
        local user_data = {
            name = name,
            username = username,
            password = password,
            session_id = session_id,
            start_session_time = current_time,
            ip_address = ip_address,
            bytes_sent = 0,
            bytes_rcvd = 0,
            packets_sent = 0,
            packets_rcvd = 0
        }

        ntop.setCache(key, json.encode(user_data))
    end

    return true
end

-- ##############################################

-- @brief Handles the Radius accounting stop request
---@param name string, used to check if name is an accounting going on
---@return boolean, true if the accounting went well, false otherwise and the stop was called
function radius_handler.accountingStop(name, terminate_cause, info)
    if not radius_handler.isAccountingEnabled() then
        return true
    end
    name = string.upper(name)

    local _, user_data = radius_handler.isAccountingRequested(name)
    -- Removing the entry from redis
    ntop.delCache(string.format(redis_accounting_key, name))

    traceError(TRACE_NORMAL, TRACE_CONSOLE, string.format("Accounting stop requested for MAC [%s]", name))

    -- Check in case no user_data is found
    if user_data then
        local bytes_sent = 0
        local bytes_rcvd = 0
        local packets_sent = 0
        local packets_rcvd = 0
        local current_time = os.time()
        local ip_address = ""
        if user_data and not isEmptyString(user_data.ip_address) then
            ip_address = user_data.ip_address
        else
            ip_address = get_first_ip(name)
            if not isEmptyString(ip_address) then
                user_data.ip_address = ip_address
            end
        end

        bytes_sent = user_data.bytes_sent or 0
        bytes_rcvd = user_data.bytes_rcvd or 0
        packets_sent = user_data.packets_sent or 0
        packets_rcvd = user_data.packets_rcvd or 0

        if info then
            -- These are in B, needs to be converted in KB
            if (info["bytes.sent"] or 0) > 0 then
                bytes_sent = math.floor((info["bytes.sent"] or 0) / 1024 + 0.5)
            end
            if (info["bytes.rcvd"] or 0) > 0 then
                bytes_rcvd = math.floor((info["bytes.rcvd"] or 0) / 1024 + 0.5)
            end
    
            if (info["packets.sent"] or 0) > 0 then
                packets_sent = info["packets.sent"] or 0
            end
            if (info["packets.rcvd"] or 0) > 0 then
                packets_rcvd = info["packets.rcvd"] or 0
            end
        end

        interface.radiusAccountingStop(user_data.username --[[ Username ]] , user_data.session_id, name --[[ MAC Address]] ,
            ip_address, bytes_sent, bytes_rcvd, packets_sent, packets_rcvd, terminate_cause,
            current_time - user_data.start_session_time)
    end
end

-- ##############################################

-- @brief Execute the accounting update if the name has to be updated
---@param name string, used to check if name is an accounting going on
---@return boolean, true if the accounting went well, false otherwise and the stop was called
function radius_handler.accountingUpdate(name, info)
    if not radius_handler.isAccountingEnabled() then
        return true
    end
    name = string.upper(name)

    local is_accounting_on, user_data = radius_handler.isAccountingRequested(name)
    local res = true

    if is_accounting_on and user_data then
        local key = string.format(redis_accounting_key, name)
        local ip_address
        if user_data and not isEmptyString(user_data.ip_address) then
            ip_address = user_data.ip_address
        else
            ip_address = get_first_ip(name)
            if not isEmptyString(ip_address) then
                -- Update the info with the ip_address if it's not empty
                user_data.ip_address = ip_address
            end
        end
        local current_time = os.time()
        local bytes_sent = user_data.bytes_sent or 0
        local bytes_rcvd = user_data.bytes_rcvd or 0
        local packets_sent = user_data.packets_sent or 0
        local packets_rcvd = user_data.packets_rcvd or 0

        -- These are in B, needs to be converted in KB
        if info and (info["bytes.sent"]) > 0 then
            bytes_sent = math.floor((info["bytes.sent"]) / 1024 + 0.5)
        end
        if info and (info["bytes.rcvd"]) > 0 then
            bytes_rcvd = math.floor((info["bytes.rcvd"]) / 1024 + 0.5)
        end

        if info and (info["packets.sent"]) > 0 then
            packets_sent = info["packets.sent"]
        end
        if info and (info["packets.rcvd"]) > 0 then
            packets_rcvd = info["packets.rcvd"]
        end

        traceError(TRACE_NORMAL, TRACE_CONSOLE,
            string.format("Accounting update [MAC: %s][In Bytes: %d][Out Bytes: %d][Radius In Bytes: %d][Radius Out Bytes: %d]", name, info["bytes.rcvd"], info["bytes.sent"], bytes_rcvd, bytes_sent))

        user_data.bytes_sent = bytes_sent
        user_data.bytes_rcvd = bytes_rcvd
        user_data.packets_sent = packets_sent
        user_data.packets_rcvd = packets_rcvd

        ntop.setCache(key, json.encode(user_data))

        interface.radiusAccountingUpdate(name, user_data.session_id, user_data.username, user_data.password, ip_address,
            bytes_sent, bytes_rcvd, packets_sent, packets_rcvd, current_time - user_data.start_session_time)
    end

    return res
end

-- ##############################################

-- @brief Check if name has an accounting going on
---@param name string, used to check if name is an accounting going on
---@return boolean, in case the accounting is up or not
---@return table, containing the user data in case an accounting is up
function radius_handler.isAccountingRequested(name)
    name = string.upper(name)
    local key = string.format(redis_accounting_key, name)
    local user_data = ntop.getCache(key)

    if not isEmptyString(user_data) then
        return true, json.decode(user_data)
    end

    return false, nil
end

-- ##############################################

function radius_handler.isAccountingEnabled()
    local accounting_enabled = ntop.getPref("ntopng.prefs.radius.accounting_enabled")
    if (not accounting_enabled) or (isEmptyString(accounting_enabled) or (accounting_enabled == "0")) then
        return false
    end

    return true
end

-- ##############################################

function radius_handler.getAllKeys()
    local keys = ntop.getKeysCache("ntopng.radius.accounting.*")
    if keys then
        require "lua_utils"
        local prefix_to_remove = "ntopng.radius.accounting."
        local characters_to_remove = string.len(prefix_to_remove)
        local new_keys = {}
        for member, _ in pairs(keys) do
            local mac_address = member:gsub(prefix_to_remove, "")
            if isMacAddress(mac_address) then
                new_keys[#new_keys + 1] = mac_address
            end
        end
        keys = new_keys
    end
    return keys or {}
end

-- ##############################################

function radius_handler.deleteAllKeys()
    ntop.delCache("ntopng.radius.accounting.*")
end

-- ##############################################

return radius_handler

-- ##############################################
