--[[
	ScriptFreak — Roblox Studio plugin
	Generated code goes straight into the Explorer, wrapped in undo records.

	Install:
	  1. Save this file as ScriptFreak.lua into your Studio Plugins folder
	     (Studio → Plugins tab → Plugins Folder button)
	  2. Restart Studio. A "ScriptFreak" icon appears in the Plugins ribbon.
	  3. Open it once, paste your API key from the website dashboard
	     (Account → Studio plugin → Create key), pick an engine, build.

	Distribution note: for Creator Store release, wrap this source in a .rbxm
	with a Script inside; logic is identical.
]]

local HttpService = game:GetService("HttpService")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Selection = game:GetService("Selection")

local PLUGIN_NAME = "ScriptFreak"
local DEFAULT_ENDPOINT = "https://scriptfreak.com"

local toolbar = plugin:CreateToolbar(PLUGIN_NAME)
local toggleButton = toolbar:CreateButton(
	"ScriptFreak",
	"Generate Roblox systems from plain English",
	"rbxassetid://10747371211" -- hammer-ish placeholder; swap with custom 128px asset later
)

local widgetInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 340, 420, 280, 320)
local widget = plugin:CreateDockWidgetPluginGui("ScriptFreakMain", widgetInfo)
widget.Title = PLUGIN_NAME
toggleButton.ClickableConnect = function() end

local function make(className, props, parent)
	local inst = Instance.new(className)
	for k, v in pairs(props) do inst[k] = v end
	inst.Parent = parent
	return inst
end

-- UI ------------------------------------------------------------------
local root = make("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.fromRGB(21, 21, 23), BorderSizePixel = 0 }, widget)
make("UICorner", { CornerRadius = UDim.new(0, 8) }, root)

local padding = make("UIPadding", {}, root)
padding.PaddingTop = UDim.new(0, 10); padding.PaddingBottom = UDim.new(0, 10)
padding.PaddingLeft = UDim.new(0, 10); padding.PaddingRight = UDim.new(0, 10)

local layout = make("UIListLayout", { Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, root)

local header = make("TextLabel", {
	Text = "ScriptFreak · studio",
	TextXAlignment = Enum.TextXAlignment.Left,
	BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 22),
	Font = Enum.Font.GothamBold, TextSize = 15, TextColor3 = Color3.fromRGB(242, 242, 240),
	LayoutOrder = 1,
}, root)

-- session state (device-code link; 24h token, only the server can issue it)
local sessionToken = plugin:GetSetting("sessionToken") or ""
local sessionExpires = plugin:GetSetting("sessionExpires") or 0
local sessionEmail = plugin:GetSetting("sessionEmail") or ""
local function sessionValid()
	return sessionToken ~= "" and os.time() < sessionExpires
end

-- connect panel (shown instead of everything else when no valid session)
local connectPanel = make("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 150), LayoutOrder = 2 }, root)
local connectLayout = make("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, connectPanel)
local connectTitle = make("TextLabel", {
	Text = "Connect your account",
	TextXAlignment = Enum.TextXAlignment.Left, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 18), TextColor3 = Color3.fromRGB(242, 242, 240),
	Font = Enum.Font.GothamBold, TextSize = 14, LayoutOrder = 1,
}, connectPanel)
local connectCode = make("TextLabel", {
	Text = "······",
	TextXAlignment = Enum.TextXAlignment.Center, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 34), TextColor3 = Color3.fromRGB(255, 197, 61),
	Font = Enum.Font.Code, TextSize = 30, LayoutOrder = 2,
}, connectPanel)
local connectHelp = make("TextLabel", {
	Text = "1. Open scriptfreak.com/studio/connect\n2. Enter this code\n3. Come back — linking is automatic",
	TextXAlignment = Enum.TextXAlignment.Left, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 48), TextWrapped = true, TextYAlignment = Enum.TextYAlignment.Top,
	TextColor3 = Color3.fromRGB(163, 163, 171), Font = Enum.Font.Gotham, TextSize = 12, LayoutOrder = 3,
}, connectPanel)
local connectStatus = make("TextLabel", {
	Text = "", TextXAlignment = Enum.TextXAlignment.Left, BackgroundTransparency = 1,
	Size = UDim2.new(1, 0, 0, 16), TextWrapped = true,
	TextColor3 = Color3.fromRGB(124, 255, 140), Font = Enum.Font.Code, TextSize = 12, LayoutOrder = 4,
}, connectPanel)
local connectRetry = make("TextButton", {
	Text = "Retry", Size = UDim2.new(0, 90, 0, 26),
	BackgroundColor3 = Color3.fromRGB(27, 27, 30), TextColor3 = Color3.fromRGB(230, 230, 235),
	Font = Enum.Font.GothamMedium, TextSize = 13, LayoutOrder = 5,
}, connectPanel)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, connectRetry)

