local ADDON_NAME, ns = ...
local WF = ns.WF

WF.Share = WF.Share or {}
local Share = WF.Share
local LibDeflate = (LibStub and LibStub("LibDeflate", true)) or _G.LibDeflate or nil

local PREFIX = "WhatFarmRun"
local SHARED_VIEWER_FRAME_NAME = "WF_SharedRunViewer"
local SHARED_VIEWER_LOCK_KEY = "uiLocked_sharedRunViewer"
local SHARED_VIEWER_WIDTH_KEY = "sharedRunViewerW"
local SHARED_VIEWER_HEIGHT_KEY = "sharedRunViewerH"
local CHUNK_SIZE = 220
local CACHE_TTL = 1800
local MAX_CACHE_ENTRIES = 25
local PENDING_TTL = 120
local MAX_CHUNKS = 256

WF.UI = WF.UI or {}
WF.UI.UtilityWindowFocus = WF.UI.UtilityWindowFocus or {}
do
  local Focus = WF.UI.UtilityWindowFocus
  if not Focus.Claim then
    function Focus:Claim(key, frame)
      if not frame then return end

      self.counter = (tonumber(self.counter) or 0) + 1
      self.activeKey = key

      if frame.SetFrameStrata then
        frame:SetFrameStrata("DIALOG")
      end

      if frame.SetFrameLevel then
        local nextLevel = 800 + (self.counter % 200)
        local bagWorth = _G and _G.WF_BagWorthBreakdownFrame or nil
        local sharedViewer = _G and _G.WF_SharedRunViewer or nil

        if bagWorth and bagWorth ~= frame and bagWorth.GetFrameLevel then
          nextLevel = math.max(nextLevel, (bagWorth:GetFrameLevel() or 0) + 20)
        end
        if sharedViewer and sharedViewer ~= frame and sharedViewer.GetFrameLevel then
          nextLevel = math.max(nextLevel, (sharedViewer:GetFrameLevel() or 0) + 20)
        end

        frame:SetFrameLevel(math.min(nextLevel, 9000))
      end

      if frame.Raise then
        frame:Raise()
      end
    end
  end
end
local CHAT_FILTER_EVENTS = {
  "CHAT_MSG_SAY",
  "CHAT_MSG_YELL",
  "CHAT_MSG_GUILD",
  "CHAT_MSG_OFFICER",
  "CHAT_MSG_PARTY",
  "CHAT_MSG_PARTY_LEADER",
  "CHAT_MSG_RAID",
  "CHAT_MSG_RAID_LEADER",
  "CHAT_MSG_RAID_WARNING",
  "CHAT_MSG_INSTANCE_CHAT",
  "CHAT_MSG_INSTANCE_CHAT_LEADER",
  "CHAT_MSG_WHISPER",
  "CHAT_MSG_WHISPER_INFORM",
  "CHAT_MSG_BN_WHISPER",
  "CHAT_MSG_BN_WHISPER_INFORM",
  "CHAT_MSG_CHANNEL",
}

Share._cache = Share._cache or {}
Share._pending = Share._pending or {}
Share._routeHints = Share._routeHints or {}
Share._setItemRefHooked = Share._setItemRefHooked or false
Share._chatFiltersHooked = Share._chatFiltersHooked or false
Share._prefixRegistered = Share._prefixRegistered or false

local function Trim(s)
  s = tostring(s or "")
  s = s:gsub("^%s+", ""):gsub("%s+$", "")
  return s
end

