local json = require("JsonParser")
local UEHelpers = require("UEHelpers")
local statics = require("Statics")

local maxVehiclePerPlayer = 10
local npcVehicleDensity = 1.0

local serverConfig = ReadFileAsString("../../../DedicatedServerConfig.json")
if serverConfig then
    local config = json.parse(serverConfig)
    if config then
        maxVehiclePerPlayer = config.MaxVehiclePerPlayer or maxVehiclePerPlayer
        npcVehicleDensity = config.NPCVehicleDensity or npcVehicleDensity
    end
end

-- Amount of poll per minute.
-- Change this value to increase/decrease median accuracy
local pollPerMin = 30
local serverFps = {} ---@type number[]

---Convert FMTZoneState to JSON serializable table
---@param zone FMTZoneState
local function ZoneToTable(zone)
    local data = {}

    data.BusTransportRate = zone.BusTransportRate
    data.FoodSupplyRate = zone.FoodSupplyRate
    data.GarbageCollectRate = zone.GarbageCollectRate
    data.PolicePatrolRate = zone.PolicePatrolRate
    data.NumResidents = zone.NumResidents
    data.ZoneKey = zone.ZoneKey:ToString()

    return data
end

---Get current server state
---@return table
local function GetServerState(zoneName)
    local gameState = GetMotorTownGameState()
    local data = {}
    if (gameState:IsValid()) then
        local state = gameState.Net_HotState

        data.FPS = state.FPS
        data.BusTransportRate = state.BusTransportRate
        data.FoodSupplyRate = state.FoodSupplyRate
        data.GarbageCollectRate = state.GarbageCollectRate
        data.NumResidents = state.NumResidents
        data.PolicePatrolRate = state.PolicePatrolRate
        data.ServerPlatformTimeSeconds = state.ServerPlatformTimeSeconds

        --local zones = {}
        --if state.ZoneStates:IsValid() then
        --  state.ZoneStates:ForEach(function(index, element)
        --      local zone = element:get() ---@type FMTZoneState
        --      table.insert(zones, ZoneToTable(zone))
        --  end)
        --end
        --data.ZoneStates = zones
    end
    return data
end

---Get the specified zone state
---@param zoneName string Return only for the specified zone state
---@return table
local function GetZoneState(zoneName)
    local gameState = GetMotorTownGameState()
    if (gameState:IsValid()) then
        local state = gameState.Net_HotState

        for i = 1, #state.ZoneStates, 1 do
            if state.ZoneStates[i].ZoneKey:ToString() == zoneName then
                return ZoneToTable(state.ZoneStates[i])
            end
        end
    end
    return {}
end

