if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Hook then TowBarMod.Hook = {} end

local DefaultTowBarTowMass = 200
local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {}
local AutoReattachPlayerCooldownHours = 1 / 14400 -- 0.25 seconds
TowBarMod.Hook.lastAutoReattachAtByPlayer = TowBarMod.Hook.lastAutoReattachAtByPlayer or {}
local FreeRollTickInterval = 15
local freeRollTickCounter = 0

local function tryVehicleCall(vehicle, methodName, arg)
    if not vehicle or not methodName then return false, nil end
    local method = vehicle[methodName]
    if method == nil then return false, nil end

    return pcall(function()
        if arg ~= nil then
            return method(vehicle, arg)
        end
        return method(vehicle)
    end)
end

local function storeOriginalVehicleCall(vehicle, modData, key, getterName)
    if not vehicle or not modData or modData[key] ~= nil then return end

    local ok, value = tryVehicleCall(vehicle, getterName)
    if ok and value ~= nil then
        modData[key] = value
    end
end

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

    local modData = vehicle:getModData()
    if not modData then return end

    if modData.towBarOriginalMass == nil then
        modData.towBarOriginalMass = vehicle:getMass()
    end
    if modData.towBarOriginalBrakingForce == nil then
        modData.towBarOriginalBrakingForce = vehicle:getBrakingForce()
    end
    storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn")
    storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "isParkingBrake")
    storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake")

    local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass)
    vehicle:setMass(configuredTowMass or DefaultTowBarTowMass)
    vehicle:setBrakingForce(0)

    tryVehicleCall(vehicle, "setParkingBrakeOn", false)
    tryVehicleCall(vehicle, "setParkingBrake", false)
    tryVehicleCall(vehicle, "setHandbrake", false)
    tryVehicleCall(vehicle, "setBrake", false)
    tryVehicleCall(vehicle, "setBraking", false)

    vehicle:constraintChanged()
    vehicle:updateTotalMass()
end

local function restoreFreeRollingTowState(vehicle, modData)
    if not vehicle or not modData then return end

    if modData.towBarOriginalMass ~= nil then
        vehicle:setMass(modData.towBarOriginalMass)
    end
    if modData.towBarOriginalBrakingForce ~= nil then
        vehicle:setBrakingForce(modData.towBarOriginalBrakingForce)
    end
    if modData.towBarOriginalParkingBrakeOn ~= nil then
        tryVehicleCall(vehicle, "setParkingBrakeOn", modData.towBarOriginalParkingBrakeOn)
    end
    if modData.towBarOriginalParkingBrake ~= nil then
        tryVehicleCall(vehicle, "setParkingBrake", modData.towBarOriginalParkingBrake)
    end
    if modData.towBarOriginalHandbrake ~= nil then
        tryVehicleCall(vehicle, "setHandbrake", modData.towBarOriginalHandbrake)
    end

    vehicle:constraintChanged()
    vehicle:updateTotalMass()
end

local function isTowBarTowPair(towingVehicle, towedVehicle)
    if not towingVehicle or not towedVehicle then return false end

    local towingModData = towingVehicle:getModData()
    local towedModData = towedVehicle:getModData()
    if not towingModData or not towedModData then return false end

    if towingModData["isTowingByTowBar"] and towedModData["isTowingByTowBar"] and towedModData["towed"] then
        return true
    end

    -- Rejoin fallback: original towbar state on the towed vehicle is enough to reapply rigid spacing.
    if towedModData.towBarOriginalScriptName ~= nil then
        return true
    end

    return false
end

local function getTowBarItem(playerObj)
    if not playerObj then return nil end
    local inventory = playerObj:getInventory()
    if not inventory then return nil end
    return inventory:getItemFromTypeRecurse("TowBar.TowBar")
end

