local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Workspace = game:GetService("Workspace")

local ROOM_SOURCE_FOLDER_NAME = "ComplexRooms"

local FLOOR_COUNT = 10
local FLOOR_HEIGHT = 500
local BASE_ROOM_Y = 850000

local PERMANENT_ROOM_COUNT = 50

local LOAD_RADIUS = 450
local UNLOAD_RADIUS = 700
local UPDATE_INTERVAL = 1
local MAX_ROOMS_PER_FLOOR = 400

local DOOR_TO_DOOR_GAP_STUDS = 1

local MAX_PLACEMENT_ATTEMPTS = 6

local RANDOM_COLOR_PART_NAME = "RANDOMCOLORPART"

local COMMON_WEIGHT = 70
local RARE_WEIGHT = 25
local VERYRARE_WEIGHT = 5
local IMPOSSIBLE_CHANCE = 0.01

local COOL_HUE_MIN = 0.33
local COOL_HUE_MAX = 0.68
local COOL_COLOR_CHANCE = 0.85
local COLOR_SATURATION_MIN = 0.5
local COLOR_SATURATION_MAX = 0.8
local COLOR_VALUE_MIN = 0.55
local COLOR_VALUE_MAX = 0.9

local SPECIAL_STAIRCASE_NAME = "SPECIAL_STAIRCASE"

local floors = {}

local roomsFolder = Instance.new("Folder")
roomsFolder.Name = "GeneratedRooms"
roomsFolder.Parent = Workspace

local floorFolders = {}
for f = 1, FLOOR_COUNT do
	local folder = Instance.new("Folder")
	folder.Name = "Floor" .. f
	folder.Parent = roomsFolder
	floorFolders[f] = folder

	floors[f] = { rooms = {}, nextId = 1, openExits = {}, activeCount = 0 }
end

local function floorBaseY(floor)
	return BASE_ROOM_Y + (floor - 1) * FLOOR_HEIGHT
end

local function randomAccentColor()
	local hue
	if math.random() < COOL_COLOR_CHANCE then
		hue = COOL_HUE_MIN + math.random() * (COOL_HUE_MAX - COOL_HUE_MIN)
	else
		local warmRange = 1 - (COOL_HUE_MAX - COOL_HUE_MIN)
		local roll = math.random() * warmRange
		if roll < COOL_HUE_MIN then
			hue = roll
		else
			hue = COOL_HUE_MAX + (roll - COOL_HUE_MIN)
		end
	end

	local saturation = COLOR_SATURATION_MIN + math.random() * (COLOR_SATURATION_MAX - COLOR_SATURATION_MIN)
	local value = COLOR_VALUE_MIN + math.random() * (COLOR_VALUE_MAX - COLOR_VALUE_MIN)

	return Color3.fromHSV(hue, saturation, value)
end

local function fixRoomColors(model)
	for _, descendant in ipairs(model:GetDescendants()) do
		if descendant:IsA("BasePart") and descendant.Name == RANDOM_COLOR_PART_NAME then
			descendant.Color = randomAccentColor()
		end
	end
end

local function getExitParts(model)
	local exits = {}
	for _, descendant in ipairs(model:GetDescendants()) do
		if descendant:IsA("BasePart") and descendant.Name == "Exit" then
			table.insert(exits, descendant)
		end
	end
	return exits
end

local function categorizeRoomTemplates()
	local sourceFolder = ServerStorage:FindFirstChild(ROOM_SOURCE_FOLDER_NAME)
	if not sourceFolder then
		return {}, {}, {}, {}
	end

	local commonRooms, rareRooms, veryRareRooms, impossibleRooms = {}, {}, {}, {}

	for _, child in ipairs(sourceFolder:GetChildren()) do
		if child:IsA("Model") then
			local entrance = child:FindFirstChild("Entrance", true)
			local exits = getExitParts(child)

			if entrance and entrance:IsA("BasePart") and #exits > 0 then
				if child.Name:sub(1, 9) == "VERYRARE_" then
					table.insert(veryRareRooms, child)
				elseif child.Name:sub(1, 11) == "IMPOSSIBLE_" then
					table.insert(impossibleRooms, child)
				elseif child.Name:sub(1, 5) == "RARE_" then
					table.insert(rareRooms, child)
				elseif child.Name:sub(1, 7) == "COMMON_" then
					table.insert(commonRooms, child)
				end
			end
		end
	end

	return commonRooms, rareRooms, veryRareRooms, impossibleRooms
