local current_valet = nil

local vehicle_mods = {
    modSpoilers = 0,
    modFrontBumper = 1,
    modRearBumper = 2,
    modSideSkirt = 3,
    modExhaust = 4,
    modFrame = 5,
    modGrille = 6,
    modHood = 7,
    modFender = 8,
    modRightFender = 9,
    modRoof = 10,
    modEngine = 11,
    modBrakes = 12,
    modTransmission = 13,
    modHorns = 14,
    modSuspension = 15,
    modArmor = 16,
    modFrontWheels = 23,
    modBackWheels = 24,
    modPlateHolder = 25,
    modVanityPlate = 26,
    modTrimA = 27,
    modOrnaments = 28,
    modDashboard = 29,
    modDial = 30,
    modDoorSpeaker = 31,
    modSeats = 32,
    modSteeringWheel = 33,
    modShifterLeavers = 34,
    modAPlate = 35,
    modSpeakers = 36,
    modTrunk = 37,
    modHydrolic = 38,
    modEngineBlock = 39,
    modAirFilter = 40,
    modStruts = 41,
    modArchCover = 42,
    modAerials = 43,
    modTrimB = 44,
    modTank = 45,
    modWindows = 46,
    modLivery = 48,
}

local phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
local garage_locale = phone_locale.Nui.Apps.garage

AddEventHandler("sky_phone:configurator:updated", function()
    phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
    garage_locale = phone_locale.Nui.Apps.garage
end)

local function vehicle_kind(model_hash, fallback)
    if IsThisModelABoat(model_hash) then
        return "boat"
    end
    if IsThisModelAPlane(model_hash) then
        return "plane"
    end
    if IsThisModelAHeli(model_hash) then
        return "helicopter"
    end
    if IsThisModelABike(model_hash) or IsThisModelABicycle(model_hash) then
        return "bike"
    end
    return fallback
end

RegisterNUICallback("garage:vehicles", function(data, cb)
    if type(data) ~= "table" then
        cb({ success = false, error = "invalid_request" })
        return
    end

    local result = Bridge.Callbacks.Trigger("sky_phone:garage:vehicles", data)
    if type(result) ~= "table" or not result.success or type(result.data) ~= "table" then
        cb(type(result) == "table" and result or { success = false, error = "request_failed" })
        return
    end
    for _, vehicle in ipairs(result.data.vehicles or {}) do
        local model_hash = tonumber(vehicle.model)
        if not model_hash and type(vehicle.model) == "string" and vehicle.model ~= "" then
            model_hash = joaat(vehicle.model)
        end
        if model_hash then
            local display_name = GetDisplayNameFromVehicleModel(model_hash)
            local label = display_name and GetLabelText(display_name) or nil
            if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
                vehicle.name = label
            elseif type(vehicle.model) == "string" and vehicle.model ~= "" then
                vehicle.name = vehicle.model
            end
            vehicle.kind = vehicle_kind(model_hash, vehicle.kind)
        end
    end
    cb(result)
end)

local function normalized_plate(value)
    return tostring(value or ""):match("^%s*(.-)%s*$")
end

local function request_model(model)
    RequestModel(model)
    local timeout = GetGameTimer() + 10000
    while not HasModelLoaded(model) and GetGameTimer() < timeout do
        Wait(50)
    end
    return HasModelLoaded(model)
end

local function set_vehicle_color(vehicle, primary, secondary)
    if type(primary) == "table" then
        SetVehicleCustomPrimaryColour(
            vehicle,
            tonumber(primary[1]) or 0,
            tonumber(primary[2]) or 0,
            tonumber(primary[3]) or 0
        )
    end
    if type(secondary) == "table" then
        SetVehicleCustomSecondaryColour(
            vehicle,
            tonumber(secondary[1]) or 0,
            tonumber(secondary[2]) or 0,
            tonumber(secondary[3]) or 0
        )
    end
    if type(primary) == "number" or type(secondary) == "number" then
        local current_primary, current_secondary = GetVehicleColours(vehicle)
        SetVehicleColours(
            vehicle,
            tonumber(primary) or current_primary,
            tonumber(secondary) or current_secondary
        )
    end
