-- AutoQuestLearner Database Module
-- Integrates quest data from pfQuest and pfQuest-ascension

AutoQuestLearner = AutoQuestLearner or CreateFrame("Frame")

-- AutoQuestLearnerData holds the copied pfQuest/pfQuest-ascension database
-- (quests, units, objects, items, zones). This is intentionally a PLAIN
-- Lua global, NOT listed in the .toc as a SavedVariable. It is rebuilt
-- from pfDB every session. Previously this data was stored inside
-- AutoQuestLearnerDB (which IS a SavedVariable), which meant the entire
-- pfQuest world database was being written to and re-parsed from your
-- WTF SavedVariables file on every login/logout. That file could grow to
-- many megabytes as a single Lua table literal, which exceeds the WoW
-- client's Lua compiler limits and produces the
-- "memory allocation error: block too big" crash on login.
AutoQuestLearnerData = AutoQuestLearnerData or {}

------------------------------------------------------------
-- GetQuestLogTitle compat wrapper
--
-- CRITICAL: on vanilla (client build <= 11200), GetQuestLogTitle
-- returns 6 values: title, level, tag, header, collapsed, complete.
-- On TBC/WotLK-family clients (which this one is — Interface 30300),
-- it returns 9 values instead, with "suggestedGroup" inserted as the
-- 4th value: title, level, tag, suggestedGroup, header, collapsed,
-- complete, daily, questID.
--
-- Every call site in this addon previously called GetQuestLogTitle()
-- directly using the vanilla-style 6-value destructuring, which reads
-- "suggestedGroup" into what it thinks is "header". suggestedGroup is
-- 0 for ordinary solo quests — and 0 is TRUTHY in Lua — so
-- "if title and not header then" was silently false for literally
-- every normal quest, causing the entire quest tracker (and anything
-- else that scans the quest log) to see nothing at all. This mirrors
-- a fix pfQuest itself has to apply for the exact same reason (see
-- pfQuest's compat/client.lua, pfQuestCompat.GetQuestLogTitle).
------------------------------------------------------------
local _, _, _, aqlClientBuild = GetBuildInfo()
aqlClientBuild = aqlClientBuild or 11200

function AutoQuestLearner_GetQuestLogTitle(id)
  local title, level, tag, group, header, collapsed, complete, daily
  if aqlClientBuild <= 11200 then -- vanilla
    title, level, tag, header, collapsed, complete = GetQuestLogTitle(id)
  else -- tbc & wotlk
    title, level, tag, group, header, collapsed, complete, daily = GetQuestLogTitle(id)
  end
  return title, level, tag, header, collapsed, complete, daily
end

local function EnsureDatabaseTables()
  AutoQuestLearnerDB = AutoQuestLearnerDB or {}
  AutoQuestLearnerDB.config = AutoQuestLearnerDB.config or {}
  AutoQuestLearnerDB.history = AutoQuestLearnerDB.history or {}
  AutoQuestLearnerDB.repeatable = AutoQuestLearnerDB.repeatable or {}
  AutoQuestLearnerDB.colors = AutoQuestLearnerDB.colors or {}

  -- One-time cleanup: strip any previously-persisted database copy out
  -- of the SavedVariable so it stops bloating the file going forward.
  AutoQuestLearnerData.quests = nil
  AutoQuestLearnerData.units = nil
  AutoQuestLearnerData.objects = nil
  AutoQuestLearnerData.items = nil
  AutoQuestLearnerData.zones = nil
  AutoQuestLearnerData.refloot = nil
  AutoQuestLearnerData.areatrigger = nil

  AutoQuestLearnerData.quests = AutoQuestLearnerData.quests or {}
  AutoQuestLearnerData.units = AutoQuestLearnerData.units or {}
  AutoQuestLearnerData.objects = AutoQuestLearnerData.objects or {}
  AutoQuestLearnerData.items = AutoQuestLearnerData.items or {}
  AutoQuestLearnerData.zones = AutoQuestLearnerData.zones or {}
  AutoQuestLearnerData.refloot = AutoQuestLearnerData.refloot or {}
  AutoQuestLearnerData.areatrigger = AutoQuestLearnerData.areatrigger or {}

  -- Initialize database tables
  AutoQuestLearnerData.quests["data"] = AutoQuestLearnerData.quests["data"] or {}
  AutoQuestLearnerData.quests["loc"] = AutoQuestLearnerData.quests["loc"] or {}
  AutoQuestLearnerData.units["data"] = AutoQuestLearnerData.units["data"] or {}
  AutoQuestLearnerData.objects["data"] = AutoQuestLearnerData.objects["data"] or {}
  AutoQuestLearnerData.items["data"] = AutoQuestLearnerData.items["data"] or {}
  AutoQuestLearnerData.items["loc"] = AutoQuestLearnerData.items["loc"] or {}
  AutoQuestLearnerData.zones["loc"] = AutoQuestLearnerData.zones["loc"] or {}
  AutoQuestLearnerData.refloot["data"] = AutoQuestLearnerData.refloot["data"] or {}
  AutoQuestLearnerData.areatrigger["data"] = AutoQuestLearnerData.areatrigger["data"] or {}
end

EnsureDatabaseTables()

AutoQuestLearnerDatabase = {}

-- Merges a newly-loaded item-drop entry into the existing one. Item
-- entries map source-type keys ("U"=unit drop chances, "O"=object loot
-- chances, "R"=reference loot template pointers, "V"=vendor sale info)
-- to their own sub-tables -- union those instead of one replacing the
-- other, same reasoning as MergeQuestEntry/MergeSpawnEntry above.
-- Merges a newly-loaded unit/object entry into the existing one rather
-- than overwriting it outright. Different community databases (pfQuest,
-- Questie-X Ascension, a real pfQuest installation if present) each
-- independently recorded their own subset of real spawn sightings for
-- the same NPC/object -- a plain overwrite means whichever source loads
-- last wins completely, silently discarding every coordinate the other
-- source(s) had that it didn't. Unioning (deduped by near-identical
-- x/y/zone) gives the fullest possible coverage from everything bundled.
local function MergeSpawnEntry(existing, incoming)
  -- Some bundled sources (e.g. items-tbc.lua) use a bare string like
  -- "_" as a lightweight placeholder for "exists but nothing useful
  -- here" instead of an empty table -- treat that the same as absent.
  if type(existing) ~= "table" then existing = nil end
  if type(incoming) ~= "table" then incoming = nil end
  if not existing then return incoming end
  if not incoming then return existing end

  local merged = {}
  for k, v in pairs(existing) do merged[k] = v end
  -- Fill in any fields the existing entry is missing.
  for k, v in pairs(incoming) do
    if merged[k] == nil then merged[k] = v end
  end

  merged["coords"] = {}
  for _, c in pairs(existing["coords"] or {}) do
    table.insert(merged["coords"], c)
  end
  for _, c in pairs(incoming["coords"] or {}) do
    local isDupe = false
    for _, existingCoord in pairs(merged["coords"]) do
      if existingCoord[3] == c[3]
          and math.abs(existingCoord[1] - c[1]) < 0.1
          and math.abs(existingCoord[2] - c[2]) < 0.1 then
        isDupe = true
        break
      end
    end
    if not isDupe then
      table.insert(merged["coords"], c)
    end
  end

  return merged
