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

require "lua_utils"
local json = require "dkjson"
local dscp_consts = require "dscp_consts"
local flow_risk_utils = require "flow_risk_utils"
local alert_utils = require "alert_utils"

local historical_flow_details_formatter = {}

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

local function empty_port(port) return port == '0' end

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

local function empty_ip(ip) return ip == '0.0.0.0' end

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

-- This function format info regarding pre/post nat ips and ports
local function format_pre_post_nat_info(flow, info)
    local tmp = {}
    local nat_values = {}

    -- Checking empty values
    -- Checking IPs
    if (not isEmptyString(info["PRE_NAT_IPV4_SRC_ADDR"]) and
        not empty_ip(info["PRE_NAT_IPV4_SRC_ADDR"])) then
        nat_values.pre_nat_src_ip = info["PRE_NAT_IPV4_SRC_ADDR"]
    end
    if (not isEmptyString(info["POST_NAT_IPV4_SRC_ADDR"]) and
        not empty_ip(info["POST_NAT_IPV4_SRC_ADDR"])) then
        nat_values.post_nat_src_ip = info["POST_NAT_IPV4_SRC_ADDR"]
    end
    if (not isEmptyString(info["PRE_NAT_IPV4_DST_ADDR"]) and
        not empty_ip(info["PRE_NAT_IPV4_DST_ADDR"])) then
        nat_values.pre_nat_dst_ip = info["PRE_NAT_IPV4_DST_ADDR"]
    end
    if (not isEmptyString(info["POST_NAT_IPV4_DST_ADDR"]) and
        not empty_ip(info["POST_NAT_IPV4_DST_ADDR"])) then
        nat_values.post_nat_dst_ip = info["POST_NAT_IPV4_DST_ADDR"]
    end
    -- Checking ports
    if (not isEmptyString(info["PRE_NAT_SRC_PORT"]) and
        not empty_port(info["PRE_NAT_SRC_PORT"])) then
        nat_values.pre_nat_src_port = info["PRE_NAT_SRC_PORT"]
    end
    if (not isEmptyString(info["POST_NAT_SRC_PORT"]) and
        not empty_port(info["POST_NAT_SRC_PORT"])) then
        nat_values.post_nat_src_port = info["POST_NAT_SRC_PORT"]
    end
    if (not isEmptyString(info["PRE_NAT_DST_PORT"]) and
        not empty_port(info["PRE_NAT_DST_PORT"])) then
        nat_values.pre_nat_dst_port = info["PRE_NAT_DST_PORT"]
    end
    if (not isEmptyString(info["POST_NAT_DST_PORT"]) and
        not empty_port(info["POST_NAT_DST_PORT"])) then
        nat_values.post_nat_dst_port = info["POST_NAT_DST_PORT"]
    end

    -- No Post-NAT values
    if not nat_values.post_nat_dst_port and not nat_values.post_nat_src_port and
        not nat_values.post_nat_dst_ip and not nat_values.post_nat_src_ip then
        return flow
    end

    -- Substituting empty values
    if not nat_values.post_nat_src_ip then
        nat_values.post_nat_src_ip = nat_values.pre_nat_src_ip
    end

    if not nat_values.post_nat_dst_ip then
        nat_values.post_nat_dst_ip = nat_values.pre_nat_dst_ip
    end

    if not nat_values.post_nat_src_port then
        nat_values.post_nat_src_port = nat_values.pre_nat_src_port
    end

    if not nat_values.post_nat_dst_port then
        nat_values.post_nat_dst_port = nat_values.pre_nat_dst_port
    end

    -- Format all info
    local pre_nat_flow = nat_values.pre_nat_src_ip .. ":" ..
                             nat_values.pre_nat_src_port ..
                             ' <i class="fas fa-exchange-alt fa-lg"></i> ' ..
                             nat_values.post_nat_src_ip .. ":" ..
                             nat_values.post_nat_src_port
    local post_nat_flow = nat_values.post_nat_src_ip .. ":" ..
                              nat_values.post_nat_src_port ..
                              ' <i class="fas fa-exchange-alt fa-lg"></i> ' ..
                              nat_values.post_nat_dst_ip .. ":" ..
                              nat_values.post_nat_dst_port
    flow[#flow + 1] = {
        name = i18n('db_explorer.pre_nat_info'),
        values = {pre_nat_flow}
    }
    flow[#flow + 1] = {
        name = i18n('db_explorer.post_nat_info'),
        values = {post_nat_flow}
    }

    return flow