end

local function findSpecialStaircaseTemplate()
	local sourceFolder = ServerStorage:FindFirstChild(ROOM_SOURCE_FOLDER_NAME)
	if not sourceFolder then
		return nil
	end

	local child = sourceFolder:FindFirstChild(SPECIAL_STAIRCASE_NAME)
	if child and child:IsA("Model") then
		local entrance = child:FindFirstChild("Entrance", true)
		local exits = getExitParts(child)
		if entrance and entrance:IsA("BasePart") and #exits > 0 then
			return child
		end
	end

	return nil
end

local roomPool = {}
local roomPoolTotalWeight = 0
local impossibleRoomTemplates = {}
local specialStaircaseTemplate = findSpecialStaircaseTemplate()

local function buildRoomPool()
	local commonRooms, rareRooms, veryRareRooms, impossibleRooms = categorizeRoomTemplates()
	impossibleRoomTemplates = impossibleRooms

	for _, template in ipairs(commonRooms) do
		table.insert(roomPool, { template = template, weight = COMMON_WEIGHT })
	end
	for _, template in ipairs(rareRooms) do
		table.insert(roomPool, { template = template, weight = RARE_WEIGHT })
	end
	for _, template in ipairs(veryRareRooms) do
		table.insert(roomPool, { template = template, weight = VERYRARE_WEIGHT })
	end

	for _, entry in ipairs(roomPool) do
		roomPoolTotalWeight += entry.weight
	end
end

buildRoomPool()

