-- RBC MCP Studio Plugin
-- Run this as a local Roblox Studio plugin while the TypeScript MCP server is running.

local HttpService = game:GetService("HttpService")
local Selection = game:GetService("Selection")
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local SERVER_URL = "http://127.0.0.1:5718"
local SESSION_ID = HttpService:GenerateGUID(false)
local PLUGIN_VERSION = "0.1.0"
local POLL_SECONDS = 0.35
local running = false

local toolbar = plugin:CreateToolbar("RBC MCP")
local toggleButton = toolbar:CreateButton("RBC MCP", "Toggle RBC MCP Studio bridge", "")
toggleButton.ClickableWhenViewportHidden = true

local function jsonEncode(value)
	local ok, encoded = pcall(function()
		return HttpService:JSONEncode(value)
	end)
	return ok and encoded or "{}"
end

local function jsonDecode(value)
	local ok, decoded = pcall(function()
		return HttpService:JSONDecode(value)
	end)
	return ok and decoded or nil
end

local function request(method, path, body)
	local ok, response = pcall(function()
		return HttpService:RequestAsync({
			Url = SERVER_URL .. path,
			Method = method,
			Headers = {
				["Content-Type"] = "application/json",
			},
			Body = body and jsonEncode(body) or nil,
		})
	end)

	if not ok then
		return false, tostring(response)
	end

	if not response.Success then
		return false, response.StatusMessage
	end

	return true, jsonDecode(response.Body)
end

local function log(level, message, data)
	request("POST", "/bridge/log", {
		sessionId = SESSION_ID,
		level = level,
		message = message,
		data = data,
	})
end

local function splitPath(path)
	local parts = {}
	for part in string.gmatch(path, "[^%.]+") do
		table.insert(parts, part)
	end
	return parts
end

local function resolvePath(path)
	if typeof(path) ~= "string" or path == "" or path == "game" then
		return game
	end

	local cleaned = path
	if string.sub(cleaned, 1, 5) == "game." then
		cleaned = string.sub(cleaned, 6)
	end

	local current = game
	for _, name in ipairs(splitPath(cleaned)) do
		if name ~= "game" then
			local child = current:FindFirstChild(name)
			if not child then
				return nil
			end
			current = child
		end
	end
	return current
end

local function instancePath(instance)
	if instance == game then
		return "game"
	end
	return "game." .. instance:GetFullName()
end

local function decodeValue(value)
	if typeof(value) ~= "table" then
		return value
	end

	if value.x and value.y and value.z then
		return Vector3.new(value.x, value.y, value.z)
	end

	if value.r and value.g and value.b then
		return Color3.fromRGB(value.r, value.g, value.b)
	end

	if value.xScale and value.xOffset and value.yScale and value.yOffset then
		return UDim2.new(value.xScale, value.xOffset, value.yScale, value.yOffset)
	end

	if value.scale and value.offset then
		return UDim.new(value.scale, value.offset)
	end

	if value.enumType and value.name and Enum[value.enumType] then
		return Enum[value.enumType][value.name]
	end

	return value
end

local function encodeValue(value)
	local valueType = typeof(value)
	if valueType == "Vector3" then
		return { x = value.X, y = value.Y, z = value.Z }
	end
	if valueType == "Vector2" then
		return { x = value.X, y = value.Y }
	end
	if valueType == "Color3" then
		return {
			r = math.floor(value.R * 255),
			g = math.floor(value.G * 255),
			b = math.floor(value.B * 255),
		}
	end
	if valueType == "UDim2" then
		return {
			xScale = value.X.Scale,
			xOffset = value.X.Offset,
			yScale = value.Y.Scale,
			yOffset = value.Y.Offset,
		}
	end
	if valueType == "CFrame" then
		local components = { value:GetComponents() }
		return components
	end
	if valueType == "EnumItem" then
		return tostring(value)
	end
	if valueType == "Instance" then
		return instancePath(value)
	end
	if valueType == "string" or valueType == "number" or valueType == "boolean" or value == nil then
		return value
	end
	return tostring(value)
end

