local Logger = require("libraries/logger")

Logger.info("THROUGHPUT_TEST", "Loading addon")

local STARTUP_BANNER = {
    "███╗   ███╗ █████╗  ██████╗██╗  ██╗██╗███╗   ██╗███████╗     ██████╗██╗   ██╗███╗   ███╗",
    "████╗ ████║██╔══██╗██╔════╝██║  ██║██║████╗  ██║██╔════╝    ██╔════╝╚██╗ ██╔╝████╗ ████║",
    "██╔████╔██║███████║██║     ███████║██║██╔██╗ ██║█████╗      ██║  ███╗╚████╔╝ ██╔████╔██║",
    "██║╚██╔╝██║██╔══██║██║     ██╔══██║██║██║╚██╗██║██╔══╝      ██║   ██║ ╚██╔╝  ██║╚██╔╝██║",
    "██║ ╚═╝ ██║██║  ██║╚██████╗██║  ██║██║██║ ╚████║███████╗    ╚██████╔╝  ██║   ██║ ╚═╝ ██║",                                                                                                                        
}

for _, line in ipairs(STARTUP_BANNER) do
    Logger.info("THROUGHPUT_TEST", line)
end

local SERVER_URL = "http://localhost:5000"
local DEFAULT_HERO = "npc_dota_hero_juggernaut"
local AUTO_ASSIGN_INTERVAL = 0.5

local HTTPClient = require("libraries/http_client")
HTTPClient.SERVER_URL = SERVER_URL
HTTPClient.DEBUG = false

local CONTROLLED_PLAYER_ID = 0
local g_LastHeroWarnTime = -999

local function log_command(message)
    local timestamp = GameRules and GameRules:GetDOTATime(false, false) or 0
    local details = string.format("[%.2f] %s", timestamp, message)
    Logger.info("CMD", details)
end

local function queue_suffix(queue)
    if queue then
        return " (queued)"
    end
    return ""
end

local function get_controlled_hero()
    if not PlayerResource then
        return nil
    end
    local hero = PlayerResource:GetSelectedHeroEntity(CONTROLLED_PLAYER_ID)
    if hero and IsValidEntity(hero) then
        return hero
    end
    return nil
end

local outstanding = 0
local last_send_tg = 0.0
local MAX_OUTSTANDING = 8          -- small leaky bucket
local STALL_SEC = 0.75              -- watchdog (game-time seconds)
local g_LastCommandPoll = 0
local g_CommandsReceived = 0
local COMMAND_POLL_INTERVAL = 0.25    -- Poll at 4Hz

local g_FrameCount = 0

local function yaw_deg(hero)
    -- Use engine-provided angles; yaw is .y in degrees
    if hero.GetAnglesAsVector then
        local ang = hero:GetAnglesAsVector()
        return (ang and ang.y) or 0.0
    end
    return 0.0
end

local function nz(v, fallback) return (v ~= nil) and v or fallback end

