local addonName = ...
local SlickUI = _G.SlickUI
if not SlickUI then return end

local Theme = SlickUI.Theme
local Planner = {}
SlickUI.GarrisonMissions = Planner
local CreateButton

local GARRISON, SHIPYARD = 1, 2
local MAX_COMBINATIONS = 3000
local DEBUG_LOG_LIMIT = 300
local DIAGNOSTICS_ENABLED = false
local MISSION_ICON_LIMIT = 3
local FOLLOWER_GROUP_LIMIT = 3
local FOLLOWER_ABILITY_LIMIT = 3
local REWARD_ICON_LIMIT = 3
local PRIORITY_DEFAULTS = { "gear", "gold", "resources", "xp", "other" }
local DEFAULTS = {
	garrisonThreshold = 80,
	shipyardThreshold = 100,
	garrisonPriorities = PRIORITY_DEFAULTS,
	shipyardPriorities = PRIORITY_DEFAULTS,
	garrisonEnabled = { gear = true, gold = true, resources = true, xp = true, other = true },
	shipyardEnabled = { gear = true, gold = true, resources = true, xp = true, other = true },
	garrisonRunRemaining = true,
	shipyardRunRemaining = true,
}

local function CopyTable(source)
	local result = {}
	for key, value in pairs(source) do
		result[key] = type(value) == "table" and CopyTable(value) or value
	end
	return result
end

local function GetDB()
	SlickUIGarrisonMissionsDB = SlickUIGarrisonMissionsDB or CopyTable(DEFAULTS)
	local db = SlickUIGarrisonMissionsDB
	-- Migrate the original shared priority list into both table-specific profiles.
	if type(db.priorities) == "table" then
		if db.garrisonPriorities == nil then db.garrisonPriorities = CopyTable(db.priorities) end
		if db.shipyardPriorities == nil then db.shipyardPriorities = CopyTable(db.priorities) end
		db.priorities = nil
	end
	for key, value in pairs(DEFAULTS) do
		if db[key] == nil then db[key] = type(value) == "table" and CopyTable(value) or value end
	end
	return db
end