local COMMON_PROPERTIES = {
	"Name",
	"ClassName",
	"Parent",
	"Anchored",
	"CanCollide",
	"CanTouch",
	"CanQuery",
	"Transparency",
	"Material",
	"Color",
	"Size",
	"Position",
	"CFrame",
	"Text",
	"TextColor3",
	"TextSize",
	"BackgroundColor3",
	"Image",
}

local function serializeInstance(instance, depth, maxDepth)
	local node = {
		name = instance.Name,
		className = instance.ClassName,
		path = instancePath(instance),
		childCount = #instance:GetChildren(),
		children = {},
	}

	if depth < maxDepth then
		for _, child in ipairs(instance:GetChildren()) do
			table.insert(node.children, serializeInstance(child, depth + 1, maxDepth))
		end
	end

	return node
end

local function safeGetProperty(instance, property)
	local ok, value = pcall(function()
		return instance[property]
	end)
	if ok then
		return encodeValue(value)
	end
	return nil
end

local function setProperties(instance, properties)
	for property, value in pairs(properties or {}) do
		if property ~= "ClassName" and property ~= "Parent" then
			pcall(function()
				instance[property] = decodeValue(value)
			end)
		end
	end
end

local function makeUndo(instance, label)
	if not instance then
		return nil
	end
	local props = {}
	for _, property in ipairs(COMMON_PROPERTIES) do
		props[property] = safeGetProperty(instance, property)
	end
	return {
		label = label,
		path = instancePath(instance),
		className = instance.ClassName,
		properties = props,
	}
end

local function createInstance(params)
	local parent = resolvePath(params.parent or params.parentPath or "game.Workspace")
	if not parent then
		error("Parent not found: " .. tostring(params.parent or params.parentPath))
	end
	local instance = Instance.new(params.className or "Part")
	instance.Name = params.name or instance.ClassName
	setProperties(instance, params.properties or {})
	instance.Parent = parent
	Selection:Set({ instance })
	return {
		path = instancePath(instance),
		className = instance.ClassName,
		name = instance.Name,
	}
end

