--!nonstrict
--[[
	X.GPT PRO - Plugin Roblox Studio

	Fichier unique : copie ce fichier "X.GPT PRO.plugin.lua" directement dans
	  %LOCALAPPDATA%\Roblox\Plugins\
	puis ouvre Roblox Studio -> onglet Plugins -> bouton "X.GPT PRO".

	Se connecte au pont X.GPT PRO (http://127.0.0.1:8765), lance
	automatiquement par l'app X.GPT PRO, et execute les commandes envoyees
	par l'IA directement dans le place ouvert.
]]

local success, err = pcall(function()

local HttpService = game:GetService("HttpService")
local BRIDGE_URL = "http://127.0.0.1:8765"
local HEARTBEAT_INTERVAL = 3
local POLL_INTERVAL = 1
local running = false
local bridgeOnline = false
local httpBlocked = false

-- Les plugins n'ont PAS besoin d'activer HttpEnabled : Studio autorise les
-- requetes HTTP des plugins directement (comme le plugin Servat AI).
-- On detecte juste si Studio bloque quand meme les requetes.
local function post(path, body)
	local ok, result = pcall(function()
		return HttpService:PostAsync(BRIDGE_URL .. path, HttpService:JSONEncode(body), Enum.HttpContentType.ApplicationJson)
	end)
	return ok, result
end

local function get(path)
	local ok, result = pcall(function()
		return HttpService:GetAsync(BRIDGE_URL .. path)
	end)
	if ok then
		ok, result = pcall(function()
			return HttpService:JSONDecode(result)
		end)
	end
	return ok, result
end

-- ---------------------------------------------------------------------------
-- Barre d'outils + panneau (API 2026, avec repli sur l'ancienne API)
-- ---------------------------------------------------------------------------

local toolbar = plugin:CreateToolbar("X.GPT PRO")
local toggleBtn = toolbar:CreateButton("X.GPT PRO", "Ouvrir/fermer le panneau X.GPT PRO", "")

local gui
local ok = false

-- Nouvelle API 2026
if DockWidgetPluginGuiInfo and plugin.CreateDockWidgetPluginGuiAsync then
	ok = pcall(function()
		local info = DockWidgetPluginGuiInfo.new(
			Enum.InitialDockState.Right,
			true,  -- InitialEnabled : le panneau s'ouvre au chargement
			false, -- InitialEnabledShouldOverrideRestore
			320,   -- FloatingXSize
			460,   -- FloatingYSize
			280,   -- MinWidth
			200    -- MinHeight
		)
		gui = plugin:CreateDockWidgetPluginGuiAsync("XGPTPRO_Main", info)
	end)
end

-- Ancienne API (repli)
if not ok or not gui then
	ok = pcall(function()
		local info = DockWidgetPluginGuiInfo.new(
			Enum.InitialDockState.Right,
			true,
			false,
			320,
			460
		)
		gui = plugin:CreateDockWidgetPluginGui("XGPTPRO_Main", info)
	end)
end

if not gui then
	print("[X.GPT PRO] ERREUR: impossible de creer l'interface")
	return
end
gui.Title = "X.GPT PRO"
gui.Name = "XGPTPRO_Main"

-- ---------------------------------------------------------------------------
-- Interface noire
-- ---------------------------------------------------------------------------

local BLACK = Color3.fromRGB(8, 8, 10)
local BLACK2 = Color3.fromRGB(16, 16, 20)
local WHITE = Color3.fromRGB(230, 230, 238)
local GREY = Color3.fromRGB(120, 120, 132)
local GREEN = Color3.fromRGB(34, 180, 100)
local RED = Color3.fromRGB(210, 60, 60)
local YELLOW = Color3.fromRGB(250, 204, 21)
local PURPLE = Color3.fromRGB(157, 140, 255)

local frame = Instance.new("Frame")
frame.Name = "XGPTPRO_Main"
frame.Size = UDim2.new(1, 0, 1, 0)
frame.BackgroundColor3 = BLACK
frame.BorderSizePixel = 0
frame.Parent = gui

local title = Instance.new("TextLabel")
title.Name = "Title"
title.Size = UDim2.new(1, 0, 0, 40)
title.BackgroundTransparency = 1
title.Font = Enum.Font.GothamBold
title.Text = "X.GPT PRO"
title.TextColor3 = PURPLE
title.TextSize = 20
title.Parent = frame

local placeLbl = Instance.new("TextLabel")
placeLbl.Name = "Place"
placeLbl.Position = UDim2.new(0, 8, 0, 42)
placeLbl.Size = UDim2.new(1, -16, 0, 20)
placeLbl.BackgroundTransparency = 1
placeLbl.Font = Enum.Font.Gotham
placeLbl.Text = "Place : " .. tostring(game.Name)
placeLbl.TextColor3 = GREY
placeLbl.TextSize = 12
placeLbl.TextXAlignment = Enum.TextXAlignment.Left
placeLbl.Parent = frame

local status = Instance.new("TextLabel")
status.Name = "Status"
status.Position = UDim2.new(0, 8, 0, 64)
status.Size = UDim2.new(1, -16, 0, 34)
status.BackgroundTransparency = 1
status.Font = Enum.Font.GothamBold
status.Text = "Deconnecte"
status.TextColor3 = GREY
status.TextWrapped = true
status.TextSize = 13
status.Parent = frame

local dot = Instance.new("Frame")
dot.Name = "Dot"
dot.Position = UDim2.new(0, 12, 0, 76)
dot.Size = UDim2.new(0, 10, 0, 10)
dot.BackgroundColor3 = GREY
dot.BorderSizePixel = 0
dot.Parent = frame

-- Bouton unique : CONNECTER (vert) / DECONNECTER (rouge)
local connectBtn = Instance.new("TextButton")
connectBtn.Name = "ConnectButton"
connectBtn.Position = UDim2.new(0, 8, 0, 104)
connectBtn.Size = UDim2.new(1, -16, 0, 48)
connectBtn.BackgroundColor3 = GREEN
connectBtn.BorderSizePixel = 0
connectBtn.Font = Enum.Font.GothamBold
connectBtn.Text = "CONNECTER"
connectBtn.TextColor3 = WHITE
connectBtn.TextSize = 16
connectBtn.Parent = frame

local log = Instance.new("TextLabel")
log.Name = "Log"
log.Position = UDim2.new(0, 8, 0, 160)
log.Size = UDim2.new(1, -16, 1, -168)
log.BackgroundColor3 = BLACK2
log.BorderSizePixel = 0
log.Font = Enum.Font.Code
log.Text = "Pret. Appuie sur CONNECTER pour lier l'IA a ce place."
log.TextColor3 = Color3.fromRGB(170, 170, 185)
log.TextXAlignment = Enum.TextXAlignment.Left
log.TextYAlignment = Enum.TextYAlignment.Top
log.TextWrapped = true
log.TextSize = 12
log.Parent = frame

local function logLine(text)
	log.Text = text .. "\n" .. log.Text
end

-- ---------------------------------------------------------------------------
-- Etat de l'interface
-- ---------------------------------------------------------------------------

local function updateUI()
	if running and bridgeOnline then
		connectBtn.Text = "DECONNECTER"
		connectBtn.BackgroundColor3 = RED
		status.Text = "Connecte a X.GPT PRO"
		status.TextColor3 = GREEN
		dot.BackgroundColor3 = GREEN
	elseif running and httpBlocked then
		connectBtn.Text = "CONNECTER"
		connectBtn.BackgroundColor3 = GREEN
		status.Text = "HTTP bloque par Studio - active les requetes HTTP"
		status.TextColor3 = RED
		dot.BackgroundColor3 = RED
	elseif running then
		connectBtn.Text = "CONNECTER"
		connectBtn.BackgroundColor3 = GREEN
		status.Text = "Pont introuvable - ouvre l'app X.GPT PRO"
		status.TextColor3 = YELLOW
		dot.BackgroundColor3 = YELLOW
	else
		connectBtn.Text = "CONNECTER"
		connectBtn.BackgroundColor3 = GREEN
		status.Text = "Deconnecte"
		status.TextColor3 = GREY
		dot.BackgroundColor3 = GREY
	end
end

-- ---------------------------------------------------------------------------
-- Coeur de connexion
-- ---------------------------------------------------------------------------

local StudioService = game:GetService("StudioService")

local function isGameplayActive()
	local okRes, active = pcall(function()
		return StudioService:IsGameplayActive()
	end)
	return okRes and active == true
end

-- Boucle qui garde le pont a jour toutes les 3s : si l'app s'ouvre apres,
-- le bouton passe tout seul sur DECONNECTER.
local function managerLoop()
	while running do
		local okRes, errMsg = post("/status", {
			connected = true,
			place = tostring(game.Name),
			version = "4.0.0",
		})
		bridgeOnline = okRes == true
		httpBlocked = false
		if not okRes and tostring(errMsg or ""):find("Http", 1, true) then
			httpBlocked = true
		end
		updateUI()
		task.wait(HEARTBEAT_INTERVAL)
	end
end

local function pollLoop()
	while running do
		if isGameplayActive() then
			task.wait(0.5)
		else
			local okRes, data = get("/commands")
			if okRes and type(data) == "table" and type(data.commands) == "table" then
				for _, command in ipairs(data.commands) do
					if not running then
						break
					end
					logLine("Commande recue: " .. tostring(command.type))
					local resultOk, output = executeCommand(command)
					post("/result", {
						id = command.id,
						ok = resultOk,
						output = output,
					})
				end
			end
			task.wait(POLL_INTERVAL)
		end
	end
end

function connect()
	if running then
		return
	end
	running = true
	bridgeOnline = false
	httpBlocked = false
	updateUI()
	logLine("Connexion au pont " .. BRIDGE_URL .. " ...")
	task.spawn(managerLoop)
	task.spawn(pollLoop)
end

function disconnect()
	running = false
	bridgeOnline = false
	post("/status", { connected = false })
	updateUI()
	logLine("Deconnecte.")
end

-- ---------------------------------------------------------------------------
-- Annulation (Ctrl+Z) : chaque action de l'IA est enregistree dans l'historique
-- ---------------------------------------------------------------------------

local ChangeHistoryService = game:GetService("ChangeHistoryService")

local function beginRecording(name)
	local okRec, recording = pcall(function()
		return ChangeHistoryService:TryBeginRecording(name)
	end)
	return okRec and recording or nil
end

local function commitRecording(recording)
	if recording then
		pcall(function()
			ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
		end)
	end
end

-- ---------------------------------------------------------------------------
-- Execution des commandes dans le place
-- ---------------------------------------------------------------------------

-- Declare a l'avance : implemente plus bas (resout un chemin vers une instance)
local resolveInstance

local function normalizeValue(value)
	if type(value) ~= "table" then
		return value
	end
	if value.__type == "Vector3" then
		return Vector3.new(value.x or 0, value.y or 0, value.z or 0)
	end
	if value.__type == "Color3" then
		return Color3.new(value.r or 0, value.g or 0, value.b or 0)
	end
	if value.x ~= nil and value.y ~= nil and value.z ~= nil then
		return Vector3.new(value.x, value.y, value.z)
	end
	if value.r ~= nil and value.g ~= nil and value.b ~= nil then
		return Color3.new(value.r, value.g, value.b)
	end
	return value
end

local function applyProperties(instance, properties)
	if type(properties) ~= "table" then
		return
	end
	for key, value in pairs(properties) do
		if key == "Parent" and type(value) == "string" then
			local resolved = resolveInstance(value)
			if resolved then
				instance.Parent = resolved
			end
		else
			local okRes, result = pcall(function()
				instance[key] = normalizeValue(value)
			end)
			if not okRes then
				warn("[X.GPT PRO] propriete ignoree ", key, result)
			end
		end
	end
end

local function getWorkspaceParent(parentName)
	local workspace = game:GetService("Workspace")
	if not parentName or parentName == "" then
		return workspace
	end
	local folder = workspace:FindFirstChild(parentName)
	if not folder then
		folder = Instance.new("Folder")
		folder.Name = parentName
		folder.Parent = workspace
	end
	return folder
end

local function createPart(args)
	local recording = beginRecording("X.GPT PRO: creer part")
	local part = Instance.new("Part")
	part.Name = args.name or "XGPTPro_Part"
	applyProperties(part, args.properties)
	part.Parent = getWorkspaceParent(args.parent)
	commitRecording(recording)
	return part:GetFullName()
end

local function createFolder(args)
	local recording = beginRecording("X.GPT PRO: creer dossier")
	local folder = Instance.new("Folder")
	folder.Name = args.name or "XGPTPro_Folder"
	applyProperties(folder, args.properties)
	folder.Parent = getWorkspaceParent(args.parent)
	commitRecording(recording)
	return folder:GetFullName()
end

local function createModel(args)
	local recording = beginRecording("X.GPT PRO: creer modele")
	local model = Instance.new("Model")
	model.Name = args.name or "XGPTPro_Model"
	applyProperties(model, args.properties)
	model.Parent = getWorkspaceParent(args.parent)
	if type(args.parts) == "table" then
		for _, partArgs in ipairs(args.parts) do
			local part = Instance.new("Part")
			part.Name = partArgs.name or "Part"
			applyProperties(part, partArgs.properties)
			part.Parent = model
		end
	end
	commitRecording(recording)
	return model:GetFullName()
end

local SCRIPT_CONTAINERS = {
	ServerScriptService = function()
		return game:GetService("ServerScriptService")
	end,
	ServerStorage = function()
		return game:GetService("ServerStorage")
	end,
	ReplicatedStorage = function()
		return game:GetService("ReplicatedStorage")
	end,
	Workspace = function()
		return game:GetService("Workspace")
	end,
	StarterPlayerScripts = function()
		local sp = game:GetService("StarterPlayer")
		local container = sp:FindFirstChild("StarterPlayerScripts")
		if not container then
			container = Instance.new("StarterPlayerScripts")
			container.Parent = sp
		end
		return container
	end,
}

local function createScript(args)
	local isClient = args.client == true
	local container
	if args.container then
		local getter = SCRIPT_CONTAINERS[args.container]
		if not getter then
			return nil, "Container inconnu: " .. tostring(args.container) .. " (accepte: " .. table.concat({ "ServerScriptService", "ServerStorage", "ReplicatedStorage", "Workspace", "StarterPlayerScripts" }, ", ") .. ")"
		end
		container = getter()
	else
		container = isClient
			and (game:GetService("StarterPlayer"):FindFirstChild("StarterPlayerScripts") or game:GetService("StarterPlayer"))
			or game:GetService("ServerScriptService")
	end
	local recording = beginRecording("X.GPT PRO: creer script")
	local script = Instance.new(isClient and "LocalScript" or "Script")
	script.Name = args.name or "XGPTPRO_Run"
	script.Source = args.code or "-- script vide"
	script.Parent = container
	commitRecording(recording)
	return script:GetFullName() .. " (s'execute quand tu appuies sur Play)", nil
end

-- ---------------------------------------------------------------------------
-- Inspection du place / scripts existants
-- ---------------------------------------------------------------------------

-- Resout un chemin du style "Workspace.MonModel.Script", "game.Workspace.X"
-- ou "StarterPlayer.StarterPlayerScripts.MonScript".
function resolveInstance(path)
	if type(path) ~= "string" or path == "" then
		return nil
	end
	local clean = (path:gsub("^game%.", "")):gsub("^%.", "")
	local current = game
	for part in string.gmatch(clean, "[^%.]+") do
		local found = current:FindFirstChild(part)
		if not found then
			return nil
		end
		current = found
	end
	return current
end

local function inspectCommand()
	local lines = {}
	local function add(text)
		if #lines < 70 then
			table.insert(lines, text)
		end
	end
	local containers = {
		game:GetService("Workspace"),
		game:GetService("ServerScriptService"),
		game:GetService("ServerStorage"),
		game:GetService("ReplicatedStorage"),
		game:GetService("Lighting"),
	}
	local starterScripts = game:GetService("StarterPlayer"):FindFirstChild("StarterPlayerScripts")
	if starterScripts then
		table.insert(containers, starterScripts)
	end
	for _, container in ipairs(containers) do
		add(container:GetFullName() .. ":")
		local children = container:GetChildren()
		if #children == 0 then
			add("  (vide)")
		end
		for _, child in ipairs(children) do
			if #lines >= 70 then
				break
			end
			add("  " .. child.Name .. " (" .. child.ClassName .. ")")
		end
	end
	add("Scripts presents dans le place:")
	for _, child in ipairs(game:GetService("Workspace"):GetDescendants()) do
		if #lines >= 70 then
			break
		end
		if child:IsA("LuaSourceContainer") then
			add("  " .. child:GetFullName())
		end
	end
	return true, table.concat(lines, "\n")
end

local function readScriptCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Script introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins exacts."
	end
	if not instance:IsA("LuaSourceContainer") then
		return false, instance:GetFullName() .. " n'est pas un script (" .. instance.ClassName .. ")."
	end
	return true, instance:GetFullName() .. "\n\n" .. tostring(instance.Source)
end

local function updateScriptCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Script introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins exacts."
	end
	if not instance:IsA("LuaSourceContainer") then
		return false, instance:GetFullName() .. " n'est pas un script (" .. instance.ClassName .. ")."
	end
	local recording = beginRecording("X.GPT PRO: modifier script")
	instance.Source = args.code or ""
	commitRecording(recording)
	return true, "Mis a jour: " .. instance:GetFullName() .. " (" .. #tostring(args.code or "") .. " caracteres)"
end

-- ---------------------------------------------------------------------------
-- Boite a outils Roblox (recherche catalogue + insertion par id)
-- ---------------------------------------------------------------------------

local CATALOG_URL = "https://catalog.roblox.com/v1/search/items/details"
local INSERTABLE_TYPES = {
	[10] = "Modele",
	[11] = "Mesh",
	[13] = "Decal",
}

local function toolboxSearchCommand(args)
	local query = tostring(args.query or "")
	if query == "" then
		return false, "Donne un mot-cle en anglais a chercher (ex: \"tree\", \"rock\", \"house\", \"door\")."
	end
	local url = CATALOG_URL .. "?Category=3&Limit=30&sortType=Relevance&Keyword=" .. HttpService:UrlEncode(query)
	local okGet, body = pcall(function()
		return HttpService:GetAsync(url)
	end)
	if not okGet then
		return false, "Recherche impossible: " .. tostring(body) .. ". Verifie dans Studio que les requetes HTTP du plugin sont autorisees."
	end
	local okDecode, data = pcall(function()
		return HttpService:JSONDecode(body)
	end)
	if not okDecode or type(data) ~= "table" or type(data.data) ~= "table" then
		return false, "Reponse Roblox inattendue."
	end
	local candidates = {}
	for _, item in ipairs(data.data) do
		local label = INSERTABLE_TYPES[item.assetType]
		if label and (item.price == nil or item.price == 0) then
			table.insert(candidates, item)
		end
	end
	-- Trier par popularite : les assets les plus favoris sont les plus beaux/fiables
	table.sort(candidates, function(a, b)
		return (a.favoriteCount or 0) > (b.favoriteCount or 0)
	end)
	local results = {}
	for i = 1, math.min(12, #candidates) do
		local item = candidates[i]
		table.insert(results, string.format(
			"  id=%d | %s | %s | par %s | \u{2764} %d | gratuit",
			item.id,
			tostring(item.name):gsub("\n", " "),
			INSERTABLE_TYPES[item.assetType],
			tostring(item.creatorName or "?"),
			item.favoriteCount or 0
		))
	end
	if #results == 0 then
		return true, "Aucun asset gratuit trouve pour \"" .. query .. "\". Essaie d'autres mots-cles en anglais (ex: \"tree\", \"rock\", \"house\", \"door\", \"lamp\", \"fence\", \"sign\", \"wall\")."
	end
	return true, "Boite a outils - resultats gratuits pour \"" .. query .. "\" (tries par popularite):\n" .. table.concat(results, "\n")		.. "\n\nPrefere les assets avec le plus de \u{2764} et utilise roblox_toolbox_insert avec l'id pour inserer."
end

-- ---------------------------------------------------------------------------
-- Test de la map (Play Solo automatique + capture des erreurs)
-- ---------------------------------------------------------------------------

local LogService = game:GetService("LogService")

local function testPlayCommand(args)
	local seconds = math.clamp(tonumber(args.seconds) or 8, 3, 30)
	local okSvc, sts = pcall(function()
		return game:GetService("StudioTestService")
	end)
	if not okSvc or not sts or not sts.ExecutePlayModeAsync then
		return false, "StudioTestService indisponible (Roblox Studio trop ancien). Mets a jour Roblox Studio."
	end
	if isGameplayActive() then
		return false, "Une simulation est deja en cours. Arrete le Play en cours puis reessaie."
	end
	local before = #LogService:GetLogHistory()
	-- Script temporaire injecte dans la session de test : il attend, collecte
	-- les erreurs/avertissements, puis termine la session (EndTest).
	local runner = Instance.new("Script")
	runner.Name = "XGPTPRO_TestRunner_" .. tostring(os.time())
	runner.Source = string.format([==[
local StudioTestService = game:GetService("StudioTestService")
local LogService = game:GetService("LogService")
local startIndex = #LogService:GetLogHistory()
task.wait(%d)
local history = LogService:GetLogHistory()
local problems = {}
for i = startIndex + 1, #history do
	local entry = history[i]
	if entry and (entry.messageType == Enum.MessageType.OutputError or entry.messageType == Enum.MessageType.OutputWarning) then
		table.insert(problems, tostring(entry.message or ""))
	end
end
StudioTestService:EndTest(table.concat(problems, "\n"))
]==], seconds)
	runner.Parent = game:GetService("ServerScriptService")

	local okRun, result = pcall(function()
		return sts:ExecutePlayModeAsync("xgpt-pro-test")
	end)
	runner:Destroy()

	-- Cote plugin (mode edition) : ce que le script de test n'a pas vu (ex. erreurs client)
	local history = LogService:GetLogHistory()
	local pluginProblems = {}
	for i = before + 1, #history do
		local entry = history[i]
		if entry and (entry.messageType == Enum.MessageType.OutputError or entry.messageType == Enum.MessageType.OutputWarning) then
			table.insert(pluginProblems, tostring(entry.message or ""))
		end
	end

	-- Fusion + dedoublonnage
	local seen = {}
	local merged = {}
	for _, message in ipairs({ tostring(result or ""), table.concat(pluginProblems, "\n") }) do
		if message ~= "" then
			for line in string.gmatch(message, "[^\r\n]+") do
				if not seen[line] then
					seen[line] = true
					table.insert(merged, line)
				end
			end
		end
	end

	if not okRun then
		return false, "Echec du lancement de la map: " .. tostring(result)
	end
	if #merged == 0 then
		return true, "Test termine sans erreur ni avertissement sur " .. seconds .. "s. La map se lance correctement."
	end
	local lines = {}
	for i = 1, math.min(30, #merged) do
		table.insert(lines, "  - " .. merged[i])
	end
	if #merged > 30 then
		table.insert(lines, "  ... et " .. (#merged - 30) .. " autre(s)")
	end
	return true, "Test termine avec " .. #merged .. " erreur(s)/avertissement(s) sur " .. seconds .. "s:\n" .. table.concat(lines, "\n") .. "\n\nCorrige avec roblox_update_script puis reteste."
end

local function toolboxInsertCommand(args)
	local assetId = tonumber(tostring(args.assetId or ""):match("%d+"))
	if not assetId then
		return false, "Donne un assetId numerique (ex: 12109814819)."
	end
	local okObjects, objects = pcall(function()
		return game:GetObjects("rbxassetid://" .. assetId)
	end)
	if not okObjects then
		return false, "Insertion impossible (asset payant, supprime, ou non accessible): " .. tostring(objects)
	end
	if type(objects) ~= "table" or #objects == 0 then
		return false, "L'asset " .. assetId .. " ne contient rien d'inserable."
	end
	local parent = getWorkspaceParent(args.parent)
	local recording = beginRecording("X.GPT PRO: inserer depuis la boite a outils")
	local names = {}
	for _, obj in ipairs(objects) do
		obj.Parent = parent
		table.insert(names, obj:GetFullName())
	end
	commitRecording(recording)
	return true, "Insere depuis la Boite a outils: " .. table.concat(names, ", ")
end

-- ---------------------------------------------------------------------------
-- Edition des objets existants : focus, suppression, renommage, proprietes,
-- terrain
-- ---------------------------------------------------------------------------

local Selection = game:GetService("Selection")

local function objectPosition(instance)
	if instance:IsA("BasePart") then
		return instance.Position
	end
	local okPivot, pivot = pcall(function()
		return instance:GetPivot().Position
	end)
	if okPivot and pivot then
		return pivot
	end
	local part = instance:FindFirstChildWhichIsA("BasePart", true)
	if part then
		return part.Position
	end
	return nil
end

local function focusCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	pcall(function()
		Selection:Set({ instance })
	end)
	local camera = workspace.CurrentCamera
	local position = objectPosition(instance)
	if camera and position then
		pcall(function()
			camera.CFrame = CFrame.lookAt(position + Vector3.new(0, 20, 40), position)
		end)
	end
	return true, "Selectionne et centre la camera sur: " .. instance:GetFullName()
end

local function deleteCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	if instance == game or instance.Parent == game then
		return false, "Refuse de supprimer un service de base (" .. instance:GetFullName() .. ")."
	end
	local fullName = instance:GetFullName()
	local recording = beginRecording("X.GPT PRO: supprimer " .. instance.Name)
	instance:Destroy()
	commitRecording(recording)
	return true, "Supprime: " .. fullName
end

local function renameCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	local newName = tostring(args.newName or ""):gsub("[^%w%p%s]", "")
	if newName == "" then
		return false, "Donne un nouveau nom (newName)."
	end
	local oldName = instance.Name
	local recording = beginRecording("X.GPT PRO: renommer " .. oldName)
	instance.Name = newName
	commitRecording(recording)
	return true, "Renomme: " .. oldName .. " -> " .. instance:GetFullName()
end

local function setPropertiesCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	local recording = beginRecording("X.GPT PRO: modifier proprietes")
	applyProperties(instance, args.properties)
	commitRecording(recording)
	return true, "Proprietes modifiees sur: " .. instance:GetFullName()
end

local function terrainCommand(args)
	local materialName = tostring(args.material or "Grass")
	local material = Enum.Material[materialName]
	if not material then
		return false, "Materiau inconnu: " .. materialName .. " (ex: Grass, Water, Sand, Rock, Ground, Slate, Snow, Ice, Concrete)."
	end
	local pos = args.position or {}
	local size = args.size or {}
	local center = Vector3.new(pos.x or 0, pos.y or 0, pos.z or 0)
	local sizeVec = Vector3.new(size.x or 100, size.y or 4, size.z or 100)
	pcall(function()
		workspace.Terrain:FillBlock(CFrame.new(center), sizeVec, material)
	end)
	return true, "Terrain applique: " .. materialName .. " (" .. tostring(sizeVec) .. ") centre sur " .. tostring(center)
end

-- Parent accepte un chemin complet ("StarterGui.MonGui") ou un nom de dossier
-- sous Workspace (cree si absent).
local function getParentFromArg(parentArg)
	if not parentArg or parentArg == "" then
		return nil
	end
	if parentArg:find("%.") then
		local resolved = resolveInstance(parentArg)
		if resolved then
			return resolved
		end
	end
	local knownServices = {
		ServerScriptService = true,
		ServerStorage = true,
		ReplicatedStorage = true,
		Workspace = true,
		StarterPlayer = true,
		StarterPack = true,
		StarterGui = true,
		Lighting = true,
		SoundService = true,
	}
	if knownServices[parentArg] then
		local okSvc, svc = pcall(function()
			return game:GetService(parentArg)
		end)
		if okSvc and svc then
			return svc
		end
	end
	return getWorkspaceParent(parentArg)
end

-- Cree n'importe quelle classe d'objet (ScreenGui, Tool, Sound, SpawnLocation,
-- TextLabel, Model, ...) avec un dossier parent intelligent.
local function createInstance(args)
	local className = tostring(args.className or "")
	if className == "" then
		return nil, "Donne un className (ex: ScreenGui, Tool, Sound, SpawnLocation, TextLabel, BillboardGui, Model...)."
	end
	local okNew, instance = pcall(function()
		return Instance.new(className)
	end)
	if not okNew or not instance then
		return nil, "Classe impossible a creer ici: " .. className .. " (" .. tostring(instance) .. ")"
	end
	local recording = beginRecording("X.GPT PRO: creer " .. className)
	instance.Name = args.name or className
	applyProperties(instance, args.properties)
	local container = getParentFromArg(args.parent)
	if not container then
		if instance:IsA("GuiObject") or instance:IsA("LayerCollector") then
			container = game:GetService("StarterGui")
		elseif instance:IsA("Tool") then
			container = game:GetService("StarterPack")
		else
			container = workspace
		end
	end
	instance.Parent = container
	commitRecording(recording)
	return instance:GetFullName(), nil
end

local function describeObject(instance)
	local parts = {}
	local function prop(name, value)
		table.insert(parts, "  " .. name .. " = " .. tostring(value))
	end
	table.insert(parts, instance:GetFullName() .. " (" .. instance.ClassName .. ")")
	if instance:IsA("BasePart") then
		prop("Position", instance.Position)
		prop("Size", instance.Size)
		prop("Color", instance.Color)
		prop("Material", instance.Material.Name)
		prop("Anchored", instance.Anchored)
		prop("Transparency", instance.Transparency)
	elseif instance:IsA("LuaSourceContainer") then
		local source = tostring(instance.Source)
		if #source > 1500 then
			source = source:sub(1, 1500) .. "\n... (tronque)"
		end
		table.insert(parts, "  Source:\n" .. source)
	elseif instance:IsA("ValueBase") then
		prop("Value", instance.Value)
	elseif instance:IsA("Model") then
		prop("Pivot", instance:GetPivot().Position)
	end
	local children = instance:GetChildren()
	if #children > 0 then
		table.insert(parts, "  Enfants:")
		for i = 1, math.min(15, #children) do
			table.insert(parts, "    " .. children[i].Name .. " (" .. children[i].ClassName .. ")")
		end
		if #children > 15 then
			table.insert(parts, "    ... et " .. (#children - 15) .. " autre(s)")
		end
	end
	return table.concat(parts, "\n")
end

local function readObjectCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	return true, describeObject(instance)
end

local function cloneCommand(args)
	local instance = resolveInstance(args.path)
	if not instance then
		return false, "Objet introuvable: " .. tostring(args.path) .. ". Utilise roblox_inspect pour voir les chemins."
	end
	if instance == game or instance.Parent == game then
		return false, "Refuse de cloner un service de base."
	end
	local count = math.clamp(tonumber(args.count) or 1, 1, 50)
	local offset = args.offset or {}
	local parent = getParentFromArg(args.parent) or instance.Parent
	local basePos = objectPosition(instance)
	local recording = beginRecording("X.GPT PRO: cloner " .. instance.Name)
	local created = {}
	for i = 0, count - 1 do
		local clone = instance:Clone()
		clone.Name = instance.Name .. "_" .. (i + 1)
		clone.Parent = parent
		if basePos and (offset.x or offset.y or offset.z) then
			local target = basePos + Vector3.new((offset.x or 0) * i, (offset.y or 0) * i, (offset.z or 0) * i)
			pcall(function()
				if clone:IsA("BasePart") then
					clone.Position = target
				else
					clone:PivotTo(CFrame.new(target))
				end
			end)
		end
		table.insert(created, clone:GetFullName())
	end
	commitRecording(recording)
	return true, "Clone(s) cree(s): " .. table.concat(created, ", ")
end

local SCENE_PRESETS = {
	Day = { clock = 14, brightness = 2, ambient = Color3.fromRGB(200, 205, 215), outdoor = Color3.fromRGB(220, 225, 235), fog = Color3.fromRGB(180, 195, 210), fogStart = 300, fogEnd = 800 },
	Sunset = { clock = 18.2, brightness = 1.6, ambient = Color3.fromRGB(140, 90, 70), outdoor = Color3.fromRGB(230, 150, 90), fog = Color3.fromRGB(255, 160, 100), fogStart = 100, fogEnd = 400 },
	Night = { clock = 0, brightness = 0.7, ambient = Color3.fromRGB(25, 30, 60), outdoor = Color3.fromRGB(30, 35, 70), fog = Color3.fromRGB(10, 12, 30), fogStart = 40, fogEnd = 250 },
	Horror = { clock = 0.2, brightness = 0.5, ambient = Color3.fromRGB(20, 15, 15), outdoor = Color3.fromRGB(25, 18, 18), fog = Color3.fromRGB(8, 8, 8), fogStart = 20, fogEnd = 180 },
	Space = { clock = 0, brightness = 0.4, ambient = Color3.fromRGB(10, 10, 25), outdoor = Color3.fromRGB(15, 15, 40), fog = Color3.fromRGB(0, 0, 10), fogStart = 10, fogEnd = 120 },
	Tropical = { clock = 11, brightness = 2.2, ambient = Color3.fromRGB(210, 225, 230), outdoor = Color3.fromRGB(240, 250, 255), fog = Color3.fromRGB(200, 230, 240), fogStart = 250, fogEnd = 700 },
	Neon = { clock = 0, brightness = 0.9, ambient = Color3.fromRGB(60, 20, 90), outdoor = Color3.fromRGB(80, 30, 120), fog = Color3.fromRGB(30, 5, 50), fogStart = 60, fogEnd = 300 },
}

local function sceneCommand(args)
	local presetName = tostring(args.preset or "Day")
	local preset = SCENE_PRESETS[presetName]
	if not preset then
		local names = {}
		for name in pairs(SCENE_PRESETS) do
			table.insert(names, name)
		end
		table.sort(names)
		return false, "Preset inconnu: " .. presetName .. " (dispo: " .. table.concat(names, ", ") .. ")"
	end
	local lighting = game:GetService("Lighting")
	local recording = beginRecording("X.GPT PRO: ambiance " .. presetName)
	lighting.ClockTime = tonumber(args.clock) or preset.clock
	lighting.Brightness = tonumber(args.brightness) or preset.brightness
	lighting.Ambient = preset.ambient
	lighting.OutdoorAmbient = preset.outdoor
	lighting.FogColor = preset.fog
	lighting.FogStart = preset.fogStart
	lighting.FogEnd = preset.fogEnd
	commitRecording(recording)
	return true, "Ambiance appliquee: " .. presetName .. " (heure " .. lighting.ClockTime .. ", luminosite " .. lighting.Brightness .. ")"
end

function executeCommand(command)
	local args = command.args or {}
	if command.type == "create" then
		local objectType = args.objectType or "part"
		if objectType == "part" then
			return true, "Cree: " .. createPart(args)
		elseif objectType == "folder" then
			return true, "Cree: " .. createFolder(args)
		elseif objectType == "model" then
			return true, "Cree: " .. createModel(args)
		elseif objectType == "script" or objectType == "localscript" then
			args.client = objectType == "localscript"
			local full, err = createScript(args)
			if not full then
				return false, err
			end
			return true, "Cree: " .. full
		elseif objectType == "instance" then
			local full, err = createInstance(args)
			if not full then
				return false, err
			end
			return true, "Cree: " .. full
		end
		return false, "Type inconnu: " .. tostring(objectType)
	elseif command.type == "run_code" then
		local full, err = createScript({ code = args.code, name = args.name, client = args.client, container = args.container })
		if not full then
			return false, err
		end
		return true, "Cree: " .. full
	elseif command.type == "inspect" then
		return inspectCommand()
	elseif command.type == "read_script" then
		return readScriptCommand(args)
	elseif command.type == "update_script" then
		return updateScriptCommand(args)
	elseif command.type == "toolbox_search" then
		return toolboxSearchCommand(args)
	elseif command.type == "test_play" then
		return testPlayCommand(args)
	elseif command.type == "focus" then
		return focusCommand(args)
	elseif command.type == "delete" then
		return deleteCommand(args)
	elseif command.type == "rename" then
		return renameCommand(args)
	elseif command.type == "set_properties" then
		return setPropertiesCommand(args)
	elseif command.type == "terrain" then
		return terrainCommand(args)
	elseif command.type == "read_object" then
		return readObjectCommand(args)
	elseif command.type == "clone" then
		return cloneCommand(args)
	elseif command.type == "scene" then
		return sceneCommand(args)
	elseif command.type == "toolbox_insert" then
		return toolboxInsertCommand(args)
	elseif command.type == "disconnect" then
		disconnect()
		return true, "Plugin deconnecte par l'IA"
	end
	return false, "Commande inconnue: " .. tostring(command.type)
end

-- ---------------------------------------------------------------------------
-- Evenements
-- ---------------------------------------------------------------------------

connectBtn.MouseButton1Click:Connect(function()
	if running then
		disconnect()
	else
		connect()
	end
end)

toggleBtn.Click:Connect(function()
	pcall(function()
		gui.Enabled = not gui.Enabled
	end)
end)

pcall(function()
	gui.Enabled = true
end)

updateUI()
print("[X.GPT PRO] OK - Plugin charge")

end) -- fin pcall

if not success then
	print("[X.GPT PRO] ERREUR:", err)
	warn("[X.GPT PRO] ERREUR:", err)
end