end

local function apply_vehicle_properties(vehicle, properties, fallback)
    properties = type(properties) == "table" and properties or {}
    SetVehicleModKit(vehicle, 0)
    set_vehicle_color(vehicle, properties.color1, properties.color2)
    if properties.pearlescentColor or properties.wheelColor then
        SetVehicleExtraColours(
            vehicle,
            tonumber(properties.pearlescentColor) or 0,
            tonumber(properties.wheelColor) or 0
        )
    end
    if properties.wheels then
        SetVehicleWheelType(vehicle, tonumber(properties.wheels) or 0)
    end
    if properties.windowTint then
        SetVehicleWindowTint(vehicle, tonumber(properties.windowTint) or 0)
    end
    if properties.xenonColor then
        SetVehicleXenonLightsColor(vehicle, tonumber(properties.xenonColor) or -1)
    end
    for property, mod_type in pairs(vehicle_mods) do
        local value = tonumber(properties[property])
        if value then
            SetVehicleMod(vehicle, mod_type, value, false)
        end
    end
    ToggleVehicleMod(vehicle, 18, properties.modTurbo == true)
    ToggleVehicleMod(vehicle, 20, properties.modSmokeEnabled == true)
    ToggleVehicleMod(vehicle, 22, properties.modXenon == true)
    if type(properties.neonEnabled) == "table" then
        for index = 0, 3 do
            SetVehicleNeonLightEnabled(vehicle, index, properties.neonEnabled[index + 1] == true)
        end
    end
    if type(properties.neonColor) == "table" then
        SetVehicleNeonLightsColour(
            vehicle,
            tonumber(properties.neonColor[1]) or 0,
            tonumber(properties.neonColor[2]) or 0,
            tonumber(properties.neonColor[3]) or 0
        )
    end
    if type(properties.tyreSmokeColor) == "table" then
        SetVehicleTyreSmokeColor(
            vehicle,
            tonumber(properties.tyreSmokeColor[1]) or 0,
            tonumber(properties.tyreSmokeColor[2]) or 0,
            tonumber(properties.tyreSmokeColor[3]) or 0
        )
    end
    if type(properties.extras) == "table" then
        for extra_id, enabled in pairs(properties.extras) do
            local numeric_id = tonumber(extra_id)
            if numeric_id and DoesExtraExist(vehicle, numeric_id) then
                SetVehicleExtra(vehicle, numeric_id, not enabled)
            end
        end
    end
    local fuel = tonumber(properties.fuelLevel or properties.fuel or fallback.fuel)
    local engine = tonumber(properties.engineHealth or properties.engine or fallback.engine and fallback.engine * 10)
    local body = tonumber(properties.bodyHealth or properties.body or fallback.body and fallback.body * 10)
    if fuel then
        SetVehicleFuelLevel(vehicle, math.max(0.0, math.min(100.0, fuel)))
    end
    if engine then
        SetVehicleEngineHealth(vehicle, math.max(-4000.0, math.min(1000.0, engine)))
    end
    if body then
        SetVehicleBodyHealth(vehicle, math.max(0.0, math.min(1000.0, body)))
    end
    if properties.dirtLevel then
        SetVehicleDirtLevel(vehicle, math.max(0.0, math.min(15.0, tonumber(properties.dirtLevel) or 0.0)))
    end
    SetVehicleNumberPlateText(vehicle, fallback.plate)
end

local function existing_vehicle(plate)
    local normalized = string.upper(normalized_plate(plate))
    for _, vehicle in ipairs(GetGamePool("CVehicle")) do
        if string.upper(normalized_plate(GetVehicleNumberPlateText(vehicle))) == normalized then
            return vehicle
        end
    end
    return nil