local function groupSelection(params)
	local selected = params.paths or {}
	if #selected == 0 then
		for _, item in ipairs(Selection:Get()) do
			table.insert(selected, instancePath(item))
		end
	end

	local parent = resolvePath(params.parent or "game.Workspace")
	if not parent then
		error("Group parent not found")
	end

	local folder = Instance.new("Folder")
	folder.Name = params.name or "RBC_Group"
	folder.Parent = parent
	for _, path in ipairs(selected) do
		local instance = resolvePath(path)
		if instance and instance ~= game then
			instance.Parent = folder
		end
	end
	Selection:Set({ folder })
	return { path = instancePath(folder), moved = #selected }
end

local function createRobloxPopUi(params)
	local playerGui = resolvePath("game.StarterGui")
	local gui = Instance.new("ScreenGui")
	gui.Name = params.name or "RBC_MainHUD"
	gui.ResetOnSpawn = false
	gui.IgnoreGuiInset = false
	gui.Parent = playerGui

	local scale = Instance.new("UIScale")
	scale.Scale = 1
	scale.Parent = gui

	local function stroke(parent, thickness)
		local uiStroke = Instance.new("UIStroke")
		uiStroke.Thickness = thickness or 3
		uiStroke.Color = Color3.fromRGB(20, 20, 30)
		uiStroke.Parent = parent
		return uiStroke
	end

	local function corner(parent, radius)
		local uiCorner = Instance.new("UICorner")
		uiCorner.CornerRadius = UDim.new(0, radius or 12)
		uiCorner.Parent = parent
		return uiCorner
	end

	local function label(parent, text, size, position, anchor, color)
		local item = Instance.new("TextLabel")
		item.BackgroundTransparency = 1
		item.Text = text
		item.Font = Enum.Font.FredokaOne
		item.TextScaled = true
		item.TextColor3 = color or Color3.fromRGB(255, 255, 255)
		item.TextStrokeTransparency = 0
		item.TextStrokeColor3 = Color3.fromRGB(18, 18, 24)
		item.Size = size
		item.Position = position
		item.AnchorPoint = anchor or Vector2.new(0, 0)
		item.Parent = parent
		local constraint = Instance.new("UITextSizeConstraint")
		constraint.MinTextSize = 12
		constraint.MaxTextSize = 34
		constraint.Parent = item
		return item
	end

	local function iconButton(parent, name, text, color, order)
		local button = Instance.new("ImageButton")
		button.Name = name
		button.BackgroundColor3 = color
		button.Size = UDim2.fromOffset(92, 76)
		button.LayoutOrder = order or 0
		button.AutoButtonColor = true
		button.Parent = parent
		corner(button, 11)
		stroke(button, 4)
		local icon = Instance.new("TextLabel")
		icon.Name = "IconFallback"
		icon.BackgroundTransparency = 1
		icon.Text = string.sub(text, 1, 1)
		icon.Font = Enum.Font.FredokaOne
		icon.TextScaled = true
		icon.TextColor3 = Color3.fromRGB(255, 255, 255)
		icon.TextStrokeTransparency = 0
		icon.Size = UDim2.new(1, 0, 0.62, 0)
		icon.Parent = button
		label(button, string.upper(text), UDim2.new(1, -8, 0, 24), UDim2.new(0, 4, 1, -28), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))
		return button
	end

	local topTimer = Instance.new("Frame")
	topTimer.Name = "TopEventTimer"
	topTimer.AnchorPoint = Vector2.new(0.5, 0)
	topTimer.Position = UDim2.new(0.5, 0, 0, 8)
	topTimer.Size = UDim2.fromOffset(390, 46)
	topTimer.BackgroundTransparency = 1
	topTimer.Parent = gui
	label(topTimer, "Next event in: 02:15", UDim2.new(1, 0, 1, 0), UDim2.fromScale(0, 0), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))

	local leftRail = Instance.new("Frame")
	leftRail.Name = "LeftIconRail"
	leftRail.BackgroundTransparency = 1
	leftRail.Position = UDim2.new(0, 8, 0.31, 0)
	leftRail.Size = UDim2.fromOffset(102, 330)
	leftRail.Parent = gui
	local leftLayout = Instance.new("UIListLayout")
	leftLayout.Padding = UDim.new(0, 8)
	leftLayout.SortOrder = Enum.SortOrder.LayoutOrder
	leftLayout.Parent = leftRail
	iconButton(leftRail, "ShopButton", "Shop", Color3.fromRGB(255, 160, 38), 1)
	iconButton(leftRail, "IndexButton", "Index", Color3.fromRGB(148, 72, 255), 2)
	iconButton(leftRail, "RewardsButton", "Rewards", Color3.fromRGB(255, 60, 75), 3)

	local currency = Instance.new("Frame")
	currency.Name = "CurrencyCounters"
	currency.BackgroundTransparency = 1
	currency.Position = UDim2.new(0, 10, 1, -118)
	currency.Size = UDim2.fromOffset(260, 104)
	currency.Parent = gui
	label(currency, "$13.93SX", UDim2.new(1, 0, 0, 42), UDim2.fromOffset(0, 28), Vector2.new(0, 0), Color3.fromRGB(33, 255, 36))
	label(currency, "5,471", UDim2.new(1, 0, 0, 34), UDim2.fromOffset(56, 0), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))
	label(currency, "Friend Boost: +10%", UDim2.new(1, 0, 0, 28), UDim2.fromOffset(0, 74), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))

	local hotbar = Instance.new("Frame")
	hotbar.Name = "BottomHotbar"
	hotbar.AnchorPoint = Vector2.new(0.5, 1)
	hotbar.Position = UDim2.new(0.5, 0, 1, -8)
	hotbar.Size = UDim2.new(0.58, 0, 0, 92)
	hotbar.BackgroundTransparency = 1
	hotbar.Parent = gui
	local grid = Instance.new("UIGridLayout")
	grid.CellSize = UDim2.new(0.125, -8, 1, 0)
	grid.CellPadding = UDim2.fromOffset(8, 0)
	grid.SortOrder = Enum.SortOrder.LayoutOrder
	grid.Parent = hotbar
	for index = 1, 8 do
		iconButton(hotbar, "Slot" .. index, tostring(index), Color3.fromRGB(65, 35, 18), index)
	end

	local shop = Instance.new("Frame")
	shop.Name = "ShopModal"
	shop.AnchorPoint = Vector2.new(0.5, 0.5)
	shop.Position = UDim2.fromScale(0.5, 0.5)
	shop.Size = UDim2.fromOffset(440, 470)
	shop.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	shop.Visible = params.includeShop ~= false
	shop.Parent = gui
	corner(shop, 18)
	stroke(shop, 4)
	local header = Instance.new("Frame")
	header.Name = "Header"
	header.BackgroundColor3 = Color3.fromRGB(255, 42, 52)
	header.Size = UDim2.new(1, 0, 0, 58)
	header.Parent = shop
	corner(header, 15)
	label(header, "SHOP", UDim2.new(0.7, 0, 1, 0), UDim2.fromOffset(18, 0), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))
	label(header, "X", UDim2.fromOffset(42, 42), UDim2.new(1, -54, 0, 8), Vector2.new(0, 0), Color3.fromRGB(255, 255, 255))
	local gridFrame = Instance.new("Frame")
	gridFrame.Name = "ItemGrid"
	gridFrame.BackgroundTransparency = 1
	gridFrame.Position = UDim2.fromOffset(20, 82)
	gridFrame.Size = UDim2.new(1, -40, 1, -102)
	gridFrame.Parent = shop
	local shopGrid = Instance.new("UIGridLayout")
	shopGrid.CellSize = UDim2.fromOffset(118, 90)
	shopGrid.CellPadding = UDim2.fromOffset(12, 12)
	shopGrid.Parent = gridFrame
	for index = 1, 9 do
		iconButton(gridFrame, "ShopItem" .. index, "Buy", Color3.fromRGB(255, 225, 55), index)
	end

	return {
		path = instancePath(gui),
		components = {
			"TopEventTimer",
			"LeftIconRail",
			"CurrencyCounters",
			"BottomHotbar",
			"ShopModal",
		},
	}
