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

TowBarMod.Utils = TowBarMod.Utils or {}
TowBarMod.UI = TowBarMod.UI or {}
TowBarMod.Hook = TowBarMod.Hook or {}

local LT = TowBarMod.Landtrain
local MAX_DIST_SQ = 3.25
local MAX_DZ = 0.9
local tmpA = Vector3f.new()
local tmpB = Vector3f.new()
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] " .. tostring(msg))
    if type(writeLog) == "function" then
        pcall(writeLog, "Landtrain", "[Landtrain] " .. tostring(msg))
    end
end

local function vehicleId(vehicle)
    return vehicle and vehicle:getId() or "nil"
end

local function hasAttachment(vehicle, attachmentId)
    if vehicle == nil or attachmentId == nil then return false end
    local script = vehicle:getScript()
    return script ~= nil and script:getAttachmentById(attachmentId) ~= nil
end

local function isTrailerScriptName(scriptName)
    if not scriptName then return false end
    return string.find(string.lower(scriptName), "trailer", 1, true) ~= nil
end

local function shouldForceNotTowingScript(vehicle)
    if not vehicle then return false end
    local scriptName = vehicle:getScriptName()
    return not isTrailerScriptName(scriptName)
end

local function restoreTrailerScriptIfNeeded(vehicle)
    if not vehicle then return end
    if vehicle:getScriptName() ~= "notTowingA_Trailer" then return end

    local md = vehicle:getModData()
    local originalScript = md and md.towBarOriginalScriptName or nil
    if originalScript and isTrailerScriptName(originalScript) then
        vehicle:setScriptName(originalScript)
    end
end

local function sideFree(vehicle, attachmentId)
    if vehicle == nil then return false end
    if attachmentId == "trailer" then return vehicle:getVehicleTowing() == nil end
    if attachmentId == "trailerfront" then return vehicle:getVehicleTowedBy() == nil end
    return true
end

local function wouldCreateTowLoop(towingVehicle, towedVehicle)
    if towingVehicle == nil or towedVehicle == nil then return false end
    if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return true end

    local visited = {}
    local cursor = towedVehicle
    while cursor do
        local id = cursor:getId()
        if visited[id] then return true end
        if id == towingVehicle:getId() then return true end
        visited[id] = true
        cursor = cursor:getVehicleTowing()
    end

    visited = {}
    cursor = towingVehicle
    while cursor do
        local id = cursor:getId()
        if visited[id] then return true end
        if id == towedVehicle:getId() then return true end
        visited[id] = true
        cursor = cursor:getVehicleTowedBy()
    end

    return false
end

local function closeEnoughFallback(vehicleA, vehicleB, attachmentA, attachmentB)
    local posA = vehicleA:getAttachmentWorldPos(attachmentA, tmpA)
    local posB = vehicleB:getAttachmentWorldPos(attachmentB, tmpB)
    if not posA or not posB then return false end
    local dx = posA:x() - posB:x()
    local dy = posA:y() - posB:y()
    local dz = posA:z() - posB:z()
    local distSq = dx * dx + dy * dy + dz * dz
    return distSq <= MAX_DIST_SQ and math.abs(dz) <= MAX_DZ
end

local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB, allowOccupiedSides)
    if not vehicleA or not vehicleB or vehicleA == vehicleB then return false end
    if wouldCreateTowLoop(vehicleA, vehicleB) then return false end
    if not hasAttachment(vehicleA, attachmentA) or not hasAttachment(vehicleB, attachmentB) then return false end
    if not allowOccupiedSides and (not sideFree(vehicleA, attachmentA) or not sideFree(vehicleB, attachmentB)) then return false end
    if vehicleA:getVehicleTowing() == vehicleB or vehicleA:getVehicleTowedBy() == vehicleB then return false end

    if vehicleA:canAttachTrailer(vehicleB, attachmentA, attachmentB) then return true end

    local eitherLinked = vehicleA:getVehicleTowing() ~= nil or vehicleA:getVehicleTowedBy() ~= nil
        or vehicleB:getVehicleTowing() ~= nil or vehicleB:getVehicleTowedBy() ~= nil
    return eitherLinked and closeEnoughFallback(vehicleA, vehicleB, attachmentA, attachmentB)
end

local function resolvePair(towingVehicle, towedVehicle, preferredA, preferredB)
    local pairs = {
        { preferredA, preferredB },
        { towingVehicle and towingVehicle:getTowAttachmentSelf() or nil, towedVehicle and towedVehicle:getTowAttachmentSelf() or nil },
        { "trailer", "trailerfront" },
        { "trailerfront", "trailer" }
    }

    for _, pair in ipairs(pairs) do
        local a, b = pair[1], pair[2]
        if a and b and a ~= b and canTowByLandtrain(towingVehicle, towedVehicle, a, b, true) then
            return a, b
        end
    end

    for _, pair in ipairs(pairs) do
        local a, b = pair[1], pair[2]
        if a and b and a ~= b and hasAttachment(towingVehicle, a) and hasAttachment(towedVehicle, b) then
            return a, b
        end
    end

    return "trailer", "trailerfront"