end

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

local function format_historical_flow_label(flow)
    local historical_flow_utils = require "historical_flow_utils"

    return {
        name = i18n("flow_details.flow_peers_client_server"),
        values = {historical_flow_utils.getHistoricalFlowLabel(flow, true)}
    }
end

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

local function format_historical_protocol_label(flow)
    local historical_flow_utils = require "historical_flow_utils"

    return {
        name = i18n("protocol") .. " / " .. i18n("application"),
        values = {historical_flow_utils.getHistoricalProtocolLabel(flow, true)}
    }
end

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

local function format_historical_last_first_seen(flow, info)
    return {
        name = i18n("db_explorer.date_time"),
        values = {[1] = info.first_seen.time, [2] = info.last_seen.time}
    }
end

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

function historical_flow_details_formatter.format_historical_total_traffic(flow)
    return {
        name = i18n("db_explorer.traffic_info"),
        values = {
            formatPackets(flow['PACKETS'] or flow["packets"]) .. ' / ' ..
                bytesToSize(flow['TOTAL_BYTES'] or flow["total_bytes"])
        }
    }
end

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

function historical_flow_details_formatter.format_qoe(flow)
    local json_info = flow["PROTOCOL_INFO_JSON"]
    if not isEmptyString(json_info) then
        json_info = json.decode(flow["PROTOCOL_INFO_JSON"])
    end

    if json_info and json_info.qoe and ntop.isEnterpriseL() and tonumber(flow.QOE_SCORE) <= 100 then
        qoe_utils = require "qoe_utils"

        return {
            name = i18n("flow_details.qoe_long"),
            values = {
                qoe_utils.formatQoE(json_info.qoe.c2s.score),
                qoe_utils.formatQoE(json_info.qoe.s2c.qoe_score)
            }
        }
    end
end

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

function historical_flow_details_formatter.format_historical_client_server_bytes(
    flow)
    return {
        name = "",
        values = {
            [1] = i18n("client") ..
                " <i class=\"fas fa-long-arrow-alt-right\"></i> " ..
                i18n("server") .. ": " ..
                formatValue(flow['SRC2DST_PACKETS'] or flow["cli2srv_packets"]) ..
                " " .. i18n("pkts") .. " / " ..
                bytesToSize(flow['SRC2DST_BYTES'] or flow["cli2srv_bytes"]),
            [2] = i18n("client") ..
                " <i class=\"fas fa-long-arrow-alt-left\"></i> " ..
                i18n("server") .. ": " ..
                formatValue(flow['DST2SRC_PACKETS'] or flow["srv2cli_packets"]) ..
                " " .. i18n("pkts") .. " / " ..
                bytesToSize(flow['DST2SRC_BYTES'] or flow["srv2cli_bytes"])
        }
    }
end

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

function historical_flow_details_formatter.format_historical_bytes_progress_bar(
    flow, info)
    local cli2srv = round(
                        ((flow["SRC2DST_BYTES"] or flow["cli2srv_bytes"] or 0) *
                            100) / (flow["TOTAL_BYTES"] or flow["total_bytes"]),
                        0)

    return {
        name = "",
        values = {
            '<div class="progress"><div class="progress-bar bg-warning" style="width: ' ..
                cli2srv .. '%;">' .. (info.cli_ip.label or '') .. '</div>' ..
                '<div class="progress-bar bg-success" style="width: ' ..
                (100 - cli2srv) .. '%;">' .. (info.srv_ip.label or '') ..
                '</div></div>'
        }
    }
end

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

local function format_historical_wlan_ssid(flow, info)
    return {
        name = i18n("flow_fields_description.wlan_ssid"),
        values = {info.wlan_ssid.label}
    }
end

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

