--!optimize 2

--[[
    Errors.lua
    Defines error types and handling utilities for the NetRay library
    Author: Asta (@TheYusufGamer)
]]

-- Define the module table first
local Errors = {}

-- Error types Enum (using a table as an enum)
-- Use PascalCase for enum-like table names by convention
local ErrorTypes = {
	NETWORK = "NETWORK_ERROR",
	TIMEOUT = "TIMEOUT_ERROR",
	VALIDATION = "VALIDATION_ERROR",
	SERIALIZATION = "SERIALIZATION_ERROR",
	RATE_LIMIT = "RATE_LIMIT_ERROR",
	CIRCUIT_OPEN = "CIRCUIT_OPEN_ERROR",
	MIDDLEWARE = "MIDDLEWARE_ERROR",
	INTERNAL = "INTERNAL_ERROR",
	NOT_FOUND = "NOT_FOUND_ERROR",
	TYPE_ERROR = "TYPE_ERROR"
}
-- Export the ErrorTypes table if needed elsewhere
export type ErrorTypesEnum = typeof(ErrorTypes)
Errors.Type = ErrorTypes -- Assign to the export table

-- Define the type for the error object itself
export type NetRayError = {
	type: string, -- Should ideally match one of the values in ErrorTypes
	message: string,
	details: any?, -- Can hold additional context
	timestamp: number,
	__tostring: (self: NetRayError) -> string, -- Metamethod signature
}

-- Define the type for the Error 'class' table (containing .new)
type NetRayErrorClass = {
	__index: NetRayErrorClass, -- Points to itself for method lookup
	new: (errorType: string?, message: string?, details: any?) -> NetRayError, -- Constructor
}

-- Define the type for the main Errors module table
export type ErrorsModule = {
	Type: ErrorTypesEnum,
	Error: NetRayErrorClass, -- The 'class' table

	-- Specific error constructor functions
	NETWORK: (message: string?, details: any?) -> NetRayError,
	TIMEOUT: (message: string?, details: any?) -> NetRayError,
	VALIDATION: (message: string?, details: any?) -> NetRayError,
	SERIALIZATION: (message: string?, details: any?) -> NetRayError,
	RATE_LIMIT: (message: string?, details: any?) -> NetRayError,
	CIRCUIT_OPEN: (message: string?, details: any?) -> NetRayError,
	MIDDLEWARE: (message: string?, details: any?) -> NetRayError,
	INTERNAL: (message: string?, details: any?) -> NetRayError,
	NOT_FOUND: (message: string?, details: any?) -> NetRayError,
	TYPE_ERROR: (message: string?, details: any?) -> NetRayError,

	-- Utility functions
	isNetRayError: (err: any) -> boolean,
	try: <T>(fn: (...any) -> T, ...any) -> {success: true, value: T} | {success: false, error: NetRayError | any}, -- Generic return type
	serialize: (err: any) -> {type: string, message: string, details: any?, timestamp: number},
	deserialize: (data: any) -> NetRayError
}

-- Implementation
local ErrorImpl = {} -- Internal table for the Error 'class' methods
ErrorImpl.__index = ErrorImpl

-- Error class constructor
function ErrorImpl.new(errorType: string?, message: string?, details: any?): NetRayError
	local self = setmetatable({}, ErrorImpl)

	self.type = errorType or ErrorTypes.INTERNAL
	self.message = message or "An unknown error occurred"
	self.details = details
	self.timestamp = tick()

	-- Ensure the returned object matches the NetRayError type structure
	return self :: NetRayError
end

-- Error class __tostring metamethod
function ErrorImpl:__tostring(): string
	return "[NetRay." .. self.type .. "] " .. self.message
end

-- Assign the implementation to the exported Error 'class' table
Errors.Error = ErrorImpl :: NetRayErrorClass

-- Create specific error constructors dynamically
for name, errorType in pairs(ErrorTypes) do
	-- Create a correctly typed function for each error type
	Errors[name] = (function(errType: string)
		return function(message: string?, details: any?): NetRayError
			return Errors.Error.new(errType, message, details)
		end
	end)(errorType) -- Pass errorType into the closure
end

-- Determine if an object is a NetRay error
function Errors.isNetRayError(err: any): boolean
	-- Check if it's a table and has the correct metatable assigned by Errors.Error.new
	return typeof(err) == "table" and getmetatable(err) == ErrorImpl
end

-- Helper to wrap pcall with error handling
-- Use generic <T> for the success return type
function Errors.try<T>(fn: (...any) -> T, ...): {success: true, value: T} | {success: false, error: NetRayError | any} -- Just use ... for varargs
	local success, result = pcall(fn, ...)

	if success then
		return {
			success = true,
			value = result :: T -- Assert the type of the successful result
		}
	else
		local errorResult: NetRayError | any
		-- Convert plain string errors to NetRay INTERNAL errors
		if typeof(result) == "string" then
			errorResult = Errors.INTERNAL(result)
		else
			-- Keep non-string errors as they are (could be NetRayError or other error objects)
			errorResult = result
		end

		return {
			success = false,
			error = errorResult
		}
	end
end

-- Format an error for transport over the network
-- Returns a plain table suitable for serialization
function Errors.serialize(err: any): {type: string, message: string, details: any?, timestamp: number}
	if Errors.isNetRayError(err) then
		-- Cast err to NetRayError to access properties safely
		local netRayErr = err :: NetRayError
		return {
			type = netRayErr.type,
			message = netRayErr.message,
			-- Note: Serializing 'details' might fail if it contains non-serializable types (Instances, functions etc.)
			details = netRayErr.details,
			timestamp = netRayErr.timestamp
		}
	else
		-- Generic error conversion
		return {
			type = ErrorTypes.INTERNAL,
			message = tostring(err),
			details = nil, -- Cannot reliably serialize details of unknown errors
			timestamp = tick()
		}
	end
end

-- Recreate a NetRayError object from serialized data
function Errors.deserialize(data: any): NetRayError
	if typeof(data) ~= "table" then
		-- Return a specific serialization error if data format is wrong
		return Errors.SERIALIZATION("Invalid error data format received for deserialization")
	end

	-- Create a new NetRayError using the data from the table
	return Errors.Error.new(
		data.type, -- Let .new handle default if type is missing/invalid
		data.message, -- Let .new handle default if message is missing
		data.details -- Pass details through
	)
end

-- Return the fully typed module table
return Errors :: ErrorsModule


--!optimize 2

--[[
    Middleware.lua
    Implements a flexible middleware system for preprocessing network events
    Optimized version with caching, performance metrics, and better error handling
    Author: Asta (@TheYusufGamer)
]]

local Utilities = require(script.Parent.Utilities)

local Players = game:GetService("Players")
type Player = Players.Player

-- Type definition for the middleware handler function
-- Returns: modifiedData (any type), or false to block, or nil to continue with original data
type MiddlewareHandler = (eventName: string, player: Player?, data: any) -> any? | boolean

-- Type definition for the internal storage of a middleware handler
type MiddlewareInfo = {
	name: string,
	handler: MiddlewareHandler,
	priority: number,
	-- Metrics per handler
	executionCount: number,
	totalExecutionTime: number,
	errors: number,
	blocked: number
}

-- Type definition for the cache entry
type MiddlewareCacheEntry = {
	result: any? | boolean,
	timestamp: number
}

-- Type definition for the precomputed path cache entry
type PrecomputedPathEntry = {
	count: number, -- How often this path was encountered
	handlers: {number}, -- Indices of handlers that actively modified data or blocked
	changesMade: boolean -- Did any handler in this path ever make a change?
}

-- Type definition for LRU Manager (assuming structure from Utilities)
type LRUManager = {
	add: (key: string) -> (),
	touch: (key: string) -> (),
	remove: (key: string) -> (),
	getSize: () -> number,
}

-- Type definition for Middleware configuration
type MiddlewareConfig = {
	enableCache: boolean,
	timeoutMs: number,
	logErrors: boolean,
	cacheTTL: number,
	enablePerformanceLogging: boolean,
	logSlowExecutions: boolean,
	slowExecutionThresholdMs: number
}

-- Type definition for Middleware performance metrics
type MiddlewareMetrics = {
	totalExecutions: number,
	cacheHits: number,
	cacheMisses: number,
	totalExecutionTime: number,
	maxExecutionTime: number,
	avgExecutionTime: number, -- Calculated in GetMetrics
	errors: number,
	blocked: number,
	timeouts: number,
	-- Calculated fields added by GetMetrics
	averageExecutionTime: number?,
	blockRate: number?,
	errorRate: number?,
	timeoutRate: number?,
	cacheHitRate: number?,
	cacheSize: number?,
	maxCacheSize: number?,
	handlerStats: {MiddlewareHandlerMetrics}?,
	precomputedPaths: number?,
}

-- Type definition for individual handler metrics within MiddlewareMetrics
type MiddlewareHandlerMetrics = {
	name: string,
	priority: number,
	executionCount: number,
	avgExecutionTime: number,
	errors: number,
	blocked: number,
	errorRate: number,
	blockRate: number,
}

-- Type definition for the Middleware class itself
export type Middleware = {
	-- Properties
	Handlers: {MiddlewareInfo},
	Cache: {[string]: MiddlewareCacheEntry?},
	MaxCacheSize: number,
	lruManager: LRUManager,
	Metrics: MiddlewareMetrics,
	Config: MiddlewareConfig,
	PrecomputedPaths: {[string]: PrecomputedPathEntry?},

	-- Methods
	Register: (self: Middleware, name: string, handler: MiddlewareHandler, priority: number?) -> (),
	Remove: (self: Middleware, name: string) -> boolean,
	Execute: (self: Middleware, eventName: string, player: Player?, data: any) -> any? | boolean, -- Mirrors handler return
	AddToCache: (self: Middleware, key: string, result: any? | boolean) -> (),
	SortHandlers: (self: Middleware) -> (),
	GetMetrics: (self: Middleware) -> MiddlewareMetrics,
	ResetMetrics: (self: Middleware) -> (),
	Configure: (self: Middleware, config: table) -> (), -- Accepts partial config table
	ClearCache: (self: Middleware) -> (),
	-- Helper Middleware Adders (optional)
	AddErrorHandling: (self: Middleware, options: {logErrors: boolean?, fallbackValue: any?, blockOnError: boolean?, priority: number?}?) -> (),
	AddRateLimiting: (self: Middleware, options: {maxRequests: number?, timeWindow: number?, cooldown: number?, logViolations: boolean?, priority: number?}?) -> (),

	-- Constructor
	new: () -> Middleware
}

-- Local function optimizations for performance (already defined)
local tableSort = table.sort
local tableFindFirst = table.find -- Renamed from table.find which isn't standard
local tableInsert = table.insert
local tableRemove = table.remove
local tableClone = table.clone or Utilities.deepCopy -- Use Utilities.Clone if available
local tick = tick
local typeof = typeof
local pcall = pcall
local warn = warn
local error = error
local tostring = tostring
local setmetatable = setmetatable

-- Define the Middleware module table
local MiddlewareImpl = {}
MiddlewareImpl.__index = MiddlewareImpl

-- Constructor
function MiddlewareImpl.new(): Middleware
	local self = setmetatable({}, MiddlewareImpl)

	self.Handlers = {} :: {MiddlewareInfo}
	self.Cache = {} :: {[string]: MiddlewareCacheEntry?}
	self.MaxCacheSize = 50

	-- Assuming Utilities.CreateLRUManager exists and returns the expected type
	self.lruManager = Utilities.CreateLRUManager(self.MaxCacheSize, function(evictedKey: string)
		self.Cache[evictedKey] = nil -- Eviction callback
	end) :: LRUManager

	self.Metrics = { -- Initialize with correct types
		totalExecutions = 0, cacheHits = 0, cacheMisses = 0,
		totalExecutionTime = 0, maxExecutionTime = 0, avgExecutionTime = 0,
		errors = 0, blocked = 0, timeouts = 0
	} :: MiddlewareMetrics -- Add type assertion

	self.Config = {
		enableCache = true, timeoutMs = 100, logErrors = true,
		cacheTTL = 60, enablePerformanceLogging = false,
		logSlowExecutions = true, slowExecutionThresholdMs = 50
	} :: MiddlewareConfig -- Add type assertion

	self.PrecomputedPaths = {} :: {[string]: PrecomputedPathEntry?}

	return self :: Middleware -- Cast the final object
end

-- Register a middleware handler
function MiddlewareImpl:Register(name: string, handler: MiddlewareHandler, priority: number?)
	if typeof(handler) ~= "function" then
		error("[NetRay Middleware] Handler must be a function", 2)
		-- No return needed after error
	end

	priority = priority or 100

	-- Check if middleware with this name already exists
	for i, middleware in ipairs(self.Handlers) do
		if middleware.name == name then
			-- Update existing middleware
			self.Handlers[i].handler = handler
			self.Handlers[i].priority = priority
			self:SortHandlers() -- Re-sort after priority change
			self:ClearCache() -- Clear cache as behavior changed
			if self.Config.enablePerformanceLogging then
				print("[NetRay Middleware] Updated middleware:", name)
			end
			return -- Exit after update
		end
	end

	-- Add new middleware
	local newMiddleware: MiddlewareInfo = {
		name = name,
		handler = handler,
		priority = priority,
		executionCount = 0,
		totalExecutionTime = 0,
		errors = 0,
		blocked = 0
	}
	tableInsert(self.Handlers, newMiddleware)
	self:SortHandlers() -- Sort after adding
	self:ClearCache() -- Clear cache as chain changed
	if self.Config.enablePerformanceLogging then
		print("[NetRay Middleware] Registered new middleware:", name)
	end
