SkyPhoneGarage = {}

Bridge.Database.AfterMigration("sky_phone", function()

local supported_systems = {
    auto = true,
    custom = true,
    esx = true,
    qb = true,
    qbox = true,
    ak47 = true,
    bp = true,
    cd = true,
    codem = true,
    ["ds-servercreator"] = true,
    hex = true,
    jg = true,
    my = true,
    okok = true,
    op = true,
    quasar = true,
    rx = true,
    vms = true,
    ws = true,
    zyke_garages = true,
}

local function decode_object(value)
    if type(value) == "table" then
        return value
    end
    if type(value) ~= "string" or value == "" then
        return {}
    end
    local success, decoded = pcall(json.decode, value)
    if not success or type(decoded) ~= "table" then
        return {}
    end
    return decoded
end

local function first_value(...)
    local values = { ... }
    for index = 1, #values do
        local value = values[index]
        if value ~= nil and value ~= "" then
            return value
        end
    end
    return nil
end

local function truthy_database_value(value)
    return value == true or value == 1 or value == "1" or value == "true"
end

local function normalized_health(value, maximum)
    local number = tonumber(value)
    if not number then
        return nil
    end
    if maximum == 1000 then
        number = number / 10
    end
    return math.max(0, math.min(100, math.floor(number + 0.5)))
end

local function vehicle_status(row, location, garage_system)
    local state = tonumber(row.state)
    local location_key = string.lower(tostring(location or ""))
    if truthy_database_value(row.impound)
        or truthy_database_value(row.impounded)
        or truthy_database_value(row.pound)
        or location_key:find("impound", 1, true)
        or location_key:find("pound", 1, true)
    then
        return "impounded"
    end
    if garage_system == "jg" then
        return truthy_database_value(row.in_garage) and "garaged" or "out"
    end
    if state == 2 then
        return "impounded"
    end
    local stored = first_value(row.stored, row.in_garage, row.parked)
    if truthy_database_value(stored) or state == 1 then
        return "garaged"
    end
    return "out"
end

local function vehicle_kind(value)
    local kind = string.lower(tostring(value or ""))
    if kind == "boat" then
        return "boat"
    end
    if kind == "plane" or kind == "air" or kind == "airplane" then
        return "plane"
    end
    if kind == "heli" or kind == "helicopter" then
        return "helicopter"
    end
    if kind == "bike" or kind == "bicycle" or kind == "motorcycle" then
        return "bike"
    end
    return "car"
end

local function vehicle_properties(row)
    local properties = decode_object(first_value(row.mods, row.vehicle_data, row.properties))
    local vehicle_object = decode_object(row.vehicle)
    if next(vehicle_object) ~= nil then
        for key, value in pairs(vehicle_object) do
            if properties[key] == nil then
                properties[key] = value
            end
        end
    end
    return properties
end

local function vehicle_image_url(model)
    local image_config = Config.Garage.VehicleImages or {}
    if image_config.Enabled ~= true then
        return nil
    end

    local configured_names = image_config.ModelNames or {}
    local model_name = configured_names[model] or configured_names[tostring(model or "")]
    if not model_name and type(model) == "string" and not tonumber(model) then
        model_name = model
    end

    model_name = string.lower(tostring(model_name or ""))
    if model_name == "" or not model_name:match("^[%w_-]+$") then
        return nil
    end

    local template = tostring(image_config.UrlTemplate or "")
    if template == "" or not template:find("{model}", 1, true) then
        return nil
    end
    return (template:gsub("{model}", model_name))
end

local function vehicle_dto(row, garage_system)
    local mods = vehicle_properties(row)
    local vehicle_value = row.vehicle

    local model = first_value(row.model, row.hash, mods.model, mods.hash)
    if type(vehicle_value) == "string" and vehicle_value:sub(1, 1) ~= "{" then
        model = first_value(model, vehicle_value)
    elseif type(vehicle_value) == "number" then
        model = first_value(model, vehicle_value)
    end
    local numeric_model = tonumber(model)
    if numeric_model then
        model = numeric_model
    end

    local location = first_value(row.garage_id, row.parking, row.garage, row.parked_at)
    local plate = tostring(first_value(row.plate, mods.plate) or ""):match("^%s*(.-)%s*$")
    return {
        id = tostring(first_value(row.id, row.vin, plate)),
        plate = plate,
        vin = tostring(row.vin or ""),
        nickname = tostring(row.nickname or ""),
        model = model,
        imageUrl = vehicle_image_url(model),
        kind = vehicle_kind(first_value(row.garage_type, row.type, mods.type)),
        status = vehicle_status(row, location, garage_system),
        location = tostring(location or ""),
        fuel = normalized_health(first_value(row.fuel, mods.fuelLevel, mods.fuel), 100),
        engine = normalized_health(first_value(row.engine, mods.engineHealth, mods.engine), 1000),
        body = normalized_health(first_value(row.body, mods.bodyHealth, mods.body), 1000),
    }
end

local function storage_config()
    local system = tostring(Config.Garage.System or "auto")
    if not supported_systems[system] then
        error(("[sky_phone] Unsupported garage system '%s'."):format(system))
    end
    if system == "custom" then
        local table_name = tostring(Config.Garage.Custom.Table or "")
        local owner_column = tostring(Config.Garage.Custom.OwnerColumn or "")
        if not table_name:match("^[%w_]+$") or not owner_column:match("^[%w_]+$") then
            error("[sky_phone] Custom garage table and owner column must be configured with SQL identifiers.")
        end
        return table_name, owner_column, system
    end
    if system == "esx" then
        return "owned_vehicles", "owner", system
    end
    if system == "qb" or system == "qbox" then
        return "player_vehicles", "citizenid", system
    end
    if system == "auto" and GetResourceState("jg-advancedgarages") == "started" then
        system = "jg"
    end
    if Bridge.Framework.GetName() == "esx" then
        return "owned_vehicles", "owner", system == "auto" and "esx" or system
    end
    return "player_vehicles", "citizenid", system == "auto" and Bridge.Framework.GetName() or system
end

local active_valets = {}
local valet_cooldowns = {}

local function normalized_plate(value)
    if type(value) ~= "string" then
        return nil
    end
    local plate = value:match("^%s*(.-)%s*$")
    if plate == "" or #plate > 16 or plate:find("[%c]") then
        return nil
    end
    return plate
end

local function owned_vehicle_row(identifier, plate)
    local table_name, owner_column, garage_system = storage_config()
    local rows = Bridge.Database.Query(
        ("SELECT * FROM %s WHERE %s = ? AND TRIM(plate) = ? LIMIT 1"):format(table_name, owner_column),
        { identifier, plate }
    )
    return rows[1], table_name, owner_column, garage_system
end

local function status_snapshot(row)
    local snapshot = {}
    for _, column in ipairs({ "stored", "state", "in_garage", "parked" }) do
        if row[column] ~= nil then
            snapshot[column] = row[column]
        end
    end
    return snapshot
end

local function write_vehicle_status(order, restore)
    local assignments = {}
    local parameters = {}
    for column, original_value in pairs(order.status) do
        assignments[#assignments + 1] = ("%s = ?"):format(column)
        parameters[#parameters + 1] = restore and original_value or 0
    end
    if #assignments == 0 then
        return false
    end
    parameters[#parameters + 1] = order.identifier
    parameters[#parameters + 1] = order.plate
    Bridge.Database.Query(
        ("UPDATE %s SET %s WHERE %s = ? AND TRIM(plate) = ?")
            :format(order.table_name, table.concat(assignments, ", "), order.owner_column),
        parameters
    )
    return true
end

local function refund_valet(source, order)
    if order.refunded then
        return
    end
    order.refunded = true
    if not Bridge.Framework.AddMoney(source, Config.Garage.Valet.Account, order.cost) then
        Bridge.Debug("error", "[sky_phone] Failed to refund valet order '%s' for player %s.", tostring(order.id), tostring(source))
        return
    end
    TriggerClientEvent("sky_phone:banking:changed", source)
end

local function cancel_valet(source, order)
    write_vehicle_status(order, true)
    refund_valet(source, order)
    active_valets[source] = nil
end

Bridge.Callbacks.Register("sky_phone:garage:valet-request", function(source, data)
    local valet = Config.Garage.Valet
    if not valet.Enabled then
        return { success = false, error = "valet_disabled" }
    end
    if not SkyPhone.AllowOperation(source, "garage_valet", valet.RequestsPerMinute, 60) then
        return { success = false, error = "rate_limited" }
    end
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return error_response
    end
    if active_valets[source] then
        return { success = false, error = "valet_active" }
    end
    local now = os.time()
    if (valet_cooldowns[source] or 0) > now then
        return {
            success = false,
            error = "valet_cooldown",
            data = { retryAfter = valet_cooldowns[source] - now },
        }
    end
    local plate = normalized_plate(data and data.plate)
    if not plate then
        return { success = false, error = "invalid_vehicle" }
    end
    local identifier = Bridge.Framework.GetIdentifier(source)
    if type(identifier) ~= "string" or identifier == "" then
        return { success = false, error = "garage_unavailable" }
    end
    local row, table_name, owner_column, garage_system = owned_vehicle_row(identifier, plate)
    if not row then
        return { success = false, error = "vehicle_not_owned" }
    end
    local vehicle = vehicle_dto(row, garage_system)
    if vehicle.status ~= "garaged" then
        return { success = false, error = "vehicle_not_garaged" }
    end
    if not valet.VehicleTypes[vehicle.kind] then
        return { success = false, error = "valet_vehicle_type" }
    end
    local price = math.max(0, math.floor(tonumber(valet.Price) or 0))
    local balance = tonumber(Bridge.Framework.GetMoney(source, valet.Account)) or 0
    if balance < price then
        return { success = false, error = "insufficient_funds" }
    end
    local order = {
        cost = price,
        expires_at = now + valet.TimeoutSeconds,
        identifier = identifier,
        id = ("%s:%s:%s"):format(source, now, math.random(100000, 999999)),
        owner_column = owner_column,
        plate = plate,
        status = status_snapshot(row),
        table_name = table_name,
    }
    if not write_vehicle_status(order, false) then
        return { success = false, error = "valet_status_unsupported" }
    end
    if price > 0 and not Bridge.Framework.RemoveMoney(source, valet.Account, price) then
        write_vehicle_status(order, true)
        return { success = false, error = "insufficient_funds" }
    end
    active_valets[source] = order
    TriggerClientEvent("sky_phone:banking:changed", source)
    return {
        success = true,
        data = {
            cost = price,
            driverModel = valet.DriverModel,
            orderId = order.id,
            vehicle = {
                body = vehicle.body,
                engine = vehicle.engine,
                fuel = vehicle.fuel,
                kind = vehicle.kind,
                model = vehicle.model,
                plate = vehicle.plate,
                properties = vehicle_properties(row),
            },
        },
    }
end)

Bridge.Callbacks.Register("sky_phone:garage:valet-cancel", function(source, data)
    local order = active_valets[source]
    if not order or type(data) ~= "table" or data.orderId ~= order.id then
        return { success = false, error = "valet_not_found" }
    end
    cancel_valet(source, order)
    return { success = true, data = { refunded = order.cost } }
end)

Bridge.Callbacks.Register("sky_phone:garage:valet-complete", function(source, data)
    local order = active_valets[source]
    if not order or type(data) ~= "table" or data.orderId ~= order.id then
        return { success = false, error = "valet_not_found" }
    end

    local network_id = tonumber(data.networkId)
    if not network_id or network_id <= 0 or network_id ~= math.floor(network_id) then
        return { success = false, error = "valet_vehicle_unverified" }
    end
    local entity = NetworkGetEntityFromNetworkId(network_id)
    if entity == 0 or not DoesEntityExist(entity) or tonumber(NetworkGetEntityOwner(entity)) ~= source then
        return { success = false, error = "valet_vehicle_unverified" }
    end

    active_valets[source] = nil
    valet_cooldowns[source] = os.time() + Config.Garage.Valet.CooldownSeconds
    return { success = true }
end)

AddEventHandler("playerDropped", function()
    local source = source
    local order = active_valets[source]
    if order then
        cancel_valet(source, order)
    end
    valet_cooldowns[source] = nil
end)

AddEventHandler("onResourceStop", function(resource_name)
    if resource_name ~= GetCurrentResourceName() then
        return
    end
    for source, order in pairs(active_valets) do
        cancel_valet(source, order)
    end
end)

CreateThread(function()
    while true do
        Wait(5000)
        local now = os.time()
        for source, order in pairs(active_valets) do
            if order.expires_at <= now then
                cancel_valet(source, order)
                TriggerClientEvent("sky_phone:garage:valet-aborted", source, "valet_timeout")
            end
        end
    end
end)

Bridge.Callbacks.Register("sky_phone:garage:vehicles", function(source)
    if not SkyPhone.AllowOperation(source, "garage_vehicles", Config.Garage.RequestsPerMinute, 60) then
        return { success = false, error = "rate_limited" }
    end
    local session, error_response = SkyPhone.RequireSession(source)
    if not session then
        return error_response
    end
    local identifier = Bridge.Framework.GetIdentifier(source)
    if type(identifier) ~= "string" or identifier == "" then
        return { success = false, error = "garage_unavailable" }
    end
    local table_name, owner_column, garage_system = storage_config()
    local rows = Bridge.Database.Query(
        ("SELECT * FROM `%s` WHERE `%s` = ? LIMIT ?"):format(table_name, owner_column),
        { identifier, Config.Garage.MaximumVehicles }
    )
    local vehicles = {}
    for _, row in ipairs(rows) do
        local vehicle = vehicle_dto(row, garage_system)
        if vehicle.plate ~= "" then
            vehicles[#vehicles + 1] = vehicle
        end
    end
    table.sort(vehicles, function(left, right)
        return left.plate < right.plate
    end)
    return {
        success = true,
        data = {
            valet = {
                account = Config.Garage.Valet.Account,
                enabled = Config.Garage.Valet.Enabled,
                price = math.max(0, math.floor(tonumber(Config.Garage.Valet.Price) or 0)),
                vehicleTypes = Config.Garage.Valet.VehicleTypes,
            },
            vehicles = vehicles,
        },
    }
end)

end)