end

local function securityScan()
	local remotes = {}
	local scriptFindings = {}
	for _, instance in ipairs(game:GetDescendants()) do
		if instance:IsA("RemoteEvent") or instance:IsA("RemoteFunction") then
			table.insert(remotes, {
				path = instancePath(instance),
				className = instance.ClassName,
				risk = "review_required",
				note = "Ensure server validates every client argument and never trusts currency, damage, rewards, inventory, or progression."
			})
		elseif instance:IsA("LocalScript") or instance:IsA("Script") or instance:IsA("ModuleScript") then
			local ok, source = pcall(function()
				return instance.Source
			end)
			if ok and source then
				if string.find(source, "FireServer") or string.find(source, "InvokeServer") then
					table.insert(scriptFindings, {
						path = instancePath(instance),
						risk = "client_remote_call",
						note = "Review matching server contract, cooldown, and validation."
					})
				end
				if string.find(source, "OnServerEvent") and (string.find(source, "leaderstats") or string.find(source, "Cash") or string.find(source, "Coins")) then
					table.insert(scriptFindings, {
						path = instancePath(instance),
						risk = "possible_client_trusted_currency",
						note = "Server reward/currency writes must be recomputed server-side."
					})
				end
			end
		end
	end
	return {
		remotes = remotes,
		scriptFindings = scriptFindings,
	}
end

