-- Vectis Code Studio Connector (Production)
local HttpService = game:GetService("HttpService")
local InsertService = game:GetService("InsertService")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local LogService = game:GetService("LogService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local StudioCaptureService = game:GetService("StudioCaptureService")

-- Production API by default. Developers can override via plugin:SetSetting("VectisApiUrl", "https://api.vectiscode.com")
local API_BASE = (plugin:GetSetting("VectisApiUrl") or "https://api.vectiscode.com"):gsub("/+$", "")
local PLUGIN_VERSION = "vectis-connector-1.17.0"
local TOOLBAR_ICON = plugin:GetSetting("VectisIconAssetId") or "rbxassetid://4458901886"
if typeof(TOOLBAR_ICON) ~= "string" or TOOLBAR_ICON == "" then
	TOOLBAR_ICON = "rbxassetid://4458901886"
end
local MAX_FULL_NODES = 5000
local MAX_DELTA_NODES = 750
local MAX_SOURCE_CHARS = 60000
local SNAPSHOT_CHUNK_MAX_NODES = 500
local SNAPSHOT_CHUNK_MAX_CHARS = 1200000
local SOURCE_TRUNCATION_MARKER = "\n-- [Vectis truncated this script for sync stability.]"
local CONNECTED_POLL_INTERVAL_SECONDS = 3
local IDLE_POLL_INTERVAL_SECONDS = 5

local COLOR_BG = Color3.fromRGB(253, 246, 227)
local COLOR_PANEL = Color3.fromRGB(255, 248, 230)
local COLOR_SOFT = Color3.fromRGB(250, 237, 209)
local COLOR_BORDER = Color3.fromRGB(223, 207, 172)
local COLOR_TEXT = Color3.fromRGB(63, 53, 37)
local COLOR_MUTED = Color3.fromRGB(108, 95, 73)
local COLOR_ACCENT = Color3.fromRGB(201, 120, 34)
local COLOR_SUCCESS = Color3.fromRGB(23, 122, 74)
local COLOR_DANGER = Color3.fromRGB(180, 40, 40)

local function settingString(key)
	local value = plugin:GetSetting(key)
	if typeof(value) ~= "string" then return "" end
	return value:gsub("^%s*(.-)%s*$", "%1")
end

local function formatPairingCode(code)
	local compact = tostring(code or ""):gsub("[^%w]", ""):upper():sub(1, 12)
	if #compact == 0 then
		return ""
	end
	local parts = {}
	for i = 1, #compact, 4 do
		table.insert(parts, compact:sub(i, i + 3))
	end
	return table.concat(parts, "-")
end

local function setButtonStyle(button, background, foreground)
	button.BackgroundColor3 = background
	button.TextColor3 = foreground
	button.Font = Enum.Font.GothamBold
	button.BorderSizePixel = 0
end

local function fitSourceForSync(source)
	if #source <= MAX_SOURCE_CHARS then
		return source
	end
	return string.sub(source, 1, MAX_SOURCE_CHARS - #SOURCE_TRUNCATION_MARKER) .. SOURCE_TRUNCATION_MARKER
end

local function readSource(inst)
	local ok, source = pcall(function() return inst.Source end)
	if ok and typeof(source) == "string" then
		return fitSourceForSync(source)
	end
	return nil
end

local function writeSource(inst, source)
	local updated = pcall(function()
		ScriptEditorService:UpdateSourceAsync(inst, function()
			return source
		end)
	end)
	if updated then return end
	inst.Source = source
end

local function beginHistoryRecording(identifier, displayName)
	local ok, recording = pcall(function()
		return ChangeHistoryService:TryBeginRecording(identifier, displayName)
	end)
	if ok and recording then
		return recording
	end
	pcall(function() ChangeHistoryService:SetWaypoint(displayName) end)
	return nil
end

local function finishHistoryRecording(recording, operation, fallbackLabel)
	if recording then
		local ok = pcall(function()
			ChangeHistoryService:FinishRecording(recording, operation)
		end)
		if ok then return end
	end
	pcall(function() ChangeHistoryService:SetWaypoint(fallbackLabel) end)
end

local function estimateNodeChars(node)
	local ok, encoded = pcall(function()
		return HttpService:JSONEncode(node)
	end)
	if ok and typeof(encoded) == "string" then
		return #encoded
	end
	return 1024
end

local function buildSnapshotChunks(nodes)
	local chunks = {}
	local current = {}
	local currentChars = 0
	local function flush()
		if #current > 0 then
			table.insert(chunks, current)
			current = {}
			currentChars = 0
		end
	end
	for _, node in ipairs(nodes) do
		local nodeChars = estimateNodeChars(node)
		if #current > 0 and (#current >= SNAPSHOT_CHUNK_MAX_NODES or currentChars + nodeChars > SNAPSHOT_CHUNK_MAX_CHARS) then
			flush()
		end
		table.insert(current, node)
		currentChars = currentChars + nodeChars
	end
	flush()
	return chunks
end

local function snapshotUploadId()
	return tostring(os.time()) .. "_" .. tostring(math.random(100000, 999999))
end

local ROOT_SERVICE_NAMES = {
	Workspace = true,
	ReplicatedStorage = true,
	ServerScriptService = true,
	ServerStorage = true,
	StarterGui = true,
	StarterPlayer = true,
	StarterPack = true
}
local ROOT_SERVICE_ORDER = {
	"ServerScriptService",
	"ReplicatedStorage",
	"StarterPlayer",
	"StarterGui",
	"ServerStorage",
	"StarterPack",
	"Workspace"
}

local function toVectisPath(inst)
	local parts = {}
	local current = inst
	while current and current ~= game do
		table.insert(parts, 1, current.Name)
		current = current.Parent
	end
	if #parts == 0 or not ROOT_SERVICE_NAMES[parts[1]] then
		return nil
	end
	return table.concat(parts, "/")
end

local function splitVectisPath(path)
	if typeof(path) ~= "string" then
		return {}
	end
	if string.find(path, "/", 1, true) then
		return string.split(path, "/")
	end
	return string.split(path, ".")
end

local toolbar = plugin:CreateToolbar("vectiscode")
local openButton = toolbar:CreateButton("vectiscode", "Open vectiscode Studio Bridge", TOOLBAR_ICON)

local widgetInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 350, 440, 320, 340)
local widget = plugin:CreateDockWidgetPluginGui("VectisCodeConnector", widgetInfo)
widget.Title = "vectiscode"

local root = Instance.new("Frame")
root.Size = UDim2.fromScale(1, 1)
root.BackgroundColor3 = COLOR_BG
root.Parent = widget

local layout = Instance.new("UIListLayout")
layout.Padding = UDim.new(0, 12)
layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
layout.VerticalAlignment = Enum.VerticalAlignment.Top
layout.SortOrder = Enum.SortOrder.LayoutOrder
layout.Parent = root

local rootPadding = Instance.new("UIPadding")
rootPadding.PaddingTop = UDim.new(0, 16)
rootPadding.PaddingBottom = UDim.new(0, 16)
rootPadding.PaddingLeft = UDim.new(0, 18)
rootPadding.PaddingRight = UDim.new(0, 18)
rootPadding.Parent = root

local statusLabel = Instance.new("TextLabel")
statusLabel.Size = UDim2.new(1, 0, 0, 34)
statusLabel.BackgroundColor3 = COLOR_SOFT
statusLabel.Font = Enum.Font.GothamBold
statusLabel.TextSize = 12
statusLabel.Text = "DISCONNECTED"
statusLabel.TextColor3 = COLOR_TEXT
statusLabel.BorderSizePixel = 0
statusLabel.LayoutOrder = 2
statusLabel.Parent = root
Instance.new("UICorner").Parent = statusLabel
local statusStroke = Instance.new("UIStroke")
statusStroke.Color = COLOR_BORDER
statusStroke.Parent = statusLabel

local sessionInfo = Instance.new("TextLabel")
sessionInfo.Size = UDim2.new(1, 0, 0, 32)
sessionInfo.BackgroundTransparency = 1
sessionInfo.Font = Enum.Font.Gotham
sessionInfo.TextSize = 11
sessionInfo.Text = "No active Vectis ID"
sessionInfo.TextColor3 = COLOR_MUTED
sessionInfo.TextWrapped = true
sessionInfo.LayoutOrder = 3
sessionInfo.Parent = root

local pairingContainer = Instance.new("Frame")
pairingContainer.Size = UDim2.new(1, 0, 0, 156)
pairingContainer.BackgroundTransparency = 1
pairingContainer.LayoutOrder = 1
pairingContainer.Parent = root

local pairingLayout = Instance.new("UIListLayout")
pairingLayout.Padding = UDim.new(0, 10)
pairingLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
pairingLayout.Parent = pairingContainer

local codeLabel = Instance.new("TextLabel")
codeLabel.Size = UDim2.new(1, 0, 0, 16)
codeLabel.BackgroundTransparency = 1
codeLabel.Font = Enum.Font.GothamBold
codeLabel.TextSize = 11
codeLabel.Text = "PAIRING CODE"
codeLabel.TextColor3 = COLOR_MUTED
codeLabel.TextXAlignment = Enum.TextXAlignment.Left
codeLabel.Parent = pairingContainer

local codeDisplay = Instance.new("TextBox")
codeDisplay.Size = UDim2.new(1, 0, 0, 56)
codeDisplay.BackgroundColor3 = COLOR_PANEL
codeDisplay.Font = Enum.Font.GothamBold
codeDisplay.TextSize = 23
codeDisplay.TextScaled = true
codeDisplay.Text = ""
codeDisplay.PlaceholderText = "---- ---- ----"
codeDisplay.TextEditable = false
codeDisplay.ClearTextOnFocus = false
codeDisplay.TextColor3 = COLOR_TEXT
codeDisplay.PlaceholderColor3 = COLOR_MUTED
codeDisplay.BorderSizePixel = 0
codeDisplay.Parent = pairingContainer
Instance.new("UICorner").Parent = codeDisplay
local codePadding = Instance.new("UIPadding")
codePadding.PaddingLeft = UDim.new(0, 10)
codePadding.PaddingRight = UDim.new(0, 10)
codePadding.Parent = codeDisplay
local codeSizeConstraint = Instance.new("UITextSizeConstraint")
codeSizeConstraint.MaxTextSize = 23
codeSizeConstraint.MinTextSize = 14
codeSizeConstraint.Parent = codeDisplay
local codeStroke = Instance.new("UIStroke")
codeStroke.Color = COLOR_BORDER
codeStroke.Parent = codeDisplay

local refreshButton = Instance.new("TextButton")
refreshButton.Size = UDim2.new(1, 0, 0, 38)
refreshButton.TextSize = 14
refreshButton.Text = "GENERATE PAIRING CODE"
refreshButton.Parent = pairingContainer
setButtonStyle(refreshButton, COLOR_ACCENT, COLOR_PANEL)
Instance.new("UICorner").Parent = refreshButton

local connectedContainer = Instance.new("Frame")
connectedContainer.Size = UDim2.new(1, 0, 0, 44)
connectedContainer.BackgroundTransparency = 1
connectedContainer.Visible = false
connectedContainer.LayoutOrder = 4
connectedContainer.Parent = root

local connectedLayout = Instance.new("UIListLayout")
connectedLayout.Padding = UDim.new(0, 10)
connectedLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
connectedLayout.Parent = connectedContainer

local function visualQaEnabled()
	return true
end

local unlinkButton = Instance.new("TextButton")
unlinkButton.Size = UDim2.new(1, 0, 0, 34)
unlinkButton.TextSize = 12
unlinkButton.Text = "UNLINK STUDIO"
unlinkButton.Parent = connectedContainer
setButtonStyle(unlinkButton, COLOR_DANGER, COLOR_PANEL)
Instance.new("UICorner").Parent = unlinkButton

local footer = Instance.new("Frame")
footer.Size = UDim2.new(1, 0, 0, 52)
footer.BackgroundTransparency = 1
footer.LayoutOrder = 20
footer.Parent = root

local footerLayout = Instance.new("UIListLayout")
footerLayout.Padding = UDim.new(0, 2)
footerLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
footerLayout.VerticalAlignment = Enum.VerticalAlignment.Center
footerLayout.Parent = footer

local footerTitle = Instance.new("TextLabel")
footerTitle.Size = UDim2.new(1, 0, 0, 22)
footerTitle.BackgroundTransparency = 1
footerTitle.Font = Enum.Font.GothamBold
footerTitle.TextSize = 16
footerTitle.Text = "vectiscode"
footerTitle.TextColor3 = COLOR_TEXT
footerTitle.Parent = footer

local footerSubtitle = Instance.new("TextLabel")
footerSubtitle.Size = UDim2.new(1, 0, 0, 18)
footerSubtitle.BackgroundTransparency = 1
footerSubtitle.Font = Enum.Font.Gotham
footerSubtitle.TextSize = 11
footerSubtitle.Text = "Studio bridge"
footerSubtitle.TextColor3 = COLOR_MUTED
footerSubtitle.Parent = footer

local function describeRequestProblem(err)
	local message = tostring(err or "Network unavailable")
	local lower = string.lower(message)
	if string.find(lower, "http requests can only be executed", 1, true) or string.find(lower, "http requests are not enabled", 1, true) then
		return "Enable HTTP Requests in Game Settings > Security, then generate a new code."
	end
	if string.find(lower, "timedout", 1, true) or string.find(lower, "timeout", 1, true) then
		return "Connection timed out. Check your internet, then try again."
	end
	if string.find(lower, "dns", 1, true) or string.find(lower, "name resolution", 1, true) then
		return "Could not reach api.vectiscode.com. Check your connection."
	end
	return message
end

local function apiRequest(method, path, body)
	local url = API_BASE .. path
	local headers = { ["Content-Type"] = "application/json" }
	local connectorToken = settingString("VectisToken")
	if connectorToken ~= "" then
		headers["X-Vectis-Connector-Token"] = connectorToken
	end
	local options = {
		Url = url,
		Method = method,
		Headers = headers
	}
	if body then options.Body = HttpService:JSONEncode(body) end

	local success, response = pcall(function() return HttpService:RequestAsync(options) end)
	if not success then return nil, tostring(response) end
	if not response.Success then return nil, "HTTP " .. tostring(response.StatusCode), response.StatusCode end

	local decoded = nil
	pcall(function() decoded = HttpService:JSONDecode(response.Body) end)
	
	if decoded and decoded.session and decoded.session.connectorToken then
		plugin:SetSetting("VectisToken", decoded.session.connectorToken)
	end
	
	return decoded, nil, response.StatusCode
end

local function apiRawRequest(method, path, contentType, rawBody, extraHeaders)
	local headers = { ["Content-Type"] = contentType }
	local connectorToken = settingString("VectisToken")
	if connectorToken ~= "" then
		headers["X-Vectis-Connector-Token"] = connectorToken
	end
	for key, value in pairs(extraHeaders or {}) do
		headers[key] = value
	end
	local success, response = pcall(function()
		return HttpService:RequestAsync({
			Url = API_BASE .. path,
			Method = method,
			Headers = headers,
			Body = rawBody
		})
	end)
	if not success then return nil, tostring(response) end
	if not response.Success then return nil, "HTTP " .. tostring(response.StatusCode), response.StatusCode end
	local decoded = nil
	pcall(function() decoded = HttpService:JSONDecode(response.Body) end)
	return decoded, nil, response.StatusCode
end

local pendingStudioLogs = {}
local MAX_PENDING_STUDIO_LOGS = 40
local studioLogFlushLock = false

local function queueStudioLog(message, messageType)
	local level = "info"
	if messageType == Enum.MessageType.MessageWarning then
		level = "warn"
	elseif messageType == Enum.MessageType.MessageError then
		level = "error"
	else
		return
	end
	table.insert(pendingStudioLogs, {
		level = level,
		message = tostring(message):sub(1, 2000)
	})
	while #pendingStudioLogs > MAX_PENDING_STUDIO_LOGS do
		table.remove(pendingStudioLogs, 1)
	end
end

local function flushStudioLogs()
	if studioLogFlushLock then return end
	local sessionId = settingString("VectisSessionId")
	local token = settingString("VectisToken")
	if sessionId == "" or token == "" then return end
	if #pendingStudioLogs == 0 then return end
	
	studioLogFlushLock = true
	local batch = {}
	local count = math.min(10, #pendingStudioLogs)
	for i = 1, count do
		table.insert(batch, {
			sessionId = sessionId,
			connectorToken = token,
			level = pendingStudioLogs[i].level,
			message = pendingStudioLogs[i].message
		})
	end
	
	local result = apiRequest("POST", "/studio/logs", batch)
	if result then
		for _ = 1, count do
			table.remove(pendingStudioLogs, 1)
		end
	end
	studioLogFlushLock = false
end

LogService.MessageOut:Connect(queueStudioLog)

local function postSnapshot(sessionId, token, mode, nodes)
	local chunks = buildSnapshotChunks(nodes)
	if #chunks <= 1 then
		return apiRequest("POST", "/studio/snapshot", {
			sessionId = sessionId,
			connectorToken = token,
			mode = mode,
			nodes = nodes
		})
	end

	local uploadId = snapshotUploadId()
	local finalResult, finalErr, finalStatusCode = nil, nil, nil
	for index, chunkNodes in ipairs(chunks) do
		local result, err, statusCode = apiRequest("POST", "/studio/snapshot", {
			sessionId = sessionId,
			connectorToken = token,
			mode = mode,
			chunk = {
				id = uploadId,
				index = index,
				total = #chunks,
				totalNodeCount = #nodes
			},
			nodes = chunkNodes
		})
		if not result then
			return nil, err, statusCode
		end
		finalResult, finalErr, finalStatusCode = result, err, statusCode
	end
	return finalResult, finalErr, finalStatusCode
end

-- Smart Sync Tracking
local dirtyNodes = {}
local hookedSources = setmetatable({}, { __mode = "k" })
local hookedInstances = setmetatable({}, { __mode = "k" })
local syncLock = false

local SYNCABLE_CLASSES = {
	Folder = true,
	Model = true,
	Part = true,
	WedgePart = true,
	CornerWedgePart = true,
	TrussPart = true,
	SpawnLocation = true,
	Tool = true,
	Animation = true,
	PointLight = true,
	SpotLight = true,
	SurfaceLight = true,
	Attachment = true,
	WeldConstraint = true,
	ProximityPrompt = true,
	ClickDetector = true,
	SurfaceGui = true,
	BillboardGui = true,
	RemoteEvent = true,
	RemoteFunction = true,
	ScreenGui = true,
	Frame = true,
	ScrollingFrame = true,
	CanvasGroup = true,
	TextLabel = true,
	TextButton = true,
	ImageLabel = true,
	ImageButton = true,
	UIListLayout = true,
	UIGridLayout = true,
	UIPadding = true,
	UICorner = true,
	UIStroke = true,
	UIGradient = true,
	UIAspectRatioConstraint = true,
	UIScale = true,
	UITextSizeConstraint = true,
	UIPageLayout = true
}

local WATCHED_PROPERTIES = {
	"Name",
	"Position",
	"Size",
	"CFrame",
	"Orientation",
	"Rotation",
	"Anchored",
	"CanCollide",
	"Color",
	"Material",
	"Shape",
	"Transparency",
	"Text",
	"Visible",
	"BackgroundColor3",
	"BackgroundTransparency",
	"Image"
}

local function encodePropertyValue(value)
	local valueType = typeof(value)
	if valueType == "Vector3" then
		return { type = "Vector3", value = { value.X, value.Y, value.Z } }
	elseif valueType == "Vector2" then
		return { type = "Vector2", value = { value.X, value.Y } }
	elseif valueType == "Color3" then
		return { type = "Color3", value = { math.floor(value.R * 255 + 0.5), math.floor(value.G * 255 + 0.5), math.floor(value.B * 255 + 0.5) } }
	elseif valueType == "UDim" then
		return { type = "UDim", value = { value.Scale, value.Offset } }
	elseif valueType == "UDim2" then
		return { type = "UDim2", value = { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset } }
	elseif valueType == "CFrame" then
		return { type = "CFrame", value = { value:GetComponents() } }
	elseif valueType == "EnumItem" then
		local enumType = tostring(value.EnumType):gsub("^Enum%.", "")
		return { type = "Enum", enumType = enumType, value = value.Name }
	elseif valueType == "string" or valueType == "number" or valueType == "boolean" then
		return value
	end
	return nil
end

local function captureSnapshotProperties(inst)
	local properties = {}
	local function add(name, getter)
		local ok, value = pcall(getter or function() return inst[name] end)
		if ok then
			local encoded = encodePropertyValue(value)
			if encoded ~= nil then
				properties[name] = encoded
			end
		end
	end

	if inst:IsA("BasePart") then
		add("Position")
		add("Size")
		add("CFrame")
		add("Orientation")
		add("Rotation")
		add("Anchored")
		add("CanCollide")
		add("Transparency")
		add("Color")
		add("Material")
		add("Shape")
	elseif inst:IsA("Model") then
		add("Pivot", function() return inst:GetPivot() end)
	elseif inst:IsA("Light") then
		add("Enabled")
		add("Brightness")
		add("Color")
		add("Range")
		add("Angle")
		add("Shadows")
		add("Face")
	elseif inst:IsA("ProximityPrompt") then
		add("Enabled")
		add("ActionText")
		add("ObjectText")
		add("HoldDuration")
		add("KeyboardKeyCode")
		add("GamepadKeyCode")
		add("MaxActivationDistance")
		add("RequiresLineOfSight")
		add("ClickablePrompt")
	elseif inst:IsA("LayerCollector") or inst:IsA("GuiObject") or inst.ClassName == "UIListLayout" or inst.ClassName == "UIGridLayout" or inst.ClassName == "UIPadding" or inst.ClassName == "UICorner" or inst.ClassName == "UIStroke" or inst.ClassName == "UIGradient" or inst.ClassName == "UIAspectRatioConstraint" or inst.ClassName == "UIScale" or inst.ClassName == "UITextSizeConstraint" or inst.ClassName == "UIPageLayout" then
		add("Enabled")
		add("Visible")
		add("Size")
		add("Position")
		add("AnchorPoint")
		add("LayoutOrder")
		add("BackgroundColor3")
		add("BackgroundTransparency")
		add("Text")
		add("TextColor3")
		add("TextSize")
		add("Font")
		add("Image")
		add("AspectRatio")
		add("AspectType")
		add("DominantAxis")
		add("Scale")
		add("MinTextSize")
		add("MaxTextSize")
	end

	return next(properties) and properties or nil
end

local function snapshotNodeFor(inst)
	local path = toVectisPath(inst)
	if not path then return nil end
	local node = { path = path, className = inst.ClassName }
	if inst:IsA("LuaSourceContainer") then node.source = readSource(inst) end
	local properties = captureSnapshotProperties(inst)
	if properties then node.properties = properties end
	return node
end
local state = { currentPatches = {}, isApplying = false, inFlightPatchIds = {}, lastResyncRequestedAt = "" }
local lastUndoStack = nil
local lastAppliedPatchId = nil
local rollbackHistory = {}
local MAX_ROLLBACK_HISTORY = 10
local COMPLETED_PATCH_REPORT_LIMIT = 50

local function getSnapshotTargets()
	local targets = {}
	for _, serviceName in ipairs(ROOT_SERVICE_ORDER) do
		local ok, service = pcall(function() return game:GetService(serviceName) end)
		if ok and service then
			table.insert(targets, service)
		end
	end
	return targets
end

local function isSyncable(inst)
	return inst:IsA("LuaSourceContainer") or SYNCABLE_CLASSES[inst.ClassName] == true
end

local function markDirty(inst)
	if isSyncable(inst) then
		local path = toVectisPath(inst)
		if path then
			dirtyNodes[inst] = {
				status = "updated",
				path = path,
				className = inst.ClassName
			}
		end
	end
end

local function markDeleted(inst)
	if isSyncable(inst) then
		local path = toVectisPath(inst)
		if path then
			dirtyNodes[inst] = {
				status = "deleted",
				path = path,
				className = inst.ClassName
			}
		end
	end
end

local function hookInstance(inst)
	if inst:IsA("LuaSourceContainer") and not hookedSources[inst] then
		hookedSources[inst] = true
		inst:GetPropertyChangedSignal("Source"):Connect(function() markDirty(inst) end)
	end
	if isSyncable(inst) and not hookedInstances[inst] then
		hookedInstances[inst] = true
		for _, propertyName in ipairs(WATCHED_PROPERTIES) do
			pcall(function()
				inst:GetPropertyChangedSignal(propertyName):Connect(function() markDirty(inst) end)
			end)
		end
	end
end

local function syncSnapshot(forceFull)
	if syncLock then return end
	
	local sessionId = settingString("VectisSessionId")
	local token = settingString("VectisToken")
	if sessionId == "" or token == "" then return end
	
	local nodes = {}
	local count = 0
	local mode = forceFull and "full" or "delta"
	local processedDirtyNodes = {}
	
	if forceFull then
		local targets = getSnapshotTargets()
		for _, root in ipairs(targets) do
			table.insert(nodes, { path = root.Name, className = root.ClassName })
			for _, inst in ipairs(root:GetDescendants()) do
				if isSyncable(inst) then
					count = count + 1
					local node = snapshotNodeFor(inst)
					if node then
						table.insert(nodes, node)
					end
				end
				if count >= MAX_FULL_NODES then break end
			end
			if count >= MAX_FULL_NODES then break end
		end
	else
		local toProcess = dirtyNodes
		dirtyNodes = {}
		for inst, change in pairs(toProcess) do
			if count >= MAX_DELTA_NODES then
				if dirtyNodes[inst] == nil then
					dirtyNodes[inst] = change
				end
			else
				count = count + 1
				processedDirtyNodes[inst] = change
				if type(change) == "table" and change.status == "deleted" then
					table.insert(nodes, { path = change.path, className = change.className, deleted = true })
				else
					local node = snapshotNodeFor(inst)
					if node then
						table.insert(nodes, node)
					end
				end
			end
		end
	end
	
	if #nodes == 0 then return end
	
	syncLock = true
	local result, err, statusCode = postSnapshot(sessionId, token, mode, nodes)
	if not result and not forceFull then
		for inst, change in pairs(processedDirtyNodes) do
			if dirtyNodes[inst] == nil then
				dirtyNodes[inst] = change
			end
		end
	end
	if not result and (statusCode == 400 or statusCode == 401 or statusCode == 403 or statusCode == 409 or statusCode == 413) then
		statusLabel.Text = "SYNC PAUSED"
		statusLabel.TextColor3 = COLOR_DANGER
		sessionInfo.Text = tostring(err or "Reconnect required")
	end
	syncLock = false
end

-- Initialize Listeners
for _, target in ipairs(getSnapshotTargets()) do
	target.DescendantAdded:Connect(function(inst) markDirty(inst) hookInstance(inst) end)
	target.DescendantRemoving:Connect(markDeleted)
	for _, inst in ipairs(target:GetDescendants()) do hookInstance(inst) end
end

-- Patching Logic
local ALLOWED_CREATE_CLASSES = {
	Script = true,
	LocalScript = true,
	ModuleScript = true,
	Folder = true,
	RemoteEvent = true,
	RemoteFunction = true,
	Tool = true,
	Part = true,
	WedgePart = true,
	CornerWedgePart = true,
	TrussPart = true,
	SpawnLocation = true,
	Model = true,
	Animation = true,
	PointLight = true,
	SpotLight = true,
	SurfaceLight = true,
	Attachment = true,
	WeldConstraint = true,
	ProximityPrompt = true,
	ClickDetector = true,
	SurfaceGui = true,
	BillboardGui = true,
	ScreenGui = true,
	Frame = true,
	ScrollingFrame = true,
	CanvasGroup = true,
	TextLabel = true,
	TextButton = true,
	ImageLabel = true,
	ImageButton = true,
	UIListLayout = true,
	UIGridLayout = true,
	UIPadding = true,
	UICorner = true,
	UIStroke = true,
	UIGradient = true,
	UIAspectRatioConstraint = true,
	UIScale = true,
	UITextSizeConstraint = true,
	UIPageLayout = true
}

local SAFE_PROPERTIES = {
	Name = true,
	Anchored = true,
	CanCollide = true,
	Transparency = true,
	Color = true,
	Material = true,
	Shape = true,
	TopSurface = true,
	BottomSurface = true,
	Size = true,
	Position = true,
	CFrame = true,
	Pivot = true,
	Orientation = true,
	Neutral = true,
	AllowTeamChangeOnTouch = true,
	RequiresHandle = true,
	CanBeDropped = true,
	ToolTip = true,
	AnimationId = true,
	Enabled = true,
	Visible = true,
	ZIndex = true,
	ResetOnSpawn = true,
	IgnoreGuiInset = true,
	ZIndexBehavior = true,
	BackgroundColor3 = true,
	BackgroundTransparency = true,
	BorderColor3 = true,
	BorderSizePixel = true,
	Text = true,
	TextColor3 = true,
	TextTransparency = true,
	TextSize = true,
	TextScaled = true,
	TextWrapped = true,
	TextXAlignment = true,
	TextYAlignment = true,
	Font = true,
	Image = true,
	ImageRectOffset = true,
	ImageRectSize = true,
	ImageColor3 = true,
	ImageTransparency = true,
	ScaleType = true,
	AutoButtonColor = true,
	ClipsDescendants = true,
	LayoutOrder = true,
	AnchorPoint = true,
	AutomaticSize = true,
	AutomaticCanvasSize = true,
	CanvasSize = true,
	ScrollBarImageColor3 = true,
	ScrollBarThickness = true,
	Padding = true,
	PaddingTop = true,
	PaddingBottom = true,
	PaddingLeft = true,
	PaddingRight = true,
	CornerRadius = true,
	Thickness = true,
	ApplyStrokeMode = true,
	FillDirection = true,
	HorizontalAlignment = true,
	VerticalAlignment = true,
	SortOrder = true,
	CellSize = true,
	CellPadding = true,
	Rotation = true,
	Offset = true,
	Scale = true,
	Brightness = true,
	Range = true,
	Angle = true,
	Shadows = true,
	Face = true,
	AlwaysOnTop = true,
	MaxDistance = true,
	LightInfluence = true,
	ActionText = true,
	ObjectText = true,
	HoldDuration = true,
	KeyboardKeyCode = true,
	GamepadKeyCode = true,
	MaxActivationDistance = true,
	RequiresLineOfSight = true,
	ClickablePrompt = true,
	AspectRatio = true,
	AspectType = true,
	DominantAxis = true,
	MinTextSize = true,
	MaxTextSize = true,
}

local ENUM_PROPERTY_TYPES = {
	Material = "Material",
	Shape = "PartType",
	TopSurface = "SurfaceType",
	BottomSurface = "SurfaceType",
	Font = "Font",
	ZIndexBehavior = "ZIndexBehavior",
	ScaleType = "ScaleType",
	TextXAlignment = "TextXAlignment",
	TextYAlignment = "TextYAlignment",
	AutomaticSize = "AutomaticSize",
	AutomaticCanvasSize = "AutomaticSize",
	FillDirection = "FillDirection",
	HorizontalAlignment = "HorizontalAlignment",
	VerticalAlignment = "VerticalAlignment",
	SortOrder = "SortOrder",
	ApplyStrokeMode = "ApplyStrokeMode",
	Face = "NormalId",
	KeyboardKeyCode = "KeyCode",
	GamepadKeyCode = "KeyCode",
	AspectType = "AspectType",
	DominantAxis = "DominantAxis"
}

local function createSafeInstance(className)
	if not ALLOWED_CREATE_CLASSES[className] then
		error("Unsupported class: " .. tostring(className))
	end
	return Instance.new(className)
end

local function resolveInstance(path, className)
	local parts = splitVectisPath(path)
	local firstPart = parts[1]
	local serviceSuccess, service = pcall(function() return game:GetService(firstPart) end)
	if not serviceSuccess or not service or not ROOT_SERVICE_NAMES[firstPart] then
		error("Service not found: " .. tostring(firstPart))
	end
	local current = service
	table.remove(parts, 1)
	for i, part in ipairs(parts) do
		if part == "" then continue end
		local nextInst = current:FindFirstChild(part)
		local isLast = (i == #parts)
		if nextInst and isLast and className and nextInst.ClassName ~= className then
			nextInst:Destroy()
			nextInst = nil
		end
		if not nextInst then
			nextInst = createSafeInstance(isLast and className or "Folder")
			nextInst.Name = part
			nextInst.Parent = current
		end
		current = nextInst
	end
	return current
end

local function findInstanceByVectisPath(path)
	local parts = splitVectisPath(path)
	local firstPart = parts[1]
	local serviceSuccess, service = pcall(function() return game:GetService(firstPart) end)
	if not serviceSuccess or not service or not ROOT_SERVICE_NAMES[firstPart] then
		return nil
	end
	local current = service
	table.remove(parts, 1)
	for _, part in ipairs(parts) do
		if part ~= "" then
			current = current:FindFirstChild(part)
			if not current then return nil end
		end
	end
	return current
end

local function resolveParentAndName(path)
	local parts = splitVectisPath(path)
	local leafName = parts[#parts]
	table.remove(parts, #parts)
	local parentPath = table.concat(parts, "/")
	local parent = findInstanceByVectisPath(parentPath)
	if not parent then
		parent = resolveInstance(parentPath, "Folder")
	end
	return parent, leafName
end

local function decodePropertyValue(key, value)
	local enumTypeName = ENUM_PROPERTY_TYPES[key]
	if type(value) == "string" and enumTypeName then
		local enumType = Enum[enumTypeName]
		if enumType then
			local ok, enumValue = pcall(function() return enumType[value] end)
			if ok and enumValue then
				return enumValue
			end
		end
	end

	if type(value) ~= "table" or value.type == nil then
		return value
	end

	local valueType = value.type
	local raw = value.value
	if valueType == "Vector3" and type(raw) == "table" then
		return Vector3.new(raw[1] or 0, raw[2] or 0, raw[3] or 0)
	elseif valueType == "Vector2" and type(raw) == "table" then
		return Vector2.new(raw[1] or 0, raw[2] or 0)
	elseif valueType == "Color3" and type(raw) == "table" then
		return Color3.fromRGB(raw[1] or 0, raw[2] or 0, raw[3] or 0)
	elseif valueType == "UDim" and type(raw) == "table" then
		return UDim.new(raw[1] or 0, raw[2] or 0)
	elseif valueType == "UDim2" and type(raw) == "table" then
		return UDim2.new(raw[1] or 0, raw[2] or 0, raw[3] or 0, raw[4] or 0)
	elseif valueType == "CFrame" and type(raw) == "table" then
		return CFrame.new(
			raw[1] or 0, raw[2] or 0, raw[3] or 0,
			raw[4] or 1, raw[5] or 0, raw[6] or 0,
			raw[7] or 0, raw[8] or 1, raw[9] or 0,
			raw[10] or 0, raw[11] or 0, raw[12] or 1
		)
	elseif valueType == "Enum" and type(value.enumType) == "string" and type(value.value) == "string" then
		local enumType = Enum[value.enumType]
		if enumType then
			local ok, enumValue = pcall(function() return enumType[value.value] end)
			if ok and enumValue then
				return enumValue
			end
		end
	end

	error("Unsupported property value type: " .. tostring(valueType))
end

local function applySafeProperties(inst, properties)
	if type(properties) ~= "table" then return end
	for key, value in pairs(properties) do
		if not SAFE_PROPERTIES[key] then
			warn("Vectis skipped unsupported property: " .. tostring(key))
			continue
		end
		local decodedOk, decodedValue = pcall(function()
			return decodePropertyValue(key, value)
		end)
		if not decodedOk then
			warn("Vectis skipped property with unsupported value: " .. tostring(key))
			continue
		end
		local assignedOk, assignError = pcall(function()
			if key == "Pivot" and inst:IsA("Model") and typeof(decodedValue) == "CFrame" then
				inst:PivotTo(decodedValue)
			else
				inst[key] = decodedValue
			end
		end)
		if not assignedOk then
			warn("Vectis skipped property " .. tostring(key) .. ": " .. tostring(assignError))
		end
	end
end

local function importReviewedAsset(file)
	local assetId = tonumber(file.assetId)
	if not assetId or assetId <= 0 then
		error("Missing assetId for import_asset")
	end

	if file.assetType == "animation" or file.className == "Animation" then
		local animation = resolveInstance(file.instancePath, "Animation")
		animation.AnimationId = "rbxassetid://" .. tostring(assetId)
		applySafeProperties(animation, file.properties)
		return animation
	end

	local parent, leafName = resolveParentAndName(file.instancePath)
	local loaded = InsertService:LoadAsset(assetId)
	local children = loaded:GetChildren()
	if #children == 0 then
		loaded:Destroy()
		error("Imported asset had no children: " .. tostring(assetId))
	end

	local imported
	if #children == 1 then
		imported = children[1]
		imported.Name = leafName
		imported.Parent = parent
		loaded:Destroy()
	else
		imported = Instance.new("Model")
		imported.Name = leafName
		imported.Parent = parent
		for _, child in ipairs(children) do
			child.Parent = imported
		end
		loaded:Destroy()
	end
	applySafeProperties(imported, file.properties)
	return imported
end

local function captureSafeProperties(inst, properties)
	local captured = {}
	if type(properties) ~= "table" then return captured end
	for key, _ in pairs(properties) do
		if SAFE_PROPERTIES[key] then
			local ok, value = pcall(function()
				if key == "Pivot" and inst:IsA("Model") then
					return inst:GetPivot()
				end
				return inst[key]
			end)
			if ok then captured[key] = value end
		end
	end
	return captured
end

local function captureUndoRecord(file)
	local inst = findInstanceByVectisPath(file.instancePath)
	if not inst then
		return {
			path = file.instancePath,
			existed = false
		}
	end

	local clone = nil
	pcall(function() clone = inst:Clone() end)
	return {
		path = file.instancePath,
		existed = true,
		className = inst.ClassName,
		clone = clone,
		source = inst:IsA("LuaSourceContainer") and readSource(inst) or nil,
		properties = captureSafeProperties(inst, file.properties)
	}
end

local function restoreUndoRecord(record)
	local current = findInstanceByVectisPath(record.path)
	if not record.existed then
		if current then current:Destroy() end
		return
	end

	if current then current:Destroy() end
	if record.clone then
		local parent, leafName = resolveParentAndName(record.path)
		local restored = record.clone:Clone()
		restored.Name = leafName
		restored.Parent = parent
		return
	end

	local restored = resolveInstance(record.path, record.className or "Folder")
	if record.source and restored:IsA("LuaSourceContainer") then
		writeSource(restored, record.source)
	end
	applySafeProperties(restored, record.properties)
end

local function restoreUndoStack(undoStack)
	for i = #undoStack, 1, -1 do
		pcall(function() restoreUndoRecord(undoStack[i]) end)
	end
end

local function rememberRollbackRecord(patchId, undoStack)
	if not patchId or patchId == "" or not undoStack or #undoStack == 0 then return end
	table.insert(rollbackHistory, 1, {
		id = patchId,
		undoStack = undoStack
	})
	while #rollbackHistory > MAX_ROLLBACK_HISTORY do
		table.remove(rollbackHistory)
	end
	lastUndoStack = undoStack
	lastAppliedPatchId = patchId
end

local function findRollbackRecord(patchId)
	for i, record in ipairs(rollbackHistory) do
		if record.id == patchId then
			return record, i
		end
	end
	return nil, nil
end

local function loadCompletedPatchReports()
	local raw = plugin:GetSetting("VectisCompletedPatchReports")
	if typeof(raw) ~= "string" or raw == "" then
		return {}
	end
	local ok, decoded = pcall(function()
		return HttpService:JSONDecode(raw)
	end)
	if not ok or type(decoded) ~= "table" then
		return {}
	end
	return decoded
end

local completedPatchReports = loadCompletedPatchReports()

local function persistCompletedPatchReports()
	local records = {}
	for patchId, report in pairs(completedPatchReports) do
		if type(report) == "table" then
			report.updatedAt = tonumber(report.updatedAt) or os.time()
			table.insert(records, {
				id = tostring(patchId),
				updatedAt = report.updatedAt
			})
		end
	end
	table.sort(records, function(a, b)
		return (a.updatedAt or 0) > (b.updatedAt or 0)
	end)
	for i = COMPLETED_PATCH_REPORT_LIMIT + 1, #records do
		completedPatchReports[records[i].id] = nil
	end
	local ok, encoded = pcall(function()
		return HttpService:JSONEncode(completedPatchReports)
	end)
	if ok then
		plugin:SetSetting("VectisCompletedPatchReports", encoded)
	end
end

local function reportPatchResult(patchId, applied, detail)
	local safeDetail = tostring(detail or "")
	if #safeDetail > 1900 then
		safeDetail = safeDetail:sub(1, 1900) .. "\n[Vectis truncated additional operation details.]"
	end
	local result = apiRequest("POST", "/studio/changes/" .. patchId .. "/apply-result", {
		sessionId = settingString("VectisSessionId"),
		connectorToken = settingString("VectisToken"),
		status = applied and "applied" or "failed",
		details = safeDetail
	})
	return result ~= nil
end

local function reportTaskStatus(patch, status)
	if not patch or typeof(patch.studioTaskRunId) ~= "string" or patch.studioTaskRunId == "" then
		return
	end
	apiRequest("POST", "/studio/task-runs/" .. patch.studioTaskRunId .. "/status", {
		sessionId = settingString("VectisSessionId"),
		connectorToken = settingString("VectisToken"),
		status = status
	})
end

local function reportTaskObservation(patch, kind, status, summary, details)
	if not patch or typeof(patch.studioTaskRunId) ~= "string" or patch.studioTaskRunId == "" then
		return
	end
	apiRequest("POST", "/studio/task-runs/" .. patch.studioTaskRunId .. "/observations", {
		sessionId = settingString("VectisSessionId"),
		connectorToken = settingString("VectisToken"),
		kind = kind,
		status = status,
		summary = summary,
		details = details
	})
end

local function captureAndUploadScreenshot(patch)
	if not visualQaEnabled() or not patch or typeof(patch.studioTaskRunId) ~= "string" or patch.studioTaskRunId == "" then
		return
	end
	local ok, err = pcall(function()
		if not StudioCaptureService:CanCaptureScreenshot() then
			warn("Vectis Visual QA skipped: Studio screenshot permission is not available.")
			return
		end
		local capture = StudioCaptureService:CaptureScreenshot({})
		local raw = buffer.tostring(capture:GetBuffer())
		if #raw == 0 then
			warn("Vectis Visual QA skipped: Studio returned an empty screenshot.")
			return
		end
		if #raw > 5 * 1024 * 1024 then
			warn("Vectis Visual QA skipped: Studio screenshot exceeds 5 MB.")
			return
		end
		local sessionId = settingString("VectisSessionId")
		local path = "/studio/task-runs/" .. patch.studioTaskRunId .. "/screenshots"
			.. "?sessionId=" .. HttpService:UrlEncode(sessionId)
		local result, uploadErr = apiRawRequest("POST", path, "application/octet-stream", raw, {
			["X-Screenshot-Format"] = tostring(capture.BufferFormat)
		})
		if not result then
			warn("Vectis Visual QA screenshot upload failed: " .. tostring(uploadErr))
		end
	end)
	if not ok then
		warn("Vectis Visual QA screenshot failed: " .. tostring(err))
	end
end

local function hasReportedPatch(patch)
	return patch and typeof(patch.id) == "string" and completedPatchReports[patch.id] and completedPatchReports[patch.id].reported
end

local function filterUnreportedPatches(patches)
	local filtered = {}
	for _, patch in ipairs(patches or {}) do
		if patch and typeof(patch.id) == "string" and not state.inFlightPatchIds[patch.id] and not hasReportedPatch(patch) then
			table.insert(filtered, patch)
		end
	end
	return filtered
end

local function applyPatch(patch)
	if not patch or typeof(patch.id) ~= "string" or patch.id == "" then
		return false, { "Patch is missing an id." }
	end
	if completedPatchReports[patch.id] then
		if not completedPatchReports[patch.id].reported then
			completedPatchReports[patch.id].reported = reportPatchResult(patch.id, completedPatchReports[patch.id].applied, completedPatchReports[patch.id].detail)
			completedPatchReports[patch.id].updatedAt = os.time()
			persistCompletedPatchReports()
		end
		return completedPatchReports[patch.id].applied, {}
	end

	local files = patch.files or {}
	reportTaskStatus(patch, "applying")
	local errors = {}
	local validated = {}
	local undoStack = {}
	local historyRecording = beginHistoryRecording("VectisPatch_" .. patch.id, "Apply Vectis task")
	for _, file in ipairs(files) do
		local ok, err = pcall(function()
			table.insert(undoStack, captureUndoRecord(file))
			if file.action == "import_asset" then
				importReviewedAsset(file)
			elseif file.action == "create" or file.action == "update" then
				local inst = resolveInstance(file.instancePath, file.className)
				if inst and file.source and inst:IsA("LuaSourceContainer") then
					writeSource(inst, file.source)
				end
				applySafeProperties(inst, file.properties)
			elseif file.action == "delete" then
				local parts = splitVectisPath(file.instancePath)
				local firstPart = parts[1]
				local serviceSuccess, service = pcall(function() return game:GetService(firstPart) end)
				if not serviceSuccess or not service or not ROOT_SERVICE_NAMES[firstPart] then
					error("Service not found: " .. tostring(firstPart))
				end
				local current = service
				table.remove(parts, 1)
				for i = 1, #parts - 1 do
					local nextNode = current:FindFirstChild(parts[i])
					if not nextNode then current = nil break end
					current = nextNode
				end
				local inst = current and current:FindFirstChild(parts[#parts])
				if inst then inst:Destroy() end
			end

			local existsAfter = findInstanceByVectisPath(file.instancePath) ~= nil
			if file.action == "delete" then
				table.insert(validated, (not existsAfter and "deleted " or "delete missing ") .. tostring(file.instancePath))
			else
				if not existsAfter then
					error("Validation failed, instance missing after apply: " .. tostring(file.instancePath))
				end
				table.insert(validated, "verified " .. tostring(file.instancePath))
			end
		end)
		if not ok then
			table.insert(errors, tostring(err))
		end
	end
	local applied = #errors == 0
	if applied then
		rememberRollbackRecord(patch.id, undoStack)
		finishHistoryRecording(historyRecording, Enum.FinishRecordingOperation.Commit, "After Vectis patch")
	else
		restoreUndoStack(undoStack)
		finishHistoryRecording(historyRecording, Enum.FinishRecordingOperation.Cancel, "Rolled back failed Vectis patch")
	end
	local detail = applied
		and ("Applied and validated " .. tostring(#validated) .. "/" .. tostring(#files) .. " operations.\n" .. table.concat(validated, "\n"))
		or ("Patch failed and was rolled back.\n" .. table.concat(errors, "\n"))
	reportTaskStatus(patch, "validating")
	reportTaskObservation(
		patch,
		"validation_probe",
		applied and "passed" or "failed",
		applied and "Verified the Studio patch after apply." or "Studio patch validation found errors.",
		{
			operationCount = #files,
			verifiedCount = #validated,
			errorCount = #errors
		}
	)
	completedPatchReports[patch.id] = {
		applied = applied,
		detail = detail,
		reported = false,
		updatedAt = os.time()
	}
	persistCompletedPatchReports()
	completedPatchReports[patch.id].reported = reportPatchResult(patch.id, applied, detail)
	completedPatchReports[patch.id].updatedAt = os.time()
	persistCompletedPatchReports()
	if applied then
		task.spawn(function() captureAndUploadScreenshot(patch) end)
	end
	return applied, errors
end

local function connect(reconnectReason)
	local existingSessionId = settingString("VectisSessionId")
	local existingToken = settingString("VectisToken")
	if existingSessionId ~= "" and existingToken ~= "" then return end
	statusLabel.Text = "CONNECTING..."
	statusLabel.TextColor3 = COLOR_TEXT
	sessionInfo.Text = reconnectReason or "Requesting a secure pairing code..."
	local result, err = apiRequest("POST", "/studio/connect", {
		pluginVersion = PLUGIN_VERSION,
		placeId = tostring(game.PlaceId),
		placeName = tostring(game.Name)
	})
	if result and result.session then
		codeDisplay.Text = formatPairingCode(result.session.pairingCode)
		statusLabel.Text = "PAIRING CODE READY"
		statusLabel.TextColor3 = COLOR_ACCENT
		sessionInfo.Text = reconnectReason or "Enter all 12 characters in the web app."
		plugin:SetSetting("VectisSessionId", result.session.id)
		plugin:SetSetting("VectisToken", result.session.connectorToken)
	else
		local friendly = describeRequestProblem(err)
		statusLabel.Text = string.find(friendly, "Enable HTTP Requests", 1, true) and "HTTP REQUESTS OFF" or "CONNECTION ERROR"
		statusLabel.TextColor3 = COLOR_DANGER
		sessionInfo.Text = friendly
	end
end

local function disconnect()
	local sessionId = settingString("VectisSessionId")
	local token = settingString("VectisToken")
	statusLabel.Text = "UNLINKING..."
	statusLabel.TextColor3 = COLOR_ACCENT
	sessionInfo.Text = "Ending this Studio bridge."
	if sessionId ~= "" and token ~= "" then
		apiRequest("POST", "/studio/session/" .. sessionId .. "/plugin-disconnect", { connectorToken = token })
	end
	plugin:SetSetting("VectisSessionId", "")
	plugin:SetSetting("VectisToken", "")
	state.currentPatches = {}
	connectedContainer.Visible = false
	pairingContainer.Visible = true
	statusLabel.Text = "DISCONNECTED"
	sessionInfo.Text = "Generate a new pairing code to link Studio again."
	connect()
end

local function applyCurrentPatches()
	if state.isApplying or #state.currentPatches == 0 then return end
	state.isApplying = true
	statusLabel.Text = "APPLYING REVIEWED PATCH..."
	statusLabel.TextColor3 = COLOR_ACCENT
	sessionInfo.Text = "Applying the patch you approved in the web app."
	local allApplied = true
	for _, patch in ipairs(state.currentPatches) do
		if patch and typeof(patch.id) == "string" then
			state.inFlightPatchIds[patch.id] = true
		end
		local applied = applyPatch(patch)
		if not applied then allApplied = false end
		if patch and typeof(patch.id) == "string" then
			state.inFlightPatchIds[patch.id] = nil
		end
	end
	state.currentPatches = {}
	state.isApplying = false
	statusLabel.Text = allApplied and "PATCH APPLIED" or "PATCH ROLLED BACK"
	statusLabel.TextColor3 = allApplied and COLOR_SUCCESS or COLOR_DANGER
	syncSnapshot(true)
end

local function undoPatch(patch)
	local rollbackRecord, rollbackIndex = findRollbackRecord(patch and patch.id)
	if not patch or not rollbackRecord or not rollbackRecord.undoStack or #rollbackRecord.undoStack == 0 then
		apiRequest("POST", "/studio/changes/" .. tostring(patch and patch.id or "") .. "/undo-result", {
			sessionId = settingString("VectisSessionId"),
			connectorToken = settingString("VectisToken"),
			status = "failed",
			details = "This rollback is not available in the current Studio session. Reopen the place from a backup or roll back a newer patch first."
		})
		return
	end
	statusLabel.Text = "UNDOING VECTIS PATCH..."
	statusLabel.TextColor3 = COLOR_ACCENT
	local historyRecording = beginHistoryRecording("VectisRollback_" .. patch.id, "Roll back Vectis task")
	restoreUndoStack(rollbackRecord.undoStack)
	finishHistoryRecording(historyRecording, Enum.FinishRecordingOperation.Commit, "Vectis patch rolled back")
	table.remove(rollbackHistory, rollbackIndex)
	local latest = rollbackHistory[1]
	lastUndoStack = latest and latest.undoStack or nil
	lastAppliedPatchId = latest and latest.id or nil
	statusLabel.Text = "PATCH UNDONE"
	statusLabel.TextColor3 = COLOR_SUCCESS
	sessionInfo.Text = "Restored the previous local Studio state."
	syncSnapshot(true)
	apiRequest("POST", "/studio/changes/" .. patch.id .. "/undo-result", {
		sessionId = settingString("VectisSessionId"),
		connectorToken = settingString("VectisToken"),
		status = "undone",
		details = "Undo applied in Studio and the generated web message can be removed."
	})
end

local RunService = game:GetService("RunService")
local ScriptDebuggerService = nil
pcall(function() ScriptDebuggerService = game:GetService("ScriptDebuggerService") end)

local commandExecutionLock = false
local MAX_COMMAND_LOGS = 100
local MAX_COMMAND_TREE_NODES = 500

local function commandResultFor(commandId, status, result, errorMessage)
	local sessionId = settingString("VectisSessionId")
	local token = settingString("VectisToken")
	if sessionId == "" or token == "" then return false end
	local payload = {
		sessionId = sessionId,
		connectorToken = token,
		commandId = commandId,
		status = status,
		result = result or {}
	}
	if errorMessage then
		payload.error = tostring(errorMessage):sub(1, 1900)
	end
	local ok = apiRequest("POST", "/studio/session/" .. sessionId .. "/command-result", payload)
	return ok ~= nil
end

local function commandError(commandId, message)
	warn("Vectis command failed: " .. tostring(message))
	return commandResultFor(commandId, "error", {}, message)
end

local function readOutputCommand(commandId, args)
	local limit = math.min(tonumber(args.limit) or 30, MAX_COMMAND_LOGS)
	local ok, history = pcall(function() return LogService:GetLogHistory() end)
	if not ok or type(history) ~= "table" then
		return commandError(commandId, "Failed to read Studio log history")
	end
	local messages = {}
	local start = math.max(1, #history - limit + 1)
	for i = start, #history do
		local entry = history[i]
		if entry then
			local level = "info"
			if entry.messageType == Enum.MessageType.MessageWarning then
				level = "warn"
			elseif entry.messageType == Enum.MessageType.MessageError then
				level = "error"
			end
			table.insert(messages, {
				level = level,
				message = tostring(entry.message):sub(1, 500)
			})
		end
	end
	return commandResultFor(commandId, "ok", { messages = messages })
end

local function setBreakpointCommand(commandId, args)
	if not ScriptDebuggerService then
		return commandError(commandId, "ScriptDebuggerService is not available in this Studio version")
	end
	local path = tostring(args.path or "")
	local line = tonumber(args.line)
	if path == "" or not line or line < 1 then
		return commandError(commandId, "Breakpoint requires a valid path and line number")
	end
	local inst = findInstanceByVectisPath(path)
	if not inst or not inst:IsA("LuaSourceContainer") then
		return commandError(commandId, "Target is not a script: " .. path)
	end
	local condition = args.condition and tostring(args.condition) or nil
	local breakpoint = { Line = line }
	if condition and condition ~= "" then
		breakpoint.Condition = condition
	end
	local ok, result = pcall(function()
		return ScriptDebuggerService:AddBreakpoint(inst, breakpoint)
	end)
	if not ok then
		return commandError(commandId, "Failed to set breakpoint: " .. tostring(result))
	end
	return commandResultFor(commandId, "ok", {
		verified = result and result.Verified or false,
		line = result and result.Line or line,
		message = result and result.Message or nil
	})
end

local function clearBreakpointsCommand(commandId, args)
	if not ScriptDebuggerService then
		return commandError(commandId, "ScriptDebuggerService is not available in this Studio version")
	end
	local ok, err = pcall(function() ScriptDebuggerService:ClearBreakpoints() end)
	if not ok then
		return commandError(commandId, "Failed to clear breakpoints: " .. tostring(err))
	end
	return commandResultFor(commandId, "ok", { cleared = true })
end

local function queryTreeCommand(commandId, args)
	local path = tostring(args.path or "")
	local root = game
	if path ~= "" then
		root = findInstanceByVectisPath(path)
		if not root then
			return commandError(commandId, "Path not found: " .. path)
		end
	end
	local maxDepth = math.min(tonumber(args.maxDepth) or 3, 5)
	local ok, children = pcall(function() return root:GetChildren() end)
	if not ok or type(children) ~= "table" then
		return commandError(commandId, "Failed to read children")
	end
	local function describeNode(inst, depth)
		local node = {
			name = inst.Name,
			className = inst.ClassName,
			path = toVectisPath(inst) or inst:GetFullName()
		}
		if depth < maxDepth then
			local childOk, childList = pcall(function() return inst:GetChildren() end)
			if childOk and type(childList) == "table" then
				local childNodes = {}
				for _, child in ipairs(childList) do
					if #childNodes >= MAX_COMMAND_TREE_NODES then break end
					table.insert(childNodes, describeNode(child, depth + 1))
				end
				node.children = childNodes
			end
		end
		return node
	end
	local nodes = {}
	for _, inst in ipairs(children) do
		if #nodes >= MAX_COMMAND_TREE_NODES then break end
		table.insert(nodes, describeNode(inst, 1))
	end
	return commandResultFor(commandId, "ok", { rootPath = path == "" and "game" or path, nodes = nodes })
end

local function inspectInstanceCommand(commandId, args)
	local path = tostring(args.path or "")
	if path == "" then
		return commandError(commandId, "inspect_instance requires a path")
	end
	local inst = findInstanceByVectisPath(path)
	if not inst then
		return commandError(commandId, "Instance not found: " .. path)
	end
	local properties = captureSnapshotProperties(inst) or {}
	local childNames = {}
	local ok, children = pcall(function() return inst:GetChildren() end)
	if ok and type(children) == "table" then
		for _, child in ipairs(children) do
			table.insert(childNames, { name = child.Name, className = child.ClassName })
		end
	end
	local source = nil
	if inst:IsA("LuaSourceContainer") then
		source = readSource(inst)
	end
	return commandResultFor(commandId, "ok", {
		path = path,
		className = inst.ClassName,
		properties = properties,
		children = childNames,
		source = source
	})
end

local function insertAssetCommand(commandId, args)
	local assetId = tonumber(args.assetId)
	if not assetId or assetId <= 0 then
		return commandError(commandId, "insert_asset requires a valid assetId")
	end
	local parentPath = tostring(args.parentPath or "")
	local parent = game
	if parentPath ~= "" then
		parent = findInstanceByVectisPath(parentPath)
		if not parent then
			return commandError(commandId, "Parent path not found: " .. parentPath)
		end
	end
	local ok, loaded = pcall(function() return InsertService:LoadAsset(assetId) end)
	if not ok or not loaded then
		return commandError(commandId, "Failed to load asset: " .. tostring(assetId))
	end
	local children = loaded:GetChildren()
	if #children == 0 then
		loaded:Destroy()
		return commandError(commandId, "Loaded asset had no children")
	end
	local inserted
	local insertedPaths = {}
	if #children == 1 then
		inserted = children[1]
		inserted.Parent = parent
	else
		inserted = Instance.new("Folder")
		inserted.Name = "ImportedAsset"
		inserted.Parent = parent
		for _, child in ipairs(children) do
			child.Parent = inserted
		end
	end
	loaded:Destroy()
	local okPaths = pcall(function()
		insertedPaths = { inserted:GetFullName() }
		for _, child in ipairs(inserted:GetChildren()) do
			table.insert(insertedPaths, child:GetFullName())
		end
	end)
	if not okPaths then insertedPaths = { tostring(assetId) } end
	return commandResultFor(commandId, "ok", { insertedPaths = insertedPaths, assetId = assetId })
end

local function startPlayCommand(commandId, args)
	local ok, err = pcall(function() RunService:Run() end)
	if not ok then
		return commandError(commandId, "Failed to start playtest: " .. tostring(err))
	end
	return commandResultFor(commandId, "ok", { started = true })
end

local function stopPlayCommand(commandId, args)
	local ok, err = pcall(function() RunService:Stop() end)
	if not ok then
		return commandError(commandId, "Failed to stop playtest: " .. tostring(err))
	end
	return commandResultFor(commandId, "ok", { stopped = true })
end

local function executeCommand(command)
	if type(command) ~= "table" then return end
	local commandId = tostring(command.id or "")
	local commandType = tostring(command.type or "")
	local args = type(command.arguments) == "table" and command.arguments or {}
	if commandId == "" or commandType == "" then return end

	if commandType == "read_output" then
		readOutputCommand(commandId, args)
	elseif commandType == "set_breakpoint" then
		setBreakpointCommand(commandId, args)
	elseif commandType == "clear_breakpoints" then
		clearBreakpointsCommand(commandId, args)
	elseif commandType == "query_tree" then
		queryTreeCommand(commandId, args)
	elseif commandType == "inspect_instance" then
		inspectInstanceCommand(commandId, args)
	elseif commandType == "insert_asset" then
		insertAssetCommand(commandId, args)
	elseif commandType == "start_play" then
		startPlayCommand(commandId, args)
	elseif commandType == "stop_play" then
		stopPlayCommand(commandId, args)
	else
		commandError(commandId, "Unknown command type: " .. commandType)
	end
end

local function applyCurrentCommands(commands)
	if commandExecutionLock then return end
	if type(commands) ~= "table" or #commands == 0 then return end
	commandExecutionLock = true
	for _, command in ipairs(commands) do
		local ok, err = pcall(function() executeCommand(command) end)
		if not ok then
			local commandId = type(command) == "table" and tostring(command.id or "") or ""
			if commandId ~= "" then
				commandError(commandId, "Unexpected command error: " .. tostring(err))
			end
		end
	end
	commandExecutionLock = false
end

local function pollStatus()
	local lastFullSync = 0
	local lastDeltaSync = 0
	local consecutiveAuthErrors = 0
	while true do
		local sessionId = settingString("VectisSessionId")
		local token = settingString("VectisToken")
		if sessionId ~= "" and token ~= "" then
			local pollResult, err, statusCode = apiRequest("GET", "/studio/session/" .. sessionId .. "/poll")
			if pollResult and pollResult.session then
				consecutiveAuthErrors = 0
				local result = pollResult
				if result.session.status == "connected" or result.session.status == "paired" then
					pairingContainer.Visible = false
					connectedContainer.Visible = true
					statusLabel.Text = "STUDIO LINKED"
					statusLabel.TextColor3 = COLOR_SUCCESS
					sessionInfo.Text = "Vectis ID: " .. tostring(result.session.vectisId or "pending")
					
					local now = os.time()
					local requestedResyncAt = tostring(result.session.resyncRequestedAt or "")
					if requestedResyncAt ~= "" and requestedResyncAt ~= state.lastResyncRequestedAt then
						state.lastResyncRequestedAt = requestedResyncAt
						lastFullSync = now
						task.spawn(function() syncSnapshot(true) end)
					elseif now - lastFullSync > 300 then 
						lastFullSync = now
						task.spawn(function() syncSnapshot(true) end)
					elseif now - lastDeltaSync > 5 then
						lastDeltaSync = now
						task.spawn(function() syncSnapshot(false) end)
					end

					task.spawn(flushStudioLogs)
					
					local patchResult = pollResult
					if patchResult and patchResult.updateRequired then
						state.currentPatches = {}
						statusLabel.Text = "PLUGIN UPDATE NEEDED"
						statusLabel.TextColor3 = COLOR_DANGER
						sessionInfo.Text = tostring(patchResult.message or "Download the latest plugin before applying patches.")
					elseif patchResult and patchResult.patches and #patchResult.patches > 0 then
						local unreportedPatches = filterUnreportedPatches(patchResult.patches)
						state.currentPatches = unreportedPatches
						if not state.isApplying then
							if #unreportedPatches > 0 then
								statusLabel.Text = "APPLYING PATCH..."
								statusLabel.TextColor3 = COLOR_ACCENT
								sessionInfo.Text = "Applying the patch approved on the web app."
								task.spawn(applyCurrentPatches)
							else
								statusLabel.Text = "STUDIO CONNECTED"
								statusLabel.TextColor3 = COLOR_SUCCESS
								sessionInfo.Text = "Linked and syncing. Waiting for the web app to acknowledge the applied patch."
							end
						end
					else
						state.currentPatches = {}
						if not state.isApplying then
							statusLabel.Text = "STUDIO CONNECTED"
							statusLabel.TextColor3 = COLOR_SUCCESS
							sessionInfo.Text = "Linked and syncing. Approved patches will apply automatically in real-time."
						end
					end

					local undoResult = pollResult
					if undoResult and undoResult.undos and #undoResult.undos > 0 and not state.isApplying then
						task.spawn(function()
							undoPatch(undoResult.undos[1])
						end)
					end

					local commandResult = pollResult
					if commandResult and commandResult.commands and #commandResult.commands > 0 and not commandExecutionLock then
						task.spawn(function()
							applyCurrentCommands(commandResult.commands)
						end)
					end
				elseif result.session.status == "waiting" then
					pairingContainer.Visible = true
					connectedContainer.Visible = false
					statusLabel.Text = "READY TO LINK"
					statusLabel.TextColor3 = COLOR_ACCENT
					sessionInfo.Text = "Enter all 12 characters in the web app."
					codeDisplay.Text = formatPairingCode(result.session.pairingCode)
				else
					local reason = result.session.disconnectReason or "The web app ended this Studio bridge."
					plugin:SetSetting("VectisSessionId", "")
					plugin:SetSetting("VectisToken", "")
					pairingContainer.Visible = true
					connectedContainer.Visible = false
					statusLabel.Text = "BRIDGE DISCONNECTED"
					statusLabel.TextColor3 = COLOR_DANGER
					sessionInfo.Text = reason
					connect(reason .. " Copy the new code into Studio Bridge to reconnect.")
				end
			elseif statusCode == 403 or statusCode == 401 or statusCode == 404 then
				consecutiveAuthErrors = consecutiveAuthErrors + 1
				if consecutiveAuthErrors >= 3 then
					consecutiveAuthErrors = 0
					pcall(function()
						apiRequest("POST", "/studio/session/" .. sessionId .. "/plugin-disconnect", { connectorToken = token })
					end)
					plugin:SetSetting("VectisSessionId", "")
					plugin:SetSetting("VectisToken", "")
					pairingContainer.Visible = true
					connectedContainer.Visible = false
					statusLabel.Text = "BRIDGE DISCONNECTED"
					statusLabel.TextColor3 = COLOR_DANGER
					sessionInfo.Text = "The website no longer accepts this bridge."
					connect("The previous bridge was disconnected. Copy the new code into Studio Bridge to reconnect.")
				else
					statusLabel.Text = "RETRYING AUTH..."
					statusLabel.TextColor3 = COLOR_ACCENT
					sessionInfo.Text = "Temporary auth issue. Retrying connection... (" .. tostring(consecutiveAuthErrors) .. "/3)"
				end
			else
				statusLabel.Text = "RETRYING CONNECTION"
				statusLabel.TextColor3 = COLOR_DANGER
				sessionInfo.Text = describeRequestProblem(err)
			end
		else
			connect()
		end
		local pollDelay = (settingString("VectisSessionId") ~= "" and settingString("VectisToken") ~= "")
			and CONNECTED_POLL_INTERVAL_SECONDS
			or IDLE_POLL_INTERVAL_SECONDS
		task.wait(pollDelay)
	end
end

refreshButton.MouseButton1Click:Connect(function()
	plugin:SetSetting("VectisSessionId", "")
	plugin:SetSetting("VectisToken", "")
	connect()
end)


unlinkButton.MouseButton1Click:Connect(disconnect)
openButton.Click:Connect(function() widget.Enabled = not widget.Enabled end)
task.spawn(pollStatus)
