local addonName = ...
local addonPrefix = "WNW2"
local separator = "\031"
local maximumGoals = 30
local maximumOfficerRankIndex = 1
local slotsPerGuildBankTab = 98

local window = WhoNeedsWhatWindow
if not window then return end

local RefreshBankPage
local guildBankOpen = false
local bankScanTimer
local lastBankScanNotice = 0
local selectedGoalID
local bankListOffset = 1
local visibleGoalRows = 8

local function WNWPrint(color, message)
    print(color .. "Who Needs What:|r " .. message)
end

local function GetPlayerFullName()
    local name, realm = UnitFullName("player")
    if not name then return "Unknown" end
    if realm and realm ~= "" then return name .. "-" .. realm end
    return name
end

local function GetShortName(name)
    return name and (name:match("^[^-]+") or name) or ""
end

local function IsSamePlayer(firstName, secondName)
    if firstName == secondName then return true end
    if not firstName or not secondName then return false end

    local firstHasRealm = firstName:find("-", 1, true)
    local secondHasRealm = secondName:find("-", 1, true)

    if firstHasRealm and secondHasRealm then return false end
    return GetShortName(firstName) == GetShortName(secondName)
end

local function Trim(text)
    return tostring(text or ""):match("^%s*(.-)%s*$") or ""
end

local function CanGuildSync()
    local insideInstance = IsInInstance()
    return IsInGuild() and not insideInstance
end

local function CanManageBankGoals()
    return IsInGuild()
        and C_GuildInfo
        and C_GuildInfo.CanEditOfficerNote
        and C_GuildInfo.CanEditOfficerNote()
end

local function FindGuildRankIndex(playerName)
    if not IsInGuild() then return nil end

    for index = 1, GetNumGuildMembers() do
        local name, _, rankIndex = GetGuildRosterInfo(index)
        if name and IsSamePlayer(name, playerName) then
            return rankIndex
        end
    end

    return nil
end

local function IsOfficerSender(sender)
    if IsSamePlayer(sender, GetPlayerFullName()) then
        return CanManageBankGoals()
    end

    local rankIndex = FindGuildRankIndex(sender)
    return rankIndex ~= nil and rankIndex <= maximumOfficerRankIndex
end

local function NewDataVersion()
    local previous = WhoNeedsWhatDB.bankGoalsVersion or 0
    local candidate = (GetServerTime() * 1000) + math.random(0, 999)
    return math.max(previous + 1, candidate)
end

local function FindGoalByID(goalID)
    if not WhoNeedsWhatDB or not WhoNeedsWhatDB.bankGoals then return nil end

    for _, goal in ipairs(WhoNeedsWhatDB.bankGoals) do
        if goal.id == goalID then return goal end
    end

    return nil
end

local function FindMatchingGoal(itemID, itemName)
    local lowerName = Trim(itemName):lower()

    for _, goal in ipairs(WhoNeedsWhatDB.bankGoals) do
        if itemID and goal.itemID and goal.itemID == itemID then
            return goal
        end

        if lowerName ~= "" and (goal.item or ""):lower() == lowerName then
            return goal
        end
    end

    return nil
end

local function GetGoalBankCount(goal)
    if not WhoNeedsWhatDB or not WhoNeedsWhatDB.bankScanTime then return nil end

    if goal.itemID and WhoNeedsWhatDB.bankCountsByID then
        local count = WhoNeedsWhatDB.bankCountsByID[tostring(goal.itemID)]
        if count ~= nil then return count end
    end

    if WhoNeedsWhatDB.bankCountsByName then
        local count = WhoNeedsWhatDB.bankCountsByName[(goal.item or ""):lower()]
        if count ~= nil then return count end
    end

    return 0
end

local function SendGoal(goal, version, delay)
    if not CanGuildSync() or not CanManageBankGoals() then return end

    local message = table.concat({
        "GOAL_SET",
        tostring(version),
        goal.id,
        tostring(goal.itemID or 0),
        tostring(goal.quality or 1),
        tostring(goal.target or 1),
        goal.item or "Unknown item"
    }, separator)

    C_Timer.After(delay or 0, function()
        if CanGuildSync() then
            C_ChatInfo.SendAddonMessage(addonPrefix, message, "GUILD")
        end
    end)
end

