local BRIDGE = {}

local B64 = {}
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for index = 1, #ALPHABET do
  B64[ALPHABET:sub(index, index)] = index - 1
end

local function floor(value)
  return value - (value % 1)
end

local bitXor = (bit and bit.bxor) or (bit32 and bit32.bxor)
local function bxor(left, right)
  if bitXor then
    return bitXor(left, right)
  end

  local result = 0
  local value = 1
  left = left or 0
  right = right or 0

  while left > 0 or right > 0 do
    local leftBit = left % 2
    local rightBit = right % 2
    if leftBit ~= rightBit then
      result = result + value
    end
    left = floor(left / 2)
    right = floor(right / 2)
    value = value * 2
  end

  return result
end

local function decode(payload, seed)
  if type(payload) ~= "string" or not string or not string.char then
    return ""
  end

  payload = payload:gsub("[^A-Za-z0-9+/=]", "")
  seed = tonumber(seed or 0) or 0

  local output = {}
  local state = seed
  local position = 0

  local function push(byte)
    state = (state * 73 + 41 + position) % 256
    local mask = bxor(state, (seed + position * 17) % 256)
    output[#output + 1] = string.char(bxor(byte, mask))
    position = position + 1
  end

  for index = 1, #payload, 4 do
    local one = payload:sub(index, index)
    local two = payload:sub(index + 1, index + 1)
    local three = payload:sub(index + 2, index + 2)
    local four = payload:sub(index + 3, index + 3)
    local first = B64[one]
    local second = B64[two]

    if first == nil or second == nil then
      break
    end

    local third = three ~= "=" and (B64[three] or 0) or 0
    local fourth = four ~= "=" and (B64[four] or 0) or 0
    local packed = first * 262144 + second * 4096 + third * 64 + fourth

    push(floor(packed / 65536) % 256)
    if three ~= "=" and three ~= "" then
      push(floor(packed / 256) % 256)
    end
    if four ~= "=" and four ~= "" then
      push(packed % 256)
    end
  end

  return table.concat(output)
end

local function activeRoot()
  local root = MerfinPlusCompanionSession
  if type(root) ~= "table" or root.active ~= true then
    return nil
  end

  local expiresAt = tonumber(root.expiresAt or 0) or 0
  if expiresAt > 0 and time and time() > expiresAt then
    return nil
  end

  return root
end

local function cloneAuraEntry(entry)
  if type(entry) ~= "table" then
    return nil
  end

  local payload = entry.encodedPayload or entry.payload
  local seed = entry.encodedSeed or entry.seed
  local encoded = decode(payload, seed)
  if encoded == "" then
    return nil
  end

  return {
    name = entry.name,
    author = entry.author or "Merfin",
    encoded = encoded,
    wagoVersion = tonumber(entry.wagoVersion or entry.version or 1) or 1,
    wagoSemver = entry.wagoSemver or entry.semver or "1.0.0",
    source = entry.source or "MerfinUI Companion"
  }
end

local function bridgeSlugFromUrl(url)
  if type(url) ~= "string" then
    return ""
  end

  return url:match("wago%.io/([^/%?#]+)") or url:match("downloads%.merfin%.uk/app/auras/([^/%?#]+)") or url:match("merfin%.uk/app/auras/([^/%?#]+)") or ""
end

local function bridgeVersionFromUrl(url)
  if type(url) ~= "string" then
    return 0
  end

  local version = url:match("wago%.io/[^/%?#]+/([0-9]+)") or url:match("downloads%.merfin%.uk/app/auras/[^/%?#]+/([0-9]+)") or url:match("merfin%.uk/app/auras/[^/%?#]+/([0-9]+)")
  return tonumber(version or "0") or 0
end

local function installedVersionForAura(aura)
  if type(aura) ~= "table" then
    return 0
  end

  local merfinVersion = tonumber(aura.merfinVersion or 0) or 0
  local urlVersion = bridgeVersionFromUrl(aura.url)
  if urlVersion > merfinVersion then
    return urlVersion
  end
  if merfinVersion > 0 then
    return merfinVersion
  end
  return tonumber(aura.version or 0) or 0
end

local function installedVersionsBySlug()
  local versions = {}
  local saved = WeakAurasSaved
  local displays = saved and saved.displays
  if type(displays) ~= "table" then
    return versions
  end

  for _, aura in pairs(displays) do
    if type(aura) == "table" and not aura.parent then
      local slug = aura.merfinSlug
      if type(slug) ~= "string" or slug == "" then
        slug = bridgeSlugFromUrl(aura.url)
      end

      if type(slug) == "string" and slug ~= "" then
        local version = installedVersionForAura(aura)
        if version > (versions[slug] or 0) then
          versions[slug] = version
        end
      end
    end
  end

  return versions
end

local function buildWeakAurasData(root)
  local source = root and root.WeakAuras
  if type(source) ~= "table" then
    return nil
  end

  local installedVersions = installedVersionsBySlug()
  local data = {
    slugs = {},
    stash = {}
  }

  for slug, entry in pairs(source.slugs or {}) do
    local targetVersion = tonumber(entry and entry.wagoVersion or 0) or 0
    local installedVersion = installedVersions[slug] or 0
    local clone = targetVersion > installedVersion and cloneAuraEntry(entry) or nil
    if clone then
      data.slugs[slug] = clone
    end
  end

  return data
end

local migrationStopWords = {
  merfin = true, mists = true, mop = true, tbc = true, classic = true,
  weakaura = true, weakauras = true, aura = true, auras = true,
  general = true, classpack = true, classpacks = true, anchor = true, anchors = true,
  string = true, strings = true, raid = true, dungeon = true, additional = true, additionals = true,
  public = true, version = true, full = true, display = true, qhd = true, fhd = true, hd = true,
  ui = true, ["and"] = true, the = true,
}

local function normalizeTokens(value)
  if type(value) ~= "string" then
    return {}
  end

  local tokens = {}
  value = value:lower():gsub("%b[]", " ")
  for token in value:gmatch("[a-z0-9]+") do
    local isRandomToken = token:match("%d") and #token >= 6
    if #token >= 3 and not migrationStopWords[token] and not token:match("^%d+$") and not token:match("^[tv]%d+$") and not isRandomToken then
      tokens[#tokens + 1] = token
    end
  end
  return tokens
end

local function tokenListsOverlap(left, right)
  for _, leftToken in ipairs(left) do
    for _, rightToken in ipairs(right) do
      if leftToken == rightToken then
        return true
      end
      if #leftToken > 3 and rightToken:find(leftToken, 1, true) then
        return true
      end
      if #rightToken > 3 and leftToken:find(rightToken, 1, true) then
        return true
      end
    end
  end
  return false
end

local function slugLooksLikeDisplay(slug, displayId)
  if type(displayId) ~= "string" or displayId == "" then
    return true
  end

  local slugTokens = normalizeTokens(slug)
  if #slugTokens == 0 then
    return true
  end

  local displayTokens = normalizeTokens(displayId)
  if #displayTokens == 0 then
    return true
  end

  return tokenListsOverlap(slugTokens, displayTokens)
end

local function slugFromUrl(url)
  if type(url) ~= "string" then
    return ""
  end

  return url:match("wago%.io/([^/%?#]+)") or url:match("downloads%.merfin%.uk/app/auras/([^/%?#]+)") or url:match("merfin%.uk/app/auras/([^/%?#]+)") or ""
end

local function versionFromUrl(url)
  if type(url) ~= "string" then
    return 0
  end

  local version = url:match("wago%.io/[^/%?#]+/([0-9]+)") or url:match("downloads%.merfin%.uk/app/auras/[^/%?#]+/([0-9]+)") or url:match("merfin%.uk/app/auras/[^/%?#]+/([0-9]+)")
  return tonumber(version or "0") or 0
end

local function currentSlug(aura)
  if type(aura) ~= "table" then
    return ""
  end

  if type(aura.merfinSlug) == "string" and aura.merfinSlug ~= "" then
    return aura.merfinSlug
  end

  return slugFromUrl(aura.url)
end

local function latestVersionForSlug(root, slug)
  local weakAuras = root and root.WeakAuras
  local slugs = weakAuras and weakAuras.slugs
  local entry = slugs and slugs[slug]
  return tonumber(entry and entry.wagoVersion or 0) or 0
end

local function canApplyMigration(root, aura, migration, displayId)
  if type(aura) ~= "table" or type(migration) ~= "table" or type(migration.slug) ~= "string" or migration.slug == "" then
    return false
  end

  if not slugLooksLikeDisplay(migration.slug, displayId) then
    return false
  end

  local installedVersion = tonumber(migration.installedVersion or 1) or 1
  local latestVersion = latestVersionForSlug(root, migration.slug)
  if latestVersion <= 0 then
    return false
  end

  local slug = currentSlug(aura)
  if slug == "" or slug ~= migration.slug then
    return true
  end

  if installedVersion <= 0 and not aura.merfinUrl and not aura.merfinSourceChecksum then
    return true
  end

  local currentVersion = installedVersionForAura(aura)
  if currentVersion <= 0 then
    currentVersion = installedVersion
  end

  return currentVersion < latestVersion
end

local function findMigrationAura(displays, migration)
  local displayId = migration and migration.displayId
  if displayId and type(displays[displayId]) == "table" then
    return displayId, displays[displayId]
  end

  local uid = migration and migration.uid
  if type(uid) == "string" and uid ~= "" then
    for id, aura in pairs(displays) do
      if type(aura) == "table" and aura.uid == uid and not aura.parent then
        return id, aura
      end
    end
  end

  return displayId, nil
end

local function applyMigrations(root)
  local migrations = root and root.Migrations
  local saved = WeakAurasSaved
  if type(migrations) ~= "table" or type(saved) ~= "table" or type(saved.displays) ~= "table" then
    return
  end

  for _, migration in ipairs(migrations) do
    local displayId, aura = findMigrationAura(saved.displays, migration)
    if type(aura) == "table" then
      local uidMatches = not migration.uid or migration.uid == "" or not aura.uid or aura.uid == migration.uid
      if uidMatches and canApplyMigration(root, aura, migration, displayId) then
        local installedVersion = tonumber(migration.installedVersion or 1) or 1
        aura.url = migration.installedUrl or ("https://wago.io/" .. migration.slug .. "/" .. installedVersion)
        aura.wagoID = migration.installedWagoID or migration.slug
        aura.version = installedVersion
        aura.semver = migration.installedSemver or "1.0.0"
        aura.merfinSlug = migration.slug
        aura.merfinVersion = installedVersion
        aura.preferToUpdate = true
        aura.ignoreWagoUpdate = false
      end
    end
  end
end

local function expandWeakAurasUpdates()
  if type(WeakAurasOptionsSaved) == "table" then
    WeakAurasOptionsSaved.pendingUpdateCollapse = nil
  end

  if WeakAuras and WeakAuras.IsOptionsOpen and WeakAuras.IsOptionsOpen() and WeakAuras.FillOptions then
    WeakAuras.FillOptions()
  end

  local optionsPrivate = WeakAuras and WeakAuras.OptionsPrivate
  local optionsFrame = optionsPrivate and optionsPrivate.frame
  local pendingUpdateButton = optionsFrame and optionsFrame.pendingUpdateButton
  if pendingUpdateButton and pendingUpdateButton.Expand then
    pendingUpdateButton:Expand()
  end
end

function BRIDGE:Register()
  local root = activeRoot()
  if not root then
    return
  end

  applyMigrations(root)

  local data = buildWeakAurasData(root)
  if WeakAuras and WeakAuras.AddCompanionData and data then
    WeakAuras.AddCompanionData(data)
  end

  expandWeakAurasUpdates()
end

BRIDGE:Register()

if CreateFrame then
  local frame = CreateFrame("Frame")
  frame:RegisterEvent("ADDON_LOADED")
  frame:RegisterEvent("PLAYER_LOGIN")
  frame:RegisterEvent("PLAYER_ENTERING_WORLD")
  frame:SetScript("OnEvent", function(self, event, addonName)
    if event == "ADDON_LOADED" and addonName ~= "WeakAuras" and addonName ~= "WeakAurasOptions" then
      return
    end

    BRIDGE:Register()

    if event == "ADDON_LOADED" and addonName == "WeakAurasOptions" then
      self:UnregisterEvent("ADDON_LOADED")
    end
    if event == "PLAYER_ENTERING_WORLD" then
      self:UnregisterEvent("PLAYER_ENTERING_WORLD")
    end
  end)
end