local function sendTowAttachCommand(playerObj, args)
    if not playerObj or not args then return end

    -- MP-safe/server-authoritative attach path (Landtrain style).
    if isClient() and isMultiplayer() then
        sendClientCommand(playerObj, "towbar", "attachTowBar", args)
        return
    end

    -- Keep vanilla attach path for SP/local behavior.
    sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
end

local TowbarVariantSize = 24
local TowbarNormalStart = 0
local TowbarLargeStart = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local VanillaScaleMin = 1.5
local VanillaScaleMax = 2.0

local function getVehicleModelScale(script)
    if not script then return nil end

    local ok, result = pcall(function()
        return script:getModelScale()
    end)
    if ok and type(result) == "number" then
        return result
    end

    ok, result = pcall(function()
        local model = script:getModel()
        if model then
            return model:getScale()
        end
        return nil
    end)
    if ok and type(result) == "number" then
        return result
    end

    return nil
end

local function isVanillaScale(script)
    local modelScale = getVehicleModelScale(script)
    if modelScale == nil then
        return true
    end

    local configuredMin = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
    local configuredMax = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
    local minScale = configuredMin or VanillaScaleMin
    local maxScale = configuredMax or VanillaScaleMax
    return modelScale >= minScale and modelScale <= maxScale
end

local function getTowbarIndexVanilla(script)
    local z = script:getPhysicsChassisShape():z() / 2 - 0.1
    local index = math.floor((z * 2 / 3 - 1) * 10)
    return math.max(0, math.min(TowbarMaxIndex, index))
end

local function getTowbarIndexSmallScale(script)
    if not script then return nil end

    local maxAbsTowZ = nil
    local trailer = script:getAttachmentById("trailer")
    if trailer then
        maxAbsTowZ = math.abs(trailer:getOffset():z())
    end
    local trailerFront = script:getAttachmentById("trailerfront")
    if trailerFront then
        local frontAbsZ = math.abs(trailerFront:getOffset():z())
        if not maxAbsTowZ or frontAbsZ > maxAbsTowZ then
            maxAbsTowZ = frontAbsZ
        end
    end

    if maxAbsTowZ ~= nil then
        -- Match KI5-size vehicles by anchoring to tow attachment depth.
        -- +0.1 keeps the bar slightly beyond the attachment point.
        local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
        return math.max(0, math.min(TowbarMaxIndex, index))
    end

    return nil
end

local function getTowbarModelSlot(script)
    local isVanilla = isVanillaScale(script)
    local index = getTowbarIndexVanilla(script)
    if not isVanilla then
        local attachmentIndex = getTowbarIndexSmallScale(script)
        if attachmentIndex ~= nil then
            index = attachmentIndex
        else
            local offset = TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
            index = math.max(0, math.min(TowbarMaxIndex, index + offset))
        end
    end
    return index, isVanilla
end

local function setTowBarModelVisible(vehicle, isVisible)
    if not vehicle then return end

    local normalPart = vehicle:getPartById("towbar")
    local largePart = vehicle:getPartById("towbarLarge")
    if normalPart == nil and largePart == nil then return end

    for j = 0, TowbarVariantSize - 1 do
        if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
        if largePart then largePart:setModelVisible("towbar" .. j, false) end
    end

    if not isVisible then
        vehicle:doDamageOverlay()
        return
    end

    local script = vehicle:getScript()
    if not script then
        vehicle:doDamageOverlay()
        return
    end

    local index, isVanilla = getTowbarModelSlot(script)
    local part = isVanilla and normalPart or largePart
    if part == nil then
        part = normalPart or largePart
    end
    if part then
        part:setModelVisible("towbar" .. index, true)
    end

    vehicle:doDamageOverlay()
end