local function GetServerFps()
    local gameState = GetMotorTownGameState()
    if (gameState:IsValid()) then
        local fps = gameState.Net_HotState.FPS ---@cast fps number
        table.insert(serverFps, fps)
        if (#serverFps > pollPerMin) then
            table.remove(serverFps, 1)
        end
        table.sort(serverFps, function(a, b)
            return a > b
        end)
        local medianIdx = math.ceil(#serverFps / 2)
        local medianFps = serverFps[medianIdx]
        return medianFps
    end
    return -1
end

local npcAmount = 1.0
---Adjust NPC traffic spawn settings at runtime.
---NOTE: This modifies MaxCount/bUseNPCVehicleDensity on the AIVehicleSpawnSystem
---but the game does NOT pick up these changes at runtime — existing traffic
---continues unchanged and new traffic still uses the original DedicatedServerConfig values.
---To actually change NPC traffic density, update NPCVehicleDensity in DedicatedServerConfig.json
---and restart the server. This function is effectively a no-op at runtime.
---@param amount number New density (0.0–1.0)
local function AdjustTrafficDensity(amount)
    if amount ~= npcAmount then
        LogOutput("INFO", "Changing traffic amount from %.0f%% to %.0f%%", npcAmount * 100, amount * 100)
        npcAmount = amount
    end
    local gameState = GetMotorTownGameState()
    if not gameState:IsValid() then
        LogOutput("WARN", "AdjustTrafficDensity: gameState not valid")
        return
    end
    if not gameState.AIVehicleSpawnSystem:IsValid() then
        LogOutput("WARN", "AdjustTrafficDensity: AIVehicleSpawnSystem not valid")
        return
    end
    local settings = gameState.AIVehicleSpawnSystem.SpawnSettings
    LogOutput("INFO", "AdjustTrafficDensity: #settings=%i", #settings)
    local densities = {
        Small = { MaxAmount = 250 },
        Special = { MaxAmount = 5 },
        Truck = { MaxAmount = 50 },
        Bus = { MaxAmount = 50 },
    }
    for i = 1, #settings, 1 do
        local setting = settings[i] ---@cast setting FMTAIVehicleSpawnSetting
        local key = setting.SettingKey:ToString()
        local density = densities[key]
        if density then
            setting.bUseNPCVehicleDensity = false
            setting.MaxCount = math.floor(density.MaxAmount * amount)
        end
    end
end

---Automatically adjust server caps
---@param override boolean?
---@return boolean
local function AutoAdjustServerCaps(override)
    local gameState = GetMotorTownGameState()
    if not gameState:IsValid() then
        return false
    end

    local currentFps = 60
    if not override then
        currentFps = GetServerFps()
    end

    if (currentFps <= 0) then
        LogOutput("DEBUG", "Invalid FPS, not changing anything")
        return false
    elseif (currentFps < 30) then
        local newLimit = math.floor(maxVehiclePerPlayer / 2)
        LogOutput("DEBUG", "Server FPS lower than 30 FPS, setting maxVehiclePerPlayer to %i", newLimit)
        gameState.Net_ServerConfig.MaxVehiclePerPlayer = newLimit
        AdjustTrafficDensity(0)
    elseif (currentFps < 40) then
        local newLimit = math.floor(maxVehiclePerPlayer / 0.75)
        LogOutput("DEBUG", "Server FPS lower than 40 FPS, setting maxVehiclePerPlayer to %i", newLimit)
        gameState.Net_ServerConfig.MaxVehiclePerPlayer = newLimit
        AdjustTrafficDensity(math.floor(npcVehicleDensity / 2))
    else
        LogOutput("DEBUG", "Server FPS at 60 FPS or in override mode, setting maxVehiclePerPlayer to %i",
            maxVehiclePerPlayer)
        gameState.Net_ServerConfig.MaxVehiclePerPlayer = maxVehiclePerPlayer
        AdjustTrafficDensity(npcVehicleDensity)
    end
    return true
end

if os.getenv("MOD_AUTO_FPS_ENABLE") then
    LoopInGameThreadWithDelay(60 * 1000 / pollPerMin, function()
        AutoAdjustServerCaps()
    end)
end

-- Register console commands

RegisterConsoleCommandHandler("getserverstate", function(Cmd, CommandParts, Ar)
    LogOutput("INFO", json.stringify(GetServerState()))
    return true
end)

RegisterConsoleCommandHandler("setnpctraffic", function(Cmd, CommandParts, Ar)
    local density = tonumber(CommandParts[1]) or 1.0
    npcVehicleDensity = density
    local gameState = GetMotorTownGameState()
    if gameState:IsValid() then
        gameState.Net_ServerConfig.MaxVehiclePerPlayer = maxVehiclePerPlayer
        AdjustTrafficDensity(npcVehicleDensity)
    end
    LogOutput("INFO", "Set NPC traffic density to %.1f%%", density * 100)
    return true
end)

-- HTTP request handlers

---Handle the get server state commands
---@type RequestPathHandler
local function HandleGetServerState(session)
    local data = {}
    data = GetServerState()
    if GetOpenFdCount then
        local fd = GetOpenFdCount()
        if fd >= 0 then
            data.open_fds = fd
            data.fd_setsize_limit = 1024
        end
    end
    local ok = true
    if not ok then return { error = "Game thread timeout" }, nil, 503 end
    return { data = data }
end

---Handle the get zone state commands
---@type RequestPathHandler
local function HandleGetZoneState(session)
    local zoneName = session.pathComponents[3]
    if zoneName then
        return { data = GetZoneState(zoneName) }
    end
    return nil, nil, 400
end

---Handle NPC traffic density update request
---@type RequestPathHandler
local function HandleUpdateNpcTraffic(session)
    local body = json.parse(session.content)
    if body then
        local density = tonumber(body.NPCVehicleDensity)
        if density then
            npcVehicleDensity = density
        end
        local maxV = tonumber(body.MaxVehiclePerPlayer)
        if maxV then
            maxVehiclePerPlayer = maxV
        end
        local gameState = GetMotorTownGameState()
        if gameState:IsValid() then
            gameState.Net_ServerConfig.MaxVehiclePerPlayer = maxVehiclePerPlayer
            AdjustTrafficDensity(npcVehicleDensity)
        end
        return { status = "ok" }
    end
    return nil, nil, 400
end

---Handle request to execute command on the server
---@type RequestPathHandler
local function HandleServerExecCommand(session)
    local data = json.parse(session.content)

    if data and data.Command then
        local world = UEHelpers.GetWorld()
        if world:IsValid() then
            local PC = data.PlayerId and GetPlayerControllerFromUniqueId(data.PlayerId) or nil
            UEHelpers.GetKismetSystemLibrary():ExecuteConsoleCommand(world, data.Command, PC)
            return { status = "ok" }, nil, 201
        end
    end
    return { error = "Invalid payload" }, nil, 400
end

---Handle get server status
---@type RequestPathHandler
local function HandleGetServerStatus(session)
    local gameState = GetMotorTownGameState()
    if not gameState:IsValid() then
        -- Game state is not created yet
        return { status = "not ready" }, nil, 503
    end
    local data = { status = "ok" }
    if GetOpenFdCount then
        local fd = GetOpenFdCount()
        if fd >= 0 then
            data.open_fds = fd
            data.fd_setsize_limit = 1024
        end
    end
    return data
end

---Handle get mod version
---@type RequestPathHandler
local function HandleGetModVersion(session)
    return { version = statics.ModVersion }
end

local function HandleSetServerConfig(session)
  local gameState = GetMotorTownGameState()
  if not gameState:IsValid() then
    return nil, nil, 503
  end

  local body = json.parse(session.content)
  if not body then
    return { error = "Invalid JSON body" }, nil, 400
  end

  -- SAFETY: Net_ServerConfig is a replicated property; mutations must be on the game thread.
  if body.MaxVehiclePerPlayer ~= nil then
    gameState.Net_ServerConfig.MaxVehiclePerPlayer = body.MaxVehiclePerPlayer
  end
  if body.bAllowCorporation ~= nil then
    gameState.Net_ServerConfig.bAllowCorporation = body.bAllowCorporation
  end
  if body.MaxHousingPlotRentalDays ~= nil then
    gameState.Net_ServerConfig.MaxHousingPlotRentalDays = body.MaxHousingPlotRentalDays
  end
  if body.bAllowAdminToRemoveAdmin ~= nil then
    gameState.Net_ServerConfig.bAllowAdminToRemoveAdmin = body.bAllowAdminToRemoveAdmin
  end
  if body.bAllowModdedVehicle ~= nil then
    gameState.Net_ServerConfig.bAllowModdedVehicle = body.bAllowModdedVehicle
  end
  if body.bAllowPlayerToJoinWithCompanyVehicles ~= nil then
    gameState.Net_ServerConfig.bAllowPlayerToJoinWithCompanyVehicles = body.bAllowPlayerToJoinWithCompanyVehicles
  end
  local ok = true
  if not ok then return { error = "Game thread timeout" }, nil, 503 end
  return nil, nil, 200
end

---Handle get police patrol areas with payment data
---@type RequestPathHandler
local function HandleGetPolicePatrolAreas(session)
    local gameState = GetMotorTownGameState()
    if not gameState:IsValid() then
        return nil, nil, 503
    end

    local areas = {}
    local ok = true
    local police = gameState.Net_Police
    if police:IsValid() then
        for i = 1, #police.Net_ColdState.PatrolAreas, 1 do
            local area = police.Net_ColdState.PatrolAreas[i]
            local points = {}
            for j = 1, #area.PointsToPatrol, 1 do
                local pt = area.PointsToPatrol[j]
                table.insert(points, {
                    PatrolPointId = pt.PatrolPointId,
                    BasePayment = pt.BasePayment,
                    AreaBonusPayment = pt.AreaBonusPayment,
                })
            end
            table.insert(areas, {
                PatrolAreaId = area.PatrolAreaId,
                ZoneKey = area.ZoneKey:ToString(),
                NumTotalPoints = area.NumTotalPoints,
                Points = points,
            })
        end
    end
    if not ok then return { error = "Game thread timeout" }, nil, 503 end
    return { data = areas }
end

ExecuteWithDelay(5000, function()
  local banker = FindFirstOf('Banker_C')
  if banker and banker:IsValid() then
    banker:K2_DestroyActor()
  end
end)

return {
    HandleGetServerState = HandleGetServerState,
    HandleGetZoneState = HandleGetZoneState,
    HandleUpdateNpcTraffic = HandleUpdateNpcTraffic,
    HandleServerExecCommand = HandleServerExecCommand,
    HandleGetServerStatus = HandleGetServerStatus,
    HandleGetModVersion = HandleGetModVersion,
    HandleSetServerConfig = HandleSetServerConfig,
    HandleGetPolicePatrolAreas = HandleGetPolicePatrolAreas,
}