local function SendAllBankGoals()
    if not CanGuildSync()
        or not CanManageBankGoals()
        or not WhoNeedsWhatDB then

        return
    end

    local version = WhoNeedsWhatDB.bankGoalsVersion or 0
    local clearMessage = table.concat({
        "GOALS_CLEAR",
        tostring(version)
    }, separator)

    C_ChatInfo.SendAddonMessage(addonPrefix, clearMessage, "GUILD")

    for index, goal in ipairs(WhoNeedsWhatDB.bankGoals) do
        SendGoal(goal, version, index * 0.10)
    end
end

local function SendGoalDelete(goalID, version)
    if not CanGuildSync() or not CanManageBankGoals() then return end

    local message = table.concat({
        "GOAL_DELETE",
        tostring(version),
        goalID
    }, separator)

    C_ChatInfo.SendAddonMessage(addonPrefix, message, "GUILD")
end

local function SendBankSnapshot()
    if not CanGuildSync()
        or not WhoNeedsWhatDB
        or not WhoNeedsWhatDB.bankSnapshotVersion
        or not WhoNeedsWhatDB.bankScanTime then

        return
    end

    local snapshotVersion = WhoNeedsWhatDB.bankSnapshotVersion
    local clearMessage = table.concat({
        "BANK_CLEAR",
        tostring(snapshotVersion),
        tostring(WhoNeedsWhatDB.bankScanTime),
        WhoNeedsWhatDB.bankScannedBy or GetPlayerFullName()
    }, separator)

    C_ChatInfo.SendAddonMessage(addonPrefix, clearMessage, "GUILD")

    for index, goal in ipairs(WhoNeedsWhatDB.bankGoals or {}) do
        local count = GetGoalBankCount(goal) or 0
        local message = table.concat({
            "BANK_COUNT",
            tostring(snapshotVersion),
            tostring(goal.itemID or 0),
            tostring(count),
            goal.item or "Unknown item"
        }, separator)

        C_Timer.After(index * 0.10, function()
            if CanGuildSync() then
                C_ChatInfo.SendAddonMessage(addonPrefix, message, "GUILD")
            end
        end)
    end
end

local function ScanGuildBank()
    bankScanTimer = nil
    if not guildBankOpen or not WhoNeedsWhatDB then return end

    local countsByID = {}
    local countsByName = {}
    local numberOfTabs = GetNumGuildBankTabs() or 0

    for tab = 1, numberOfTabs do
        local _, _, isViewable = GetGuildBankTabInfo(tab)

        if isViewable then
            for slot = 1, slotsPerGuildBankTab do
                local itemLink = GetGuildBankItemLink(tab, slot)
                local _, itemCount = GetGuildBankItemInfo(tab, slot)

                if itemLink and itemCount and itemCount > 0 then
                    local itemID = tonumber(itemLink:match("item:(%d+)"))
                    local itemName = itemLink:match("%[(.-)%]")

                    if itemID then
                        local key = tostring(itemID)
                        countsByID[key] = (countsByID[key] or 0) + itemCount
                    end

                    if itemName then
                        local key = itemName:lower()
                        countsByName[key] = (countsByName[key] or 0) + itemCount
                    end
                end
            end
        end
    end

    WhoNeedsWhatDB.bankCountsByID = countsByID
    WhoNeedsWhatDB.bankCountsByName = countsByName
    WhoNeedsWhatDB.bankScanTime = GetServerTime()
    WhoNeedsWhatDB.bankScannedBy = GetPlayerFullName()
    WhoNeedsWhatDB.bankSnapshotVersion =
        math.max(
            (WhoNeedsWhatDB.bankSnapshotVersion or 0) + 1,
            (GetServerTime() * 1000) + math.random(0, 999)
        )

    if RefreshBankPage then RefreshBankPage() end
    SendBankSnapshot()

    if GetTime() - lastBankScanNotice > 3 then
        lastBankScanNotice = GetTime()
        WNWPrint("|cff33ff99", "Guild bank stock counts updated!")
    end
end

local function ScheduleGuildBankScan(delay)
    if bankScanTimer then bankScanTimer:Cancel() end
    bankScanTimer = C_Timer.NewTimer(delay or 1, ScanGuildBank)
end