local function resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
    if not towingVehicle or not towedVehicle then
        return nil, nil
    end

    local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer"
    local attachmentB = towingVehicle:getTowAttachmentOther()
        or (towedModData and towedModData["towBarChangedAttachmentId"])
        or "trailerfront"

    if not towingVehicle:canAttachTrailer(towedVehicle, attachmentA, attachmentB) then
        if towingVehicle:canAttachTrailer(towedVehicle, "trailer", "trailerfront") then
            attachmentA = "trailer"
            attachmentB = "trailerfront"
        elseif towingVehicle:canAttachTrailer(towedVehicle, "trailerfront", "trailer") then
            attachmentA = "trailerfront"
            attachmentB = "trailer"
        end
    end

    return attachmentA, attachmentB
end

local function hasTowBarTowState(modData)
    if not modData then
        return false
    end

    if modData["isTowingByTowBar"] and modData["towed"] then
        return true
    end

    -- Rejoin fallback: legacy saves may only have the original-script marker.
    if modData.towBarOriginalScriptName ~= nil then
        return true
    end

    return false
end

local function isActiveTowBarTowedVehicle(vehicle, modData)
    if not vehicle or not modData then
        return false
    end

    if modData["isTowingByTowBar"] and modData["towed"] then
        return true
    end

    -- Rejoin fallback: if the tow link exists, original-script marker is enough.
    if vehicle:getVehicleTowedBy() and modData.towBarOriginalScriptName ~= nil then
        return true
    end

    return false
end

local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requireDriver)
    if not playerObj or not towingVehicle or not towedVehicle then
        return false
    end
    if requireDriver and not towingVehicle:isDriver(playerObj) then
        return false
    end

    local towingModData = towingVehicle:getModData()
    local towedModData = towedVehicle:getModData()
    if not towingModData or not towedModData then
        return false
    end
    if requireDriver then
        if not isTowBarTowPair(towingVehicle, towedVehicle) then
            return false
        end
    else
        if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then
            return false
        end
    end

    local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
    if not attachmentA or not attachmentB then
        return false
    end

    local towingScript = towingVehicle:getScript()
    local towedScript = towedVehicle:getScript()
    if not towingScript or not towedScript then
        return false
    end
    if not towingScript:getAttachmentById(attachmentA) or not towedScript:getAttachmentById(attachmentB) then
        return false
    end

    TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)

    towedModData.towBarOriginalScriptName = towedModData.towBarOriginalScriptName or towedVehicle:getScriptName()
    applyFreeRollingTowState(towedVehicle)

    towingModData["isTowingByTowBar"] = true
    towedModData["isTowingByTowBar"] = true
    towedModData["towed"] = true
    towingVehicle:transmitModData()
    towedVehicle:transmitModData()

    setTowBarModelVisible(towedVehicle, true)
    towedVehicle:setScriptName("notTowingA_Trailer")

    local args = {
        vehicleA = towingVehicle:getId(),
        vehicleB = towedVehicle:getId(),
        attachmentA = attachmentA,
        attachmentB = attachmentB
    }
    sendTowAttachCommand(playerObj, args)
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
    return true
end

local function reattachTowBarPairAfterCleanDetach(playerObj, towingVehicle, towedVehicle, requireDriver)
    if not playerObj or not towingVehicle or not towedVehicle then
        return false
    end
    if requireDriver and not towingVehicle:isDriver(playerObj) then
        return false
    end

    local detachArgs = {
        towingVehicle = towingVehicle:getId(),
        vehicle = towedVehicle:getId()
    }
    sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs)

    -- World load/spawn can restore constraints in a bad state. Reattach one
    -- short tick later so the detach is fully applied first.
    ISTimedActionQueue.add(TowBarScheduleAction:new(
        playerObj,
        1,
        reattachTowBarPair,
        towingVehicle,
        towedVehicle,
        requireDriver
    ))
    return true
end