local function handleCommand(command)
	local tool = command.tool
	local action = command.action
	local params = command.params or {}

	if tool == "rbc_system" then
		return {
			sessionId = SESSION_ID,
			version = PLUGIN_VERSION,
			running = running,
			placeId = game.PlaceId,
			placeName = game.Name,
		}
	end

	if tool == "rbc_explorer" then
		if action == "selection" then
			local selection = {}
			for _, instance in ipairs(Selection:Get()) do
				table.insert(selection, serializeInstance(instance, 0, 1))
			end
			return { selection = selection }
		end
		local root = resolvePath(params.root or "game")
		if not root then
			error("Explorer root not found")
		end
		return serializeInstance(root, 0, params.maxDepth or 4)
	end

	if tool == "rbc_instances" then
		if action == "create" or action == "create_tree" then
			return createInstance(params)
		elseif action == "delete" then
			local instance = resolvePath(params.path)
			if not instance or instance == game then
				error("Delete target not found")
			end
			local undo = makeUndo(instance, "delete")
			instance:Destroy()
			return { deleted = params.path, undo = undo }
		elseif action == "move" then
			local instance = resolvePath(params.path)
			local parent = resolvePath(params.parent or params.newParent)
			if not instance or not parent then
				error("Move target or parent not found")
			end
			local undo = makeUndo(instance, "move")
			instance.Parent = parent
			return { path = instancePath(instance), undo = undo }
		elseif action == "anchor" then
			local targets = params.paths or { params.path }
			local changed = {}
			for _, path in ipairs(targets) do
				local instance = resolvePath(path)
				if instance and instance:IsA("BasePart") then
					instance.Anchored = params.anchored ~= false
					table.insert(changed, instancePath(instance))
				end
			end
			return { changed = changed }
		elseif action == "scale" then
			local instance = resolvePath(params.path)
			if not instance then
				error("Scale target not found")
			end
			local factor = params.factor or 1
			if instance:IsA("BasePart") then
				instance.Size = instance.Size * factor
			elseif instance:IsA("Model") then
				instance:ScaleTo(factor)
			end
			return { path = instancePath(instance), factor = factor }
		elseif action == "group" then
			return groupSelection(params)
		elseif action == "duplicate" then
			local instance = resolvePath(params.path)
			if not instance then
				error("Duplicate target not found")
			end
			local clone = instance:Clone()
			clone.Name = params.name or (instance.Name .. "_Copy")
			clone.Parent = resolvePath(params.parent or instance.Parent:GetFullName()) or instance.Parent
			Selection:Set({ clone })
			return { path = instancePath(clone) }
		elseif action == "pivot" or action == "snap" or action == "align" then
			local instance = resolvePath(params.path)
			if not instance then
				error("Target not found")
			end
			local position = decodeValue(params.position or params.targetPosition)
			if typeof(position) == "Vector3" then
				if instance:IsA("Model") then
					instance:PivotTo(CFrame.new(position))
				elseif instance:IsA("BasePart") then
					instance.Position = position
				end
			end
			return { path = instancePath(instance), position = encodeValue(position) }
		end
	end

	if tool == "rbc_properties" then
		local instance = resolvePath(params.path)
		if not instance then
			error("Property target not found")
		end
		if action == "get" then
			return { value = safeGetProperty(instance, params.property) }
		elseif action == "get_all" then
			local values = {}
			for _, property in ipairs(COMMON_PROPERTIES) do
				values[property] = safeGetProperty(instance, property)
			end
			return values
		elseif action == "set" then
			local undo = makeUndo(instance, "property_set")
			instance[params.property] = decodeValue(params.value)
			return { path = instancePath(instance), property = params.property, value = safeGetProperty(instance, params.property), undo = undo }
		elseif action == "set_many" then
			local undo = makeUndo(instance, "property_set_many")
			setProperties(instance, params.properties or {})
			return { path = instancePath(instance), undo = undo }
		elseif action == "tag_add" then
			CollectionService:AddTag(instance, params.tag)
			return { tags = CollectionService:GetTags(instance) }
		elseif action == "tag_remove" then
			CollectionService:RemoveTag(instance, params.tag)
			return { tags = CollectionService:GetTags(instance) }
		elseif action == "tag_list" then
			return { tags = CollectionService:GetTags(instance) }
		elseif action == "attribute_set" then
			instance:SetAttribute(params.name, decodeValue(params.value))
			return { attributes = instance:GetAttributes() }
		elseif action == "attribute_delete" then
			instance:SetAttribute(params.name, nil)
			return { attributes = instance:GetAttributes() }
		end
	end

	if tool == "rbc_scripts" then
		if action == "create" then
			local parent = resolvePath(params.parent or "game.ServerScriptService")
			if not parent then
				error("Script parent not found")
			end
			local className = params.className or "Script"
			local scriptInstance = Instance.new(className)
			scriptInstance.Name = params.name or "RBC_Script"
			scriptInstance.Source = params.source or ""
			scriptInstance.Parent = parent
			return { path = instancePath(scriptInstance) }
		end

		local instance = resolvePath(params.path)
		if not instance then
			error("Script target not found")
		end

		if action == "get_source" then
			return { path = instancePath(instance), source = instance.Source }
		elseif action == "set_source" then
			local undo = {
				path = instancePath(instance),
				source = instance.Source,
			}
			instance.Source = params.source or ""
			return { path = instancePath(instance), undo = undo }
		elseif action == "delete" then
			local undo = {
				path = instancePath(instance),
				className = instance.ClassName,
				source = instance.Source,
			}
			instance:Destroy()
			return { deleted = params.path, undo = undo }
		end
	end

	if tool == "rbc_luau" and action == "execute" then
		if typeof(loadstring) ~= "function" then
			error("loadstring is not available in this Studio/plugin context")
		end
		local fn, compileError = loadstring(params.source or "")
		if not fn then
			error(compileError)
		end
		local ok, result = pcall(fn)
		if not ok then
			error(result)
		end
		return { result = encodeValue(result) }
	end

	if tool == "rbc_testing" then
		if action == "logs" then
			return { note = "Studio output logs are posted to the RBC dashboard when plugin handlers emit logs." }
		end
		if action == "find_unanchored" then
			local parts = {}
			for _, instance in ipairs(game:GetDescendants()) do
				if instance:IsA("BasePart") and not instance.Anchored then
					table.insert(parts, instancePath(instance))
				end
			end
			return { unanchored = parts }
		end
		if action == "scan_broken_scripts" then
			local findings = {}
			for _, instance in ipairs(game:GetDescendants()) do
				if instance:IsA("Script") or instance:IsA("LocalScript") or instance:IsA("ModuleScript") then
					local ok, source = pcall(function()
						return instance.Source
					end)
					if not ok or source == nil then
						table.insert(findings, { path = instancePath(instance), issue = "source_unreadable" })
					end
				end
			end
			return { findings = findings }
		end
		return {
			note = "Playtest control is scaffolded in the MCP contract. Use Studio controls for this MVP plugin build.",
			isRunning = RunService:IsRunning(),
		}
	end

	if tool == "rbc_ui" then
		return createRobloxPopUi(params)
	end

	if tool == "rbc_security" then
		return securityScan()
	end

	if tool == "rbc_undo" and action == "revert" then
		return {
			note = "Undo command received. Full object restoration is stored in the server contract and should be expanded per mutation type.",
			payload = params,
		}
	end

	return {
		note = "Command contract is implemented on the MCP server; Studio-side handler is pending for this action.",
		tool = tool,
		action = action,
		params = params,
	}