local function UrlEncode(s)
  s = tostring(s or "")
  if s == "" then return "" end

  local out = {}
  for i = 1, #s do
    local b = s:byte(i)
    if (b >= 48 and b <= 57) or (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or b == 45 or b == 46 or b == 95 or b == 126 then
      out[#out + 1] = string.char(b)
    elseif b == 32 then
      out[#out + 1] = "+"
    else
      out[#out + 1] = string.format("%%%02X", b)
    end
  end

  return table.concat(out)
end

local function UrlDecode(s)
  s = tostring(s or "")
  if s == "" then return "" end

  s = s:gsub("%+", " ")
  s = s:gsub("%%(%x%x)", function(h)
    return string.char(tonumber(h, 16) or 0)
  end)
  return s
end

local function Utf8CharLenAt(s, i)
  s = tostring(s or "")
  i = tonumber(i) or 1
  local b = s:byte(i)
  if not b then return 0 end
  if b < 0x80 then return 1 end
  if b >= 0xC2 and b <= 0xDF then return 2 end
  if b >= 0xE0 and b <= 0xEF then return 3 end
  if b >= 0xF0 and b <= 0xF4 then return 4 end
  return 1
end

local function Utf8TruncateChars(s, maxChars)
  s = tostring(s or "")
  maxChars = math.max(0, tonumber(maxChars) or 0)
  if s == "" or maxChars <= 0 then
    return ""
  end

  local i = 1
  local count = 0
  while i <= #s and count < maxChars do
    local len = Utf8CharLenAt(s, i)
    if len <= 0 then break end
    if (i + len - 1) > #s then
      break
    end
    i = i + len
    count = count + 1
  end

  return s:sub(1, i - 1)
end

local function ClampEncodedText(s, maxEncodedLen)
  s = tostring(s or "")
  maxEncodedLen = math.max(0, tonumber(maxEncodedLen) or 0)
  if s == "" or maxEncodedLen <= 0 then
    return ""
  end

  local out = {}
  local used = 0
  local i = 1
  while i <= #s do
    local len = Utf8CharLenAt(s, i)
    if len <= 0 then break end
    if (i + len - 1) > #s then
      break
    end
    local ch = s:sub(i, i + len - 1)
    local enc = UrlEncode(ch)
    local partLen = string.len(enc)
    if (used + partLen) > maxEncodedLen then
      break
    end
    out[#out + 1] = ch
    used = used + partLen
    i = i + len
  end

  return table.concat(out)
end

local function NormalizeName(name)
  name = tostring(name or "")
  name = name:gsub("%s+", "")
  name = name:lower()
  return name
end

local function HexByteAt(s, pos)
  local pair = tostring(s or ""):sub(pos, pos + 1)
  if #pair ~= 2 or pair:find("[^%x]", 1) then
    return nil
  end
  return tonumber(pair, 16)
end

local function RepairStrippedUrlUtf8(s)
  s = tostring(s or "")
  if s == "" then return "" end

  local out = {}
  local i = 1
  while i <= #s do
    local b1 = HexByteAt(s, i)
    local bytes = nil
    local used = 0

    if b1 and b1 >= 0xC2 and b1 <= 0xDF then
      local b2 = HexByteAt(s, i + 2)
      if b2 and b2 >= 0x80 and b2 <= 0xBF then
        bytes = string.char(b1, b2)
        used = 4
      end
    elseif b1 and b1 >= 0xE0 and b1 <= 0xEF then
      local b2 = HexByteAt(s, i + 2)
      local b3 = HexByteAt(s, i + 4)
      if b2 and b3 and b2 >= 0x80 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF then
        bytes = string.char(b1, b2, b3)
        used = 6
      end
    elseif b1 and b1 >= 0xF0 and b1 <= 0xF4 then
      local b2 = HexByteAt(s, i + 2)
      local b3 = HexByteAt(s, i + 4)
      local b4 = HexByteAt(s, i + 6)
      if b2 and b3 and b4 and b2 >= 0x80 and b2 <= 0xBF and b3 >= 0x80 and b3 <= 0xBF and b4 >= 0x80 and b4 <= 0xBF then
        bytes = string.char(b1, b2, b3, b4)
        used = 8
      end
    end

    if bytes then
      out[#out + 1] = bytes
      i = i + used
    else
      out[#out + 1] = s:sub(i, i)
      i = i + 1
    end
  end

  return table.concat(out)
end


local function IsValidUtf8String(s)
  s = tostring(s or "")
  local i = 1
  while i <= #s do
    local b1 = s:byte(i)
    if not b1 then return true end

    if b1 < 0x80 then
      i = i + 1
    elseif b1 >= 0xC2 and b1 <= 0xDF then
      local b2 = s:byte(i + 1)
      if not b2 or b2 < 0x80 or b2 > 0xBF then return false end
      i = i + 2
    elseif b1 >= 0xE0 and b1 <= 0xEF then
      local b2, b3 = s:byte(i + 1), s:byte(i + 2)
      if not b2 or not b3 or b2 < 0x80 or b2 > 0xBF or b3 < 0x80 or b3 > 0xBF then return false end
      i = i + 3
    elseif b1 >= 0xF0 and b1 <= 0xF4 then
      local b2, b3, b4 = s:byte(i + 1), s:byte(i + 2), s:byte(i + 3)
      if not b2 or not b3 or not b4 or b2 < 0x80 or b2 > 0xBF or b3 < 0x80 or b3 > 0xBF or b4 < 0x80 or b4 > 0xBF then return false end
      i = i + 4
    else
      return false
    end
  end
  return true
end

local function Utf8CodepointAt(s, i)
  local b1 = s:byte(i)
  if not b1 then return nil, 0 end
  if b1 < 0x80 then return b1, 1 end

  local len = Utf8CharLenAt(s, i)
  if len == 2 then
    local b2 = s:byte(i + 1)
    if not b2 then return nil, 1 end
    return ((b1 - 0xC0) * 0x40) + (b2 - 0x80), 2
  elseif len == 3 then
    local b2, b3 = s:byte(i + 1), s:byte(i + 2)
    if not b2 or not b3 then return nil, 1 end
    return ((b1 - 0xE0) * 0x1000) + ((b2 - 0x80) * 0x40) + (b3 - 0x80), 3
  elseif len == 4 then
    local b2, b3, b4 = s:byte(i + 1), s:byte(i + 2), s:byte(i + 3)
    if not b2 or not b3 or not b4 then return nil, 1 end
    return ((b1 - 0xF0) * 0x40000) + ((b2 - 0x80) * 0x1000) + ((b3 - 0x80) * 0x40) + (b4 - 0x80), 4
  end

  return nil, 1
end

local function RepairLatin1Mojibake(s)
  s = tostring(s or "")
  if s == "" then return "" end
  if not s:find("Ã", 1, true) and not s:find("Â", 1, true) then
    return s
  end

  local out = {}
  local i = 1
  while i <= #s do
    local cp, len = Utf8CodepointAt(s, i)
    if not cp or len <= 0 then
      return s
    end
    if cp > 255 then
      return s
    end
    out[#out + 1] = string.char(cp)
    i = i + len
  end

  local fixed = table.concat(out)
  if fixed ~= "" and IsValidUtf8String(fixed) then
    return fixed
  end
  return s
end

local function CleanPlayerNameText(name)
  name = tostring(name or "")

  name = name:gsub("|c%x%x%x%x%x%x%x%x", "")
  name = name:gsub("|r", "")
  name = name:gsub("|H.-|h(.-)|h", "%1")
  name = name:gsub("[%z\001-\031\127]", "")
  name = name:gsub("\194\160", " ")

  for _ = 1, 2 do
    if name:find("%%[%x][%x]") then
      name = UrlDecode(name)
    end
  end

  name = RepairStrippedUrlUtf8(name)
  name = RepairLatin1Mojibake(name)
  name = Trim(name)

  local player, realm = name:match("^(.+)%-(.+)$")
  if player and realm then
    player = Trim(player)
    realm = Trim(realm):gsub("%s+", "")
    if player ~= "" and realm ~= "" then
      name = player .. "-" .. realm
    end
  end

  return name
end

local function FullPlayerName()
  local name, realm
  if UnitFullName then
    name, realm = UnitFullName("player")
  end
  name = name or UnitName("player") or "player"
  realm = realm or GetRealmName() or ""
  realm = tostring(realm or ""):gsub("%s+", "")
  if realm ~= "" and not tostring(name):find("-", 1, true) then
    return CleanPlayerNameText(tostring(name) .. "-" .. tostring(realm))
  end
  return CleanPlayerNameText(name)
end

local function Debug(msg)
  if WF and WF.Debug then
    WF:Debug("Share: " .. tostring(msg))
  end
end

local function Notify(msg)
  msg = tostring(msg or "")
  if msg == "" then return end
  if WF and WF.UI and WF.UI.Help and WF.UI.Help.Toast then
    WF.UI.Help:Toast(msg)
    return
  end
  if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
    DEFAULT_CHAT_FRAME:AddMessage("|cff66c2ffWhatFarm:|r " .. msg)
  end
end

local function EscapeBlob(s)
  s = tostring(s or "")
  s = s:gsub("~", "~~")
  s = s:gsub("\n", "~0A")
  s = s:gsub("([^%w%p ])", function(c)
    return string.format("~%02X", string.byte(c))
  end)
  return s
end

local function UnescapeBlob(s)
  s = tostring(s or "")
  local placeholder = "\001"
  s = s:gsub("~~", placeholder)
  s = s:gsub("~(%x%x)", function(h)
    return string.char(tonumber(h, 16) or 0)
  end)
  s = s:gsub(placeholder, "~")
  return s
end

local function EncodePayload(raw)
  raw = tostring(raw or "")
  if LibDeflate and LibDeflate.CompressDeflate and LibDeflate.EncodeForPrint then
    local comp = LibDeflate:CompressDeflate(raw)
    local enc = comp and LibDeflate:EncodeForPrint(comp) or nil
    if enc and enc ~= "" then
      return "WFRS2:" .. enc
    end
  end
  return "WFRS1:" .. EscapeBlob(raw)
end

local function DecodePayload(blob)
  blob = tostring(blob or "")
  if blob:sub(1, 6) == "WFRS2:" and LibDeflate and LibDeflate.DecodeForPrint and LibDeflate.DecompressDeflate then
    local bin = LibDeflate:DecodeForPrint(blob:sub(7))
    local raw = bin and LibDeflate:DecompressDeflate(bin) or nil
    if type(raw) == "string" then return raw end
  end
  if blob:sub(1, 6) == "WFRS1:" then
    return UnescapeBlob(blob:sub(7))
  end
  return nil
end

local function BuildSessionLabel(sess)
  if type(sess) ~= "table" then return "Run" end
  local name = ""
  if WF and WF.UI and WF.UI.Help and WF.UI.Help.GetSessionDisplayName then
    name = WF.UI.Help:GetSessionDisplayName(sess) or ""
  end
  name = Trim(name)
  if name == "" then name = Trim(sess._wfCustomFarmName or sess.farmName or sess.zone or "Run") end
  if name == "" then name = "Run" end
  if #name > 42 then
    name = Utf8TruncateChars(name, 39) .. "..."
  end
  return name
end

local function BuildChatLabel(label)
  label = Trim(label)
  label = label:gsub("[%[%]{}<>|]", "")
  label = label:gsub("%s+", " ")
  if label == "" then label = "Run" end
  if #label > 42 then
    label = Utf8TruncateChars(label, 39) .. "..."
  end
  return label
end

local function ToBase36(n)
  n = math.floor(tonumber(n) or 0)
  if n <= 0 then
    return "0"
  end

  local chars = {}
  while n > 0 do
    local rem = n % 36
    if rem < 10 then
      chars[#chars + 1] = string.char(48 + rem)
    else
      chars[#chars + 1] = string.char(55 + rem)
    end
    n = math.floor(n / 36)
  end

  local out = {}
  for i = #chars, 1, -1 do
    out[#out + 1] = chars[i]
  end
  return table.concat(out)
end

local function FromBase36(s)
  s = tostring(s or ""):upper()
  if s == "" then
    return 0
  end

  local total = 0
  for i = 1, #s do
    local b = s:byte(i)
    local v = nil
    if b >= 48 and b <= 57 then
      v = b - 48
    elseif b >= 65 and b <= 90 then
      v = b - 55
    else
      return 0
    end
    total = (total * 36) + v
  end

  return total
end

local function HexEncode(s)
  s = tostring(s or "")
  return (s:gsub(".", function(c)
    return string.format("%02X", string.byte(c))
  end))
end

local function HexDecode(s)
  s = tostring(s or "")
  if s == "" or (#s % 2) ~= 0 or s:find("[^%x]", 1) then
    return nil
  end

  return (s:gsub("(%x%x)", function(h)
    return string.char(tonumber(h, 16) or 0)
  end))
end


local function SafeTextEncode(s)
  s = tostring(s or "")
  if s == "" then return "" end

  if s:find("[^%w%-%._~ ]") then
    return "~H" .. HexEncode(s)
  end

  return UrlEncode(s)
end

local function SafeTextDecode(s)
  s = tostring(s or "")
  if s == "" then return "" end

  if s:sub(1, 2) == "~H" then
    local decoded = HexDecode(s:sub(3))
    if type(decoded) == "string" then
      return decoded
    end
  end

  return UrlDecode(s)
end

local function ClampSafeEncodedText(s, maxEncodedLen)
  s = tostring(s or "")
  maxEncodedLen = math.max(0, tonumber(maxEncodedLen) or 0)
  if s == "" or maxEncodedLen <= 0 then
    return ""
  end

  if string.len(SafeTextEncode(s)) <= maxEncodedLen then
    return s
  end

  local out = {}
  local i = 1
  while i <= #s do
    local len = Utf8CharLenAt(s, i)
    if len <= 0 or (i + len - 1) > #s then
      break
    end

    local ch = s:sub(i, i + len - 1)
    out[#out + 1] = ch
    local candidate = table.concat(out)
    if string.len(SafeTextEncode(candidate)) > maxEncodedLen then
      out[#out] = nil
      break
    end

    i = i + len
  end

  return table.concat(out)
end

local function BuildDisplayHyperlink(routeType, routeValue, token, label)
  routeType = tostring(routeType or "W")
  local encodedRouteValue = ""

  if routeType == "B" then
    encodedRouteValue = tostring(tonumber(routeValue) or 0)
  else
    routeType = "W"
    encodedRouteValue = HexEncode(CleanPlayerNameText(routeValue))
  end

  local display = "[WF Run: " .. BuildChatLabel(label) .. "]"
  return "|cff66c2ff|Hwhatfarmrun:2:" .. routeType .. ":" .. encodedRouteValue .. ":" .. token .. "|h" .. display .. "|h|r"
end

local function BuildStandaloneDisplayHyperlink(sender, payloadBlob, label)
  sender = CleanPlayerNameText(sender)
  local display = "[WF Run: " .. BuildChatLabel(label) .. "]"
  return "|cff66c2ff|Hwhatfarmrun:3:" .. HexEncode(sender) .. ":" .. tostring(payloadBlob or "") .. "|h" .. display .. "|h|r"
end

local function BuildCompactStandaloneDisplayHyperlink(sender, compactBlob, label)
  sender = CleanPlayerNameText(sender)
  local display = "[WF Run: " .. BuildChatLabel(label) .. "]"
  return "|cff66c2ff|Hwhatfarmrun:4:" .. HexEncode(sender) .. ":" .. tostring(compactBlob or "") .. "|h" .. display .. "|h|r"
end

local function BuildQuickDisplayHyperlink(quickBlob, label)
  local display = "[WF Run: " .. BuildChatLabel(label) .. "]"
  return "|cff66c2ff|Hwhatfarmrun:5:" .. tostring(quickBlob or "") .. "|h" .. display .. "|h|r"
end

local function BuildFullExportDisplayHyperlink(fullBlob, label)
  local display = "[WF Run: " .. BuildChatLabel(label) .. "]"
  return "|cff66c2ff|Hwhatfarmrun:6:" .. tostring(fullBlob or "") .. "|h" .. display .. "|h|r"
end

local function SnapshotVisibility(sess)
  local out = { hide = {}, exclude = {} }
  local vis = type(sess) == "table" and type(sess._wfSessionLootVisibility) == "table" and sess._wfSessionLootVisibility or nil
  if not vis then return out end

  local hideTop = type(vis.hideTop) == "table" and vis.hideTop or nil
  if hideTop then
    for k, v in pairs(hideTop) do
      if v then out.hide[#out.hide + 1] = tonumber(k) or tonumber(v) end
    end
  end

  local ex = type(vis.excludeValue) == "table" and vis.excludeValue or nil
  if ex then
    for k, v in pairs(ex) do
      if v then out.exclude[#out.exclude + 1] = tonumber(k) or tonumber(v) end
    end
  end

  table.sort(out.hide)
  table.sort(out.exclude)
  return out
end

local function BuildRawSession(sess)
  if type(sess) ~= "table" then return nil end

  local lines = { "WFSESSION1" }
  local vis = SnapshotVisibility(sess)
  local header = {
    "H",
    "farm=" .. UrlEncode(sess._wfCustomFarmName or sess.farmName or ""),
    "zone=" .. UrlEncode(sess.zone or ""),
    "mode=" .. UrlEncode(sess.mode or ""),
    "note=" .. UrlEncode(sess.note or ""),
    "st=" .. tostring(tonumber(sess.startedAt) or 0),
    "et=" .. tostring(tonumber(sess.endedAt or sess.stoppedAt) or 0),
    "el=" .. tostring(tonumber(sess.elapsed or sess.duration) or 0),
    "tv=" .. tostring(tonumber(sess.totalValue or sess.total or sess.value) or 0),
    "mg=" .. tostring(tonumber(sess.moneyGained or sess.lootedGoldValue or sess.lootedGold or sess.goldLooted or sess.rawGold or sess.money or sess.gold) or 0),
    "mc=" .. tostring(tonumber(sess._wfMergedCount) or 1),
    "map=" .. tostring(tonumber(sess.mapId) or 0),
    "x=" .. tostring(tonumber(sess.x) or 0),
    "y=" .. tostring(tonumber(sess.y) or 0),
    "wmap=" .. tostring(tonumber(sess.worldMapId) or 0),
    "wx=" .. tostring(tonumber(sess.worldX) or 0),
    "wy=" .. tostring(tonumber(sess.worldY) or 0),
    "hide=" .. UrlEncode(table.concat(vis.hide, ",")),
    "exclude=" .. UrlEncode(table.concat(vis.exclude, ",")),
  }
  lines[#lines + 1] = table.concat(header, "|")

  local items = {}
  if type(sess.items) == "table" then
    for itemID, e in pairs(sess.items) do
      local iid = tonumber(itemID) or tonumber(e and e.itemID) or 0
      if iid > 0 then
        items[#items + 1] = {
          itemID = iid,
          count = tonumber(e and e.count) or 0,
          value = tonumber(e and e.value) or 0,
          link = tostring(e and e.link or ""),
        }
      end
    end
  end

  table.sort(items, function(a, b)
    if a.value ~= b.value then return a.value > b.value end
    return a.itemID < b.itemID
  end)

  for i = 1, #items do
    local it = items[i]
    lines[#lines + 1] = table.concat({
      "I",
      tostring(it.itemID),
      tostring(it.count),
      tostring(it.value),
      UrlEncode(it.link),
    }, "|")
  end

  return table.concat(lines, "\n")
end

local HydrateItem
local ParseRawSession
local CountItems
local CountValuableItems
local _ShareThemeColors
local _ShareApplyBackdrop
local _ShareCreateLabel
local _ShareCreatePanel
local _ShareCreateSection
local _ShareCreateMetricTile
local _ShareLayoutMetrics


local function GetSessionShareSource(sess)
  if type(sess) ~= "table" then
    return FullPlayerName()
  end

  local source = Trim(
    sess._wfShareOriginalSender
    or sess._wfOriginalSharer
    or sess._wfSharedFrom
    or sess.sharedBy
    or sess.sender
    or ""
  )

  source = CleanPlayerNameText(source)

  if source == "" then
    source = FullPlayerName()
  end

  return CleanPlayerNameText(source)
end

local function ResolveItemQuality(entry, itemID, link)
  itemID = tonumber(itemID) or tonumber(entry and entry.itemID) or 0
  link = tostring(link or (entry and entry.link) or "")

  if link ~= "" and GetItemInfo then
    local _, _, linkQuality = GetItemInfo(link)
    if type(linkQuality) == "number" then
      return linkQuality
    end
  end

  local storedQuality = tonumber(entry and (entry.quality or entry.itemQuality or entry.itemRarity or entry.rarity or entry.qualityID))
  if storedQuality then
    return storedQuality
  end

  if itemID > 0 and C_Item and C_Item.GetItemQualityByID then
    local idQuality = C_Item.GetItemQualityByID(itemID)
    if type(idQuality) == "number" then
      return idQuality
    end
  end

  if itemID > 0 and GetItemInfo then
    local _, _, idQuality = GetItemInfo(itemID)
    if type(idQuality) == "number" then
      return idQuality
    end
  end

  return nil
end

local function BuildQuickSessionBlob(sess)
  if type(sess) ~= "table" then return nil end

  local source = GetSessionShareSource(sess)
  local farm = Trim(sess._wfCustomFarmName or sess.farmName or "")
  local zone = Trim(sess.zone or "")
  local mode = Trim(sess.mode or "")
  local startedAt = tonumber(sess.startedAt) or tonumber(sess.endedAt) or 0
  local elapsed = tonumber(sess.elapsed or sess.duration) or 0
  local totalValue = tonumber(sess.totalValue or sess.total or sess.value) or 0
  local moneyGained = tonumber(sess.moneyGained or sess.lootedGoldValue or sess.lootedGold or sess.goldLooted or sess.rawGold or sess.money or sess.gold) or 0
  local uniqueItems, totalItems = CountItems(sess)
  local valuableUnique, valuableTotal = CountValuableItems(sess)

  if elapsed <= 0 then
    local endedAt = tonumber(sess.endedAt or sess.stoppedAt) or startedAt
    elapsed = math.max(0, endedAt - startedAt)
  end

  if zone ~= "" and zone == farm then
    zone = ""
  end

  local items = {}
  if type(sess.items) == "table" then
    for itemID, e in pairs(sess.items) do
      local iid = tonumber(itemID) or tonumber(e and e.itemID) or 0
      local count = tonumber(e and e.count) or 0
      local value = tonumber(e and e.value) or 0
      if iid > 0 and (count > 0 or value > 0) then
        items[#items + 1] = {
          itemID = iid,
          count = math.max(0, count),
          value = math.max(0, value),
          quality = ResolveItemQuality(e, iid, e and e.link or nil),
        }
      end
    end
  end

  table.sort(items, function(a, b)
    if a.value ~= b.value then return a.value > b.value end
    if a.count ~= b.count then return a.count > b.count end
    return a.itemID < b.itemID
  end)

  local function BuildWithConfig(cfg)
    local itemParts = {}
    local limit = math.max(0, tonumber(cfg.topN) or 0)
    for i = 1, math.min(limit, #items) do
      local it = items[i]
      local parts = {
        ToBase36(it.itemID),
        ToBase36(it.count),
        ToBase36(it.value),
      }
      if type(it.quality) == "number" then
        parts[#parts + 1] = ToBase36(it.quality)
      end
      itemParts[#itemParts + 1] = table.concat(parts, ",")
    end

    local blob = table.concat({
      "WFQS1",
      SafeTextEncode(ClampSafeEncodedText(source, cfg.sourceChars or 16)),
      SafeTextEncode(ClampSafeEncodedText(farm, cfg.farmChars or 20)),
      SafeTextEncode(ClampSafeEncodedText(zone, cfg.zoneChars or 12)),
      SafeTextEncode(ClampSafeEncodedText(mode, cfg.modeChars or 10)),
      ToBase36(startedAt),
      ToBase36(elapsed),
      ToBase36(totalValue),
      ToBase36(moneyGained),
      ToBase36(totalItems),
      ToBase36(uniqueItems),
      ToBase36(valuableTotal),
      ToBase36(valuableUnique),
      table.concat(itemParts, ";"),
    }, "$")

    return blob
  end

  local configs = {
    { topN = 8, sourceChars = 26, farmChars = 30, zoneChars = 20, modeChars = 14 },
    { topN = 6, sourceChars = 24, farmChars = 28, zoneChars = 18, modeChars = 14 },
    { topN = 5, sourceChars = 22, farmChars = 26, zoneChars = 16, modeChars = 12 },
    { topN = 4, sourceChars = 20, farmChars = 24, zoneChars = 14, modeChars = 12 },
    { topN = 3, sourceChars = 18, farmChars = 22, zoneChars = 12, modeChars = 10 },
    { topN = 2, sourceChars = 16, farmChars = 20, zoneChars = 10, modeChars = 8  },
    { topN = 1, sourceChars = 14, farmChars = 18, zoneChars = 0,  modeChars = 8  },
    { topN = 0, sourceChars = 12, farmChars = 14, zoneChars = 0,  modeChars = 0  },
  }

  for i = 1, #configs do
    local blob = BuildWithConfig(configs[i])
    if blob and (string.len("<wfq:" .. blob .. ">") <= 235) then
      return blob
    end
  end

  local finalBlob = BuildWithConfig({ topN = 0, sourceChars = 10, farmChars = 12, zoneChars = 0, modeChars = 0 })
  if finalBlob and (string.len("<wfq:" .. finalBlob .. ">") <= 235) then
    return finalBlob
  end

  return table.concat({
    "WFQS1",
    "",
    SafeTextEncode(ClampSafeEncodedText(farm ~= "" and farm or zone, 10)),
    "",
    "",
    ToBase36(startedAt),
    ToBase36(elapsed),
    ToBase36(totalValue),
    ToBase36(moneyGained),
    ToBase36(totalItems),
    ToBase36(uniqueItems),
    ToBase36(valuableTotal),
    ToBase36(valuableUnique),
    "",
  }, "$")
end

local function ParseQuickSessionBlob(blob)
  blob = tostring(blob or "")
  local sourceEnc, farmEnc, zoneEnc, modeEnc, stS, elS, tvS, goldS, totalItemsS, uniqueItemsS, valuableTotalS, valuableUniqueS, itemsBlob =
    blob:match("^WFQS1%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$?(.*)$")

  if not sourceEnc then
    return nil, "Unsupported quick share payload"
  end

  local sess = {
    farmName = SafeTextDecode(farmEnc or ""),
    zone = SafeTextDecode(zoneEnc or ""),
    mode = SafeTextDecode(modeEnc or ""),
    startedAt = FromBase36(stS),
    elapsed = FromBase36(elS),
    totalValue = FromBase36(tvS),
    moneyGained = FromBase36(goldS),
    note = "",
    items = {},
    _wfSessionLootVisibility = { hideTop = {}, excludeValue = {} },
    _wfImportedShared = true,
    _wfQuickSummary = true,
    _wfOriginalSharer = CleanPlayerNameText(SafeTextDecode(sourceEnc or "")),
    _wfShareTotalItems = FromBase36(totalItemsS),
    _wfShareUniqueItems = FromBase36(uniqueItemsS),
    _wfShareValuableTotal = FromBase36(valuableTotalS),
    _wfShareValuableUnique = FromBase36(valuableUniqueS),
  }

  if sess.farmName ~= "" then
    sess._wfCustomFarmName = sess.farmName
  end
  if (not sess.zone or sess.zone == "") and sess.farmName ~= "" then
    sess.zone = sess.farmName
  end

  if type(itemsBlob) == "string" and itemsBlob ~= "" then
    for spec in itemsBlob:gmatch("[^;]+") do
      local idS, countS, valueS, qualityS = spec:match("^([^,]+),([^,]+),([^,]+),?([^,]*)$")
      local itemID = FromBase36(idS)
      if itemID > 0 then
        local entry = {
          itemID = itemID,
          count = FromBase36(countS),
          value = FromBase36(valueS),
          link = "",
        }
        if type(qualityS) == "string" and qualityS ~= "" then
          entry.quality = FromBase36(qualityS)
        end
        HydrateItem(entry, itemID)
        sess.items[itemID] = entry
      end
    end
  end

  if not sess.startedAt or sess.startedAt <= 0 then
    sess.startedAt = tonumber(time()) or 0
  end
  if not sess.elapsed or sess.elapsed < 0 then
    sess.elapsed = 0
  end
  sess.endedAt = sess.startedAt + sess.elapsed
  return sess
end

local function BuildFullExportString(sess)
  if type(sess) ~= "table" then return nil end
  local raw = BuildRawSession(sess)
  if type(raw) ~= "string" or raw == "" then
    return nil
  end

  local payload = EncodePayload(raw)
  if type(payload) ~= "string" or payload == "" then
    return nil
  end

  return "WFX3$" .. SafeTextEncode(GetSessionShareSource(sess)) .. "$" .. payload
end

local function BuildChatSafeFullExportBlob(sess)
  local export = BuildFullExportString(sess)
  if type(export) ~= "string" or export == "" then
    return nil
  end

  local blob = UrlEncode(export)
  if type(blob) ~= "string" or blob == "" then
    return nil
  end

  return blob, export
end

local function ParseFullExportString(raw)
  raw = Trim(raw or "")
  local sourceEnc, payload = raw:match("WFX3%$([^%$]*)%$(WFRS[12]:.+)")
  if not sourceEnc or not payload then
    return nil, nil, "Invalid export string"
  end

  local decoded = DecodePayload(payload)
  if not decoded then
    return nil, nil, "That export string couldn't be decoded."
  end

  local sess, err = ParseRawSession(decoded)
  if not sess then
    return nil, nil, err or "Invalid export data"
  end

  return sess, CleanPlayerNameText(SafeTextDecode(sourceEnc or ""))
end

local function BuildCompactSessionBlob(sess)
  if type(sess) ~= "table" then return nil end

  local source = GetSessionShareSource(sess)
  local farm = Trim(sess._wfCustomFarmName or sess.farmName or "")
  local zone = Trim(sess.zone or "")
  local mode = Trim(sess.mode or "")
  local startedAt = tonumber(sess.startedAt) or tonumber(sess.endedAt) or 0
  local elapsed = tonumber(sess.elapsed or sess.duration) or 0
  local totalValue = tonumber(sess.totalValue or sess.total or sess.value) or 0
  local moneyGained = tonumber(sess.moneyGained or sess.lootedGoldValue or sess.lootedGold or sess.goldLooted or sess.rawGold or sess.money or sess.gold) or 0

  if elapsed <= 0 then
    local endedAt = tonumber(sess.endedAt or sess.stoppedAt) or startedAt
    elapsed = math.max(0, endedAt - startedAt)
  end

  if zone ~= "" and zone == farm then
    zone = ""
  end

  local items = {}
  if type(sess.items) == "table" then
    for itemID, e in pairs(sess.items) do
      local iid = tonumber(itemID) or tonumber(e and e.itemID) or 0
      local count = tonumber(e and e.count) or 0
      local value = tonumber(e and e.value) or 0
      if iid > 0 and (count > 0 or value > 0) then
        items[#items + 1] = {
          itemID = iid,
          count = math.max(0, count),
          value = math.max(0, value),
          quality = ResolveItemQuality(e, iid, e and e.link or nil),
        }
      end
    end
  end

  table.sort(items, function(a, b)
    if a.value ~= b.value then return a.value > b.value end
    if a.count ~= b.count then return a.count > b.count end
    return a.itemID < b.itemID
  end)

  local itemParts = {}
  for i = 1, #items do
    local it = items[i]
    local parts = {
      ToBase36(it.itemID),
      ToBase36(it.count),
      ToBase36(it.value),
    }
    if type(it.quality) == "number" then
      parts[#parts + 1] = ToBase36(it.quality)
    end
    itemParts[#itemParts + 1] = table.concat(parts, ",")
  end

  return table.concat({
    "WFSC2",
    SafeTextEncode(source),
    SafeTextEncode(farm),
    SafeTextEncode(zone),
    SafeTextEncode(mode),
    ToBase36(startedAt),
    ToBase36(elapsed),
    ToBase36(totalValue),
    ToBase36(moneyGained),
    table.concat(itemParts, ";"),
  }, "$")
end

local function ParseCompactSessionBlob(blob)
  blob = tostring(blob or "")

  local sourceEnc, farmEnc, zoneEnc, modeEnc, stS, elS, tvS, goldS, itemsBlob = blob:match("^WFSC2%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$?(.*)$")
  if not sourceEnc then
    sourceEnc, farmEnc, zoneEnc, modeEnc, stS, elS, tvS, goldS, itemsBlob = blob:match("^WFSC2|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|?(.*)$")
  end

  if not sourceEnc then
    farmEnc, zoneEnc, modeEnc, stS, elS, tvS, goldS, itemsBlob = blob:match("^WFSC1%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$([^%$]*)%$?(.*)$")
    if not farmEnc then
      farmEnc, zoneEnc, modeEnc, stS, elS, tvS, goldS, itemsBlob = blob:match("^WFSC1|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|?(.*)$")
    end
    sourceEnc = ""
  end

  if not farmEnc then
    return nil, "Unsupported compact share payload"
  end

  local sess = {
    farmName = SafeTextDecode(farmEnc or ""),
    zone = SafeTextDecode(zoneEnc or ""),
    mode = SafeTextDecode(modeEnc or ""),
    startedAt = FromBase36(stS),
    elapsed = FromBase36(elS),
    totalValue = FromBase36(tvS),
    moneyGained = FromBase36(goldS),
    note = "",
    items = {},
    _wfSessionLootVisibility = { hideTop = {}, excludeValue = {} },
    _wfImportedShared = true,
    _wfCompactShared = true,
    _wfOriginalSharer = CleanPlayerNameText(SafeTextDecode(sourceEnc or "")),
  }

  if sess.farmName ~= "" then
    sess._wfCustomFarmName = sess.farmName
  end
  if (not sess.zone or sess.zone == "") and sess.farmName ~= "" then
    sess.zone = sess.farmName
  end

  if type(itemsBlob) == "string" and itemsBlob ~= "" then
    for spec in itemsBlob:gmatch("[^;]+") do
      local idS, countS, valueS, qualityS = spec:match("^([^,]+),([^,]+),([^,]+),?([^,]*)$")
      local itemID = FromBase36(idS)
      if itemID > 0 then
        local entry = {
          itemID = itemID,
          count = FromBase36(countS),
          value = FromBase36(valueS),
          link = "",
        }
        if type(qualityS) == "string" and qualityS ~= "" then
          entry.quality = FromBase36(qualityS)
        end
        HydrateItem(entry, itemID)
        sess.items[itemID] = entry
      end
    end
  end

  if not sess.startedAt or sess.startedAt <= 0 then
    sess.startedAt = tonumber(time()) or 0
  end
  if not sess.elapsed or sess.elapsed < 0 then
    sess.elapsed = 0
  end
  sess.endedAt = sess.startedAt + sess.elapsed
  return sess
end

local function ParseCSVNumbers(s)
  local out = {}
  s = Trim(s or "")
  if s == "" then return out end
  for n in string.gmatch(s, "[^,]+") do
    n = tonumber(n)
    if n and n > 0 then
      out[n] = true
    end
  end
  return out
end

HydrateItem = function(entry, itemID)
  if type(entry) ~= "table" then return end
  if not entry.icon and GetItemIcon then
    entry.icon = GetItemIcon(itemID)
  end
  if (not entry.name or entry.name == "") and type(entry.link) == "string" and entry.link ~= "" then
    local nm = entry.link:match("%[(.-)%]")
    if nm and nm ~= "" then entry.name = nm end
  end
  if ((not entry.name or entry.name == "") or not entry.icon or entry.quality == nil) and GetItemInfo then
    local query = (type(entry.link) == "string" and entry.link ~= "" and entry.link) or itemID
    local name, link, quality, _, _, _, _, _, _, icon = GetItemInfo(query)
    if not entry.name or entry.name == "" then entry.name = name or entry.name end
    if not entry.link or entry.link == "" then entry.link = link or entry.link end
    if not entry.icon then entry.icon = icon end
    if entry.quality == nil and type(quality) == "number" then entry.quality = quality end
  end
end

ParseRawSession = function(raw)
  raw = tostring(raw or "")
  if raw == "" then return nil, "Empty payload" end

  local lines = {}
  for line in string.gmatch(raw, "[^\n]+") do
    lines[#lines + 1] = line
  end
  if lines[1] ~= "WFSESSION1" then
    return nil, "Unsupported session payload"
  end

  local sess = {
    items = {},
    _wfSessionLootVisibility = { hideTop = {}, excludeValue = {} },
  }

  for i = 2, #lines do
    local line = lines[i]
    if line:sub(1, 2) == "H|" then
      for part in string.gmatch(line, "([^|]+)") do
        if part ~= "H" then
          local k, v = part:match("^([^=]+)=(.*)$")
          if k then
            if k == "farm" then sess.farmName = UrlDecode(v)
            elseif k == "zone" then sess.zone = UrlDecode(v)
            elseif k == "mode" then sess.mode = UrlDecode(v)
            elseif k == "note" then sess.note = UrlDecode(v)
            elseif k == "st" then sess.startedAt = tonumber(v) or 0
            elseif k == "et" then sess.endedAt = tonumber(v) or 0
            elseif k == "el" then sess.elapsed = tonumber(v) or 0
            elseif k == "tv" then sess.totalValue = tonumber(v) or 0
            elseif k == "mg" then sess.moneyGained = tonumber(v) or 0
            elseif k == "mc" then sess._wfMergedCount = tonumber(v) or 1
            elseif k == "map" then local n = tonumber(v) or 0 if n > 0 then sess.mapId = n end
            elseif k == "x" then local n = tonumber(v) or 0 if n > 0 then sess.x = n end
            elseif k == "y" then local n = tonumber(v) or 0 if n > 0 then sess.y = n end
            elseif k == "wmap" then local n = tonumber(v) or 0 if n > 0 then sess.worldMapId = n end
            elseif k == "wx" then local n = tonumber(v) or 0 if n > 0 then sess.worldX = n end
            elseif k == "wy" then local n = tonumber(v) or 0 if n > 0 then sess.worldY = n end
            elseif k == "hide" then sess._wfSessionLootVisibility.hideTop = ParseCSVNumbers(UrlDecode(v))
            elseif k == "exclude" then sess._wfSessionLootVisibility.excludeValue = ParseCSVNumbers(UrlDecode(v)) end
          end
        end
      end
    elseif line:sub(1, 2) == "I|" then
      local idS, countS, valueS, linkEnc = line:match("^I|([^|]*)|([^|]*)|([^|]*)|?(.*)$")
      local itemID = tonumber(idS) or 0
      if itemID > 0 then
        local entry = {
          itemID = itemID,
          count = tonumber(countS) or 0,
          value = tonumber(valueS) or 0,
          link = UrlDecode(linkEnc or ""),
        }
        HydrateItem(entry, itemID)
        sess.items[itemID] = entry
      end
    end
  end

  if not sess.endedAt or sess.endedAt <= 0 then
    sess.endedAt = sess.startedAt or time()
  end
  if not sess.elapsed or sess.elapsed <= 0 then
    local st = tonumber(sess.startedAt) or 0
    local et = tonumber(sess.endedAt) or st
    sess.elapsed = math.max(0, et - st)
  end
  sess._wfImportedShared = true
  return sess
end

local function CleanupCache(self)
  local now = time()
  local keep = {}
  for token, rec in pairs(self._cache) do
    local age = now - (tonumber(rec and rec.createdAt) or now)
    if age <= CACHE_TTL then
      keep[#keep + 1] = { token = token, t = tonumber(rec.createdAt) or 0 }
    else
      self._cache[token] = nil
    end
  end
  table.sort(keep, function(a, b) return a.t > b.t end)
  for i = MAX_CACHE_ENTRIES + 1, #keep do
    self._cache[keep[i].token] = nil
  end
end

local function CleanupPending(self)
  local now = time()
  for token, rec in pairs(self._pending) do
    local age = now - (tonumber(rec and rec.startedAt) or now)
    if age > PENDING_TTL then
      self._pending[token] = nil
    end
  end

  for token, rec in pairs(self._routeHints or {}) do
    local age = now - (tonumber(rec and rec.updatedAt) or now)
    if age > CACHE_TTL then
      self._routeHints[token] = nil
    end
  end
end

local function PendingSenderMatches(rec, sender, senderTarget, channel)
  if type(rec) ~= "table" then return false end

  local route = tostring(channel or rec.route or "WHISPER")
  if route == "BN_WHISPER" then
    local expected = tonumber(rec.target) or 0
    local got = tonumber(senderTarget or sender) or 0
    return expected > 0 and got > 0 and expected == got
  end

  if NormalizeName(rec.sender or "") == "" then return true end
  return NormalizeName(rec.sender) == NormalizeName(sender)
end

local function SendAddon(prefix, payload, channel, target)
  channel = tostring(channel or "WHISPER")

  if channel == "BN_WHISPER" then
    local presenceID = tonumber(target) or 0
    if presenceID <= 0 or type(BNSendGameData) ~= "function" then
      return false
    end
    BNSendGameData(presenceID, prefix, payload)
    return true
  end

  if C_ChatInfo and C_ChatInfo.SendAddonMessage then
    C_ChatInfo.SendAddonMessage(prefix, payload, channel, target)
    return true
  end
  return false
end

local function InsertIntoChat(linkText)
  if type(linkText) ~= "string" or linkText == "" then return false end
  if ChatEdit_InsertLink and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
    ChatEdit_InsertLink(linkText)
    return true
  end
  if ChatFrame_OpenChat then
    ChatFrame_OpenChat(linkText)
    return true
  end
  if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
    DEFAULT_CHAT_FRAME:AddMessage(linkText)
    return true
  end
  return false
end

local function GenerateToken(self)
  self._tokenCounter = (tonumber(self._tokenCounter) or 0) + 1

  local now = tonumber(time()) or 0
  local precise = 0
  if type(GetTimePreciseSec) == "function" then
    precise = math.floor((GetTimePreciseSec() % 1) * 1000000)
  elseif type(GetTime) == "function" then
    precise = math.floor((GetTime() % 1) * 1000)
  end

  local playerSalt = HexEncode(FullPlayerName() or "")
  if playerSalt == "" then
    playerSalt = "WF"
  end

  return string.format("%x%04x%s%06x", now, self._tokenCounter % 65536, playerSalt:sub(1, 8), precise % 0xFFFFFF)
end

function Share:CreateRunLink(sess)
  if type(sess) ~= "table" then return nil end

  CleanupCache(self)
  CleanupPending(self)

  local compactBlob = BuildCompactSessionBlob(sess)
  if compactBlob and compactBlob ~= "" then
    local compactToken = "<wfc:" .. compactBlob .. ">"
    if string.len(compactToken) <= 255 then
      return compactToken
    end
  end

  local fullBlob = BuildChatSafeFullExportBlob(sess)
  if fullBlob and fullBlob ~= "" then
    local fullToken = "<wfx:" .. fullBlob .. ">"
    if string.len(fullToken) <= 255 then
      return fullToken
    end
  end

  local quickBlob = BuildQuickSessionBlob(sess)
  if not quickBlob or quickBlob == "" then
    return nil
  end

  return "<wfq:" .. quickBlob .. ">"
end

function Share:CreateFullRunExport(sess)
  if type(sess) ~= "table" then return nil end
  return BuildFullExportString(sess)
end

function Share:InsertRunLink(sess)
  local link = self:CreateRunLink(sess)
  if not link then
    Notify("Couldn't build a share link for that run.")
    return false
  end
  if InsertIntoChat(link) then
    return true
  end
  Notify("Run link created, but chat couldn't be opened automatically.")
  return false
end

local function ParseAnySharedString(raw)
  raw = Trim(raw or "")
  if raw == "" then
    return nil, nil, "Paste a WhatFarm share string first."
  end

  local hyperFull = raw:match("|Hwhatfarmrun:6:([^|]+)|h")
  if hyperFull then
    local export = UrlDecode(hyperFull)
    local sess, sender, err = ParseFullExportString(export)
    if not sess then return nil, nil, err end
    sender = Trim(sender or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local tokenFull = raw:match("<wfx:([^>]+)>")
  if tokenFull then
    local export = UrlDecode(tokenFull)
    local sess, sender, err = ParseFullExportString(export)
    if not sess then return nil, nil, err end
    sender = Trim(sender or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local hyperQuick = raw:match("|Hwhatfarmrun:5:([^|]+)|h")
  if hyperQuick then
    local sess, err = ParseQuickSessionBlob(hyperQuick)
    if not sess then return nil, nil, err end
    local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local tokenQuick = raw:match("<wfq:([^>]+)>")
  if tokenQuick then
    local sess, err = ParseQuickSessionBlob(tokenQuick)
    if not sess then return nil, nil, err end
    local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local tokenCompact = raw:match("<wfc:([^>]+)>")
  if tokenCompact then
    local sess, err = ParseCompactSessionBlob(tokenCompact)
    if not sess then return nil, nil, err end
    local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local fullSess, fullSender, fullErr = ParseFullExportString(raw)
  if fullSess then
    return fullSess, (CleanPlayerNameText(fullSender) ~= "" and CleanPlayerNameText(fullSender) or "Shared Export")
  end

  local compactBlob = raw:match("|Hwhatfarmrun:4:[^:]*:(.+)|h") or raw:match("<wf:4:[^:]*:([^>]+)>")
  if compactBlob then
    local sess, err = ParseCompactSessionBlob(compactBlob)
    if not sess then return nil, nil, err end
    local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
    if sender == "" then sender = "Shared Export" end
    return sess, sender
  end

  local senderEnc, payload = raw:match("WFX3%$([^%$]*)%$(WFRS[12]:.+)")
  if senderEnc and payload then
    local decoded = DecodePayload(payload)
    if decoded then
      local sess, err = ParseRawSession(decoded)
      if sess then
        return sess, (CleanPlayerNameText(SafeTextDecode(senderEnc or "")) ~= "" and CleanPlayerNameText(SafeTextDecode(senderEnc or "")) or "Shared Export")
      end
      return nil, nil, err
    end
  end

  if raw:find("WFSC[12]", 1, false) then
    local sess, err = ParseCompactSessionBlob(raw:match("(WFSC[12].+)$") or raw)
    if sess then
      local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
      if sender == "" then sender = "Shared Export" end
      return sess, sender
    end
    return nil, nil, err
  end

  return nil, nil, fullErr or "That doesn't look like a valid WhatFarm share string."
end

local function EnsureShareTextModal(self)
  if self._textModal and self._textModal.frame then
    return self._textModal
  end

  local theme = _ShareThemeColors()
  local widgets = WF and WF.UI and WF.UI.Widgets or nil
  local modal = {}
  self._textModal = modal

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  modal.frame = blocker
  blocker:SetAllPoints(UIParent)
  blocker:SetFrameStrata("DIALOG")
  blocker:EnableMouse(true)
  blocker:EnableKeyboard(true)
  if blocker.SetPropagateKeyboardInput then
    blocker:SetPropagateKeyboardInput(false)
  end
  _ShareApplyBackdrop(blocker, { 0, 0, 0, 0.55 }, { 0, 0, 0, 0 })
  blocker:Hide()
  blocker:SetScript("OnKeyDown", function(f, key)
    if key == "ESCAPE" then
      f:Hide()
    end
  end)

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  modal.box = box
  box:SetSize(760, 460)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 20)
  _ShareApplyBackdrop(box, theme.bg, theme.border)
  box:EnableMouse(true)

  local header = _ShareCreatePanel(box, theme, theme.panel, theme.border)
  modal.header = header
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(36)

  local title = _ShareCreateLabel(header, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  modal.title = title
  title:SetPoint("LEFT", 14, 0)
  title:SetText("Shared Run")

  local close = CreateFrame("Button", nil, header, "UIPanelCloseButton")
  modal.close = close
  close:SetPoint("RIGHT", -4, 0)
  if widgets and widgets.SkinCloseButton then
    widgets:SkinCloseButton(close)
  end
  close:SetScript("OnClick", function() blocker:Hide() end)

  local body = _ShareCreateLabel(box, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.body = body
  body:SetPoint("TOPLEFT", 16, -48)
  body:SetPoint("TOPRIGHT", -16, -48)
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")

  local panel = CreateFrame("Frame", nil, box, "BackdropTemplate")
  modal.panel = panel
  panel:SetPoint("TOPLEFT", 16, -92)
  panel:SetPoint("BOTTOMRIGHT", -16, 52)
  _ShareApplyBackdrop(panel, theme.panel, theme.border)

  local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
  modal.scroll = scroll
  scroll:SetPoint("TOPLEFT", 8, -8)
  scroll:SetPoint("BOTTOMRIGHT", -24, 8)

  if scroll.ScrollBar then
    scroll.ScrollBar:ClearAllPoints()
    scroll.ScrollBar:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -8, -8)
    scroll.ScrollBar:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -8, 8)
  end

  if widgets and widgets.SkinScrollBar and scroll.ScrollBar then
    widgets:SkinScrollBar(scroll.ScrollBar)
  end

  local edit = CreateFrame("EditBox", nil, scroll)
  modal.edit = edit
  edit:SetMultiLine(true)
  edit:SetAutoFocus(false)
  edit:SetFontObject(_G.WF_GameFontHighlight or _G.ChatFontNormal)
  if edit.SetTextColor then
    edit:SetTextColor((theme.text and theme.text[1]) or 1, (theme.text and theme.text[2]) or 1, (theme.text and theme.text[3]) or 1, 1)
  end
  if edit.SetJustifyH then edit:SetJustifyH("LEFT") end
  if edit.SetJustifyV then edit:SetJustifyV("TOP") end
  edit:SetTextInsets(0, 0, 0, 0)
  edit:SetWidth(680)

  local measure = panel:CreateFontString(nil, "ARTWORK")
  modal.measure = measure
  measure:Hide()
  measure:SetFontObject(_G.WF_GameFontHighlight or _G.ChatFontNormal)
  measure:SetJustifyH("LEFT")
  measure:SetJustifyV("TOP")
  if measure.SetWordWrap then measure:SetWordWrap(true) end
  if measure.SetNonSpaceWrap then measure:SetNonSpaceWrap(true) end
  measure:SetWidth(680)
  measure:SetText(" ")

  local function UpdateScrollBarVisibility(resetWhenHidden)
    local range = scroll and scroll.GetVerticalScrollRange and tonumber(scroll:GetVerticalScrollRange()) or 0
    local holderH = holder and holder.GetHeight and tonumber(holder:GetHeight()) or 0
    local editH = edit and edit.GetHeight and tonumber(edit:GetHeight()) or 0
    local showBar = (range > 0.5) and (editH > (holderH + 2))

    if scroll and scroll.ScrollBar then
      if showBar then
        scroll.ScrollBar:Show()
        if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Show then
          scroll.ScrollBar._wfBG:Show()
        end
      else
        if resetWhenHidden and scroll.SetVerticalScroll then
          pcall(scroll.SetVerticalScroll, scroll, 0)
        end
        if resetWhenHidden and scroll.ScrollBar.SetValue then
          pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
        end
        scroll.ScrollBar:Hide()
        if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Hide then
          scroll.ScrollBar._wfBG:Hide()
        end
      end
    end
  end

  local function ResetScrollPosition(cursorToStart)
    local function Apply()
      if edit and edit.SetCursorPosition and cursorToStart then
        pcall(edit.SetCursorPosition, edit, 0)
      end
      if scroll and scroll.SetVerticalScroll then
        pcall(scroll.SetVerticalScroll, scroll, 0)
      end
      if scroll and scroll.ScrollBar and scroll.ScrollBar.SetValue then
        pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
      end
      UpdateScrollBarVisibility(true)
    end

    Apply()
    if C_Timer and C_Timer.After then
      C_Timer.After(0, Apply)
    end
  end

  local function ScrollByDelta(delta)
    local range = scroll and scroll.GetVerticalScrollRange and tonumber(scroll:GetVerticalScrollRange()) or 0
    if range <= 0 then
      UpdateScrollBarVisibility(true)
      return
    end

    local step = 28
    local current = scroll and scroll.GetVerticalScroll and tonumber(scroll:GetVerticalScroll()) or 0
    local target = math.max(0, math.min(range, current - ((delta or 0) * step)))

    if scroll and scroll.SetVerticalScroll then
      scroll:SetVerticalScroll(target)
    end
    if scroll and scroll.ScrollBar and scroll.ScrollBar.SetValue then
      scroll.ScrollBar:SetValue(target)
    end
    UpdateScrollBarVisibility(false)
  end

  local function UpdateEditHeight(selfEdit)
    if not selfEdit then return end
    local w = selfEdit:GetWidth()
    if not w or w < 1 then w = 680 end
    measure:SetWidth(w)
    local text = selfEdit:GetText()
    if text == nil or text == "" then
      text = " "
    end
    measure:SetText(text)
    local h = math.max(1, measure:GetStringHeight() + 24)
    selfEdit:SetHeight(h)
    if scroll and scroll.UpdateScrollChildRect then
      scroll:UpdateScrollChildRect()
    end
    UpdateScrollBarVisibility(false)
  end

  edit:SetScript("OnEscapePressed", function() blocker:Hide() end)
  edit:EnableMouseWheel(true)
  edit:SetScript("OnMouseWheel", function(_, delta)
    ScrollByDelta(delta)
  end)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(_, delta)
    ScrollByDelta(delta)
  end)
  edit:SetScript("OnCursorChanged", function()
    if scroll and scroll.UpdateScrollChildRect then
      scroll:UpdateScrollChildRect()
    end
    UpdateScrollBarVisibility(false)
  end)
  edit:SetScript("OnSizeChanged", function(selfEdit)
    UpdateEditHeight(selfEdit)
  end)
  edit:SetScript("OnTextChanged", function(selfEdit)
    UpdateEditHeight(selfEdit)
  end)
  scroll:SetScrollChild(edit)
  if scroll.HookScript then
    scroll:HookScript("OnShow", function()
      UpdateScrollBarVisibility(false)
      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          UpdateScrollBarVisibility(false)
        end)
      end
    end)
    scroll:HookScript("OnSizeChanged", function()
      UpdateScrollBarVisibility(false)
      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          UpdateScrollBarVisibility(false)
        end)
      end
    end)
  end
  if scroll.ScrollBar and scroll.ScrollBar.HookScript then
    scroll.ScrollBar:HookScript("OnMinMaxChanged", function()
      UpdateScrollBarVisibility(false)
    end)
  end

  local primary = widgets and widgets.Button and widgets:Button(box, "Done", 108, 24) or CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
  modal.primary = primary
  primary:SetPoint("BOTTOMRIGHT", -16, 14)
  if not (widgets and widgets.Button) then primary:SetSize(108, 24) end

  local secondary = widgets and widgets.Button and widgets:Button(box, "Select All", 108, 24) or CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
  modal.secondary = secondary
  secondary:SetPoint("RIGHT", primary, "LEFT", -8, 0)
  if not (widgets and widgets.Button) then secondary:SetSize(108, 24) end

  function modal:Open(opts)
    opts = type(opts) == "table" and opts or {}
    self._onPrimary = opts.onPrimary
    self._onSecondary = opts.onSecondary
    self._readOnly = opts.readOnly and true or false

    self.title:SetText(opts.title or "Shared Run")
    self.body:SetText(opts.body or "")
    self.edit:SetText(tostring(opts.text or ""))
    ResetScrollPosition(true)
    self.edit:HighlightText()
    if self._readOnly then
      self.edit:ClearFocus()
    else
      self.edit:SetFocus()
      ResetScrollPosition(true)
    end

    if self.primary.SetText then self.primary:SetText(opts.primaryText or "Done") end
    if self.secondary.SetText then self.secondary:SetText(opts.secondaryText or "Select All") end

    blocker:Show()
  end

  primary:SetScript("OnClick", function()
    local cb = modal._onPrimary
    if type(cb) == "function" then
      cb(edit:GetText() or "", modal)
    else
      blocker:Hide()
    end
  end)

  secondary:SetScript("OnClick", function()
    local cb = modal._onSecondary
    if type(cb) == "function" then
      cb(edit:GetText() or "", modal)
      return
    end
    edit:SetFocus()
    edit:HighlightText()
  end)

  return modal
end

function Share:ShowExportDialog(sess)
  if type(sess) ~= "table" then
    Notify("Select a run first.")
    return false
  end

  local export = self:CreateFullRunExport(sess)
  if type(export) ~= "string" or export == "" then
    Notify("Couldn't build an export for that run.")
    return false
  end

  local modal = EnsureShareTextModal(self)
  modal:Open({
    title = "Export Shared Run",
    body = "This is the full offline export string. Highlight it and copy it anywhere.",
    text = export,
    primaryText = "Close",
    secondaryText = "Select All",
    readOnly = true,
    onPrimary = function()
      modal.frame:Hide()
    end,
  })
  return true
end

function Share:ShowImportDialog(initialText)
  local modal = EnsureShareTextModal(self)
  modal:Open({
    title = "Import Shared Run",
    body = "Paste a WhatFarm share string below. Quick share links and full exports are both supported.",
    text = tostring(initialText or ""),
    primaryText = "Import",
    secondaryText = "Focus",
    readOnly = false,
    onPrimary = function(raw)
      local sess, sender, err = ParseAnySharedString(raw)
      if not sess then
        Notify(err or "That doesn't look like a valid WhatFarm share string.")
        return
      end
      modal.frame:Hide()
      self:_OpenSharedSession(sess, sender or "Shared Export")
    end,
    onSecondary = function()
      modal.edit:SetFocus()
    end,
  })
  return true
end

local function _ShareCreateTextArea(parent, theme, width, height, readOnly)
  local widgets = WF and WF.UI and WF.UI.Widgets or nil
  local holder = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  _ShareApplyBackdrop(holder, theme.bg, theme.border)
  holder:SetSize(width, height)

  local scroll = CreateFrame("ScrollFrame", nil, holder, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", 8, -8)
  scroll:SetPoint("BOTTOMRIGHT", -24, 8)

  if scroll.ScrollBar then
    scroll.ScrollBar:ClearAllPoints()
    scroll.ScrollBar:SetPoint("TOPRIGHT", holder, "TOPRIGHT", -8, -8)
    scroll.ScrollBar:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -8, 8)
  end

  if widgets and widgets.SkinScrollBar and scroll.ScrollBar then
    widgets:SkinScrollBar(scroll.ScrollBar)
  end

  local function GetInnerWidth()
    local baseWidth = holder and holder.GetWidth and holder:GetWidth() or width
    if not baseWidth or baseWidth < 1 then
      baseWidth = width
    end
    return math.max(60, baseWidth - 34)
  end

  local edit = CreateFrame("EditBox", nil, scroll)
  edit:SetMultiLine(true)
  edit:SetAutoFocus(false)
  edit:SetFontObject(_G.WF_GameFontHighlight or _G.ChatFontNormal)
  if edit.SetTextColor then
    edit:SetTextColor((theme.text and theme.text[1]) or 1, (theme.text and theme.text[2]) or 1, (theme.text and theme.text[3]) or 1, 1)
  end
  if edit.SetJustifyH then edit:SetJustifyH("LEFT") end
  if edit.SetJustifyV then edit:SetJustifyV("TOP") end
  edit:SetTextInsets(0, 0, 0, 0)
  edit:SetWidth(GetInnerWidth())
  edit:SetScript("OnEscapePressed", function(selfEdit)
    if selfEdit and selfEdit.ClearFocus then
      selfEdit:ClearFocus()
    end
  end)
  edit:EnableMouse(true)
  if readOnly then
    edit:SetMaxLetters(0)
  end

  local measure = holder:CreateFontString(nil, "ARTWORK")
  measure:Hide()
  measure:SetFontObject(_G.WF_GameFontHighlight or _G.ChatFontNormal)
  measure:SetJustifyH("LEFT")
  measure:SetJustifyV("TOP")
  if measure.SetWordWrap then measure:SetWordWrap(true) end
  if measure.SetNonSpaceWrap then measure:SetNonSpaceWrap(true) end
  measure:SetWidth(GetInnerWidth())
  measure:SetText(" ")

  local function UpdateScrollBarVisibility(resetWhenHidden)
    local range = scroll and scroll.GetVerticalScrollRange and tonumber(scroll:GetVerticalScrollRange()) or 0
    local holderH = holder and holder.GetHeight and tonumber(holder:GetHeight()) or 0
    local editH = edit and edit.GetHeight and tonumber(edit:GetHeight()) or 0
    local showBar = (range > 0.5) and (editH > (holderH + 2))

    if scroll and scroll.ScrollBar then
      if showBar then
        scroll.ScrollBar:Show()
        if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Show then
          scroll.ScrollBar._wfBG:Show()
        end
      else
        if resetWhenHidden and scroll.SetVerticalScroll then
          pcall(scroll.SetVerticalScroll, scroll, 0)
        end
        if resetWhenHidden and scroll.ScrollBar.SetValue then
          pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
        end
        scroll.ScrollBar:Hide()
        if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Hide then
          scroll.ScrollBar._wfBG:Hide()
        end
      end
    end
  end

  local function ResetScrollPosition(cursorToStart)
    local function Apply()
      if edit and edit.SetCursorPosition and cursorToStart then
        pcall(edit.SetCursorPosition, edit, 0)
      end
      if scroll and scroll.SetVerticalScroll then
        pcall(scroll.SetVerticalScroll, scroll, 0)
      end
      if scroll and scroll.ScrollBar and scroll.ScrollBar.SetValue then
        pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
      end
      UpdateScrollBarVisibility(true)
    end

    Apply()
    if C_Timer and C_Timer.After then
      C_Timer.After(0, Apply)
    end
  end

  local function ScrollByDelta(delta)
    local range = scroll and scroll.GetVerticalScrollRange and tonumber(scroll:GetVerticalScrollRange()) or 0
    if range <= 0 then
      UpdateScrollBarVisibility(true)
      return
    end

    local step = 28
    local current = scroll and scroll.GetVerticalScroll and tonumber(scroll:GetVerticalScroll()) or 0
    local target = math.max(0, math.min(range, current - ((delta or 0) * step)))

    if scroll and scroll.SetVerticalScroll then
      scroll:SetVerticalScroll(target)
    end
    if scroll and scroll.ScrollBar and scroll.ScrollBar.SetValue then
      scroll.ScrollBar:SetValue(target)
    end
    UpdateScrollBarVisibility(false)
  end

  local function UpdateEditHeight(selfEdit)
    if not selfEdit then return end
    local innerWidth = GetInnerWidth()
    selfEdit:SetWidth(innerWidth)
    measure:SetWidth(innerWidth)

    local text = selfEdit:GetText()
    if text == nil or text == "" then
      text = " "
    end
    measure:SetText(text)

    local baseHeight = holder and holder.GetHeight and holder:GetHeight() or height
    if not baseHeight or baseHeight < 1 then
      baseHeight = height
    end

    local h = math.max(math.max(1, baseHeight - 16), measure:GetStringHeight() + 20)
    selfEdit:SetHeight(h)
    if scroll and scroll.UpdateScrollChildRect then
      scroll:UpdateScrollChildRect()
    end
    UpdateScrollBarVisibility(false)
  end

  edit:EnableMouseWheel(true)
  edit:SetScript("OnMouseWheel", function(_, delta)
    ScrollByDelta(delta)
  end)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(_, delta)
    ScrollByDelta(delta)
  end)

  edit:SetScript("OnCursorChanged", function()
    if scroll and scroll.UpdateScrollChildRect then
      scroll:UpdateScrollChildRect()
    end
    UpdateScrollBarVisibility(false)
  end)
  edit:SetScript("OnSizeChanged", function(selfEdit)
    UpdateEditHeight(selfEdit)
  end)
  edit:SetScript("OnTextChanged", function(selfEdit)
    UpdateEditHeight(selfEdit)
  end)

  holder:SetScript("OnSizeChanged", function()
    UpdateEditHeight(edit)
  end)

  scroll:SetScrollChild(edit)
  if scroll.HookScript then
    scroll:HookScript("OnShow", function()
      UpdateScrollBarVisibility(false)
      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          UpdateScrollBarVisibility(false)
        end)
      end
    end)
    scroll:HookScript("OnSizeChanged", function()
      UpdateScrollBarVisibility(false)
      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          UpdateScrollBarVisibility(false)
        end)
      end
    end)
  end
  if scroll.ScrollBar and scroll.ScrollBar.HookScript then
    scroll.ScrollBar:HookScript("OnMinMaxChanged", function()
      UpdateScrollBarVisibility(false)
    end)
  end
  holder.scroll = scroll
  holder.edit = edit
  holder.measure = measure
  holder.UpdateHeight = UpdateEditHeight
  holder.ResetScroll = ResetScrollPosition
  return holder
end

local function EnsureShareHubModal(self)
  if self._shareHub and self._shareHub.frame then
    return self._shareHub
  end

  local theme = _ShareThemeColors()
  local widgets = WF and WF.UI and WF.UI.Widgets or nil
  local modal = {}
  self._shareHub = modal

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  modal.frame = blocker
  blocker:SetAllPoints(UIParent)
  blocker:SetFrameStrata("DIALOG")
  blocker:EnableMouse(true)
  blocker:EnableKeyboard(true)
  if blocker.SetPropagateKeyboardInput then
    blocker:SetPropagateKeyboardInput(false)
  end
  _ShareApplyBackdrop(blocker, { 0, 0, 0, 0.55 }, { 0, 0, 0, 0 })
  blocker:Hide()
  blocker:SetScript("OnKeyDown", function(f, key)
    if key == "ESCAPE" then
      f:Hide()
    end
  end)

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  modal.box = box
  box:SetSize(840, 540)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 12)
  _ShareApplyBackdrop(box, theme.bg, theme.border)
  box:EnableMouse(true)

  local header = _ShareCreatePanel(box, theme, theme.panel, theme.border)
  modal.header = header
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(36)

  local title = _ShareCreateLabel(header, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  modal.title = title
  title:SetPoint("LEFT", 14, 0)
  title:SetText("Share Run")

  local subtitle = _ShareCreateLabel(header, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  modal.subtitle = subtitle
  subtitle:SetPoint("LEFT", title, "RIGHT", 10, 0)
  subtitle:SetText("Quick share, full export, and import")

  local close = CreateFrame("Button", nil, header, "UIPanelCloseButton")
  modal.close = close
  close:SetPoint("RIGHT", -4, 0)
  if widgets and widgets.SkinCloseButton then
    widgets:SkinCloseButton(close)
  end
  close:SetScript("OnClick", function() blocker:Hide() end)

  local quickSection = _ShareCreateSection(box, theme, "SHARE")
  modal.quickSection = quickSection
  quickSection:SetPoint("TOPLEFT", 12, -46)
  quickSection:SetPoint("TOPRIGHT", -12, -46)
  quickSection:SetHeight(144)

  local quickBody = _ShareCreateLabel(quickSection, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.quickBody = quickBody
  quickBody:SetPoint("TOPLEFT", 12, -32)
  quickBody:SetPoint("TOPRIGHT", -12, -32)
  quickBody:SetJustifyH("LEFT")
  quickBody:SetText("Uses the full offline run when it fits in chat. Falls back to a shorter summary share when it doesn't.")

  local quickArea = _ShareCreateTextArea(quickSection, theme, 808, 48, true)
  modal.quickArea = quickArea
  quickArea:ClearAllPoints()
  quickArea:SetPoint("TOPLEFT", 12, -58)
  quickArea:SetPoint("TOPRIGHT", -12, -58)
  quickArea:SetPoint("BOTTOMLEFT", 12, 40)
  quickArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local quickInsert = widgets and widgets.Button and widgets:Button(quickSection, "Insert to Chat", 110, 22) or CreateFrame("Button", nil, quickSection, "UIPanelButtonTemplate")
  modal.quickInsert = quickInsert
  if not (widgets and widgets.Button) then quickInsert:SetSize(110, 22) end
  quickInsert:SetPoint("BOTTOMRIGHT", -12, 10)

  local quickSelect = widgets and widgets.Button and widgets:Button(quickSection, "Select All", 88, 22) or CreateFrame("Button", nil, quickSection, "UIPanelButtonTemplate")
  modal.quickSelect = quickSelect
  if not (widgets and widgets.Button) then quickSelect:SetSize(88, 22) end
  quickSelect:SetPoint("RIGHT", quickInsert, "LEFT", -8, 0)

  local fullSection = _ShareCreateSection(box, theme, "FULL EXPORT")
  modal.fullSection = fullSection
  fullSection:SetPoint("TOPLEFT", quickSection, "BOTTOMLEFT", 0, -8)
  fullSection:SetPoint("TOPRIGHT", quickSection, "BOTTOMRIGHT", 0, -8)
  fullSection:SetHeight(168)

  local fullBody = _ShareCreateLabel(fullSection, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.fullBody = fullBody
  fullBody:SetPoint("TOPLEFT", 12, -32)
  fullBody:SetPoint("TOPRIGHT", -12, -32)
  fullBody:SetJustifyH("LEFT")
  fullBody:SetText("Full offline export for YouTube descriptions, Discord, notes, or manual import later.")

  local fullArea = _ShareCreateTextArea(fullSection, theme, 808, 72, true)
  modal.fullArea = fullArea
  fullArea:ClearAllPoints()
  fullArea:SetPoint("TOPLEFT", 12, -58)
  fullArea:SetPoint("TOPRIGHT", -12, -58)
  fullArea:SetPoint("BOTTOMLEFT", 12, 40)
  fullArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local fullSelect = widgets and widgets.Button and widgets:Button(fullSection, "Select All", 88, 22) or CreateFrame("Button", nil, fullSection, "UIPanelButtonTemplate")
  modal.fullSelect = fullSelect
  if not (widgets and widgets.Button) then fullSelect:SetSize(88, 22) end
  fullSelect:SetPoint("BOTTOMRIGHT", -12, 10)

  local importSection = _ShareCreateSection(box, theme, "IMPORT")
  modal.importSection = importSection
  importSection:SetPoint("TOPLEFT", fullSection, "BOTTOMLEFT", 0, -8)
  importSection:SetPoint("TOPRIGHT", fullSection, "BOTTOMRIGHT", 0, -8)
  importSection:SetPoint("BOTTOMLEFT", box, "BOTTOMLEFT", 12, 12)
  importSection:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -12, 12)

  local importBody = _ShareCreateLabel(importSection, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.importBody = importBody
  importBody:SetPoint("TOPLEFT", 12, -32)
  importBody:SetPoint("TOPRIGHT", -12, -32)
  importBody:SetJustifyH("LEFT")
  importBody:SetText("Paste a WhatFarm quick share or full export here to open it in the shared run viewer.")

  local importArea = _ShareCreateTextArea(importSection, theme, 808, 56, false)
  modal.importArea = importArea
  importArea:ClearAllPoints()
  importArea:SetPoint("TOPLEFT", 12, -58)
  importArea:SetPoint("TOPRIGHT", -12, -58)
  importArea:SetPoint("BOTTOMLEFT", 12, 40)
  importArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local importOpen = widgets and widgets.Button and widgets:Button(importSection, "Import", 88, 22) or CreateFrame("Button", nil, importSection, "UIPanelButtonTemplate")
  modal.importOpen = importOpen
  if not (widgets and widgets.Button) then importOpen:SetSize(88, 22) end
  importOpen:SetPoint("BOTTOMRIGHT", -12, 10)

  local importFocus = widgets and widgets.Button and widgets:Button(importSection, "Focus", 76, 22) or CreateFrame("Button", nil, importSection, "UIPanelButtonTemplate")
  modal.importFocus = importFocus
  if not (widgets and widgets.Button) then importFocus:SetSize(76, 22) end
  importFocus:SetPoint("RIGHT", importOpen, "LEFT", -8, 0)

  local importClear = widgets and widgets.Button and widgets:Button(importSection, "Clear", 76, 22) or CreateFrame("Button", nil, importSection, "UIPanelButtonTemplate")
  modal.importClear = importClear
  if not (widgets and widgets.Button) then importClear:SetSize(76, 22) end
  importClear:SetPoint("RIGHT", importFocus, "LEFT", -8, 0)

  local function SetReadOnlyVisual(area, isReadOnly)
    if not area or not area.edit then return end
    area.edit:SetTextInsets(0, 0, 0, 0)
    area._readOnly = isReadOnly and true or false
  end

  SetReadOnlyVisual(quickArea, true)
  SetReadOnlyVisual(fullArea, true)
  SetReadOnlyVisual(importArea, false)

  quickArea.edit:SetScript("OnMouseUp", function(selfEdit)
    selfEdit:SetFocus()
    if quickArea.ResetScroll then
      quickArea.ResetScroll(true)
    end
    selfEdit:HighlightText()
  end)
  fullArea.edit:SetScript("OnMouseUp", function(selfEdit)
    selfEdit:SetFocus()
    if fullArea.ResetScroll then
      fullArea.ResetScroll(true)
    end
    selfEdit:HighlightText()
  end)

  function modal:RefreshFields(sess, initialImport)
    self._currentSess = sess

    local quick = ""
    local full = ""
    if type(sess) == "table" then
      quick = Share:CreateRunLink(sess) or ""
      full = Share:CreateFullRunExport(sess) or ""
    end

    self.quickArea.edit:SetText(quick)
    self.quickArea.UpdateHeight(self.quickArea.edit)
    if self.quickArea.ResetScroll then
      self.quickArea.ResetScroll(true)
    end

    self.fullArea.edit:SetText(full)
    self.fullArea.UpdateHeight(self.fullArea.edit)
    if self.fullArea.ResetScroll then
      self.fullArea.ResetScroll(true)
    end

    self.importArea.edit:SetText(tostring(initialImport or ""))
    self.importArea.UpdateHeight(self.importArea.edit)
    if self.importArea.ResetScroll then
      self.importArea.ResetScroll(false)
    end

    local hasSess = type(sess) == "table"
    if hasSess then
      if quick:find("^<wfc:") then
        self.quickBody:SetText("This run fits in chat, so Share is using the compact full run export with the full loot table.")
      elseif quick:find("^<wfx:") then
        self.quickBody:SetText("This run fits in chat, so Share is using the full offline export.")
      else
        self.quickBody:SetText("This run is too large for a full chat export, so Share is using a shorter summary link.")
      end
    else
      self.quickBody:SetText("Select a History run or a shared run first to generate share and full-export strings. You can still use Import below.")
    end
    self.fullBody:SetText(hasSess and "Full offline export for YouTube descriptions, Discord, notes, or manual import later." or "Full export appears here when a run is selected.")

    if self.quickInsert and self.quickInsert.Enable then
      if hasSess and quick ~= "" then self.quickInsert:Enable() else self.quickInsert:Disable() end
    end
    if self.quickSelect and self.quickSelect.Enable then
      if quick ~= "" then self.quickSelect:Enable() else self.quickSelect:Disable() end
    end
    if self.fullSelect and self.fullSelect.Enable then
      if full ~= "" then self.fullSelect:Enable() else self.fullSelect:Disable() end
    end
  end

  function modal:Open(sess, initialImport)
    self.title:SetText(type(sess) == "table" and "Share Run" or "Share / Import")
    self:RefreshFields(sess, initialImport)
    blocker:Show()
    self.importArea.edit:ClearFocus()
  end

  quickInsert:SetScript("OnClick", function()
    local sess = modal._currentSess
    if type(sess) ~= "table" then return end
    if not Share:InsertRunLink(sess) then
      Notify("Couldn't insert a quick share for that run.")
    end
  end)

  quickSelect:SetScript("OnClick", function()
    quickArea.edit:SetFocus()
    if quickArea.ResetScroll then
      quickArea.ResetScroll(true)
    end
    quickArea.edit:HighlightText()
  end)

  fullSelect:SetScript("OnClick", function()
    fullArea.edit:SetFocus()
    if fullArea.ResetScroll then
      fullArea.ResetScroll(true)
    end
    fullArea.edit:HighlightText()
  end)

  importFocus:SetScript("OnClick", function()
    importArea.edit:SetFocus()
    if importArea.ResetScroll then
      importArea.ResetScroll(true)
    end
  end)

  importClear:SetScript("OnClick", function()
    importArea.edit:SetText("")
    importArea.UpdateHeight(importArea.edit)
    if importArea.ResetScroll then
      importArea.ResetScroll(true)
    end
    importArea.edit:SetFocus()
  end)

  importOpen:SetScript("OnClick", function()
    local raw = importArea.edit:GetText() or ""
    local sess, sender, err = ParseAnySharedString(raw)
    if not sess then
      Notify(err or "That doesn't look like a valid WhatFarm share string.")
      return
    end
    blocker:Hide()
    Share:_OpenSharedSession(sess, sender or "Shared Export")
  end)

  return modal
end

function Share:ShowShareDialog(sess, initialImport)
  local modal = EnsureShareHubModal(self)
  modal:Open(sess, initialImport)
  return true
end


-- ---------------------------------------------------------
-- Farm share / import
-- ---------------------------------------------------------
local function FarmShareChecksum(data)
  data = tostring(data or "")

  local a, b = 1, 0
  for i = 1, #data do
    a = (a + (string.byte(data, i) or 0)) % 65521
    b = (b + a) % 65521
  end

  return string.format("%04X%04X", b, a)
end

local function FarmShareMoney(copper)
  copper = tonumber(copper) or 0
  if WF and WF.Util and WF.Util.FormatMoney then
    return WF.Util:FormatMoney(copper)
  end
  return tostring(math.floor(copper + 0.5))
end

local function FarmShareModeLabel(db)
  local active = "Default"
  if WF and WF.DB and WF.DB.GetActiveProfileName then
    active = tostring(WF.DB:GetActiveProfileName() or "Default")
    if active == "" then active = "Default" end
  end

  local pref = (WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("farmLibraryViewMode")) or "Active"
  pref = tostring(pref or "Active")
  if pref == "" or pref == "Active" then
    return active
  end
  return pref
end

local function FarmShareSplitTags(tags)
  if type(tags) == "table" then
    local out = {}
    for i = 1, #tags do
      local v = Trim(tags[i])
      if v ~= "" then
        out[#out + 1] = v
      end
    end
    return out
  end

  local out = {}
  tags = tostring(tags or "")
  for part in string.gmatch(tags, "[^,]+") do
    local v = Trim(part)
    if v ~= "" then
      out[#out + 1] = v
    end
  end
  return out
end

local function FarmShareJoinTags(tags)
  local clean = FarmShareSplitTags(tags)
  if #clean <= 0 then return "" end
  return table.concat(clean, ",")
end

local function FarmSharePreviewItemsFromFarm(farm, limit)
  limit = math.max(1, tonumber(limit) or 5)
  local items = {}
  if type(farm) ~= "table" or type(farm.previewItems) ~= "table" then
    return items
  end

  local source = farm.previewItems
  for i = 1, #source do
    local it = source[i]
    if type(it) == "table" then
      local name = Trim(it.name or it.itemName or it.label or "")
      local count = tonumber(it.count or it.qty or it.amount or 0) or 0
      local value = tonumber(it.value or it.totalValue or it.total or 0) or 0
      local itemID = tonumber(it.itemID or it.id) or 0
      local link = tostring(it.link or "")
      if name ~= "" or itemID > 0 then
        items[#items + 1] = {
          itemID = itemID,
          name = name ~= "" and name or (link:match("%[(.-)%]") or (itemID > 0 and ("Item " .. tostring(itemID)) or "Item")),
          count = math.max(0, count),
          value = math.max(0, value),
          link = link,
        }
      end
    end
  end

  table.sort(items, function(a, b)
    if a.value ~= b.value then return a.value > b.value end
    if a.count ~= b.count then return a.count > b.count end
    return tostring(a.name or "") < tostring(b.name or "")
  end)

  local out = {}
  for i = 1, math.min(limit, #items) do
    out[i] = items[i]
  end
  return out
end

local function FarmShareTopItems(db, farm, limit)
  limit = math.max(1, tonumber(limit) or 5)
  if type(farm) ~= "table" then return {} end

  local farmId = tonumber(farm.id or farm._id) or nil
  local sessions = db and db.history and db.history.sessions or nil
  local totals = {}

  local function bucketFor(itemID, name, link)
    local key
    itemID = tonumber(itemID) or 0
    name = Trim(name)
    if itemID > 0 then
      key = "id:" .. tostring(itemID)
    elseif name ~= "" then
      key = "name:" .. string.lower(name)
    else
      return nil
    end

    local cur = totals[key]
    if not cur then
      cur = {
        itemID = itemID,
        name = name,
        link = tostring(link or ""),
        count = 0,
        value = 0,
      }
      totals[key] = cur
    else
      if cur.itemID <= 0 and itemID > 0 then cur.itemID = itemID end
      if (not cur.name or cur.name == "") and name ~= "" then cur.name = name end
      if (not cur.link or cur.link == "") and tostring(link or "") ~= "" then cur.link = tostring(link or "") end
    end
    return cur
  end

  local function entryValue(it, count)
    local value = tonumber(it and (it.value or it.totalValue or it.total or it.priceTotal)) or 0
    if value > 0 then return value end

    local unit = tonumber(it and (it.unitValue or it.unitPrice or it.priceEach or it.eachValue or it.price)) or 0
    count = tonumber(count) or 0
    if unit > 0 and count > 0 then
      return unit * count
    end
    return 0
  end

  local function add(itemID, name, count, value, link)
    count = tonumber(count) or 0
    value = tonumber(value) or 0
    local bucket = bucketFor(itemID, name, link)
    if not bucket then return end
    if count <= 0 and value <= 0 then return end
    bucket.count = (bucket.count or 0) + count
    bucket.value = (bucket.value or 0) + value
  end

  local function parseItems(items)
    if type(items) ~= "table" then return end

    if items[1] ~= nil then
      for i = 1, #items do
        local it = items[i]
        if type(it) == "table" then
          local name = it.name or it.itemName or it.label or it.text
          local count = it.count or it.qty or it.amount or it.n or 0
          local itemID = tonumber(it.itemID or it.id) or 0
          local link = tostring(it.link or "")
          if name or itemID > 0 then
            add(itemID, name, count, entryValue(it, count), link)
          end
        end
      end
      return
    end

    for k, v in pairs(items) do
      if type(v) == "table" then
        local itemID = tonumber(v.itemID or v.id) or tonumber(k) or 0
        local name = v.name or v.itemName or v.label or ((itemID <= 0 and tostring(k)) or "")
        local count = v.count or v.qty or v.amount or v.n or v[1] or 0
        local link = tostring(v.link or "")
        add(itemID, name, count, entryValue(v, count), link)
      elseif type(v) == "number" then
        local itemID = tonumber(k) or 0
        add(itemID, itemID <= 0 and tostring(k) or "", v, 0, "")
      end
    end
  end

  if farmId and type(sessions) == "table" then
    for i = 1, #sessions do
      local s = sessions[i]
      if type(s) == "table" then
        local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
        if tonumber(sid) == tonumber(farmId) then
          parseItems(s.items)
          parseItems(s.loot)
          parseItems(s.drops)
          parseItems(s.topItems)
          parseItems(s.itemsByName)
          parseItems(s.itemsById)
          parseItems(s.lootedItems)
        end
      end
    end
  end

  local out = {}
  for _, v in pairs(totals) do
    if (not v.name or v.name == "") and v.link ~= "" then
      v.name = v.link:match("%[(.-)%]") or v.name
    end
    if ((not v.name or v.name == "") and (v.itemID or 0) > 0) and GetItemInfo then
      local name, link = GetItemInfo(v.link ~= "" and v.link or v.itemID)
      if not v.name or v.name == "" then v.name = name or v.name end
      if (not v.link or v.link == "") and link then v.link = link end
    end
    if (not v.name or v.name == "") then
      v.name = (v.itemID and v.itemID > 0) and ("Item " .. tostring(v.itemID)) or "Item"
    end
    out[#out + 1] = v
  end

  table.sort(out, function(a, b)
    if a.value ~= b.value then return a.value > b.value end
    if a.count ~= b.count then return a.count > b.count end
    return tostring(a.name or "") < tostring(b.name or "")
  end)

  if #out <= 0 then
    return FarmSharePreviewItemsFromFarm(farm, limit)
  end

  local final = {}
  for i = 1, math.min(limit, #out) do
    final[i] = out[i]
  end
  return final
end

local function FarmShareAverageGPH(db, farm)
  if type(farm) ~= "table" then return 0, "" end
  local id = tonumber(farm.id or farm._id) or nil
  local mode = FarmShareModeLabel(db)

  if id and type(db) == "table" then
    local minSeconds = tonumber((db.settings and db.settings.historyAvgMinSeconds) or 0) or 0
    if minSeconds > 0 then
      local h = db.history or {}
      local t = h.totalsByFarmMode and h.totalsByFarmMode[id] and h.totalsByFarmMode[id][mode]
      if t and tonumber(t.time or 0) > 0 then
        return ((tonumber(t.value) or 0) / math.max(1, tonumber(t.time) or 1)) * 3600, mode
      end
    end

    local fs = db.farmStats and (db.farmStats[id] or db.farmStats[tostring(id)]) or nil
    local bucket = fs and fs.modes and fs.modes[mode] or nil
    if bucket and tonumber(bucket.time or 0) > 0 then
      return ((tonumber(bucket.value) or 0) / math.max(1, tonumber(bucket.time) or 1)) * 3600, mode
    end
  end

  local preview = tonumber(farm.previewAvgGPH or farm.sharedAvgGPH) or 0
  if preview > 0 then
    return preview, tostring(farm.previewMode or farm.sharedMode or mode or "")
  end

  return 0, mode
end

local function BuildRawFarmPayload(farm)
  if type(farm) ~= "table" then return nil end

  local db = (WF and WF.DB and WF.DB.Get and WF.DB:Get()) or nil
  local avgGPH, mode = FarmShareAverageGPH(db, farm)
  local topItems = FarmShareTopItems(db, farm, 5)
  local tags = FarmShareJoinTags(farm.tags)

  local header = {
    "H",
    "sender=" .. UrlEncode(FullPlayerName()),
    "name=" .. UrlEncode(farm.name or ""),
    "zone=" .. UrlEncode(farm.zone or ""),
    "exp=" .. UrlEncode(farm.expansion or ""),
    "aim=" .. UrlEncode(farm.aim or ""),
    "type=" .. UrlEncode(farm.farmType or ""),
    "expected=" .. tostring(tonumber(farm.expectedGPH) or 0),
    "avg=" .. tostring(math.floor((avgGPH or 0) + 0.5)),
    "mode=" .. UrlEncode(mode or ""),
    "map=" .. tostring(tonumber(farm.mapId) or 0),
    "x=" .. tostring(tonumber(farm.x) or 0),
    "y=" .. tostring(tonumber(farm.y) or 0),
    "wmap=" .. tostring(tonumber(farm.worldMapId) or 0),
    "wx=" .. tostring(tonumber(farm.worldX) or 0),
    "wy=" .. tostring(tonumber(farm.worldY) or 0),
    "image=" .. UrlEncode(farm.image or ""),
    "tags=" .. UrlEncode(tags),
    "notes=" .. UrlEncode(farm.notes or ""),
    "seed=" .. UrlEncode(farm.seedKey or ""),
    "created=" .. tostring(tonumber(farm.createdAt) or 0),
    "updated=" .. tostring(tonumber(farm.updatedAt) or 0),
  }

  local lines = {
    "WFFARM1",
    table.concat(header, "|"),
  }

  for i = 1, #topItems do
    local it = topItems[i]
    lines[#lines + 1] = table.concat({
      "P",
      tostring(tonumber(it.itemID) or 0),
      tostring(tonumber(it.count) or 0),
      tostring(tonumber(it.value) or 0),
      UrlEncode(it.name or ""),
      UrlEncode(it.link or ""),
    }, "|")
  end

  return table.concat(lines, "\n")
end

local function BuildFarmExportString(farm)
  local raw = BuildRawFarmPayload(farm)
  if type(raw) ~= "string" or raw == "" then
    return nil
  end

  local payload = EncodePayload(raw)
  if type(payload) ~= "string" or payload == "" then
    return nil
  end

  local encoded = UrlEncode(payload)
  return "WFFARM2:" .. encoded .. ":" .. FarmShareChecksum(encoded)
end

local function ParseFarmExportString(raw)
  raw = Trim(raw or "")
  if raw == "" then return nil, "Empty farm share string" end

  local encoded, checksum = raw:match("^WFFARM2[:|;,.](.+)[:|;,.]([0-9A-Fa-f]+)$")
  if not encoded or not checksum or #checksum ~= 8 then
    return nil, "Malformed WhatFarm farm string"
  end

  checksum = string.upper(checksum)
  if FarmShareChecksum(encoded) ~= checksum then
    return nil, "Farm string checksum mismatch"
  end

  local payload = UrlDecode(encoded)
  local decoded = DecodePayload(payload)
  if type(decoded) ~= "string" or decoded == "" then
    return nil, "That farm string couldn't be decoded."
  end

  local lines = {}
  for line in string.gmatch(decoded, "[^\n]+") do
    lines[#lines + 1] = line
  end
  if lines[1] ~= "WFFARM1" then
    return nil, "Unsupported farm payload"
  end

  local farm = {
    tags = {},
    previewItems = {},
    _wfImportedSharedFarm = true,
  }

  for i = 2, #lines do
    local line = lines[i]
    if line:sub(1, 2) == "H|" then
      for part in string.gmatch(line, "([^|]+)") do
        if part ~= "H" then
          local k, v = part:match("^([^=]+)=(.*)$")
          if k then
            if k == "sender" then farm.sharedBy = UrlDecode(v)
            elseif k == "name" then farm.name = UrlDecode(v)
            elseif k == "zone" then farm.zone = UrlDecode(v)
            elseif k == "exp" then farm.expansion = UrlDecode(v)
            elseif k == "aim" then farm.aim = UrlDecode(v)
            elseif k == "type" then farm.farmType = UrlDecode(v)
            elseif k == "expected" then farm.expectedGPH = tonumber(v) or nil
            elseif k == "avg" then farm.previewAvgGPH = tonumber(v) or nil
            elseif k == "mode" then farm.previewMode = UrlDecode(v)
            elseif k == "map" then local n = tonumber(v) or 0 if n > 0 then farm.mapId = n end
            elseif k == "x" then local n = tonumber(v) or 0 if n > 0 then farm.x = n end
            elseif k == "y" then local n = tonumber(v) or 0 if n > 0 then farm.y = n end
            elseif k == "wmap" then local n = tonumber(v) or 0 if n > 0 then farm.worldMapId = n end
            elseif k == "wx" then local n = tonumber(v) or 0 if n > 0 then farm.worldX = n end
            elseif k == "wy" then local n = tonumber(v) or 0 if n > 0 then farm.worldY = n end
            elseif k == "image" then farm.image = UrlDecode(v)
            elseif k == "tags" then farm.tags = FarmShareSplitTags(UrlDecode(v))
            elseif k == "notes" then farm.notes = UrlDecode(v)
            elseif k == "seed" then farm.seedKey = UrlDecode(v)
            elseif k == "created" then farm.createdAt = tonumber(v) or nil
            elseif k == "updated" then farm.updatedAt = tonumber(v) or nil end
          end
        end
      end
    elseif line:sub(1, 2) == "P|" then
      local itemIDS, countS, valueS, nameEnc, linkEnc = line:match("^P|([^|]*)|([^|]*)|([^|]*)|([^|]*)|?(.*)$")
      local item = {
        itemID = tonumber(itemIDS) or 0,
        count = tonumber(countS) or 0,
        value = tonumber(valueS) or 0,
        name = UrlDecode(nameEnc or ""),
        link = UrlDecode(linkEnc or ""),
      }
      if item.name ~= "" or item.itemID > 0 then
        if item.name == "" and item.link ~= "" then
          item.name = item.link:match("%[(.-)%]") or item.name
        end
        if (item.name == "" or item.link == "") and item.itemID > 0 then
          HydrateItem(item, item.itemID)
        end
        if item.name == "" then
          item.name = (item.itemID > 0) and ("Item " .. tostring(item.itemID)) or "Item"
        end
        farm.previewItems[#farm.previewItems + 1] = item
      end
    end
  end

  if type(farm.tags) ~= "table" then
    farm.tags = FarmShareSplitTags(farm.tags)
  end
  if Trim(farm.name or "") == "" then
    farm.name = "Imported Farm"
  end
  if Trim(farm.zone or "") == "" then
    farm.zone = farm.name
  end

  return farm
end

local function BuildFarmImportPreviewText(farm)
  if type(farm) ~= "table" then return "No farm data." end

  local lines = {}
  local sender = Trim(farm.sharedBy or "")
  if sender ~= "" then
    lines[#lines + 1] = "Sender: " .. sender
  end
  lines[#lines + 1] = "Farm: " .. Trim(farm.name or "Imported Farm")

  local zone = Trim(farm.zone or "")
  if zone ~= "" then
    lines[#lines + 1] = "Zone: " .. zone
  end

  if farm.x and farm.y then
    lines[#lines + 1] = string.format("Coords: %.1f, %.1f", (tonumber(farm.x) or 0) * 100, (tonumber(farm.y) or 0) * 100)
  end

  local exp = Trim(farm.expansion or "")
  if exp ~= "" then
    lines[#lines + 1] = "Expansion: " .. exp
  end

  local aim = Trim(farm.aim or "")
  if aim ~= "" then
    lines[#lines + 1] = "Aim: " .. aim
  end

  local farmType = Trim(farm.farmType or "")
  if farmType ~= "" then
    lines[#lines + 1] = "Type: " .. farmType
  end

  if tonumber(farm.expectedGPH) and tonumber(farm.expectedGPH) > 0 then
    lines[#lines + 1] = "Expected: " .. FarmShareMoney(farm.expectedGPH)
  end

  if tonumber(farm.previewAvgGPH) and tonumber(farm.previewAvgGPH) > 0 then
    local mode = Trim(farm.previewMode or "")
    if mode ~= "" then
      lines[#lines + 1] = "Sender Avg GPH (" .. mode .. "): " .. FarmShareMoney(farm.previewAvgGPH)
    else
      lines[#lines + 1] = "Sender Avg GPH: " .. FarmShareMoney(farm.previewAvgGPH)
    end
  end

  local tags = FarmShareJoinTags(farm.tags)
  if tags ~= "" then
    lines[#lines + 1] = "Tags: " .. tags:gsub(",", ", ")
  end

  local notes = Trim(farm.notes or "")
  if notes ~= "" then
    lines[#lines + 1] = ""
    lines[#lines + 1] = "Notes:"
    lines[#lines + 1] = notes
  end

  local items = type(farm.previewItems) == "table" and farm.previewItems or {}
  if #items > 0 then
    lines[#lines + 1] = ""
    lines[#lines + 1] = "Top loot preview:"
    for i = 1, math.min(5, #items) do
      local it = items[i]
      local row = "• " .. tostring(it.name or "Item")
      local count = tonumber(it.count) or 0
      local value = tonumber(it.value) or 0
      if count > 0 then
        row = row .. " x" .. tostring(count)
      end
      if value > 0 then
        row = row .. " — " .. FarmShareMoney(value)
      end
      lines[#lines + 1] = row
    end
  end

  return table.concat(lines, "\n")
end

local function ImportSharedFarm(farm, overrideName)
  if type(farm) ~= "table" or not (WF and WF.DB and WF.DB.Get) then
    return nil, "Farm import isn't available right now."
  end

  local db = WF.DB:Get()
  if type(db) ~= "table" then
    return nil, "Farm database unavailable."
  end

  db.farms = db.farms or {}
  local now = time()
  local id = (WF.DB.NextFarmId and WF.DB:NextFarmId()) or (#db.farms + 1)

  local record = {
    id = id,
    name = Trim(overrideName or farm.name or "Imported Farm"),
    zone = Trim(farm.zone or ""),
    expansion = Trim(farm.expansion or ""),
    aim = Trim(farm.aim or ""),
    farmType = Trim(farm.farmType or ""),
    expectedGPH = tonumber(farm.expectedGPH) or nil,
    mapId = tonumber(farm.mapId) or nil,
    x = tonumber(farm.x) or nil,
    y = tonumber(farm.y) or nil,
    worldMapId = tonumber(farm.worldMapId) or nil,
    worldX = tonumber(farm.worldX) or nil,
    worldY = tonumber(farm.worldY) or nil,
    image = Trim(farm.image or ""),
    notes = Trim(farm.notes or ""),
    tags = FarmShareSplitTags(farm.tags),
    seedKey = Trim(farm.seedKey or ""),
    createdAt = tonumber(farm.createdAt) or now,
    updatedAt = tonumber(farm.updatedAt) or now,
    previewAvgGPH = tonumber(farm.previewAvgGPH) or nil,
    previewMode = Trim(farm.previewMode or ""),
    previewItems = {},
    sharedBy = Trim(farm.sharedBy or ""),
    importedAt = now,
    importedSharedFarm = true,
  }

  if record.name == "" then
    record.name = "Imported Farm"
  end
  if record.zone == "" then
    record.zone = record.name
  end
  if record.image == "" then record.image = nil end
  if record.notes == "" then record.notes = nil end
  if record.seedKey == "" then record.seedKey = nil end
  if type(record.tags) == "table" and #record.tags <= 0 then record.tags = nil end
  if record.previewMode == "" then record.previewMode = nil end
  if not (record.previewAvgGPH and record.previewAvgGPH > 0) then record.previewAvgGPH = nil end

  local sourceItems = type(farm.previewItems) == "table" and farm.previewItems or {}
  for i = 1, math.min(5, #sourceItems) do
    local it = sourceItems[i]
    if type(it) == "table" then
      record.previewItems[#record.previewItems + 1] = {
        itemID = tonumber(it.itemID) or 0,
        name = Trim(it.name or ""),
        count = tonumber(it.count) or 0,
        value = tonumber(it.value) or 0,
        link = tostring(it.link or ""),
      }
    end
  end
  if #record.previewItems <= 0 then record.previewItems = nil end

  db.farms[id] = record

  if WF and WF.UI and WF.UI.Help and WF.UI.Help.RefreshFarms then
    pcall(WF.UI.Help.RefreshFarms, WF.UI.Help)
  end

  Notify("Imported farm: " .. record.name)
  return id, record
end

local function EnsureFarmImportPreviewModal(self)
  if self._farmImportPreview and self._farmImportPreview.frame then
    return self._farmImportPreview
  end

  local theme = _ShareThemeColors()
  local widgets = WF and WF.UI and WF.UI.Widgets or nil
  local modal = {}
  self._farmImportPreview = modal

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  modal.frame = blocker
  blocker:SetAllPoints(UIParent)
  blocker:SetFrameStrata("FULLSCREEN_DIALOG")
  blocker:EnableMouse(true)
  blocker:EnableKeyboard(true)
  if blocker.SetPropagateKeyboardInput then
    blocker:SetPropagateKeyboardInput(false)
  end
  _ShareApplyBackdrop(blocker, { 0, 0, 0, 0.55 }, { 0, 0, 0, 0 })
  blocker:Hide()
  blocker:SetScript("OnKeyDown", function(f, key)
    if key == "ESCAPE" then
      f:Hide()
    end
  end)

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  modal.box = box
  box:SetSize(680, 500)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 18)
  _ShareApplyBackdrop(box, theme.bg, theme.border)
  box:EnableMouse(true)

  local header = _ShareCreatePanel(box, theme, theme.panel, theme.border)
  modal.header = header
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(36)

  local title = _ShareCreateLabel(header, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  modal.title = title
  title:SetPoint("LEFT", 14, 0)
  title:SetText("Import Farm Preview")

  local close = CreateFrame("Button", nil, header, "UIPanelCloseButton")
  modal.close = close
  close:SetPoint("RIGHT", -4, 0)
  if widgets and widgets.SkinCloseButton then
    widgets:SkinCloseButton(close)
  end
  close:SetScript("OnClick", function() blocker:Hide() end)

  local body = _ShareCreateLabel(box, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.body = body
  body:SetPoint("TOPLEFT", 14, -48)
  body:SetPoint("TOPRIGHT", -14, -48)
  body:SetJustifyH("LEFT")
  body:SetText("Review the shared farm before importing it.")

  local nameLabel = _ShareCreateLabel(box, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.nameLabel = nameLabel
  nameLabel:SetPoint("TOPLEFT", 14, -76)
  nameLabel:SetText("Save as")

  local inputWrap = CreateFrame("Frame", nil, box, "BackdropTemplate")
  modal.inputWrap = inputWrap
  _ShareApplyBackdrop(inputWrap, theme.panel, theme.border)
  inputWrap:SetPoint("LEFT", nameLabel, "RIGHT", 12, 0)
  inputWrap:SetSize(320, 26)

  local nameInput = CreateFrame("EditBox", nil, inputWrap)
  modal.nameInput = nameInput
  nameInput:SetAutoFocus(false)
  if _G.WF_ChatFontNormal or _G.ChatFontNormal then
    nameInput:SetFontObject(_G.WF_ChatFontNormal or _G.ChatFontNormal)
  end
  nameInput:SetTextInsets(6, 6, 0, 0)
  nameInput:SetPoint("TOPLEFT", 0, 0)
  nameInput:SetPoint("BOTTOMRIGHT", 0, 0)
  nameInput:SetTextColor(theme.text[1], theme.text[2], theme.text[3], 1)
  nameInput:SetScript("OnEscapePressed", function() blocker:Hide() end)

  local previewSection = _ShareCreateSection(box, theme, "PREVIEW")
  modal.previewSection = previewSection
  previewSection:SetPoint("TOPLEFT", 14, -104)
  previewSection:SetPoint("TOPRIGHT", -14, -104)
  previewSection:SetPoint("BOTTOMLEFT", 14, 52)
  previewSection:SetPoint("BOTTOMRIGHT", -14, 52)

  local previewArea = _ShareCreateTextArea(previewSection, theme, 620, 300, true)
  modal.previewArea = previewArea
  previewArea:ClearAllPoints()
  previewArea:SetPoint("TOPLEFT", 12, -32)
  previewArea:SetPoint("TOPRIGHT", -12, -32)
  previewArea:SetPoint("BOTTOMLEFT", 12, 12)
  previewArea:SetPoint("BOTTOMRIGHT", -12, 12)
  previewArea.edit:SetScript("OnMouseUp", function(selfEdit)
    selfEdit:SetFocus()
    if previewArea.ResetScroll then
      previewArea.ResetScroll(true)
    end
  end)

  local bCancel = widgets and widgets.Button and widgets:Button(box, "Cancel", 88, 24) or CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
  modal.cancel = bCancel
  if not (widgets and widgets.Button) then bCancel:SetSize(88, 24) end
  bCancel:SetPoint("BOTTOMRIGHT", -14, 14)

  local bImport = widgets and widgets.Button and widgets:Button(box, "Import", 96, 24) or CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
  modal.import = bImport
  if not (widgets and widgets.Button) then bImport:SetSize(96, 24) end
  bImport:SetPoint("RIGHT", bCancel, "LEFT", -8, 0)

  local function Close()
    blocker:Hide()
    nameInput:ClearFocus()
    modal._farm = nil
    modal._onImport = nil
  end

  blocker:SetScript("OnHide", function()
    nameInput:ClearFocus()
    modal._farm = nil
    modal._onImport = nil
  end)

  bCancel:SetScript("OnClick", Close)
  bImport:SetScript("OnClick", function()
    local farm = modal._farm
    local cb = modal._onImport
    if type(cb) ~= "function" or type(farm) ~= "table" then
      Close()
      return
    end
    local overrideName = Trim(nameInput:GetText() or "")
    Close()
    cb(farm, overrideName)
  end)
  nameInput:SetScript("OnEnterPressed", function() bImport:Click() end)

  function modal:Open(farm, onImport)
    self._farm = farm
    self._onImport = onImport
    title:SetText("Import Farm Preview")
    nameInput:SetText(Trim(farm and farm.name or "Imported Farm"))
    previewArea.edit:SetText(BuildFarmImportPreviewText(farm))
    previewArea.UpdateHeight(previewArea.edit)
    if previewArea.ResetScroll then
      previewArea.ResetScroll(true)
    end
    blocker:Show()
    nameInput:SetFocus()
    nameInput:HighlightText()
  end

  return modal
end

local function EnsureFarmShareModal(self)
  if self._farmShare and self._farmShare.frame then
    return self._farmShare
  end

  local theme = _ShareThemeColors()
  local widgets = WF and WF.UI and WF.UI.Widgets or nil
  local modal = {}
  self._farmShare = modal

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  modal.frame = blocker
  blocker:SetAllPoints(UIParent)
  blocker:SetFrameStrata("DIALOG")
  blocker:EnableMouse(true)
  blocker:EnableKeyboard(true)
  if blocker.SetPropagateKeyboardInput then
    blocker:SetPropagateKeyboardInput(false)
  end
  _ShareApplyBackdrop(blocker, { 0, 0, 0, 0.55 }, { 0, 0, 0, 0 })
  blocker:Hide()
  blocker:SetScript("OnKeyDown", function(f, key)
    if key == "ESCAPE" then
      f:Hide()
    end
  end)

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  modal.box = box
  box:SetSize(760, 500)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 22)
  _ShareApplyBackdrop(box, theme.bg, theme.border)
  box:EnableMouse(true)

  local header = _ShareCreatePanel(box, theme, theme.panel, theme.border)
  modal.header = header
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(36)

  local title = _ShareCreateLabel(header, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  modal.title = title
  title:SetPoint("LEFT", 14, 0)
  title:SetText("Share / Import Farm")

  local close = CreateFrame("Button", nil, header, "UIPanelCloseButton")
  modal.close = close
  close:SetPoint("RIGHT", -4, 0)
  if widgets and widgets.SkinCloseButton then
    widgets:SkinCloseButton(close)
  end
  close:SetScript("OnClick", function() blocker:Hide() end)

  local body = _ShareCreateLabel(box, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.body = body
  body:SetPoint("TOPLEFT", 14, -48)
  body:SetPoint("TOPRIGHT", -14, -48)
  body:SetJustifyH("LEFT")
  body:SetText("Export a farm or paste a shared farm string below.")

  local exportSection = _ShareCreateSection(box, theme, "EXPORT")
  modal.exportSection = exportSection
  exportSection:SetPoint("TOPLEFT", 14, -72)
  exportSection:SetPoint("TOPRIGHT", -14, -72)
  exportSection:SetHeight(180)

  local exportBody = _ShareCreateLabel(exportSection, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.exportBody = exportBody
  exportBody:SetPoint("TOPLEFT", 12, -32)
  exportBody:SetPoint("TOPRIGHT", -12, -32)
  exportBody:SetJustifyH("LEFT")
  exportBody:SetText("Select all and copy this string to share the selected farm.")

  local exportArea = _ShareCreateTextArea(exportSection, theme, 700, 88, true)
  modal.exportArea = exportArea
  exportArea:ClearAllPoints()
  exportArea:SetPoint("TOPLEFT", 12, -58)
  exportArea:SetPoint("TOPRIGHT", -12, -58)
  exportArea:SetPoint("BOTTOMLEFT", 12, 40)
  exportArea:SetPoint("BOTTOMRIGHT", -12, 40)
  exportArea.edit:SetScript("OnMouseUp", function(selfEdit)
    selfEdit:SetFocus()
    if exportArea.ResetScroll then
      exportArea.ResetScroll(true)
    end
    selfEdit:HighlightText()
  end)

  local bExportSelect = widgets and widgets.Button and widgets:Button(exportSection, "Select All", 92, 22) or CreateFrame("Button", nil, exportSection, "UIPanelButtonTemplate")
  modal.exportSelect = bExportSelect
  if not (widgets and widgets.Button) then bExportSelect:SetSize(92, 22) end
  bExportSelect:SetPoint("BOTTOMRIGHT", -12, 10)

  local importSection = _ShareCreateSection(box, theme, "IMPORT")
  modal.importSection = importSection
  importSection:SetPoint("TOPLEFT", exportSection, "BOTTOMLEFT", 0, -10)
  importSection:SetPoint("TOPRIGHT", exportSection, "BOTTOMRIGHT", 0, -10)
  importSection:SetPoint("BOTTOMLEFT", box, "BOTTOMLEFT", 14, 52)
  importSection:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -14, 52)

  local importBody = _ShareCreateLabel(importSection, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  modal.importBody = importBody
  importBody:SetPoint("TOPLEFT", 12, -32)
  importBody:SetPoint("TOPRIGHT", -12, -32)
  importBody:SetJustifyH("LEFT")
  importBody:SetText("Paste a shared farm string here. You'll get a preview before anything is imported.")

  local importArea = _ShareCreateTextArea(importSection, theme, 700, 110, false)
  modal.importArea = importArea
  importArea:ClearAllPoints()
  importArea:SetPoint("TOPLEFT", 12, -58)
  importArea:SetPoint("TOPRIGHT", -12, -58)
  importArea:SetPoint("BOTTOMLEFT", 12, 40)
  importArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local bPreview = widgets and widgets.Button and widgets:Button(importSection, "Preview", 88, 22) or CreateFrame("Button", nil, importSection, "UIPanelButtonTemplate")
  modal.preview = bPreview
  if not (widgets and widgets.Button) then bPreview:SetSize(88, 22) end
  bPreview:SetPoint("BOTTOMRIGHT", -12, 10)

  local bClear = widgets and widgets.Button and widgets:Button(importSection, "Clear", 76, 22) or CreateFrame("Button", nil, importSection, "UIPanelButtonTemplate")
  modal.clear = bClear
  if not (widgets and widgets.Button) then bClear:SetSize(76, 22) end
  bClear:SetPoint("RIGHT", bPreview, "LEFT", -8, 0)

  local bClose = widgets and widgets.Button and widgets:Button(box, "Close", 92, 24) or CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
  modal.closeButton = bClose
  if not (widgets and widgets.Button) then bClose:SetSize(92, 24) end
  bClose:SetPoint("BOTTOMRIGHT", -14, 14)
  bClose:SetScript("OnClick", function() blocker:Hide() end)

  bExportSelect:SetScript("OnClick", function()
    exportArea.edit:SetFocus()
    if exportArea.ResetScroll then
      exportArea.ResetScroll(true)
    end
    exportArea.edit:HighlightText()
  end)

  bClear:SetScript("OnClick", function()
    importArea.edit:SetText("")
    importArea.UpdateHeight(importArea.edit)
    if importArea.ResetScroll then
      importArea.ResetScroll(true)
    end
    importArea.edit:SetFocus()
  end)

  bPreview:SetScript("OnClick", function()
    local raw = importArea.edit:GetText() or ""
    local farm, err = ParseFarmExportString(raw)
    if not farm then
      Notify(err or "That doesn't look like a valid WhatFarm farm string.")
      return
    end
    local preview = EnsureFarmImportPreviewModal(Share)
    preview:Open(farm, function(parsedFarm, overrideName)
      local ok, recOrErr = ImportSharedFarm(parsedFarm, overrideName)
      if not ok then
        Notify(recOrErr or "Farm import failed.")
        return
      end
      blocker:Hide()
    end)
  end)

  function modal:Open(farm, initialImport)
    self._farm = farm
    local exportText = ""
    if type(farm) == "table" then
      exportText = BuildFarmExportString(farm) or ""
    end

    title:SetText("Share / Import Farm")
    body:SetText(type(farm) == "table" and ("Export \"" .. tostring(farm.name or "Farm") .. "\" or paste a shared farm string below.") or "Paste a shared farm string below.")
    exportBody:SetText(type(farm) == "table" and ("Select all and copy this string to share \"" .. tostring(farm.name or "Farm") .. "\".") or "Select a farm first to generate an export string.")

    exportArea.edit:SetText(exportText)
    exportArea.UpdateHeight(exportArea.edit)
    if exportArea.ResetScroll then
      exportArea.ResetScroll(true)
    end

    importArea.edit:SetText(tostring(initialImport or ""))
    importArea.UpdateHeight(importArea.edit)
    if importArea.ResetScroll then
      importArea.ResetScroll(false)
    end

    if bExportSelect.Enable then
      if exportText ~= "" then bExportSelect:Enable() else bExportSelect:Disable() end
    end

    blocker:Show()
    importArea.edit:ClearFocus()
  end

  return modal
end

function Share:CreateFarmExport(farm)
  return BuildFarmExportString(farm)
end

function Share:ShowFarmShareDialog(farm, initialImport)
  local modal = EnsureFarmShareModal(self)
  modal:Open(farm, initialImport)
  return true
end

local function ResolveDecoratedSender(author, event, fallbackSender)
  local sender = CleanPlayerNameText(fallbackSender)
  if sender == "" then
    sender = CleanPlayerNameText(author)
  end
  if event == "CHAT_MSG_WHISPER_INFORM" and sender == "" then
    sender = FullPlayerName()
  end
  if sender == "" then
    sender = FullPlayerName()
  end
  return CleanPlayerNameText(sender)
end

function Share:DecorateChatMessage(msg, author, event, bnSenderID)
  msg = tostring(msg or "")
  if msg == "" or (not msg:find("<wf:", 1, true) and not msg:find("<wfq:", 1, true) and not msg:find("<wfx:", 1, true) and not msg:find("<wfc:", 1, true)) then
    return msg, false
  end

  local changed = false

  msg = msg:gsub("<wfx:([^>]+)>", function(fullBlob)
    local export = UrlDecode(fullBlob or "")
    local sess = select(1, ParseFullExportString(export))
    if not sess then
      return "<wfx:" .. tostring(fullBlob or "") .. ">"
    end

    changed = true
    return BuildFullExportDisplayHyperlink(fullBlob, BuildSessionLabel(sess))
  end)

  msg = msg:gsub("<wfc:([^>]+)>", function(compactBlob)
    local sess = select(1, ParseCompactSessionBlob(compactBlob))
    if not sess then
      return "<wfc:" .. tostring(compactBlob or "") .. ">"
    end

    local sender = CleanPlayerNameText(sess._wfOriginalSharer or "")
    changed = true
    return BuildCompactStandaloneDisplayHyperlink(sender, compactBlob, BuildSessionLabel(sess))
  end)

  msg = msg:gsub("<wfq:([^>]+)>", function(quickBlob)
    local sess, err = ParseQuickSessionBlob(quickBlob)
    if not sess then
      return "<wfq:" .. tostring(quickBlob or "") .. ">"
    end

    changed = true
    return BuildQuickDisplayHyperlink(quickBlob, BuildSessionLabel(sess))
  end)

  msg = msg:gsub("%[WF Run: ([^%]]+)%] <wf:(4):([0-9A-Fa-f]*):([^>]+)>", function(label, version, senderHex, compactBlob)
    if version ~= "4" or tostring(compactBlob or "") == "" then
      return nil
    end

    local embeddedSender = HexDecode(senderHex)
    if type(embeddedSender) ~= "string" then
      embeddedSender = ""
    end

    changed = true
    return BuildCompactStandaloneDisplayHyperlink(embeddedSender, compactBlob, label)
  end)

  msg = msg:gsub("%[WF Run: ([^%]]+)%] <wf:(3):([0-9A-Fa-f]*):([^>]+)>", function(label, version, senderHex, payloadBlob)
    if version ~= "3" then
      return nil
    end

    local embeddedSender = HexDecode(senderHex)
    if type(embeddedSender) ~= "string" then
      embeddedSender = ""
    end

    local payload = UrlDecode(payloadBlob or "")
    if payload == "" then
      return nil
    end

    changed = true
    return BuildStandaloneDisplayHyperlink(embeddedSender, payloadBlob, label)
  end)

  local function DecorateOne(label, token, embeddedSender)
    embeddedSender = CleanPlayerNameText(embeddedSender)

    local routeType = "W"
    local routeValue = ResolveDecoratedSender(author, event, embeddedSender)
    local senderLabel = embeddedSender ~= "" and embeddedSender or routeValue

    if event == "CHAT_MSG_BN_WHISPER" then
      local presenceID = tonumber(bnSenderID) or 0
      if presenceID > 0 then
        routeType = "B"
        routeValue = presenceID
        senderLabel = CleanPlayerNameText(author)
        if senderLabel == "" then
          senderLabel = embeddedSender
        end
      end
    elseif event == "CHAT_MSG_BN_WHISPER_INFORM" then
      senderLabel = embeddedSender ~= "" and embeddedSender or FullPlayerName()
    end

    if routeType ~= "B" and routeValue == "" then
      return nil
    end

    changed = true
    self._routeHints[token] = {
      route = routeType == "B" and "BN_WHISPER" or "WHISPER",
      target = routeType == "B" and (tonumber(routeValue) or 0) or nil,
      sender = senderLabel ~= "" and senderLabel or routeValue,
      updatedAt = time(),
    }
    return BuildDisplayHyperlink(routeType, routeValue, token, label)
  end

  local out = msg:gsub("%[WF Run: ([^%]]+)%] <wf:(2):([0-9A-Fa-f]+):([0-9A-Za-z]+)>", function(label, version, senderHex, token)
    if version ~= "2" or token == "" then
      return nil
    end

    local embeddedSender = HexDecode(senderHex)
    if type(embeddedSender) ~= "string" then
      embeddedSender = ""
    end

    return DecorateOne(label, token, embeddedSender)
  end)

  out = out:gsub("%[WF Run: ([^%]]+)%] <wf:(1):([0-9A-Za-z]+)>", function(label, version, token)
    if version ~= "1" or token == "" then
      return nil
    end
    return DecorateOne(label, token, "")
  end)

  return out, changed
end

local function FormatMoney(copper)
  if WF and WF.Util and WF.Util.FormatMoney then
    return WF.Util:FormatMoney(copper)
  end
  copper = tonumber(copper) or 0
  if copper <= 0 then
    return "0g 0s 0c"
  end
  local g = math.floor(copper / 10000)
  local s = math.floor((copper % 10000) / 100)
  local c = copper % 100
  return string.format("%dg %ds %dc", g, s, c)
end

local function Round(n)
  n = tonumber(n) or 0
  return math.floor(n + 0.5)
end

local function FormatDateTime(ts)
  ts = tonumber(ts) or 0
  if ts <= 0 then
    return "Unknown"
  end
  return date("%b %d, %Y %H:%M", ts)
end

local function FormatDuration(seconds)
  seconds = tonumber(seconds) or 0
  if seconds < 0 then seconds = 0 end

  local h = math.floor(seconds / 3600)
  local m = math.floor((seconds % 3600) / 60)
  local s = math.floor(seconds % 60)

  if h > 0 then
    return string.format("%dh %dm %ds", h, m, s)
  end
  if m > 0 then
    return string.format("%dm %ds", m, s)
  end
  return string.format("%ds", s)
end

CountItems = function(sess)
  local quickUnique = tonumber(sess and sess._wfShareUniqueItems)
  local quickTotal = tonumber(sess and sess._wfShareTotalItems)
  if quickUnique and quickTotal then
    return math.max(0, quickUnique), math.max(0, quickTotal)
  end

  local unique, total = 0, 0
  local items = type(sess) == "table" and type(sess.items) == "table" and sess.items or nil
  if not items then return 0, 0 end

  for _, entry in pairs(items) do
    unique = unique + 1
    total = total + (tonumber(entry and entry.count) or 0)
  end

  return unique, total
end

CountValuableItems = function(sess)
  local quickUnique = tonumber(sess and sess._wfShareValuableUnique)
  local quickTotal = tonumber(sess and sess._wfShareValuableTotal)
  if quickUnique and quickTotal then
    return math.max(0, quickUnique), math.max(0, quickTotal)
  end

  local unique, total = 0, 0
  local items = type(sess) == "table" and type(sess.items) == "table" and sess.items or nil
  if not items then return 0, 0 end

  for _, entry in pairs(items) do
    local value = tonumber(entry and entry.value) or 0
    if value > 0 then
      unique = unique + 1
      total = total + (tonumber(entry and entry.count) or 0)
    end
  end

  return unique, total
end

local function SessionGPH(sess)
  local elapsed = tonumber(sess and (sess.elapsed or sess.duration)) or 0
  local total = tonumber(sess and (sess.totalValue or sess.total or sess.value)) or 0
  if elapsed <= 0 or total <= 0 then
    return 0
  end
  return (total / elapsed) * 3600
end

local function SessionLIV(sess)
  local total = tonumber(sess and (sess.totalValue or sess.total or sess.value)) or 0
  local gold = tonumber(sess and (sess.moneyGained or sess.lootedGoldValue or sess.lootedGold or sess.goldLooted or sess.rawGold or sess.money or sess.gold)) or 0
  local liv = total - gold
  if liv < 0 then liv = 0 end
  return liv, gold, total
end

local function SessionPrimaryLabel(sess)
  local farm = Trim(sess and (sess._wfCustomFarmName or sess.farmName) or "")
  local zone = Trim(sess and sess.zone or "")
  if farm ~= "" then return farm end
  if zone ~= "" then return zone end
  return "Shared Run"
end

local function SessionSecondaryLabel(sess)
  local farm = Trim(sess and (sess._wfCustomFarmName or sess.farmName) or "")
  local zone = Trim(sess and sess.zone or "")
  local mode = Trim(sess and sess.mode or "")
  local parts = {}

  if zone ~= "" and zone ~= farm then
    parts[#parts + 1] = zone
  end
  if mode ~= "" then
    parts[#parts + 1] = mode
  end

  return table.concat(parts, "  •  ")
end

local function BuildSummaryLines(sess, sender)
  local lines = {}
  local startedAt = tonumber(sess and sess.startedAt) or tonumber(sess and sess.endedAt) or 0
  local elapsed = tonumber(sess and (sess.elapsed or sess.duration)) or 0
  local liv, gold, total = SessionLIV(sess)
  local gph = SessionGPH(sess)
  local uniqueItems, totalItems = CountItems(sess)
  local valuableUnique, valuableTotal = CountValuableItems(sess)
  local senderText = CleanPlayerNameText(sender)

  lines[#lines + 1] = "Shared by: " .. (senderText ~= "" and senderText or "Unknown")
  lines[#lines + 1] = "Date: " .. FormatDateTime(startedAt)
  lines[#lines + 1] = "Duration: " .. FormatDuration(elapsed)
  lines[#lines + 1] = "Total value: " .. FormatMoney(Round(total))
  lines[#lines + 1] = "Looted item value: " .. FormatMoney(Round(liv))
  lines[#lines + 1] = "Looted gold: " .. FormatMoney(Round(gold))
  lines[#lines + 1] = "Rate: " .. FormatMoney(Round(gph)) .. "/hr"
  lines[#lines + 1] = "Items looted: " .. tostring(totalItems) .. " (" .. tostring(uniqueItems) .. " types)"
  lines[#lines + 1] = "Valuable items: " .. tostring(valuableTotal) .. " (" .. tostring(valuableUnique) .. " types)"
  lines[#lines + 1] = "This view is shared data only. It is not part of your saved history."

  return table.concat(lines, "\n")
end



local _wfSharedLootQCache = Share._qualityCache or {}
local _wfSharedLootQPending = Share._qualityPending or {}
local _wfSharedLootQWait = Share._qualityWait or {}
Share._qualityCache = _wfSharedLootQCache
Share._qualityPending = _wfSharedLootQPending
Share._qualityWait = _wfSharedLootQWait

local function _ShareSetDefaultItemColor(row)
  if not row or not row.name then return end
  local theme = WF and WF.UI and WF.UI.Theme or nil
  local textColor = (theme and theme.text) or { 0.92, 0.92, 0.92, 1.00 }
  row.name:SetTextColor(textColor[1] or 0.92, textColor[2] or 0.92, textColor[3] or 0.92, textColor[4] or 1.00)
end

local function _ShareSetLootRowQuality(row, tok, quality)
  if type(quality) ~= "number" then return end
  if not row or not row.name then return end
  if row._wfSharedQTok ~= tok then return end

  local r, g, b
  if GetItemQualityColor then
    r, g, b = GetItemQualityColor(quality)
  end

  if r and g and b then
    row.name:SetTextColor(r, g, b, 1)
  else
    _ShareSetDefaultItemColor(row)
  end
end

local function _ShareQueueLootQualityLoad(key, itemID, link)
  if not key or _wfSharedLootQPending[key] then return end
  _wfSharedLootQPending[key] = true

  local function _OnLoaded()
    _wfSharedLootQPending[key] = nil

    local quality = nil
    if type(link) == "string" and link ~= "" and GetItemInfo then
      local _, _, q2 = GetItemInfo(link)
      if type(q2) == "number" then
        quality = q2
      end
    end
    if quality == nil and itemID and C_Item and C_Item.GetItemQualityByID then
      quality = C_Item.GetItemQualityByID(itemID)
    end
    if quality == nil and GetItemInfo then
      local _, _, q2 = GetItemInfo(itemID or key)
      if type(q2) == "number" then
        quality = q2
      end
    end

    if type(quality) == "number" then
      _wfSharedLootQCache[key] = quality
    end

    local wait = _wfSharedLootQWait[key]
    _wfSharedLootQWait[key] = nil
    if not wait then return end

    if type(quality) == "number" then
      for row, waitTok in pairs(wait) do
        _ShareSetLootRowQuality(row, waitTok, quality)
      end
    else
      for row in pairs(wait) do
        _ShareSetDefaultItemColor(row)
      end
    end
  end

  if Item then
    if itemID and Item.CreateFromItemID then
      local itemObj = Item:CreateFromItemID(itemID)
      if itemObj and itemObj.ContinueOnItemLoad then
        itemObj:ContinueOnItemLoad(_OnLoaded)
        return
      end
    end
    if Item.CreateFromItemLink and type(link) == "string" and link ~= "" and link:find("|H", 1, true) then
      local itemObj = Item:CreateFromItemLink(link)
      if itemObj and itemObj.ContinueOnItemLoad then
        itemObj:ContinueOnItemLoad(_OnLoaded)
        return
      end
    end
  end

  if C_Timer and C_Timer.After then
    C_Timer.After(0.15, _OnLoaded)
  else
    _OnLoaded()
  end
end

local function _ShareApplyLootQualityColor(row, link, itemID, qualityOverride)
  if not row or not row.name then return end

  row._wfSharedQTok = (row._wfSharedQTok or 0) + 1
  local tok = row._wfSharedQTok

  local wantsQualityColor = true
  if WF and WF.DB and WF.DB.GetSetting then
    wantsQualityColor = WF.DB:GetSetting("colorByQuality") and true or false
  end

  if not wantsQualityColor then
    _ShareSetDefaultItemColor(row)
    row._wfSharedQKey = nil
    return
  end

  local overrideQuality = tonumber(qualityOverride)
  if overrideQuality then
    row._wfSharedQKey = nil
    _ShareSetLootRowQuality(row, tok, overrideQuality)
    return
  end

  local id = tonumber(itemID)
  if type(link) == "string" and link ~= "" then
    local parsedID = nil
    if GetItemInfoInstant then
      parsedID = select(1, GetItemInfoInstant(link))
    end
    if not parsedID then
      parsedID = tonumber(link:match("item:(%d+)"))
    end
    if parsedID then
      id = tonumber(parsedID)
    end
  end

  local key = id or link
  row._wfSharedQKey = key

  if key and _wfSharedLootQCache[key] ~= nil then
    local cachedQuality = _wfSharedLootQCache[key]
    if type(link) == "string" and link ~= "" then
      local _, _, q2 = GetItemInfo(link)
      if type(q2) == "number" and q2 ~= cachedQuality then
        cachedQuality = q2
        _wfSharedLootQCache[key] = q2
      end
    end
    _ShareSetLootRowQuality(row, tok, cachedQuality)
    return
  end

  local quality = nil
  if type(link) == "string" and link ~= "" then
    local _, _, q2 = GetItemInfo(link)
    if type(q2) == "number" then
      quality = q2
    end
  end
  if quality == nil and id and C_Item and C_Item.GetItemQualityByID then
    quality = C_Item.GetItemQualityByID(id)
  end
  if quality == nil and id and GetItemInfo then
    local _, _, q2 = GetItemInfo(id)
    if type(q2) == "number" then
      quality = q2
    end
  end

  if type(quality) == "number" then
    if key then
      _wfSharedLootQCache[key] = quality
    end
    _ShareSetLootRowQuality(row, tok, quality)
    return
  end

  _ShareSetDefaultItemColor(row)

  if not key then return end

  _wfSharedLootQWait[key] = _wfSharedLootQWait[key] or {}
  _wfSharedLootQWait[key][row] = tok
  _ShareQueueLootQualityLoad(key, id, link)
end

_ShareThemeColors = function()
  local theme = WF and WF.UI and WF.UI.Theme or nil
  local accent = (theme and theme.accent) or { 0.35, 0.78, 1.00, 1.00 }
  local text = (theme and theme.text) or { 0.92, 0.92, 0.92, 1.00 }
  local muted = (theme and theme.muted) or { 0.65, 0.65, 0.65, 1.00 }
  local border = (theme and theme.border) or { 0.18, 0.18, 0.18, 0.90 }

  return {
    bg = { 0.030, 0.030, 0.030, 0.985 },
    panel = { 0.040, 0.040, 0.040, 0.985 },
    panel2 = { 0.055, 0.055, 0.055, 0.985 },
    panel3 = { 0.075, 0.075, 0.075, 0.985 },
    border = { border[1], border[2], border[3], 0.90 },
    text = text,
    muted = muted,
    accent = accent,
    row = { 0.085, 0.085, 0.085, 0.22 },
    rowAlt = { 1.00, 1.00, 1.00, 0.018 },
    rowHover = { accent[1], accent[2], accent[3], 0.08 },
  }
end

local function _ShareColor4(t, fallback)
  if type(t) == "table" then
    return t[1] or 1, t[2] or 1, t[3] or 1, t[4] or 1
  end
  if type(fallback) == "table" then
    return fallback[1] or 1, fallback[2] or 1, fallback[3] or 1, fallback[4] or 1
  end
  return 1, 1, 1, 1
end

_ShareApplyBackdrop = function(frame, bg, border)
  if not frame or not frame.SetBackdrop then return end
  frame:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  frame:SetBackdropColor(_ShareColor4(bg))
  frame:SetBackdropBorderColor(_ShareColor4(border))
end

local function _ShareSetFontObject(fs, preferred, fallback)
  if not fs then return end
  local obj = preferred and _G and _G[preferred] or nil
  if (not obj) and fallback then
    obj = _G and _G[fallback] or nil
  end
  if obj and fs.SetFontObject then
    fs:SetFontObject(obj)
  end
end

local function _ShareSetTextColor(fs, color)
  if not fs then return end
  local r, g, b, a = _ShareColor4(color)
  fs:SetTextColor(r, g, b, a)
end

_ShareCreateLabel = function(parent, preferredFont, fallbackFont, color)
  local fs = parent:CreateFontString(nil, "OVERLAY")
  _ShareSetFontObject(fs, preferredFont, fallbackFont)
  _ShareSetTextColor(fs, color)
  return fs
end

_ShareCreatePanel = function(parent, theme, bg, border)
  local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  _ShareApplyBackdrop(f, bg or theme.panel, border or theme.border)
  return f
end

_ShareCreateSection = function(parent, theme, titleText)
  local panel = _ShareCreatePanel(parent, theme, theme.bg, theme.border)

  panel.header = panel:CreateTexture(nil, "ARTWORK")
  panel.header:SetTexture("Interface/Buttons/WHITE8x8")
  panel.header:SetPoint("TOPLEFT", 1, -1)
  panel.header:SetPoint("TOPRIGHT", -1, -1)
  panel.header:SetHeight(24)
  panel.header:SetVertexColor(theme.panel2[1], theme.panel2[2], theme.panel2[3], 1)

  panel.headerLine = panel:CreateTexture(nil, "OVERLAY")
  panel.headerLine:SetTexture("Interface/Buttons/WHITE8x8")
  panel.headerLine:SetPoint("TOPLEFT", panel.header, "BOTTOMLEFT", 0, 0)
  panel.headerLine:SetPoint("TOPRIGHT", panel.header, "BOTTOMRIGHT", 0, 0)
  panel.headerLine:SetHeight(1)
  panel.headerLine:SetVertexColor(theme.border[1], theme.border[2], theme.border[3], 1)

  panel.accent = panel:CreateTexture(nil, "OVERLAY")
  panel.accent:SetTexture("Interface/Buttons/WHITE8x8")
  panel.accent:SetPoint("BOTTOMLEFT", panel.header, "BOTTOMLEFT", 10, 0)
  panel.accent:SetSize(44, 1)
  panel.accent:SetVertexColor(0, 0, 0, 0)

  panel.title = _ShareCreateLabel(panel, "WF_GameFontNormal", "GameFontNormal", theme.text)
  panel.title:SetPoint("LEFT", panel.header, "LEFT", 10, 0)
  panel.title:SetJustifyH("LEFT")
  panel.title:SetText(titleText or "Section")

  return panel
end

_ShareCreateMetricTile = function(parent, theme, labelText)
  local tile = _ShareCreatePanel(parent, theme, theme.bg, theme.border)

  tile.label = _ShareCreateLabel(tile, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  tile.label:SetPoint("TOPLEFT", 10, -7)
  tile.label:SetPoint("TOPRIGHT", -10, -7)
  tile.label:SetJustifyH("LEFT")
  tile.label:SetText(labelText or "Metric")

  tile.value = _ShareCreateLabel(tile, "WF_GameFontHighlight", "GameFontHighlight", theme.text)
  tile.value:SetPoint("BOTTOMLEFT", 10, 7)
  tile.value:SetPoint("BOTTOMRIGHT", -10, 7)
  tile.value:SetJustifyH("LEFT")
  tile.value:SetText("--")

  return tile
end

_ShareLayoutMetrics = function(viewer)
  if not viewer or not viewer.metricGrid or not viewer.metricTiles then return end

  local grid = viewer.metricGrid
  local width = math.max(1, grid:GetWidth() or 1)
  local gap = 8
  local cols = 4
  if width < 720 then
    cols = 3
  end
  if width < 560 then
    cols = 2
  end

  local tileWidth = math.floor((width - ((cols - 1) * gap)) / cols)
  if tileWidth < 120 then tileWidth = 120 end
  local tileHeight = 42

  local total = #viewer.metricTiles
  local rows = math.max(1, math.ceil(total / cols))

  for i = 1, total do
    local tile = viewer.metricTiles[i]
    local row = math.floor((i - 1) / cols)
    local col = (i - 1) % cols
    tile:ClearAllPoints()
    tile:SetSize(tileWidth, tileHeight)
    tile:SetPoint("TOPLEFT", grid, "TOPLEFT", col * (tileWidth + gap), -(row * (tileHeight + gap)))
  end

  grid:SetHeight((rows * tileHeight) + ((rows - 1) * gap))
end

local function _ShareLayoutLootColumns(viewer)
  if not viewer or not viewer.lootPanel then return end

  if viewer.scroll and viewer.content and viewer.content.SetWidth then
    local contentWidth = math.max(1, (viewer.scroll:GetWidth() or 1))
    viewer.content:SetWidth(contentWidth)
  end

  local width = math.max(320, viewer.lootPanel:GetWidth() or 320)
  local valueW = 118
  local itemValueW = 108
  local countW = 62
  local rightPad = 12
  local iconLeft = 12
  local iconSize = 18
  local nameLeft = iconLeft + iconSize + 10
  local headerAnchor = viewer.columnBar or viewer.lootPanel

  viewer._lootLayout = viewer._lootLayout or {}
  viewer._lootLayout.valueW = valueW
  viewer._lootLayout.itemValueW = itemValueW
  viewer._lootLayout.countW = countW
  viewer._lootLayout.rightPad = rightPad
  viewer._lootLayout.iconLeft = iconLeft
  viewer._lootLayout.nameLeft = nameLeft

  if viewer.columnBar then
    viewer.columnBar:ClearAllPoints()
    viewer.columnBar:SetPoint("TOPLEFT", viewer.lootPanel, "TOPLEFT", 8, -30)
    viewer.columnBar:SetPoint("TOPRIGHT", viewer.lootPanel, "TOPRIGHT", -28, -30)
    viewer.columnBar:SetHeight(22)
  end

  if viewer.colValue then
    viewer.colValue:ClearAllPoints()
    viewer.colValue:SetPoint("RIGHT", headerAnchor, "RIGHT", -rightPad, 0)
    viewer.colValue:SetWidth(valueW)
  end

  if viewer.colItemValue then
    viewer.colItemValue:ClearAllPoints()
    viewer.colItemValue:SetPoint("RIGHT", viewer.colValue, "LEFT", -14, 0)
    viewer.colItemValue:SetWidth(itemValueW)
  end

  if viewer.colCount then
    viewer.colCount:ClearAllPoints()
    viewer.colCount:SetPoint("RIGHT", viewer.colItemValue, "LEFT", -14, 0)
    viewer.colCount:SetWidth(countW)
  end

  if viewer.colName then
    viewer.colName:ClearAllPoints()
    viewer.colName:SetPoint("LEFT", headerAnchor, "LEFT", nameLeft, 0)
    viewer.colName:SetPoint("RIGHT", viewer.colCount, "LEFT", -14, 0)
  end

  for i = 1, #(viewer.rows or {}) do
    local row = viewer.rows[i]
    if row and row.name and row.count and row.itemValue and row.value and row.icon then
      row.icon:ClearAllPoints()
      row.icon:SetPoint("LEFT", iconLeft, 0)

      row.value:ClearAllPoints()
      row.value:SetPoint("RIGHT", row, "RIGHT", -rightPad, 0)
      row.value:SetWidth(valueW)

      row.itemValue:ClearAllPoints()
      row.itemValue:SetPoint("RIGHT", row.value, "LEFT", -14, 0)
      row.itemValue:SetWidth(itemValueW)

      row.count:ClearAllPoints()
      row.count:SetPoint("RIGHT", row.itemValue, "LEFT", -14, 0)
      row.count:SetWidth(countW)

      row.name:ClearAllPoints()
      row.name:SetPoint("LEFT", row.icon, "RIGHT", 10, 0)
      row.name:SetPoint("RIGHT", row.count, "LEFT", -14, 0)
    end
  end
end

local function _ShareGetViewerBounds(defaultW, defaultH)
  local minW, minH = 760, 500
  local maxW, maxH = 1600, 1000

  if UIParent and UIParent.GetWidth and UIParent.GetHeight then
    local pw = UIParent:GetWidth() or maxW
    local ph = UIParent:GetHeight() or maxH
    maxW = math.max(minW, math.floor(pw - 40))
    maxH = math.max(minH, math.floor(ph - 40))
  end

  local w = tonumber(defaultW) or 900
  local h = tonumber(defaultH) or 640
  w = math.min(math.max(w, minW), maxW)
  h = math.min(math.max(h, minH), maxH)

  return w, h, minW, minH, maxW, maxH
end

local function _SharePersistViewerSize(frame)
  if not (frame and WF and WF.DB and WF.DB.SetSetting) then return end
  local w, h = frame:GetSize()
  if not (w and h) then return end
  WF.DB:SetSetting(SHARED_VIEWER_WIDTH_KEY, math.floor((w or 0) + 0.5))
  WF.DB:SetSetting(SHARED_VIEWER_HEIGHT_KEY, math.floor((h or 0) + 0.5))
end

local function _ShareIsViewerLocked()
  if WF and WF.UI and WF.UI.Widgets and WF.UI.Widgets.IsLocked then
    return WF.UI.Widgets:IsLocked(SHARED_VIEWER_LOCK_KEY) and true or false
  end
  if WF and WF.DB and WF.DB.GetSetting then
    return WF.DB:GetSetting(SHARED_VIEWER_LOCK_KEY) and true or false
  end
  return false
end

local function _ShareClaimUtilityWindowFocus(frame)
  local focus = WF and WF.UI and WF.UI.UtilityWindowFocus or nil
  if focus and focus.Claim then
    focus:Claim("sharedRunViewer", frame)
    return
  end

  if frame and frame.Raise then
    frame:Raise()
  end
end

local function EnsureSharedRunViewer(self)
  if self._viewer and self._viewer.frame then
    if self._viewer.shareBtn then
      self._viewer.shareBtn:SetScript("OnClick", nil)
      self._viewer.shareBtn:Hide()
      self._viewer.shareBtn = nil
    end
    return self._viewer
  end

  local theme = _ShareThemeColors()
  local viewer = {}
  local frame = CreateFrame("Frame", SHARED_VIEWER_FRAME_NAME, UIParent, "BackdropTemplate")
  viewer.frame = frame
  self._viewer = viewer

  do
    local savedW = WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting(SHARED_VIEWER_WIDTH_KEY) or nil
    local savedH = WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting(SHARED_VIEWER_HEIGHT_KEY) or nil
    local w, h, minW, minH, maxW, maxH = _ShareGetViewerBounds(savedW or 900, savedH or 640)
    frame:SetSize(w, h)
    frame:SetResizable(true)
    if frame.SetResizeBounds then
      frame:SetResizeBounds(minW, minH, maxW, maxH)
    else
      if frame.SetMinResize then
        frame:SetMinResize(minW, minH)
      end
      if frame.SetMaxResize then
        frame:SetMaxResize(maxW, maxH)
      end
    end
  end
  frame:SetPoint("CENTER")
  frame:SetFrameStrata("DIALOG")
  frame:SetClampedToScreen(true)
  frame:EnableMouse(true)
  if frame.HookScript then
    frame:HookScript("OnMouseDown", function(f)
      _ShareClaimUtilityWindowFocus(f)
    end)
  end
  frame:SetMovable(true)
  frame:RegisterForDrag("LeftButton")
  frame:SetScript("OnDragStart", function(f)
    if _ShareIsViewerLocked() then
      return
    end
    _ShareClaimUtilityWindowFocus(f)
    f:StartMoving()
  end)
  frame:SetScript("OnDragStop", function(f)
    f:StopMovingOrSizing()
  end)
  _ShareApplyBackdrop(frame, theme.bg, theme.border)
  frame:Hide()

  if type(UISpecialFrames) == "table" then
    local exists = false
    for i = 1, #UISpecialFrames do
      if UISpecialFrames[i] == SHARED_VIEWER_FRAME_NAME then
        exists = true
        break
      end
    end
    if not exists then
      table.insert(UISpecialFrames, SHARED_VIEWER_FRAME_NAME)
    end
  end

  frame:EnableKeyboard(true)
  if frame.SetPropagateKeyboardInput then
    frame:SetPropagateKeyboardInput(true)
  end
  frame:SetScript("OnKeyDown", function(self, key)
    if key == "ESCAPE" then
      if GameTooltip then
        GameTooltip:Hide()
      end
      self:Hide()
    end
  end)
  frame:SetScript("OnHide", function()
    if GameTooltip then
      GameTooltip:Hide()
    end
  end)

  frame.shadow = frame:CreateTexture(nil, "BACKGROUND")
  frame.shadow:SetTexture("Interface/Buttons/WHITE8x8")
  frame.shadow:SetPoint("TOPLEFT", -8, 8)
  frame.shadow:SetPoint("BOTTOMRIGHT", 8, -8)
  frame.shadow:SetVertexColor(0, 0, 0, 0.34)

  local header = _ShareCreatePanel(frame, theme, theme.panel, theme.border)
  viewer.header = header
  header:SetPoint("TOPLEFT", 0, 0)
  header:SetPoint("TOPRIGHT", 0, 0)
  header:SetHeight(36)

  header.topShade = header:CreateTexture(nil, "BACKGROUND")
  header.topShade:SetTexture("Interface/Buttons/WHITE8x8")
  header.topShade:SetPoint("TOPLEFT", 1, -1)
  header.topShade:SetPoint("TOPRIGHT", -1, -1)
  header.topShade:SetHeight(14)
  header.topShade:SetVertexColor(theme.panel2[1], theme.panel2[2], theme.panel2[3], 0.90)

  header.accent = header:CreateTexture(nil, "ARTWORK")
  header.accent:SetTexture("Interface/Buttons/WHITE8x8")
  header.accent:SetPoint("BOTTOMLEFT", 1, 0)
  header.accent:SetPoint("BOTTOMRIGHT", -1, 0)
  header.accent:SetHeight(1)
  header.accent:SetVertexColor(0, 0, 0, 0)

  local brand = _ShareCreateLabel(header, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  viewer.brand = brand
  brand:SetPoint("LEFT", 14, 1)
  brand:SetText("WHAT FARM?")

  local modeText = _ShareCreateLabel(header, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.modeText = modeText
  modeText:SetPoint("LEFT", brand, "RIGHT", 10, 0)
  modeText:SetText("Shared Run Viewer")

  local close = CreateFrame("Button", nil, header, "UIPanelCloseButton")
  viewer.close = close
  close:SetPoint("RIGHT", -4, 0)
  if WF and WF.UI and WF.UI.Widgets and WF.UI.Widgets.SkinCloseButton then
    WF.UI.Widgets:SkinCloseButton(close)
  end
  close:SetScript("OnClick", function()
    if GameTooltip then
      GameTooltip:Hide()
    end
    frame:Hide()
  end)


  local hero = _ShareCreateSection(frame, theme, "RUN DETAILS")
  viewer.hero = hero
  hero:SetPoint("TOPLEFT", 12, -46)
  hero:SetPoint("TOPRIGHT", -12, -46)
  hero:SetHeight(88)

  local runTitle = _ShareCreateLabel(hero, "WF_GameFontNormalLarge", "GameFontNormalLarge", theme.text)
  viewer.runTitle = runTitle
  runTitle:SetPoint("TOPLEFT", 12, -33)
  runTitle:SetPoint("TOPRIGHT", -12, -33)
  runTitle:SetJustifyH("LEFT")
  runTitle:SetText("Shared Run")

  local runSubtitle = _ShareCreateLabel(hero, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  viewer.runSubtitle = runSubtitle
  runSubtitle:SetPoint("TOPLEFT", runTitle, "BOTTOMLEFT", 0, -4)
  runSubtitle:SetPoint("TOPRIGHT", -12, -58)
  runSubtitle:SetJustifyH("LEFT")

  local byline = _ShareCreateLabel(hero, "WF_GameFontHighlightSmall", "GameFontHighlightSmall", theme.accent)
  viewer.byline = byline
  byline:SetPoint("TOPRIGHT", -12, -34)
  byline:SetJustifyH("RIGHT")
  byline:SetText("")

  local historyNote = _ShareCreateLabel(hero, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.historyNote = historyNote
  historyNote:SetPoint("TOPRIGHT", -12, -54)
  historyNote:SetJustifyH("RIGHT")
  historyNote:SetText("Shared data only · not saved to your history")

  local metricGrid = CreateFrame("Frame", nil, frame)
  viewer.metricGrid = metricGrid
  metricGrid:SetPoint("TOPLEFT", hero, "BOTTOMLEFT", 0, -10)
  metricGrid:SetPoint("TOPRIGHT", hero, "BOTTOMRIGHT", 0, -10)
  metricGrid:SetHeight(92)

  viewer.metricTiles = {
    _ShareCreateMetricTile(metricGrid, theme, "Date"),
    _ShareCreateMetricTile(metricGrid, theme, "Duration"),
    _ShareCreateMetricTile(metricGrid, theme, "GPH"),
    _ShareCreateMetricTile(metricGrid, theme, "Total Value"),
    _ShareCreateMetricTile(metricGrid, theme, "Item Value"),
    _ShareCreateMetricTile(metricGrid, theme, "Looted Gold"),
    _ShareCreateMetricTile(metricGrid, theme, "Items"),
    _ShareCreateMetricTile(metricGrid, theme, "Valuable Items"),
  }

  local lootPanel = _ShareCreateSection(frame, theme, "LOOT")
  viewer.lootPanel = lootPanel
  lootPanel:SetPoint("TOPLEFT", metricGrid, "BOTTOMLEFT", 0, -10)
  lootPanel:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 12)

  local columnBar = CreateFrame("Frame", nil, lootPanel, "BackdropTemplate")
  viewer.columnBar = columnBar
  _ShareApplyBackdrop(columnBar, theme.panel, theme.border)

  local colName = _ShareCreateLabel(columnBar, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.colName = colName
  colName:SetJustifyH("LEFT")
  colName:SetText("Item")

  local colCount = _ShareCreateLabel(columnBar, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.colCount = colCount
  colCount:SetJustifyH("RIGHT")
  colCount:SetText("Count")

  local colItemValue = _ShareCreateLabel(columnBar, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.colItemValue = colItemValue
  colItemValue:SetJustifyH("RIGHT")
  colItemValue:SetText("Item Value")

  local colValue = _ShareCreateLabel(columnBar, "WF_GameFontDisableSmall", "GameFontDisableSmall", theme.muted)
  viewer.colValue = colValue
  colValue:SetJustifyH("RIGHT")
  colValue:SetText("Value")

  local scroll = CreateFrame("ScrollFrame", nil, lootPanel, "UIPanelScrollFrameTemplate")
  viewer.scroll = scroll
  scroll:SetPoint("TOPLEFT", lootPanel, "TOPLEFT", 8, -56)
  scroll:SetPoint("BOTTOMRIGHT", -28, 8)
  if WF and WF.UI and WF.UI.Widgets and WF.UI.Widgets.SkinScrollBar and scroll.ScrollBar then
    WF.UI.Widgets:SkinScrollBar(scroll.ScrollBar)
  end

  local content = CreateFrame("Frame", nil, scroll)
  viewer.content = content
  content:SetSize(1, 1)
  scroll:SetScrollChild(content)

  local empty = _ShareCreateLabel(content, "WF_GameFontDisable", "GameFontDisable", theme.muted)
  viewer.empty = empty
  empty:SetPoint("CENTER", 0, 0)
  empty:SetWidth(420)
  empty:SetJustifyH("CENTER")
  empty:SetText("No loot data was included with this shared run.")

  viewer.rows = {}

  local function Relayout()
    _ShareLayoutMetrics(viewer)
    _ShareLayoutLootColumns(viewer)
  end

  frame:HookScript("OnShow", function(f)
    _ShareClaimUtilityWindowFocus(f)
    Relayout()
  end)
  frame:HookScript("OnSizeChanged", Relayout)
  frame:HookScript("OnSizeChanged", function(f, w, h)
    local newW, newH, minW, minH, maxW, maxH = _ShareGetViewerBounds(w, h)
    if (math.abs((newW or 0) - (w or 0)) > 0.5) or (math.abs((newH or 0) - (h or 0)) > 0.5) then
      if not f._wfSharedViewerApplyingSize then
        f._wfSharedViewerApplyingSize = true
        f:SetSize(newW, newH)
        f._wfSharedViewerApplyingSize = nil
      end
      return
    end

    if f._wfSharedViewerApplyingSize then
      return
    end

    _SharePersistViewerSize(f)
  end)
  metricGrid:HookScript("OnSizeChanged", function() _ShareLayoutMetrics(viewer) end)
  lootPanel:HookScript("OnSizeChanged", function() _ShareLayoutLootColumns(viewer) end)

  do
    local widgets = WF and WF.UI and WF.UI.Widgets or nil
    if widgets and widgets.AttachCornerLockGrip then
      viewer.resizeGrip = widgets:AttachCornerLockGrip(frame, {
        lockKey = SHARED_VIEWER_LOCK_KEY,
        size = 16,
        x = -8,
        y = 8,
        alpha = 0.60,
        onResizeStop = function(f)
          _SharePersistViewerSize(f)
        end,
      })
    end
  end

  return viewer
end

local function EnsureSharedLootRow(viewer, index)
  local rows = viewer.rows
  local row = rows[index]
  if row then
    return row
  end

  local theme = _ShareThemeColors()

  row = CreateFrame("Button", nil, viewer.content, "BackdropTemplate")
  rows[index] = row
  row:SetHeight(26)
  row:SetPoint("LEFT", 0, 0)
  row:SetPoint("RIGHT", 0, 0)
  _ShareApplyBackdrop(row, theme.row, { theme.border[1], theme.border[2], theme.border[3], 0 })

  row.alt = row:CreateTexture(nil, "BACKGROUND")
  row.alt:SetTexture("Interface/Buttons/WHITE8x8")
  row.alt:SetAllPoints()
  row.alt:SetVertexColor(theme.rowAlt[1], theme.rowAlt[2], theme.rowAlt[3], theme.rowAlt[4])

  row.hover = row:CreateTexture(nil, "ARTWORK")
  row.hover:SetTexture("Interface/Buttons/WHITE8x8")
  row.hover:SetPoint("TOPLEFT", 1, -1)
  row.hover:SetPoint("BOTTOMRIGHT", -1, 1)
  row.hover:SetVertexColor(theme.rowHover[1], theme.rowHover[2], theme.rowHover[3], theme.rowHover[4])
  row.hover:Hide()

  row.icon = row:CreateTexture(nil, "ARTWORK")
  row.icon:SetSize(18, 18)

  row.name = _ShareCreateLabel(row, "WF_GameFontHighlight", "GameFontHighlight", theme.text)
  row.name:SetJustifyH("LEFT")
  row.name:SetWordWrap(false)

  row.count = _ShareCreateLabel(row, "WF_GameFontNormalSmall", "GameFontNormalSmall", theme.text)
  row.count:SetJustifyH("RIGHT")

  row.itemValue = _ShareCreateLabel(row, "WF_GameFontNormalSmall", "GameFontNormalSmall", theme.muted)
  row.itemValue:SetJustifyH("RIGHT")

  row.value = _ShareCreateLabel(row, "WF_GameFontNormalSmall", "GameFontNormalSmall", theme.accent)
  row.value:SetJustifyH("RIGHT")

  row:SetScript("OnEnter", function(self)
    self.hover:Show()
    if not GameTooltip then return end
    local Widgets = WF and WF.UI and WF.UI.Widgets
    if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    local link = nil
    if self._link and self._link ~= "" then
      link = self._link
    elseif self._itemID and self._itemID > 0 then
      link = "item:" .. tostring(self._itemID)
    end
    local okLink = false
    if link and WF and WF.Util and WF.Util.TrySetHyperlinkCached then
      okLink = WF.Util:TrySetHyperlinkCached(GameTooltip, link)
    elseif link and GameTooltip.SetHyperlink then
      okLink = pcall(GameTooltip.SetHyperlink, GameTooltip, link)
    end
    if not okLink then
      GameTooltip:ClearLines()
      GameTooltip:SetText((self.name and self.name.GetText and self.name:GetText()) or "Item", 1, 1, 1, true)
      GameTooltip:AddLine("Full tooltip unavailable", 0.70, 0.70, 0.70, true)
    end
    GameTooltip:Show()
  end)

  row:SetScript("OnLeave", function(self)
    self.hover:Hide()
    if GameTooltip then
      GameTooltip:Hide()
    end
  end)

  _ShareLayoutLootColumns(viewer)
  return row
end

function Share:ShowSharedSession(sess, sender)
  if type(sess) ~= "table" then return end

  local viewer = EnsureSharedRunViewer(self)
  local frame = viewer.frame
  local liv, gold, total = SessionLIV(sess)
  local gph = SessionGPH(sess)
  local uniqueItems, totalItems = CountItems(sess)
  local valuableUnique, valuableTotal = CountValuableItems(sess)
  local startedAt = tonumber(sess.startedAt) or tonumber(sess.endedAt) or 0
  local senderText = CleanPlayerNameText(sender)

  viewer._currentSess = sess
  viewer._currentSender = senderText

  viewer.byline:SetText((senderText ~= "" and senderText or "Unknown") .. " shared this run")
  viewer.runTitle:SetText(SessionPrimaryLabel(sess))

  if sess._wfQuickSummary then
    viewer.historyNote:SetText("Quick share summary · not saved to your history")
  else
    viewer.historyNote:SetText("Shared data only · not saved to your history")
  end

  local secondary = SessionSecondaryLabel(sess)
  if secondary ~= "" then
    viewer.runSubtitle:SetText(secondary)
    viewer.runSubtitle:Show()
  else
    viewer.runSubtitle:SetText("Shared data only")
    viewer.runSubtitle:Show()
  end

  viewer.metricTiles[1].value:SetText(FormatDateTime(startedAt))
  viewer.metricTiles[2].value:SetText(FormatDuration(tonumber(sess.elapsed or sess.duration) or 0))
  viewer.metricTiles[3].value:SetText(FormatMoney(Round(gph)))
  viewer.metricTiles[4].value:SetText(FormatMoney(Round(total)))
  viewer.metricTiles[5].value:SetText(FormatMoney(Round(liv)))
  viewer.metricTiles[6].value:SetText(FormatMoney(Round(gold)))
  viewer.metricTiles[7].value:SetText(tostring(totalItems) .. " · " .. tostring(uniqueItems) .. " types")
  viewer.metricTiles[8].value:SetText(tostring(valuableTotal) .. " · " .. tostring(valuableUnique) .. " types")

  local items = {}
  if type(sess.items) == "table" then
    for itemID, entry in pairs(sess.items) do
      local id = tonumber(itemID) or tonumber(entry and entry.itemID) or 0
      if id > 0 then
        items[#items + 1] = {
          itemID = id,
          count = tonumber(entry and entry.count) or 0,
          value = tonumber(entry and entry.value) or 0,
          link = tostring(entry and entry.link or ""),
          name = tostring(entry and entry.name or ""),
          icon = entry and entry.icon or nil,
          quality = ResolveItemQuality(entry, id, entry and entry.link or nil),
        }
      end
    end
  end

  table.sort(items, function(a, b)
    if a.value ~= b.value then
      return a.value > b.value
    end
    if a.count ~= b.count then
      return a.count > b.count
    end
    return a.itemID < b.itemID
  end)

  for i = 1, #viewer.rows do
    viewer.rows[i]:Hide()
  end

  if #items == 0 then
    viewer.empty:Show()
    viewer.content:SetHeight(50)
  else
    viewer.empty:Hide()
    for i = 1, #items do
      local item = items[i]
      local row = EnsureSharedLootRow(viewer, i)
      row:ClearAllPoints()
      row:SetPoint("TOPLEFT", 0, -((i - 1) * 28))
      row:SetPoint("TOPRIGHT", 0, -((i - 1) * 28))
      row._itemID = item.itemID
      row._link = item.link
      row.icon:SetTexture(item.icon or GetItemIcon(item.itemID) or 134400)
      row.alt:SetShown((i % 2) == 0)

      local name = item.name
      if name == "" and item.link ~= "" then
        name = item.link:match("%[(.-)%]") or ""
      end
      if name == "" and GetItemInfo then
        name = GetItemInfo(item.link ~= "" and item.link or item.itemID) or ""
      end
      if name == "" then
        name = "Item " .. tostring(item.itemID)
      end

      local count = tonumber(item.count) or 0
      if count < 0 then count = 0 end
      local unitValue = 0
      if count > 0 then
        unitValue = (tonumber(item.value) or 0) / count
      else
        unitValue = tonumber(item.value) or 0
      end

      row.name:SetText(name)
      _ShareApplyLootQualityColor(row, item.link, item.itemID, item.quality)
      row.count:SetText("x" .. tostring(count))
      row.itemValue:SetText(FormatMoney(Round(unitValue)))
      row.value:SetText(FormatMoney(Round(item.value)))
      row:Show()
    end
    viewer.content:SetHeight(#items * 28)
  end

  _ShareLayoutMetrics(viewer)
  _ShareLayoutLootColumns(viewer)

  if viewer.scroll and viewer.scroll.SetVerticalScroll then
    viewer.scroll:SetVerticalScroll(0)
  end

  frame:Show()
  _ShareClaimUtilityWindowFocus(frame)
end

function Share:_OpenSharedSession(sess, sender)
  if type(sess) ~= "table" then return end
  sender = CleanPlayerNameText(sender)
  sess._wfSharedFrom = sender
  sess._wfCustomFarmName = sess._wfCustomFarmName or sess.farmName

  if InCombatLockdown and InCombatLockdown() then
    Notify("Shared run will open after combat ends.")
    local waitFrame = CreateFrame("Frame")
    waitFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    local selfRef = self
    waitFrame:SetScript("OnEvent", function(f)
      f:UnregisterAllEvents()
      selfRef:ShowSharedSession(sess, sender)
    end)
    return
  end

  self:ShowSharedSession(sess, sender)
end

function Share:_RequestRun(sender, token, route, target)
  sender = CleanPlayerNameText(sender)
  token = Trim(token)
  route = tostring(route or "WHISPER")

  if token == "" then return false end
  if route == "BN_WHISPER" then
    target = tonumber(target) or 0
    if target <= 0 then return false end
  elseif sender == "" then
    return false
  end

  CleanupPending(self)

  self._pending[token] = {
    sender = sender,
    startedAt = time(),
    total = 0,
    chunks = {},
    route = route,
    target = target,
  }

  local ok
  if route == "BN_WHISPER" then
    ok = SendAddon(PREFIX, "REQ|" .. token, "BN_WHISPER", target)
  else
    ok = SendAddon(PREFIX, "REQ|" .. token, "WHISPER", sender)
  end

  if ok then
    Notify("Requesting shared run from " .. (sender ~= "" and sender or "your Battle.net friend") .. "...")
    return true
  end

  Notify("Your client couldn't send the share request.")
  self._pending[token] = nil
  return false
end

function Share:HandleLink(link, text, button, chatFrame)
  CleanupPending(self)
  if type(link) ~= "string" then return false end

  local fullBlob = link:match("^whatfarmrun:6:(.+)$")
  if fullBlob then
    local export6 = UrlDecode(fullBlob)
    local sess6, sender6, err6 = ParseFullExportString(export6)
    if not sess6 then
      Notify("That shared run couldn't be opened: " .. tostring(err6 or "Invalid payload"))
      return true
    end

    sender6 = CleanPlayerNameText(sender6 or "")
    if sender6 == "" then
      sender6 = "Shared Export"
    end

    self:_OpenSharedSession(sess6, sender6)
    return true
  end

  local quickBlob = link:match("^whatfarmrun:5:(.+)$")
  if quickBlob then
    local sess5, err5 = ParseQuickSessionBlob(quickBlob)
    if not sess5 then
      Notify("That quick share couldn't be opened: " .. tostring(err5 or "Invalid payload"))
      return true
    end

    local openedFrom5 = CleanPlayerNameText(sess5._wfOriginalSharer or "")
    if openedFrom5 == "" then
      openedFrom5 = "Shared Export"
    end

    self:_OpenSharedSession(sess5, openedFrom5)
    return true
  end

  local version4, senderHex4, compactBlob = link:match("^whatfarmrun:(%d+):([0-9A-Fa-f]*):(.+)$")
  if version4 == "4" then
    local sender4 = HexDecode(senderHex4)
    if type(sender4) ~= "string" then
      sender4 = ""
    end
    sender4 = CleanPlayerNameText(sender4)

    local sess4, err4 = ParseCompactSessionBlob(compactBlob)
    if not sess4 then
      Notify("That shared run couldn't be opened: " .. tostring(err4 or "Invalid payload"))
      return true
    end

    local openedFrom4 = CleanPlayerNameText(sess4._wfOriginalSharer or "")
    if openedFrom4 == "" then
      openedFrom4 = sender4 ~= "" and sender4 or "Shared Export"
    end

    self:_OpenSharedSession(sess4, openedFrom4)
    return true
  end

  local version3, senderHex3, payloadBlob3 = link:match("^whatfarmrun:(%d+):([0-9A-Fa-f]*):(.+)$")
  if version3 == "3" then
    local sender3 = HexDecode(senderHex3)
    if type(sender3) ~= "string" then
      sender3 = ""
    end
    sender3 = CleanPlayerNameText(sender3)

    local payload3 = UrlDecode(payloadBlob3 or "")
    local raw3 = DecodePayload(payload3)
    if not raw3 then
      Notify("That shared run couldn't be decoded.")
      return true
    end

    local sess3, err3 = ParseRawSession(raw3)
    if not sess3 then
      Notify("That shared run couldn't be opened: " .. tostring(err3 or "Invalid payload"))
      return true
    end

    self:_OpenSharedSession(sess3, sender3 ~= "" and sender3 or "Shared Export")
    return true
  end

  local version2, routeType, routeValue, token = link:match("^whatfarmrun:(%d+):([WB]):([^:]+):(.+)$")
  local sender = nil
  local presenceID = 0

  if version2 then
    token = Trim(token)
    if routeType == "B" then
      presenceID = tonumber(routeValue) or 0
      if presenceID <= 0 then
        Notify("That shared run link is invalid.")
        return true
      end
    else
      sender = CleanPlayerNameText(HexDecode(routeValue))
      if type(sender) ~= "string" or sender == "" then
        Notify("That shared run link is invalid.")
        return true
      end
    end
  else
    local version1, senderHex, legacyToken = link:match("^whatfarmrun:(%d+):([0-9A-Fa-f]*):(.+)$")
    if not version1 or not senderHex or not legacyToken then
      return false
    end
    sender = CleanPlayerNameText(HexDecode(senderHex))
    token = Trim(legacyToken)
    routeType = "W"
    if type(sender) ~= "string" or sender == "" then
      Notify("That shared run link is invalid.")
      return true
    end
  end

  local hint = self._routeHints and self._routeHints[token] or nil
  local me = FullPlayerName()
  local rec = self._cache[token]
  if rec and NormalizeName(rec.sender or "") == NormalizeName(me) then
    local raw = DecodePayload(rec.payload)
    local sess, err = ParseRawSession(raw)
    if not sess then
      Notify("Couldn't open your shared run: " .. tostring(err or "Invalid payload"))
      return true
    end
    self:_OpenSharedSession(sess, me)
    return true
  end

  local pending = self._pending[token]
  if pending then
    if routeType == "B" then
      if PendingSenderMatches(pending, nil, presenceID, "BN_WHISPER") then
        Notify("That shared run is already being requested...")
        return true
      end
    elseif PendingSenderMatches(pending, sender, nil, "WHISPER") then
      Notify("That shared run is already being requested...")
      return true
    end
  end

  if routeType == "B" then
    local senderDisplay = hint and Trim(hint.sender) or ""
    if senderDisplay == "" then
      senderDisplay = "your Battle.net friend"
    end
    return self:_RequestRun(senderDisplay, token, "BN_WHISPER", presenceID)
  end

  return self:_RequestRun(sender, token, "WHISPER", sender)
end

function Share:_SendRunTo(target, token, route)
  route = tostring(route or "WHISPER")
  token = Trim(token)

  if route == "BN_WHISPER" then
    target = tonumber(target) or 0
    if target <= 0 or token == "" then return end
  else
    target = Trim(target)
    if target == "" or token == "" then return end
  end

  CleanupCache(self)

  local rec = self._cache[token]
  if not rec or type(rec.payload) ~= "string" or rec.payload == "" then
    SendAddon(PREFIX, "MISS|" .. token, route, target)
    return
  end

  local total = math.max(1, math.ceil(#rec.payload / CHUNK_SIZE))
  if total > MAX_CHUNKS then
    SendAddon(PREFIX, "MISS|" .. token, route, target)
    return
  end
  SendAddon(PREFIX, "BEGIN|" .. token .. "|" .. tostring(total), route, target)
  for i = 1, total do
    local a = ((i - 1) * CHUNK_SIZE) + 1
    local b = math.min(i * CHUNK_SIZE, #rec.payload)
    local piece = rec.payload:sub(a, b)
    SendAddon(PREFIX, "CHUNK|" .. token .. "|" .. tostring(i) .. "|" .. piece, route, target)
  end
  SendAddon(PREFIX, "END|" .. token, route, target)
end

function Share:_HandleAddonMessage(msg, channel, sender)
  CleanupPending(self)
  if type(msg) ~= "string" or msg == "" then return end
  channel = tostring(channel or "WHISPER")
  local cmd, rest = msg:match("^([^|]+)|?(.*)$")
  if cmd == "REQ" then
    local token = Trim(rest)
    if token ~= "" then
      self:_SendRunTo(sender, token, channel)
    end
    return
  end

  if cmd == "MISS" then
    local token = Trim(rest)
    local p = self._pending[token]
    if p and PendingSenderMatches(p, sender, sender, channel) then
      self._pending[token] = nil
      Notify("That shared run is no longer available from " .. tostring(p.sender or sender or "the sender") .. ".")
    end
    return
  end

  if cmd == "BEGIN" then
    local token, totalS = rest:match("^([^|]+)|([^|]+)$")
    token = Trim(token)
    local p = self._pending[token]
    if p and PendingSenderMatches(p, sender, sender, channel) then
      local total = tonumber(totalS) or 0
      if total <= 0 or total > MAX_CHUNKS then
        self._pending[token] = nil
        Notify("Shared run transfer was rejected (invalid size).")
        return
      end
      p.total = total
      p.chunks = {}
      if channel ~= "BN_WHISPER" then
        p.sender = sender or p.sender
      end
    end
    return
  end

  if cmd == "CHUNK" then
    local token, idxS, piece = rest:match("^([^|]+)|([^|]+)|(.+)$")
    token = Trim(token)
    local p = self._pending[token]
    if p and PendingSenderMatches(p, sender, sender, channel) then
      local idx = tonumber(idxS) or 0
      if idx > 0 and idx <= MAX_CHUNKS then
        p.chunks[idx] = piece or ""
        p.startedAt = time() -- reset TTL while transfer is active
      end
      if channel ~= "BN_WHISPER" then
        p.sender = sender or p.sender
      end
    end
    return
  end

  if cmd == "END" then
    local token = Trim(rest)
    local p = self._pending[token]
    if not p or not PendingSenderMatches(p, sender, sender, channel) then return end
    self._pending[token] = nil

    local total = tonumber(p.total) or 0
    if total <= 0 then
      Notify("Shared run transfer failed (missing header).")
      return
    end

    local parts = {}
    for i = 1, total do
      if type(p.chunks[i]) ~= "string" then
        Notify("Shared run transfer was incomplete.")
        return
      end
      parts[#parts + 1] = p.chunks[i]
    end

    local payload = table.concat(parts, "")
    local raw = DecodePayload(payload)
    if not raw then
      Notify("Shared run transfer couldn't be decoded.")
      return
    end

    local sess, err = ParseRawSession(raw)
    if not sess then
      Notify("Shared run couldn't be opened: " .. tostring(err or "Invalid payload"))
      return
    end

    local openedFrom = p.sender or sender or "Unknown"
    self:_OpenSharedSession(sess, openedFrom)
    return
  end
end

local function ChatMessageFilter(_, event, msg, author, ...)
  if not (WF and WF.Share and WF.Share.DecorateChatMessage) then
    return false, msg, author, ...
  end

  local bnSenderID = nil
  if event == "CHAT_MSG_BN_WHISPER" or event == "CHAT_MSG_BN_WHISPER_INFORM" then
    bnSenderID = select(11, ...)
  end

  local newMsg, changed = WF.Share:DecorateChatMessage(msg, author, event, bnSenderID)
  if changed then
    return false, newMsg, author, ...
  end

  return false, msg, author, ...
end

local function HookChatFilters()
  if Share._chatFiltersHooked then return end
  if type(ChatFrame_AddMessageEventFilter) ~= "function" then return end

  Share._chatFiltersHooked = true
  for i = 1, #CHAT_FILTER_EVENTS do
    ChatFrame_AddMessageEventFilter(CHAT_FILTER_EVENTS[i], ChatMessageFilter)
  end
end

local function HookItemRef()
  if Share._setItemRefHooked then return end
  if type(SetItemRef) ~= "function" or type(hooksecurefunc) ~= "function" then return end

  Share._setItemRefHooked = true
  hooksecurefunc("SetItemRef", function(link, text, button, chatFrame)
    if type(link) ~= "string" or not link:match("^whatfarmrun:") then return end

    local ok = false
    if WF and WF.Share and WF.Share.HandleLink then
      ok = WF.Share:HandleLink(link, text, button, chatFrame) and true or false
    end

    if ok and ItemRefTooltip and ItemRefTooltip:IsShown() then
      ItemRefTooltip:Hide()
    end
  end)
end

WF.Events:On("ADDON_LOADED", function(_, name)
  if name ~= ADDON_NAME then return end
  if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix and not Share._prefixRegistered then
    Share._prefixRegistered = true
    C_ChatInfo.RegisterAddonMessagePrefix(PREFIX)
  end
  HookChatFilters()
  HookItemRef()
end)

WF.Events:On("CHAT_MSG_ADDON", function(_, prefix, msg, channel, sender)
  if prefix ~= PREFIX then return end
  if WF and WF.Share and WF.Share._HandleAddonMessage then
    WF.Share:_HandleAddonMessage(msg, channel, sender)
  end
end)

WF.Events:On("BN_CHAT_MSG_ADDON", function(_, prefix, msg, channel, senderPresenceID)
  if prefix ~= PREFIX then return end
  if WF and WF.Share and WF.Share._HandleAddonMessage then
    WF.Share:_HandleAddonMessage(msg, "BN_WHISPER", senderPresenceID)
  end
end)
