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

WF.Util = WF.Util or {}
WF.Util.FriendsScrambler = WF.Util.FriendsScrambler or {}

local Scrambler = WF.Util.FriendsScrambler

Scrambler.enabled = false
Scrambler.aliases = Scrambler.aliases or {}
Scrambler.nextAlias = Scrambler.nextAlias or 1
Scrambler.lastFakeByFontString = Scrambler.lastFakeByFontString or {}

local function Chat(msg)
  if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
    DEFAULT_CHAT_FRAME:AddMessage("|cff66c2ffWhatFarm:|r " .. tostring(msg))
  end
end

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

local function Normalize(s)
  s = tostring(s or "")
  s = s:gsub("|c%x%x%x%x%x%x%x%x", "")
  s = s:gsub("|r", "")
  s = s:gsub("|T.-|t", "")
  s = s:gsub("|A.-|a", "")
  s = s:gsub("%s+", " ")
  return Trim(s)
end

local staticText = {
  ["contacts"] = true,
  ["friends"] = true,
  ["recent allies"] = true,
  ["recruit a friend"] = true,
  ["add friend"] = true,
  ["send message"] = true,
  ["quick join"] = true,
  ["who"] = true,
  ["raid"] = true,
  ["ignored"] = true,
  ["blocked"] = true,
  ["battle.net"] = true,
  ["battle.net friends"] = true,
  ["real id"] = true,
  ["world of warcraft"] = true,
  ["wow"] = true,
  ["available"] = true,
  ["away"] = true,
  ["busy"] = true,
  ["online"] = true,
  ["offline"] = true,
  ["in game"] = true,
  ["in app"] = true,
  ["mobile"] = true,
  ["afk"] = true,
  ["dnd"] = true,
}

local function IsStaticText(text)
  local plain = Normalize(text)
  if plain == "" then return true end

  local lower = plain:lower()
  if staticText[lower] then return true end
  if lower:match("^quick join%s*%(%d+%)$") then return true end
  if lower:match("^friends%s*%(%d+%)$") then return true end
  if lower:match("^%d+/%d+$") then return true end
  if lower:match("^%d+$") then return true end
  if lower:match("^%d+:%d%d$") then return true end

  return false
end

local function IsAlreadyFake(text)
  local plain = Normalize(text)
  if plain:match("^Friend %d%d$") then return true end
  if plain:match("^Friend#%d%d%d%d$") then return true end
  if plain == "In Game" or plain == "Online" or plain == "Offline" or plain == "Last online recently" then return true end
  return false
end

local function PreserveColor(original, replacement)
  local prefix = tostring(original or ""):match("^(|c%x%x%x%x%x%x%x%x)")
  local suffix = prefix and "|r" or ""
  if prefix then
    return prefix .. replacement .. suffix
  end
  return replacement
end

local function AliasFor(self, key, battleTag)
  key = Normalize(key)
  if key == "" then key = "unknown" end

  local existing = self.aliases[key]
  if existing then return existing end

  local idx = self.nextAlias or 1
  self.nextAlias = idx + 1

  local fake
  if battleTag then
    fake = string.format("Friend#%04d", 1000 + (idx % 8999))
  else
    fake = string.format("Friend %02d", idx)
  end

  self.aliases[key] = fake
  return fake
end

local function LooksLikeStatusLine(text)
  local plain = Normalize(text)
  local lower = plain:lower()

  if lower:find("last online", 1, true) then return true end
  if lower:find("last seen", 1, true) then return true end
  if lower:find("offline", 1, true) then return true end
  if lower:find("online", 1, true) then return true end
  if lower:find("in app", 1, true) then return true end
  if lower:find("world of warcraft", 1, true) then return true end
  if lower:find("classic", 1, true) then return true end
  if lower:find("retail", 1, true) then return true end
  if lower:match("^level%s+%d+") then return true end
  if lower:match("^last online%s+") then return true end

  return false
end

local function LooksLikeSensitiveName(text)
  local plain = Normalize(text)
  if plain == "" then return false end
  if IsStaticText(plain) then return false end
  if IsAlreadyFake(plain) then return false end

  if plain:find("#%d", 1) then return true end
  if plain:match("^[%a][%w%p%s']-$") and #plain >= 3 and #plain <= 40 then
    return true
  end

  return false
end

local function GetFontStringContext(fs)
  local name = ""
  if fs and fs.GetName then
    name = tostring(fs:GetName() or "")
  end
  if name == "" and fs and fs.GetParent and fs:GetParent() and fs:GetParent().GetName then
    name = tostring(fs:GetParent():GetName() or "")
  end
  name = name:lower()

  if name:find("info", 1, true) or name:find("note", 1, true) or name:find("status", 1, true) or name:find("area", 1, true) or name:find("zone", 1, true) then
    return "status"
  end

  if name:find("name", 1, true) then
    return "name"
  end

  return "unknown"