end

local function hello()
	return request("POST", "/bridge/hello", {
		sessionId = SESSION_ID,
		pluginVersion = PLUGIN_VERSION,
		placeId = tostring(game.PlaceId),
		placeName = game.Name,
		jobId = game.JobId,
		studioVersion = version(),
	})
end

local function sendResult(commandId, ok, data, errorMessage)
	request("POST", "/bridge/result", {
		sessionId = SESSION_ID,
		commandId = commandId,
		ok = ok,
		data = data,
		error = errorMessage,
		undo = data and data.undo or nil,
	})
end

local function pollLoop()
	hello()
	log("info", "RBC MCP Studio plugin connected", { sessionId = SESSION_ID })
	while running do
		local ok, response = request("GET", "/bridge/next?sessionId=" .. HttpService:UrlEncode(SESSION_ID))
		if ok and response and response.command then
			local command = response.command
			local success, result = pcall(function()
				return handleCommand(command)
			end)
			if success then
				sendResult(command.id, true, result, nil)
			else
				sendResult(command.id, false, nil, tostring(result))
				log("error", tostring(result), { command = command })
			end
		end
		task.wait(POLL_SECONDS)
	end
	log("info", "RBC MCP Studio plugin stopped", { sessionId = SESSION_ID })
end

toggleButton.Click:Connect(function()
	running = not running
	toggleButton:SetActive(running)
	if running then
		task.spawn(pollLoop)
	end
end)

print("[RBC MCP] Plugin loaded. Click the RBC MCP toolbar button to connect to " .. SERVER_URL)