end

-- Merges a newly-loaded quest entry into the existing one, field by field,
-- instead of overwriting the whole record. Quest sources vary in
-- completeness, so a plain overwrite could mean a quest ID present in
-- both sources loses its objective data whenever the sparser source is
-- merged last. For "start"/"end"/"obj", each sub-table maps a type key
-- ("U"=unit, "O"=object, "I"=item, "A"=areatrigger) to a list of IDs --
-- those lists are unioned rather than one replacing the other, so a
-- quest that's genuinely differently sourced in two databases (e.g.
-- different NPC IDs recorded for the same objective) keeps both.
local function MergeQuestEntry(existing, incoming)
  if type(existing) ~= "table" then existing = nil end
  if type(incoming) ~= "table" then incoming = nil end
  if not existing then return incoming end
  if not incoming then return existing end

  local merged = {}
  for k, v in pairs(existing) do merged[k] = v end
  for k, v in pairs(incoming) do
    if merged[k] == nil then merged[k] = v end
  end

  for _, field in pairs({ "start", "end", "obj" }) do
    local a = existing[field]
    local b = incoming[field]
    if a or b then
      local mergedField = {}
      for _, sub in pairs({ "U", "O", "I", "A" }) do
        local listA = a and a[sub]
        local listB = b and b[sub]
        if listA or listB then
          local seen = {}
          local combined = {}
          for _, id in pairs(listA or {}) do
            if not seen[id] then
              seen[id] = true
              table.insert(combined, id)
            end
          end
          for _, id in pairs(listB or {}) do
            if not seen[id] then
              seen[id] = true
              table.insert(combined, id)
            end
          end
          mergedField[sub] = combined
        end
      end
      merged[field] = mergedField
    end
  end

  return merged
end

local function MergeItemEntry(existing, incoming)
  if type(existing) ~= "table" then existing = nil end
  if type(incoming) ~= "table" then incoming = nil end
  if not existing then return incoming end
  if not incoming then return existing end

  local merged = {}
  for k, v in pairs(existing) do merged[k] = v end

  for _, sub in pairs({ "U", "O", "R", "V" }) do
    local a = existing[sub]
    local b = incoming[sub]
    if a or b then
      local combined = {}
      if a then for k, v in pairs(a) do combined[k] = v end end
      if b then for k, v in pairs(b) do if combined[k] == nil then combined[k] = v end end end
      merged[sub] = combined
    end
  end

  return merged
end

