if isClient() then return end

local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local pendingSync = {}
local snapshotTickCounter = 0

TowingCommands.wantNoise = getDebug() or false

local noise = function(msg)
    if TowingCommands.wantNoise then
        print("TowBarCommands: " .. msg)
    end
end

local function queueSync(kind, player, args)
    if not args then return end
    table.insert(pendingSync, {
        kind = kind,
        ticks = SyncDelayTicks,
        player = player,
        args = args
    })
end

local function resolveAttachmentA(args, vehicleA)
    if args and args.attachmentA then return args.attachmentA end
    if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
    return "trailer"
end

local function resolveAttachmentB(args, vehicleB)
    if args and args.attachmentB then return args.attachmentB end
    if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end
    return "trailerfront"
end

local function isLinked(vehicleA, vehicleB)
    if not vehicleA or not vehicleB then return false end
    return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
end

local function hasTowBarState(vehicle)
    if not vehicle then return false end
    local md = vehicle:getModData()
    if not md then return false end
    return md["isTowingByTowBar"] == true
end

local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
    if not vehicleA or not vehicleB then return end
    sendServerCommand("towbar", "forceAttachSync", {
        vehicleA = vehicleA:getId(),
        vehicleB = vehicleB:getId(),
        attachmentA = attachmentA,
        attachmentB = attachmentB
    })
end

local function broadcastDetach(vehicleAId, vehicleBId)
    sendServerCommand("towbar", "forceDetachSync", {
        vehicleA = vehicleAId,
        vehicleB = vehicleBId
    })
end

local function processAttachSync(item)
    local args = item.args or {}
    local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
    local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
    if not vehicleA or not vehicleB then
        noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
        return
    end

    local attachmentA = resolveAttachmentA(args, vehicleA)
    local attachmentB = resolveAttachmentB(args, vehicleB)
    if not isLinked(vehicleA, vehicleB) then
        vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
    end
    if isLinked(vehicleA, vehicleB) then
        broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
    end
end

local function processDetachSync(item)
    local args = item.args or {}
    local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
    local vehicleBId = args.vehicleB
    broadcastDetach(vehicleAId, vehicleBId)
end

local function snapshotActiveTowbarLinksServer()
    local cell = getCell()
    if not cell then return end
    local list = cell:getVehicles()
    if not list then return end

    local it = list:iterator()
    while it:hasNext() do
        local towingVehicle = it:next()
        local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
        if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
            if hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle) then
                local attachmentA = resolveAttachmentA(nil, towingVehicle)
                local towedMd = towedVehicle:getModData()
                local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle)
                if attachmentA == attachmentB then
                    attachmentA = "trailer"
                    attachmentB = "trailerfront"
                end
                broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
            end
        end
    end
end

local function processPendingSync()
    snapshotTickCounter = snapshotTickCounter + 1
    if snapshotTickCounter >= SnapshotIntervalTicks then
        snapshotTickCounter = 0
        snapshotActiveTowbarLinksServer()
    end

    if #pendingSync == 0 then return end

    local remaining = {}
    for i = 1, #pendingSync do
        local item = pendingSync[i]
        item.ticks = item.ticks - 1
        if item.ticks <= 0 then
            if item.kind == "attach" then
                processAttachSync(item)
            elseif item.kind == "detach" then
                processDetachSync(item)
            end
        else
            table.insert(remaining, item)
        end
    end
    pendingSync = remaining
end

function Commands.attachTowBar(player, args)
    local vehicleA = getVehicleById(args.vehicleA)
    local vehicleB = getVehicleById(args.vehicleB)
    if not vehicleA then
        noise("no such vehicle (A) id=" .. tostring(args.vehicleA))
        return
    end
    if not vehicleB then
        noise("no such vehicle (B) id=" .. tostring(args.vehicleB))
        return
    end

    vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
end

function Commands.detachTowBar(player, args)
    local towingVehicle = args.towingVehicle and getVehicleById(args.towingVehicle) or nil
    local towedVehicle = args.vehicle and getVehicleById(args.vehicle) or nil

    if not towingVehicle and towedVehicle then
        towingVehicle = towedVehicle:getVehicleTowedBy()
    end
    if not towedVehicle and towingVehicle then
        towedVehicle = towingVehicle:getVehicleTowing()
    end

    if towedVehicle then
        towedVehicle:breakConstraint(true, false)
    end
    if towingVehicle and towingVehicle ~= towedVehicle then
        towingVehicle:breakConstraint(true, false)
    end
end

function Commands.consumeTowBar(player, args)
    if not player then return end
    local inventory = player:getInventory()
    if not inventory then return end

    local towBarItem = nil
    local itemId = args and args.itemId
    if itemId then
        towBarItem = inventory:getItemWithID(itemId)
    end
    if not towBarItem then
        towBarItem = inventory:getFirstTypeRecurse(TowBarItemType)
    end
    if not towBarItem then return end

    local wasPrimary = player:isPrimaryHandItem(towBarItem)
    local wasSecondary = player:isSecondaryHandItem(towBarItem)
    player:removeFromHands(towBarItem)
    inventory:Remove(towBarItem)
    sendRemoveItemFromContainer(inventory, towBarItem)

    if wasPrimary or wasSecondary then
        sendEquip(player)
    end
end

function Commands.giveTowBar(player, args)
    if not player then return end
    local inventory = player:getInventory()
    if not inventory then return end

    local towBarItem = inventory:AddItem(TowBarItemType)
    if not towBarItem then return end
    sendAddItemToContainer(inventory, towBarItem)

    if args and args.equipPrimary then
        player:setPrimaryHandItem(towBarItem)
        sendEquip(player)
    end
end

-- Compatibility aliases for older command names.
Commands.attachConstraint = Commands.attachTowBar
Commands.detachConstraint = Commands.detachTowBar

TowingCommands.OnClientCommand = function(module, command, player, args)
    -- Only sync explicit towbar commands so vanilla towing stays untouched.
    if module == "towbar" and command == "attachTowBar" then
        queueSync("attach", player, args)
    elseif module == "towbar" and command == "detachTowBar" then
        queueSync("detach", player, args)
    end

    if module == "towbar" and Commands[command] then
        local argStr = ""
        args = args or {}
        for k, v in pairs(args) do
            argStr = argStr .. " " .. tostring(k) .. "=" .. tostring(v)
        end
        noise("received " .. module .. " " .. command .. " " .. tostring(player) .. argStr)
        Commands[command](player, args)
    end
end

Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
Events.OnTick.Add(processPendingSync)