local function PrepareGuildBankScan()
    if not guildBankOpen then
        WNWPrint("|cffffaa00", "Open the guild bank first, then scan it.")
        return
    end

    local queryDelay = 0
    local numberOfTabs = GetNumGuildBankTabs() or 0

    for tab = 1, numberOfTabs do
        local _, _, isViewable = GetGuildBankTabInfo(tab)

        if isViewable then
            local tabToQuery = tab
            C_Timer.After(queryDelay, function()
                if guildBankOpen then QueryGuildBankTab(tabToQuery) end
            end)
            queryDelay = queryDelay + 0.35
        end
    end

    ScheduleGuildBankScan(queryDelay + 1.2)
end

-- Load and migrate the guild-bank data.
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")

loader:SetScript("OnEvent", function(self, event, loadedAddonName)
    if loadedAddonName ~= addonName then return end

    WhoNeedsWhatDB = WhoNeedsWhatDB or {}
    WhoNeedsWhatDB.bankGoals = WhoNeedsWhatDB.bankGoals or {}
    WhoNeedsWhatDB.bankGoalsVersion = WhoNeedsWhatDB.bankGoalsVersion or 0
    WhoNeedsWhatDB.bankCountsByID = WhoNeedsWhatDB.bankCountsByID or {}
    WhoNeedsWhatDB.bankCountsByName = WhoNeedsWhatDB.bankCountsByName or {}

    if GuildRoster then GuildRoster() end
    if RefreshBankPage then RefreshBankPage() end

    self:UnregisterEvent("ADDON_LOADED")
end)

-- Receive guild-bank goals and stock snapshots.
local syncFrame = CreateFrame("Frame")
syncFrame:RegisterEvent("CHAT_MSG_ADDON")
syncFrame:RegisterEvent("GUILD_ROSTER_UPDATE")

syncFrame:SetScript("OnEvent", function(
    self,
    event,
    prefix,
    message,
    channel,
    sender
)
    if event == "GUILD_ROSTER_UPDATE" then
        if RefreshBankPage then RefreshBankPage() end
        return
    end

    if prefix ~= addonPrefix then return end

    if message == "SYNC_REQUEST" then
        if not IsSamePlayer(sender, GetPlayerFullName()) then
            C_Timer.After(math.random(8, 16) / 10, SendAllBankGoals)
            C_Timer.After(math.random(17, 24) / 10, SendBankSnapshot)
        end
        return
    end

    local action, field2, field3, field4, field5, field6, field7 =
        strsplit(separator, message, 7)

    if action == "GOALS_CLEAR" then
        if not IsOfficerSender(sender) then return end

        local version = tonumber(field2) or 0
        if version < (WhoNeedsWhatDB.bankGoalsVersion or 0) then return end

        WhoNeedsWhatDB.bankGoals = {}
        WhoNeedsWhatDB.bankGoalsVersion = version
        selectedGoalID = nil
        if RefreshBankPage then RefreshBankPage() end
        return
    end

    if action == "GOAL_SET" then
        if not IsOfficerSender(sender) then return end

        local version = tonumber(field2) or 0
        if version < (WhoNeedsWhatDB.bankGoalsVersion or 0) then return end
        if not field3 or not field7 or field7 == "" then return end

        local goal = FindGoalByID(field3)
        if not goal then
            goal = { id = field3 }
            table.insert(WhoNeedsWhatDB.bankGoals, goal)
        end

        local itemID = tonumber(field4)
        goal.itemID = itemID and itemID > 0 and itemID or nil
        goal.quality = tonumber(field5) or 1
        goal.target = math.max(1, tonumber(field6) or 1)
        goal.item = field7
        goal.addedBy = sender

        WhoNeedsWhatDB.bankGoalsVersion = version
        if RefreshBankPage then RefreshBankPage() end
        return
    end

    if action == "GOAL_DELETE" then
        if not IsOfficerSender(sender) then return end

        local version = tonumber(field2) or 0
        if version < (WhoNeedsWhatDB.bankGoalsVersion or 0) then return end

        for index = #WhoNeedsWhatDB.bankGoals, 1, -1 do
            if WhoNeedsWhatDB.bankGoals[index].id == field3 then
                table.remove(WhoNeedsWhatDB.bankGoals, index)
                break
            end
        end

        WhoNeedsWhatDB.bankGoalsVersion = version
        if selectedGoalID == field3 then selectedGoalID = nil end
        if RefreshBankPage then RefreshBankPage() end
        return
    end

    if action == "BANK_CLEAR" then
        local snapshotVersion = tonumber(field2) or 0
        if snapshotVersion < (WhoNeedsWhatDB.bankSnapshotVersion or 0) then
            return
        end

        WhoNeedsWhatDB.bankSnapshotVersion = snapshotVersion
        WhoNeedsWhatDB.bankScanTime = tonumber(field3)
        WhoNeedsWhatDB.bankScannedBy = field4 or sender
        WhoNeedsWhatDB.bankSnapshotSender = sender
        WhoNeedsWhatDB.bankCountsByID = {}
        WhoNeedsWhatDB.bankCountsByName = {}
        if RefreshBankPage then RefreshBankPage() end
        return
    end

    if action == "BANK_COUNT" then
        local snapshotVersion = tonumber(field2) or 0
        if snapshotVersion ~= WhoNeedsWhatDB.bankSnapshotVersion then return end
        if not IsSamePlayer(sender, WhoNeedsWhatDB.bankSnapshotSender) then return end

        local itemID = tonumber(field3)
        local count = math.max(0, tonumber(field4) or 0)
        local itemName = field5

        if itemID and itemID > 0 then
            WhoNeedsWhatDB.bankCountsByID[tostring(itemID)] = count
        end

        if itemName and itemName ~= "" then
            WhoNeedsWhatDB.bankCountsByName[itemName:lower()] = count
        end

        if RefreshBankPage then RefreshBankPage() end
    end
end)