end

local function resolveMenuPair(towingVehicle, towedVehicle, preferredA, preferredB)
    local candidates = {
        { preferredA, preferredB },
        { "trailer", "trailerfront" },
        { towingVehicle and towingVehicle:getTowAttachmentSelf() or nil, towedVehicle and towedVehicle:getTowAttachmentSelf() or nil },
        { "trailerfront", "trailer" }
    }

    for _, pair in ipairs(candidates) do
        local a, b = pair[1], pair[2]
        if a and b and a ~= b
            and hasAttachment(towingVehicle, a)
            and hasAttachment(towedVehicle, b)
            and canTowByLandtrain(towingVehicle, towedVehicle, a, b, false) then
            return a, b
        end
    end

    return nil, nil
end

local function clearChangedOffset(vehicle)
    if not vehicle then return end
end

local function restoreTowBarDefaults(vehicle)
    if not vehicle then return end

    local script = vehicle:getScript()
    if script and script.getMass then
        local scriptMass = tonumber(script:getMass())
        if scriptMass and scriptMass > 0 then
            vehicle:setMass(scriptMass)
        end
    end

    vehicle:constraintChanged()
    vehicle:updateTotalMass()
end

local function applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
    if not TowBarMod.Utils or not TowBarMod.Utils.updateAttachmentsForRigidTow then return end
    clearChangedOffset(towedVehicle)
    TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
end

local function setTowBarModelVisible(vehicle, isVisible)
    if not vehicle then return end
    local part = vehicle:getPartById("towbar")
    if part == nil then return end

    for j = 0, 23 do
        part:setModelVisible("towbar" .. j, false)
    end

    if isVisible then
        local script = vehicle:getScript()
        if script then
            local scale = script:getModelScale()
            if scale >= 1.5 and scale <= 2 then
                local z = script:getPhysicsChassisShape():z() / 2 - 0.1
                local index = math.floor((z * 2 / 3 - 1) * 10)
                if index < 0 then index = 0 end
                if index > 23 then index = 23 end
                part:setModelVisible("towbar" .. index, true)
            end
        end
    end

    vehicle:doDamageOverlay()
end

local function refreshTowBarState(vehicle)
    if not vehicle then return end
    setTowBarModelVisible(vehicle, vehicle:getVehicleTowedBy() ~= nil)
end

local function refreshAround(vehicle)
    if not vehicle then return end
    refreshTowBarState(vehicle)
    refreshTowBarState(vehicle:getVehicleTowedBy())
    refreshTowBarState(vehicle:getVehicleTowing())
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 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 getLoadedVehicles()
    local results = {}
    local cell = getCell()
    if not cell then return results end
    local list = cell:getVehicles()
    if not list then return results end

    for i = 0, list:size() - 1 do
        local vehicle = list:get(i)
        if vehicle then
            table.insert(results, vehicle)
        end
    end
    return results
end

local function indexVehiclesBySql(vehicles)
    local bySql = {}
    for _, vehicle in ipairs(vehicles) do
        local sqlId = vehicle and vehicle.getSqlId and tonumber(vehicle:getSqlId()) or nil
        if sqlId and sqlId > 0 then
            bySql[sqlId] = vehicle
        end
    end
    return bySql
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 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 = resolvePair(cursor, rearVehicle)
            storePersistedFrontLinkData(cursor, rearVehicle, attachmentA, attachmentB)
        end

        cursor = rearVehicle
        order = order + 1
    end
end

local function snapshotActiveTowLinks()
    local vehicles = getLoadedVehicles()
    if #vehicles == 0 then return end

    local visitedIds = {}

    for _, vehicle in ipairs(vehicles) do
        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 _, vehicle in ipairs(vehicles) do
        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 restoreDefaultsIfLeadLost(vehicle)
    if not vehicle then return end
    if vehicle:getVehicleTowedBy() ~= nil then return end

    clearChangedOffset(vehicle)
    restoreTowBarDefaults(vehicle)
    clearPersistedFrontLinkData(vehicle)
    if vehicle:getVehicleTowing() == nil then
        clearPersistedChainOrderData(vehicle)
    end

    local md = vehicle:getModData()
    if md then
        md["towed"] = false
        if vehicle:getVehicleTowing() == nil then
            md["isTowingByTowBar"] = false
            md.towBarOriginalScriptName = nil
            md.towBarOriginalMass = nil
            md.towBarOriginalBrakingForce = nil
        end
        vehicle:transmitModData()
    end

    setTowBarModelVisible(vehicle, false)
end

local function queueAction(playerObj, delay, fn, arg)
    if not playerObj or not fn then return end
    if TowBarScheduleAction == nil then
        fn(playerObj, arg)
        return
    end
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay or 10, fn, arg))
end