end

-- Remove a middleware handler by name
function MiddlewareImpl:Remove(name: string): boolean
	for i, middleware in ipairs(self.Handlers) do
		if middleware.name == name then
			tableRemove(self.Handlers, i)
			self:ClearCache() -- Clear cache as chain changed
			if self.Config.enablePerformanceLogging then
				print("[NetRay Middleware] Removed middleware:", name)
			end
			return true -- Indicate success
		end
	end
	return false -- Not found
end

-- Execute the middleware chain
function MiddlewareImpl:Execute(eventName: string, player: Player?, data: any): any? | boolean
	self.Metrics.totalExecutions += 1
	local startTime = tick()

	if #self.Handlers == 0 then
		return data -- Return original data if no middleware
	end

	-- Caching logic (simplified for readability, original was complex)
	-- Consider a simpler cache key strategy if the original proves too complex/buggy
	local cacheKey: string? = nil
	if self.Config.enableCache and typeof(data) ~= "table" and typeof(data) ~= "userdata" and typeof(data) ~= "function" then -- Only cache simple, hashable types
		local playerKey = typeof(player) == "Instance" and tostring(player.UserId) or "nil" -- Use UserId string
		cacheKey = eventName .. "_" .. playerKey .. "_" .. tostring(data)

		local cachedEntry = self.Cache[cacheKey]
		if cachedEntry then
			if (tick() - cachedEntry.timestamp) < self.Config.cacheTTL then
				self.Metrics.cacheHits += 1
				self.lruManager:touch(cacheKey) -- Update LRU status
				return cachedEntry.result -- Return cached result
			else
				-- Expired cache entry
				self.Cache[cacheKey] = nil
				self.lruManager:remove(cacheKey)
			end
		end
		-- If not cached or expired
		self.Metrics.cacheMisses += 1
	end
	-- Precomputation path logic removed for simplification, can be added back if needed

	-- Execute handlers
	local currentData = data
	local timeoutAt = startTime + (self.Config.timeoutMs / 1000)

	for i, middleware in ipairs(self.Handlers) do
		-- Check for timeout before executing handler
		if tick() > timeoutAt then
			warn(("[NetRay Middleware] Chain timed out for event '%s' at handler '%s'"):format(eventName, middleware.name))
			self.Metrics.timeouts += 1
			break -- Stop processing chain
		end

		local handlerStartTime = tick()
		-- Use pcall to safely execute user-provided middleware handler
		local success, resultOrError = pcall(middleware.handler, eventName, player, currentData)
		local handlerExecutionTime = tick() - handlerStartTime

		-- Update metrics for this handler
		middleware.executionCount += 1
		middleware.totalExecutionTime += handlerExecutionTime

		if not success then
			-- Handler errored
			self.Metrics.errors += 1
			middleware.errors += 1
			if self.Config.logErrors then
				warn(("[NetRay Middleware] Error in '%s': %s"):format(middleware.name, tostring(resultOrError)))
			end

		elseif resultOrError == false then
			-- Handler returned false, block the event
			self.Metrics.blocked += 1
			middleware.blocked += 1
			-- Cache the 'false' result if applicable
			if cacheKey then self:AddToCache(cacheKey, false) end
			-- Update overall metrics
			local executionTime = tick() - startTime
			self.Metrics.totalExecutionTime += executionTime
			self.Metrics.maxExecutionTime = math.max(self.Metrics.maxExecutionTime, executionTime)
			return false -- Stop chain and return false

		elseif resultOrError ~= nil then
			-- Handler returned modified data
			currentData = resultOrError
		end -- If resultOrError is nil, currentData remains unchanged

		-- Log slow executions
		if self.Config.logSlowExecutions and handlerExecutionTime * 1000 > self.Config.slowExecutionThresholdMs then
			warn(("[NetRay Middleware] Slow execution in '%s' (%d ms)"):format(middleware.name, handlerExecutionTime * 1000))
		end
	end

	-- Chain completed (or timed out)
	-- Cache the final result if caching was applicable
	if cacheKey then
		self:AddToCache(cacheKey, currentData)
	end

	-- Update overall metrics
	local executionTime = tick() - startTime
	self.Metrics.totalExecutionTime += executionTime
	self.Metrics.maxExecutionTime = math.max(self.Metrics.maxExecutionTime, executionTime)
	-- Note: avgExecutionTime calculated in GetMetrics

	return currentData -- Return the final (potentially modified) data
end

-- Add result to cache and manage LRU
function MiddlewareImpl:AddToCache(key: string, result: any? | boolean)
	if not self.Config.enableCache then return end -- Check if cache is enabled

	self.Cache[key] = {
		result = result,
		timestamp = tick()
	}
	self.lruManager:add(key) -- Let LRU manager handle size limits/eviction
end

-- Sort handlers by priority
function MiddlewareImpl:SortHandlers()
	tableSort(self.Handlers, function(a: MiddlewareInfo, b: MiddlewareInfo)
		return a.priority < b.priority
	end)
end

-- Get performance metrics
function MiddlewareImpl:GetMetrics(): MiddlewareMetrics
	-- Use tableClone or Utilities.Clone if available and deep cloning is needed, otherwise shallow copy is fine for metrics struct
	local metrics: MiddlewareMetrics = tableClone(self.Metrics)

	-- Calculate derived metrics
	local totalExecutions = metrics.totalExecutions
	if totalExecutions > 0 then
		metrics.averageExecutionTime = metrics.totalExecutionTime / totalExecutions
		metrics.blockRate = metrics.blocked / totalExecutions
		metrics.errorRate = metrics.errors / totalExecutions
		metrics.timeoutRate = metrics.timeouts / totalExecutions
	else
		metrics.averageExecutionTime = 0; metrics.blockRate = 0; metrics.errorRate = 0; metrics.timeoutRate = 0;
	end

	local totalCacheLookups = metrics.cacheHits + metrics.cacheMisses
	if totalCacheLookups > 0 then
		metrics.cacheHitRate = metrics.cacheHits / totalCacheLookups
	else
		metrics.cacheHitRate = 0
	end

	metrics.cacheSize = self.lruManager:getSize()
	metrics.maxCacheSize = self.MaxCacheSize

	-- Add individual handler stats
	metrics.handlerStats = {} :: {MiddlewareHandlerMetrics} -- Initialize with type
	for _, middleware in ipairs(self.Handlers) do
		local handlerExecCount = middleware.executionCount
		local avgTime = 0
		if handlerExecCount > 0 then
			avgTime = middleware.totalExecutionTime / handlerExecCount
		end
		local handlerStat: MiddlewareHandlerMetrics = {
			name = middleware.name,
			priority = middleware.priority,
			executionCount = handlerExecCount,
			avgExecutionTime = avgTime,
			errors = middleware.errors,
			blocked = middleware.blocked,
			errorRate = handlerExecCount > 0 and middleware.errors / handlerExecCount or 0,
			blockRate = handlerExecCount > 0 and middleware.blocked / handlerExecCount or 0
		}
		tableInsert(metrics.handlerStats, handlerStat)
	end

	-- Count precomputed paths (if logic is re-enabled)
	metrics.precomputedPaths = 0
	-- for _ in pairs(self.PrecomputedPaths) do metrics.precomputedPaths += 1 end

	return metrics
end

-- Reset performance metrics
function MiddlewareImpl:ResetMetrics()
	-- Reset overall metrics
	self.Metrics = {
		totalExecutions = 0, cacheHits = 0, cacheMisses = 0,
		totalExecutionTime = 0, maxExecutionTime = 0, avgExecutionTime = 0,
		errors = 0, blocked = 0, timeouts = 0
	} :: MiddlewareMetrics

	-- Reset metrics within each handler info object
	for _, middleware in ipairs(self.Handlers) do
		middleware.executionCount = 0
		middleware.totalExecutionTime = 0
		middleware.errors = 0
		middleware.blocked = 0
	end
	if self.Config.enablePerformanceLogging then
		print("[NetRay Middleware] Metrics Reset.")
	end
end

-- Configure middleware settings
function MiddlewareImpl:Configure(config: table) -- Accept partial config
	local cacheStateChanged = false
	local oldCacheState = self.Config.enableCache

	for key, value in pairs(config) do
		-- Only update keys that exist in the default Config table
		if self.Config[key] ~= nil and typeof(self.Config[key]) == typeof(value) then
			if key == "enableCache" and self.Config.enableCache ~= value then
				cacheStateChanged = true
			end
			self.Config[key] = value
		elseif self.Config[key] ~= nil then
			warn(("[NetRay Middleware] Configure: Type mismatch for key '%s'. Expected %s, got %s."):format(key, typeof(self.Config[key]), typeof(value)))
		end
	end

	-- If cache enabled status changed, potentially clear cache
	if cacheStateChanged and not self.Config.enableCache then
		self:ClearCache()
		if self.Config.enablePerformanceLogging then
			print("[NetRay Middleware] Cache Disabled via Configure.")
		end
	elseif cacheStateChanged and self.Config.enableCache then
		if self.Config.enablePerformanceLogging then
			print("[NetRay Middleware] Cache Enabled via Configure.")
		end
	end

	-- Update MaxCacheSize if provided in config and re-init LRU manager
	if config.MaxCacheSize and type(config.MaxCacheSize) == "number" and config.MaxCacheSize > 0 then
		self.MaxCacheSize = config.MaxCacheSize
		self.lruManager = Utilities.CreateLRUManager(self.MaxCacheSize, function(evictedKey: string)
			self.Cache[evictedKey] = nil
		end) :: LRUManager
		self:ClearCache() -- Clear cache when size changes
		if self.Config.enablePerformanceLogging then
			print("[NetRay Middleware] MaxCacheSize set to", self.MaxCacheSize)
		end
	end
end

-- Clear caches
function MiddlewareImpl:ClearCache()
	self.Cache = {} :: {[string]: MiddlewareCacheEntry?}
	self.PrecomputedPaths = {} :: {[string]: PrecomputedPathEntry?}
	-- Recreate LRU Manager to clear its internal state as well
	self.lruManager = Utilities.CreateLRUManager(self.MaxCacheSize, function(evictedKey: string)
		self.Cache[evictedKey] = nil
	end) :: LRUManager
	if self.Config.enablePerformanceLogging then
		print("[NetRay Middleware] Cache Cleared.")
	end
end

-- Add Error Handling Middleware helper
function MiddlewareImpl:AddErrorHandling(options: {logErrors: boolean?, fallbackValue: any?, blockOnError: boolean?, priority: number?}?)
	options = options or {}

	local errorHandler: MiddlewareHandler = function(eventName: string, player: Player?, data: any)
		warn("[NetRay Middleware] AddErrorHandling helper is flawed and may not provide intended error isolation.")
		return data -- Pass data through unchanged
	end

	self:Register("ErrorHandler_" .. (options.priority or 10), errorHandler, options.priority or 10)
end

-- Add Rate Limiting Middleware helper
function MiddlewareImpl:AddRateLimiting(options: {maxRequests: number?, timeWindow: number?, cooldown: number?, logViolations: boolean?, priority: number?}?)
	options = options or {}

	-- Use a closure to maintain state for this specific rate limiter instance
	local limits: {[string]: {count: number, lastReset: number, cooldownUntil: number}} = {}
	local maxRequests = options.maxRequests or 10
	local timeWindow = options.timeWindow or 1 -- seconds
	local cooldown = options.cooldown or 5 -- seconds

	local rateLimiter: MiddlewareHandler = function(eventName: string, player: Player?, data: any)
		if not player then return data end -- Only rate limit player actions

		local userId = player.UserId
		-- Use a key combining event and user for per-event limits, or just user for global limits
		local key = userId .. "_" .. eventName -- Per-event limit key

		local limitData = limits[key]
		local now = tick()

		if not limitData then
			limitData = { count = 0, lastReset = now, cooldownUntil = 0 }
			limits[key] = limitData
		end

		-- Check cooldown
		if limitData.cooldownUntil > now then
			return false -- Blocked due to cooldown
		end

		-- Reset window if needed
		if now - limitData.lastReset > timeWindow then
			limitData.count = 0
			limitData.lastReset = now
		end

		-- Increment and check limit
		limitData.count += 1
		if limitData.count > maxRequests then
			limitData.cooldownUntil = now + cooldown
			limitData.count = 0 -- Reset count when cooldown starts
			if options.logViolations ~= false then
				warn(("[NetRay Middleware] Rate limit exceeded for User %d on Event '%s'. Cooldown applied."):format(userId, eventName))
			end
			return false -- Block
		end

		return data -- Allow
	end

	self:Register("RateLimiter_"..(options.priority or 20), rateLimiter, options.priority or 20)
end

return MiddlewareImpl :: Middleware


--!optimize 2

--[[
    Queue.lua
    Implementation of a FIFO queue system for network operations (Not priority queue)
    Optimized version with memory efficiency and performance improvements
    Author: Asta (@TheYusufGamer)
]]

-- Type definitions for Luau

-- Stats structure (internal, used to build QueueStats)
type InternalQueueStats = {
	enqueues: number,
	dequeues: number,
	maxSize: number,
	resizes: number,
	peekCount: number,
	processingTimeTotal: number, -- Internal sum for avg calculation
	processingCount: number -- Internal count for avg calculation
}