-- Detect and scan the live guild bank.
local bankEvents = CreateFrame("Frame")
bankEvents:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_SHOW")
bankEvents:RegisterEvent("PLAYER_INTERACTION_MANAGER_FRAME_HIDE")
bankEvents:RegisterEvent("GUILDBANKBAGSLOTS_CHANGED")

local guildBankInteractionType =
    Enum
    and Enum.PlayerInteractionType
    and Enum.PlayerInteractionType.GuildBanker
    or 10

bankEvents:SetScript("OnEvent", function(self, event, interactionType)
    if event == "PLAYER_INTERACTION_MANAGER_FRAME_SHOW"
        and interactionType == guildBankInteractionType then

        guildBankOpen = true
        C_Timer.After(0.5, PrepareGuildBankScan)
        return
    end

    if event == "PLAYER_INTERACTION_MANAGER_FRAME_HIDE"
        and interactionType == guildBankInteractionType then

        guildBankOpen = false
        return
    end

    if event == "GUILDBANKBAGSLOTS_CHANGED" and guildBankOpen then
        ScheduleGuildBankScan(1)
    end
end)

-- Enlarge the original window and add the two page tabs.
window:SetSize(640, 520)

local bankPage = CreateFrame("Frame", nil, window, "BackdropTemplate")
bankPage:SetPoint("TOPLEFT", 10, -28)
bankPage:SetPoint("BOTTOMRIGHT", -10, 10)
bankPage:SetFrameLevel(window:GetFrameLevel() + 50)
bankPage:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
bankPage:SetBackdropColor(0.035, 0.035, 0.035, 1)
bankPage:SetBackdropBorderColor(0.35, 0.35, 0.35, 1)
bankPage:Hide()

local requestsTab = CreateFrame(
    "Button",
    nil,
    window,
    "UIPanelButtonTemplate"
)
requestsTab:SetSize(110, 26)
requestsTab:SetPoint("TOPRIGHT", -178, -31)
requestsTab:SetText("Requests")
requestsTab:SetFrameLevel(window:GetFrameLevel() + 80)

local bankTab = CreateFrame(
    "Button",
    nil,
    window,
    "UIPanelButtonTemplate"
)
bankTab:SetSize(155, 26)
bankTab:SetPoint("LEFT", requestsTab, "RIGHT", 5, 0)
bankTab:SetText("Guild Bank Needs")
bankTab:SetFrameLevel(window:GetFrameLevel() + 80)

local function ShowRequestsPage()
    bankPage:Hide()
    requestsTab:Disable()
    bankTab:Enable()
end