-- Load pfQuest data if available. Checks BOTH this addon's own bundled
-- snapshot (kept under distinct "-aqlbundled" keys, see bundled-db/*.lua
-- headers) AND the standard pfQuest keys a real pfQuest installation
-- would use, merging whichever are present via the same field-level
-- union logic used everywhere else in this file -- rather than one
-- silently overwriting the other depending on load order. Previously
-- this addon's own bundled quests.lua/units.lua/etc used the exact same
-- pfDB keys real pfQuest uses, and being a direct table-literal
-- assignment (not a merge), whichever loaded last completely replaced
-- whatever the other had already written -- meaning a real pfQuest
-- installation's own (potentially newer/more complete) data could get
-- silently discarded the moment this addon's own bundled snapshot
-- loaded afterward, with no combination of the two at all.
local function LoadPfQuestData()
  if not pfDB then return false end

  -- Quests: vanilla + TBC, from both a real pfQuest install (if
  -- present) and this addon's own bundled snapshot.
  if pfDB["quests"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["quests"][key] then
        for k, v in pairs(pfDB["quests"][key]) do
          AutoQuestLearnerData.quests["data"][k] = MergeQuestEntry(AutoQuestLearnerData.quests["data"][k], v)
        end
      end
    end
    for _, key in pairs({ "enUS", "enUS-tbc", "enUS-aqlbundled", "enUS-tbc-aqlbundled" }) do
      if pfDB["quests"][key] then
        for k, v in pairs(pfDB["quests"][key]) do
          -- enUS name tables are flat [id]="Title" (unlike the
          -- Ascension/Questie-derived "loc-questiex" tables, which are
          -- [id]={["T"]="Title"}) -- normalize to the {["T"]=...} shape
          -- this addon expects everywhere else.
          AutoQuestLearnerData.quests["loc"][k] = type(v) == "table" and v or { ["T"] = v }
        end
      end
    end
  end

  -- Units: coords (vanilla + TBC) and names.
  if pfDB["units"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["units"][key] then
        for k, v in pairs(pfDB["units"][key]) do
          AutoQuestLearnerData.units["data"][k] = MergeSpawnEntry(AutoQuestLearnerData.units["data"][k], v)
        end
      end
    end
    AutoQuestLearnerData.units["loc"] = AutoQuestLearnerData.units["loc"] or {}
    for _, key in pairs({ "enUS", "enUS-tbc", "enUS-aqlbundled", "enUS-tbc-aqlbundled" }) do
      if pfDB["units"][key] then
        for k, v in pairs(pfDB["units"][key]) do
          AutoQuestLearnerData.units["loc"][k] = v
        end
      end
    end
  end

  -- Objects: coords (vanilla + TBC) and names.
  if pfDB["objects"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["objects"][key] then
        for k, v in pairs(pfDB["objects"][key]) do
          AutoQuestLearnerData.objects["data"][k] = MergeSpawnEntry(AutoQuestLearnerData.objects["data"][k], v)
        end
      end
    end
    AutoQuestLearnerData.objects["loc"] = AutoQuestLearnerData.objects["loc"] or {}
    for _, key in pairs({ "enUS", "enUS-tbc", "enUS-aqlbundled", "enUS-tbc-aqlbundled" }) do
      if pfDB["objects"][key] then
        for k, v in pairs(pfDB["objects"][key]) do
          AutoQuestLearnerData.objects["loc"][k] = v
        end
      end
    end
  end

  -- Items: drop-source data (vanilla + TBC), needed to resolve
  -- item-collection quest objectives ("obj"["I"]) to real kill/interact
  -- locations via ResolveItemSources() below.
  if pfDB["items"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["items"][key] then
        for k, v in pairs(pfDB["items"][key]) do
          AutoQuestLearnerData.items["data"][k] = MergeItemEntry(AutoQuestLearnerData.items["data"][k], v)
        end
      end
    end
    for _, key in pairs({ "enUS", "enUS-tbc", "enUS-aqlbundled", "enUS-tbc-aqlbundled" }) do
      if pfDB["items"][key] then
        for k, v in pairs(pfDB["items"][key]) do
          AutoQuestLearnerData.items["loc"][k] = v
        end
      end
    end
  end

  -- Reference loot data (item ID -> shared loot-template ID -> actual
  -- NPC/object sources) -- some items don't list their sources
  -- directly and only point at a reference template via
  -- items["data"][id]["R"], which this table resolves.
  if pfDB["refloot"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["refloot"][key] then
        for k, v in pairs(pfDB["refloot"][key]) do
          AutoQuestLearnerData.refloot["data"][k] = MergeItemEntry(AutoQuestLearnerData.refloot["data"][k], v)
        end
      end
    end
  end

  -- Areatrigger coordinates (vanilla + TBC) -- needed to resolve
  -- "Find X nearby" style quest objectives ("obj"["A"]), which point at
  -- a trigger zone/radius rather than a specific NPC or object.
  if pfDB["areatrigger"] then
    for _, key in pairs({ "data", "data-tbc", "data-aqlbundled", "data-tbc-aqlbundled" }) do
      if pfDB["areatrigger"][key] then
        for k, v in pairs(pfDB["areatrigger"][key]) do
          AutoQuestLearnerData.areatrigger["data"][k] = MergeSpawnEntry(AutoQuestLearnerData.areatrigger["data"][k], v)
        end
      end
    end
  end

  -- Zone names (vanilla + TBC).
  if pfDB["zones"] then
    for _, key in pairs({ "enUS", "enUS-tbc", "enUS-aqlbundled", "enUS-tbc-aqlbundled" }) do
      if pfDB["zones"][key] then
        for k, v in pairs(pfDB["zones"][key]) do
          AutoQuestLearnerData.zones["loc"][k] = v
        end
      end
    end
  end

  -- Minimap zone dimensions (vanilla + TBC) -- AutoQuestLearnerMap's
  -- minimap pin placement (and the minimap route line) needs a zone's
  -- physical width/height in yards to convert a world coordinate into
  -- a position on the round minimap texture. Without this table
  -- populated, UpdateMinimap() bails out immediately for every zone
  -- (mapWidth/mapHeight read as 0), meaning nothing has ever rendered
  -- on the minimap at all -- not a clusters-specific issue, a total
  -- minimap outage that just happened to be first noticed via missing
  -- clusters. AutoQuestLearnerDB.minimap (not AutoQuestLearnerData) is
  -- intentional here -- AutoQuestLearner_Map.lua reads it directly from
  -- the SavedVariable table, not from AutoQuestLearnerData.
  AutoQuestLearnerDB.minimap = AutoQuestLearnerDB.minimap or {}
  if pfDB["minimap"] then
    for k, v in pairs(pfDB["minimap"]) do
      AutoQuestLearnerDB.minimap[k] = v
    end
  end
  if pfDB["minimap-tbc"] then
    for k, v in pairs(pfDB["minimap-tbc"]) do
      AutoQuestLearnerDB.minimap[k] = v
    end
  end

  return true
end

local function LoadAscensionData()
  -- WotLK content deliberately not bundled/merged -- vanilla + TBC only.
  local unitSources = { "data-ascension", "data-questiex", "data-coa", "data-addressbook" }
  local unitLocSources = { "loc-questiex" }
  local objectSources = { "data-questiex" }
  local objectLocSources = { "loc-questiex" }
  local questSources = { "data-ascension", "data-questiex", "data-questiex-obj", "data-worlddb", "data-wcq", "data-questie2" }
  local questLocSources = { "loc-questiex" }

  if pfDB and pfDB["units"] then
    for _, key in pairs(unitSources) do
      if pfDB["units"][key] then
        for k, v in pairs(pfDB["units"][key]) do
          AutoQuestLearnerData.units["data"][k] = MergeSpawnEntry(AutoQuestLearnerData.units["data"][k], v)
        end
      end
    end
    for _, key in pairs(unitLocSources) do
      if pfDB["units"][key] then
        for k, v in pairs(pfDB["units"][key]) do
          AutoQuestLearnerData.units["loc"] = AutoQuestLearnerData.units["loc"] or {}
          AutoQuestLearnerData.units["loc"][k] = v
        end
      end
    end
  end

  if pfDB and pfDB["objects"] then
    for _, key in pairs(objectSources) do
      if pfDB["objects"][key] then
        for k, v in pairs(pfDB["objects"][key]) do
          AutoQuestLearnerData.objects["data"][k] = MergeSpawnEntry(AutoQuestLearnerData.objects["data"][k], v)
        end
      end
    end
    for _, key in pairs(objectLocSources) do
      if pfDB["objects"][key] then
        for k, v in pairs(pfDB["objects"][key]) do
          AutoQuestLearnerData.objects["loc"] = AutoQuestLearnerData.objects["loc"] or {}
          AutoQuestLearnerData.objects["loc"][k] = v
        end
      end
    end
  end

  if pfDB and pfDB["quests"] then
    for _, key in pairs(questSources) do
      if pfDB["quests"][key] then
        for k, v in pairs(pfDB["quests"][key]) do
          AutoQuestLearnerData.quests["data"][k] = MergeQuestEntry(AutoQuestLearnerData.quests["data"][k], v)
        end
      end
    end
    for _, key in pairs(questLocSources) do
      if pfDB["quests"][key] then
        for k, v in pairs(pfDB["quests"][key]) do
          AutoQuestLearnerData.quests["loc"][k] = v
        end
      end
    end
  end

  return true
end

-- Format quest text (handles special formatting codes)
function AutoQuestLearnerDatabase:FormatQuestText(text)
  if not text then return "" end
  
  -- Remove color codes and formatting
  text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
  text = string.gsub(text, "|r", "")
  text = string.gsub(text, "|n", "\n")
  text = string.gsub(text, "\239\188\154", ":")
  
  return text
end

-- Get quest IDs by quest log index
function AutoQuestLearnerDatabase:GetQuestIDs(qlogid)
  if not qlogid then return nil end

  -- Most reliable: pull the quest ID directly out of the quest's own
  -- hyperlink. This works without any external database at all — it's
  -- exactly how pfQuest itself resolves quest log entries to IDs (see
  -- pfQuest's database.lua, pfDatabase:GetQuestIDs).
  if GetQuestLink then
    local questLink = GetQuestLink(qlogid)
    if questLink then
      local _, _, id = strfind(questLink, "|c.*|Hquest:([%d]+):([%-]?[%d]+)|h%[(.*)%]|h|r")
      if id then return { tonumber(id) } end
    end
  end

  local title, level, tag, header, collapsed, complete = AutoQuestLearner_GetQuestLogTitle(qlogid)
  if not title or header then return nil end

  -- Search for quest ID by title in our database. Duplicate titles are
  -- common (faction-mirrored quests, multi-stage chains that reuse the
  -- same title at every step -- e.g. "Call of Water" alone spans 20
  -- different quest IDs in the bundled database), so this collects
  -- EVERY matching ID rather than returning whichever one pairs()
  -- happens to iterate to first (which isn't even deterministic between
  -- game sessions). When there's more than one candidate, the quest's
  -- displayed level (always available from the log itself) is used to
  -- pick the one whose bundled level range actually fits -- a cheap,
  -- effective disambiguator for exactly this case, though not a
  -- guarantee for same-title/same-level quests (rare).
  local matches = {}
  for id, data in pairs(AutoQuestLearnerData.quests["loc"]) do
    if data.T == title then
      table.insert(matches, id)
    end
  end

  if table.getn(matches) == 1 then
    return matches
  elseif table.getn(matches) > 1 then
    if level then
      for _, id in pairs(matches) do
        local qdata = AutoQuestLearnerData.quests["data"][id]
        if qdata and (qdata["lvl"] == level or qdata["min"] == level) then
          return { id }
        end
      end
    end
    -- No level match (or no level info) -- fall back to the first
    -- candidate rather than nothing, but this is the ambiguous case;
    -- learned location data may end up attributed to the wrong ID
    -- among the same-titled group.
    return { matches[1] }
  end
  
  -- Return title as fallback if not found in database
  return { title }
end

-- Get quest IDs by name
function AutoQuestLearnerDatabase:GetIDByName(name, type)
  local ids = {}
  type = type or "quests"
  
  if not AutoQuestLearnerData[type] or not AutoQuestLearnerData[type]["loc"] then
    return ids
  end
  
  for id, data in pairs(AutoQuestLearnerData[type]["loc"]) do
    if data.T == name then
      table.insert(ids, id)
    end
  end
  
  return ids
end

-- Search quests for the map system
-- Plots every known spawn coordinate for a single unit, object, or
-- areatrigger ID as a map/minimap node tied to a quest. pfDB's real
-- structure stores start/end/objective targets as plain arrays of IDs
-- keyed by "U" (units), "O" (objects), "I" (items), or "A"
-- (areatrigger) -- e.g. quests["data"][id]["start"]["U"] = {npcID1,
-- npcID2}. Items have no coordinates of their own (they don't spawn
-- anywhere, only their sources do -- see ResolveItemSources), so only
-- "U", "O", and "A" produce map nodes directly.
local function AddSpawnNode(maps, meta, questID, questLoc, questData, idType, id, texture, vertex, roleLabel, singleOnly, isObjective)
  local data
  if idType == "U" then
    data = AutoQuestLearnerData.units["data"][id]
  elseif idType == "O" then
    data = AutoQuestLearnerData.objects["data"][id]
  elseif idType == "A" then
    data = AutoQuestLearnerData.areatrigger["data"][id]
  end
  if not data or not data["coords"] then return end

  for _, coord in pairs(data["coords"]) do
    -- coord[3] is the zone ID; coord[4] is respawn time, NOT an
    -- alternate zone field (matches pfQuest's own SearchMobID, which
    -- uses coord[3] directly and never falls back to coord[4]).
    local mapID = coord[3]
    if mapID and mapID > 0 then
      maps[mapID] = maps[mapID] or {}

      AutoQuestLearnerMap:AddNode({
        addon = meta.addon or "AUTOQUESTLEARNER",
        title = questLoc.T or ("Quest " .. questID),
        zone = mapID,
        x = coord[1],
        y = coord[2],
        spawn = (idType == "U" and AutoQuestLearner_GetUnitName(id))
            or (idType == "O" and AutoQuestLearner_GetObjectName(id))
            or (idType == "A" and "Search area")
            or (roleLabel .. " " .. id),
        texture = texture,
        vertex = vertex,
        questid = questID,
        qlvl = questData["lvl"],
        qmin = questData["min"],
        isObjective = isObjective,
      })

      -- Simple (non-cluster) mode: only plot one representative spawn
      -- point per mob/object rather than every known location.
      if singleOnly then return end
    end
  end
end

-- Resolves an item ID to its actual kill/interact drop sources, using
-- pfQuest's own items+refloot database structure: an item's direct
-- "U"(nit)/"O"(bject) sources, plus any indirect sources reached via a
-- shared reference loot template ("R", used when many similar mobs
-- share one loot table rather than each having their own entry).
--
-- This is what lets an item-collection quest ("Collect 8 Witchwing
-- Talons") resolve to actual NPC spawn points. Without it, that entire
-- (very common) quest type showed zero objective nodes -- there's no
-- NPC/object ID directly attached to the quest, only an item
-- requirement, and this addon had no item-drop database at all to
-- cross-reference it against until now.
local function ResolveItemSources(itemID)
  local sources = {}
  local itemData = AutoQuestLearnerData.items and AutoQuestLearnerData.items["data"]
      and AutoQuestLearnerData.items["data"][itemID]
  if not itemData then return sources end

  if itemData["U"] then
    for npcID in pairs(itemData["U"]) do
      table.insert(sources, { type = "U", id = npcID })
    end
  end
  if itemData["O"] then
    for objID in pairs(itemData["O"]) do
      table.insert(sources, { type = "O", id = objID })
    end
  end
  if itemData["R"] then
    for refID in pairs(itemData["R"]) do
      local refData = AutoQuestLearnerData.refloot and AutoQuestLearnerData.refloot["data"]
          and AutoQuestLearnerData.refloot["data"][refID]
      if refData then
        if refData["U"] then
          for npcID in pairs(refData["U"]) do
            table.insert(sources, { type = "U", id = npcID })
          end
        end
        if refData["O"] then
          for objID in pairs(refData["O"]) do
            table.insert(sources, { type = "O", id = objID })
          end
        end
      end
    end
  end

  return sources
end

-- Looks up a unit or object's display name from AutoQuestLearnerData's
-- "loc" tables. Handles both formats that data can arrive in: a bare
-- string (pfQuest's own enUS/units.lua, enUS/objects.lua) or a
-- {["T"]="Name"} table (Questie-derived sources, matching the quest
-- title table's own shape). Used to make the arrow and map node
-- tooltips say the actual NPC/object name ("Turn in to Cliffwatcher
-- Longhorn") instead of just the quest title with a generic suffix.
function AutoQuestLearner_GetUnitName(id)
  local v = AutoQuestLearnerData.units and AutoQuestLearnerData.units["loc"] and AutoQuestLearnerData.units["loc"][id]
  if type(v) == "string" then return v end
  if type(v) == "table" and v["T"] then return v["T"] end
  return nil
end

function AutoQuestLearner_GetObjectName(id)
  local v = AutoQuestLearnerData.objects and AutoQuestLearnerData.objects["loc"] and AutoQuestLearnerData.objects["loc"][id]
  if type(v) == "string" then return v end
  if type(v) == "table" and v["T"] then return v["T"] end
  return nil
end

function AutoQuestLearner_GetItemName(id)
  local v = AutoQuestLearnerData.items and AutoQuestLearnerData.items["loc"] and AutoQuestLearnerData.items["loc"][id]
  if type(v) == "string" and v ~= "_" then return v end
  if type(v) == "table" and v["T"] and v["T"] ~= "_" then return v["T"] end
  return nil
end


function AutoQuestLearner_ResolveItemSources(itemID)
  return ResolveItemSources(itemID)
end

function AutoQuestLearnerDatabase:SearchQuestID(questID, meta, maps)
  if not questID then return maps or {} end
  if not meta then meta = {} end
  if not maps then maps = {} end
  
  -- Get quest data
  local questData = AutoQuestLearnerData.quests["data"][questID]
  local questLoc = AutoQuestLearnerData.quests["loc"][questID]
  
  if not questData or not questLoc then return maps end

  local showSingleMarkers = AutoQuestLearnerDB.config.nodestyle ~= "spawn"

  -- Quest start (where you pick it up): pfQuest's own icon for this is
  -- "available_c" -- the muted/grey "!" mark, used regardless of
  -- whether the quest is in your log yet (matches pfQuest's
  -- SearchQuestID exactly: this branch only ever runs when
  -- meta["qlogid"] is NOT set, i.e. before it's in your log). No
  -- vertex-color override, matching pfQuest -- the icon's own baked-in
  -- color carries the meaning, tinting it would just look wrong.
  -- BUGFIX: if we've personally confirmed the giver's location for this
  -- quest, that learned node (drawn separately by DrawLearnedNode in
  -- AutoQuestLearner.lua) already covers it -- skip the static one
  -- entirely instead of drawing both. Ascension is a private server
  -- with its own custom NPC placements, so the bundled static database
  -- (converted from vanilla/WotLK sources) is frequently just wrong
  -- here; showing both pins meant the wrong static one was still
  -- sitting on the map right alongside the correct learned one.
  local learnedEntry = AutoQuestLearner_GetLearnedQuestEntry
      and AutoQuestLearnerDB.learned
      and AutoQuestLearner_GetLearnedQuestEntry(AutoQuestLearnerDB.learned, questID, questLoc and questLoc.T)
  local hasLearnedGiver = learnedEntry and learnedEntry.giver and learnedEntry.giver.mapID
  local hasLearnedTurnin = learnedEntry and learnedEntry.turnin and learnedEntry.turnin.mapID

  if showSingleMarkers and questData["start"] and not meta["qlogid"] and not hasLearnedGiver then
    if questData["start"]["U"] then
      for _, npcID in pairs(questData["start"]["U"]) do
        AddSpawnNode(maps, meta, questID, questLoc, questData, "U", npcID,
          "Interface\\AddOns\\AutoQuestLearner\\img\\available_c", { 1, 1, 1 }, "NPC")
      end
    end
    if questData["start"]["O"] then
      for _, objID in pairs(questData["start"]["O"]) do
        AddSpawnNode(maps, meta, questID, questLoc, questData, "O", objID,
          "Interface\\AddOns\\AutoQuestLearner\\img\\available_c", { 1, 1, 1 }, "Object")
      end
    end
  end

  -- Quest end (where you turn it in): pfQuest uses "complete" (bright
  -- "?") while the quest still has incomplete objectives, and
  -- "complete_c" (the alternate state) once it's actually ready to hand
  -- in -- same convention pfQuest itself uses, replicated here.
  --
  -- NOTE: pfQuest's own completion check falls back to treating
  -- GetNumQuestLeaderBoards(qlogid) == 0 as "complete", which is NOT
  -- used here -- that heuristic is unreliable on this client (a
  -- leaderboard that hasn't finished loading yet also reads as 0,
  -- which previously caused real incomplete quests to be misreported).
  -- Instead this cross-checks every individual leaderboard entry, the
  -- same fix already applied to the arrow's own completion detection.
  if showSingleMarkers and questData["end"] and not hasLearnedTurnin then
    local endTexture = "Interface\\AddOns\\AutoQuestLearner\\img\\complete_c"
    if meta["qlogid"] then
      local _, _, _, _, _, isComplete = AutoQuestLearner_GetQuestLogTitle(meta["qlogid"])
      if not isComplete then
        local numObjectives = GetNumQuestLeaderBoards(meta["qlogid"])
        if numObjectives and numObjectives > 0 then
          isComplete = true
          for i = 1, numObjectives do
            local _, _, objDone = GetQuestLogLeaderBoard(i, meta["qlogid"])
            if not objDone then
              isComplete = false
              break
            end
          end
        end
      end
      endTexture = isComplete
          and "Interface\\AddOns\\AutoQuestLearner\\img\\complete_c"
          or "Interface\\AddOns\\AutoQuestLearner\\img\\complete"
    end

    if questData["end"]["U"] then
      for _, npcID in pairs(questData["end"]["U"]) do
        AddSpawnNode(maps, meta, questID, questLoc, questData, "U", npcID,
          endTexture, { 1, 1, 1 }, "NPC")
      end
    end
    if questData["end"]["O"] then
      for _, objID in pairs(questData["end"]["O"]) do
        AddSpawnNode(maps, meta, questID, questLoc, questData, "O", objID,
          endTexture, { 1, 1, 1 }, "Object")
      end
    end
  end

  -- Quest objectives: kill/interact targets. In cluster mode this
  -- plots every known spawn point for the required mob or object as
  -- its own node (the "cloud of dots" style); in simple mode it plots
  -- just one representative marker per mob/object instead.
  --
  -- Plain colored circles instead of pfQuest's cluster_mob/cluster_misc
  -- icon assets (a sword badge for kill targets, a cog/gear badge for
  -- object/misc targets) -- readable at a glance without needing to
  -- squint at which tiny symbol is baked into each dot.
  --
  -- Colored per specific target ID (the exact NPC/object/areatrigger),
  -- not just per objective type -- a quest asking for parts from 4
  -- different creatures would otherwise show all 4 creatures' spawn
  -- clouds in one indistinguishable color. Reuses the same ID->color
  -- hash AutoQuestLearner.lua's learned-objective clusters use
  -- (AutoQuestLearner_IDColor), so a given NPC gets a consistent color
  -- whether its spawn points came from the static database or from
  -- your own play.
  local useClusters = AutoQuestLearnerDB.config.nodestyle ~= "simple"
  -- Ring instead of a solid filled dot -- see AutoQuestLearner.lua's
  -- matching comment for the reasoning (visibility of the character
  -- arrow underneath a dense cluster).
  local objTexture = "Interface\\AddOns\\AutoQuestLearner\\img\\node_ring"

  -- Skip any sub-objective that's already individually done, by name --
  -- same approach as FindClosestQuestDestination/DrawStaticObjectiveClusters.
  -- Without this, this function (used on quest state-change reloads and
  -- the manual "Show" tracker button) could redraw an already-culled
  -- finished sub-objective's dots right back.
  local doneObjectiveTexts = {}
  local qlogid = AutoQuestLearner.questlog and AutoQuestLearner.questlog[questID] and AutoQuestLearner.questlog[questID].qlogid
  if qlogid then
    local numObjectives = GetNumQuestLeaderBoards(qlogid)
    if numObjectives and numObjectives > 0 then
      for oi = 1, numObjectives do
        local otext, _, odone = GetQuestLogLeaderBoard(oi, qlogid)
        if odone and otext then
          local label = string.match(otext, "^(.-)%s*:?%s*%d") or otext
          table.insert(doneObjectiveTexts, string.lower(label))
        end
      end
    end
  end
  local function isAlreadyDone(name)
    if not name or table.getn(doneObjectiveTexts) == 0 then return false end
    local nameLower = string.lower(name)
    for _, doneText in pairs(doneObjectiveTexts) do
      if string.find(doneText, nameLower, 1, true) or string.find(nameLower, doneText, 1, true) then
        return true
      end
    end
    return false
  end

  -- Static objective clusters are shown alongside any learned data,
  -- not replaced by it -- this pass is for visual/area context on the
  -- map (showing everywhere the mob/object is known to possibly be),
  -- while the ARROW's target selection (FindClosestQuestDestination in
  -- AutoQuestLearner.lua) is what actually needs to trust learned data
  -- exclusively for navigation. Those are different jobs: suppressing
  -- static data here too meant a single learned point replaced what
  -- used to be a rich cluster of dozens of static dots, so nearly
  -- every quest with any learned data at all appeared to lose most of
  -- its cluster.
  if questData["obj"] then
    if questData["obj"]["U"] then
      for _, npcID in pairs(questData["obj"]["U"]) do
        if not isAlreadyDone(AutoQuestLearner_GetUnitName and AutoQuestLearner_GetUnitName(npcID)) then
          AddSpawnNode(maps, meta, questID, questLoc, questData, "U", npcID,
            objTexture, { AutoQuestLearner_IDColor(npcID) }, "NPC", not useClusters, true)
        end
      end
    end
    if questData["obj"]["O"] then
      for _, objID in pairs(questData["obj"]["O"]) do
        if not isAlreadyDone(AutoQuestLearner_GetObjectName and AutoQuestLearner_GetObjectName(objID)) then
          AddSpawnNode(maps, meta, questID, questLoc, questData, "O", objID,
            objTexture, { AutoQuestLearner_IDColor(objID) }, "Object", not useClusters, true)
        end
      end
    end
    if questData["obj"]["I"] then
      for _, itemID in pairs(questData["obj"]["I"]) do
        local itemName = AutoQuestLearner_GetItemName and AutoQuestLearner_GetItemName(itemID)
        if not isAlreadyDone(itemName) then
          local sources = ResolveItemSources(itemID)
          for _, src in pairs(sources) do
            local srcName = itemName
                or (src.type == "U" and AutoQuestLearner_GetUnitName and AutoQuestLearner_GetUnitName(src.id))
                or (AutoQuestLearner_GetObjectName and AutoQuestLearner_GetObjectName(src.id))
            if not isAlreadyDone(srcName) then
              if src.type == "U" then
                AddSpawnNode(maps, meta, questID, questLoc, questData, "U", src.id,
                  objTexture, { AutoQuestLearner_IDColor(src.id) }, "NPC", not useClusters, true)
              else
                AddSpawnNode(maps, meta, questID, questLoc, questData, "O", src.id,
                  objTexture, { AutoQuestLearner_IDColor(src.id) }, "Object", not useClusters, true)
              end
            end
          end
        end
      end
    end
    if questData["obj"]["A"] then
      for _, triggerID in pairs(questData["obj"]["A"]) do
        if not isAlreadyDone("search area") then
          AddSpawnNode(maps, meta, questID, questLoc, questData, "A", triggerID,
            objTexture, { AutoQuestLearner_IDColor(triggerID) }, "Area", not useClusters, true)
        end
      end
    end
  end
  
  return maps
end

-- Search for available quests (quest givers)
function AutoQuestLearnerDatabase:SearchQuests(meta)
  if not meta then meta = {} end
  
  local playerLevel = UnitLevel("player")
  local mapID = (AutoQuestLearner_GetCurrentMap and AutoQuestLearner_GetCurrentMap())
      or AutoQuestLearnerMap:GetMapID(GetCurrentMapContinent(), GetCurrentMapZone())

  -- BUGFIX: this never filtered by faction at all -- a Horde player
  -- could see Alliance-only quest givers (and vice versa) shown as
  -- "available" on their map, most likely to actually be noticed in
  -- contested/shared zones where both factions' content coexists.
  -- Uses the shared bitwise check (AutoQuestLearner_IsWrongFactionQuest
  -- in AutoQuestLearner.lua) -- a plain fixed-value comparison here
  -- previously only caught quests restricted to an entire faction's
  -- combined race set, missing single-race restrictions like
  -- Draenei-only or Night Elf-only.
  local playerFaction = UnitFactionGroup and UnitFactionGroup("player")
  
  -- Search through all quests for available ones
  for questID, questData in pairs(AutoQuestLearnerData.quests["data"]) do
    local questLoc = AutoQuestLearnerData.quests["loc"][questID]
    if questLoc then
      -- Skip internal/deprecated entries (OLD/BETA/UNUSED/FLAG/[Not
      -- Used] prefixes) baked into the bundled database -- see
      -- AutoQuestLearner_IsDeprecatedQuestTitle's own comment in
      -- AutoQuestLearner.lua for the full count/explanation.
      local shouldSkip = AutoQuestLearner_IsDeprecatedQuestTitle and AutoQuestLearner_IsDeprecatedQuestTitle(questLoc.T)
      local raceField = AutoQuestLearner_GetEffectiveRaceField and AutoQuestLearner_GetEffectiveRaceField(questID, questData) or questData["race"]
      if not shouldSkip and AutoQuestLearner_IsWrongFactionQuest and AutoQuestLearner_IsWrongFactionQuest(raceField, playerFaction) then
        shouldSkip = true
      end
      -- Profession-gated quests shouldn't be suggested to a player who
      -- doesn't have the required profession (see the matching
      -- check/comment in RefreshAvailableGiverCache in
      -- AutoQuestLearner.lua for the full explanation).
      if not shouldSkip and AutoQuestLearner_HasRequiredSkill and not AutoQuestLearner_HasRequiredSkill(questData["skill"]) then
        shouldSkip = true
      end
      
      -- Check if quest is appropriate for player level
      local minLevel = questData["min"] or 0
      local questLevel = questData["lvl"] or minLevel
      
      -- Skip if too high level
      if not shouldSkip and AutoQuestLearnerDB.config.showhighlevel == "0" and questLevel > playerLevel + 3 then
        shouldSkip = true
      end
      
      -- Skip if too low level
      if not shouldSkip and AutoQuestLearnerDB.config.showlowlevel == "0" and questLevel < playerLevel - 5 then
        shouldSkip = true
      end
      
      -- Check if already completed, using the server's actual
      -- completion history (via QueryQuestsCompleted/GetQuestsCompleted
      -- in AutoQuestLearner.lua) rather than only this addon's own
      -- history table, which has no record of anything completed
      -- before this addon was installed/updated -- that gap made
      -- long-since-finished quests keep showing as "available"
      -- forever. Falls back to the addon's own history (checked by
      -- both numeric quest ID and title text, for the same alias
      -- reasons as elsewhere) if the server API isn't available.
      if not shouldSkip and AutoQuestLearner_IsQuestCompleted and AutoQuestLearner_IsQuestCompleted(questID, questLoc.T) then
        shouldSkip = true
      end

      -- Skip quests gated behind an incomplete prerequisite (see the
      -- matching check/comment in RefreshAvailableGiverCache in
      -- AutoQuestLearner.lua for the full explanation).
      if not shouldSkip and AutoQuestLearner_GetQuestPrerequisites then
        local prereqIDs = AutoQuestLearner_GetQuestPrerequisites(questID)
        if prereqIDs then
          for _, prereqID in ipairs(prereqIDs) do
            local prereqLoc = AutoQuestLearnerData.quests["loc"][prereqID]
            if not (AutoQuestLearner_IsQuestCompleted and AutoQuestLearner_IsQuestCompleted(prereqID, prereqLoc and prereqLoc.T)) then
              shouldSkip = true
              break
            end
          end
        end
      end
      
      -- Check if currently in quest log (same alias reasoning as above
      -- -- AutoQuestLearner.questlog can be keyed by title instead of
      -- numeric ID for the same reason).
      if not shouldSkip and AutoQuestLearner.questlog and
          (AutoQuestLearner.questlog[questID] or (questLoc.T and AutoQuestLearner.questlog[questLoc.T])) then
        if AutoQuestLearnerDB.config.currentquestgivers == "0" then
          shouldSkip = true
        end
      end
      
      if not shouldSkip and questData["start"] then
        -- Quest givers, current-zone only (pfDB's real "U"/"O" array
        -- structure -- see SearchQuestID for details).
        local foundGiverInZone = false
        if questData["start"]["U"] then
          for _, npcID in pairs(questData["start"]["U"]) do
            local npcData = AutoQuestLearnerData.units["data"][npcID]
            if npcData and npcData["coords"] then
              for _, coord in pairs(npcData["coords"]) do
                local npcMapID = coord[3]
                if npcMapID == mapID then
                  foundGiverInZone = true
                  AutoQuestLearnerMap:AddNode({
                    addon = "AUTOQUESTLEARNER",
                    title = questLoc.T or ("Quest " .. questID),
                    zone = mapID,
                    x = coord[1],
                    y = coord[2],
                    spawn = npcData["name"] or ("NPC " .. npcID),
                    texture = "Interface\\AddOns\\AutoQuestLearner\\img\\node_ring",
                    vertex = { 0, 1, 0 },
                    questid = questID,
                    qlvl = questLevel,
                    qmin = minLevel,
                  })
                end
              end
            end
          end
        end
        if questData["start"]["O"] then
          for _, objID in pairs(questData["start"]["O"]) do
            local objData = AutoQuestLearnerData.objects["data"][objID]
            if objData and objData["coords"] then
              for _, coord in pairs(objData["coords"]) do
                local objMapID = coord[3]
                if objMapID == mapID then
                  foundGiverInZone = true
                  AutoQuestLearnerMap:AddNode({
                    addon = "AUTOQUESTLEARNER",
                    title = questLoc.T or ("Quest " .. questID),
                    zone = mapID,
                    x = coord[1],
                    y = coord[2],
                    spawn = objData["name"] or ("Object " .. objID),
                    texture = "Interface\\AddOns\\AutoQuestLearner\\img\\node_ring",
                    vertex = { 0, 1, 0 },
                    questid = questID,
                    qlvl = questLevel,
                    qmin = minLevel,
                  })
                end
              end
            end
          end
        end
        -- Fallback: precise quest-giver coordinate from RXPGuides (see
        -- bundled-db/rxp-coords.lua), only used when no NPC/object
        -- match was found in this zone at all -- avoids duplicate pins
        -- for quests we already have good data for, while still
        -- covering ones where our own start-NPC data doesn't resolve.
        if not foundGiverInZone then
          local numericQuestID3 = tonumber(questID)
          local rxpGive = numericQuestID3 and AutoQuestLearnerRXPAccept and AutoQuestLearnerRXPAccept[numericQuestID3]
          if rxpGive and rxpGive[3] == mapID then
            AutoQuestLearnerMap:AddNode({
              addon = "AUTOQUESTLEARNER",
              title = questLoc.T or ("Quest " .. questID),
              zone = mapID,
              x = rxpGive[1],
              y = rxpGive[2],
              spawn = "Quest Giver",
              texture = "Interface\\AddOns\\AutoQuestLearner\\img\\node_ring",
              vertex = { 0, 1, 0 },
              questid = questID,
              qlvl = questLevel,
              qmin = minLevel,
            })
          end
        end
      end
    end
  end
end

-- Build quest description for tooltips
function AutoQuestLearnerDatabase:BuildQuestDescription(meta)
  if not meta then return {} end
  
  local desc = {}
  
  if meta.questid and AutoQuestLearnerData.quests["loc"][meta.questid] then
    local questLoc = AutoQuestLearnerData.quests["loc"][meta.questid]
    local questData = AutoQuestLearnerData.quests["data"][meta.questid]
    
    desc.quest = questLoc.T or ""
    desc.qlvl = questData and questData["lvl"]
    desc.qmin = questData and questData["min"]
  end
  
  if meta.spawn then
    desc.spawn = meta.spawn
  end
  
  return desc
end

-- Get best map for displaying quest
function AutoQuestLearnerDatabase:GetBestMap(maps)
  if not maps then return nil end
  
  -- Prefer current map
  local currentMap = (AutoQuestLearner_GetCurrentMap and AutoQuestLearner_GetCurrentMap())
      or AutoQuestLearnerMap:GetMapID(GetCurrentMapContinent(), GetCurrentMapZone())
  if maps[currentMap] then
    return currentMap
  end
  
  -- Otherwise return first available map
  for mapID, _ in pairs(maps) do
    return mapID
  end
  
  return nil
end

-- Initialize database on load
local function InitializeDatabase()
  EnsureDatabaseTables()
  LoadPfQuestData()
  LoadAscensionData()
  
  -- Mark database as localized
  AutoQuestLearnerDB.localized = true

  -- NOTE: table.getn()/# only give a meaningful count on a sequential
  -- array (1, 2, 3, ...). AutoQuestLearnerData.quests["data"] is keyed
  -- by sparse quest IDs (9, 233, 580168, ...), so table.getn() on it
  -- always returned 0 regardless of how many quests actually loaded --
  -- this was purely a cosmetic bug in this print statement, not a sign
  -- the database failed to load.
  local questCount, unitCount, objectCount = 0, 0, 0
  for _ in pairs(AutoQuestLearnerData.quests["data"]) do questCount = questCount + 1 end
  for _ in pairs(AutoQuestLearnerData.units["data"]) do unitCount = unitCount + 1 end
  for _ in pairs(AutoQuestLearnerData.objects["data"]) do objectCount = objectCount + 1 end

  print("|cff00ff00AutoQuestLearner|r loading complete:")
  print("  |cffaaaaaaDatabase:|r " .. questCount .. " quests, " .. unitCount ..
        " NPCs, " .. objectCount .. " objects (bundled + pfQuest, if present)")

  local learnedQuestCount, learnedObjCount = 0, 0
  if AutoQuestLearnerDB.learned then
    if AutoQuestLearnerDB.learned.quests then
      for _ in pairs(AutoQuestLearnerDB.learned.quests) do learnedQuestCount = learnedQuestCount + 1 end
    end
    if AutoQuestLearnerDB.learned.objectives then
      for _, objectives in pairs(AutoQuestLearnerDB.learned.objectives) do
        for _, obj in pairs(objectives) do
          learnedObjCount = learnedObjCount + table.getn(obj.spawns or {})
        end
      end
    end
  end
  print("  |cffaaaaaaYour learned data:|r " .. learnedQuestCount .. " quests, " ..
        learnedObjCount .. " objective spawn points (shared across all your characters)")
  print("  |cffaaaaaaSource:|r bundled starter database (pfQuest + Questie-X data), automatically merged with a separately-installed pfQuest if you also have one")
  print("  Run |cffffcc00/aql debug|r or |cffffcc00/aql arrow|r any time for a live status check.")
end

-- Register for addon loaded to initialize database
local dbFrame = CreateFrame("Frame")
dbFrame:RegisterEvent("ADDON_LOADED")
dbFrame:SetScript("OnEvent", function()
  if arg1 == "AutoQuestLearner" then
    InitializeDatabase()
  end
end)

-- Make globally available
AutoQuestLearner = AutoQuestLearner or CreateFrame("Frame")
AutoQuestLearner.db = AutoQuestLearnerDatabase

-- Timing instrumentation for the "available quest givers" scan --
-- iterates the entire bundled quest database (6600+ entries) and is a
-- strong candidate for being the actual expensive operation, separate
-- from the map/minimap drawing instrumented in AutoQuestLearner_Map.lua.
-- NOTE: this table is defined idempotently here rather than only in
-- Map.lua, since Database.lua loads BEFORE Map.lua per the .toc --
-- checking for it without also being able to create it here would have
-- silently skipped this wrapper entirely.
AutoQuestLearner_PerfDebug = AutoQuestLearner_PerfDebug or { active = false, totals = {}, calls = {} }
local originalSearchQuests = AutoQuestLearnerDatabase.SearchQuests
AutoQuestLearnerDatabase.SearchQuests = function(self, ...)
  if not AutoQuestLearner_PerfDebug.active then
    return originalSearchQuests(self, ...)
  end
  local ok, startTime = pcall(debugprofilestop)
  if not ok then
    return originalSearchQuests(self, ...)
  end
  local a, b, c = originalSearchQuests(self, ...)
  local elapsed = debugprofilestop() - startTime
  AutoQuestLearner_PerfDebug.totals["SearchQuests"] = (AutoQuestLearner_PerfDebug.totals["SearchQuests"] or 0) + elapsed
  AutoQuestLearner_PerfDebug.calls["SearchQuests"] = (AutoQuestLearner_PerfDebug.calls["SearchQuests"] or 0) + 1
  return a, b, c
end

-- Zone completion tracking ("X/Y known quests done in this zone").
--
-- Counts every bundled quest whose START (giver) NPC/object has a
-- coordinate in the player's current zone -- the same definition of
-- "quest belongs to this zone" that SearchQuests already uses for the
-- available-quest-givers feature above, reused here for consistency
-- rather than inventing a different rule. A quest counts as "done" if
-- it's in AutoQuestLearnerDB.history, checked with the same
-- numeric-ID-or-title alias tolerance used everywhere else in this
-- addon (completion can get recorded under either form depending on
-- how the quest was resolved at turn-in time).
--
-- This scans the full bundled quest database (6600+ entries), same
-- cost class as SearchQuests -- intentionally an on-demand command
-- (/aql zone), not something polled continuously.
function AutoQuestLearnerDatabase:GetZoneCompletion()
  local mapID = (AutoQuestLearner_GetCurrentMap and AutoQuestLearner_GetCurrentMap())
      or AutoQuestLearnerMap:GetMapID(GetCurrentMapContinent(), GetCurrentMapZone())

  -- Faction filtering: a quest whose "race" field is exclusively the
  -- OPPOSITE faction's combined race bitmask should never count toward
  -- this character's zone completion -- they can never accept or
  -- complete it, so including it just inflates the "remaining" count
  -- with quests that were never actually available. Values verified
  -- directly against the real bundled data (not assumed): summing each
  -- faction's individual race bits gives exactly the two most frequent
  -- values found in a scan of the actual database. 77/178 are the
  -- vanilla-only values; TBC adds Draenei/Blood Elf, giving 1101/690.
  -- Faction check uses the shared bitwise test
  -- (AutoQuestLearner_IsWrongFactionQuest in AutoQuestLearner.lua) --
  -- see its own comment for why a plain fixed-value comparison misses
  -- single-race restrictions.
  local playerFaction = UnitFactionGroup and UnitFactionGroup("player")

  local total, done = 0, 0
  local incompleteTitles = {}

  for questID, questData in pairs(AutoQuestLearnerData.quests["data"]) do
    local questLoc = AutoQuestLearnerData.quests["loc"][questID]
    local raceField = AutoQuestLearner_GetEffectiveRaceField and AutoQuestLearner_GetEffectiveRaceField(questID, questData) or questData["race"]
    local wrongFaction = AutoQuestLearner_IsWrongFactionQuest and AutoQuestLearner_IsWrongFactionQuest(raceField, playerFaction)

    if questLoc and questData["start"] and not wrongFaction then
      local inZone = false

      if questData["start"]["U"] then
        for _, npcID in pairs(questData["start"]["U"]) do
          local npcData = AutoQuestLearnerData.units["data"][npcID]
          if npcData and npcData["coords"] then
            for _, coord in pairs(npcData["coords"]) do
              if coord[3] == mapID then
                inZone = true
                break
              end
            end
          end
          if inZone then break end
        end
      end

      if not inZone and questData["start"]["O"] then
        for _, objID in pairs(questData["start"]["O"]) do
          local objData = AutoQuestLearnerData.objects["data"][objID]
          if objData and objData["coords"] then
            for _, coord in pairs(objData["coords"]) do
              if coord[3] == mapID then
                inZone = true
                break
              end
            end
          end
          if inZone then break end
        end
      end

      if inZone then
        total = total + 1
        local isDone = AutoQuestLearner_IsQuestCompleted and AutoQuestLearner_IsQuestCompleted(questID, questLoc.T)
        if isDone then
          done = done + 1
        elseif questLoc.T and table.getn(incompleteTitles) < 15 then
          table.insert(incompleteTitles, questLoc.T)
        end
      end
    end
  end

  return total, done, incompleteTitles, mapID
end