-- Exported Stats structure (omits internal calculation fields)
export type QueueStats = {
	enqueues: number,
	dequeues: number,
	maxSize: number,
	resizes: number,
	peekCount: number,
	avgProcessingTime: number -- Calculated average
}

-- Queue class definition (Generic T for stored item type)
local Queue = {}
Queue.__index = Queue

-- Explicitly type the Queue object instance structure using generic T
-- Note: Direct typeof() on setmetatable doesn't work well with generics.
-- Define the instance type separately.
export type Queue<T> = {
	-- Internal properties (conventionally private)
	_items: {[number]: T?}, -- Array used as circular buffer, values can be nil after dequeue
	_size: number, -- Current number of items in queue
	_head: number, -- Physical index of the first item
	_tail: number, -- Physical index *after* the last item
	_capacity: number, -- Current physical size of the _items array
	_stats: InternalQueueStats, -- Internal stats tracking
	-- Homogeneous tracking (less useful without priority, but keep if used elsewhere)
	_isHomogeneous: boolean,
	_itemType: string,

	-- Methods
	Enqueue: (self: Queue<T>, item: T) -> (),
	Dequeue: (self: Queue<T>) -> T?,
	Peek: (self: Queue<T>) -> T?,
	Size: (self: Queue<T>) -> number,
	IsEmpty: (self: Queue<T>) -> boolean,
	Clear: (self: Queue<T>) -> (),
	GetItems: (self: Queue<T>) -> {T}, -- Returns array of type T
	Find: (self: Queue<T>, predicate: (value: T) -> boolean) -> number?,
	Remove: (self: Queue<T>, predicate: (value: T) -> boolean) -> T?,
	BatchEnqueue: (self: Queue<T>, items: {T}) -> (),
	GetStats: (self: Queue<T>) -> QueueStats,
	-- Internal methods
	_PhysicalIndex: (self: Queue<T>, logicalIndex: number) -> number,
	_Resize: (self: Queue<T>, newCapacity: number) -> (),
}

-- Constants
local DEFAULT_INITIAL_CAPACITY = 16
local GROWTH_FACTOR = 1.5
local SHRINK_THRESHOLD = 0.25
local Utilities = require(script.Parent.Utilities)

-- Local utility functions
local tableCreate = table.create
local tableClone = table.clone or Utilities.deepCopy
local mathCeil = math.ceil
local mathFloor = math.floor
local mathMax = math.max -- Cache math.max

-- Constructor function
function Queue.new<T>(initialCapacity: number?): Queue<T>
	local capacity = mathMax(initialCapacity or DEFAULT_INITIAL_CAPACITY, 1) -- Ensure positive capacity
	local self = setmetatable({}, Queue)
	self._items = tableCreate(capacity) :: {[number]: T?} -- Assert type of items table
	self._size = 0
	self._head = 1
	self._tail = 1
	self._capacity = capacity
	self._isHomogeneous = true
	self._itemType = ""
	self._stats = { -- Initialize internal stats
		enqueues = 0, dequeues = 0, maxSize = 0, resizes = 0, peekCount = 0,
		processingTimeTotal = 0, processingCount = 0
	} :: InternalQueueStats -- Assert type of stats table

	return self :: Queue<T> -- Assert return type
end

-- Add an item to the queue
function Queue:Enqueue(item: T) -- Type T for item
	self._stats.enqueues += 1
--	print(os.time(), string.format("[Queue] Enqueued. Size: %d, Head: %d, Tail: %d, Cap: %d", self._size, self._head, self._tail, self._capacity))
	if self._size >= self._capacity then
		self:_Resize(mathCeil(self._capacity * GROWTH_FACTOR))
	end

	-- Update homogeneous tracking (removed priority check)
	if self._size == 0 and item ~= nil then self._itemType = typeof(item)
	elseif self._isHomogeneous and item ~= nil and typeof(item) ~= self._itemType then self._isHomogeneous = false
	end

	self._items[self._tail] = item
	self._tail = (self._tail % self._capacity) + 1
	self._size += 1
	self._stats.maxSize = mathMax(self._size, self._stats.maxSize)
end

-- Convert logical index to physical index
function Queue:_PhysicalIndex(logicalIndex: number): number
	if logicalIndex < 1 or logicalIndex > self._size then error("Logical index out of bounds", 2) end
	return (self._head + logicalIndex - 2) % self._capacity + 1
end

-- Resize internal array
-- In Queue.lua
function Queue:_Resize(newCapacity: number)
	if newCapacity <= self._size then error("New capacity must be larger than current size", 2) end
	self._stats.resizes += 1
	local newItems = tableCreate(newCapacity) :: {[number]: T?}

--	print(os.time(), string.format("[Queue Resize Start] OldCap: %d, NewCap: %d, Size: %d, Head: %d, Tail: %d", self._capacity, newCapacity, self._size, self._head, self._tail)) -- Add log

	-- Copy items into newItems starting at index 1
	local currentTargetIndex = 1
	if self._head < self._tail then -- Contiguous block (Normal Case)
		-- Copy from old physical index self._head up to self._tail - 1
		for i = self._head, self._tail - 1 do
			newItems[currentTargetIndex] = self._items[i]
			currentTargetIndex = currentTargetIndex + 1
		end
	else -- Wrapped around block
		-- Copy from old physical index self._head up to self._capacity
		for i = self._head, self._capacity do
			newItems[currentTargetIndex] = self._items[i]
			currentTargetIndex = currentTargetIndex + 1
		end
		-- Copy from old physical index 1 up to self._tail - 1
		for i = 1, self._tail - 1 do
			newItems[currentTargetIndex] = self._items[i]
			currentTargetIndex = currentTargetIndex + 1
		end
	end

	-- Check if the number of copied items matches the size
	local itemsCopied = currentTargetIndex - 1
	if itemsCopied ~= self._size then
		warn(string.format("[Queue Resize WARN] Size mismatch after copy! Expected %d, Copied %d. State(H:%d,T:%d,C:%d)", self._size, itemsCopied, self._head, self._tail, self._capacity))
		-- This indicates a potential logic error in the copy loops above
	end

	-- Update internal state
	self._items = newItems
	self._head = 1                     -- New head is always 1
	self._tail = self._size + 1       -- New tail is size + 1 (index after last item)
	self._capacity = newCapacity

	-- Log state AFTER resize (Using corrected print from last time)
	--print(os.time(), string.format("[Queue Resize Complete] New Cap: %d, Size: %d, Head: %d, Tail: %d", self._capacity, self._size, self._head, self._tail))
	local snippet = {}; local log = {"    New Items (Start): {"}; local first=true
	for i = 1, math.min(5, self._size) do if not first then table.insert(log,", ") end; table.insert(log,string.format("[%d]=",i)); table.insert(log,tostring(self._items[i])); first=false; end
	table.insert(log," }");-- print(table.concat(log))
	-- Check if the item at the new head is nil IMMEDIATELY after resize
	if self._size > 0 and self._items[self._head] == nil then
		warn("[Queue Resize CRITICAL] Item at new head (index 1) is nil immediately after resize!")
	end
end

-- Remove and return item from front
function Queue:Dequeue(): T? -- Returns optional T
	if self._size == 0 then return nil end
	local startTime = tick()
	self._stats.dequeues += 1

	local physicalHead = self._head -- Store physical index before incrementing
--	print("----> Queue State Before Dequeue <----")
--	print("      Size:", self._size, "Head:", self._head, "Tail:", self._tail, "Cap:", self._capacity)
	-- Log only a relevant snippet around head if table is huge
	local snippet = {}
	for i = math.max(1, self._head - 2), math.min(self._capacity, self._head + 2) do
		snippet[i] = self._items[i]
	end
--	print("      Items near head:", snippet)
	-- Or print the whole table if small enough for debugging:
	-- print("      Full _items:", self._items)
--	print("------------------------------------")
	local item = self._items[physicalHead]
	self._items[physicalHead] = nil -- Clear reference
	self._head = (self._head % self._capacity) + 1
	self._size -= 1

	-- Shrink logic
	if self._capacity > DEFAULT_INITIAL_CAPACITY and self._size < self._capacity * SHRINK_THRESHOLD then
		local newCapacity = mathMax(mathCeil(self._capacity / GROWTH_FACTOR), DEFAULT_INITIAL_CAPACITY)
		if newCapacity < self._capacity then self:_Resize(newCapacity) end -- Only resize if smaller
	end

	local processingTime = tick() - startTime
	self._stats.processingTimeTotal += processingTime
	self._stats.processingCount += 1
--	print(os.time(), string.format("[Queue] Dequeued. New Size: %d, New Head: %d, Tail: %d", self._size, self._head, self._tail))
--	print("    Dequeued Item:", item)
	return item -- Return the dequeued item
end

-- Peek at front item
function Queue:Peek(): T? -- Returns optional T
	if self._size == 0 then return nil end
	self._stats.peekCount += 1
	return self._items[self._head]
end

-- Get current size
function Queue:Size(): number return self._size end

-- Check if empty
function Queue:IsEmpty(): boolean return self._size == 0 end

-- Clear the queue
function Queue:Clear()
	if self._size > 1000 then self._items = tableCreate(self._capacity) :: {[number]: T?} -- Faster clear for large queues
	else for i = 1, self._size do self._items[self:_PhysicalIndex(i)] = nil end end -- Explicit clear for smaller
	self._size = 0; self._head = 1; self._tail = 1; self._isHomogeneous = true; self._itemType = ""
end

-- Get all items as an array
function Queue:GetItems(): {T} -- Returns array of T
	local result = tableCreate(self._size) :: {T} -- Assert type
	if self._size == 0 then return result end
	-- No need to check self._isHomogeneous here, just copy
	for i = 1, self._size do result[i] = self._items[self:_PhysicalIndex(i)] :: T end -- Assert item type
	return result
end

-- Find item by predicate
function Queue:Find(predicate: (value: T) -> boolean): number? -- Predicate takes T
	for i = 1, self._size do
		local value = self._items[self:_PhysicalIndex(i)] :: T -- Assert T
		if predicate(value) then return i end
	end
	return nil
end

-- Remove item by predicate
function Queue:Remove(predicate: (value: T) -> boolean): T? -- Predicate takes T, returns optional T
	local index = self:Find(predicate)
	if not index then return nil end
	local physicalIndex = self:_PhysicalIndex(index)
	local value = self._items[physicalIndex] -- Get value before potential modification
	if index == 1 then -- Remove from head
		self._items[physicalIndex] = nil; self._head = (self._head % self._capacity) + 1
	elseif index == self._size then -- Remove from tail
		local physicalTailBefore = (self._tail - 2 + self._capacity) % self._capacity + 1 -- Calculate correct tail index *before* modification
		self._items[physicalTailBefore] = nil
		self._tail = physicalTailBefore -- Update tail index
	else -- Remove from middle (shift elements)
		for i = index, self._size - 1 do
			local currentIdx = self:_PhysicalIndex(i)
			local nextIdx = self:_PhysicalIndex(i + 1)
			self._items[currentIdx] = self._items[nextIdx]
		end
		local physicalTailBefore = (self._tail - 2 + self._capacity) % self._capacity + 1
		self._items[physicalTailBefore] = nil -- Clear the last shifted element
		self._tail = physicalTailBefore
	end
	self._size -= 1
	return value
end

-- Batch enqueue items
function Queue:BatchEnqueue(items: {T}) -- Takes array of T
	local numItems = #items; if numItems == 0 then return end
	local requiredCapacity = self._size + numItems
	if requiredCapacity > self._capacity then
		local newCapacity = self._capacity; while newCapacity < requiredCapacity do newCapacity = mathCeil(newCapacity * GROWTH_FACTOR) end
		if newCapacity > self._capacity then self:_Resize(newCapacity) end
	end
	-- Use individual Enqueue to maintain stats and homogeneous checks correctly
	for i = 1, numItems do self:Enqueue(items[i]) end
end

-- Get statistics
function Queue:GetStats(): QueueStats
	local stats = tableClone(self._stats) :: InternalQueueStats -- Clone internal stats
	local avgTime = 0
	if stats.processingCount > 0 then avgTime = stats.processingTimeTotal / stats.processingCount end
	-- Create the public QueueStats object
	local publicStats: QueueStats = {
		enqueues = stats.enqueues, dequeues = stats.dequeues, maxSize = stats.maxSize,
		resizes = stats.resizes, peekCount = stats.peekCount, avgProcessingTime = avgTime
	}
	return publicStats
end

-- Define the type for the module table being returned
export type QueueModule = {
	new: <T>(initialCapacity: number?) -> Queue<T>, -- Export the generic constructor
	-- Export the Queue *type* itself if needed for external annotations
	-- This is tricky with generics. Exporting a base type is safer.
	Queue: Queue<any> | nil -- Placeholder for base type if needed externally
}

-- Return the module table, asserting its type
return Queue :: QueueModule


--!optimize 2
--!native

--[[
    NetRaySerializer

    Serializes Luau data structures into optimized buffers with type identifiers.
    Implements precise subtype selection for Numbers, Vectors, CFrames, and Booleans.
	Handles Homogeneous Arrays and Dictionaries distinctly.

	Only Used in Request function where data send is less frequent and large

	Author: Asta (@TheYusufGamer)
]]