local function ShowBankPage()
    bankPage:Show()
    requestsTab:Enable()
    bankTab:Disable()
    if RefreshBankPage then RefreshBankPage() end

    if CanGuildSync() then
        C_ChatInfo.SendAddonMessage(addonPrefix, "SYNC_REQUEST", "GUILD")
    end
end

requestsTab:SetScript("OnClick", ShowRequestsPage)
bankTab:SetScript("OnClick", ShowBankPage)
ShowRequestsPage()

local pageTitle = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontNormalLarge"
)
pageTitle:SetPoint("TOPLEFT", 18, -42)
pageTitle:SetText("Guild Bank Stock Targets")

local pageHelp = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontHighlightSmall"
)
pageHelp:SetPoint("TOPLEFT", pageTitle, "BOTTOMLEFT", 0, -7)
pageHelp:SetText(
    "Officers set targets. Open the guild bank to refresh its stock counts."
)

local bankItemLabel = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontNormal"
)
bankItemLabel:SetPoint("TOPLEFT", 18, -98)
bankItemLabel:SetText("Item  |cffaaaaaaType 3+ letters|r")

local bankItemBox = CreateFrame(
    "EditBox",
    nil,
    bankPage,
    "InputBoxTemplate"
)
bankItemBox:SetSize(330, 30)
bankItemBox:SetPoint("TOPLEFT", bankItemLabel, "BOTTOMLEFT", 0, -4)
bankItemBox:SetAutoFocus(false)
bankItemBox:SetMaxLetters(100)

local targetLabel = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontNormal"
)
targetLabel:SetPoint("TOPLEFT", 378, -98)
targetLabel:SetText("Target amount")

local targetBox = CreateFrame(
    "EditBox",
    nil,
    bankPage,
    "InputBoxTemplate"
)
targetBox:SetSize(90, 30)
targetBox:SetPoint("TOPLEFT", targetLabel, "BOTTOMLEFT", 0, -4)
targetBox:SetAutoFocus(false)
targetBox:SetNumeric(true)
targetBox:SetMaxLetters(6)
targetBox:SetText("100")

-- Autocomplete for the bank-goal item box.
local suggestionButtons = {}
local selectedSuggestion = 0
local maximumSuggestions = 7

local suggestionFrame = CreateFrame(
    "Frame",
    nil,
    bankPage,
    "BackdropTemplate"
)
suggestionFrame:SetSize(360, maximumSuggestions * 24 + 12)
suggestionFrame:SetPoint("TOPLEFT", bankItemBox, "BOTTOMLEFT", -5, 4)
suggestionFrame:SetFrameLevel(bankPage:GetFrameLevel() + 20)
suggestionFrame:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
suggestionFrame:SetBackdropColor(0.03, 0.03, 0.03, 0.99)
suggestionFrame:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
suggestionFrame:Hide()

local function RefreshSuggestionHighlight()
    for index, button in ipairs(suggestionButtons) do
        button.selectedTexture:SetShown(
            index == selectedSuggestion and button:IsShown()
        )
    end
end

