if isClient() then return end

if not TowBarMod then TowBarMod = {} end
TowBarMod.Landtrain = TowBarMod.Landtrain or {}
if TowBarMod.Landtrain._towSyncServerLoaded then return end
TowBarMod.Landtrain._towSyncServerLoaded = true

local SYNC_DELAY_TICKS = 2
local SNAPSHOT_INTERVAL_TICKS = 120
local pending = {}
local snapshotTickCounter = 0
local PERSIST_FRONT_SQL_KEY = "landtrainFrontTowSqlId"
local PERSIST_FRONT_ID_KEY = "landtrainFrontTowId"
local PERSIST_ATTACHMENT_A_KEY = "landtrainFrontTowAttachmentA"
local PERSIST_ATTACHMENT_B_KEY = "landtrainFrontTowAttachmentB"
local PERSIST_CHAIN_HEAD_SQL_KEY = "landtrainChainHeadSql"
local PERSIST_CHAIN_ORDER_KEY = "landtrainChainOrder"
local PERSIST_CHAIN_TOKEN_KEY = "landtrainChainToken"

local function log(msg)
    print("[Landtrain][TowSyncServer] " .. tostring(msg))
end

local function storePersistedFrontLinkData(frontVehicle, rearVehicle, attachmentA, attachmentB)
    if not frontVehicle or not rearVehicle then return end
    local md = rearVehicle:getModData()
    if not md then return end

    local frontSql = frontVehicle.getSqlId and tonumber(frontVehicle:getSqlId()) or nil
    local frontId = frontVehicle:getId()
    local resolvedAttachmentA = attachmentA or "trailer"
    local resolvedAttachmentB = attachmentB or "trailerfront"
    local changed = false

    if frontSql and frontSql > 0 then
        if tonumber(md[PERSIST_FRONT_SQL_KEY]) ~= frontSql then
            md[PERSIST_FRONT_SQL_KEY] = frontSql
            changed = true
        end
    end

    if tonumber(md[PERSIST_FRONT_ID_KEY]) ~= frontId then
        md[PERSIST_FRONT_ID_KEY] = frontId
        changed = true
    end
    if md[PERSIST_ATTACHMENT_A_KEY] ~= resolvedAttachmentA then
        md[PERSIST_ATTACHMENT_A_KEY] = resolvedAttachmentA
        changed = true
    end
    if md[PERSIST_ATTACHMENT_B_KEY] ~= resolvedAttachmentB then
        md[PERSIST_ATTACHMENT_B_KEY] = resolvedAttachmentB
        changed = true
    end

    if changed then
        rearVehicle:transmitModData()
    end
end

local function storePersistedChainOrderData(vehicle, headSql, orderIndex, chainToken)
    if not vehicle then return end
    local md = vehicle:getModData()
    if not md then return end

    local resolvedHeadSql = headSql and tonumber(headSql) or nil
    local resolvedOrder = orderIndex and tonumber(orderIndex) or nil
    local resolvedToken = chainToken and tostring(chainToken) or nil
    if resolvedToken == "" then resolvedToken = nil end
    local changed = false

    if resolvedHeadSql and resolvedHeadSql > 0 then
        if tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) ~= resolvedHeadSql then
            md[PERSIST_CHAIN_HEAD_SQL_KEY] = resolvedHeadSql
            changed = true
        end
    elseif md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
        md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
        changed = true
    end

    if resolvedOrder and resolvedOrder > 0 then
        if tonumber(md[PERSIST_CHAIN_ORDER_KEY]) ~= resolvedOrder then
            md[PERSIST_CHAIN_ORDER_KEY] = resolvedOrder
            changed = true
        end
    elseif md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
        md[PERSIST_CHAIN_ORDER_KEY] = nil
        changed = true
    end

    if resolvedToken then
        if tostring(md[PERSIST_CHAIN_TOKEN_KEY]) ~= resolvedToken then
            md[PERSIST_CHAIN_TOKEN_KEY] = resolvedToken
            changed = true
        end
    elseif md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
        md[PERSIST_CHAIN_TOKEN_KEY] = nil
        changed = true
    end

    if changed then
        vehicle:transmitModData()
    end
end

local function clearPersistedChainOrderData(vehicle)
    if not vehicle then return end
    local md = vehicle:getModData()
    if not md then return end

    local changed = false
    if md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
        md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
        changed = true
    end
    if md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
        md[PERSIST_CHAIN_ORDER_KEY] = nil
        changed = true
    end
    if md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
        md[PERSIST_CHAIN_TOKEN_KEY] = nil
        changed = true
    end

    if changed then
        vehicle:transmitModData()
    end