end

local function public_valet_state()
    if not current_valet then
        return nil
    end
    return {
        canCancel = current_valet.can_cancel,
        cost = current_valet.cost,
        distance = current_valet.distance,
        etaSeconds = current_valet.eta_seconds,
        orderId = current_valet.order_id,
        plate = current_valet.plate,
        status = current_valet.status,
        vehicleName = current_valet.vehicle_name,
    }
end

local function send_valet_state()
    SendNUIMessage({
        type = "garage:valet-status",
        data = public_valet_state(),
    })
end

local function remove_valet_blip()
    if current_valet and current_valet.blip and DoesBlipExist(current_valet.blip) then
        RemoveBlip(current_valet.blip)
        current_valet.blip = nil
    end
end

local function delete_valet_entities()
    if not current_valet then
        return
    end
    remove_valet_blip()
    if current_valet.driver and DoesEntityExist(current_valet.driver) then
        DeleteEntity(current_valet.driver)
    end
    if current_valet.vehicle and DoesEntityExist(current_valet.vehicle) then
        DeleteEntity(current_valet.vehicle)
    end
end

local function fail_valet(error_code, server_cancel)
    if not current_valet then
        return
    end
    local order_id = current_valet.order_id
    current_valet.cancelled = true
    delete_valet_entities()
    if server_cancel then
        Bridge.Callbacks.Trigger("sky_phone:garage:valet-cancel", { orderId = order_id })
    end
    current_valet.status = "failed"
    current_valet.can_cancel = false
    current_valet.error = error_code
    send_valet_state()
    local locale = garage_locale
    Bridge.Framework.Notify(locale.name, locale.errors[error_code] or locale.errors.default, "error", 5000)
    SetTimeout(8000, function()
        if current_valet and current_valet.order_id == order_id then
            current_valet = nil
            send_valet_state()
        end
    end)
end

local function update_drive_target(driver, vehicle)
    local coords = GetEntityCoords(PlayerPedId())
    TaskVehicleDriveToCoordLongrange(
        driver,
        vehicle,
        coords.x,
        coords.y,
        coords.z,
        Config.Garage.Valet.DriveSpeed,
        Config.Garage.Valet.DrivingStyle,
        Config.Garage.Valet.ArrivalDistance
    )
end