local function sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
    if not playerObj or not towingVehicle or not towedVehicle then return end
    if towingVehicle:getId() == towedVehicle:getId() then return end

    sendClientCommand(playerObj, "vehicle", "attachTrailer", {
        vehicleA = towingVehicle:getId(),
        vehicleB = towedVehicle:getId(),
        attachmentA = attachmentA,
        attachmentB = attachmentB
    })

    if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
        queueAction(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)
    end
end

local function isLinkedPair(towingVehicle, towedVehicle)
    if not towingVehicle or not towedVehicle then return false end
    return towingVehicle:getVehicleTowing() == towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle
end

local function markTowBarPair(towingVehicle, towedVehicle)
    local towingMd = towingVehicle and towingVehicle:getModData() or nil
    local towedMd = towedVehicle and towedVehicle:getModData() or nil

    if towingMd then
        local towingChanged = false
        if towingMd["isTowingByTowBar"] ~= true then
            towingMd["isTowingByTowBar"] = true
            towingChanged = true
        end
        if towingChanged then
            towingVehicle:transmitModData()
        end
    end
    if towedMd then
        local towedChanged = false
        local currentScript = towedVehicle and towedVehicle:getScriptName() or nil
        if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
            towedMd.towBarOriginalScriptName = currentScript
            towedChanged = true
        end
        if towedMd.towBarOriginalMass == nil and towedVehicle then
            towedMd.towBarOriginalMass = towedVehicle:getMass()
            towedChanged = true
        end
        if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then
            towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
            towedChanged = true
        end
        if towedMd["isTowingByTowBar"] ~= true then
            towedMd["isTowingByTowBar"] = true
            towedChanged = true
        end
        if towedMd["towed"] ~= true then
            towedMd["towed"] = true
            towedChanged = true
        end
        if towedChanged then
            towedVehicle:transmitModData()
        end
    end
end

local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, preferredA, preferredB, alwaysSend)
    if not playerObj or not towingVehicle or not towedVehicle then
        log("ensurePairAttached skipped: missing refs A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle)))
        return false
    end
    if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then
        log("ensurePairAttached skipped: identical vehicles id=" .. tostring(vehicleId(towingVehicle)))
        return false
    end

    restoreTrailerScriptIfNeeded(towingVehicle)

    if wouldCreateTowLoop(towingVehicle, towedVehicle) then
        log("ensurePairAttached skipped: loop A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle)))
        return false
    end

    local attachmentA, attachmentB = resolvePair(towingVehicle, towedVehicle, preferredA, preferredB)
    if not hasAttachment(towingVehicle, attachmentA) or not hasAttachment(towedVehicle, attachmentB) then
        log("ensurePairAttached skipped: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB)
            .. " ids=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle)))
        return false
    end

    applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
    towedVehicle:setScriptName("notTowingA_Trailer")
    markTowBarPair(towingVehicle, towedVehicle)
    storePersistedFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)

    local linked = isLinkedPair(towingVehicle, towedVehicle)
    log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
        .. " linked=" .. tostring(linked) .. " alwaysSend=" .. tostring(alwaysSend)
        .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))

    if linked and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
        pcall(TowBarMod.Hook.setVehiclePostAttach, nil, towedVehicle)
    end

    if alwaysSend or not linked then
        sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
    elseif TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
        queueAction(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)
    end

    setTowBarModelVisible(towedVehicle, true)
    refreshAround(towingVehicle)
    refreshAround(towedVehicle)
    snapshotActiveTowLinks()
    return true
end

local function captureFrontLinks(middleVehicle)
    if not middleVehicle then return nil end

    local links = {}
    local cursor = middleVehicle
    local visited = {}

    while cursor do
        if visited[cursor] then break end
        visited[cursor] = true

        local frontVehicle = cursor:getVehicleTowedBy()
        if not frontVehicle then break end
        if frontVehicle == cursor or visited[frontVehicle] then break end

        local a, b = resolvePair(frontVehicle, cursor)
        table.insert(links, { front = frontVehicle, middle = cursor, a = a, b = b })
        cursor = frontVehicle
    end

    if #links == 0 then return nil end
    return links
end

local function restoreFrontLink(playerObj, link)
    if not link then return end
    local front = link.front
    local middle = link.middle
    if not front or not middle then return end
    if middle:getVehicleTowedBy() ~= nil then return end
    if front == middle or front:getId() == middle:getId() then return end
    if wouldCreateTowLoop(front, middle) then return end

    ensurePairAttached(playerObj, front, middle, link.a, link.b, false)
end

local function restoreFrontLinks(playerObj, links)
    if not links then return end
    for _, link in ipairs(links) do
        restoreFrontLink(playerObj, link)
    end
end

local function queueRestoreFrontLinks(playerObj, links, delay)
    if not playerObj or not links then return end
    queueAction(playerObj, delay or 12, function(character, linksArg)
        restoreFrontLinks(character, linksArg)
    end, links)
end