local function ChooseSuggestion(result)
    if not result then return end

    bankItemBox:SetText(result.name)
    bankItemBox:SetCursorPosition(#result.name)
    bankItemBox.wnwItemID = result.id
    bankItemBox.wnwQuality = result.quality
    suggestionFrame:Hide()
    selectedSuggestion = 0
end

for index = 1, maximumSuggestions do
    local button = CreateFrame("Button", nil, suggestionFrame)
    button:SetHeight(24)
    button:SetPoint("TOPLEFT", 6, -6 - ((index - 1) * 24))
    button:SetPoint("TOPRIGHT", -6, -6 - ((index - 1) * 24))

    button.selectedTexture = button:CreateTexture(nil, "BACKGROUND")
    button.selectedTexture:SetAllPoints()
    button.selectedTexture:SetColorTexture(0.2, 0.45, 0.8, 0.45)
    button.selectedTexture:Hide()

    button.text = button:CreateFontString(
        nil,
        "OVERLAY",
        "GameFontHighlight"
    )
    button.text:SetPoint("LEFT", 6, 0)
    button.text:SetPoint("RIGHT", -6, 0)
    button.text:SetJustifyH("LEFT")

    button:SetScript("OnEnter", function()
        selectedSuggestion = index
        RefreshSuggestionHighlight()
    end)

    button:SetScript("OnClick", function(self)
        ChooseSuggestion(self.result)
    end)

    suggestionButtons[index] = button
end

local function ShowItemSuggestions()
    if not WhoNeedsWhatItemSearch or not WhoNeedsWhatItemSearch.Find then
        suggestionFrame:Hide()
        return
    end

    local results = WhoNeedsWhatItemSearch.Find(
        bankItemBox:GetText(),
        maximumSuggestions
    )

    if #results == 0 then
        suggestionFrame:Hide()
        selectedSuggestion = 0
        return
    end

    selectedSuggestion = 0

    for index, button in ipairs(suggestionButtons) do
        local result = results[index]
        button.result = result

        if result then
            button.text:SetText(result.name)

            local color = ITEM_QUALITY_COLORS[result.quality or 1]
            if color then
                button.text:SetTextColor(color.r, color.g, color.b)
            else
                button.text:SetTextColor(1, 1, 1)
            end

            button:Show()
        else
            button:Hide()
        end
    end

    RefreshSuggestionHighlight()
    suggestionFrame:Show()
end

bankItemBox:SetScript("OnTextChanged", function(self, userInput)
    if userInput then
        self.wnwItemID = nil
        self.wnwQuality = nil
        ShowItemSuggestions()
    end
end)

bankItemBox:SetScript("OnArrowPressed", function(self, key)
    if not suggestionFrame:IsShown() then return end

    local shownButtons = 0
    for _, button in ipairs(suggestionButtons) do
        if button:IsShown() then shownButtons = shownButtons + 1 end
    end

    if shownButtons == 0 then return end

    if key == "DOWN" then
        selectedSuggestion = selectedSuggestion + 1
        if selectedSuggestion > shownButtons then selectedSuggestion = 1 end
    elseif key == "UP" then
        selectedSuggestion = selectedSuggestion - 1
        if selectedSuggestion < 1 then selectedSuggestion = shownButtons end
    end

    RefreshSuggestionHighlight()
end)

bankItemBox:SetScript("OnEnterPressed", function()
    if suggestionFrame:IsShown() then
        local index = selectedSuggestion > 0 and selectedSuggestion or 1
        ChooseSuggestion(suggestionButtons[index].result)
    else
        bankItemBox:ClearFocus()
    end
end)

bankItemBox:SetScript("OnTabPressed", function()
    if suggestionFrame:IsShown() then
        local index = selectedSuggestion > 0 and selectedSuggestion or 1
        ChooseSuggestion(suggestionButtons[index].result)
    end
end)

bankItemBox:SetScript("OnEscapePressed", function()
    suggestionFrame:Hide()
    selectedSuggestion = 0
    bankItemBox:ClearFocus()
end)

local addGoalButton = CreateFrame(
    "Button",
    nil,
    bankPage,
    "UIPanelButtonTemplate"
)
addGoalButton:SetSize(145, 30)
addGoalButton:SetPoint("TOPLEFT", 18, -159)
addGoalButton:SetText("Add / Update Goal")

local removeGoalButton = CreateFrame(
    "Button",
    nil,
    bankPage,
    "UIPanelButtonTemplate"
)
removeGoalButton:SetSize(145, 30)
removeGoalButton:SetPoint("LEFT", addGoalButton, "RIGHT", 10, 0)
removeGoalButton:SetText("Remove Selected")

local scanButton = CreateFrame(
    "Button",
    nil,
    bankPage,
    "UIPanelButtonTemplate"
)
scanButton:SetSize(145, 30)
scanButton:SetPoint("LEFT", removeGoalButton, "RIGHT", 10, 0)
scanButton:SetText("Scan Bank Now")
scanButton:SetScript("OnClick", PrepareGuildBankScan)

local permissionText = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontHighlightSmall"
)
permissionText:SetPoint("TOPLEFT", 18, -199)

local itemHeader = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontNormal"
)
itemHeader:SetPoint("TOPLEFT", 24, -226)
itemHeader:SetText("Item")

local haveHeader = bankPage:CreateFontString(nil, "OVERLAY", "GameFontNormal")
haveHeader:SetPoint("TOPLEFT", 358, -226)
haveHeader:SetText("Bank")

local targetHeader = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontNormal"
)
targetHeader:SetPoint("TOPLEFT", 438, -226)
targetHeader:SetText("Target")

