--[[
 route.lua — запрашивает pulse-lets-go для маршрутизации вызова.
 Телефонный балансировщик нагрузки на базе FreeSWITCH + NATS.

 ┌─ Размещение ─────────────────────────────────────────────────────┐
 │ /opt/pulse-lets-go/contrib/route.lua                             │
 │                                                                   │
 │ ┌─ Использование в dialplan ────────────────────────────────────┐│
 │ │ <extension name="route_call">                                 ││
 │ │   <condition field="destination_number" expression="^.*$">    ││
 │ │     <action application="lua" data="route.lua"/>              ││
 │ │   </condition>                                                ││
 │ │ </extension>                                                  ││
 │ └───────────────────────────────────────────────────────────────┘│
 │                                                                   │
 │ ┌─ Кэширование (защита от блокировки dialplan-тредов) ───────────┐│
 │ │ Уровень 1: FS global variable (ESL push от balancer, ~0.1µs)  ││
 │ │ Уровень 2: File cache (/tmp/pulse_route_cache.json, ~50µs)    ││
 │ │ Уровень 3: HTTP API (/api/route, ~1-5ms, safety net)         ││
 │ └───────────────────────────────────────────────────────────────┘│
 │                                                                   │
 │ ┌─ Переменные канала (опционально) ─────────────────────────────┐│
 │ │ balancer_url      — URL pulse-lets-go (default: localhost:8080)││
 │ │ ingress_trunk     — ID входящего транка                        ││
 │ │ route_timeout     — таймаут HTTP fallback (default: 3с)       ││
 │ │ route_cache_path  — путь к файловому кэшу (default: <prefix>/data/…) ││
 │ └───────────────────────────────────────────────────────────────┘│
 │                                                                   │
 │ ┌─ Требования FreeSWITCH ───────────────────────────────────────┐│
 │ │ mod_curl  — <load module="mod_curl"/> (только для уровня 3)   ││
 │ │ cjson     — входит в стандартную поставку FS Lua              ││
 │ └───────────────────────────────────────────────────────────────┘│
 └───────────────────────────────────────────────────────────────────┘

 Логика:
   1. Попробовать FS global variable pulse_route_json (ESL push)
   2. Попробовать файловый кэш
   3. HTTP API /api/route (fallback)
   4. Разобрать JSON → bridge на sip_gateway
]]--

-- ===================================================================
-- Конфигурация
-- ===================================================================

-- URL балансировщика (переопределяется через переменную канала)
local BALANCER_URL = session:getVariable("balancer_url") or "http://localhost:8080"

-- Таймаут HTTP-запроса в секундах
local HTTP_TIMEOUT = tonumber(session:getVariable("route_timeout") or "3")

-- Путь к файловому кэшу маршрута
local ROUTE_CACHE_PATH = session:getVariable("route_cache_path") or "/opt/pulse-lets-go/data/route_cache.json"

-- JSON-парсер
local cjson = require("cjson")

-- API FreeSWITCH для вызова curl
local api = freeswitch.API()

-- ===================================================================
-- Вспомогательные функции
-- ===================================================================

-- Логирование с префиксом [route]
local function log(level, msg)
    freeswitch.consoleLog(level, "[route] " .. msg .. "\n")
end

-- Безопасная отправка трубки с кодом причины
local function safe_hangup(cause)
    if session:ready() then
        session:hangup(cause or "NORMAL_TEMPORARY_FAILURE")
    end
end

-- Попробовать получить JSON-маршрут из FS global variable (уровень 1).
local function try_global_cache()
    -- session:getVariable с префиксом global_ читает глобальные переменные FS
    local val = session:getVariable("global_pulse_route_json")
    if val and val ~= "" then
        return val
    end
    return nil
end

-- Попробовать получить JSON-маршрут из файлового кэша (уровень 2).
local function try_file_cache()
    local f, err = io.open(ROUTE_CACHE_PATH, "r")
    if not f then
        return nil
    end
    local content = f:read("*a")
    f:close()
    if content and content ~= "" then
        return content
    end
    return nil
end

-- Попробовать получить JSON-маршрут через HTTP API (уровень 3, fallback).
local function try_http(caller_id, dest, ing_trunk)
    local query = string.format("caller_id=%s&dest=%s", caller_id, dest)
    if ing_trunk ~= "" then
        query = query .. "&ingress_trunk=" .. ing_trunk
    end
    local url = BALANCER_URL .. "/api/route?" .. query

    local cmd = "curl " .. url
    local raw = api:execute("curl", cmd)

    if not raw or raw == "" then
        return nil, "пустой ответ от curl"
    end

    -- Отделяем заголовки от тела (mod_curl возвращает сырой HTTP)
    local body = raw:match("\r?\n\r?\n(.+)")
    if not body then
        body = raw
    end
    return body, nil
end

-- Разобрать JSON и выполнить bridge.
-- Возвращает true если звонок обработан (bridge или error).
local function process_route_json(json_str, source)
    if not json_str then
        return false
    end

    local ok, route = pcall(cjson.decode, json_str)
    if not ok or type(route) ~= "table" then
        log("warn", source .. ": некорректный JSON")
        return false
    end

    -- Ошибка балансировщика (нет зарегистрированных нод)
    if route.error then
        log("err", source .. ": " .. tostring(route.error))
        safe_hangup()
        return true
    end

    -- Фолбэк (все ноды unhealthy)
    if route.fallback and route.sip_gateway and route.sip_gateway ~= "" then
        log("info", string.format("%s: FALLBACK → %s (причина: %s)",
            source, route.sip_gateway, route.reason or "all_nodes_unhealthy"))
        session:execute("bridge", route.sip_gateway)
        return true
    end

    -- Успешный маршрут
    if route.sip_gateway and route.sip_gateway ~= "" then
        log("info", string.format("%s: ROUTE %s score=%s → %s",
            source, route.node_id or "?", tostring(route.score or 0), route.sip_gateway))
        session:execute("bridge", route.sip_gateway)
        return true
    end

    log("warn", source .. ": нет sip_gateway в ответе")
    return false
end

-- ===================================================================
-- Основная логика (трёхуровневый кэш)
-- ===================================================================

-- Извлекаем параметры вызова
local caller_id = session:getVariable("caller_id_number") or ""
local dest      = session:getVariable("destination_number") or ""
local ing_trunk = session:getVariable("ingress_trunk") or ""

-- Уровень 1: FS global variable (ESL push, sub-µs)
local json_str = try_global_cache()
if process_route_json(json_str, "global") then
    return
end

-- Уровень 2: файловый кэш (локальный, ~50µs)
json_str = try_file_cache()
if process_route_json(json_str, "file") then
    return
end

-- Уровень 3: HTTP API (fallback, ~1-5ms)
log("info", "запрос маршрута HTTP: " .. BALANCER_URL .. "/api/route?caller_id=" .. caller_id .. "&dest=" .. dest)

local body, http_err = try_http(caller_id, dest, ing_trunk)
if not body then
    log("err", "все уровни кэша недоступны: " .. (http_err or "unknown"))
    safe_hangup()
    return
end

if not process_route_json(body, "http") then
    log("err", "непредвиденный формат ответа балансировщика")
    safe_hangup()
end