-- Type Imports (assuming these modules export types)
local CursorModule = require(script.Parent.Parent.Types.Cursor)
local TypesModule = require(script.Parent.Parent.Types.Types)

-- Define assumed types for dependencies
type Cursor = CursorModule.Cursor -- Assuming Cursor type with methods/properties
type TypeWriterFunc = (cursor: Cursor, value: any) -> ()
type TypeReaderFunc = (cursor: Cursor) -> any
type TypesRegistry = {
	Writes: { [string]: TypeWriterFunc },
	Reads: { [string]: TypeReaderFunc }
}
-- Use the assumed type for the required module
local Types: TypesRegistry = TypesModule

-- Local function optimizations (standard Lua globals)
local mathFloor = math.floor; local mathAbs = math.abs; local mathMax = math.max
local bufferCreate = buffer.create; local bufferLen = buffer.len
local tableCreate = table.create; local tick = tick; local typeof = typeof
local tostring = tostring; local stringFormat = string.format; local stringByte = string.byte
local pairs = pairs; local ipairs = ipairs; local pcall = pcall
local error = error; local warn = warn

-- Compression Headers (Constants)
local BINARY_HEADER_BYTE: number = 0x02
local UNCOMPRESSED_HEADER_BYTE: number = 0x01

-- Exported Type Definitions
export type TypeName = string -- e.g., "NumberU8", "String", "Dictionary"
export type TypeCode = number -- e.g., 0x00, 0x40, 0xD1
export type SerializedResult = buffer -- The result of Serialize

-- Forward declaration for recursive functions with type annotations
local writeTypedValue: (cursor: Cursor, value: any) -> ()
local readTypedValue: (cursor: Cursor) -> any

-- Module Table Definition
local NetRaySerializer = {}

-- Type for internal metrics structure
type SerializerMetrics = {
	serializeCount: number,
	deserializeCount: number,
	totalSerializeTime: number,
	totalDeserializeTime: number,
	errors: number,
	largestSerialized: number,
	homogeneousArrayCount: number,
	dictionaryCount: number,
	-- Calculated fields from GetMetrics
	averageSerializeTime: number?,
	averageDeserializeTime: number?,
}

-- Type for the main module export
export type NetRaySerializerModule = {
	Serialize: (data: any) -> SerializedResult?, -- Returns buffer on success, nil on error
	Deserialize: (buf: buffer) -> any?, -- Returns deserialized value or nil on error
	EstimateSize: (data: any) -> number, -- Returns estimated byte size
	GetMetrics: () -> SerializerMetrics,
	ResetMetrics: () -> (),
	ClearCache: () -> (), -- Placeholder
}

-- Internal metrics state
local metrics: SerializerMetrics = {
	serializeCount = 0, deserializeCount = 0, totalSerializeTime = 0, totalDeserializeTime = 0,
	errors = 0, largestSerialized = 0, homogeneousArrayCount = 0, dictionaryCount = 0,
}

-- Type Codes Constant Table
local TYPE_CODES: {[string]: TypeCode} = {
	Nil = 0x00,
	NumberU8 = 0x02, NumberU16 = 0x03, NumberU24 = 0x04, NumberU32 = 0x05,
	NumberS8 = 0x10, NumberS16 = 0x11, NumberS24 = 0x12, NumberS32 = 0x13,
	NumberF16 = 0x20, NumberF24 = 0x21, NumberF32 = 0x22, NumberF64 = 0x23,
	Boolean8 = 0x30,
	String = 0x40, Characters = 0x41, Buffer = 0x42, Binary = 0x43,
	Vector2S16 = 0x50, Vector2F24 = 0x51, Vector2F32 = 0x52,
	Vector3S16 = 0x60, Vector3F24 = 0x61, Vector3F32 = 0x62,
	CFrameF24U8 = 0x70, CFrameF32U8 = 0x71, CFrameF32U16 = 0x72,
	Color3 = 0x80, NumberRange = 0x81, NumberSequence = 0x82, ColorSequence = 0x83,
	BrickColor = 0x84, UDim = 0x90, UDim2 = 0x91, Rect = 0x92, Region3 = 0x93,
	EnumItem = 0xA0, Instance = 0xA1,
	HomogeneousArray = 0xD0, Dictionary = 0xD1,
}

-- Reverse Mapping Constant Table
local BYTE_TO_TYPE_NAME: { [TypeCode]: TypeName } = {}
for name, code in pairs(TYPE_CODES) do BYTE_TO_TYPE_NAME[code] = name end -- Populate dynamically

-- Epsilon Constant
local FLOAT_EPSILON = 1e-5

-- Helper: isInteger
local function isInteger(value: number): boolean
	return mathAbs(value - mathFloor(value)) < FLOAT_EPSILON
end

-- Helper: determineType
local function determineType(value: any): (TypeName, TypeCode)
	local valueType = typeof(value)
	if value == nil then return "Nil", TYPE_CODES.Nil end

	if valueType == "number" then
		local numValue = value :: number
		if isInteger(numValue) then
			if numValue >= 0 then -- Unsigned
				if numValue <= 255 then return "NumberU8", TYPE_CODES.NumberU8 end
				if numValue <= 65535 then return "NumberU16", TYPE_CODES.NumberU16 end
				if numValue <= 16777215 then return "NumberU24", TYPE_CODES.NumberU24 end
				if numValue <= 4294967295 then return "NumberU32", TYPE_CODES.NumberU32 end
			else -- Signed
				if numValue >= -128 then return "NumberS8", TYPE_CODES.NumberS8 end -- S8 range includes -128 to 127
				if numValue >= -32768 then return "NumberS16", TYPE_CODES.NumberS16 end
				if numValue >= -8388608 then return "NumberS24", TYPE_CODES.NumberS24 end
				if numValue >= -2147483648 then return "NumberS32", TYPE_CODES.NumberS32 end
			end
		end
		-- Float (fallback if not integer or exceeds int range)
		local magnitude = mathAbs(numValue)
		-- These checks are simplified heuristics, actual float representation is complex.
		-- Check F16/F24 only if magnitude allows reasonable precision, else default F32/F64
		-- This needs refinement based on actual F16/F24 implementation details if used.
		-- Sticking to F32/F64 based on typical ranges for now.
		if magnitude <= 3.402823e+38 then return "NumberF32", TYPE_CODES.NumberF32 -- Approx Max Single Float
		else return "NumberF64", TYPE_CODES.NumberF64 -- Double Float
		end
		-- Simplified F16/F24 logic removed unless specific implementation exists in Types.Writes/Reads
		-- if magnitude <= 2048 then return "NumberF16", TYPE_CODES.NumberF16 end
		-- if magnitude <= 262144 then return "NumberF24", TYPE_CODES.NumberF24 end
		-- if magnitude <= 16777216 then return "NumberF32", TYPE_CODES.NumberF32 end
		-- return "NumberF64", TYPE_CODES.NumberF64
	end

	if valueType == "boolean" then return "Boolean8", TYPE_CODES.Boolean8 end

	if valueType == "string" then
		local len = #value
		if len > 0 then
			local firstByte = stringByte(value, 1)
			if firstByte == BINARY_HEADER_BYTE or firstByte == UNCOMPRESSED_HEADER_BYTE then
				-- Binary should probably be stored as buffer? If passed as string, handle appropriately.
				return "Binary", TYPE_CODES.Binary -- Requires reader/writer to handle string->binary->string
			else
				if len <= 255 then return "String", TYPE_CODES.String
				elseif len <= 65535 then return "Characters", TYPE_CODES.Characters
				else error("String exceeds 65535 bytes limit", 2) end
			end
		else
			return "String", TYPE_CODES.String -- Empty string uses String type code
		end
	end
	if valueType == "buffer" then
		if bufferLen(value) <= 65535 then return "Buffer", TYPE_CODES.Buffer
		else error("Buffer exceeds 65535 bytes limit", 2) end
	end

	if valueType == "Vector2" then
		local v2 = value :: Vector2
		if isInteger(v2.X) and isInteger(v2.Y) and v2.X >= -32768 and v2.X <= 32767 and v2.Y >= -32768 and v2.Y <= 32767 then return "Vector2S16", TYPE_CODES.Vector2S16 end
		-- Refined float checks based on typical usage (F32 is standard)
		return "Vector2F32", TYPE_CODES.Vector2F32 -- Default to F32, F24 optimization might be complex
		-- local mag = v2.Magnitude; if mag <= 262144 then return "Vector2F24", TYPE_CODES.Vector2F24 else return "Vector2F32", TYPE_CODES.Vector2F32 end
	end

	if valueType == "Vector3" then
		local v3 = value :: Vector3
		if isInteger(v3.X) and isInteger(v3.Y) and isInteger(v3.Z) and v3.X >= -32768 and v3.X <= 32767 and v3.Y >= -32768 and v3.Y <= 32767 and v3.Z >= -32768 and v3.Z <= 32767 then return "Vector3S16", TYPE_CODES.Vector3S16 end
		-- Default to F32
		return "Vector3F32", TYPE_CODES.Vector3F32
		-- local mag = v3.Magnitude; if mag <= 262144 then return "Vector3F24", TYPE_CODES.Vector3F24 else return "Vector3F32", TYPE_CODES.Vector3F32 end
	end

	if valueType == "CFrame" then
		-- Simplified CFrame logic, default to a common precise format unless specific optimizations needed
		return "CFrameF32U8", TYPE_CODES.CFrameF32U8 -- Common format balancing precision/size
		-- local cfValue = value :: CFrame; local posMag = cfValue.Position.Magnitude
		-- if posMag < 262144 then return "CFrameF24U8", TYPE_CODES.CFrameF24U8
		-- elseif posMag < 16777216 then return "CFrameF32U8", TYPE_CODES.CFrameF32U8
		-- else return "CFrameF32U16", TYPE_CODES.CFrameF32U16 end
	end

	if valueType == "Color3" then return "Color3", TYPE_CODES.Color3 end
	if valueType == "NumberRange" then return "NumberRange", TYPE_CODES.NumberRange end
	if valueType == "NumberSequence" then return "NumberSequence", TYPE_CODES.NumberSequence end
	if valueType == "ColorSequence" then return "ColorSequence", TYPE_CODES.ColorSequence end
	if valueType == "BrickColor" then return "BrickColor", TYPE_CODES.BrickColor end
	if valueType == "UDim" then return "UDim", TYPE_CODES.UDim end
	if valueType == "UDim2" then return "UDim2", TYPE_CODES.UDim2 end
	if valueType == "Rect" then return "Rect", TYPE_CODES.Rect end
	if valueType == "Region3" then return "Region3", TYPE_CODES.Region3 end
	if valueType == "EnumItem" then return "EnumItem", TYPE_CODES.EnumItem end
	if valueType == "Instance" then return "Instance", TYPE_CODES.Instance end

	if valueType == "table" then return "Table", 0 end -- Placeholder

	error(stringFormat("Unsupported type for serialization: %s", valueType), 2)
end

-- Helper: analyzeTable
-- Returns: Table Type Code (Dictionary/Homogeneous), Element TypeName?, Element TypeCode?
local function analyzeTable(tbl: {any}): (TypeCode, TypeName?, TypeCode?)
	local count = 0; local maxIndex = 0; local isArray = true
	local firstElementType: TypeName? = nil; local firstElementCode: TypeCode? = nil
	local isHomogeneous = true

	for k, v in pairs(tbl) do
		count += 1
		if typeof(k) == "number" and k == mathFloor(k) and k > 0 then maxIndex = mathMax(maxIndex, k) else isArray = false end
		if isHomogeneous then
			local success, valueTypeNameOrErr, valueTypeCode = pcall(determineType, v)
			if not success then isHomogeneous = false
			else local valueTypeName = valueTypeNameOrErr :: TypeName
				if valueTypeName == "Table" or valueTypeName == "Nil" or valueTypeName == "Instance" or valueTypeName == "Buffer" or valueTypeName == "Binary" then isHomogeneous = false
				elseif firstElementType == nil then firstElementType = valueTypeName; firstElementCode = valueTypeCode
				elseif valueTypeName ~= firstElementType or valueTypeCode ~= firstElementCode then isHomogeneous = false
				end
			end
		end
		if not isArray and not isHomogeneous then break end
	end

	if count == 0 or (isArray and count ~= maxIndex) then isArray = false; isHomogeneous = false end

	if isArray and isHomogeneous then
		metrics.homogeneousArrayCount += 1
		return TYPE_CODES.HomogeneousArray, firstElementType, firstElementCode
	else
		metrics.dictionaryCount += 1
		return TYPE_CODES.Dictionary, nil, nil
	end
end

-- Recursive Writer Function Implementation
writeTypedValue = function(cursor: Cursor, value: any)
	local initialTypeName, initialTypeCode = determineType(value)

	if initialTypeName == "Table" then
		local tblValue = value :: {any} -- Assert table type
		local specificTableTypeCode, elementTypeName, elementTypeCode = analyzeTable(tblValue)
		cursor:WriteU1(specificTableTypeCode)

		if specificTableTypeCode == TYPE_CODES.HomogeneousArray then
			cursor:WriteU1(elementTypeCode :: TypeCode) -- Element type code
			local len = #tblValue
			cursor:WriteU2(len) -- Array length
			local writerFunc = Types.Writes[elementTypeName :: TypeName]
			if not writerFunc then error("Missing writer for homogeneous element type: " .. (elementTypeName or "nil"), 2) end
			for i = 1, len do writerFunc(cursor, tblValue[i]) end -- Write elements

		elseif specificTableTypeCode == TYPE_CODES.Dictionary then
			local count = 0; for _, _ in pairs(tblValue) do count += 1 end
			cursor:WriteU2(count) -- Number of key-value pairs
			for k, v in pairs(tblValue) do
				writeTypedValue(cursor, k) -- Write key (recursively)
				writeTypedValue(cursor, v) -- Write value (recursively)
			end
		end
	else
		-- Not a table, write type code and use specific writer
		cursor:WriteU1(initialTypeCode)
		if initialTypeName ~= "Nil" then -- Don't write data for Nil
			local writerFunc = Types.Writes[initialTypeName]
			if not writerFunc then error("Missing writer function for type: " .. initialTypeName, 2) end
			writerFunc(cursor, value)
		end
	end