local function resolveDetachPair(towingVehicle, towedVehicle)
    local resolvedTowing = towingVehicle
    local resolvedTowed = resolvedTowing and resolvedTowing:getVehicleTowing() or nil

    if resolvedTowed == nil and towedVehicle == nil and resolvedTowing and resolvedTowing:getVehicleTowedBy() then
        local front = resolvedTowing:getVehicleTowedBy()
        if front and front:getVehicleTowing() == resolvedTowing then
            resolvedTowed = resolvedTowing
            resolvedTowing = front
        end
    end

    if resolvedTowed == nil and towedVehicle and towedVehicle:getVehicleTowedBy() then
        resolvedTowing = towedVehicle:getVehicleTowedBy()
        resolvedTowed = towedVehicle
    elseif resolvedTowed == nil then
        resolvedTowed = towedVehicle
    end

    if resolvedTowing == nil and resolvedTowed then
        resolvedTowing = resolvedTowed:getVehicleTowedBy()
    end

    if resolvedTowing and resolvedTowed and resolvedTowed:getVehicleTowedBy() ~= resolvedTowing then
        local directRear = resolvedTowing:getVehicleTowing()
        if directRear and directRear:getVehicleTowedBy() == resolvedTowing then
            resolvedTowed = directRear
        else
            return nil, nil
        end
    end

    return resolvedTowing, resolvedTowed
end

local function getDetachAction()
    return TowBarMod.Hook and (TowBarMod.Hook.deattachTowBarAction or TowBarMod.Hook.detachTowBarAction) or nil
end

local function getPerformDetachHook()
    return TowBarMod.Hook and (TowBarMod.Hook.performDetachTowBar or TowBarMod.Hook.performDeattachTowBar) or nil
end

local function getLandtrainHookTypeVariants(vehicleA, vehicleB, hasTowBar)
    local variants = {}
    if not hasTowBar or not vehicleA or not vehicleB or vehicleA == vehicleB then return variants end

    restoreTrailerScriptIfNeeded(vehicleA)
    restoreTrailerScriptIfNeeded(vehicleB)

    local function addVariant(towingVehicle, towedVehicle)
        local attachmentA, attachmentB = resolveMenuPair(towingVehicle, towedVehicle)
        if not attachmentA or not attachmentB then
            return false
        end

        table.insert(variants, {
            name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(towedVehicle) .. "\n" .. getText("UI_Text_Towing_byTowBar"),
            func = TowBarMod.Hook.attachByTowBarAction,
            towingVehicle = towingVehicle,
            towedVehicle = towedVehicle,
            towingPoint = attachmentA,
            towedPoint = attachmentB,
            textureName = "tow_bar_icon"
        })
        return true
    end

    local addedForward = addVariant(vehicleA, vehicleB)
    if not addedForward then
        addVariant(vehicleB, vehicleA)
    end

    return variants
end

local function menuHasTowbarAttachSlice(menu)
    if menu == nil or menu.slices == nil then return false end
    local attachAction = TowBarMod.Hook and TowBarMod.Hook.attachByTowBarAction or nil
    local chooseAction = TowBarMod.UI and TowBarMod.UI.showChooseVehicleMenu or nil
    if attachAction == nil and chooseAction == nil then return false end

    for _, slice in ipairs(menu.slices) do
        local command = slice.command and slice.command[1]
        if command == attachAction or command == chooseAction then
            return true
        end
    end
    return false
end

local function menuHasTowbarDetachSlice(menu)
    if menu == nil or menu.slices == nil then return false end
    local detachAction = getDetachAction()
    if detachAction == nil then return false end

    for _, slice in ipairs(menu.slices) do
        local command = slice.command and slice.command[1]
        if command == detachAction then
            return true
        end
    end
    return false
end

local function getNearbyTargets(mainVehicle)
    local targets = {}
    local square = mainVehicle and mainVehicle:getSquare() or nil
    if square == nil then return targets end

    for y = square:getY() - 6, square:getY() + 6 do
        for x = square:getX() - 6, square:getX() + 6 do
            local square2 = getCell():getGridSquare(x, y, square:getZ())
            if square2 then
                for i = 1, square2:getMovingObjects():size() do
                    local obj = square2:getMovingObjects():get(i - 1)
                    if obj and instanceof(obj, "BaseVehicle") and obj ~= mainVehicle then
                        local variants = getLandtrainHookTypeVariants(mainVehicle, obj, true)
                        if #variants > 0 then
                            table.insert(targets, { vehicle = obj, variants = variants })
                        end
                    end
                end
            end
        end
    end

    return targets
end

local function addAttachOptionToMenu(playerObj, vehicle)
    if playerObj == nil or vehicle == nil then return end
    local inventory = playerObj:getInventory()
    if inventory == nil or inventory:getItemFromTypeRecurse("TowBar.TowBar") == nil then return end

    local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
    if menu == nil then return end

    local targets = getNearbyTargets(vehicle)
    if #targets == 0 then return end

    if #targets == 1 then
        local hookType = targets[1].variants[1]
        menu:addSlice(hookType.name, getTexture("media/textures/tow_bar_attach.png"), hookType.func, playerObj, hookType.towingVehicle, hookType.towedVehicle, hookType.towingPoint, hookType.towedPoint)
        return
    end

    local vehicleList = {}
    for _, entry in ipairs(targets) do table.insert(vehicleList, entry.vehicle) end
    menu:addSlice(getText("UI_Text_Towing_attach") .. "...", getTexture("media/textures/tow_bar_attach.png"), TowBarMod.UI.showChooseVehicleMenu, playerObj, vehicle, vehicleList, true)