local function format_historical_wtp_mac_address(flow, info)
    return {
        name = i18n("flow_fields_description.wtp_mac_address"),
        values = {info.apn_mac.label}
    }
end

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

local function format_historical_tos(flow)
    return {
        name = i18n("db_explorer.tos"),
        values = {
            [1] = dscp_consts.dscp_descr(flow['SRC2DST_DSCP']),
            [2] = dscp_consts.dscp_descr(flow['DST2SRC_DSCP'])
        }
    }
end

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

local function format_historical_tcp_flags(flow, info)
    return {
        name = i18n("tcp_flags"),
        values = {
            [1] = i18n("client") ..
                " <i class=\"fas fa-long-arrow-alt-right\"></i> " ..
                i18n("server") .. ": " .. info.src2dst_tcp_flags.label,
            [2] = i18n("client") ..
                " <i class=\"fas fa-long-arrow-alt-left\"></i> " ..
                i18n("server") .. ": " .. info.dst2src_tcp_flags.label
        }
    }
end

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

local function format_historical_host_pool(flow, info)
    return {
        name = i18n("details.host_pool"),
        values = {
            [1] = i18n("client") .. " " .. i18n("pools.pool") .. ": " ..
                info.cli_host_pool_id.label,
            [2] = i18n("server") .. " " .. i18n("pools.pool") .. ": " ..
                info.srv_host_pool_id.label
        }
    }
end

-- a###############################################

local function format_historical_issue_description(alert, alert_id, score,
                                                   title, msg, info,
                                                   alert_scores,
                                                   add_remediation, riskInfo,
                                                   alert_info)
    local alert_consts = require "alert_consts"
    local alert_entities = require "alert_entities"

    if not alert_id or alert_id == "0" then return nil end

    if alert_scores and alert_scores[alert_id] then
        score = alert_scores[alert_id] or 0
    end
    -- If alert risk is 0 then it comes from ntonpg, else nDPI
    local alert_risk = ntop.getFlowAlertRisk(tonumber(alert_id))
    local alert_src
    local riskLabel = ""

    if (tonumber(alert_risk) == 0) then
        alert_src = "ntopng"
        alert_risk = alert_id
        if isEmptyString(msg) and not isEmptyString(info) then msg = info end
        -- Adapting to the new alerts format
        if alert_info and alert then
            alert.alert_id = alert_id
            info = alert_utils.formatFlowAlertMessage(interface.getId(), alert,
                                                      alert_info, false, true,
                                                      true)
        end
    else
        alert_src = "nDPI"
    end

    if riskInfo then
        if type(riskInfo) == "string" then -- backward compatibility
            riskInfo = json.decode(riskInfo)
        end

        if riskInfo and riskInfo[tostring(alert_risk)] then
            riskLabel = riskInfo[tostring(alert_risk)]
        end
    end

    local alert_source = " <span class='badge bg-info'>" .. alert_src ..
                             "</span>"

    local severity_id = map_score_to_severity(score)
    local severity = alert_consts.alertSeverityById(severity_id)
    local remediation = flow_risk_utils.get_remediation_documentation_link(
                            tostring(alert_risk), alert_src)

    local html = "<tr><td>" .. (msg or "") .. alert_source .. "</td>" ..
                     '<td align=center><span style="color:' .. severity.color ..
                     '">' .. score .. '</span></td>'

    if not isEmptyString(riskLabel) then info = riskLabel end
    if (add_remediation) then
        html = html .. "<td>" .. info .. " " .. remediation .. "</td>"
    else
        html = html .. "<td>" .. info .. "</td>"
    end

    -- Add Mitre info
    local alert_key = alert_consts.getAlertType(alert_id,
                                                alert_entities.flow.entity_id)

    if alert_key then
        local mitre_info = alert_consts.getAlertMitreInfo(alert_key)

        if mitre_info and mitre_info.mitre_id then
            local keys = split(mitre_info.mitre_id, "%.")
            local url = "https://attack.mitre.org/techniques/" ..
                            keys[1]:gsub("%%", "") .. "/"

            if keys[2] ~= nil then
                url = url .. keys[2]:gsub("%%", "") .. "/"
            end

            html =
                html .. '<td><a href="' .. url .. '">' .. mitre_info.mitre_id ..
                    "</A>"

            if (mitre_info.mitre_tactic) and
                (mitre_info.mitre_tactic.i18n_label) then
                html = html .. '<br>' ..
                           i18n(mitre_info.mitre_tactic.i18n_label) .. "</td>"
            end
        else
            html = html .. "<td>&nbsp;</td>"
        end
    else
        html = html .. "<td>&nbsp;</td>"
    end

    return html