local engineRow = make("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 28), LayoutOrder = 3 }, root)
local engineLayout = make("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6) }, engineRow)
local engines = { "fast", "standard", "max" }
local selectedEngine = plugin:GetSetting("engine") or "standard"
local engineButtons = {}
for _, name in ipairs(engines) do
	local b = make("TextButton", {
		Text = string.upper(string.sub(name, 1, 1)) .. string.sub(name, 2),
		Size = UDim2.new(0, 92, 1, 0),
		BackgroundColor3 = (name == selectedEngine) and Color3.fromRGB(255, 197, 61) or Color3.fromRGB(27, 27, 30),
		TextColor3 = (name == selectedEngine) and Color3.fromRGB(20, 19, 16) or Color3.fromRGB(163, 163, 171),
		Font = Enum.Font.GothamMedium, TextSize = 13,
	}, engineRow)
	make("UICorner", { CornerRadius = UDim.new(0, 6) }, b)
	engineButtons[name] = b
	b.MouseButton1Click:Connect(function()
		selectedEngine = name
		plugin:SetSetting("engine", name)
		for n, btn in pairs(engineButtons) do
			btn.BackgroundColor3 = (n == name) and Color3.fromRGB(255, 197, 61) or Color3.fromRGB(27, 27, 30)
			btn.TextColor3 = (n == name) and Color3.fromRGB(20, 19, 16) or Color3.fromRGB(163, 163, 171)
		end
	end)
end

local function paintPill(btn, on)
	btn.BackgroundColor3 = on and Color3.fromRGB(255, 197, 61) or Color3.fromRGB(27, 27, 30)
	btn.TextColor3 = on and Color3.fromRGB(20, 19, 16) or Color3.fromRGB(163, 163, 171)
end

local promptBox = make("TextBox", {
	PlaceholderText = "Describe the system to build…",
	Text = "", MultiLine = true, TextWrapped = true,
	Size = UDim2.new(1, 0, 0, 90), ClearTextOnFocus = false, TextYAlignment = Enum.TextYAlignment.Top,
	BackgroundColor3 = Color3.fromRGB(14, 14, 16), TextColor3 = Color3.fromRGB(230, 230, 235),
	Font = Enum.Font.Gotham, TextSize = 13,
	LayoutOrder = 7,
}, root)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, promptBox)

-- mode row (system | module | chat)
local modeRow = make("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 28), LayoutOrder = 4 }, root)
make("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6) }, modeRow)
local selectedMode = plugin:GetSetting("mode") or "system"
if selectedMode ~= "system" and selectedMode ~= "module" and selectedMode ~= "chat" then selectedMode = "system" end
local modeButtons = {}
for _, name in ipairs({ "system", "module", "chat" }) do
	local b = make("TextButton", {
		Text = string.upper(string.sub(name, 1, 1)) .. string.sub(name, 2),
		Size = UDim2.new(0, 92, 1, 0),
		BackgroundColor3 = Color3.fromRGB(27, 27, 30), TextColor3 = Color3.fromRGB(163, 163, 171),
		Font = Enum.Font.GothamMedium, TextSize = 13,
	}, modeRow)
	make("UICorner", { CornerRadius = UDim.new(0, 6) }, b)
	modeButtons[name] = b
end

-- module-only rows: template picker + project name
local templateRow = make("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 28), LayoutOrder = 5 }, root)
local templatePrev = make("TextButton", {
	Text = "<", Size = UDim2.new(0, 28, 1, 0), Position = UDim2.new(0, 0, 0, 0),
	BackgroundColor3 = Color3.fromRGB(27, 27, 30), TextColor3 = Color3.fromRGB(163, 163, 171),
	Font = Enum.Font.Code, TextSize = 14,
}, templateRow)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, templatePrev)
local templateNext = make("TextButton", {
	Text = ">", Size = UDim2.new(0, 28, 1, 0), Position = UDim2.new(1, -28, 0, 0),
	BackgroundColor3 = Color3.fromRGB(27, 27, 30), TextColor3 = Color3.fromRGB(163, 163, 171),
	Font = Enum.Font.Code, TextSize = 14,
}, templateRow)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, templateNext)
local templateLabel = make("TextLabel", {
	Text = "Template", TextTruncate = Enum.TextTruncate.AtEnd,
	Size = UDim2.new(1, -64, 1, 0), Position = UDim2.new(0, 36, 0, 0),
	BackgroundTransparency = 1, TextColor3 = Color3.fromRGB(230, 230, 235),
	Font = Enum.Font.GothamMedium, TextSize = 13,
}, templateRow)

local projectBox = make("TextBox", {
	PlaceholderText = "Project name (groups your modules)",
	Text = plugin:GetSetting("projectName") or "",
	Size = UDim2.new(1, 0, 0, 30), ClearTextOnFocus = false,
	BackgroundColor3 = Color3.fromRGB(14, 14, 16), TextColor3 = Color3.fromRGB(230, 230, 235),
	Font = Enum.Font.Code, TextSize = 13,
	LayoutOrder = 6,
}, root)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, projectBox)

-- chat pane (visible only in chat mode)
local chatFrame = make("ScrollingFrame", {
	Size = UDim2.new(1, 0, 0, 150), CanvasSize = UDim2.new(),
	BackgroundColor3 = Color3.fromRGB(14, 14, 16), BorderSizePixel = 0,
	ScrollBarThickness = 4, AutomaticCanvasSize = Enum.AutomaticSize.Y,
	LayoutOrder = 7,
}, root)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, chatFrame)
make("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, chatFrame)
make("UIPadding", { PaddingTop = UDim.new(0, 6), PaddingBottom = UDim.new(0, 6), PaddingLeft = UDim.new(0, 6), PaddingRight = UDim.new(0, 6) }, chatFrame)

local chatInputRow = make("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 30), LayoutOrder = 8 }, root)
local chatInput = make("TextBox", {
	PlaceholderText = "Ask anything about your game… (Enter sends)",
	Text = "", ClearTextOnFocus = false,
	Size = UDim2.new(1, -70, 1, 0),
	BackgroundColor3 = Color3.fromRGB(14, 14, 16), TextColor3 = Color3.fromRGB(230, 230, 235),
	Font = Enum.Font.Code, TextSize = 13, TextWrapped = true,
}, chatInputRow)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, chatInput)
local chatSendBtn = make("TextButton", {
	Text = "Send", Size = UDim2.new(0, 62, 1, 0), Position = UDim2.new(1, -62, 0, 0),
	BackgroundColor3 = Color3.fromRGB(255, 197, 61), TextColor3 = Color3.fromRGB(20, 19, 16),
	Font = Enum.Font.GothamBold, TextSize = 13,
}, chatInputRow)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, chatSendBtn)