end

local function addDetachOptionToMenu(playerObj, vehicle)
    if playerObj == nil or vehicle == nil then return end
    local detachAction = getDetachAction()
    if detachAction == nil then return end

    local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
    if menu == nil or menuHasTowbarDetachSlice(menu) then return end
    if vehicle:getVehicleTowing() == nil and vehicle:getVehicleTowedBy() == nil then return end

    local target = vehicle:getVehicleTowing() or vehicle
    menu:addSlice(getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(target)), getTexture("media/textures/tow_bar_detach.png"), detachAction, playerObj, target)
end

LT.getHookTypeVariantsWrapper = function(vehicleA, vehicleB, hasTowBar)
    return getLandtrainHookTypeVariants(vehicleA, vehicleB, hasTowBar)
end

LT.performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
    local original = TowBarMod.Hook and TowBarMod.Hook._ltOriginalPerformAttach or nil
    if original == nil or towingVehicle == nil or towedVehicle == nil then return end
    if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return end
    if wouldCreateTowLoop(towingVehicle, towedVehicle) then return end

    local frontLinks = captureFrontLinks(towingVehicle)
    log("performAttachWrapper begin pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
        .. " frontLinks=" .. tostring(frontLinks and #frontLinks or 0))
    original(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)

    restoreFrontLinks(playerObj, frontLinks)
    queueRestoreFrontLinks(playerObj, frontLinks, 12)
    queueRestoreFrontLinks(playerObj, frontLinks, 30)

    ensurePairAttached(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB, false)

    local attachState = {
        towing = towingVehicle,
        towed = towedVehicle,
        attachmentA = attachmentA,
        attachmentB = attachmentB
    }
    queueAction(playerObj, 14, function(character, state)
        if not state then return end
        ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true)
    end, attachState)
    queueAction(playerObj, 34, function(character, state)
        if not state then return end
        ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true)
    end, attachState)
end

LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
    local original = TowBarMod.Hook and TowBarMod.Hook._ltOriginalPerformDetach or nil
    if original == nil then return end

    local resolvedTowing, resolvedTowed = resolveDetachPair(towingVehicle, towedVehicle)
    if resolvedTowing == nil or resolvedTowed == nil then return end

    local frontLinks = captureFrontLinks(resolvedTowing)
    original(playerObj, resolvedTowing, resolvedTowed)

    restoreFrontLinks(playerObj, frontLinks)
    queueRestoreFrontLinks(playerObj, frontLinks, 12)
    queueRestoreFrontLinks(playerObj, frontLinks, 30)

    restoreDefaultsIfLeadLost(resolvedTowing)
    restoreDefaultsIfLeadLost(resolvedTowed)
    queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowing)
    queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed)

    setTowBarModelVisible(resolvedTowed, false)
    snapshotActiveTowLinks()
end

LT.showRadialWrapper = function(playerObj)
    local original = TowBarMod.UI and TowBarMod.UI._ltOriginalShowRadial or nil
    if original then original(playerObj) end

    if playerObj == nil or playerObj:getVehicle() then return end
    if ISVehicleMenu == nil or ISVehicleMenu.getVehicleToInteractWith == nil then return end

    local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
    if vehicle == nil then return end

    local linked = vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
    if not linked then return end

    if TowBarMod.UI and TowBarMod.UI.removeDefaultDetachOption then
        TowBarMod.UI.removeDefaultDetachOption(playerObj)
    end
    addDetachOptionToMenu(playerObj, vehicle)

    local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
    if menu and not menuHasTowbarAttachSlice(menu) then
        addAttachOptionToMenu(playerObj, vehicle)
    end
end

LT.onDetachTrailerWrapper = function(playerObj, vehicle, ...)
    local original = TowBarMod.UI and TowBarMod.UI._ltOriginalOnDetachTrailer or nil
    local detachAction = getDetachAction()

    local vehicleToCheck = vehicle
    if vehicleToCheck == nil and playerObj and ISVehicleMenu and ISVehicleMenu.getVehicleToInteractWith then
        vehicleToCheck = ISVehicleMenu.getVehicleToInteractWith(playerObj)
    end

    if playerObj and vehicleToCheck and detachAction then
        if vehicleToCheck:getVehicleTowing() or vehicleToCheck:getVehicleTowedBy() then
            detachAction(playerObj, vehicleToCheck)
            return
        end
    end

    if original then
        return original(playerObj, vehicle, ...)
    end
end