local function DebugValue(value, depth)
	depth = depth or 0
	if type(canaccessvalue) == "function" then
		local ok, accessible = pcall(canaccessvalue, value)
		if ok and not accessible then return "<secret>" end
	end
	local valueType = type(value)
	if valueType == "nil" or valueType == "number" or valueType == "boolean" then return tostring(value) end
	if valueType == "string" then return value end
	if valueType ~= "table" then return "<" .. valueType .. ">" end
	if depth >= 1 then return "<table>" end
	local parts, count = {}, 0
	for key, entry in pairs(value) do
		count = count + 1
		if count > 12 then parts[#parts + 1] = "..." break end
		parts[#parts + 1] = DebugValue(key, depth + 1) .. "=" .. DebugValue(entry, depth + 1)
	end
	return "{" .. table.concat(parts, ", ") .. "}"
end

function Planner:Log(eventName, ...)
	if not DIAGNOSTICS_ENABLED then return end
	local db = GetDB()
	db.diagnosticLog = type(db.diagnosticLog) == "table" and db.diagnosticLog or {}
	local values = {}
	for index = 1, select("#", ...) do values[index] = DebugValue(select(index, ...)) end
	local stamp = date and date("%Y-%m-%d %H:%M:%S") or tostring(GetTime())
	db.diagnosticLog[#db.diagnosticLog + 1] = stamp .. " | " .. tostring(eventName) .. (#values > 0 and " | " .. table.concat(values, " | ") or "")
	while #db.diagnosticLog > DEBUG_LOG_LIMIT do table.remove(db.diagnosticLog, 1) end
	if self.debugEditBox and self.debugFrame and self.debugFrame:IsShown() then self:RefreshDebugLog() end
end

function Planner:GetDebugText()
	local metadata = C_AddOns and C_AddOns.GetAddOnMetadata and C_AddOns.GetAddOnMetadata(addonName, "Version") or "unknown"
	local lines = {
		"SlickUI Garrison Missions Debug Journal",
		"Version: " .. tostring(metadata),
		"Generated: " .. (date and date("%Y-%m-%d %H:%M:%S") or "unknown"),
		"Entries: " .. tostring(#(GetDB().diagnosticLog or {})),
		"",
	}
	for _, entry in ipairs(GetDB().diagnosticLog or {}) do lines[#lines + 1] = entry end
	return table.concat(lines, "\n")
end

function Planner:RefreshDebugLog()
	if not self.debugEditBox then return end
	self.debugEditBox:SetText(self:GetDebugText())
	self.debugEditBox:SetCursorPosition(0)
end

function Planner:ShowDebugLog()
	if not self.debugFrame then
		local frame = CreateFrame("Frame", "SlickUIGarrisonMissionDebugFrame", UIParent, "BackdropTemplate")
		frame:SetSize(900, 620)
		frame:SetPoint("CENTER")
		frame:SetFrameStrata("FULLSCREEN_DIALOG")
		frame:SetFrameLevel(1000)
		frame:SetToplevel(true)
		frame:SetClampedToScreen(true)
		frame:SetMovable(true)
		frame:EnableMouse(true)
		frame:RegisterForDrag("LeftButton")
		frame:SetScript("OnDragStart", frame.StartMoving)
		frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
		Theme:CreatePanel(frame, 0.98, true)
		local title = Theme:CreateText(frame, 20, "OVERLAY")
		title:SetPoint("TOPLEFT", 24, -20)
		title:SetText("SlickUI Mission Debug Journal")
		local help = Theme:CreateText(frame, 11, "OVERLAY")
		help:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
		help:SetText("Click Select All, press Ctrl+C, then paste the journal into the development chat.")
		local close = CreateButton(frame, "X", 32, function() frame:Hide() end)
		close:SetPoint("TOPRIGHT", -12, -12)
		local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
		scroll:SetPoint("TOPLEFT", 24, -78)
		scroll:SetPoint("BOTTOMRIGHT", -46, 58)
		local edit = CreateFrame("EditBox", nil, scroll)
		edit:SetMultiLine(true)
		edit:SetAutoFocus(false)
		edit:SetFontObject(ChatFontNormal)
		edit:SetWidth(810)
		edit:SetTextInsets(4, 4, 4, 4)
		edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
		scroll:SetScrollChild(edit)
		self.debugEditBox = edit
		local selectAll = CreateButton(frame, "Select All for Copy", 190, function()
			edit:SetFocus()
			edit:HighlightText()
		end)
		selectAll:SetPoint("BOTTOMLEFT", 24, 20)
		local clear = CreateButton(frame, "Clear Debug Log", 170, function()
			GetDB().diagnosticLog = {}
			self:Log("JOURNAL_CLEARED")
		end)
		clear:SetPoint("LEFT", selectAll, "RIGHT", 10, 0)
		self.debugFrame = frame
	end
	self:RefreshDebugLog()
	self.debugFrame:Show()
	self.debugFrame:Raise()
end

CreateButton = function(parent, text, width, onClick)
	local button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
	button:SetSize(width or 110, 24)
	button:SetText(text)
	button:SetScript("OnClick", onClick)
	return button
end

local function CreateCheckbox(parent, label, onClick)
	local check = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
	check:SetSize(24, 24)
	check.label = Theme:CreateText(check, 12, "OVERLAY")
	check.label:SetText(label)
	check.label:SetPoint("LEFT", check, "RIGHT", 2, 0)
	check:SetScript("OnClick", function(self) onClick(self:GetChecked() and true or false) end)
	return check
end

local function CreatePlanIcon(parent)
	local button = CreateFrame("Frame", nil, parent, "BackdropTemplate")
	button:SetSize(18, 18)
	button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
	button:SetBackdropColor(0.03, 0.03, 0.04, 1)
	button:SetBackdropBorderColor(0.5, 0.5, 0.55, 1)
	button.icon = button:CreateTexture(nil, "ARTWORK")
	button.icon:SetPoint("TOPLEFT", 1, -1)
	button.icon:SetPoint("BOTTOMRIGHT", -1, 1)
	button.icon:SetTexCoord(0.06, 0.94, 0.06, 0.94)
	button.count = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
	button.count:SetPoint("BOTTOMRIGHT", 1, -1)
	button.count:SetTextColor(1, 1, 1)
	button:SetScript("OnEnter", function(self)
		if not self.tooltipTitle then return end
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		GameTooltip:SetText(self.tooltipTitle, 1, 0.82, 0)
		if self.tooltipText and self.tooltipText ~= "" then
			GameTooltip:AddLine(self.tooltipText, 1, 1, 1, true)
		end
		GameTooltip:Show()
	end)
	button:SetScript("OnLeave", GameTooltip_Hide)
	button:Hide()
	return button
end

local function SetPlanIcon(button, texture, title, description, count)
	if not button or not texture then
		if button then button:Hide() end
		return
	end
	button.icon:SetTexture(texture)
	button.tooltipTitle = title or "Details"
	button.tooltipText = description
	button.count:SetText(count and tonumber(count) and tonumber(count) > 1 and tostring(count) or "")
	button:Show()
end

local function SafeCall(func, ...)
	if type(func) ~= "function" then return false end
	return pcall(func, ...)
end

local function CallAccepted(func, ...)
	local ok, accepted = SafeCall(func, ...)
	return ok and accepted ~= false and accepted ~= nil
end

local function MissionLevel(mission)
	return tonumber(mission.level) or tonumber(mission.iLevel) or 0
end

local function RewardKind(mission)
	local rewards = mission.rewards or {}
	for _, reward in pairs(rewards) do
		local title = tostring(reward.title or reward.itemLink or reward.currencyID or ""):lower()
		if title:find("gold", 1, true) or tonumber(reward.currencyID) == 0 and tonumber(reward.quantity) then return "gold" end
		if reward.itemID or title:find("equipment", 1, true) or title:find("armor", 1, true) or title:find("weapon", 1, true) then return "gear" end
		if reward.currencyID == 824 or reward.currencyID == 1101 or title:find("resource", 1, true) or title:find("oil", 1, true) then return "resources" end
		if reward.followerXP or title:find("xp", 1, true) or title:find("experience", 1, true) then return "xp" end
	end
	return "other"
end

local function ProfilePrefix(followerTypeID)
	return followerTypeID == SHIPYARD and "shipyard" or "garrison"
end

local function ProfileValues(followerTypeID)
	local db, prefix = GetDB(), ProfilePrefix(followerTypeID)
	return db[prefix .. "Priorities"], db[prefix .. "Enabled"], db[prefix .. "RunRemaining"]
end

local function PriorityRank(kind, followerTypeID)
	local priorities, enabled, runRemaining = ProfileValues(followerTypeID)
	for index, value in ipairs(priorities) do
		if value == kind then
			if enabled[value] ~= false then return index end
			return runRemaining and (#priorities + 1) or nil
		end
	end
	return runRemaining and (#priorities + 1) or nil
end

local function AvailableFollowers(followerTypeID)
	local result = {}
	local followers = C_Garrison.GetFollowers(followerTypeID) or {}
	for _, follower in ipairs(followers) do
		if type(follower.followerID) == "string" and follower.isCollected ~= false then
			local status = C_Garrison.GetFollowerStatus(follower.followerID)
			if not status then
				result[#result + 1] = follower
			end
		end
	end
	table.sort(result, function(a, b)
		local al = tonumber(a.level) or 0
		local bl = tonumber(b.level) or 0
		if al == bl then return (tonumber(a.iLevel) or 0) < (tonumber(b.iLevel) or 0) end
		return al < bl
	end)
	return result
end

local function EvaluateTeam(missionID, team)
	local added = {}
	for slot, follower in ipairs(team) do
		local ok = CallAccepted(C_Garrison.AddFollowerToMission, missionID, follower.followerID, slot)
		if not ok then break end
		added[#added + 1] = follower
	end
	local chance = 0
	if #added == #team then
		local ok, value = SafeCall(C_Garrison.GetMissionSuccessChance, missionID)
		if ok then chance = tonumber(value) or 0 end
	end
	for slot = #added, 1, -1 do
		SafeCall(C_Garrison.RemoveFollowerFromMission, missionID, added[slot].followerID, slot)
	end
	return chance
end

local function MissionMechanics(missionID)
	local mechanics = {}
	local ok, deployment = SafeCall(C_Garrison.GetMissionDeploymentInfo, missionID)
	if not ok or type(deployment) ~= "table" then return mechanics end
	for _, enemy in ipairs(deployment.enemies or {}) do
		for _, mechanic in pairs(enemy.mechanics or {}) do
			local mechanicID = mechanic.mechanicTypeID
			if mechanicID then mechanics[mechanicID] = true end
		end
	end
	return mechanics
end

local function TeamCounterCoverage(team, missionMechanics, abilityCache)
	local covered = {}
	for _, follower in ipairs(team) do
		local followerID = follower.followerID
		local abilities = abilityCache[followerID]
		if abilities == nil then
			local ok, result = SafeCall(C_Garrison.GetFollowerAbilities, followerID)
			abilities = ok and type(result) == "table" and result or {}
			abilityCache[followerID] = abilities
		end
		for _, ability in ipairs(abilities) do
			for mechanicID in pairs(ability.counters or {}) do
				if missionMechanics[mechanicID] then covered[mechanicID] = true end
			end
		end
	end
	local count = 0
	for _ in pairs(covered) do count = count + 1 end
	return count
end

local function FindTeam(mission, followers, reserved, threshold)
	local slots = math.max(1, tonumber(mission.numFollowers) or 1)
	local chosen, best, bestChance, bestCounters, bestLevelTotal, tested = {}, nil, -1, -1, math.huge, 0
	local missionMechanics, abilityCache = MissionMechanics(mission.missionID), {}
	local function Search(startIndex)
		if tested >= MAX_COMBINATIONS then return end
		if #chosen == slots then
			tested = tested + 1
			local chance = EvaluateTeam(mission.missionID, chosen)
			local counters, levelTotal = TeamCounterCoverage(chosen, missionMechanics, abilityCache), 0
			for _, follower in ipairs(chosen) do levelTotal = levelTotal + (tonumber(follower.level) or 0) end
			local better = not best
				or counters > bestCounters
				or counters == bestCounters and levelTotal < bestLevelTotal
				or counters == bestCounters and levelTotal == bestLevelTotal and chance < bestChance
			if chance >= threshold and better then
				best, bestChance, bestCounters, bestLevelTotal = {}, chance, counters, levelTotal
				for i, follower in ipairs(chosen) do best[i] = follower end
			end
			if tested % 20 == 0 then coroutine.yield() end
			return
		end
		for index = startIndex, #followers do
			local follower = followers[index]
			if not reserved[follower.followerID] then
				chosen[#chosen + 1] = follower
				Search(index + 1)
				chosen[#chosen] = nil
			end
		end
	end
	Search(1)
	return best, bestChance, bestCounters
end

function Planner:GetThreshold()
	return self.followerTypeID == SHIPYARD and GetDB().shipyardThreshold or GetDB().garrisonThreshold
end

function Planner:SetThreshold(value)
	value = math.max(50, math.min(100, math.floor((tonumber(value) or 100) / 5 + 0.5) * 5))
	if self.followerTypeID == SHIPYARD then GetDB().shipyardThreshold = value else GetDB().garrisonThreshold = value end
	if self.thresholdText then self.thresholdText:SetText(value .. "%") end
end

function Planner:RefreshAvailableFollowers()
	if not self.availableFollowersText then return end
	local count = #AvailableFollowers(self.followerTypeID)
	if self.followerTypeID == SHIPYARD then
		self.availableFollowersText:SetFormattedText("%d ship%s available", count, count == 1 and "" or "s")
	else
		self.availableFollowersText:SetFormattedText("%d follower%s available", count, count == 1 and "" or "s")
	end
end

function Planner:SetFollowerType(followerTypeID)
	if followerTypeID == GARRISON or followerTypeID == SHIPYARD then self.followerTypeID = followerTypeID end
	self.followerTypeID = self.followerTypeID or ((_G.GarrisonMissionFrame and _G.GarrisonMissionFrame.followerTypeID) or GARRISON)
	if self.modeText then self.modeText:SetText(self.followerTypeID == SHIPYARD and "Shipyard Missions" or "Garrison Missions") end
	if self.shipConstructionPanel then
		self.shipConstructionPanel:SetShown(self.followerTypeID == SHIPYARD)
		if self.followerTypeID == SHIPYARD then self:RefreshShipConstruction() end
	end
	if self.footerNote then
		self.footerNote:ClearAllPoints()
		if self.followerTypeID == SHIPYARD then
			self.footerNote:SetPoint("BOTTOMLEFT", 260, 26)
			self.footerNote:SetWidth(384)
		else
			self.footerNote:SetPoint("BOTTOMLEFT", 24, 26)
			self.footerNote:SetWidth(620)
		end
	end
	self:SetThreshold(self:GetThreshold())
	self:RefreshAvailableFollowers()
	self:RefreshPriorityControls()
	self:RefreshCompletionButton()
	self:AttachTableButtons()
end

function Planner:RefreshShipConstruction()
	local panel = self.shipConstructionPanel
	if not panel then return end
	local options = _G.GarrisonFollowerOptions and _G.GarrisonFollowerOptions[SHIPYARD]
	local garrisonType = options and options.garrisonType
	if not garrisonType then
		panel.queue:SetText("Construction data unavailable")
		panel.next:SetText("")
		panel.all:SetText("")
		return
	end
	local containers, seenContainers = {}, {}
	local function AddShipmentContainers(api)
		local ok, values = SafeCall(api, garrisonType)
		if not ok or type(values) ~= "table" then return end
		for _, containerID in ipairs(values) do
			containerID = tonumber(containerID)
			if containerID and not seenContainers[containerID] then
				seenContainers[containerID] = true
				containers[#containers + 1] = containerID
			end
		end
	end
	AddShipmentContainers(C_Garrison.GetFollowerShipments)
	AddShipmentContainers(C_Garrison.GetLooseShipments)
	local shipment, fallbackShipment
	for _, containerID in ipairs(containers) do
		local infoOK, name, _, _, ready, total, creationTime, duration, timeLeft,
			_, _, _, _, followerID = pcall(C_Garrison.GetLandingPageShipmentInfoByContainerID, containerID)
		if infoOK and name and tonumber(total) and tonumber(total) > 0 then
			local candidate = {
				name = name,
				ready = tonumber(ready) or 0,
				total = tonumber(total) or 0,
				creationTime = tonumber(creationTime) or 0,
				duration = tonumber(duration) or 0,
				timeLeft = timeLeft,
			}
			fallbackShipment = fallbackShipment or candidate
			if followerID then
				shipment = candidate
				break
			end
		end
	end
	shipment = shipment or fallbackShipment
	panel.shipment = shipment
	if not shipment then
		panel.queue:SetText("No ships under construction")
		panel.next:SetText("")
		panel.all:SetText("")
		return
	end
	panel.queue:SetFormattedText("Queue: %d ready / %d total", shipment.ready, shipment.total)
	self:UpdateShipConstructionTimer()
end

function Planner:UpdateShipConstructionTimer()
	local panel = self.shipConstructionPanel
	local shipment = panel and panel.shipment
	if not panel or not shipment then return end
	local pending = math.max(0, shipment.total - shipment.ready)
	if pending == 0 then
		panel.next:SetText("Next ship: Ready")
		panel.all:SetText("All queued ships are ready")
		return
	end
	local remaining = math.max(0, shipment.creationTime + shipment.duration - GetServerTime())
	local allRemaining = remaining + math.max(0, pending - 1) * shipment.duration
	panel.next:SetText("Next ship: " .. SecondsToTime(remaining, false, true, 1))
	panel.all:SetText("All complete: " .. SecondsToTime(allRemaining, false, true, 1))
end

function Planner:CreateShipConstructionPanel(host)
	if self.shipConstructionPanel or not self.frame then return end
	local panel = CreateFrame("Frame", "SlickUIShipConstructionPanel", self.frame, "BackdropTemplate")
	panel:SetSize(230, 90)
	panel:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 24, -484)
	panel:SetFrameLevel(self.frame:GetFrameLevel() + 5)
	Theme:CreatePanel(panel, 0.96, true)
	panel.title = Theme:CreateText(panel, 14, "OVERLAY")
	panel.title:SetPoint("TOPLEFT", 12, -10)
	panel.title:SetText("Ship Construction")
	panel.queue = Theme:CreateText(panel, 11, "OVERLAY")
	panel.queue:SetPoint("TOPLEFT", panel.title, "BOTTOMLEFT", 0, -8)
	panel.next = Theme:CreateText(panel, 11, "OVERLAY")
	panel.next:SetPoint("TOPLEFT", panel.queue, "BOTTOMLEFT", 0, -5)
	panel.all = Theme:CreateText(panel, 11, "OVERLAY")
	panel.all:SetPoint("TOPLEFT", panel.next, "BOTTOMLEFT", 0, -5)
	panel:SetScript("OnShow", function()
		SafeCall(C_Garrison.RequestLandingPageShipmentInfo)
		Planner:RefreshShipConstruction()
	end)
	panel:SetScript("OnUpdate", function(self, elapsed)
		self.elapsed = (self.elapsed or 0) + elapsed
		if self.elapsed >= 1 then
			self.elapsed = 0
			Planner:UpdateShipConstructionTimer()
		end
	end)
	self.shipConstructionPanel = panel
	panel:SetShown(self.followerTypeID == SHIPYARD)
	self:RefreshShipConstruction()
end

function Planner:GetCompletedMissions()
	local ok, missions = SafeCall(C_Garrison.GetCompleteMissions, self.followerTypeID)
	return ok and type(missions) == "table" and missions or {}
end

function Planner:RefreshCompletionButton()
	if not self.completeButton then return end
	local count = #self:GetCompletedMissions()
	self.completeButton:SetText(count > 0 and ("Complete Ready (" .. count .. ")") or "Complete Ready")
	self.completeButton:SetEnabled(count > 0 and not self.completing and not self.planning and not self.startPending)
end

local function HideBlizzardCompleteDialog(followerTypeID)
	local hosts = { _G.GarrisonMissionFrame, _G.GarrisonShipyardFrame }
	for _, host in ipairs(hosts) do
		if host and (not host.followerTypeID or host.followerTypeID == followerTypeID) then
			local ok, dialog = SafeCall(host.GetCompleteDialog, host)
			if ok and dialog and dialog.Hide then dialog:Hide() end
		end
	end
end

function Planner:ConfirmMissionStarted(followerTypeID, missionID)
	self:Log("MISSION_STARTED_EVENT", followerTypeID, missionID, self.followerTypeID, self.pendingMissionID)
	if not self.startPending or missionID ~= self.pendingMissionID then return end
	if followerTypeID ~= self.followerTypeID then return end

	local entry = self.pendingStartEntry
	if self.plan and self.plan[1] and self.plan[1].mission.missionID == missionID then
		table.remove(self.plan, 1)
	end
	self.startConfirmed = true
	self.startPending = false
	self.pendingMissionID = nil
	self.pendingStartEntry = nil
	self:RefreshRows()
	self:RefreshAvailableFollowers()
	self.status:SetText(((entry and entry.mission.name) or "Mission") .. " started.")
end

function Planner:GetProfile()
	local db, prefix = GetDB(), ProfilePrefix(self.followerTypeID)
	return db[prefix .. "Priorities"], db[prefix .. "Enabled"], prefix
end

function Planner:RefreshPriorityControls()
	if not self.priorityRows then return end
	local priorities, enabled, prefix = self:GetProfile()
	local labels = {
		gear = "Gear / equipment",
		gold = "Gold",
		resources = self.followerTypeID == SHIPYARD and "Oil / resources" or "Garrison resources",
		xp = "Follower XP",
		other = "Other rewards",
	}
	for index, row in ipairs(self.priorityRows) do
		local kind = priorities[index]
		row.kind = kind
		row.check:SetChecked(enabled[kind] ~= false)
		row.check.label:SetText(index .. ".  " .. (labels[kind] or kind))
		row.up:SetEnabled(index > 1)
		row.down:SetEnabled(index < #priorities)
	end
	if self.remainingCheck then self.remainingCheck:SetChecked(GetDB()[prefix .. "RunRemaining"] ~= false) end
end

function Planner:MovePriority(index, direction)
	local priorities = self:GetProfile()
	local target = index + direction
	if target < 1 or target > #priorities then return end
	priorities[index], priorities[target] = priorities[target], priorities[index]
	self.plan = nil
	self:RefreshPriorityControls()
	self:RefreshRows()
end

function Planner:ResetPriorityProfile()
	local db, prefix = GetDB(), ProfilePrefix(self.followerTypeID)
	db[prefix .. "Priorities"] = CopyTable(PRIORITY_DEFAULTS)
	db[prefix .. "Enabled"] = CopyTable(DEFAULTS[prefix .. "Enabled"])
	db[prefix .. "RunRemaining"] = true
	self.plan = nil
	self:RefreshPriorityControls()
	self:RefreshRows()
end

function Planner:ClearQueue()
	if self.completing then
		self.status:SetText("Wait for mission completion collection to finish before clearing the queue.")
		return
	end

	-- Start Next can only stage the first queued mission. Clear that lineup if
	-- the server rejected it, without touching manually staged later missions.
	local stagedEntry = self.pendingStartEntry or (self.plan and self.plan[1])
	local stagedMissionID = stagedEntry and stagedEntry.mission and stagedEntry.mission.missionID
	if stagedMissionID then
		local infoOK, info = SafeCall(C_Garrison.GetBasicMissionInfo, stagedMissionID)
		local followers = infoOK and info and info.followers
		if type(followers) == "table" then
			for _, assigned in pairs(followers) do
				local followerID = type(assigned) == "table" and assigned.followerID or assigned
				if followerID then SafeCall(C_Garrison.RemoveFollowerFromMission, stagedMissionID, followerID) end
			end
		end
	end

	self.worker = nil
	self.planning = false
	self.startPending = false
	self.startConfirmed = false
	self.pendingMissionID = nil
	self.pendingStartEntry = nil
	self.plan = nil
	if self.frame then self.frame:SetScript("OnUpdate", nil) end
	if self.progressBar then self.progressBar:Hide() end
	if self.status then self.status:Show() end
	self:RefreshRows()
	self:RefreshAvailableFollowers()
	self.status:SetText("Mission queue cleared.")
end

function Planner:UpdatePlanningProgress(processed, total)
	if not self.progressBar then return end
	total = math.max(0, tonumber(total) or 0)
	processed = math.max(0, math.min(total, tonumber(processed) or 0))
	self.progressBar:SetMinMaxValues(0, math.max(1, total))
	self.progressBar:SetValue(processed)
	self.progressBar.text:SetFormattedText("Building teams: %d / %d", processed, total)
end

function Planner:PopulatePlanIcons(row, entry)
	for _, icon in ipairs(row.missionIcons) do icon:Hide() end
	for _, group in ipairs(row.followerGroups) do
		group.portrait:Hide()
		for _, icon in ipairs(group.abilities) do icon:Hide() end
	end
	for _, icon in ipairs(row.rewardIcons) do icon:Hide() end
	if not entry then return end

	local deploymentOK, deployment = SafeCall(C_Garrison.GetMissionDeploymentInfo, entry.mission.missionID)
	local missionIndex = 1
	if deploymentOK and type(deployment) == "table" then
		for _, enemy in pairs(deployment.enemies or {}) do
			for _, mechanic in pairs(enemy.mechanics or {}) do
				local ability = mechanic.ability or mechanic
				if ability.icon and missionIndex <= #row.missionIcons then
					SetPlanIcon(row.missionIcons[missionIndex], ability.icon, ability.name or mechanic.name or "Mission ability", ability.description or mechanic.description)
					missionIndex = missionIndex + 1
				end
			end
		end
	end

	for followerIndex, follower in ipairs(entry.team or {}) do
		local group = row.followerGroups[followerIndex]
		if not group then break end
		local portrait = follower.portraitIconID or follower.iconID
		SetPlanIcon(group.portrait, portrait, follower.name or "Follower", string.format("Level %d", tonumber(follower.level) or 0))
		local abilitiesOK, abilities = SafeCall(C_Garrison.GetFollowerAbilities, follower.followerID)
		if abilitiesOK and type(abilities) == "table" then
			local displayed = {}
			for abilityIndex, ability in ipairs(abilities) do
				local icon = group.abilities[abilityIndex]
				if ability.icon and icon then
					local title = (follower.name and (follower.name .. ": ") or "") .. (ability.name or "Follower ability")
					SetPlanIcon(icon, ability.icon, title, ability.description)
					displayed[#displayed + 1] = icon
				end
			end
			for iconIndex, icon in ipairs(displayed) do
				icon:ClearAllPoints()
				icon:SetPoint("TOP", group.portrait, "BOTTOM", (iconIndex - ((#displayed + 1) / 2)) * 20, 0)
			end
		end
	end

	local rewardsOK, rewards = SafeCall(C_Garrison.GetMissionRewardInfo, entry.mission.missionID)
	rewards = rewardsOK and type(rewards) == "table" and rewards or entry.mission.rewards or {}
	local rewardIndex = 1
	for _, reward in pairs(rewards) do
		if rewardIndex > #row.rewardIcons then break end
		local texture, title = reward.icon, reward.title
		if reward.itemID then
			texture = texture or (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(reward.itemID))
			title = title or (C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(reward.itemID))
		elseif reward.currencyID == 0 then
			texture = texture or 133784
			title = title or "Gold"
		elseif reward.currencyID then
			local currencyInfo = C_CurrencyInfo.GetCurrencyInfo(reward.currencyID)
			texture = texture or (currencyInfo and currencyInfo.iconFileID)
			title = title or (currencyInfo and currencyInfo.name)
		elseif reward.followerXP then
			texture = texture or 894556
			title = title or "Follower XP"
		end
		if texture then
			SetPlanIcon(row.rewardIcons[rewardIndex], texture, title or "Mission reward", reward.itemLink, reward.quantity or reward.followerXP)
			rewardIndex = rewardIndex + 1
		end
	end
end

function Planner:RefreshRows()
	if not self.rows then return end
	local count = self.plan and #self.plan or 0
	if self.EnsureMissionRows then self:EnsureMissionRows(count) end
	for i, row in ipairs(self.rows) do
		local entry = self.plan and self.plan[i]
		if entry then
			row:Show()
			row.name:SetText(entry.mission.name or ("Mission " .. entry.mission.missionID))
			self:PopulatePlanIcons(row, entry)
			row.detail:SetText(string.format("%s  •  Level %d  •  %d%%  •  %d follower%s", entry.kind, MissionLevel(entry.mission), entry.chance, #entry.team, #entry.team == 1 and "" or "s"))
		else
			self:PopulatePlanIcons(row, nil)
			row:Hide()
		end
	end
	if self.missionScrollChild then
		self.missionScrollChild:SetHeight(math.max(self.missionScrollHeight or 1, count * 71))
	end
	self.status:SetText(count > 0 and (count .. " mission" .. (count == 1 and "" or "s") .. " planned") or "No mission plan calculated.")
	self.startButton:SetEnabled(count > 0 and not self.planning and not self.completing and not self.startPending)
	self:RefreshCompletionButton()
end

function Planner:Plan()
	if self.planning then return end
	self:SetFollowerType(self.followerTypeID)
	local missions = {}
	local missionsOK, returnedMissions = SafeCall(C_Garrison.GetAvailableMissions, missions, self.followerTypeID)
	if #missions == 0 and missionsOK and type(returnedMissions) == "table" then missions = returnedMissions end
	local filtered = {}
	for _, mission in ipairs(missions) do
		if PriorityRank(RewardKind(mission), self.followerTypeID) then filtered[#filtered + 1] = mission end
	end
	missions = filtered
	if #missions == 0 then
		self.plan = {}
		self:RefreshRows()
		self.status:SetText(self.followerTypeID == SHIPYARD and "No available Shipyard missions match these reward settings." or "No available Garrison missions match these reward settings.")
		return
	end
	table.sort(missions, function(a, b)
		local ar, br = PriorityRank(RewardKind(a), self.followerTypeID), PriorityRank(RewardKind(b), self.followerTypeID)
		if ar == br then return MissionLevel(a) > MissionLevel(b) end
		return ar < br
	end)
	local followers, threshold = AvailableFollowers(self.followerTypeID), self:GetThreshold()
	self.plan, self.planning = {}, true
	self.planProcessed = 0
	self.planTotal = #missions
	self.status:SetText("Planning missions…")
	self.startButton:SetEnabled(false)
	self.status:Hide()
	self.progressBar:Show()
	self:UpdatePlanningProgress(0, self.planTotal)
	self.worker = coroutine.create(function()
		local reserved = {}
		for _, mission in ipairs(missions) do
			local team, chance, counters = FindTeam(mission, followers, reserved, threshold)
			if team then
				for _, follower in ipairs(team) do reserved[follower.followerID] = true end
				self.plan[#self.plan + 1] = { mission = mission, team = team, chance = chance, counters = counters, kind = RewardKind(mission) }
			end
			self.planProcessed = self.planProcessed + 1
			coroutine.yield()
		end
	end)
	self.frame:SetScript("OnUpdate", function()
		for _ = 1, 2 do
			if not self.worker or coroutine.status(self.worker) == "dead" then
				self.frame:SetScript("OnUpdate", nil)
				self.planning, self.worker = false, nil
				self.progressBar:Hide()
				self.status:Show()
				self:RefreshRows()
				return
			end
			local ok, message = coroutine.resume(self.worker)
			if not ok then
				self.frame:SetScript("OnUpdate", nil)
				self.planning, self.worker = false, nil
				self.progressBar:Hide()
				self.status:Show()
				self.status:SetText("Planning stopped: " .. tostring(message))
				return
			end
			self:UpdatePlanningProgress(self.planProcessed, self.planTotal)
		end
	end)
end

function Planner:CompleteReady()
	if self.completing or self.planning or self.startPending then return end
	local missions = self:GetCompletedMissions()
	if #missions == 0 then
		self.status:SetText("No completed missions are ready.")
		self:RefreshCompletionButton()
		return
	end

	self.completing = true
	self.startButton:SetEnabled(false)
	self.completeButton:SetEnabled(false)
	local total, completed, index = #missions, 0, 1

	local function Finish(message)
		self.completing = false
		self.plan = nil -- Completed followers may now be available for a new plan.
		HideBlizzardCompleteDialog(self.followerTypeID)
		self:RefreshRows()
		self.status:SetText(message)
		self:RefreshCompletionButton()
	end

	local function ProcessNext()
		local mission = missions[index]
		if not mission then
			Finish(string.format("Completed %d mission%s and collected the rewards.", completed, completed == 1 and "" or "s"))
			return
		end

		local missionID = mission.missionID
		local missionName = mission.name or ("Mission " .. tostring(missionID))
		self.status:SetText(string.format("Completing %d/%d: %s...", index, total, missionName))
		local marked = SafeCall(C_Garrison.MarkMissionComplete, missionID)
		local rolled = marked and SafeCall(C_Garrison.MissionBonusRoll, missionID)
		if not marked or not rolled then
			Finish("Blizzard did not accept completion for " .. missionName .. ". Try again.")
			return
		end

		local attempts = 0
		local function ConfirmCompletion()
			attempts = attempts + 1
			local stillReady = false
			for _, readyMission in ipairs(self:GetCompletedMissions()) do
				if readyMission.missionID == missionID then
					stillReady = true
					break
				end
			end
			if not stillReady then
				completed = completed + 1
				index = index + 1
				C_Timer.After(0.15, ProcessNext)
			elseif attempts < 24 then
				C_Timer.After(0.25, ConfirmCompletion)
			else
				Finish(missionName .. " did not finish on the server. It was left ready so you can retry safely.")
			end
		end
		C_Timer.After(0.25, ConfirmCompletion)
	end

	ProcessNext()
end

function Planner:StartNext()
	if self.planning or self.startPending or not self.plan or not self.plan[1] then return end
	local entry = self.plan[1]
	local missionID = entry.mission.missionID
	self:Log("START_CLICK", self.followerTypeID, missionID, entry.mission.name, #entry.team)

	-- Blizzard silently rejects StartMission while the character is above the
	-- active follower cap. Detect that before changing the staged lineup.
	local capOK, aboveSoftCap = SafeCall(C_Garrison.IsAboveFollowerSoftCap, self.followerTypeID)
	local allowOK, allowAboveSoftCap = SafeCall(C_Garrison.AllowMissionStartAboveSoftCap, self.followerTypeID)
	if capOK and aboveSoftCap and (not allowOK or not allowAboveSoftCap) then
		self.status:SetText("Too many active followers. Deactivate a Garrison follower, then click Start Next again.")
		return
	end

	-- The Blizzard mission UI can leave followers staged on a mission while the
	-- player previews teams. Clear that pending lineup first; otherwise a slot
	-- can reject our planned follower and the mission will never actually start.
	local pendingOK, pendingInfo = SafeCall(C_Garrison.GetBasicMissionInfo, missionID)
	local pendingFollowers = pendingOK and pendingInfo and pendingInfo.followers
	self:Log("PENDING_LINEUP", pendingOK, pendingFollowers)
	if type(pendingFollowers) == "table" then
		for _, assigned in pairs(pendingFollowers) do
			local followerID = type(assigned) == "table" and assigned.followerID or assigned
			if followerID then SafeCall(C_Garrison.RemoveFollowerFromMission, missionID, followerID) end
		end
	end

	local added = {}
	for slot, follower in ipairs(entry.team) do
		local accepted = CallAccepted(C_Garrison.AddFollowerToMission, missionID, follower.followerID, slot)
		self:Log("ADD_FOLLOWER", slot, follower.followerID, accepted)
		if accepted then
			added[#added + 1] = follower
		end
	end
	if #added ~= #entry.team then
		for slot = #added, 1, -1 do SafeCall(C_Garrison.RemoveFollowerFromMission, missionID, added[slot].followerID) end
		self.status:SetText("Followers changed; calculate the plan again.")
		self:RefreshRows()
		return
	end

	-- Confirm that Blizzard has the exact planned lineup before asking it to
	-- start the mission. StartMission itself has no useful success return.
	local infoOK, info = SafeCall(C_Garrison.GetBasicMissionInfo, missionID)
	local lineup = infoOK and info and info.followers
	self:Log("IMMEDIATE_LINEUP", infoOK, lineup)
	local expected = {}
	for _, follower in ipairs(entry.team) do expected[follower.followerID] = true end
	local matched = type(lineup) == "table" and #lineup == #entry.team
	if matched then
		for _, assigned in ipairs(lineup) do
			local followerID = type(assigned) == "table" and assigned.followerID or assigned
			if not expected[followerID] then matched = false break end
		end
	end
	if not matched then
		for slot = #added, 1, -1 do SafeCall(C_Garrison.RemoveFollowerFromMission, missionID, added[slot].followerID) end
		self.status:SetText("The planned followers were not assigned. Calculate again.")
		self:RefreshRows()
		return
	end

	self.startPending = true
	self.startConfirmed = false
	self.pendingMissionID = missionID
	self.pendingStartEntry = entry
	self.startButton:SetEnabled(false)
	self.status:SetText("Starting " .. (entry.mission.name or "mission") .. "...")

	-- Keep the start inside the Start Next button's hardware event after the
	-- lineup has been synchronously verified above.
	local startOK, startResult = SafeCall(C_Garrison.StartMission, missionID)
	self:Log("START_MISSION_CALLED", startOK, startResult)
	if not startOK then
		self.startPending = false
		self.pendingMissionID = nil
		self.pendingStartEntry = nil
		self:RefreshRows()
		self.status:SetText("Blizzard rejected the mission start. It remains in the plan.")
		return
	end

	-- The server event is authoritative. Keep the row in the plan until
	-- GARRISON_MISSION_STARTED confirms this exact mission.
	local attempts = 0
	local function ConfirmStart()
		if self.startConfirmed or not self.startPending or self.pendingMissionID ~= missionID then return end
		attempts = attempts + 1
		if attempts < 20 then
			C_Timer.After(0.25, ConfirmStart)
		else
			self.startPending = false
			self.pendingMissionID = nil
			self.pendingStartEntry = nil
			self:RefreshRows()
			self.status:SetText("Blizzard did not confirm the mission start. It remains in the plan; try again.")
		end
	end
	C_Timer.After(0.25, ConfirmStart)
end

function Planner:CreateWindow()
	if self.frame then return end
	local frame = CreateFrame("Frame", "SlickUIGarrisonMissionsFrame", UIParent, "BackdropTemplate")
	frame:SetSize(1000, 610)
	frame:SetPoint("CENTER")
	frame:SetFrameStrata("DIALOG")
	frame:SetClampedToScreen(true)
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", frame.StartMoving)
	frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
	Theme:CreatePanel(frame, 0.97, true)
	frame:Hide()
	self.frame = frame

	local addonIcon = frame:CreateTexture(nil, "ARTWORK")
	addonIcon:SetSize(44, 44)
	addonIcon:SetPoint("TOPLEFT", 20, -15)
	addonIcon:SetTexture("Interface\\AddOns\\SlickUI_GarrisonMissions\\Media\\SlickUI_GarrisonMissions_Icon.png")
	addonIcon:SetTexCoord(0.04, 0.96, 0.04, 0.96)

	local title = Theme:CreateText(frame, 25, "OVERLAY")
	title:SetText("SlickUI: Garrison & Shipyard")
	title:SetPoint("TOPLEFT", 74, -20)
	self.modeText = Theme:CreateText(frame, 15, "OVERLAY")
	self.modeText:SetText("Garrison Missions")
	self.modeText:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -5)
	local close = CreateButton(frame, "X", 30, function() frame:Hide() end)
	close:SetPoint("TOPRIGHT", -12, -12)

	local thresholdLabel = Theme:CreateText(frame, 13, "OVERLAY")
	thresholdLabel:SetText("Minimum success")
	thresholdLabel:SetPoint("TOPLEFT", 24, -82)
	local minus = CreateButton(frame, "-", 30, function() self:SetThreshold(self:GetThreshold() - 5) end)
	minus:SetPoint("LEFT", thresholdLabel, "RIGHT", 16, 0)
	self.thresholdText = Theme:CreateText(frame, 14, "OVERLAY")
	self.thresholdText:SetText("80%")
	self.thresholdText:SetPoint("LEFT", minus, "RIGHT", 12, 0)
	local plus = CreateButton(frame, "+", 30, function() self:SetThreshold(self:GetThreshold() + 5) end)
	plus:SetPoint("LEFT", self.thresholdText, "RIGHT", 12, 0)

	local plan = CreateButton(frame, "Plan Missions", 150, function() self:Plan() end)
	plan:SetPoint("TOPRIGHT", -190, -76)
	local clearQueue = CreateButton(frame, "Clear Queue", 110, function() self:ClearQueue() end)
	clearQueue:SetPoint("LEFT", plan, "RIGHT", 8, 0)

	self.availableFollowersText = Theme:CreateText(frame, 13, "OVERLAY")
	self.availableFollowersText:SetPoint("TOP", frame, "TOP", 0, -83)
	self.availableFollowersText:SetText("0 followers available")

	local warning = Theme:CreateText(frame, 12, "OVERLAY")
	warning:SetText("Shipyard defaults to 100% because failed missions may destroy ships. Missions are never started automatically.")
	warning:SetTextColor(1, 0.55, 0.1)
	warning:SetPoint("TOPLEFT", 24, -120)
	warning:SetWidth(950)

	self.status = Theme:CreateText(frame, 13, "OVERLAY")
	self.status:SetText("No mission plan calculated.")
	self.status:SetPoint("TOPLEFT", 282, -152)

	self.progressBar = CreateFrame("StatusBar", nil, frame, "BackdropTemplate")
	self.progressBar:SetSize(690, 18)
	self.progressBar:SetPoint("TOPLEFT", 282, -145)
	self.progressBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
	self.progressBar:SetStatusBarColor(1, 0.48, 0.05, 1)
	self.progressBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
	self.progressBar:SetBackdropColor(0.05, 0.06, 0.08, 0.95)
	self.progressBar:SetBackdropBorderColor(0.35, 0.38, 0.45, 1)
	self.progressBar.text = Theme:CreateText(self.progressBar, 11, "OVERLAY")
	self.progressBar.text:SetPoint("CENTER")
	self.progressBar.text:SetText("Building teams: 0 / 0")
	self.progressBar:Hide()

	local priorityTitle = Theme:CreateText(frame, 16, "OVERLAY")
	priorityTitle:SetText("Reward Priorities")
	priorityTitle:SetPoint("TOPLEFT", 24, -152)
	local priorityHelp = Theme:CreateText(frame, 10, "OVERLAY")
	priorityHelp:SetText("Enable rewards and arrange their planning order.")
	priorityHelp:SetTextColor(0.7, 0.75, 0.82)
	priorityHelp:SetPoint("TOPLEFT", priorityTitle, "BOTTOMLEFT", 0, -3)
	self.priorityRows = {}
	for index = 1, #PRIORITY_DEFAULTS do
		local rowIndex = index
		local row = CreateFrame("Frame", nil, frame)
		row:SetSize(238, 32)
		row:SetPoint("TOPLEFT", 24, -196 - ((index - 1) * 36))
		row.check = CreateCheckbox(row, "Priority", function(checked)
			local _, enabled = self:GetProfile()
			if row.kind then enabled[row.kind] = checked end
			self.plan = nil
			self:RefreshRows()
		end)
		row.check:SetPoint("LEFT", 0, 0)
		row.up = CreateButton(row, "Up", 38, function() self:MovePriority(rowIndex, -1) end)
		row.up:SetPoint("RIGHT", row, "RIGHT", -42, 0)
		row.down = CreateButton(row, "Dn", 38, function() self:MovePriority(rowIndex, 1) end)
		row.down:SetPoint("RIGHT", row, "RIGHT", 0, 0)
		self.priorityRows[rowIndex] = row
	end
	self.remainingCheck = CreateCheckbox(frame, "Run remaining missions", function(checked)
		local _, _, prefix = self:GetProfile()
		GetDB()[prefix .. "RunRemaining"] = checked
		self.plan = nil
		self:RefreshRows()
	end)
	self.remainingCheck:SetPoint("TOPLEFT", 24, -386)
	local resetPriorities = CreateButton(frame, "Restore Priority Defaults", 210, function() self:ResetPriorityProfile() end)
	resetPriorities:SetPoint("TOPLEFT", 24, -422)
	local separateNote = Theme:CreateText(frame, 10, "OVERLAY")
	separateNote:SetText("Garrison and Shipyard settings are saved separately.")
	separateNote:SetTextColor(0.7, 0.75, 0.82)
	separateNote:SetPoint("TOPLEFT", 24, -456)
	separateNote:SetWidth(230)

	local missionScroll = CreateFrame("ScrollFrame", "SlickUIGarrisonMissionsScrollFrame", frame, "UIPanelScrollFrameTemplate")
	missionScroll:SetPoint("TOPLEFT", 282, -176)
	missionScroll:SetPoint("BOTTOMRIGHT", -28, 62)
	missionScroll:SetClipsChildren(true)
	self.missionScroll = missionScroll
	self.missionScrollHeight = 372

	local missionScrollChild = CreateFrame("Frame", nil, missionScroll)
	missionScrollChild:SetSize(690, self.missionScrollHeight)
	missionScroll:SetScrollChild(missionScrollChild)
	self.missionScrollChild = missionScrollChild

	self.rows = {}
	local function CreateMissionRow(i)
		local row = CreateFrame("Frame", nil, missionScrollChild, "BackdropTemplate")
		row:SetSize(690, 68)
		row:SetPoint("TOPLEFT", 0, -((i - 1) * 71))
		row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
		row:SetBackdropColor(0.06, 0.07, 0.09, i % 2 == 0 and 0.75 or 0.5)
		row.name = Theme:CreateText(row, 13, "OVERLAY")
		row.name:SetText("Mission")
		row.name:SetPoint("TOPLEFT", 8, -24)
		row.name:SetWidth(335)
		row.name:SetWordWrap(false)
		row.detail = Theme:CreateText(row, 10, "OVERLAY")
		row.detail:SetText("")
		row.detail:SetTextColor(0.7, 0.75, 0.82)
		row.detail:SetPoint("TOPLEFT", 8, -43)
		row.detail:SetWidth(335)
		row.detail:SetWordWrap(false)

		row.missionLabel = Theme:CreateText(row, 8, "OVERLAY")
		row.missionLabel:SetText("MISSION")
		row.missionLabel:SetTextColor(1, 0.55, 0.15)
		row.missionLabel:SetPoint("TOPLEFT", 352, -1)
		row.followerLabel = Theme:CreateText(row, 8, "OVERLAY")
		row.followerLabel:SetText("FOLLOWERS")
		row.followerLabel:SetTextColor(0.3, 0.8, 1)
		row.followerLabel:SetPoint("TOPLEFT", 424, -1)
		row.rewardLabel = Theme:CreateText(row, 8, "OVERLAY")
		row.rewardLabel:SetText("REWARDS")
		row.rewardLabel:SetTextColor(0.4, 1, 0.4)
		row.rewardLabel:SetPoint("TOPLEFT", 624, -1)

		row.missionIcons = {}
		for iconIndex = 1, MISSION_ICON_LIMIT do
			local icon = CreatePlanIcon(row)
			icon:SetPoint("TOPLEFT", 352 + ((iconIndex - 1) * 21), -14)
			row.missionIcons[iconIndex] = icon
		end
		row.followerGroups = {}
		for followerIndex = 1, FOLLOWER_GROUP_LIMIT do
			local group = { abilities = {} }
			local groupX = 424 + ((followerIndex - 1) * 64)
			group.portrait = CreatePlanIcon(row)
			group.portrait:SetSize(36, 36)
			group.portrait:SetPoint("TOPLEFT", groupX + 11, -13)
			for abilityIndex = 1, FOLLOWER_ABILITY_LIMIT do
				local icon = CreatePlanIcon(row)
				icon:SetPoint("TOPLEFT", groupX + ((abilityIndex - 1) * 20), -49)
				group.abilities[abilityIndex] = icon
			end
			row.followerGroups[followerIndex] = group
		end
		row.rewardIcons = {}
		for iconIndex = 1, REWARD_ICON_LIMIT do
			local icon = CreatePlanIcon(row)
			icon:SetPoint("TOPLEFT", 624 + ((iconIndex - 1) * 21), -14)
			row.rewardIcons[iconIndex] = icon
		end
		self.rows[i] = row
	end
	function self:EnsureMissionRows(count)
		for i = #self.rows + 1, count do CreateMissionRow(i) end
	end
	self:EnsureMissionRows(6)
	self.startButton = CreateButton(frame, "Start Next", 150, function() self:StartNext() end)
	self.startButton:SetPoint("BOTTOMRIGHT", -24, 20)
	self.startButton:SetEnabled(false)
	self.completeButton = CreateButton(frame, "Complete Ready", 170, function() self:CompleteReady() end)
	self.completeButton:SetPoint("RIGHT", self.startButton, "LEFT", -8, 0)
	self.completeButton:SetEnabled(false)
	if DIAGNOSTICS_ENABLED then
		self.debugButton = CreateButton(frame, "Debug Journal", 130, function() self:ShowDebugLog() end)
		self.debugButton:SetPoint("RIGHT", self.completeButton, "LEFT", -8, 0)
	end
	local note = Theme:CreateText(frame, 11, "OVERLAY")
	note:SetText("Complete Ready collects finished missions without the long presentation. Higher-level missions are planned first within each reward type.")
	note:SetPoint("BOTTOMLEFT", 24, 26)
	note:SetWidth(620)
	self.footerNote = note
	self:SetFollowerType(self.followerTypeID)
end

function Planner:AttachTableButtons()
	local hosts = { _G.GarrisonMissionFrame, _G.GarrisonShipyardFrame }
	for _, host in ipairs(hosts) do
		if host and not host.SlickUIMissionsCloseHooked then
			host.SlickUIMissionsCloseHooked = true
			host:HookScript("OnHide", function()
				if self.frame then self.frame:Hide() end
			end)
		end
		if host and not host.SlickMissionsButton then
			local buttonHost = host
			local button = CreateButton(buttonHost, "Slick Missions", 132, function()
				if self.frame and self.frame:IsShown() then
					self.frame:Hide()
				else
					self:Show(buttonHost.followerTypeID)
				end
			end)
			button:SetPoint("TOPLEFT", buttonHost, "TOPLEFT", 225, -5)
			button:SetFrameLevel(buttonHost:GetFrameLevel() + 20)
			buttonHost.SlickMissionsButton = button
		end
		if host == _G.GarrisonShipyardFrame then self:CreateShipConstructionPanel(host) end
	end
end

function Planner:Show(followerTypeID)
	self:CreateWindow()
	if followerTypeID == GARRISON or followerTypeID == SHIPYARD then
		self:SetFollowerType(followerTypeID)
	elseif _G.GarrisonShipyardFrame and _G.GarrisonShipyardFrame:IsShown() then
		self:SetFollowerType(_G.GarrisonShipyardFrame.followerTypeID)
	else
		self:SetFollowerType((_G.GarrisonMissionFrame and _G.GarrisonMissionFrame.followerTypeID) or self.followerTypeID)
	end
	self.frame:Show()
	self.frame:Raise()
	self:RefreshRows()
	self:RefreshAvailableFollowers()
end

local function AttachWhenReady(attempt)
	attempt = attempt or 1
	Planner:AttachTableButtons()
	if _G.GarrisonShipyardFrame then
		SafeCall(C_Garrison.RequestLandingPageShipmentInfo)
		Planner:RefreshShipConstruction()
	elseif attempt < 8 then
		C_Timer.After(attempt == 1 and 0 or 0.25, function() AttachWhenReady(attempt + 1) end)
	end
end

Planner.followerTypeID = (_G.GarrisonMissionFrame and _G.GarrisonMissionFrame.followerTypeID) or GARRISON
Planner:CreateWindow()
AttachWhenReady()

local events = CreateFrame("Frame")
events:RegisterEvent("ADDON_LOADED")
events:RegisterEvent("GARRISON_MISSION_NPC_OPENED")
events:RegisterEvent("GARRISON_MISSION_STARTED")
events:RegisterEvent("GARRISON_MISSION_FINISHED")
events:RegisterEvent("GARRISON_MISSION_LIST_UPDATE")
events:RegisterEvent("GARRISON_FOLLOWER_LIST_UPDATE")
events:RegisterEvent("GARRISON_LANDINGPAGE_SHIPMENTS")
events:RegisterEvent("GARRISON_SHIPMENT_RECEIVED")
events:RegisterEvent("SHIPMENT_UPDATE")
events:SetScript("OnEvent", function(_, event, ...)
	Planner:Log("EVENT", event, ...)
	if event == "GARRISON_MISSION_NPC_OPENED" then
		local followerTypeID = ...
		Planner:SetFollowerType(followerTypeID)
		AttachWhenReady()
	elseif event == "ADDON_LOADED" then
		local loadedAddon = ...
		if loadedAddon == "Blizzard_GarrisonUI" then AttachWhenReady() end
	elseif event == "GARRISON_MISSION_STARTED" then
		Planner:ConfirmMissionStarted(...)
	elseif event == "GARRISON_LANDINGPAGE_SHIPMENTS" or event == "GARRISON_SHIPMENT_RECEIVED" or event == "SHIPMENT_UPDATE" then
		Planner:RefreshShipConstruction()
	else
		Planner:RefreshCompletionButton()
		Planner:RefreshAvailableFollowers()
	end
end)