local function pickRoomTemplate()
	if #impossibleRoomTemplates > 0 and math.random() < IMPOSSIBLE_CHANCE then
		return impossibleRoomTemplates[math.random(1, #impossibleRoomTemplates)]
	end

	if #roomPool == 0 then
		if #impossibleRoomTemplates > 0 then
			return impossibleRoomTemplates[math.random(1, #impossibleRoomTemplates)]
		end
		return nil
	end

	local roll = math.random() * roomPoolTotalWeight
	local cumulative = 0
	for _, entry in ipairs(roomPool) do
		cumulative += entry.weight
		if roll <= cumulative then
			return entry.template
		end
	end

	return roomPool[#roomPool].template
end

local function alignRoomToTarget(clone, entrancePart, targetCFrame)
	local currentModelCFrame = clone:GetPivot()
	local currentEntranceCFrame = entrancePart.CFrame
	local entranceOffset = currentModelCFrame:ToObjectSpace(currentEntranceCFrame)
	local newModelCFrame = targetCFrame * entranceOffset:Inverse()
	clone:PivotTo(newModelCFrame)
end

local function addOpenExit(floorData, roomId, exitPart)
	table.insert(floorData.openExits, { roomId = roomId, exitPart = exitPart })
end

local function removeOpenExit(floorData, exitPart)
	for i, entry in ipairs(floorData.openExits) do
		if entry.exitPart == exitPart then
			table.remove(floorData.openExits, i)
			return
		end
	end
end

local function collectSiblingModels(floorData, parentEntry)
	local models = {}
	if parentEntry then
		if parentEntry.model then
			table.insert(models, parentEntry.model)
		end
		for _, roomEntry in pairs(floorData.rooms) do
			if roomEntry.parentRoomId == parentEntry.id and roomEntry.model then
				table.insert(models, roomEntry.model)
			end
		end
	end
	return models
end

local function isSpaceBlocked(clone, ignoreModels)
	local cf, size = clone:GetBoundingBox()
	local overlapParams = OverlapParams.new()

	local ignoreList = {clone}
	if ignoreModels then
		for _, m in ipairs(ignoreModels) do
			table.insert(ignoreList, m)
		end
	end
	overlapParams.FilterDescendantsInstances = ignoreList
	overlapParams.FilterType = Enum.RaycastFilterType.Exclude

	local checkSize = size * 0.75
	local touching = Workspace:GetPartBoundsInBox(cf, checkSize, overlapParams)

	for _, tPart in ipairs(touching) do
		if tPart.CanCollide and tPart:IsDescendantOf(roomsFolder) then
			return true
		end
	end

	local entrance = clone:FindFirstChild("Entrance", true)
	if entrance then
		local entTouching = Workspace:GetPartsInPart(entrance, overlapParams)
		for _, tPart in ipairs(entTouching) do
			if tPart.CanCollide and tPart:IsDescendantOf(roomsFolder) then
				if tPart.Name ~= "Exit" and tPart.Name ~= "Entrance" then
					return true
				end
			end
		end
	end

	return false
end

local function spawnRoom(floor, permanent, dockToCFrame, fallbackCFrame, parentEntry, parentExitPart, forcedTemplate)
	local floorData = floors[floor]
	if floorData.activeCount >= MAX_ROOMS_PER_FLOOR then
		return nil, "max_rooms"
	end

	local template = forcedTemplate or pickRoomTemplate()
	if not template then
		return nil, "no_template"
	end

	local clone = template:Clone()
	local entrancePart = clone:FindFirstChild("Entrance", true)
	local exitParts = getExitParts(clone)

	if dockToCFrame then
		alignRoomToTarget(clone, entrancePart, dockToCFrame)
	else
		clone:PivotTo(fallbackCFrame)
	end

	if dockToCFrame and isSpaceBlocked(clone, collectSiblingModels(floorData, parentEntry)) then
		clone:Destroy()
		return nil, "blocked"
	end

	clone.Parent = floorFolders[floor]
	fixRoomColors(clone)

	local id = floorData.nextId
	floorData.nextId += 1

	local parentRoomId = parentEntry and parentEntry.id or nil

	local entry = {
		id = id,
		model = clone,
		permanent = permanent,
		floor = floor,
		entrance = entrancePart,
		exits = exitParts,
		childCount = 0,
		parentRoomId = parentRoomId,
		parentExitPart = parentExitPart,
	}

	floorData.rooms[id] = entry
	floorData.activeCount += 1

	for _, exitPart in ipairs(exitParts) do
		addOpenExit(floorData, id, exitPart)
	end

	return entry, "success"
end

local function safeSpawnRoom(...)
	local ok, newEntry, reason = pcall(spawnRoom, ...)
	if not ok then
		return nil, "blocked"
	end
	return newEntry, reason
end

local function sealExit(exitPart)
	if exitPart and exitPart.Parent then
		exitPart.Transparency = 0
		exitPart.CanCollide = true
	end
end

local function extendFromExit(floorData, sourceEntry, exitPart, permanent)
	if not exitPart or not exitPart.Parent then
		removeOpenExit(floorData, exitPart)
		return nil
	end

	local dockCFrame = exitPart.CFrame * CFrame.new(0, 0, -DOOR_TO_DOOR_GAP_STUDS)

	local newEntry, reason

	for attempt = 1, MAX_PLACEMENT_ATTEMPTS do
		newEntry, reason = safeSpawnRoom(sourceEntry.floor, permanent, dockCFrame, nil, sourceEntry, exitPart)
		if newEntry or reason ~= "blocked" then
			break
		end
	end

	if not newEntry and reason == "blocked" and specialStaircaseTemplate then
		newEntry, reason = safeSpawnRoom(sourceEntry.floor, permanent, dockCFrame, nil, sourceEntry, exitPart, specialStaircaseTemplate)
	end

	if newEntry then
		removeOpenExit(floorData, exitPart)
		sourceEntry.childCount += 1
	elseif reason == "blocked" then
		sealExit(exitPart)
		removeOpenExit(floorData, exitPart)
	end

	return newEntry
end

local function destroyRoomEntry(floor, id)
	local floorData = floors[floor]
	local entry = floorData.rooms[id]
	if not entry then
		return
	end

	if entry.model then
		entry.model:Destroy()
	end

	floorData.rooms[id] = nil
	floorData.activeCount -= 1

	for _, exitPart in ipairs(entry.exits) do
		removeOpenExit(floorData, exitPart)
	end

	if entry.parentRoomId then
		local parentEntry = floorData.rooms[entry.parentRoomId]
		if parentEntry then
			parentEntry.childCount -= 1
			addOpenExit(floorData, entry.parentRoomId, entry.parentExitPart)
		end
	end
end

local function generatePermanentChain(floor)
	local floorData = floors[floor]
	local fallbackCFrame = CFrame.new(0, floorBaseY(floor), 0)

	local currentEntry = spawnRoom(floor, true, nil, fallbackCFrame, nil, nil)
	if not currentEntry then
		return
	end

	local queue = {currentEntry}
	local count = 1

	while #queue > 0 and count < PERMANENT_ROOM_COUNT do
		local entry = table.remove(queue, 1)

		local exits = table.clone(entry.exits)
		for _, exitPart in ipairs(exits) do
			if count >= PERMANENT_ROOM_COUNT then
				break
			end

			local isOpen = false
			for _, oe in ipairs(floorData.openExits) do
				if oe.exitPart == exitPart then
					isOpen = true
					break
				end
			end

			if isOpen then
				local ok, newEntry = pcall(extendFromExit, floorData, entry, exitPart, true)
				if not ok then
					sealExit(exitPart)
					removeOpenExit(floorData, exitPart)
				elseif newEntry then
					table.insert(queue, newEntry)
					count += 1
				end
			end
		end
	end
end

for f = 1, FLOOR_COUNT do
	generatePermanentChain(f)
end

local function getPlayerFloorPositions()
	local results = {}
	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		local rootPart = character and character:FindFirstChild("HumanoidRootPart")
		if rootPart then
			local pos = rootPart.Position

			local closestFloor, closestDist = 1, math.huge
			for f = 1, FLOOR_COUNT do
				local dist = math.abs(pos.Y - floorBaseY(f))
				if dist < closestDist then
					closestFloor, closestDist = f, dist
				end
			end

			table.insert(results, { position = pos, floor = closestFloor })
		end
	end
	return results
end

local MAX_CASCADE_ITERATIONS_PER_PASS = 25

local function extendChains(floor, playerPositionsOnFloor)
	local floorData = floors[floor]
	if #playerPositionsOnFloor == 0 then
		return
	end

	local iterations = 0
	local didExtend = true

	while didExtend and iterations < MAX_CASCADE_ITERATIONS_PER_PASS do
		didExtend = false
		iterations += 1

		local exitsToCheck = table.clone(floorData.openExits)

		for _, openExit in ipairs(exitsToCheck) do
			local roomEntry = floorData.rooms[openExit.roomId]
			if roomEntry then
				local exitWorldPos = openExit.exitPart.Position

				local nearPlayer = false
				for _, playerPos in ipairs(playerPositionsOnFloor) do
					if (exitWorldPos - playerPos).Magnitude <= LOAD_RADIUS then
						nearPlayer = true
						break
					end
				end

				if nearPlayer then
					local ok, newEntry = pcall(extendFromExit, floorData, roomEntry, openExit.exitPart, false)
					if not ok then
						sealExit(openExit.exitPart)
						removeOpenExit(floorData, openExit.exitPart)
					elseif newEntry then
						didExtend = true
					end
				end
			end
		end
	end
end

local function unloadFarRooms(floor, playerPositionsOnFloor)
	local floorData = floors[floor]
	local toRemove = {}

	for id, entry in pairs(floorData.rooms) do
		if not entry.permanent and entry.childCount == 0 then
			local roomWorldPos = entry.model:GetPivot().Position

			local nearAnyPlayer = false
			for _, playerPos in ipairs(playerPositionsOnFloor) do
				if (roomWorldPos - playerPos).Magnitude <= UNLOAD_RADIUS then
					nearAnyPlayer = true
					break
				end
			end

			if not nearAnyPlayer then
				table.insert(toRemove, id)
			end
		end
	end

	for _, id in ipairs(toRemove) do
		destroyRoomEntry(floor, id)
	end
end

local function streamingPass()
	local playerFloorPositions = getPlayerFloorPositions()
	if #playerFloorPositions == 0 then
		return
	end

	local byFloor = {}
	for f = 1, FLOOR_COUNT do
		byFloor[f] = {}
	end
	for _, entry in ipairs(playerFloorPositions) do
		table.insert(byFloor[entry.floor], entry.position)
	end

	for f = 1, FLOOR_COUNT do
		extendChains(f, byFloor[f])
		unloadFarRooms(f, byFloor[f])
	end
end

task.spawn(function()
	while true do
		task.wait(UPDATE_INTERVAL)
		pcall(streamingPass)
	end
end)