end

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

function historical_flow_details_formatter.format_historical_issues(
    flow_details, flow, is_alert)
    local historical_flow_utils = require "historical_flow_utils"
    local alert_store_utils = require "alert_store_utils"
    local alert_entities = require "alert_entities"
    local format_utils = require "format_utils"
    local alert_consts = require "alert_consts"
    local alert_utils = require "alert_utils"
    local alert_store_instances = alert_store_utils.all_instances_factory()
    local alert_json = json.decode(flow["ALERT_JSON"] or flow["json"] or '') or
                           {}
    local details = ""
    local alert
    local riskInfo = {}
    local alerts_map = flow['ALERTS_MAP'] or flow["alerts_map_l"] or ""
    local score = tonumber(flow["SCORE"]) or tonumber(flow["score"]) or 0
    local alert_scores = {}
    local alert_id = tonumber(flow["STATUS"] or flow["alert_id"] or 0)
    local html =
        "<table class=\"table table-bordered table-striped\" width=100%>\n" ..
            "<tr><th>" .. i18n("description") .. "</th><th>" .. i18n("score") ..
            "</th><th>" .. i18n("info") .. " / " .. i18n("remediation") ..
            "</th><th>" .. i18n("mitre_id") .. "</th></tr>\n"
    local alert_store_instance = alert_store_instances[alert_entities["flow"]
                                     .alert_store_name]
    local main_alert_score

    if not is_alert then
        alert = historical_flow_utils.convertFlowToAlert(flow)
    else
        alert = flow
    end
    if score > 0 then
        details = alert_utils.formatFlowAlertMessage(interface.getId(), alert,
                                                     nil, false, true, true)
    end

    if alert_json and alert_json.flow_risk_info then
        riskInfo = alert_json.flow_risk_info
    elseif alert_json and alert_json.alert_generation and
        alert_json.alert_generation.flow_risk_info then
        -- Keep the code divided due to optimizations
        riskInfo = alert_json.alert_generation.flow_risk_info
    end

    if alert_json and alert_json.alerts then
        for alert_id, values in pairs(alert_json.alerts or {}) do
            alert_scores[alert_id] = values.score
        end
    else
        local alert_label = i18n("flow_details.normal")
        alert_scores = alert_json.alert_score
        main_alert_score = ntop.getFlowAlertScore(tonumber(alert_id))

        -- No status set
        if (alert_id ~= 0) then
            alert_label = alert_consts.alertTypeLabel(alert_id, true)
            html = html ..
                       format_historical_issue_description(alert,
                                                           tostring(alert_id),
                                                           tonumber(
                                                               main_alert_score),
                                                           i18n("issues_score"),
                                                           alert_label, details,
                                                           alert_scores, true,
                                                           riskInfo)
        end
    end

    -- Check if there is a custom score
    if alert_scores and alert_scores[tostring(alert_id)] then
        main_alert_score = alert_scores[tostring(alert_id)]
    end

    local severity_id = map_score_to_severity(main_alert_score)
    local severity = alert_consts.alertSeverityById(severity_id)
    local _, other_issues = alert_utils.format_other_alerts(alerts_map,
                                                            alert_id,
                                                            alert_json, false,
                                                            nil, true)

    flow_details[#flow_details + 1] = {
        name = i18n('total_flow_score'),
        values = {
            '<span style="color:' .. severity.color .. '">' ..
                format_utils.formatValue(score) .. '</span>', ''
        }
    }

    if table.len(other_issues) > 0 then
        for _, issue in pairsByField(other_issues or {}, "score", rev) do
            local msg, info
            local pieces = string.split(issue.msg, "%[")

            if (pieces ~= nil) then
                msg = pieces[1]
                info = string.gsub(pieces[2], "%]", "")
            else
                msg = issue.msg
                info = ""
            end
            local alert_info = nil
            if alert_json and alert_json.alerts then
                alert_info = alert_json.alerts[tostring(issue.alert_id)]
            end
            html = html ..
                       format_historical_issue_description(alert, tostring(
                                                               issue.alert_id),
                                                           tonumber(issue.score),
                                                           '', msg, info,
                                                           alert_scores, true,
                                                           riskInfo, alert_info)
        end
    end

    flow_details[#flow_details + 1] = {
        name = i18n('detected_issues'),
        values = {html}
    }

    return flow_details