local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
    if not vehicle then return end

    local modData = vehicle:getModData()
    if not hasTowBarTowState(modData) then
        return
    end

    local retries = tonumber(retriesLeft) or 0
    local localPlayer = playerObj or getPlayer()
    local towingVehicle = vehicle:getVehicleTowedBy()

    if towingVehicle then
        -- Apply rigid spacing as soon as the tow link exists to avoid a visible
        -- bumper-to-bumper snap while waiting for reattach recovery.
        TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
    end

    if localPlayer and towingVehicle then
        if reattachTowBarPairAfterCleanDetach(localPlayer, towingVehicle, vehicle, false) then
            return
        end
    end

    if localPlayer and retries > 0 then
        -- During world load, tow links can become available a few ticks later.
        ISTimedActionQueue.add(TowBarScheduleAction:new(localPlayer, 10, recoverTowBarVehicleAfterLoad, vehicle, retries - 1))
        return
    end

    -- Fallback: keep original post-attach restoration behavior.
    setTowBarModelVisible(vehicle, true)
    TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end

function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLeft)
    if not towedVehicle then return end

    local towedModData = towedVehicle:getModData()
    if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end

    if towedModData and towedModData.towBarOriginalScriptName then
        towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
    end

    local towingVehicle = towedVehicle:getVehicleTowedBy()
    if towingVehicle then
        local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
        if attachmentA and attachmentB then
            TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
        end
    end

    applyFreeRollingTowState(towedVehicle)

    -- Re-show the towbar model after the script name has been restored.
    -- setScriptName() resets model visibility, so we must set it again here.
    setTowBarModelVisible(towedVehicle, true)
end

function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
    if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
    if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end

    local towBarItem = getTowBarItem(playerObj)
    if towBarItem ~= nil then
        sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
    end
    playerObj:setPrimaryHandItem(nil)

    TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)

    local towingModData = towingVehicle:getModData()
    local towedModData = towedVehicle:getModData()

    towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
    applyFreeRollingTowState(towedVehicle)

    towingModData["isTowingByTowBar"] = true
    towedModData["isTowingByTowBar"] = true
    towedModData["towed"] = true
    towingVehicle:transmitModData()
    towedVehicle:transmitModData()

    setTowBarModelVisible(towedVehicle, true)

    -- Match the known-good rigid tow path: fake trailer + vanilla attach command.
    towedVehicle:setScriptName("notTowingA_Trailer")

    local args = {
        vehicleA = towingVehicle:getId(),
        vehicleB = towedVehicle:getId(),
        attachmentA = attachmentA,
        attachmentB = attachmentB
    }
    sendTowAttachCommand(playerObj, args)
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
end

function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle)
    if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end

    TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)

    local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
    sendClientCommand(playerObj, "towbar", "detachTowBar", args)

    local towedModData = towedVehicle:getModData()
    if towedModData.towBarOriginalScriptName then
        towedVehicle:setScriptName(towedModData.towBarOriginalScriptName)
    end
    restoreFreeRollingTowState(towedVehicle, towedModData)

    sendClientCommand(playerObj, "towbar", "giveTowBar", { equipPrimary = true })

    local towingModData = towingVehicle:getModData()
    towingModData["isTowingByTowBar"] = false
    towedModData["isTowingByTowBar"] = false
    towedModData["towed"] = false
    towedModData.towBarOriginalScriptName = nil
    towedModData.towBarOriginalMass = nil
    towedModData.towBarOriginalBrakingForce = nil
    towedModData.towBarOriginalParkingBrakeOn = nil
    towedModData.towBarOriginalParkingBrake = nil
    towedModData.towBarOriginalHandbrake = nil
    towingVehicle:transmitModData()
    towedVehicle:transmitModData()

    TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil

    setTowBarModelVisible(towedVehicle, false)
end

function TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
    if not playerObj or not towingVehicle then return end

    local towedVehicle = towingVehicle:getVehicleTowing()
    if not towedVehicle then return end

    reattachTowBarPair(playerObj, towingVehicle, towedVehicle, true)
end