local function run_valet_delivery(order)
    local model = tonumber(order.vehicle.model)
    if not model and type(order.vehicle.model) == "string" then
        model = joaat(order.vehicle.model)
    end
    local driver_model = joaat(order.driverModel)
    if not model or not IsModelInCdimage(model) or not IsModelAVehicle(model) then
        fail_valet("invalid_vehicle_model", true)
        return
    end
    if existing_vehicle(order.vehicle.plate) then
        fail_valet("vehicle_already_out", true)
        return
    end
    current_valet.status = "preparing"
    send_valet_state()
    if not request_model(model) or not request_model(driver_model) then
        SetModelAsNoLongerNeeded(model)
        SetModelAsNoLongerNeeded(driver_model)
        fail_valet("valet_spawn_failed", true)
        return
    end

    local player_ped = PlayerPedId()
    local player_coords = GetEntityCoords(player_ped)
    local target = GetOffsetFromEntityInWorldCoords(player_ped, 0.0, -Config.Garage.Valet.SpawnDistance, 0.0)
    local found, spawn_coords, spawn_heading = GetClosestVehicleNodeWithHeading(
        target.x,
        target.y,
        target.z,
        1,
        3.0,
        0
    )
    if not found then
        SetModelAsNoLongerNeeded(model)
        SetModelAsNoLongerNeeded(driver_model)
        fail_valet("valet_no_road", true)
        return
    end

    local vehicle = CreateVehicle(
        model,
        spawn_coords.x,
        spawn_coords.y,
        spawn_coords.z,
        spawn_heading,
        true,
        true
    )
    if vehicle == 0 or not DoesEntityExist(vehicle) then
        SetModelAsNoLongerNeeded(model)
        SetModelAsNoLongerNeeded(driver_model)
        fail_valet("valet_spawn_failed", true)
        return
    end
    current_valet.vehicle = vehicle
    SetEntityAsMissionEntity(vehicle, true, true)
    SetVehicleOnGroundProperly(vehicle)
    apply_vehicle_properties(vehicle, order.vehicle.properties, order.vehicle)
    SetVehicleHasBeenOwnedByPlayer(vehicle, true)
    SetVehicleDoorsLocked(vehicle, 2)
    SetVehicleEngineOn(vehicle, true, true, false)
    local network_id = NetworkGetNetworkIdFromEntity(vehicle)
    SetNetworkIdCanMigrate(network_id, true)

    local driver = CreatePedInsideVehicle(vehicle, 26, driver_model, -1, true, true)
    if driver == 0 or not DoesEntityExist(driver) then
        SetModelAsNoLongerNeeded(model)
        SetModelAsNoLongerNeeded(driver_model)
        fail_valet("valet_spawn_failed", true)
        return
    end
    current_valet.driver = driver
    SetEntityAsMissionEntity(driver, true, true)
    SetBlockingOfNonTemporaryEvents(driver, true)
    SetPedKeepTask(driver, true)
    SetDriverAbility(driver, 1.0)
    SetDriverAggressiveness(driver, 0.0)
    SetModelAsNoLongerNeeded(model)
    SetModelAsNoLongerNeeded(driver_model)

    current_valet.blip = AddBlipForEntity(vehicle)
    SetBlipSprite(current_valet.blip, 225)
    SetBlipColour(current_valet.blip, 3)
    SetBlipRoute(current_valet.blip, true)
    current_valet.status = "en_route"
    current_valet.can_cancel = true
    update_drive_target(driver, vehicle)
    send_valet_state()

    local timeout = GetGameTimer() + (Config.Garage.Valet.TimeoutSeconds * 1000)
    local next_retask = GetGameTimer() + 8000
    while current_valet and not current_valet.cancelled and GetGameTimer() < timeout do
        if not DoesEntityExist(vehicle)
            or not DoesEntityExist(driver)
            or IsEntityDead(driver)
            or not IsVehicleDriveable(vehicle, false)
        then
            fail_valet("valet_interrupted", true)
            return
        end
        player_coords = GetEntityCoords(PlayerPedId())
        local vehicle_coords = GetEntityCoords(vehicle)
        local distance = #(vehicle_coords - player_coords)
        current_valet.distance = math.floor(distance + 0.5)
        current_valet.eta_seconds = math.max(1, math.ceil(distance / math.max(1.0, Config.Garage.Valet.DriveSpeed)))
        if distance <= Config.Garage.Valet.ArrivalDistance then
            break
        end
        if GetGameTimer() >= next_retask then
            update_drive_target(driver, vehicle)
            next_retask = GetGameTimer() + 8000
        end
        send_valet_state()
        Wait(750)
    end
    if not current_valet or current_valet.cancelled then
        return
    end
    if GetGameTimer() >= timeout then
        fail_valet("valet_timeout", true)
        return
    end

    current_valet.status = "arriving"
    current_valet.can_cancel = false
    current_valet.distance = 0
    current_valet.eta_seconds = 0
    send_valet_state()
    TaskVehicleTempAction(driver, vehicle, 27, 1800)
    Wait(1800)
    SetVehicleEngineOn(vehicle, false, true, true)
    SetVehicleDoorsLocked(vehicle, 1)
    TaskLeaveVehicle(driver, vehicle, 0)

    local leave_timeout = GetGameTimer() + 5000
    while IsPedInVehicle(driver, vehicle, false) and GetGameTimer() < leave_timeout do
        Wait(100)
    end
    local driver_target = GetOffsetFromEntityInWorldCoords(vehicle, 4.0, 8.0, 0.0)
    TaskGoStraightToCoord(
        driver,
        driver_target.x,
        driver_target.y,
        driver_target.z,
        1.0,
        10000,
        GetEntityHeading(driver),
        0.5
    )
    SetPedKeepTask(driver, true)
    SetEntityAsNoLongerNeeded(driver)
    current_valet.driver = nil

    local completion = Bridge.Callbacks.Trigger(
        "sky_phone:garage:valet-complete",
        {
            orderId = current_valet.order_id,
            networkId = NetworkGetNetworkIdFromEntity(vehicle),
        }
    )
    if not completion or not completion.success then
        fail_valet("valet_completion_failed", false)
        return
    end
    remove_valet_blip()
    SetEntityAsNoLongerNeeded(vehicle)
    current_valet.vehicle = nil
    current_valet.status = "delivered"
    current_valet.can_cancel = false
    send_valet_state()
    local locale = garage_locale
    Bridge.Framework.Notify(locale.name, locale.valetDelivered, "success", 5000)
    local completed_order_id = current_valet.order_id
    SetTimeout(12000, function()
        if current_valet and current_valet.order_id == completed_order_id then
            current_valet = nil
            send_valet_state()
        end
    end)