end

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

local function format_tcp_connection_states(info)
    local conn_states = {}
    conn_states[#conn_states + 1] = string.format("%s: %s (%s)", i18n(
                                                      "flow_fields_description.major_connection_state"),
                                                  i18n(
                                                      string.format(
                                                          "flow_fields_description.major_connection_states.%s",
                                                          info.major_connection_state
                                                              .value)),
                                                  i18n(
                                                      string.format(
                                                          "flow_fields_description.minor_connection_states_info.%u",
                                                          info.minor_connection_state
                                                              .value)))
    conn_states[#conn_states + 1] = string.format("%s: %s (%s)", i18n(
                                                      "flow_fields_description.minor_connection_state"),
                                                  i18n(
                                                      string.format(
                                                          "flow_fields_description.minor_connection_states.%s",
                                                          info.minor_connection_state
                                                              .value)),
                                                  i18n(
                                                      string.format(
                                                          "flow_fields_description.minor_connection_states_info.%u",
                                                          info.minor_connection_state
                                                              .value)))
    return conn_states
end

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

local function format_historical_community_id(flow)
    return {
        name = "<A class='ntopng-external-link' href=\"https://github.com/corelight/community-id-spec\">" ..
            i18n("db_explorer.community_id") ..
            " <i class=\"fas fa-external-link-alt\"></i></A>",
        values = {
            flow["COMMUNITY_ID"] ..
                "<button style=\"\" class=\"btn btn-sm border ms-1\" data=\"" ..
                flow["COMMUNITY_ID"] ..
                "\" onclick=\"NtopUtils.copyToClipboard(this.getAttribute('data'), '" ..
                i18n('copied') .. "', '" .. i18n('request_failed_message') ..
                "', this)\">" .. "<i class=\"fas fa-copy\"></i></button>"
        }
    }
end

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

local function add_info_field(flow)
    local protocol_info_json = json.decode(flow["PROTOCOL_INFO_JSON"] or '') or
                                   {}
    local proto_details = {}
    local add_info = true
    if table.len(protocol_info_json) >= 1 then
        for proto, info in pairs(protocol_info_json["proto"] or {}) do
            if proto == "tls" then
                add_info = isEmptyString(info.client_requested_server_name)
                break
            elseif proto == "dns" then
                add_info = isEmptyString(info.last_query)
                break
            elseif proto == "http" then
                add_info = isEmptyString(info.last_url)
                break
            elseif proto == "icmp" then
                -- Alwais add for icmp
                break
            end
        end
    end

    return add_info
end

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

local function format_historical_info(flow)
    local historical_flow_utils = require "historical_flow_utils"
    local info_field = historical_flow_utils.get_historical_url(flow["INFO"],
                                                                "info",
                                                                flow["INFO"],
                                                                true,
                                                                flow["INFO"],
                                                                true)

    return {name = i18n("db_explorer.info"), values = {info_field}}
end

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