end

-- Recursive Reader Function Implementation
readTypedValue = function(cursor: Cursor): any
	local typeCode = cursor:ReadU1()
	local typeName = BYTE_TO_TYPE_NAME[typeCode]
	if not typeName then error(stringFormat("Unknown type code: 0x%02X at pos %d", typeCode, cursor.Index - 1), 2) end

	if typeName == "Nil" then return nil end

	if typeName == "HomogeneousArray" then
		local elementTypeCode = cursor:ReadU1()
		local len = cursor:ReadU2()
		local elementTypeName = BYTE_TO_TYPE_NAME[elementTypeCode]
		if not elementTypeName then error(stringFormat("Unknown element type code for Array: 0x%02X at pos %d", elementTypeCode, cursor.Index - 1), 2) end
		local readerFunc = Types.Reads[elementTypeName]
		if not readerFunc then error("Missing reader for homogeneous element type: " .. elementTypeName, 2) end
		local resultTable = tableCreate(len)
		for i = 1, len do resultTable[i] = readerFunc(cursor) end
		return resultTable

	elseif typeName == "Dictionary" then
		local count = cursor:ReadU2()
		local resultTable = tableCreate(count) -- Pre-allocate approx size
		for _ = 1, count do
			local key = readTypedValue(cursor)
			local value = readTypedValue(cursor)
			if key ~= nil then resultTable[key] = value
			else warn("NetRaySerializer.Deserialize: Nil key encountered in dictionary, value ignored.") end
		end
		return resultTable
	end

	-- Handle simple types
	local readerFunc = Types.Reads[typeName]
	if not readerFunc then error("Missing reader function for type: " .. typeName, 2) end
	return readerFunc(cursor)
end

-- Serialize Public Method
function NetRaySerializer.Serialize(data: any): SerializedResult?
	metrics.serializeCount += 1
	local startTime = tick()
	local resultBuffer: SerializedResult? = nil

	local estimatedSize = NetRaySerializer.EstimateSize(data)
	local bufferInstance = bufferCreate(mathMax(estimatedSize, 16)) -- Use local name
	local cursor = CursorModule(bufferInstance) -- Assume Cursor constructor

	local success, err = pcall(writeTypedValue, cursor, data)

	if success then
		resultBuffer = cursor:Truncate()
		metrics.largestSerialized = mathMax(metrics.largestSerialized, bufferLen(resultBuffer))
	else
		metrics.errors += 1
		warn(stringFormat("NetRaySerializer.Serialize Error: %s", tostring(err)))
	end

	metrics.totalSerializeTime += (tick() - startTime)
	return resultBuffer
end

-- Deserialize Public Method
function NetRaySerializer.Deserialize(buf: buffer): any?
	metrics.deserializeCount += 1
	local startTime = tick()
	local result: any = nil

	if typeof(buf) ~= "buffer" or bufferLen(buf) == 0 then
		warn("NetRaySerializer.Deserialize Error: Input must be a non-empty buffer.")
		metrics.totalDeserializeTime += (tick() - startTime); metrics.errors += 1; return nil
	end

	local cursor = CursorModule(buf) -- Assume Cursor constructor
	local success, valueOrError = pcall(readTypedValue, cursor)

	if success then
		if cursor.Index < bufferLen(buf) then
			warn(stringFormat("NetRaySerializer.Deserialize Warning: Buffer not fully consumed (%d / %d bytes).", cursor.Index, bufferLen(buf)))
		elseif cursor.Index > bufferLen(buf) then
			warn(stringFormat("NetRaySerializer.Deserialize Error: Read past end of buffer (%d / %d bytes).", cursor.Index, bufferLen(buf)))
			metrics.errors += 1; result = nil -- Force nil result on over-read
		end
		-- Assign result only if no critical over-read error occurred
		if cursor.Index <= bufferLen(buf) then
			result = valueOrError
		end
	else
		metrics.errors += 1
		warn(stringFormat("NetRaySerializer.Deserialize Error: %s", tostring(valueOrError)))
		result = nil
	end

	metrics.totalDeserializeTime += (tick() - startTime)
	return result
end

-- EstimateSize Public Method
function NetRaySerializer.EstimateSize(data: any): number
	local valueType = typeof(data)
	local size = 1 -- TypeCode

	if data == nil then return size end
	if valueType == "boolean" then return size + 1 end
	if valueType == "number" then return size + 5 end -- Average guess
	if valueType == "string" then local len = #data; return size + (if len <= 255 then 1 else 2) + len end
	if valueType == "buffer" then return size + 2 + bufferLen(data) end
	if valueType == "Vector2" then return size + 6 end
	if valueType == "Vector3" then return size + 10 end
	if valueType == "CFrame" then return size + 18 end
	if valueType == "Instance" then return size + 8 end -- Ref approx
	if valueType == "Color3" then return size + 12 end
	if valueType == "BrickColor" then return size + 2 end
	if valueType == "UDim" then return size + 8 end
	if valueType == "UDim2" then return size + 16 end
	if valueType == "Rect" then return size + 16 end
	if valueType == "Region3" then return size + 24 end
	if valueType == "NumberRange" then return size + 8 end
	if valueType == "EnumItem" then return size + 4 end
	if valueType == "NumberSequence" then local seq = data :: NumberSequence; return size + 2 + #seq.Keypoints * 12 end
	if valueType == "ColorSequence" then local seq = data :: ColorSequence; return size + 2 + #seq.Keypoints * 16 end

	if valueType == "table" then
		local tbl = data :: {any}
		local est = size + 2; local elementSize = 0; local homo = true
		for k, v in pairs(tbl) do
			if typeof(k) ~= "number" then homo = false end
			est += NetRaySerializer.EstimateSize(k) + NetRaySerializer.EstimateSize(v)
			if homo and elementSize == 0 then elementSize = NetRaySerializer.EstimateSize(v) - 1 end
		end
		if homo and #tbl > 0 then local homoEst = size + 1 + 2 + #tbl * elementSize; return mathMax(4, math.min(est * 1.1, homoEst))
		else return mathMax(3, est * 1.1) end
	end

	warn("NetRaySerializer.EstimateSize: Generic fallback estimate for type:", valueType); return size + 16
end

-- GetMetrics Public Method
function NetRaySerializer.GetMetrics(): SerializerMetrics
	local metricsCopy = {}
	for k, v in pairs(metrics) do metricsCopy[k] = v end
	metricsCopy.averageSerializeTime = if metrics.serializeCount > 0 then metrics.totalSerializeTime / metrics.serializeCount else 0
	metricsCopy.averageDeserializeTime = if metrics.deserializeCount > 0 then metrics.totalDeserializeTime / metrics.deserializeCount else 0
	return metricsCopy :: SerializerMetrics -- Assert return type
end

-- ResetMetrics Public Method
function NetRaySerializer.ResetMetrics()
	metrics = {
		serializeCount = 0, deserializeCount = 0, totalSerializeTime = 0, totalDeserializeTime = 0,
		errors = 0, largestSerialized = 0, homogeneousArrayCount = 0, dictionaryCount = 0,
	} :: SerializerMetrics
end

-- ClearCache Public Method
function NetRaySerializer.ClearCache() end -- No cache implemented yet

return NetRaySerializer :: NetRaySerializerModule


--!optimize 2

--[[
    Utilities.lua
    Common utility functions used throughout the NetRay library
    Optimized version with caching and improved performance
    Author: Asta (@TheYusufGamer)
]]

local Utilities = {}

-- Type definition for LRU Manager (simplified placeholder)
-- Actual type depends on the implementation in CreateLRUManager
type LRUManager = {
	add: (key: string) -> (),
	touch: (key: string) -> (),
	remove: (key: string) -> (),
	getSize: () -> number,
}

-- Type definition for the Utilities module
export type UtilitiesModule = {
	generateId: () -> string,
	deepCopy: <T>(original: T, seen: {any}?) -> T,
	shallowCopy: <T>(original: T) -> T,
	timer: () -> (() -> number),
	throttle: <F>(fn: F, limit: number) -> F,
	debounce: <F>(fn: F, wait: number) -> F,
	safeCall: <T>(fn: (...any) -> T, ... any) -> (boolean, T | any),
	memoize: <F>(fn: F, keyFn: ((...any) -> string)?, ttl: number?) -> F,
	isInstanceOf: (object: any, className: string) -> boolean,
	formatNetworkSize: (bytes: number) -> string,
	createLogger: (prefix: string) -> ((message: any, level: string?, context: any?) -> ()),
	waitFor: <T>(condition: () -> T?, timeout: number?, checkInterval: number?, onProgress: ((elapsed: number, total: number) -> ())?) -> (boolean, T | string),
	getMemoizeStats: () -> { cacheGroups: number, cacheEntries: number, cacheHits: number, cacheMisses: number, hitRatio: number },
	clearMemoizeCache: (fnId: string?) -> (),
	CreateLRUManager: (maxSize: number, onEvict: (key: string) -> ()) -> LRUManager,
	Table: {
		getPath: <T>(tbl: any, path: {string | number}, defaultValue: T?) -> T?,
		setPath: (tbl: any, path: {string | number}, value: any) -> (),
		isEmpty: (tbl: any) -> boolean,
		serialize: (tbl: table) -> string, -- Added type for serialize helper
	}
}

-- Cache stats (global for memoize, though individual instances have own cache now)
local memoizeCacheHits = 0
local memoizeCacheMisses = 0
local MAX_MEMOIZE_CACHE_SIZE = 100

-- Counter for ID generation (closure)
local idCounter = (function()
	local counter = 0
	return function(): number
		counter += 1
		return counter
	end
end)()

-- Local optimizations
local mathFloor = math.floor
local mathRandom = math.random
local mathMax = math.max -- Cache math.max
local stringFormat = string.format
local stringByte = string.byte
local stringSub = string.sub -- Cache string.sub
local tableCreate = table.create
local tableInsert = table.insert -- Cache table.insert
local tableRemove = table.remove
local tableConcat = table.concat -- Cache table.concat
local osTime = os.time
local tick = tick
local type = type -- Cache global type
local pairs = pairs -- Cache global pairs
local ipairs = ipairs -- Cache global ipairs
local pcall = pcall
local task = task -- Cache task library
local setmetatable = setmetatable
local getmetatable = getmetatable
local unpack = unpack -- Not used, can remove
local tostring = tostring -- Cache global tostring

-- Generate unique ID
function Utilities.generateId(): string
	local counter = idCounter()
	local time = osTime()
	local rand = mathRandom(0, 1048575)
	return stringFormat("%x%x%x", time, rand, counter % 16777215)
end

-- Deep copy with circular reference handling (generic)
function Utilities.deepCopy<T>(original: T, seen: {any}?): T
	if type(original) ~= "table" then return original end
	seen = seen or {}
	if seen[original] then return seen[original] end

	local copy = setmetatable({}, getmetatable(original))
	seen[original] = copy

	-- Simpler loop for deep copy, handles arrays implicitly
	for k, v in pairs(original) do
		copy[k] = Utilities.deepCopy(v, seen) -- Recursive call
	end

	return copy :: T -- Assert return type
end

-- Shallow copy (generic)
function Utilities.shallowCopy<T>(original: T): T
	if type(original) ~= "table" then return original end
	local copy = {}
	local mt = getmetatable(original)
	if mt then setmetatable(copy, mt) end
	for k, v in pairs(original) do copy[k] = v end
	return copy :: T -- Assert return type
end

-- Timer function
function Utilities.timer(): () -> number
	local startTime = tick()
	return function(): number
		return tick() - startTime
	end
end