local needHeader = bankPage:CreateFontString(nil, "OVERLAY", "GameFontNormal")
needHeader:SetPoint("TOPLEFT", 515, -226)
needHeader:SetText("Status")

local listFrame = CreateFrame("Frame", nil, bankPage)
listFrame:SetSize(580, visibleGoalRows * 25)
listFrame:SetPoint("TOPLEFT", 18, -246)
listFrame:EnableMouseWheel(true)

local goalRows = {}

for index = 1, visibleGoalRows do
    local row = CreateFrame("Button", nil, listFrame)
    row:SetSize(580, 24)
    row:SetPoint("TOPLEFT", 0, -((index - 1) * 25))

    row.selectedTexture = row:CreateTexture(nil, "BACKGROUND")
    row.selectedTexture:SetAllPoints()
    row.selectedTexture:SetColorTexture(0.2, 0.45, 0.8, 0.35)
    row.selectedTexture:Hide()

    row.itemText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.itemText:SetPoint("LEFT", 6, 0)
    row.itemText:SetWidth(320)
    row.itemText:SetJustifyH("LEFT")

    row.haveText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.haveText:SetPoint("LEFT", 340, 0)
    row.haveText:SetWidth(65)
    row.haveText:SetJustifyH("CENTER")

    row.targetText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.targetText:SetPoint("LEFT", 420, 0)
    row.targetText:SetWidth(65)
    row.targetText:SetJustifyH("CENTER")

    row.needText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.needText:SetPoint("LEFT", 490, 0)
    row.needText:SetWidth(90)
    row.needText:SetJustifyH("CENTER")

    row:SetScript("OnClick", function(self)
        if not self.goal then return end

        selectedGoalID = self.goal.id
        bankItemBox:SetText(self.goal.item or "")
        bankItemBox.wnwItemID = self.goal.itemID
        bankItemBox.wnwQuality = self.goal.quality
        targetBox:SetText(tostring(self.goal.target or 1))
        if RefreshBankPage then RefreshBankPage() end
    end)

    goalRows[index] = row
end

listFrame:SetScript("OnMouseWheel", function(self, delta)
    local numberOfGoals = WhoNeedsWhatDB
        and WhoNeedsWhatDB.bankGoals
        and #WhoNeedsWhatDB.bankGoals
        or 0

    local maximumOffset = math.max(1, numberOfGoals - visibleGoalRows + 1)
    bankListOffset = math.max(
        1,
        math.min(maximumOffset, bankListOffset - delta)
    )

    if RefreshBankPage then RefreshBankPage() end
end)

local scanStatus = bankPage:CreateFontString(
    nil,
    "OVERLAY",
    "GameFontHighlightSmall"
)
scanStatus:SetPoint("BOTTOMLEFT", 18, 18)
scanStatus:SetWidth(580)
scanStatus:SetJustifyH("LEFT")