local function pack_hero(hero)
    local eid   = hero:entindex()
    local pid   = hero.GetPlayerID and hero:GetPlayerID() or -1
    local hid   = hero.GetHeroID and hero:GetHeroID() or -1
    local team  = hero.GetTeamNumber and hero:GetTeamNumber() or -1

    local alive = (hero.IsAlive and hero:IsAlive()) and 1 or 0

    local hp    = nz(hero.GetHealth and hero:GetHealth(), 0)
    local hpmax = nz(hero.GetMaxHealth and hero:GetMaxHealth(), 0)
    local mp    = nz(hero.GetMana and hero:GetMana(), 0)
    local mpmax = nz(hero.GetMaxMana and hero:GetMaxMana(), 0)

    local p     = hero.GetAbsOrigin and hero:GetAbsOrigin() or Vector(0,0,0)
    local x,y,z = nz(p.x,0), nz(p.y,0), nz(p.z,0)
    local yaw   = yaw_deg(hero)

    local gold  = nz(hero.GetGold and hero:GetGold(), 0)
    local xp    = nz(hero.GetCurrentXP and hero:GetCurrentXP(), 0)

    local bb_cd    = nz(hero.GetBuybackCooldownTime and hero:GetBuybackCooldownTime(), 0.0)
    local bb_cost  = nz(hero.GetBuybackCost and hero:GetBuybackCost(), 0)
    local bb_limit = nz(hero.GetBuybackGoldLimitTime and hero:GetBuybackGoldLimitTime(), 0.0)

    local str   = nz(hero.GetStrength and hero:GetStrength(), 0.0)
    local agi   = nz(hero.GetAgility and hero:GetAgility(), 0.0)
    local intl  = nz(hero.GetIntellect and hero:GetIntellect(true), 0.0)
    local pri   = nz(hero.GetPrimaryAttribute and hero:GetPrimaryAttribute(), 0) -- enum
    local atksp = nz(hero.GetIncreasedAttackSpeed and hero:GetIncreasedAttackSpeed(false), 0.0)
    local dmin  = nz(hero.GetBaseDamageMin and hero:GetBaseDamageMin(), 0)
    local dmax  = nz(hero.GetBaseDamageMax and hero:GetBaseDamageMax(), 0)
    local armor = nz(hero.GetPhysicalArmorBaseValue and hero:GetPhysicalArmorBaseValue(), 0.0)

    local kills   = nz(hero.GetKills and hero:GetKills(), 0)
    local deaths  = nz(hero.GetDeaths and hero:GetDeaths(), 0)
    local assists = nz(hero.GetAssists and hero:GetAssists(), 0)
    local lh      = nz(hero.GetLastHits and hero:GetLastHits(), 0)
    local denies  = nz(hero.GetDenies and hero:GetDenies(), 0)

    local resp    = nz(hero.GetTimeUntilRespawn and hero:GetTimeUntilRespawn(), 0.0)
    local lastDmg = nz(hero.GetMostRecentDamageTime and hero:GetMostRecentDamageTime(), 0.0)

    return string.format(
        "%d,%d,%d,%d,%d,%d,%d,%d,%d,%.0f,%.0f,%.0f,%.2f,%d,%d,%.2f,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%.2f,%d,%d,%d,%d,%d,%.2f,%.2f",
        eid,pid,hid,team,
        alive,
        hp,hpmax,mp,mpmax,
        x,y,z,yaw,
        gold,xp,
        bb_cd,bb_cost,bb_limit,
        str,agi,intl,pri,atksp,dmin,dmax,armor,
        kills,deaths,assists,lh,denies,
        resp,lastDmg
    )
end

local function pack_creep(unit)
    local eid   = unit:entindex()
    local team  = unit.GetTeamNumber and unit:GetTeamNumber() or -1
    local hp    = nz(unit.GetHealth and unit:GetHealth(), 0)
    local hpmax = nz(unit.GetMaxHealth and unit:GetMaxHealth(), 0)
    local p     = unit.GetAbsOrigin and unit:GetAbsOrigin() or Vector(0,0,0)
    local x,y,z = nz(p.x,0), nz(p.y,0), nz(p.z,0)

    return string.format("%d,%d,%d,%d,%.0f,%.0f,%.0f", eid, team, hp, hpmax, x, y, z)
end

function Precache(_) end

local g_StartTime = 0

function Activate()
    SendToServerConsole("sv_cheats 1")
    SendToServerConsole("host_timescale 1")
    SendToServerConsole("dota_bot_disable 1")
    GameRules:SetHeroSelectionTime(0)
    GameRules:SetPreGameTime(0)
    GameRules:SetStrategyTime(0)
    GameRules:SetShowcaseTime(0)
    g_StartTime = GameRules:GetGameTime()
    local ent = GameRules:GetGameModeEntity()
    if ent and ent.SetCreepSpawningEnabled then
        ent:SetCreepSpawningEnabled(false)
    end
    ent:SetThink(function() return SendState() end, "SendState", 0.0028)
    ent:SetThink(AutoAssignHeroes, "AutoAssignHeroes", AUTO_ASSIGN_INTERVAL)
end

local g_AssignedHeroes = {}

function AutoAssignHeroes()
    for playerID = 0, 23 do
        if PlayerResource and PlayerResource:IsValidPlayerID(playerID) then
            local hero = PlayerResource:GetSelectedHeroEntity(playerID)
            if hero and IsValidEntity(hero) then
                if not g_AssignedHeroes[playerID] then
                    g_AssignedHeroes[playerID] = true
                    log_command(string.format("Hero already assigned for player %d: %s", playerID, hero:GetUnitName()))
                end
            else
                local player = PlayerResource:GetPlayer(playerID)
                if player and not g_AssignedHeroes[playerID] then
                    log_command(string.format("Assigning default hero to player %d", playerID))
                    PlayerResource:ReplaceHeroWith(playerID, DEFAULT_HERO, 0, 0)
                    g_AssignedHeroes[playerID] = true
                end
            end
        end
    end
    return AUTO_ASSIGN_INTERVAL