-- Throttle function (generic)
function Utilities.throttle<F>(fn: F, limit: number): F
	if typeof(fn) ~= "function" then error("throttle expects a function", 2) end
	local lastCall = 0
	local queuedThread: thread | nil = nil
	local lastArgs: {any} | nil = nil

	return (function(...)
		local now = tick()
		local args = {...}
		lastArgs = args -- Store latest args

		if now - lastCall >= limit then
			lastCall = now
			if queuedThread then task.cancel(queuedThread); queuedThread = nil end
			-- Call fn directly (type F implies it's callable)
			local success, result = pcall(fn, unpack(args))
			if not success then warn("Error in throttled function:", result) end
			return result -- Return result of immediate call
		elseif not queuedThread then
			local timeToWait = limit - (now - lastCall)
			queuedThread = task.delay(timeToWait, function()
				queuedThread = nil
				lastCall = tick()
				if lastArgs then -- Check if args exist before unpacking
					local success, reason = pcall(fn, unpack(lastArgs))
					if not success then warn("Error in throttled delayed function:", reason) end
					lastArgs = nil -- Clear args after use
				end
			end)
		end
		return nil -- Throttled
	end) :: F -- Assert return type
end

-- Debounce function (generic)
function Utilities.debounce<F>(fn: F, wait: number): F
	if typeof(fn) ~= "function" then error("debounce expects a function", 2) end
	local scheduled: thread | nil = nil
	local argsRef: {any} | nil = nil

	return (function(...)
		argsRef = {...} -- Store latest args
		if scheduled then task.cancel(scheduled) end
		scheduled = task.delay(wait, function()
			scheduled = nil
			if argsRef then -- Check if args exist
				local success, reason = pcall(fn, unpack(argsRef))
				if not success then warn("Error in debounced function:", reason) end
				argsRef = nil -- Clear args after use
			end
		end)
	end) :: F -- Assert return type
end

-- Safe call (generic return)
function Utilities.safeCall<T>(fn: (...any) -> T, ...: any): (boolean, T | any)
	return pcall(fn, ...) -- Pass varargs using '...'
end

-- Memoize function (generic)
function Utilities.memoize<F>(fn: F, keyFn: ((...any) -> string)?, ttl: number?): F
	if typeof(fn) ~= "function" then error("memoize expects a function", 2) end

	local cache: {[string]: { result: {any}, time: number }?} = {}
	local lruManager = Utilities.CreateLRUManager(MAX_MEMOIZE_CACHE_SIZE, function(evictedKey: string)
		cache[evictedKey] = nil
	end)

	return (function(...)
		local args = {...}
		local key: string

		if keyFn then
			local keySuccess, keyResult = pcall(keyFn, unpack(args))
			if not keySuccess or type(keyResult) ~= "string" then
				warn("Memoize keyFn failed or returned non-string:", keyResult)
				key = "__MEMOIZE_KEY_ERROR__" -- Fallback key
			else
				key = keyResult
			end
		else
			-- Default key generation (simplified)
			key = ""
			for i=1, #args do
				key = key .. tostring(args[i]) .. "_"
			end
		end

		local cachedEntry = cache[key]
		if cachedEntry then
			if not ttl or (tick() - cachedEntry.time <= ttl) then
				memoizeCacheHits += 1
				lruManager:touch(key)
				return unpack(cachedEntry.result) -- Return cached result tuple
			else
				-- TTL expired
				cache[key] = nil
				lruManager:remove(key)
			end
		end

		-- Cache miss or expired
		memoizeCacheMisses += 1
		local result = {pcall(fn, unpack(args))} -- Call original function safely and store results in a table

		-- Let's cache regardless of pcall success for now. The result table contains [success, valueOrError]
		cache[key] = {
			result = result,
			time = tick()
		}
		lruManager:add(key)

		-- Check if the pcall succeeded before returning the actual value
		if result[1] then
			return unpack(result, 2) -- Return values from index 2 onwards
		else
			error(result[2]) -- Re-throw error if pcall failed
		end
	end) :: F -- Assert return type
end

-- Check custom __type field (not standard Lua OOP, specific to this library's potential pattern)
function Utilities.isInstanceOf(object: any, className: string): boolean
	return typeof(object) == "table" and rawget(object, "__type") == className
end

-- Format network size
function Utilities.formatNetworkSize(bytes: number): string
	if bytes < 1024 then return stringFormat("%d B", bytes)
	elseif bytes < 1048576 then return stringFormat("%.2f KB", bytes / 1024)
	else return stringFormat("%.2f MB", bytes / 1048576) -- Corrected MB calculation
	end
end

-- Create logger function
function Utilities.createLogger(prefix: string): (message: any, level: string?, context: any?) -> ()
	local formats = {
		INFO = "[NetRay:%s] INFO: %s", WARN = "[NetRay:%s] WARN: %s",
		ERROR = "[NetRay:%s] ERROR: %s", DEBUG = "[NetRay:%s] DEBUG: %s",
	}
	return function(message: any, level: string?, context: any?)
		level = level and string.upper(level) or "INFO" -- Ensure uppercase level
		local msgStr = typeof(message) == "table" and tableConcat(message, " ") or tostring(message)
		if context then
			local ctxStr = typeof(context) == "table" and Utilities.Table.serialize(context) or tostring(context) -- Example: Serialize table context
			msgStr = msgStr .. " " .. ctxStr
		end
		local fmt = formats[level] or formats.INFO
		local formatted = stringFormat(fmt, prefix, msgStr)
		if level == "ERROR" then error(formatted, 0) -- Level 0 to hide logger call itself
		elseif level == "WARN" then warn(formatted)
		else print(formatted) -- Print INFO and DEBUG
		end
	end
end

-- Wait for condition (generic success result)
function Utilities.waitFor<T>(condition: () -> T?, timeout: number?, checkInterval: number?, onProgress: ((elapsed: number, total: number) -> ())?): (boolean, T | string)
	timeout = timeout or 10
	checkInterval = checkInterval or 0.1
	local startTime = tick()
	local endTime = startTime + timeout

	while tick() < endTime do
		local success, result = pcall(condition) -- Call condition safely
		if success and result ~= nil then -- Check for non-nil success result
			return true, result :: T -- Return success and the value
		elseif not success then
			warn("Error in waitFor condition function:", result) -- Log error from condition
		end

		if onProgress then pcall(onProgress, tick() - startTime, timeout) end -- Call progress safely
		task.wait(checkInterval)
	end
	return false, "Timeout waiting for condition" -- Timeout
end

-- Get global memoize stats
function Utilities.getMemoizeStats(): { cacheGroups: number, cacheEntries: number, cacheHits: number, cacheMisses: number, hitRatio: number }
	local totalAttempts = memoizeCacheHits + memoizeCacheMisses
	local hitRatio = if totalAttempts > 0 then memoizeCacheHits / totalAttempts else 0
	return {
		cacheGroups = -1, -- Indicate not tracked globally
		cacheEntries = -1, -- Indicate not tracked globally
		cacheHits = memoizeCacheHits,
		cacheMisses = memoizeCacheMisses,
		hitRatio = hitRatio,
	}
end

-- Clear global memoize stats
function Utilities.clearMemoizeCache(fnId: string?) -- fnId has no effect now
	-- Only resets global counters, doesn't clear individual memoize caches
	memoizeCacheHits = 0
	memoizeCacheMisses = 0
end

-- Table utilities
Utilities.Table = {
	getPath = function<T>(tbl: any, path: {string | number}, default: T?): T?
		if type(tbl) ~= "table" then return default end
		local current = tbl
		for i=1, #path do
			local key = path[i]
			if typeof(current) ~= "table" then return default end -- Check intermediate step
			current = current[key]
			if current == nil then return default end
		end
		return current :: T? -- Assert return type
	end,
	setPath = function(tbl: any, path: {string | number}, value: any)
		if type(tbl) ~= "table" or #path == 0 then return end
		local current = tbl
		for i = 1, #path - 1 do
			local key = path[i]
			if typeof(current[key]) ~= "table" then current[key] = {} end -- Create table if needed
			current = current[key]
		end
		current[path[#path]] = value
	end,
	isEmpty = function(tbl: any): boolean
		return type(tbl) == "table" and next(tbl) == nil
	end,
	-- Add serialize function if needed by logger context
	serialize = function(tbl: table): string
		local parts = {}
		for k, v in pairs(tbl) do
			tableInsert(parts, tostring(k)..":"..tostring(v))
		end
		return "{" .. tableConcat(parts, ",") .. "}"
	end
}

-- Create LRU Manager instance
function Utilities.CreateLRUManager(maxSize: number, onEvict: (key: string) -> ()): LRUManager
	maxSize = mathMax(1, maxSize) -- Ensure maxSize is at least 1
	local lruList = { first = nil, last = nil, size = 0 } -- Doubly-linked list: {key, prev, next}
	local lruMap: {[string]: {key: string, prev: any?, next: any?}?} = {} -- Maps key -> node
	local manager = {} -- The object to return

	-- Define node type internally for clarity
	type LRUNode = {key: string, prev: LRUNode?, next: LRUNode?}

	-- Internal Helper Functions (defined locally, using explicit types)
	local removeNodeHelper: (node: LRUNode?) -> string?
	local removeLastHelper: () -> string?
	local moveToFrontHelper: (node: LRUNode?) -> ()
	local addNodeHelper: (key: string) -> ()

	removeNodeHelper = function(node: LRUNode?): string?
		if not node then return nil end
		local key = node.key
		if not lruMap[key] then return nil end -- Already removed?

		lruMap[key] = nil -- Remove from map first
		lruList.size -= 1

		local prevNode = node.prev
		local nextNode = node.next

		if node == lruList.first then lruList.first = nextNode end
		if node == lruList.last then lruList.last = prevNode end

		if prevNode then prevNode.next = nextNode end
		if nextNode then nextNode.prev = prevNode end

		return key
	end

	removeLastHelper = function(): string?
		return removeNodeHelper(lruList.last)
	end

	moveToFrontHelper = function(node: LRUNode?)
		if not node or node == lruList.first then return end -- No node or already at front

		local prevNode = node.prev
		local nextNode = node.next

		-- Detach node
		if node == lruList.last then
			lruList.last = prevNode
			if prevNode then prevNode.next = nil end
		else
			if prevNode then prevNode.next = nextNode end
			if nextNode then nextNode.prev = prevNode end
		end

		-- Attach to front
		node.prev = nil -- It becomes the new first node
		node.next = lruList.first
		if lruList.first then lruList.first.prev = node end
		lruList.first = node
		-- If list was empty/became empty, last should also be this node
		if not lruList.last then lruList.last = node end
	end

	addNodeHelper = function(key: string)
		if lruMap[key] then return end -- Key already exists, do nothing (or maybe move to front?)

		local node: LRUNode = { key = key, prev = nil, next = nil }
		lruMap[key] = node
		lruList.size += 1

		-- Insert at front
		node.next = lruList.first
		if lruList.first then
			lruList.first.prev = node
		end
		lruList.first = node
		-- If list was empty, it's also the last node
		if not lruList.last then
			lruList.last = node
		end

		-- Check eviction
		if lruList.size > maxSize then
			local evictedKey = removeLastHelper()
			if evictedKey and onEvict then
				-- Use pcall for safety as onEvict is user code
				local success, err = pcall(onEvict, evictedKey)
				if not success then warn("Error during LRU onEvict callback:", err) end
			end
		end
	end

	-- Public Methods (typed) assigned to manager table
	function manager:add(key: string) addNodeHelper(key) end
	function manager:touch(key: string) moveToFrontHelper(lruMap[key]) end -- Pass node to helper
	function manager:remove(key: string) removeNodeHelper(lruMap[key]) end -- Pass node to helper
	function manager:getSize(): number return lruList.size end

	return manager :: LRUManager -- Assert return type
end

return Utilities :: UtilitiesModule


--!optimize 2
--!native

-- BinaryEncoder.lua (Revised for LZ4 Compatibility, Cyclic References, Pack Formats, Recursion Fixes, and Integer Check)
-- Author: Asta (@TheYusufGamer)
-- Version: v1.5.4 (Fixed instance handling)

local BinaryEncoder = {}

-- Binary type identifiers
local TYPE = {
	NIL = 0, BOOLEAN_FALSE = 1, BOOLEAN_TRUE = 2, NUMBER_INT8 = 3, NUMBER_INT16 = 4,
	NUMBER_INT32 = 5, NUMBER_FLOAT32 = 6, STRING_SHORT = 7, STRING_LONG = 8, TABLE_START = 9,
	TABLE_END = 10, ARRAY_START = 11, ARRAY_END = 12, REFERENCE = 13, VECTOR3 = 14,
	COLOR3 = 15, UDIM2 = 16, CFRAME = 17, VECTOR2 = 18, RECT = 19, ENUM = 20,
	INSTANCE_REF = 21, DATETIME = 22, BRICKCOLOR = 23, NUMBERSEQUENCE = 24, COLORSEQUENCE = 25,
	BUFFER = 26, ARRAY_HOMOGENEOUS = 27, VALUE_TYPE_ID = 28, NUMBER_ZERO = 29, STRING_EMPTY = 30,
}

-- HOMOGENEOUS_TYPE_MAP
local HOMOGENEOUS_TYPE_MAP = {
	["number"] = TYPE.NUMBER_FLOAT32, ["string"] = TYPE.STRING_SHORT, ["Vector3"] = TYPE.VECTOR3,
	["Vector2"] = TYPE.VECTOR2, ["Color3"] = TYPE.COLOR3, ["boolean"] = TYPE.BOOLEAN_FALSE,
	["CFrame"] = TYPE.CFRAME, ["UDim2"] = TYPE.UDIM2, ["Rect"] = TYPE.RECT, ["EnumItem"] = TYPE.ENUM,
	["BrickColor"] = TYPE.BRICKCOLOR, ["DateTime"] = TYPE.DATETIME, ["buffer"] = TYPE.BUFFER,
}

-- Roblox Globals Cache
local Vector3_new = Vector3.new; local Vector2_new = Vector2.new; local Color3_new = Color3.new
local UDim2_new = UDim2.new; local Rect_new = Rect.new; local CFrame_new = CFrame.new
local Enum_Type = Enum; local DateTime_FromMillis = DateTime.fromUnixTimestampMillis
local BrickColor_new = BrickColor.new; local NumberSequence_new = NumberSequence.new
local ColorSequence_new = ColorSequence.new; local NumberSequenceKeypoint_new = NumberSequenceKeypoint.new
local ColorSequenceKeypoint_new = ColorSequenceKeypoint.new; local buffer_len = buffer.len
local buffer_tostring = buffer.tostring; local buffer_fromstring = buffer.fromstring
local string_pack = string.pack; local string_unpack = string.unpack; local string_sub = string.sub
local string_byte = string.byte; local string_char = string.char; local table_insert = table.insert
local table_concat = table.concat; local pairs = pairs; local ipairs = ipairs; local type = type
local typeof = typeof; local math_floor = math.floor; local math_max = math.max
local math_abs = math.abs; local pcall = pcall; local warn = warn; local error = error
local tostring = tostring

-- Reuse string characters Cache
local CHAR_CACHE = {}
for i = 0, 255 do CHAR_CACHE[i] = string_char(i) end

-- Recursion depth limit
local MAX_DEPTH = 500

-- Error formatting helper
local function encodingError(msg: string, currentDepth: number): nil error(("[BE Enc Err d%d] %s"):format(currentDepth, msg), 3) return nil end
local function decodingError(msg: string, index: number): nil error(("[BE Dec Err @%d] %s"):format(index, msg), 3) return nil end

--[[ Helper: isInteger ]]
local function isInteger(value: number): boolean return math_abs(value - math_floor(value)) < 0.00001 end

-- Helper to write/read length
local function writeLength(len: number): string if len < 255 then return CHAR_CACHE[len] else return CHAR_CACHE[255] .. string_pack(">I4", len) end end
local function readLength(data: string, index: number): (number?, number?) local b=string_byte(data,index); if not b then return decodingError("readLen byte",index) end; index+=1; if b<255 then return b,index else if index+3>#data then return decodingError("readLen data",index) end; local l,ni=string_unpack(">I4",data,index); if not l then return decodingError("readLen unpack",index) end; return l,ni end end

--[[ Helper: Get Instance Path ]]
local function getInstancePath(instance: Instance): string?
	if not instance then return nil end -- Handle nil input gracefully

	local names = {}
	local current: Instance? = instance
	local gameService = game -- Cache the global

	-- Walk up the hierarchy until 'game' or nil is reached
	while current and current ~= gameService do
		-- Check for invalid characters in names if necessary, though usually not an issue
		-- if string.find(current.Name, ".", 1, true) then
		--     warn("[BE Enc] Instance name contains '.' which might break pathfinding:", current:GetFullName())
		-- end
		table_insert(names, 1, current.Name) -- Prepend name
		current = current.Parent
	end

	-- If the loop stopped because we hit 'game', the path is valid relative to game
	if current == gameService then
		return table_concat(names, ".")
	else
		-- Instance is not a descendant of game (e.g., parented to nil, CoreGui, etc.)
		-- These often cannot or should not be reliably serialized by path.
		warn(("[BE Enc] Instance %q is not a descendant of game, cannot create serializable path."):format(tostring(instance)))
		return nil
	end
end

--[[ Helper: Get Instance At Path ]]
function GetInstanceAtPath(path)
	local current = game
	for segment in string.gmatch(path, "[^%.]+") do
		current = current:FindFirstChild(segment)
		if not current then
			return nil
		end
	end
	return current
end

--[[--- Internal Encoding Logic ---]]
local function encodeValue(value: any, refs_encoding: {}, buffer: {string}, currentDepth: number)

	if currentDepth > MAX_DEPTH then return encodingError("Max depth", currentDepth) end

	local valueType = typeof(value)
	local rawType = type(value)

	if value == nil then table_insert(buffer, CHAR_CACHE[TYPE.NIL])
	elseif rawType == "boolean" then table_insert(buffer, CHAR_CACHE[value and TYPE.BOOLEAN_TRUE or TYPE.BOOLEAN_FALSE])
	elseif rawType == "number" then
		if value == 0 then table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_ZERO])
		elseif isInteger(value) then
			if value >= -128 and value <= 127 then table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_INT8]); table_insert(buffer, string_pack("<i1", value))
			elseif value >= -32768 and value <= 32767 then table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_INT16]); table_insert(buffer, string_pack("<i2", value))
			elseif value >= -2147483648 and value <= 2147483647 then table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_INT32]); table_insert(buffer, string_pack("<i4", value))
			else warn("[BE Enc] Int >32bit->Float32:", value); table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_FLOAT32]); table_insert(buffer, string_pack("<f", value)) end
		else table_insert(buffer, CHAR_CACHE[TYPE.NUMBER_FLOAT32]); table_insert(buffer, string_pack("<f", value)) end
	elseif rawType == "string" then
		local len = #value; if len == 0 then table_insert(buffer, CHAR_CACHE[TYPE.STRING_EMPTY])
		else local lb=writeLength(len); table_insert(buffer, CHAR_CACHE[#lb==1 and TYPE.STRING_SHORT or TYPE.STRING_LONG]); table_insert(buffer,lb); table_insert(buffer, value) end
	elseif rawType == "table" then
		local ref_id = refs_encoding[value]; if ref_id then table_insert(buffer, CHAR_CACHE[TYPE.REFERENCE]); encodeValue(ref_id, refs_encoding, buffer, currentDepth + 1)
		else
			local current_ref_id = #refs_encoding + 1; refs_encoding[value] = current_ref_id
			local count, max_idx, is_dictionary = 0, 0, false
			for k in pairs(value) do count += 1; if type(k) == "number" and k >= 1 and math_floor(k) == k then max_idx = math_max(max_idx, k) else is_dictionary = true end end
			if not is_dictionary and count > 0 and max_idx > count * 2 and max_idx > 10 then is_dictionary = true end -- Sparse check

			if not is_dictionary and count == max_idx then -- Array
				if count == 0 then table_insert(buffer, CHAR_CACHE[TYPE.ARRAY_START]); table_insert(buffer, CHAR_CACHE[TYPE.ARRAY_END])
				else
					local fV, is_homo, fVTI, fVRT, fVTo = value[1], true, nil, nil, nil
					fVRT = type(fV); fVTo = typeof(fV); local tidfc = HOMOGENEOUS_TYPE_MAP[fVTo]
					if count > 1 and tidfc then
						fVTI = tidfc; local isFBool = fVTo=="boolean"
						for i = 2, count do local cV = value[i]; local cVTo = typeof(cV); if cVTo ~= fVTo and not (isFBool and cVTo == "boolean") then is_homo = false; break end; if fVRT == "number" and type(cV) ~= "number" then is_homo = false; break end end
					else is_homo = false end

					if is_homo and fVTI then -- Homo Array
						table_insert(buffer, CHAR_CACHE[TYPE.ARRAY_HOMOGENEOUS]); local ttw = (fVTI==TYPE.BOOLEAN_TRUE or fVTI==TYPE.BOOLEAN_FALSE) and TYPE.BOOLEAN_FALSE or fVTI
						table_insert(buffer, CHAR_CACHE[ttw]); table_insert(buffer, CHAR_CACHE[TYPE.VALUE_TYPE_ID]); encodeValue(count, refs_encoding, buffer, currentDepth + 1)
						if ttw == TYPE.NUMBER_FLOAT32 then for i=1,count do local n=value[i]; if n==0 then table_insert(buffer,CHAR_CACHE[TYPE.NUMBER_ZERO]) else table_insert(buffer,string_pack("<f",n)) end end
						elseif ttw == TYPE.BOOLEAN_FALSE then for i=1,count do table_insert(buffer,CHAR_CACHE[value[i] and TYPE.BOOLEAN_TRUE or TYPE.BOOLEAN_FALSE]) end
						elseif ttw == TYPE.STRING_SHORT then for i=1, count do encodeValue(value[i], refs_encoding, buffer, currentDepth + 1) end
						else for i=1, count do encodeValue(value[i], refs_encoding, buffer, currentDepth + 1) end end
					else -- Regular Array
						table_insert(buffer, CHAR_CACHE[TYPE.ARRAY_START]); for i=1, count do encodeValue(value[i], refs_encoding, buffer, currentDepth + 1) end; table_insert(buffer, CHAR_CACHE[TYPE.ARRAY_END])
					end
				end
			else -- Dictionary
				table_insert(buffer, CHAR_CACHE[TYPE.TABLE_START])
				for k, v in pairs(value) do local kt=type(k); if kt=="string" or kt=="number" then encodeValue(k, refs_encoding, buffer, currentDepth+1); encodeValue(v, refs_encoding, buffer, currentDepth+1) else warn("[BE Enc] Skip table key:", kt, k) end end
				table_insert(buffer, CHAR_CACHE[TYPE.TABLE_END])
			end
		end
		-- Roblox Types (Standardized pack formats)
	elseif valueType == "Vector3" then table_insert(buffer,CHAR_CACHE[TYPE.VECTOR3]); table_insert(buffer, string_pack("<ddd", value.X, value.Y, value.Z)) -- FIX: <ddd
	elseif valueType == "Vector2" then table_insert(buffer,CHAR_CACHE[TYPE.VECTOR2]); table_insert(buffer, string_pack("<dd", value.X, value.Y))             -- FIX: <dd
	elseif valueType == "Color3" then table_insert(buffer,CHAR_CACHE[TYPE.COLOR3]); table_insert(buffer, string_pack("<ddd", value.R, value.G, value.B))   -- FIX: <ddd
	elseif valueType == "UDim2" then table_insert(buffer,CHAR_CACHE[TYPE.UDIM2]); table_insert(buffer, string_pack("<dddd", value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset)) -- FIX: <dddd
	elseif valueType == "Rect" then table_insert(buffer,CHAR_CACHE[TYPE.RECT]); table_insert(buffer, string_pack("<dddd", value.Min.X, value.Min.Y, value.Max.X, value.Max.Y))       -- FIX: <dddd
	elseif valueType == "CFrame" then table_insert(buffer,CHAR_CACHE[TYPE.CFRAME]); table_insert(buffer, string_pack("<dddddddddddd", value:GetComponents())) -- FIX: <d * 12
	elseif valueType == "EnumItem" then table_insert(buffer,CHAR_CACHE[TYPE.ENUM]); encodeValue(tostring(value.EnumType), refs_encoding, buffer, currentDepth+1); encodeValue(tostring(value.Name), refs_encoding, buffer, currentDepth+1)
	elseif valueType == "Instance" then
		-- Use the robust path function (Unchanged)
		local path = getInstancePath(value)
		if path then
			if path == "" then
				warn("[BE Enc] Instance path resulted in empty string for:", value)
				table_insert(buffer, CHAR_CACHE[TYPE.NIL])
			else
				table_insert(buffer, CHAR_CACHE[TYPE.INSTANCE_REF])
				encodeValue(path, refs_encoding, buffer, currentDepth + 1)
			end
		else
			-- getInstancePath already warned if it couldn't generate a path
			table_insert(buffer, CHAR_CACHE[TYPE.NIL]) -- Encode as nil if path generation failed
		end
	elseif valueType == "DateTime" then table_insert(buffer,CHAR_CACHE[TYPE.DATETIME]); table_insert(buffer, string_pack("<q", value.UnixTimestampMillis))
	elseif valueType == "BrickColor" then table_insert(buffer,CHAR_CACHE[TYPE.BRICKCOLOR]); encodeValue(value.Number, refs_encoding, buffer, currentDepth+1)
	elseif valueType == "NumberSequence" then table_insert(buffer,CHAR_CACHE[TYPE.NUMBERSEQUENCE]); local kp=value.Keypoints; encodeValue(#kp, refs_encoding, buffer, currentDepth+1); for _,k in ipairs(kp) do table_insert(buffer, string_pack("<ddd", k.Time, k.Value, k.Envelope)) end -- Kept <ddd
	elseif valueType == "ColorSequence" then table_insert(buffer,CHAR_CACHE[TYPE.COLORSEQUENCE]); local kp=value.Keypoints; encodeValue(#kp, refs_encoding, buffer, currentDepth+1); for _,k in ipairs(kp) do table_insert(buffer, string_pack("<dddd", k.Time, k.Value.R, k.Value.G, k.Value.B)) end -- Kept <dddd
	elseif valueType == "buffer" then table_insert(buffer, CHAR_CACHE[TYPE.BUFFER]); local l=buffer_len(value); local lb=writeLength(l); table_insert(buffer,lb); table_insert(buffer, buffer_tostring(value))
	else warn("[BE Enc] Unsupported:", valueType, value); table_insert(buffer, CHAR_CACHE[TYPE.NIL])
	end
end

--[[--- Internal Decoding Logic ---]]
local function decodeValue(data: string, indexRef: {number}, refs_decoding: {}, currentDepth: number)

	if currentDepth > MAX_DEPTH then return decodingError("Max depth", indexRef[1]) end
	local index = indexRef[1]; if index > #data then return decodingError("EOF start", index) end
	local typeId = string_byte(data, index); if not typeId then return decodingError("EOF TypeID", index) end
	index += 1; indexRef[1] = index

	if typeId == TYPE.NIL then return nil
	elseif typeId == TYPE.BOOLEAN_FALSE then return false
	elseif typeId == TYPE.BOOLEAN_TRUE then return true
	elseif typeId == TYPE.NUMBER_ZERO then return 0
	elseif typeId == TYPE.STRING_EMPTY then return ""
	elseif typeId == TYPE.NUMBER_INT8 then if index>#data then return decodingError("EOF I8",index) end; local v,idx=string_unpack("<i1",data,index); if not idx then return decodingError("Unpk I8",index) end; indexRef[1]=idx; return v
	elseif typeId == TYPE.NUMBER_INT16 then if index+1>#data then return decodingError("EOF I16",index) end; local v,idx=string_unpack("<i2",data,index); if not idx then return decodingError("Unpk I16",index) end; indexRef[1]=idx; return v
	elseif typeId == TYPE.NUMBER_INT32 then if index+3>#data then return decodingError("EOF I32",index) end; local v,idx=string_unpack("<i4",data,index); if not idx then return decodingError("Unpk I32",index) end; indexRef[1]=idx; return v
	elseif typeId == TYPE.NUMBER_FLOAT32 then if index+3>#data then return decodingError("EOF F32",index) end; local v,idx=string_unpack("<f",data,index); if not idx then return decodingError("Unpk F32",index) end; indexRef[1]=idx; return v
	elseif typeId == TYPE.STRING_SHORT or typeId == TYPE.STRING_LONG then local l,il=readLength(data,indexRef[1]); if not l then return nil end; local eos=il+l; if eos>#data+1 then return decodingError("Str OOB "..l,il) end; local s=string_sub(data,il,eos-1); indexRef[1]=eos; return s
	elseif typeId == TYPE.REFERENCE then local rid=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(rid)~="number" then return decodingError("RefID type "..type(rid), indexRef[1]) end; local v=refs_decoding[rid]; if v==nil then return decodingError("RefID inv "..rid, indexRef[1]) end; return v
	elseif typeId == TYPE.TABLE_START then local tbl={}; local id=#refs_decoding+1; refs_decoding[id]=tbl; while true do if indexRef[1]>#data then return decodingError("EOF TBL", indexRef[1]) end; local p=string_byte(data,indexRef[1]); if not p then return decodingError("Peek TBL", indexRef[1]) end; if p==TYPE.TABLE_END then indexRef[1]+=1; break end; local k=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if indexRef[1]>#data and string_byte(data, indexRef[1])~=TYPE.TABLE_END then return decodingError("EOF TBL Key", indexRef[1]) end; local v=decodeValue(data,indexRef,refs_decoding,currentDepth+1); tbl[k]=v end; return tbl
	elseif typeId == TYPE.ARRAY_START then local arr={}; local id=#refs_decoding+1; refs_decoding[id]=arr; local i=1; while true do if indexRef[1]>#data then return decodingError("EOF ARR", indexRef[1]) end; local p=string_byte(data,indexRef[1]); if not p then return decodingError("Peek ARR", indexRef[1]) end; if p==TYPE.ARRAY_END then indexRef[1]+=1; break end; arr[i]=decodeValue(data,indexRef,refs_decoding,currentDepth+1); i+=1 end; return arr
	elseif typeId == TYPE.ARRAY_HOMOGENEOUS then local arr={}; local id=#refs_decoding+1; refs_decoding[id]=arr; index=indexRef[1]; if index+1>#data then return decodingError("EOF HomoHdr",index) end; local vtid=string_byte(data,index); index+=1; local sep=string_byte(data,index); index+=1; indexRef[1]=index; if sep~=TYPE.VALUE_TYPE_ID then return decodingError("Homo Sep "..sep,index-1) end; local cnt=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(cnt)~="number" or cnt<0 or cnt~=math_floor(cnt) then return decodingError("Homo Cnt "..tostring(cnt), indexRef[1]) end
		if cnt > 0 then index=indexRef[1]; local isBool=vtid==TYPE.BOOLEAN_FALSE or vtid==TYPE.BOOLEAN_TRUE; local evtid=isBool and TYPE.BOOLEAN_FALSE or vtid;
			if evtid == TYPE.NUMBER_FLOAT32 then for i=1,cnt do if index>#data then return decodingError(("HomoNum EOF %d/%d"):format(i,cnt),index) end; local p=string_byte(data,index); if p==TYPE.NUMBER_ZERO then arr[i]=0; index+=1 else if index+3>#data then return decodingError(("HomoFlt EOF %d/%d"):format(i,cnt),index) end; local v,ni=string_unpack("<f",data,index); if not ni then return decodingError(("HomoFlt Unpk %d/%d"):format(i,cnt),index) end; arr[i]=v; index=ni end end
			elseif isBool then if index+cnt>#data+1 then return decodingError(("HomoBool EOF %d/%d"):format(cnt,cnt),index) end; for i=1,cnt do local b=string_byte(data,index); arr[i]=(b==TYPE.BOOLEAN_TRUE); index+=1 end
			elseif evtid == TYPE.STRING_SHORT then for i=1,cnt do indexRef[1]=index; local s=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(s)~="string" then return decodingError(("HomoStr Type %d/%d got %s"):format(i,cnt,type(s)),index) end; arr[i]=s; index=indexRef[1] end
			else for i=1, cnt do indexRef[1]=index; arr[i]=decodeValue(data,indexRef,refs_decoding,currentDepth+1); index=indexRef[1] end
			end; indexRef[1]=index
		end; return arr
		-- Roblox Types (Standardized unpack formats)
	elseif typeId == TYPE.VECTOR3 then if indexRef[1]+23>#data then return decodingError("EOF VEC3",indexRef[1]) end; local x,y,z,idx=string_unpack("<ddd",data,indexRef[1]); if not idx then return decodingError("Unpk VEC3",indexRef[1]) end; indexRef[1]=idx; return Vector3_new(x,y,z) -- FIX: <ddd
	elseif typeId == TYPE.VECTOR2 then if indexRef[1]+15>#data then return decodingError("EOF VEC2",indexRef[1]) end; local x,y,idx=string_unpack("<dd",data,indexRef[1]); if not idx then return decodingError("Unpk VEC2",indexRef[1]) end; indexRef[1]=idx; return Vector2_new(x,y)     -- FIX: <dd
	elseif typeId == TYPE.COLOR3 then if indexRef[1]+23>#data then return decodingError("EOF COL3",indexRef[1]) end; local r,g,b,idx=string_unpack("<ddd",data,indexRef[1]); if not idx then return decodingError("Unpk COL3",indexRef[1]) end; indexRef[1]=idx; return Color3_new(r,g,b)    -- FIX: <ddd
	elseif typeId == TYPE.UDIM2 then if indexRef[1]+31>#data then return decodingError("EOF UDIM2",indexRef[1]) end; local xs,xo,ys,yo,idx=string_unpack("<dddd",data,indexRef[1]); if not idx then return decodingError("Unpk UDIM2",indexRef[1]) end; indexRef[1]=idx; return UDim2_new(xs,xo,ys,yo) -- FIX: <dddd
	elseif typeId == TYPE.RECT then if indexRef[1]+31>#data then return decodingError("EOF RECT",indexRef[1]) end; local nx,ny,xx,xy,idx=string_unpack("<dddd",data,indexRef[1]); if not idx then return decodingError("Unpk RECT",indexRef[1]) end; indexRef[1]=idx; return Rect_new(Vector2_new(nx,ny),Vector2_new(xx,xy)) -- FIX: <dddd
	elseif typeId == TYPE.CFRAME then if indexRef[1]+95>#data then return decodingError("EOF CFRAME",indexRef[1]) end; local c={string_unpack("<dddddddddddd",data,indexRef[1])}; local idx=table.remove(c); if not idx then return decodingError("Unpk CFRAME",indexRef[1]) end; indexRef[1]=idx; return CFrame_new(table.unpack(c,1,12)) -- FIX: <d * 12
	elseif typeId == TYPE.ENUM then local etn=decodeValue(data,indexRef,refs_decoding,currentDepth+1); local evn=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(etn)~="string" or type(evn)~="string" then return decodingError("Enum str",indexRef[1]) end; local ok,ei=pcall(function()return Enum_Type[etn][evn]end); if ok and ei then return ei else warn("[BE Dec] Enum fail:",etn,evn); return nil end
	elseif typeId == TYPE.INSTANCE_REF then
		local path = decodeValue(data, indexRef, refs_decoding, currentDepth + 1)
		print(path)
		if type(path) ~= "string" then
			return decodingError("Instance reference path must be a string, got " .. type(path), indexRef[1])
		end
		if path == "" then
			warn("[BE Dec] Decoded empty instance path.")
			return nil -- Treat empty path as nil
		end

		print(GetInstanceAtPath(path))
		local instance = GetInstanceAtPath(path)

		if instance then
			-- Found the instance successfully
			return instance
		else
			-- Instance not found (FindFirstChild returned nil)
			warn(("[BE Dec] Instance not found via path %q. (Possible reasons: Not replicated yet, FilteringEnabled issue, destroyed, invalid path)."):format(path))
			return nil -- Return nil as the instance couldn't be resolved
		end
	elseif typeId == TYPE.DATETIME then if indexRef[1]+7>#data then return decodingError("EOF DT",indexRef[1]) end; local ms,idx=string_unpack("<q",data,indexRef[1]); if not idx then return decodingError("Unpk DT",indexRef[1]) end; indexRef[1]=idx; local ok,dt=pcall(DateTime_FromMillis,ms); if ok then return dt else warn("[BE Dec] DT fail:",ms); return nil end
	elseif typeId == TYPE.BRICKCOLOR then local num=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(num)~="number" then return decodingError("BC num",indexRef[1]) end; local ok,bc=pcall(BrickColor_new,num); if ok then return bc else warn("[BE Dec] BC fail:",num); return nil end
	elseif typeId == TYPE.NUMBERSEQUENCE then local nk=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(nk)~="number" or nk<0 or nk>20 then warn("[BE Dec] NS cnt clamp:",nk); nk=math.max(0,math.min(20,nk)) elseif nk~=math.floor(nk) then return decodingError("NS cnt int",indexRef[1]) end; local kps={}; local idx=indexRef[1]; local bn=nk*24; if idx+bn>#data+1 then return decodingError(("NS EOF %d need %d"):format(nk,bn),idx) end
		for i=1,nk do local t,v,e,ni=string_unpack("<ddd",data,idx); if not ni then return decodingError(("NS Unpk %d/%d"):format(i,nk),idx) end; idx=ni; local ok,kp=pcall(NumberSequenceKeypoint_new,t,v,e); if ok then kps[i]=kp else warn("[BE Dec] NS Key fail:",t,v,e) end end; indexRef[1]=idx; local ok,ns=pcall(NumberSequence_new,kps); if ok then return ns else warn("[BE Dec] NS New fail"); return nil end -- Kept <ddd
	elseif typeId == TYPE.COLORSEQUENCE then local nk=decodeValue(data,indexRef,refs_decoding,currentDepth+1); if type(nk)~="number" or nk<0 or nk>20 then warn("[BE Dec] CS cnt clamp:",nk); nk=math.max(0,math.min(20,nk)) elseif nk~=math.floor(nk) then return decodingError("CS cnt int",indexRef[1]) end; local kps={}; local idx=indexRef[1]; local bn=nk*32; if idx+bn>#data+1 then return decodingError(("CS EOF %d need %d"):format(nk,bn),idx) end
		for i=1,nk do local t,r,g,b,ni=string_unpack("<dddd",data,idx); if not ni then return decodingError(("CS Unpk %d/%d"):format(i,nk),idx) end; idx=ni; local co,cv=pcall(Color3_new,r,g,b); if co then local ok,kp=pcall(ColorSequenceKeypoint_new,t,cv); if ok then kps[i]=kp else warn("[BE Dec] CS Key fail:",t,cv) end else warn("[BE Dec] CS Col3 fail:",r,g,b) end end; indexRef[1]=idx; local ok,cs=pcall(ColorSequence_new,kps); if ok then return cs else warn("[BE Dec] CS New fail"); return nil end -- Kept <dddd
	elseif typeId == TYPE.BUFFER then local l,il=readLength(data,indexRef[1]); if not l then return nil end; local eob=il+l; if eob>#data+1 then return decodingError("Buf OOB "..l,il) end; local bd=string_sub(data,il,eob-1); indexRef[1]=eob; local ok,buf=pcall(buffer_fromstring,bd); if ok then return buf else warn("[BE Dec] Buf fail"); return nil end
	else return decodingError("Unknown TypeID: " .. typeId, indexRef[1] - 1)
	end
end

--[[--- Public Interface ---]]
function BinaryEncoder.encode(value: any): (string?)
	local buffer = {}
	local refs_encoding = {}
	local ok, err = pcall(encodeValue, value, refs_encoding, buffer, 0)
	if not ok then
		-- Rethrow the error, ensuring it's captured by any outer pcall (like the one around LZ4)
		error("[BinaryEncoder.encode error] " .. tostring(err), 0) -- Use level 0 to preserve stack trace from encodeValue
		-- return nil -- Alternative: return nil instead of erroring
	end
	return table_concat(buffer)
end

function BinaryEncoder.decode(data: string): any
	if type(data) ~= "string" then error("BE decode requires string, got "..type(data), 2) end
	if #data == 0 then error("BE decode requires non-empty string", 2) end
	local refs_decoding = {}
	local indexRef = {1}
	-- Wrap decodeValue in pcall to catch internal errors more gracefully if needed,
	-- though decodeValue already uses decodingError which halts execution.
	local result = decodeValue(data, indexRef, refs_decoding, 0)
	if indexRef[1] <= #data then
		warn(("[BE Dec Warning] Extra %d bytes left. End: %d, Total: %d"):format(#data - indexRef[1] + 1, indexRef[1], #data))
	end
	return result
end

return BinaryEncoder