RefreshBankPage = function()
    if not WhoNeedsWhatDB or not WhoNeedsWhatDB.bankGoals then
        permissionText:SetText("Loading guild-bank targets...")
        return
    end

    local canManage = CanManageBankGoals()
    if canManage then
        addGoalButton:Enable()
        removeGoalButton:Enable()
        permissionText:SetText(
            "|cff33ff99Officer controls enabled.|r Click a row to edit or remove it."
        )
    else
        addGoalButton:Disable()
        removeGoalButton:Disable()
        permissionText:SetText(
            "|cffffaa00View only.|r A guild officer manages these targets."
        )
    end

    local goals = {}
    for _, goal in ipairs(WhoNeedsWhatDB.bankGoals) do
        table.insert(goals, goal)
    end

    table.sort(goals, function(first, second)
        return (first.item or ""):lower() < (second.item or ""):lower()
    end)

    local maximumOffset = math.max(1, #goals - visibleGoalRows + 1)
    bankListOffset = math.min(bankListOffset, maximumOffset)

    for rowIndex, row in ipairs(goalRows) do
        local goal = goals[bankListOffset + rowIndex - 1]
        row.goal = goal

        if goal then
            row.itemText:SetText(goal.item or "Unknown item")

            local qualityColor = ITEM_QUALITY_COLORS[goal.quality or 1]
            if qualityColor then
                row.itemText:SetTextColor(
                    qualityColor.r,
                    qualityColor.g,
                    qualityColor.b
                )
            else
                row.itemText:SetTextColor(1, 1, 1)
            end

            local count = GetGoalBankCount(goal)
            row.haveText:SetText(count == nil and "?" or tostring(count))
            row.targetText:SetText(tostring(goal.target or 1))

            if count == nil then
                row.needText:SetText("Not scanned")
                row.needText:SetTextColor(1, 0.82, 0)
            else
                local needed = math.max((goal.target or 1) - count, 0)

                if needed > 0 then
                    row.needText:SetText("Need " .. needed)
                    row.needText:SetTextColor(1, 0.25, 0.25)
                else
                    row.needText:SetText("Stocked")
                    row.needText:SetTextColor(0.2, 1, 0.4)
                end
            end

            row.selectedTexture:SetShown(goal.id == selectedGoalID)
            row:Show()
        else
            row:Hide()
        end
    end

    if #goals == 0 then
        scanStatus:SetText(
            "No stock targets yet. An officer can add the first item above."
        )
    elseif WhoNeedsWhatDB.bankScanTime then
        scanStatus:SetText(
            "Last bank scan: "
            .. date("%d %b %Y at %H:%M", WhoNeedsWhatDB.bankScanTime)
            .. " by "
            .. (WhoNeedsWhatDB.bankScannedBy or "Unknown")
            .. (#goals > visibleGoalRows and "  |  Mouse-wheel to scroll." or "")
        )
    else
        scanStatus:SetText(
            "Bank not scanned yet. Open the guild bank to update the counts."
        )
    end
end

addGoalButton:SetScript("OnClick", function()
    if not CanManageBankGoals() then
        WNWPrint("|cffff4444", "Only guild officers can change bank targets.")
        return
    end

    local itemName = Trim(bankItemBox:GetText())
    local target = math.max(1, tonumber(targetBox:GetText()) or 1)

    if itemName == "" then
        WNWPrint("|cffff4444", "Please choose an item first.")
        return
    end

    local itemID = bankItemBox.wnwItemID
    local goal = FindMatchingGoal(itemID, itemName)

    if not goal and #WhoNeedsWhatDB.bankGoals >= maximumGoals then
        WNWPrint("|cffff4444", "The bank list can hold up to 30 targets.")
        return
    end

    if not goal then
        goal = {
            id = table.concat({
                GetPlayerFullName(),
                GetServerTime(),
                math.random(1000, 9999)
            }, ":")
        }
        table.insert(WhoNeedsWhatDB.bankGoals, goal)
    end

    goal.item = itemName
    goal.itemID = itemID
    goal.quality = bankItemBox.wnwQuality or goal.quality or 1
    goal.target = target
    goal.addedBy = GetPlayerFullName()
    selectedGoalID = goal.id

    local version = NewDataVersion()
    WhoNeedsWhatDB.bankGoalsVersion = version
    SendGoal(goal, version, 0)
    RefreshBankPage()

    WNWPrint(
        "|cff33ff99",
        "Bank target set: " .. target .. "x " .. itemName
    )

    bankItemBox:SetText("")
    bankItemBox.wnwItemID = nil
    bankItemBox.wnwQuality = nil
    targetBox:SetText("100")
    suggestionFrame:Hide()
end)

removeGoalButton:SetScript("OnClick", function()
    if not CanManageBankGoals() then
        WNWPrint("|cffff4444", "Only guild officers can change bank targets.")
        return
    end

    if not selectedGoalID then
        WNWPrint("|cffff4444", "Click a bank target first.")
        return
    end

    local removedName
    for index = #WhoNeedsWhatDB.bankGoals, 1, -1 do
        if WhoNeedsWhatDB.bankGoals[index].id == selectedGoalID then
            removedName = WhoNeedsWhatDB.bankGoals[index].item
            table.remove(WhoNeedsWhatDB.bankGoals, index)
            break
        end
    end

    if not removedName then return end

    local removedID = selectedGoalID
    selectedGoalID = nil
    local version = NewDataVersion()
    WhoNeedsWhatDB.bankGoalsVersion = version
    SendGoalDelete(removedID, version)
    RefreshBankPage()

    WNWPrint("|cff33ff99", "Removed bank target: " .. removedName)
end)

RefreshBankPage()