local function tryAutoReattachFromCharacter(character)
    if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end

    local playerObj = character
    local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0
    local playerNum = playerObj:getPlayerNum()
    local lastPlayerHours = TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum]
    if lastPlayerHours and (nowHours - lastPlayerHours) < AutoReattachPlayerCooldownHours then
        return
    end

    local towingVehicle = playerObj:getVehicle()
    if not towingVehicle then return end
    if not towingVehicle:isDriver(playerObj) then return end
    local towedVehicle = towingVehicle:getVehicleTowing()
    if not towedVehicle then return end
    if not isTowBarTowPair(towingVehicle, towedVehicle) then return end

    local vehicleId = towingVehicle:getId()
    local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId]
    if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then
        return
    end

    TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum] = nowHours
    TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId] = nowHours
    TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
end

local function forEachCollectionItem(collection, callback)
    if not collection then return end

    local ok, iterator = pcall(function()
        return collection:iterator()
    end)
    if ok and iterator then
        while iterator:hasNext() do
            callback(iterator:next())
        end
        return
    end

    local size
    ok, size = pcall(function()
        return collection:size()
    end)
    if not ok or type(size) ~= "number" then return end

    for i = 0, size - 1 do
        callback(collection:get(i))
    end
end

local function keepTowBarVehiclesFreeRolling()
    freeRollTickCounter = freeRollTickCounter + 1
    if freeRollTickCounter < FreeRollTickInterval then
        return
    end
    freeRollTickCounter = 0

    local cell = getCell()
    if not cell then return end

    local vehicles = cell:getVehicles()
    if not vehicles then return end

    forEachCollectionItem(vehicles, function(vehicle)
        local modData = vehicle and vehicle:getModData() or nil
        if isActiveTowBarTowedVehicle(vehicle, modData) then
            applyFreeRollingTowState(vehicle)
        end
    end)
end

function TowBarMod.Hook.OnEnterVehicle(character)
    tryAutoReattachFromCharacter(character)
end

function TowBarMod.Hook.OnSwitchVehicleSeat(character)
    tryAutoReattachFromCharacter(character)
end

function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
    if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end

    local item = getTowBarItem(playerObj)
    if item == nil then return end
    if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end

    local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1)
    if hookPoint == nil then return end
    ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))

    if not playerObj:getInventory():contains("TowBar.TowBar") then
        ISTimedActionQueue.add(ISInventoryTransferAction:new(playerObj, item, item:getContainer(), playerObj:getInventory(), nil))
    end

    local storePrim = playerObj:getPrimaryHandItem()
    if storePrim == nil or storePrim ~= item then
        ISTimedActionQueue.add(ISEquipWeaponAction:new(playerObj, item, 12, true))
    end

    ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 300, TowBarMod.Config.lowLevelAnimation))

    hookPoint = towingVehicle:getAttachmentWorldPos("trailer", TowBarMod.Utils.tempVector1)
    if hookPoint == nil then return end
    ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))

    ISTimedActionQueue.add(TowBarHookVehicle:new(
        playerObj,
        100,
        TowBarMod.Config.lowLevelAnimation,
        TowBarMod.Hook.performAttachTowBar,
        towingVehicle,
        towedVehicle,
        "trailer",
        "trailerfront"
    ))
end

function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
    local towingVehicle = vehicle
    local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil
    if vehicle and vehicle:getVehicleTowedBy() then
        towingVehicle = vehicle:getVehicleTowedBy()
        towedVehicle = vehicle
    end
    if towingVehicle == nil or towedVehicle == nil then return end

    local localPoint = towingVehicle:getAttachmentLocalPos(towingVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1)
    local shift = 0
    if towingVehicle:getModData()["isChangedTowedAttachment"] then
        shift = localPoint:z() > 0 and -1 or 1
    end
    local hookPoint = towingVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2)
    if hookPoint == nil then return end
    ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))

    local storePrim = playerObj:getPrimaryHandItem()
    if storePrim ~= nil then
        ISTimedActionQueue.add(ISUnequipAction:new(playerObj, storePrim, 12))
    end

    ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 100, TowBarMod.Config.lowLevelAnimation))

    localPoint = towedVehicle:getAttachmentLocalPos(towedVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1)
    shift = 0
    if towedVehicle:getModData()["isChangedTowedAttachment"] then
        shift = localPoint:z() > 0 and -1 or 1
    end
    hookPoint = towedVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2)
    if hookPoint == nil then return end
    ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z()))

    ISTimedActionQueue.add(TowBarHookVehicle:new(
        playerObj,
        300,
        TowBarMod.Config.lowLevelAnimation,
        TowBarMod.Hook.performDetachTowBar,
        towingVehicle,
        towedVehicle
    ))