local function format_historical_probe(flow_details, flow, info)
    local historical_flow_utils = require "historical_flow_utils"
    local format_utils = require "format_utils"

    local alias = getFlowDevAlias(info["probe_ip"]["value"], true)
    local name

    if alias == info["probe_ip"]["value"] then
        name = format_name_value(info["probe_ip"]["value"],
                                 info["probe_ip"]["label"], true)
    else
        name = alias
    end

    local info_field = {
        device_ip = historical_flow_utils.get_historical_url(name, "probe_ip",
                                                             info["probe_ip"]["value"],
                                                             true,
                                                             info["probe_ip"]["title"])
    }

    if (flow["INPUT_SNMP"]) and (tonumber(flow["INPUT_SNMP"]) ~= 0) then
        info_field["input_interface"] =
            historical_flow_utils.get_historical_url(
                format_utils.formatSNMPInterface(flow["PROBE_IP"],
                                                 flow["INPUT_SNMP"]),
                "input_snmp", info["input_snmp"]["value"], true,
                info["input_snmp"]["title"])
    end

    if (flow["OUTPUT_SNMP"]) and (tonumber(flow["OUTPUT_SNMP"]) ~= 0) then
        info_field["output_interface"] =
            historical_flow_utils.get_historical_url(
                format_utils.formatSNMPInterface(flow["PROBE_IP"],
                                                 flow["OUTPUT_SNMP"]),
                "output_snmp", info["output_snmp"]["value"], true,
                info["output_snmp"]["title"])
    end

    if table.len(info_field) > 1 then
        flow_details[#flow_details + 1] = {
            name = i18n("details.flow_snmp_localization"),
            values = {""}
        }
        for field, value in pairs(info_field) do
            flow_details[#flow_details + 1] = {
                name = "",
                values = {i18n(field), value}
            }
        end
    end

    return flow_details
end

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

local function format_historical_latency(flow, value, cli_or_srv)
    return {
        name = i18n("db_explorer." .. cli_or_srv .. "_latency"),
        values = {(tonumber(flow[value]) / 1000) .. " msec"}
    }
end

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

local function format_historical_application_latency(latency)
    return {
        name = i18n("flow_details.application_latency"),
        values = {(tonumber(latency)) .. " ms"}
    }
end

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

local function format_historical_obs_point(flow)
    return {
        name = i18n("db_explorer.observation_point"),
        values = {getObsPointAlias(flow["OBSERVATION_POINT_ID"], true, true)}
    }
end

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

local function format_historical_proto_info(flow_details, proto_info)
    local info = format_proto_info(flow_details, proto_info)
    return info
end

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

local function format_historical_custom_fields(flow_details, custom_fields)
    if table.len(custom_fields) > 0 then
        require "flow_utils"
        local flow_field_value_maps = require "flow_field_value_maps"
        flow_details[#flow_details + 1] = {
            name = i18n("flow_details.additional_flow_elements"),
            values = {""}
        }

        for key, value in pairs(custom_fields) do
            local nprobe_descr, value = flow_field_value_maps.map_field_value(
                                            interface.getId(), key, value)

            if not (nprobe_descr) then
                nprobe_descr = interface.getZMQFlowFieldDescr(key)
                if isEmptyString(nprobe_descr) then
                    nprobe_descr = key
                end
            else
                nprobe_descr = getFlowKey(nprobe_descr)
            end
            flow_details[#flow_details + 1] = {
                name = "",
                values = {nprobe_descr, value}
            }
        end
    end

    return flow_details
end

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

local function format_historical_flow_traffic_stats(rowspan, cli2srv_retr,
                                                    srv2cli_retr, cli2srv_ooo,
                                                    srv2cli_ooo, cli2srv_lost,
                                                    srv2cli_lost)
    local flow_details = {}

    if rowspan > 0 then
        flow_details[#flow_details + 1] = {
            name = i18n("flow_details.tcp_packet_analysis"),
            values = {
                "",
                i18n("client") ..
                    " <i class=\"fas fa-long-arrow-alt-right\" ></i> " ..
                    i18n("server") .. " / " .. i18n("client") ..
                    " <i class=\"fas fa-long-arrow-alt-left\"></i> " ..
                    i18n("server")
            }
        }

        if ((cli2srv_retr and (tonumber(cli2srv_retr) > 0)) or
            (srv2cli_retr and (tonumber(srv2cli_retr) > 0))) then
            flow_details[#flow_details + 1] = {
                name = "",
                values = {
                    i18n("details.retransmissions"),
                    formatPackets(cli2srv_retr) .. " / " ..
                        formatPackets(srv2cli_retr)
                }
            }
        end
        if ((cli2srv_ooo and (tonumber(cli2srv_ooo) > 0)) or
            (srv2cli_ooo and (tonumber(srv2cli_ooo) > 0))) then
            flow_details[#flow_details + 1] = {
                name = "",
                values = {
                    i18n("details.out_of_order"),
                    formatPackets(cli2srv_ooo) .. " / " ..
                        formatPackets(srv2cli_ooo)
                }
            }
        end
        if ((cli2srv_ooo and (tonumber(cli2srv_ooo) > 0)) or
            (srv2cli_ooo and (tonumber(srv2cli_ooo) > 0))) then
            flow_details[#flow_details + 1] = {
                name = "",
                values = {
                    i18n("details.lost"),
                    formatPackets(cli2srv_lost) .. " / " ..
                        formatPackets(srv2cli_lost)
                }
            }
        end
    end

    return flow_details