-- templates: fetched live from the endpoint, static fallback if unreachable
local TEMPLATE_FALLBACK = {
	{ id = "weapon", name = "Weapon", blurb = "Melee or ranged weapon, server-validated damage" },
	{ id = "pet", name = "Pet", blurb = "Floating follower with stat buff" },
	{ id = "enemy", name = "Enemy", blurb = "Pathfinding NPC with drops" },
	{ id = "pickup", name = "Pickup", blurb = "Collectible with respawn" },
	{ id = "projectile", name = "Projectile", blurb = "Server-owned projectile with effects" },
	{ id = "ability", name = "Ability", blurb = "Cooldown ability with validation" },
}
local templates = {}
local templateIndex = 1

local function showTemplate()
	local t = templates[templateIndex]
	if not t then return end
	templateLabel.Text = t.name .. " · " .. t.blurb
	plugin:SetSetting("template", t.id)
end

local function shiftTemplate(dir)
	if #templates == 0 then return end
	templateIndex = ((templateIndex - 1 + dir) % #templates) + 1
	showTemplate()
end

local function fetchTemplates()
	local ok, res = pcall(function()
		return HttpService:RequestAsync({
			Url = DEFAULT_ENDPOINT .. "/api/modules",
			Method = "OPTIONS",
			Headers = { ["Content-Type"] = "application/json" },
		})
	end)
	if not (ok and res.Success) then return end
	local okDecode, data = pcall(function() return HttpService:JSONDecode(res.Body) end)
	if not (okDecode and type(data.templates) == "table" and #data.templates > 0) then return end
	local list = {}
	for _, t in ipairs(data.templates) do
		if type(t.id) == "string" then
			table.insert(list, { id = t.id, name = tostring(t.name or t.id), blurb = tostring(t.blurb or "") })
		end
	end
	if #list == 0 then return end
	templates = list
	local saved = plugin:GetSetting("template")
	templateIndex = 1
	for i, t in ipairs(list) do
		if t.id == saved then templateIndex = i break end
	end
	showTemplate()
end

templates = {}
for _, t in ipairs(TEMPLATE_FALLBACK) do table.insert(templates, t) end
showTemplate()

local function updateModeUi()
	local linked = sessionValid()
	local mod = selectedMode == "module"
	local isChat = selectedMode == "chat"
	connectPanel.Visible = not linked
	engineRow.Visible = linked
	modeRow.Visible = linked
	templateRow.Visible = linked and mod
	projectBox.Visible = linked and mod
	promptBox.Visible = linked and not isChat
	generateBtn.Visible = linked and not isChat
	chatFrame.Visible = linked and isChat
	chatInputRow.Visible = linked and isChat
	if linked then
		connectStatus.Text = "Connected as " .. (sessionEmail ~= "" and sessionEmail or "your account")
			.. " · " .. math.max(0, math.floor((sessionExpires - os.time()) / 3600)) .. "h left"
	end
	promptBox.PlaceholderText = mod
		and "Describe the variant to build (empty = base template)"
		or "Describe the system to build…"
	for n, b in pairs(modeButtons) do paintPill(b, n == selectedMode) end
end

-- chat state + helpers (declared before the click wiring below uses them)
local chatLog = {}
local chatBusy = false
local chatIndex = 0

local function addChatLine(role, text)
	chatIndex += 1
	local isUser = role == "user"
	local label = make("TextLabel", {
		Text = (isUser and "You: " or "SF: ") .. tostring(text):sub(1, 2000),
		TextWrapped = true, TextXAlignment = Enum.TextXAlignment.Left,
		TextYAlignment = Enum.TextYAlignment.Top,
		Size = UDim2.new(1, -8, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		TextColor3 = isUser and Color3.fromRGB(255, 197, 61) or Color3.fromRGB(200, 200, 205),
		Font = Enum.Font.Code, TextSize = 12, LayoutOrder = chatIndex,
	}, chatFrame)
	return label
end

local function selectionContext()
	local items = Selection:Get()
	if #items == 0 then return nil end
	local lines = {}
	for _, inst in ipairs(items) do
		table.insert(lines, inst.ClassName .. " " .. inst.Name)
		for _, d in ipairs(inst:GetDescendants()) do
			table.insert(lines, "  " .. d.ClassName .. " " .. d.Name)
			if #lines >= 30 then break end
		end
		if #lines >= 30 then break end
	end
	return table.concat(lines, "\n")
end

local function sendChat()
	if chatBusy then return end
	local msg = chatInput.Text:match("^%s*(.-)%s*$")
	if #msg < 2 then return end
	ensureSession(function(linked)
		if not linked then return end
		table.insert(chatLog, { role = "user", content = msg })
		addChatLine("user", msg)
		while #chatLog > 12 do table.remove(chatLog, 1) end
		chatInput.Text = ""
		chatBusy = true
		local placeholder = addChatLine("assistant", "…")

		task.spawn(function()
			local ok, res = pcall(function()
				return HttpService:RequestAsync({
					Url = DEFAULT_ENDPOINT .. "/api/plugin/chat",
					Method = "POST",
					Headers = {
						["Content-Type"] = "application/json",
						["Authorization"] = "Bearer " .. sessionToken,
					},
					Body = HttpService:JSONEncode({
						messages = chatLog,
						context = selectionContext(),
					}),
				})
			end)
			local reply = nil
			if ok and res.StatusCode == 401 then
				invalidateSession()
				reply = "Session expired — press Connect account to link again."
			elseif ok and res.Success then
				local okDecode, data = pcall(function() return HttpService:JSONDecode(res.Body) end)
				if okDecode and type(data.reply) == "string" then reply = data.reply
				elseif okDecode and type(data.error) == "string" then reply = data.error end
			elseif ok then
				reply = "HTTP " .. tostring(res.StatusCode)
			else
				reply = tostring(res)
			end
			-- swap the placeholder line for the real reply
			if placeholder then
				placeholder:Destroy()
				placeholder = nil
			end
			addChatLine("assistant", reply or "No response. Try again.")
			chatBusy = false
		end)
	end)
end

for _, name in ipairs({ "system", "module", "chat" }) do
	modeButtons[name].MouseButton1Click:Connect(function()
		selectedMode = name
		plugin:SetSetting("mode", name)
		updateModeUi()
	end)
end
templatePrev.MouseButton1Click:Connect(function() shiftTemplate(-1) end)
templateNext.MouseButton1Click:Connect(function() shiftTemplate(1) end)
chatSendBtn.MouseButton1Click:Connect(sendChat)
chatInput.FocusLost:Connect(function(enterPressed)
	if enterPressed then task.spawn(sendChat) end
end)

updateModeUi()
task.spawn(fetchTemplates)

local generateBtn = make("TextButton", {
	Text = "Build",
	Size = UDim2.new(1, 0, 0, 34), BackgroundColor3 = Color3.fromRGB(255, 197, 61),
	TextColor3 = Color3.fromRGB(20, 19, 16), Font = Enum.Font.GothamBold, TextSize = 15,
	LayoutOrder = 8,
}, root)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, generateBtn)

local logBox = make("ScrollingFrame", {
	Size = UDim2.new(1, 0, 1, -320), CanvasSize = UDim2.new(),
	BackgroundColor3 = Color3.fromRGB(14, 14, 16), BorderSizePixel = 0,
	ScrollBarThickness = 4, LayoutOrder = 9,
}, root)
make("UICorner", { CornerRadius = UDim.new(0, 6) }, logBox)
local logList = make("UIListLayout", { Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder }, logBox)

local logIndex = 0
local function log(msg, color)
	logIndex += 1
	local line = make("TextLabel", {
		Text = msg, TextWrapped = true, TextXAlignment = Enum.TextXAlignment.Left,
		Size = UDim2.new(1, -8, 0, 18), BackgroundTransparency = 1,
		TextColor3 = color or Color3.fromRGB(163, 163, 171),
		Font = Enum.Font.Code, TextSize = 12, LayoutOrder = logIndex,
	}, logBox)
	logBox.CanvasSize = UDim2.new(0, 0, 0, logIndex * 20)
	return line
end

-- device-code link flow ---------------------------------------------------
local linking = false
local function startConnect(done)
	if linking then return end
	linking = true
	connectStatus.Text = ""
	connectCode.Text = "······"
	updateModeUi()

	task.spawn(function()
		local ok, res = pcall(function()
			return HttpService:RequestAsync({
				Url = DEFAULT_ENDPOINT .. "/api/plugin/link/start",
				Method = "POST", Headers = { ["Content-Type"] = "application/json" }, Body = "{}",
			})
		end)
		local code = nil
		if ok and res.Success then
			local okD, data = pcall(function() return HttpService:JSONDecode(res.Body) end)
			if okD and type(data.code) == "string" then code = data.code end
		end
		if not code then
			connectStatus.Text = "Could not reach scriptfreak.com. Press Retry."
			connectStatus.TextColor3 = Color3.fromRGB(255, 120, 120)
			linking = false
			done(false)
			return
		end

		connectCode.Text = code
		connectStatus.Text = "Waiting for approval…"
		connectStatus.TextColor3 = Color3.fromRGB(163, 163, 171)

		for _ = 1, 60 do
			task.wait(2.5)
			local okW, resW = pcall(function()
				return HttpService:RequestAsync({
					Url = DEFAULT_ENDPOINT .. "/api/plugin/link/wait",
					Method = "POST",
					Headers = { ["Content-Type"] = "application/json" },
					Body = HttpService:JSONEncode({ code = code }),
				})
			end)
			if okW and resW.Success then
				local okD, data = pcall(function() return HttpService:JSONDecode(resW.Body) end)
				if okD and data.status == "ok" then
					sessionToken = data.token
					sessionExpires = tonumber(data.expires_at) or 0
					sessionEmail = tostring(data.email or "")
					plugin:SetSetting("sessionToken", sessionToken)
					plugin:SetSetting("sessionExpires", sessionExpires)
					plugin:SetSetting("sessionEmail", sessionEmail)
					linking = false
					log("Connected as " .. sessionEmail, Color3.fromRGB(124, 255, 140))
					updateModeUi()
					done(true)
					return
				elseif okD and data.status == "expired" then
					break
				end
			end
		end

		connectStatus.Text = "Code expired or unreachable. Press Retry."
		connectStatus.TextColor3 = Color3.fromRGB(255, 120, 120)
		linking = false
		done(false)
	end)
end

local function ensureSession(done)
	if sessionValid() then
		done(true)
	else
		startConnect(done)
	end
end

local function invalidateSession()
	sessionToken = ""
	sessionExpires = 0
	plugin:SetSetting("sessionToken", "")
	plugin:SetSetting("sessionExpires", 0)
	updateModeUi()
end

connectRetry.MouseButton1Click:Connect(function()
	if not linking then startConnect(function() end) end
end)

-- insertion map --------------------------------------------------------
local SERVICE_MAP = {
	ServerScriptService = "ServerScriptService",
	ServerStorage = "ServerStorage",
	ReplicatedStorage = "ReplicatedStorage",
	StarterGui = "StarterGui",
	StarterPack = "StarterPack",
	Workspace = "Workspace",
}
local function serviceFor(prefix)
	return SERVICE_MAP[prefix] or (
		prefix == "StarterPlayerScripts" and game:GetService("StarterPlayer"):FindFirstChildOfClass("StarterPlayerScripts")
		or prefix == "StarterCharacterScripts" and game:GetService("StarterPlayer"):FindFirstChildOfClass("StarterCharacterScripts")
		or nil
	)
end

local function instanceClassFor(path)
	if path:match("%.client%.luau$") then return "LocalScript" end
	if path:match("%.shared%.luau$") or path:match("%.server%.shared%.luau$") or path:match("ModuleScript") then return "ModuleScript" end
	return "Script"
end

local function instanceNameFor(fileName)
	local name = fileName:gsub("%.luau$", ""):gsub("%.lua$", "")
	name = name:gsub("%.server$", ""):gsub("%.client$", ""):gsub("%.shared$", "")
	return name
end

local function insertFile(relPath, content)
	local parts = relPath:split("/")
	local prefix = parts[1]
	table.remove(parts, 1)
	local fileName = table.concat(parts, "/")
	local serviceName = serviceFor(prefix)
	if not serviceName or typeof(serviceName) ~= "Instance" and not game:FindFirstChild(serviceName) then
		log("⚠ unknown location: " .. prefix, Color3.fromRGB(255, 120, 120))
		return nil
	end
	local parent = (typeof(serviceName) == "Instance") and serviceName or game:GetService(serviceName)

	local script = Instance.new(instanceClassFor(fileName))
	script.Name = instanceNameFor(fileName)
	script.Source = content
	script.Parent = parent
	return script
end

-- generation -----------------------------------------------------------
local building = false
generateBtn.MouseButton1Click:Connect(function()
	if building then return end
	ensureSession(function(linked)
		if not linked then return end
		local prompt = promptBox.Text:match("^%s*(.-)%s*$")

		if selectedMode == "system" and #prompt < 5 then log("Describe what to build.", Color3.fromRGB(255, 197, 61)) return end

		local payload = { prompt = prompt, engine = selectedEngine }
		if selectedMode == "module" then
			local t = templates[templateIndex]
			if not t then
				log("No template available for module mode.", Color3.fromRGB(255, 120, 120))
				return
			end
			payload.mode = "module"
			payload.template = t.id
			local proj = projectBox.Text:match("^%s*(.-)%s*$")
			payload.project = (#proj > 0) and proj or "My Project"
			plugin:SetSetting("projectName", payload.project)
		end

		building = true
		generateBtn.Text = "Building…"
		log("→ sending request (" .. selectedMode .. " · " .. selectedEngine .. ")")

		local ok, err = pcall(function()
			local res = HttpService:RequestAsync({
				Url = DEFAULT_ENDPOINT .. "/api/plugin/generate",
				Method = "POST",
				Headers = {
					["Content-Type"] = "application/json",
					["Authorization"] = "Bearer " .. sessionToken,
				},
				Body = HttpService:JSONEncode(payload),
			})

			if res.StatusCode == 401 then
				log("Session expired — reconnect your account.", Color3.fromRGB(255, 197, 61))
				invalidateSession()
				return
			end
			if not res.Success then
				log("✗ HTTP " .. tostring(res.StatusCode), Color3.fromRGB(255, 120, 120))
				return
			end

			local data = HttpService:JSONDecode(res.Body)
			if data.error then
				log("✗ " .. tostring(data.error), Color3.fromRGB(255, 120, 120))
				return
			end

			local recording = ChangeHistoryService:TryBeginRecording("ScriptFreak build")
			local inserted = 0
			for _, f in ipairs(data.files or {}) do
				local created = insertFile(f.path, f.content)
				if created then
					inserted += 1
					log("✓ " .. f.path, Color3.fromRGB(124, 255, 140))
				end
			end
			if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) end

			log(("Done — %d files inserted (%s). Ctrl+Z undoes everything.")
				:format(inserted, tostring(data.remaining or "?")) .. "", Color3.fromRGB(255, 197, 61))
			if data.setup and #data.setup > 0 then
				log("Setup notes:")
				for _, s in ipairs(data.setup) do log("· " .. s) end
			end
			Selection:Set({})
		end)

		if not ok then log("✗ " .. tostring(err), Color3.fromRGB(255, 120, 120)) end
		building = false
		generateBtn.Text = "Build"
	end)
end)

toggleButton.Click:Connect(function()
	widget.Enabled = not widget.Enabled
	toggleButton:SetActive(widget.Enabled)
end)
widget:GetPropertyChangedSignal("Enabled"):Connect(function()
	toggleButton:SetActive(widget.Enabled)
end)