end

function PollCommands()
    local tg = GameRules:GetGameTime()
    
    if (tg - g_LastCommandPoll) < COMMAND_POLL_INTERVAL then
        return
    end
    g_LastCommandPoll = tg

    local hero = get_controlled_hero()
    if not hero then
        if tg - g_LastHeroWarnTime > 1.0 then
            Logger.info("CMD", "Deferring command poll; hero not available yet")
            g_LastHeroWarnTime = tg
        end
        return
    end
    
    local req = CreateHTTPRequestScriptVM("GET", SERVER_URL .. "/commands")
    req:Send(function(result)
        if result.StatusCode == 200 and result.Body ~= "" then
            g_CommandsReceived = g_CommandsReceived + 1
            
            -- Process the commands
            ProcessCommandBatch(result.Body)
            
            if g_CommandsReceived % 100 == 0 then
                local elapsed = GameRules:GetGameTime() - g_StartTime
                Logger.info(
                    "CMD_METRICS",
                    string.format(
                        "Processed %d batches in %.2f sec (%.2f batch/sec)",
                        g_CommandsReceived,
                        elapsed,
                        g_CommandsReceived / elapsed
                    )
                )
            end
        end
    end)
end

function SendState()
    local tg = GameRules:GetGameTime()

    -- Watchdog: if something wedged (no callback), reset
    if outstanding > 0 and (tg - last_send_tg) > STALL_SEC then
        outstanding = 0
    end

    if outstanding >= MAX_OUTSTANDING then
        return 0.0028
    end
    g_FrameCount = g_FrameCount + 1

    local heroes = {}
    local heroEntities = {}
    for i = 0, 9 do
        local hero = PlayerResource:GetSelectedHeroEntity(i)
        if hero and IsValidEntity(hero) then
            heroes[#heroes+1] = pack_hero(hero)
            heroEntities[#heroEntities+1] = hero
        end
    end

    local creeps = {}
    local seen = {}
    for _, hero in ipairs(heroEntities) do
        local units = FindUnitsInRadius(
            hero:GetTeamNumber(),
            hero:GetAbsOrigin(),
            nil,
            1600,
            DOTA_UNIT_TARGET_TEAM_ENEMY,
            DOTA_UNIT_TARGET_BASIC,
            DOTA_UNIT_TARGET_FLAG_NONE,
            FIND_ANY_ORDER,
            false
        )

        for _, unit in ipairs(units) do
            if unit and IsValidEntity(unit) and unit:IsAlive() then
                local eid = unit:entindex()
                if not seen[eid] then
                    seen[eid] = true
                    creeps[#creeps+1] = pack_creep(unit)
                end
            end
        end
    end

    local dotaTime = GameRules:GetDOTATime(false,false)
    local gameTime = GameRules:GetGameTime()

    local data = string.format('{"f":%d,"td":%.3f,"tg":%.3f,"h":[%s],"c":[%s]}',
        g_FrameCount, dotaTime, gameTime, table.concat(heroes, ","), table.concat(creeps, ","))

    local req = CreateHTTPRequestScriptVM("POST", SERVER_URL .. "/state")
    req:SetHTTPRequestRawPostBody("application/json", data)

    -- Optional: if available in your build, give the request its own network timeout
    if req.SetHTTPRequestNetworkActivityTimeout then
        req:SetHTTPRequestNetworkActivityTimeout(1)  -- seconds
    end

    outstanding = outstanding + 1
    last_send_tg = tg
    req:Send(function(_)
        if outstanding > 0 then outstanding = outstanding - 1 end
    end)

    PollCommands()

    return 0.0028
end

local g_LastProcessedCommand = 0

function ProcessCommandBatch(commandJson)
    -- Simple JSON array parser for commands
    for cmd in string.gmatch(commandJson, "{[^}]+}") do
        local id = tonumber(string.match(cmd, '"id":(%d+)'))
        local cmdType = string.match(cmd, '"type":"([^"]+)"')

        if id and cmdType and id > g_LastProcessedCommand then
            g_LastProcessedCommand = id

            local x = tonumber(string.match(cmd, '"x":([%d%-%.]+)'))
            local y = tonumber(string.match(cmd, '"y":([%d%-%.]+)'))
            local targetId = tonumber(string.match(cmd, '"target_id":(%d+)'))
            local queueToken = string.match(cmd, '"queue":(true|false)')
            local queue = queueToken == "true"
            local range = tonumber(string.match(cmd, '"range":([%d%-%.]+)'))

            if cmdType == "move" and x and y then
                local msg = string.format("Command move id=%d x=%.1f y=%.1f", id, x, y)
                log_command(msg .. queue_suffix(queue))
                ExecuteHeroMove(x, y, queue)
            elseif cmdType == "stop" then
                log_command(string.format("Command stop id=%d", id))
                ExecuteHeroStop()
            elseif cmdType == "attack" then
                local msg = string.format("Command attack id=%d", id)
                if targetId then
                    msg = msg .. string.format(" target=%d", targetId)
                elseif x and y then
                    msg = msg .. string.format(" pos=%.1f,%.1f", x, y)
                end
                if range then
                    msg = msg .. string.format(" range=%.1f", range)
                end
                log_command(msg .. queue_suffix(queue))
                ExecuteHeroAttack(targetId, x, y, range, queue)
            end
        end
    end
end

function ExecuteHeroMove(x, y, queue)
    local hero = get_controlled_hero()
    if not (hero and hero:IsAlive()) then
        return false
    end

    ExecuteOrderFromTable({
        UnitIndex = hero:entindex(),
        OrderType = DOTA_UNIT_ORDER_MOVE_TO_POSITION,
        Position = Vector(x, y, 0),
        Queue = queue or false
    })
    log_command(string.format("Execute move -> %.1f %.1f", x, y) .. queue_suffix(queue))
    return true
end

function ExecuteHeroStop()
    local hero = get_controlled_hero()
    if not (hero and hero:IsAlive()) then
        return false
    end

    ExecuteOrderFromTable({
        UnitIndex = hero:entindex(),
        OrderType = DOTA_UNIT_ORDER_STOP,
        Queue = false
    })
    log_command("Execute stop")
    return true
end

local function FindNearestEnemy(hero, searchRadius)
    searchRadius = searchRadius or 800
    local enemies = FindUnitsInRadius(
        hero:GetTeamNumber(),
        hero:GetAbsOrigin(),
        nil,
        searchRadius,
        DOTA_UNIT_TARGET_TEAM_ENEMY,
        DOTA_UNIT_TARGET_BASIC + DOTA_UNIT_TARGET_HERO,
        DOTA_UNIT_TARGET_FLAG_NONE,
        FIND_CLOSEST,
        false
    )

    if enemies and #enemies > 0 then
        return enemies[1]
    end

    return nil
end

function ExecuteHeroAttack(targetId, x, y, searchRadius, queue)
    local hero = get_controlled_hero()
    if not (hero and hero:IsAlive()) then
        return false
    end

    local target = nil

    if targetId then
        local handle = EntIndexToHScript(targetId)
        if handle and IsValidEntity(handle) and handle:IsAlive() and handle:GetTeamNumber() ~= hero:GetTeamNumber() then
            target = handle
        end
    end

    if target then
        ExecuteOrderFromTable({
            UnitIndex = hero:entindex(),
            OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
            TargetIndex = target:entindex(),
            Queue = queue or false
        })
        log_command(string.format("Execute attack target=%d", target:entindex()) .. queue_suffix(queue))
        return true
    end

    if x and y then
        ExecuteOrderFromTable({
            UnitIndex = hero:entindex(),
            OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
            Position = Vector(x, y, 0),
            Queue = queue or false
        })
        log_command(string.format("Execute attack-move -> %.1f %.1f", x, y) .. queue_suffix(queue))
        return true
    end

    local nearestEnemy = FindNearestEnemy(hero, searchRadius)
    if nearestEnemy then
        ExecuteOrderFromTable({
            UnitIndex = hero:entindex(),
            OrderType = DOTA_UNIT_ORDER_ATTACK_TARGET,
            TargetIndex = nearestEnemy:entindex(),
            Queue = queue or false
        })
        log_command(string.format("Execute attack nearest target=%d", nearestEnemy:entindex()) .. queue_suffix(queue))
        return true
    else
        ExecuteOrderFromTable({
            UnitIndex = hero:entindex(),
            OrderType = DOTA_UNIT_ORDER_ATTACK_MOVE,
            Position = hero:GetAbsOrigin(),
            Queue = queue or false
        })
        log_command("Execute fallback attack-move to hero origin" .. queue_suffix(queue))
    end

    return true
end