end

RegisterNUICallback("garage:valet-request", function(data, cb)
    if type(data) ~= "table" then
        cb({ success = false, error = "invalid_request" })
        return
    end
    if current_valet then
        cb({ success = false, error = "valet_active" })
        return
    end
    local result = Bridge.Callbacks.Trigger("sky_phone:garage:valet-request", data)
    if not result or not result.success or type(result.data) ~= "table" then
        cb(result or { success = false, error = "request_failed" })
        return
    end
    local vehicle = result.data.vehicle
    local model_hash = tonumber(vehicle.model)
    if not model_hash and type(vehicle.model) == "string" then
        model_hash = joaat(vehicle.model)
    end
    local display_name = model_hash and GetDisplayNameFromVehicleModel(model_hash) or nil
    local label = display_name and GetLabelText(display_name) or nil
    current_valet = {
        can_cancel = false,
        cancelled = false,
        cost = result.data.cost,
        distance = nil,
        eta_seconds = nil,
        order_id = result.data.orderId,
        plate = vehicle.plate,
        status = "ordering",
        vehicle_name = label and label ~= "NULL" and label ~= "CARNOTFOUND"
            and label
            or tostring(vehicle.model),
    }
    cb({ success = true, data = public_valet_state() })
    send_valet_state()
    CreateThread(function()
        run_valet_delivery(result.data)
    end)
end)

RegisterNUICallback("garage:valet-cancel", function(_, cb)
    if not current_valet or not current_valet.can_cancel then
        cb({ success = false, error = "valet_not_cancellable" })
        return
    end
    local order_id = current_valet.order_id
    local result = Bridge.Callbacks.Trigger("sky_phone:garage:valet-cancel", { orderId = order_id })
    if not result or not result.success then
        cb(result or { success = false, error = "request_failed" })
        return
    end
    current_valet.cancelled = true
    delete_valet_entities()
    current_valet.status = "cancelled"
    current_valet.can_cancel = false
    send_valet_state()
    cb({ success = true, data = public_valet_state() })
    SetTimeout(5000, function()
        if current_valet and current_valet.order_id == order_id then
            current_valet = nil
            send_valet_state()
        end
    end)
end)

RegisterNUICallback("garage:valet-state", function(_, cb)
    cb({ success = true, data = public_valet_state() })
end)

RegisterNetEvent("sky_phone:garage:valet-aborted", function(error_code)
    if current_valet then
        fail_valet(error_code or "valet_timeout", false)
    end
end)

AddEventHandler("onResourceStop", function(resource_name)
    if resource_name ~= GetCurrentResourceName() or not current_valet then
        return
    end
    delete_valet_entities()
end)