end

local function format_historical_flow_rtt(client_nw_latency, server_nw_latency)
    local rtt = client_nw_latency + server_nw_latency
    local cli2srv = round(client_nw_latency, 3)
    local srv2cli = round(server_nw_latency, 3)
    local values =
        '<div class="progress"><div class="progress-bar bg-warning" style="width: ' ..
            (cli2srv * 100 / rtt) .. '%;">' .. cli2srv .. ' ms (client)</div>' ..
            '<div class="progress-bar bg-success" style="width: ' ..
            (srv2cli * 100 / rtt) .. '%;">' .. srv2cli ..
            ' ms (server)</div></div>'
    return {name = i18n("flow_details.rtt_breakdown"), values = {values}}
end

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

-- This function format the historical flow details page
function historical_flow_details_formatter.formatHistoricalFlowDetails(flow)
    local historical_flow_utils = require "historical_flow_utils"
    local flow_details = {}

    if flow then
        local info = historical_flow_utils.format_clickhouse_record(flow)
        flow_details[#flow_details + 1] = format_historical_flow_label(flow)
        flow_details[#flow_details + 1] = format_historical_protocol_label(flow)
        flow_details[#flow_details + 1] =
            format_historical_last_first_seen(flow, info)
        flow_details[#flow_details + 1] =
            historical_flow_details_formatter.format_historical_total_traffic(
                flow)
        flow_details[#flow_details + 1] =
            historical_flow_details_formatter.format_historical_client_server_bytes(
                flow)
        flow_details[#flow_details + 1] =
            historical_flow_details_formatter.format_historical_bytes_progress_bar(
                flow, info)
        if flow["QOE_SCORE"] and tonumber(flow["QOE_SCORE"]) > 0 then
            flow_details[#flow_details + 1] =
                historical_flow_details_formatter.format_qoe(flow, info)
        end

        if ((tonumber(flow["SERVER_NW_LATENCY_US"]) > 0) or
            (tonumber(flow["CLIENT_NW_LATENCY_US"]) > 0)) then
            flow_details[#flow_details + 1] =
                format_historical_flow_rtt(tonumber(flow["SERVER_NW_LATENCY_US"]),
                                           tonumber(flow["CLIENT_NW_LATENCY_US"]))
        end

        if (info['dst2src_dscp']) and (info['src2dst_dscp']) then
            flow_details[#flow_details + 1] = format_historical_tos(flow)
        end

        if (info["l4proto"]) and (info["l4proto"]["label"] == 'TCP') then
            flow_details[#flow_details + 1] =
                format_historical_tcp_flags(flow, info)

            if (info["major_connection_state"] ~= 0 and
                info["minor_connection_state"] ~= 0) then
                local conn_states = format_tcp_connection_states(info)

                for _, state in pairs(conn_states or {}) do
                    flow_details[#flow_details + 1] = {
                        name = '', -- Empty label
                        values = {state}
                    }
                end
            end
        end

        if (info["cli_host_pool_id"]) and
            (info["cli_host_pool_id"]["value"] ~= '0') and
            (info["srv_host_pool_id"]["value"] ~= '0') then
            flow_details[#flow_details + 1] =
                format_historical_host_pool(flow, info)
        end

        if (info["score"]) and (info["score"]["value"] ~= 0) then
            flow_details =
                historical_flow_details_formatter.format_historical_issues(
                    flow_details, flow)
        end

        if (info['community_id']) and (not isEmptyString(info['community_id'])) then
            flow_details[#flow_details + 1] =
                format_historical_community_id(flow)
        end

        if (info['info']) and (not isEmptyString(info['info']["title"])) then
            if add_info_field(flow) then
                flow_details[#flow_details + 1] = format_historical_info(flow)
            end
        end

        flow_details = format_pre_post_nat_info(flow_details, flow, info)

        if (flow["PROBE_IP"] and not isEmptyString(flow['PROBE_IP']) and
            (flow['PROBE_IP'] ~= '0.0.0.0')) then
            flow_details = format_historical_probe(flow_details, flow, info)
        end

        if tonumber(flow["CLIENT_NW_LATENCY_US"]) ~= 0 then
            flow_details[#flow_details + 1] =
                format_historical_latency(flow, "CLIENT_NW_LATENCY_US", "cli")
        end

        if tonumber(flow["SERVER_NW_LATENCY_US"]) ~= 0 then
            flow_details[#flow_details + 1] =
                format_historical_latency(flow, "SERVER_NW_LATENCY_US", "srv")
        end

        local protocol_info_json =
            json.decode(flow["PROTOCOL_INFO_JSON"] or '') or {}

        if (protocol_info_json["appl_latency"]) then
            flow_details[#flow_details + 1] =
                format_historical_application_latency(
                    protocol_info_json["appl_latency"])
        end

        if (protocol_info_json["traffic_stats"] and
            table.len(protocol_info_json["traffic_stats"]) > 0) then
            local rowspan = 1;

            if (protocol_info_json["traffic_stats"]["cli2srv_retransmissions"] ~=
                0 or
                protocol_info_json["traffic_stats"]["srv2cli_retransmissions"] ~=
                0) then rowspan = rowspan + 1 end

            if (protocol_info_json["traffic_stats"]["cli2srv_out_of_order"] ~= 0 or
                protocol_info_json["traffic_stats"]["srv2cli_out_of_order"] ~= 0) then
                rowspan = rowspan + 1
            end

            if (protocol_info_json["traffic_stats"]["cli2srv_lost"] ~= 0 or
                protocol_info_json["traffic_stats"]["srv2cli_lost"] ~= 0) then
                rowspan = rowspan + 1
            end
            flow_details = table.merge(flow_details,
                                       format_historical_flow_traffic_stats(
                                           rowspan,
                                           protocol_info_json["traffic_stats"]["cli2srv_retransmissions"],
                                           protocol_info_json["traffic_stats"]["srv2cli_retransmissions"],
                                           protocol_info_json["traffic_stats"]["cli2srv_out_of_order"],
                                           protocol_info_json["traffic_stats"]["srv2cli_out_of_order"],
                                           protocol_info_json["traffic_stats"]["cli2srv_lost"],
                                           protocol_info_json["traffic_stats"]["srv2cli_lost"]))
        end

        if tonumber(flow["OBSERVATION_POINT_ID"]) ~= 0 then
            flow_details[#flow_details + 1] = format_historical_obs_point(flow)
        end

        if not isEmptyString(info.wlan_ssid) then
            flow_details[#flow_details + 1] =
                format_historical_wlan_ssid(flow, info)
        end

        if info.apn_mac and not isEmptyString(info.apn_mac.value) then
            flow_details[#flow_details + 1] =
                format_historical_wtp_mac_address(flow, info)
        end

        if table.len(protocol_info_json["proto"]) > 0 then
            flow_details = format_historical_proto_info(flow_details,
                                                        protocol_info_json["proto"])

            if (type(flow_details[#flow_details]['values']) == 'table') and
                (table.len(flow_details[#flow_details]['values']) == 0) then
                table.remove(flow_details, #flow_details)
            end
        end

        if table.len(protocol_info_json["proto"]) > 0 then
            flow_details = format_historical_custom_fields(flow_details,
                                                           protocol_info_json["custom_fields"])
        end
    end
    return flow_details
end

return historical_flow_details_formatter