end

function TowBarMod.Hook.OnSpawnVehicle(vehicle)
    recoverTowBarVehicleAfterLoad(nil, vehicle, 6)
end

function TowBarMod.Hook.OnGameStart()
    local cell = getCell()
    if not cell then return end

    local vehicles = cell:getVehicles()
    if not vehicles then return end

    local playerObj = getPlayer()
    forEachCollectionItem(vehicles, function(vehicle)
        recoverTowBarVehicleAfterLoad(playerObj, vehicle, 6)
    end)
end

---------------------------------------------------------------------------
--- Dev / debug helpers
---------------------------------------------------------------------------

function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
    if not vehicle then return end
    local normalPart = vehicle:getPartById("towbar")
    local largePart = vehicle:getPartById("towbarLarge")
    if normalPart == nil and largePart == nil then
        print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
        return
    end
    local script = vehicle:getScript()
    local chassisZ = script and script:getPhysicsChassisShape():z() or 0
    local halfZ = chassisZ / 2
    local modelScale = script and getVehicleModelScale(script) or nil
    local index, isVanilla = 0, true
    if script then
        index, isVanilla = getTowbarModelSlot(script)
    end
    local selectedPart = isVanilla and "towbar" or "towbarLarge"
    print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName()))
    print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
    print("[TowBar DEV] modelScale = " .. tostring(modelScale) .. ", part = " .. selectedPart)
    print("[TowBar DEV] Formula picks index = " .. tostring(index) .. " (towbar" .. tostring(index) .. " at Z offset " .. tostring(1.0 + index * 0.1) .. ")")
    print("[TowBar DEV] Showing towbar0..towbar23 on both parts")
    for j = 0, TowbarVariantSize - 1 do
        if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
        if largePart then largePart:setModelVisible("towbar" .. j, true) end
    end
    vehicle:doDamageOverlay()
end

function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle)
    if not vehicle then return end
    local normalPart = vehicle:getPartById("towbar")
    local largePart = vehicle:getPartById("towbarLarge")
    if normalPart == nil and largePart == nil then
        print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
        return
    end
    print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
    for j = 0, TowbarVariantSize - 1 do
        if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
        if largePart then largePart:setModelVisible("towbar" .. j, false) end
    end
    vehicle:doDamageOverlay()
end

function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index)
    if not vehicle then return end
    local normalPart = vehicle:getPartById("towbar")
    local largePart = vehicle:getPartById("towbarLarge")
    if normalPart == nil and largePart == nil then
        print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
        return
    end

    local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
    local useLargePart = index >= TowbarVariantSize

    for j = 0, TowbarVariantSize - 1 do
        if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
        if largePart then largePart:setModelVisible("towbar" .. j, false) end
    end

    local part = useLargePart and largePart or normalPart
    if part == nil then
        part = normalPart or largePart
    end
    print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. tostring(useLargePart and "towbarLarge" or "towbar") .. " (Z offset " .. tostring(1.0 + localIndex * 0.1) .. ") on " .. tostring(vehicle:getScriptName()))
    if part then
        part:setModelVisible("towbar" .. localIndex, true)
    end
    vehicle:doDamageOverlay()
end

Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle)
if Events.OnGameStart then
    Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart)
end
Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