local function collectPersistedLinks(vehicles)
    local links = {}

    for _, rearVehicle in ipairs(vehicles) do
        local md = rearVehicle and rearVehicle:getModData() or nil
        local frontSql = md and tonumber(md[PERSIST_FRONT_SQL_KEY]) or nil
        local headSql = md and tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
        local rearOrder = md and tonumber(md[PERSIST_CHAIN_ORDER_KEY]) or nil
        local chainToken = md and tostring(md[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
        if not (headSql and headSql > 0) then headSql = nil end
        if not (rearOrder and rearOrder >= 2) then rearOrder = nil end
        if chainToken == "" then chainToken = nil end
        if (frontSql and frontSql > 0) or ((headSql or chainToken) and rearOrder) then
            local rearSql = rearVehicle.getSqlId and tonumber(rearVehicle:getSqlId()) or nil
            local link = {
                rear = rearVehicle,
                rearSql = rearSql,
                frontSql = (frontSql and frontSql > 0) and frontSql or nil,
                headSql = headSql,
                rearOrder = rearOrder,
                chainToken = chainToken,
                attachmentA = md[PERSIST_ATTACHMENT_A_KEY] or "trailer",
                attachmentB = md[PERSIST_ATTACHMENT_B_KEY] or "trailerfront"
            }
            table.insert(links, link)
        end
    end

    return links
end

local function computePersistedDepth(link, byRearSql, visitedSql)
    if not link or not link.frontSql then return 0 end
    visitedSql = visitedSql or {}
    if visitedSql[link.frontSql] then return 0 end

    local frontLink = byRearSql[link.frontSql]
    if not frontLink then return 0 end

    visitedSql[link.frontSql] = true
    return 1 + computePersistedDepth(frontLink, byRearSql, visitedSql)
end

local function filterAmbiguousPersistedLinks(links)
    if #links <= 1 then return links end

    local frontCounts = {}
    for _, link in ipairs(links) do
        if link.frontSql then
            frontCounts[link.frontSql] = (frontCounts[link.frontSql] or 0) + 1
        end
    end

    local filtered = {}
    local dropped = 0
    for _, link in ipairs(links) do
        if link.frontSql and frontCounts[link.frontSql] and frontCounts[link.frontSql] > 1 then
            dropped = dropped + 1
        else
            table.insert(filtered, link)
        end
    end

    if dropped > 0 then
        log("load rebuild skipped ambiguous links dropped=" .. tostring(dropped))
    end

    return filtered
end

local function linkOrderKey(link)
    if not link then return nil end
    if link.headSql and link.headSql > 0 then
        return "sql:" .. tostring(link.headSql)
    end
    if link.chainToken and tostring(link.chainToken) ~= "" then
        return "tok:" .. tostring(link.chainToken)
    end
    return nil
end

local function filterConflictingOrderedLinks(links)
    if #links <= 1 then return links end

    local orderSlots = {}
    for _, link in ipairs(links) do
        local orderKey = linkOrderKey(link)
        if orderKey and link.rearOrder and link.rearOrder >= 2 then
            local key = orderKey .. ":" .. tostring(link.rearOrder)
            orderSlots[key] = (orderSlots[key] or 0) + 1
        end
    end

    local filtered = {}
    local dropped = 0
    for _, link in ipairs(links) do
        local orderKey = linkOrderKey(link)
        if orderKey and link.rearOrder and link.rearOrder >= 2 then
            local key = orderKey .. ":" .. tostring(link.rearOrder)
            if (orderSlots[key] or 0) > 1 then
                dropped = dropped + 1
            else
                table.insert(filtered, link)
            end
        else
            table.insert(filtered, link)
        end
    end

    if dropped > 0 then
        log("load rebuild skipped conflicting ordered links dropped=" .. tostring(dropped))
    end

    return filtered
end

local function indexPersistedLinksByRearSql(links)
    local byRearSql = {}
    for _, link in ipairs(links) do
        if link.rearSql and link.rearSql > 0 then
            byRearSql[link.rearSql] = link
        end
    end
    return byRearSql
end

local function indexVehiclesByHeadAndOrder(vehicles)
    local byHead = {}
    for _, vehicle in ipairs(vehicles) do
        local md = vehicle and vehicle:getModData() or nil
        local headSql = md and tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
        local order = md and tonumber(md[PERSIST_CHAIN_ORDER_KEY]) or nil
        local chainToken = md and tostring(md[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
        if chainToken == "" then chainToken = nil end
        local key = nil
        if headSql and headSql > 0 then
            key = "sql:" .. tostring(headSql)
        elseif chainToken then
            key = "tok:" .. tostring(chainToken)
        end
        if key and order and order > 0 then
            byHead[key] = byHead[key] or {}
            if byHead[key][order] == nil then
                byHead[key][order] = vehicle
            else
                byHead[key][order] = false
            end
        end
    end
    return byHead
end

local function sortPersistedLinks(links, byRearSql)
    table.sort(links, function(a, b)
        local orderKeyA = linkOrderKey(a)
        local orderKeyB = linkOrderKey(b)
        if orderKeyA and orderKeyB then
            if orderKeyA ~= orderKeyB then
                return orderKeyA < orderKeyB
            end
            if a.rearOrder and b.rearOrder and a.rearOrder ~= b.rearOrder then
                return a.rearOrder < b.rearOrder
            end
        end

        local depthA = computePersistedDepth(a, byRearSql, {})
        local depthB = computePersistedDepth(b, byRearSql, {})
        if depthA == depthB then
            return vehicleId(a.rear) < vehicleId(b.rear)
        end
        return depthA < depthB
    end)
end

local function isTowbarPairStateComplete(frontVehicle, rearVehicle)
    return isLinkedPair(frontVehicle, rearVehicle)
end

local function restoreViaAttachWrapper(playerObj, frontVehicle, rearVehicle, attachmentA, attachmentB)
    if not playerObj or not frontVehicle or not rearVehicle then return false end
    if not LT.performAttachWrapper then return false end
    if not TowBarMod or not TowBarMod.Hook or TowBarMod.Hook._ltOriginalPerformAttach == nil then
        return false
    end

    local ok, err = pcall(LT.performAttachWrapper, playerObj, frontVehicle, rearVehicle, attachmentA, attachmentB)
    if not ok then
        log("load rebuild wrapper attach failed pair=" .. tostring(vehicleId(frontVehicle)) .. "->" .. tostring(vehicleId(rearVehicle))
            .. " err=" .. tostring(err))
        return false
    end
    return true
end

local function restorePersistedLink(playerObj, link, bySql, byHeadOrder)
    if not playerObj or not link then return false end

    local rearVehicle = link.rear
    local frontVehicle = nil
    if link.frontSql and bySql then
        frontVehicle = bySql[link.frontSql]
    end
    local orderKey = linkOrderKey(link)
    if not frontVehicle and orderKey and link.rearOrder and link.rearOrder >= 2 and byHeadOrder then
        local byOrder = byHeadOrder[orderKey]
        if byOrder then
            local candidate = byOrder[link.rearOrder - 1]
            if candidate and candidate ~= false then
                frontVehicle = candidate
            end
        end
    end
    if not rearVehicle or not frontVehicle then return false end
    if rearVehicle == frontVehicle or rearVehicle:getId() == frontVehicle:getId() then return false end

    if isTowbarPairStateComplete(frontVehicle, rearVehicle) then
        return false
    end

    if rearVehicle:getVehicleTowedBy() ~= nil and rearVehicle:getVehicleTowedBy() ~= frontVehicle then
        return false
    end
    if frontVehicle:getVehicleTowing() ~= nil and frontVehicle:getVehicleTowing() ~= rearVehicle then
        return false
    end
    if orderKey and link.rearOrder and link.rearOrder >= 2 then
        local frontMd = frontVehicle and frontVehicle:getModData() or nil
        local frontHeadSql = frontMd and tonumber(frontMd[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
        local frontOrder = frontMd and tonumber(frontMd[PERSIST_CHAIN_ORDER_KEY]) or nil
        local frontChainToken = frontMd and tostring(frontMd[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
        if frontChainToken == "" then frontChainToken = nil end
        local frontOrderKey = nil
        if frontHeadSql and frontHeadSql > 0 then
            frontOrderKey = "sql:" .. tostring(frontHeadSql)
        elseif frontChainToken then
            frontOrderKey = "tok:" .. tostring(frontChainToken)
        end
        if frontOrderKey ~= orderKey or frontOrder ~= (link.rearOrder - 1) then
            return false
        end
    end
    if wouldCreateTowLoop(frontVehicle, rearVehicle) then return false end

    local linkedNow = isLinkedPair(frontVehicle, rearVehicle)
    if not linkedNow then
        if not canTowByLandtrain(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB, true) then
            return false
        end
        if restoreViaAttachWrapper(playerObj, frontVehicle, rearVehicle, link.attachmentA, link.attachmentB) then
            return true
        end
    end

    return ensurePairAttached(playerObj, frontVehicle, rearVehicle, link.attachmentA, link.attachmentB, not linkedNow)
end

local function runPersistedLoadRebuildPass()
    if not TowBarMod or not TowBarMod.Hook or TowBarMod.Hook._ltOriginalPerformAttach == nil then
        return
    end

    local playerObj = getPlayer() or getSpecificPlayer(0)
    if not playerObj then return end

    local vehicles = getLoadedVehicles()
    if #vehicles == 0 then return end

    local bySql = indexVehiclesBySql(vehicles)
    local byHeadOrder = indexVehiclesByHeadAndOrder(vehicles)
    local links = collectPersistedLinks(vehicles)
    links = filterAmbiguousPersistedLinks(links)
    links = filterConflictingOrderedLinks(links)
    if #links == 0 then return end
    local byRearSql = indexPersistedLinksByRearSql(links)
    sortPersistedLinks(links, byRearSql)

    local attempted = 0
    local restored = 0
    for _, link in ipairs(links) do
        attempted = attempted + 1
        if restorePersistedLink(playerObj, link, bySql, byHeadOrder) then
            restored = restored + 1
        end
    end

    if restored > 0 then
        log("load rebuild pass restored=" .. tostring(restored) .. "/" .. tostring(attempted))
    end
end

local loadRebuildWarmupTicks = 0
local loadRebuildIntervalTicks = 0

local function loadRebuildTick()
    if loadRebuildWarmupTicks > 0 then
        loadRebuildWarmupTicks = loadRebuildWarmupTicks - 1
        return
    end

    loadRebuildIntervalTicks = loadRebuildIntervalTicks + 1
    if loadRebuildIntervalTicks < 180 then return end
    loadRebuildIntervalTicks = 0

    runPersistedLoadRebuildPass()
end

local function startLoadRebuildWatch()
    loadRebuildWarmupTicks = 120
    loadRebuildIntervalTicks = 0
    Events.OnTick.Remove(loadRebuildTick)
    Events.OnTick.Add(loadRebuildTick)
end

local function ensureTrailerAttachments()
    local sm = getScriptManager()
    if sm == nil then return end
    local scripts = sm:getAllVehicleScripts()
    if scripts == nil then return end

    for i = 0, scripts:size() - 1 do
        local script = scripts:get(i)
        local name = script and script:getName() or nil
        if script and name and string.match(string.lower(name), "trailer") then
            local wheelCount = script:getWheelCount()
            local yOffset = -0.5
            if wheelCount > 0 then
                yOffset = script:getWheel(0):getOffset():y() + 0.1
            end

            if script:getAttachmentById("trailer") == nil then
                local rearAttach = ModelAttachment.new("trailer")
                rearAttach:getOffset():set(0, yOffset, -script:getPhysicsChassisShape():z() / 2 - 0.1)
                rearAttach:setZOffset(-1)
                script:addAttachment(rearAttach)
            end

            if script:getAttachmentById("trailerfront") == nil then
                local frontAttach = ModelAttachment.new("trailerfront")
                frontAttach:getOffset():set(0, yOffset, script:getPhysicsChassisShape():z() / 2 + 0.1)
                frontAttach:setZOffset(1)
                script:addAttachment(frontAttach)
            end
        end
    end
end

local function installPatch()
    if TowBarMod.Utils == nil or TowBarMod.UI == nil or TowBarMod.Hook == nil then return false end
    if TowBarMod.Hook.attachByTowBarAction == nil or ISVehicleMenu == nil then return false end
    if TowBarMod.Hook.performAttachTowBar == nil then return false end
    if getPerformDetachHook() == nil then return false end

    if TowBarMod.Utils._ltOriginalGetHookTypeVariants == nil then
        TowBarMod.Utils._ltOriginalGetHookTypeVariants = TowBarMod.Utils.getHookTypeVariants
    end
    TowBarMod.Utils.getHookTypeVariants = LT.getHookTypeVariantsWrapper

    if TowBarMod.Hook._ltOriginalPerformAttach == nil then
        TowBarMod.Hook._ltOriginalPerformAttach = TowBarMod.Hook.performAttachTowBar
    end
    if TowBarMod.Hook._ltOriginalPerformDetach == nil then
        TowBarMod.Hook._ltOriginalPerformDetach = getPerformDetachHook()
    end
    if TowBarMod.Hook._ltOriginalPerformAttach == nil or TowBarMod.Hook._ltOriginalPerformDetach == nil then
        return false
    end

    TowBarMod.Hook.performAttachTowBar = LT.performAttachWrapper
    TowBarMod.Hook.performDetachTowBar = LT.performDetachWrapper
    TowBarMod.Hook.performDeattachTowBar = LT.performDetachWrapper

    if TowBarMod.UI._ltOriginalShowRadial == nil then
        TowBarMod.UI._ltOriginalShowRadial = ISVehicleMenu.showRadialMenu
    end
    ISVehicleMenu.showRadialMenu = LT.showRadialWrapper

    if TowBarMod.UI._ltOriginalOnDetachTrailer == nil then
        TowBarMod.UI._ltOriginalOnDetachTrailer = ISVehicleMenu.onDetachTrailer
    end
    ISVehicleMenu.onDetachTrailer = LT.onDetachTrailerWrapper

    TowBarMod.Utils._landtrainUnlimitedChainsInstalled = true
    return true
end

local watchTicks = 0
local readyLogged = false

local function watchdogTick()
    if watchTicks <= 0 then
        Events.OnTick.Remove(watchdogTick)
        return
    end
    watchTicks = watchTicks - 1

    if installPatch() then
        if not readyLogged then
            log("Towbar chain hooks active")
            readyLogged = true
        end
        Events.OnTick.Remove(watchdogTick)
    end
end

local function startWatchdog()
    readyLogged = false
    watchTicks = 1800
    Events.OnTick.Remove(watchdogTick)
    Events.OnTick.Add(watchdogTick)
    installPatch()
    startLoadRebuildWatch()
end

Events.OnGameBoot.Add(ensureTrailerAttachments)
Events.OnGameBoot.Add(startWatchdog)
Events.OnGameStart.Add(startWatchdog)

log("LandtrainTowbarChainsRebuild loaded")