end

local function clearPersistedFrontLinkData(rearVehicle)
    if not rearVehicle then return end
    local md = rearVehicle:getModData()
    if not md then return end

    local changed = false
    if md[PERSIST_FRONT_SQL_KEY] ~= nil then
        md[PERSIST_FRONT_SQL_KEY] = nil
        changed = true
    end
    if md[PERSIST_FRONT_ID_KEY] ~= nil then
        md[PERSIST_FRONT_ID_KEY] = nil
        changed = true
    end
    if md[PERSIST_ATTACHMENT_A_KEY] ~= nil then
        md[PERSIST_ATTACHMENT_A_KEY] = nil
        changed = true
    end
    if md[PERSIST_ATTACHMENT_B_KEY] ~= nil then
        md[PERSIST_ATTACHMENT_B_KEY] = nil
        changed = true
    end

    if changed then
        rearVehicle:transmitModData()
    end
end

local function resolveDetachPair(vehicleA, vehicleB)
    local front = vehicleA
    local rear = vehicleB

    if front and rear then
        if front:getVehicleTowing() == rear and rear:getVehicleTowedBy() == front then
            return front, rear
        end
        if rear:getVehicleTowing() == front and front:getVehicleTowedBy() == rear then
            return rear, front
        end
    end

    if front and not rear then
        local directRear = front:getVehicleTowing()
        if directRear and directRear:getVehicleTowedBy() == front then
            return front, directRear
        end
        local directFront = front:getVehicleTowedBy()
        if directFront and directFront:getVehicleTowing() == front then
            return directFront, front
        end
    end

    if rear and not front then
        local directFront = rear:getVehicleTowedBy()
        if directFront and directFront:getVehicleTowing() == rear then
            return directFront, rear
        end
        local directRear = rear:getVehicleTowing()
        if directRear and directRear:getVehicleTowedBy() == rear then
            return rear, directRear
        end
    end

    return nil, nil
end

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

local function vehicleHasAttachment(vehicle, attachmentId)
    if not vehicle or not attachmentId then return false end
    local script = vehicle:getScript()
    return script ~= nil and script:getAttachmentById(attachmentId) ~= nil
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 hasValidTowLink(vehicle)
    if not vehicle then return false end
    local rear = vehicle:getVehicleTowing()
    if rear and rear:getVehicleTowedBy() == vehicle then
        return true
    end
    local front = vehicle:getVehicleTowedBy()
    if front and front:getVehicleTowing() == vehicle then
        return true
    end
    return false
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 resolveSnapshotAttachmentPair(vehicleA, vehicleB)
    local selfA = vehicleA and vehicleA:getTowAttachmentSelf() or nil
    local selfB = vehicleB and vehicleB:getTowAttachmentSelf() or nil
    local candidates = {
        { selfA, selfB },
        { "trailer", "trailerfront" },
        { "trailerfront", "trailer" }
    }

    for _, pair in ipairs(candidates) do
        local attachmentA = pair[1]
        local attachmentB = pair[2]
        if attachmentA and attachmentB and attachmentA ~= attachmentB
            and vehicleHasAttachment(vehicleA, attachmentA) and vehicleHasAttachment(vehicleB, attachmentB) then
            return attachmentA, attachmentB
        end
    end

    return "trailer", "trailerfront"
end