end

local function FakeForText(self, text, fs)
  local plain = Normalize(text)
  if plain == "" then return nil end
  if IsStaticText(plain) or IsAlreadyFake(plain) then return nil end

  local context = GetFontStringContext(fs)

  if context == "status" then
    local lower = plain:lower()
    if lower:find("offline", 1, true) or lower:find("last online", 1, true) or lower:find("last seen", 1, true) then
      return PreserveColor(text, "Last online recently")
    end
    return PreserveColor(text, "In Game")
  end

  if plain:find("#%d", 1) then
    return PreserveColor(text, AliasFor(self, plain, true))
  end

  if LooksLikeStatusLine(plain) then
    if plain:lower():find("offline", 1, true) or plain:lower():find("last online", 1, true) or plain:lower():find("last seen", 1, true) then
      return PreserveColor(text, "Last online recently")
    end
    return PreserveColor(text, "In Game")
  end

  if LooksLikeSensitiveName(plain) then
    return PreserveColor(text, AliasFor(self, plain, false))
  end

  return nil
end

local function ScrubFontString(self, fs)
  if not fs or not fs.GetText or not fs.SetText then return end
  if fs.IsForbidden and fs:IsForbidden() then return end
  if fs.IsProtected and fs:IsProtected() then return end

  local ok, text = pcall(fs.GetText, fs)
  if not ok or not text or text == "" then return end

  local lastFake = self.lastFakeByFontString[fs]
  if lastFake and text == lastFake then return end

  local fake = FakeForText(self, text, fs)
  if fake and fake ~= text then
    local okSet = pcall(fs.SetText, fs, fake)
    if okSet then
      self.lastFakeByFontString[fs] = fake
    end
  end
end

local function ScrubFrame(self, frame, seen)
  if not frame or seen[frame] then return end
  if frame.IsForbidden and frame:IsForbidden() then return end
  seen[frame] = true

  if frame.GetRegions then
    local regions = { frame:GetRegions() }
    for i = 1, #regions do
      local region = regions[i]
      if region and region.GetObjectType and region:GetObjectType() == "FontString" then
        ScrubFontString(self, region)
      end
    end
  end

  if frame.GetChildren then
    local children = { frame:GetChildren() }
    for i = 1, #children do
      ScrubFrame(self, children[i], seen)
    end
  end
end

local rootNames = {
  "FriendsFrameFriendsScrollFrame",
  "FriendsListFrame",
  "FriendsFrameBattlenetFrame",
  "FriendsFrameFriendsScrollFrameScrollChild",
  "QuickJoinFrame",
}

function Scrambler:ScrambleNow()
  if not self.enabled then return end

  local seen = {}
  local touched = false

  for i = 1, #rootNames do
    local root = _G[rootNames[i]]
    if root and root.IsShown and root:IsShown() then
      touched = true
      ScrubFrame(self, root, seen)
    end
  end

  local friendsFrame = _G.FriendsFrame
  if not touched and friendsFrame and friendsFrame.IsShown and friendsFrame:IsShown() then
    ScrubFrame(self, friendsFrame, seen)
  end
end

function Scrambler:StartTicker()
  if self.ticker then return end

  if C_Timer and C_Timer.NewTicker then
    self.ticker = C_Timer.NewTicker(0.15, function(ticker)
      if not Scrambler.enabled then
        if ticker and ticker.Cancel then ticker:Cancel() end
        Scrambler.ticker = nil
        return
      end
      Scrambler:ScrambleNow()
    end)
    return
  end

  if not self.frame then
    self.frame = CreateFrame("Frame")
  end

  self.elapsed = 0
  self.frame:SetScript("OnUpdate", function(_, elapsed)
    if not Scrambler.enabled then
      Scrambler.frame:SetScript("OnUpdate", nil)
      return
    end
    Scrambler.elapsed = (Scrambler.elapsed or 0) + (elapsed or 0)
    if Scrambler.elapsed >= 0.15 then
      Scrambler.elapsed = 0
      Scrambler:ScrambleNow()
    end
  end)
end

function Scrambler:SetEnabled(enabled)
  enabled = enabled and true or false

  if enabled then
    self.enabled = true
    self:StartTicker()
    self:ScrambleNow()
    Chat("Friends list scrambler ON. Use /wf scramblefriends off to turn it off.")
    return
  end

  self.enabled = false
  Chat("Friends list scrambler OFF. Reopen Contacts or /reload if any fake names are still visible.")
end

function Scrambler:Toggle()
  self:SetEnabled(not self.enabled)
end