local function snapshotChainFromHead(headVehicle, visitedIds)
    if not headVehicle then return end

    local headSql = headVehicle.getSqlId and tonumber(headVehicle:getSqlId()) or nil
    if not (headSql and headSql > 0) then
        headSql = nil
    end
    local headMd = headVehicle:getModData()
    local chainToken = headMd and tostring(headMd[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
    if chainToken == "" then
        if headSql then
            chainToken = "sql:" .. tostring(headSql)
        else
            chainToken = "veh:" .. tostring(headVehicle:getId())
        end
    end
    if headVehicle:getVehicleTowedBy() == nil then
        clearPersistedFrontLinkData(headVehicle)
    end

    local cursor = headVehicle
    local order = 1
    local localVisited = {}
    while cursor do
        local cursorId = cursor:getId()
        if visitedIds[cursorId] or localVisited[cursorId] then
            break
        end

        visitedIds[cursorId] = true
        localVisited[cursorId] = true
        storePersistedChainOrderData(cursor, headSql, order, chainToken)

        local rearVehicle = cursor:getVehicleTowing()
        if rearVehicle and rearVehicle:getVehicleTowedBy() == cursor then
            local attachmentA, attachmentB = resolveSnapshotAttachmentPair(cursor, rearVehicle)
            storePersistedFrontLinkData(cursor, rearVehicle, attachmentA, attachmentB)
        end

        cursor = rearVehicle
        order = order + 1
    end
end

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

    local visitedIds = {}

    for i = 0, list:size() - 1 do
        local vehicle = list:get(i)
        local vehicleId = vehicle and vehicle:getId() or nil
        if vehicleId and not visitedIds[vehicleId] and vehicle:getVehicleTowedBy() == nil then
            local rearVehicle = vehicle:getVehicleTowing()
            if rearVehicle and rearVehicle:getVehicleTowedBy() == vehicle then
                snapshotChainFromHead(vehicle, visitedIds)
            end
        end
    end

    for i = 0, list:size() - 1 do
        local vehicle = list:get(i)
        local vehicleId = vehicle and vehicle:getId() or nil
        if vehicleId and not visitedIds[vehicleId] and hasValidTowLink(vehicle) then
            snapshotChainFromHead(vehicle, visitedIds)
        end
    end
end

local function processAttach(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
        log("attach sync skipped: missing vehicle A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
        return
    end

    local attachmentA = resolveAttachmentA(args, vehicleA)
    local attachmentB = resolveAttachmentB(args, vehicleB)

    local linked = isLinked(vehicleA, vehicleB)
    if not linked then
        if not vehicleHasAttachment(vehicleA, attachmentA) or not vehicleHasAttachment(vehicleB, attachmentB) then
            log("attach sync failed: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB))
            return
        end

        vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
        linked = isLinked(vehicleA, vehicleB)
        if not linked then
            log("attach sync failed: server link not established A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
            return
        end
    else
        log("attach sync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
    end

    storePersistedFrontLinkData(vehicleA, vehicleB, attachmentA, attachmentB)
    snapshotActiveTowLinksServer()

    log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
        .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
    sendServerCommand("landtrain", "forceAttachSync", {
        vehicleA = vehicleA:getId(),
        vehicleB = vehicleB:getId(),
        attachmentA = attachmentA,
        attachmentB = attachmentB
    })
end

local function processDetach(item)
    local args = item.args or {}
    local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
    local vehicleBId = args.vehicleB
    local vehicleA = vehicleAId and getVehicleById(vehicleAId) or nil
    local vehicleB = vehicleBId and getVehicleById(vehicleBId) or nil
    local _, resolvedRear = resolveDetachPair(vehicleA, vehicleB)
    if resolvedRear then
        clearPersistedFrontLinkData(resolvedRear)
        clearPersistedChainOrderData(resolvedRear)
    elseif vehicleB then
        clearPersistedFrontLinkData(vehicleB)
        clearPersistedChainOrderData(vehicleB)
    end

    sendServerCommand("landtrain", "forceDetachSync", {
        vehicleA = vehicleAId,
        vehicleB = vehicleBId
    })
    snapshotActiveTowLinksServer()
end

local function processPending()
    snapshotTickCounter = snapshotTickCounter + 1
    if snapshotTickCounter >= SNAPSHOT_INTERVAL_TICKS then
        snapshotTickCounter = 0
        snapshotActiveTowLinksServer()
    end

    if #pending == 0 then return end

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

local function onClientCommand(module, command, player, args)
    if module == "vehicle" and command == "attachTrailer" then
        log("queue attach from " .. tostring(player and player:getUsername() or "unknown")
            .. " A=" .. tostring(args and args.vehicleA) .. " B=" .. tostring(args and args.vehicleB)
            .. " attachmentA=" .. tostring(args and args.attachmentA) .. " attachmentB=" .. tostring(args and args.attachmentB))
        queueSync("attach", player, args)
    elseif module == "vehicle" and command == "detachTrailer" then
        log("queue detach(vehicle) from " .. tostring(player and player:getUsername() or "unknown"))
        queueSync("detach", player, args)
    elseif module == "towbar" and command == "detachTowBar" then
        log("queue detach(towbar) from " .. tostring(player and player:getUsername() or "unknown"))
        queueSync("detach", player, args)
    end
end

Events.OnClientCommand.Add(onClientCommand)
Events.OnTick.Add(processPending)

log("LandtrainTowSyncServer loaded")
