local MerfinPlus = LibStub("AceAddon-3.0"):GetAddon("MerfinPlus")

local frame
local importHookInstalled = false
local originalWeakAurasImport
local outdatedWarningShown = false
local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata

local function SplitVersion(version)
  if type(version) ~= "string" and type(version) ~= "number" then
    return nil
  end

  local parts = {}
  for part in tostring(version):gmatch("%d+") do
    parts[#parts + 1] = tonumber(part)
  end

  if #parts == 0 then
    return nil
  end

  return parts
end

local function CompareVersions(leftVersion, rightVersion)
  local left = SplitVersion(leftVersion)
  local right = SplitVersion(rightVersion)

  if not left or not right then
    return nil
  end

  local length = math.max(#left, #right)
  for index = 1, length do
    local leftPart = left[index] or 0
    local rightPart = right[index] or 0

    if leftPart < rightPart then
      return -1
    elseif leftPart > rightPart then
      return 1
    end
  end

  return 0
end

local function CheckRequiredMerfinPlusVersion()
  if outdatedWarningShown or type(MerfinPlusCompanionData) ~= "table" then
    return
  end

  local requiredVersion = MerfinPlusCompanionData.requiredMerfinPlusVersion
  if type(requiredVersion) ~= "string" or requiredVersion == "" or not GetAddOnMetadata then
    return
  end

  local currentVersion = GetAddOnMetadata("MerfinPlus", "Version")
  local comparison = CompareVersions(currentVersion, requiredVersion)
  if comparison and comparison < 0 then
    outdatedWarningShown = true
    local downloadUrl = MerfinPlusCompanionData.requiredMerfinPlusUrl
      or "https://www.curseforge.com/wow/addons/merfinplus"

    print(
      "|cff40c7ebMerfinPlus:|r MerfinPlus is outdated. Update to Version "
        .. requiredVersion
        .. "+. You can download the Addon on "
        .. downloadUrl
    )
  end
end

local function NormalizeUrl(value)
  if type(value) ~= "string" then
    return ""
  end
  value = value:gsub("#.*$", ""):gsub("%?.*$", ""):gsub("/+$", "")
  return value
end

local function WagoSlug(value)
  value = NormalizeUrl(value)
  return value:match("wago%.io/([^/%?%#]+)") or ""
end

local function UrlSlugAndVersion(value)
  value = NormalizeUrl(value)
  local slug, version = value:match("wago%.io/([^/%?%#]+)/([0-9]+)$")
  if slug then
    return slug, tonumber(version)
  end
  slug = value:match("wago%.io/([^/%?%#]+)$")
  return slug or "", nil
end

local function CompanionSlugData(slug)
  if type(MerfinPlusCompanionData) ~= "table" then
    return nil
  end
  local weakAurasData = MerfinPlusCompanionData.WeakAuras
  if type(weakAurasData) ~= "table" or type(weakAurasData.slugs) ~= "table" then
    return nil
  end
  return weakAurasData.slugs[slug]
end

local function DisplaySlug(display)
  if type(display) ~= "table" then
    return ""
  end
  if type(display.merfinSlug) == "string" and display.merfinSlug ~= "" then
    return display.merfinSlug
  end
  local slug = UrlSlugAndVersion(display.url)
  return slug or ""
end

local function CompanionVersionForSlug(slug)
  local companionData = CompanionSlugData(slug)
  local version = companionData and tonumber(companionData.wagoVersion)
  if version and version > 0 then
    return math.floor(version), companionData.wagoSemver
  end
  return nil, nil
end

local function StampDisplayAsCurrent(display, slug)
  if type(display) ~= "table" or type(slug) ~= "string" or slug == "" then
    return false
  end

  local version, semver = CompanionVersionForSlug(slug)
  if not version then
    return false
  end

  display.url = "https://wago.io/" .. slug .. "/" .. tostring(version)
  display.version = version
  display.semver = semver or display.semver or ("1.0." .. tostring(math.max(version - 1, 0)))
  display.merfinSlug = slug
  display.merfinVersion = version
  return true
end

local function StampDisplayTree(display, slug, seen)
  if type(display) ~= "table" then
    return
  end

  seen = seen or {}
  local key = display.uid or display.id
  if key and seen[key] then
    return
  end
  if key then
    seen[key] = true
  end

  StampDisplayAsCurrent(display, slug)

  if type(display.controlledChildren) == "table"
      and type(WeakAurasSaved) == "table"
      and type(WeakAurasSaved.displays) == "table" then
    for _, childId in ipairs(display.controlledChildren) do
      StampDisplayTree(WeakAurasSaved.displays[childId], slug, seen)
    end
  end
end

local function FindDisplayByUID(uid)
  if type(uid) ~= "string"
      or type(WeakAurasSaved) ~= "table"
      or type(WeakAurasSaved.displays) ~= "table" then
    return nil
  end

  for _, display in pairs(WeakAurasSaved.displays) do
    if type(display) == "table" and display.uid == uid then
      return display
    end
  end

  return nil
end

local function DisplayFromImportTarget(target)
  if type(target) == "table" then
    return target
  end
  if type(target) == "string" and type(WeakAurasSaved) == "table" and type(WeakAurasSaved.displays) == "table" then
    return WeakAurasSaved.displays[target] or FindDisplayByUID(target)
  end
  return nil
end

local function ImportSlugFromLinkedAuras(target, linkedAuras)
  local display = DisplayFromImportTarget(target)
  local slug = DisplaySlug(display)
  if slug ~= "" and CompanionSlugData(slug) then
    return slug
  end

  if type(linkedAuras) == "table" and type(WeakAurasSaved) == "table" and type(WeakAurasSaved.displays) == "table" then
    for _, auraId in pairs(linkedAuras) do
      display = WeakAurasSaved.displays[auraId] or FindDisplayByUID(auraId)
      slug = DisplaySlug(display)
      if slug ~= "" and CompanionSlugData(slug) then
        return slug
      end
    end
  end

  return ""
end

local function StampLinkedImport(target, linkedAuras, importedId, slug)
  if type(WeakAurasSaved) ~= "table" or type(WeakAurasSaved.displays) ~= "table" then
    return
  end

  local stamped = false
  local displays = WeakAurasSaved.displays
  local display = importedId and displays[importedId]
  if display then
    StampDisplayTree(display, slug)
    stamped = true
  end

  display = DisplayFromImportTarget(target)
  if display then
    StampDisplayTree(display, slug)
    stamped = true
  end

  if type(linkedAuras) == "table" then
    for _, auraId in pairs(linkedAuras) do
      display = displays[auraId] or FindDisplayByUID(auraId)
      if display then
        StampDisplayTree(display, slug)
        stamped = true
      end
    end
  end

  for _, aura in pairs(displays) do
    if DisplaySlug(aura) == slug then
      StampDisplayTree(aura, slug)
      stamped = true
    end
  end

  if stamped and WeakAuras and type(WeakAuras.ScanEvents) == "function" then
    WeakAuras.ScanEvents("MERFIN_PLUS_COMPANION_UPDATED", slug)
  end
end

local function DisplayHasMerfinVersion(display, migration, installedVersion)
  if type(display) ~= "table" or type(migration) ~= "table" then
    return false
  end

  installedVersion = tonumber(installedVersion) or 1

  local merfinVersion = tonumber(display.merfinVersion)
  if display.merfinSlug == migration.slug and merfinVersion and merfinVersion >= installedVersion then
    return true
  end

  local slug, urlVersion = UrlSlugAndVersion(display.url)
  if slug == migration.slug then
    return (tonumber(urlVersion) or tonumber(display.version) or 1) >= installedVersion
  end

  return false
end

local function DisplayHasCompanionVersion(display, slug, companionVersion)
  if type(display) ~= "table" or type(slug) ~= "string" or slug == "" then
    return false
  end

  companionVersion = tonumber(companionVersion) or 1
  local displaySlug = DisplaySlug(display)
  if displaySlug ~= slug then
    return false
  end

  local _, urlVersion = UrlSlugAndVersion(display.url)
  local displayVersion = tonumber(urlVersion) or tonumber(display.merfinVersion) or tonumber(display.version) or 1
  return displayVersion >= companionVersion
end

local function IsCompanionSlugCurrent(slug, companionVersion)
  if type(WeakAurasSaved) ~= "table" or type(WeakAurasSaved.displays) ~= "table" then
    return false
  end

  for _, display in pairs(WeakAurasSaved.displays) do
    if DisplayHasCompanionVersion(display, slug, companionVersion) then
      return true
    end
  end

  return false
end

local function RemoveCurrentCompanionStash(weakAurasData)
  if type(weakAurasData) ~= "table" or type(weakAurasData.stash) ~= "table" then
    return
  end

  for slug, aura in pairs(weakAurasData.stash) do
    local version = type(aura) == "table" and tonumber(aura.wagoVersion) or 1
    if IsCompanionSlugCurrent(slug, version) then
      weakAurasData.stash[slug] = nil
    end
  end
end

local function MigrationMatches(display, migration)
  if type(display) ~= "table" or type(migration) ~= "table" then
    return false
  end

  if migration.uid ~= "" and display.uid == migration.uid then
    if migration.displayId == "" or display.id == migration.displayId then
      return true
    end
  end

  if migration.uid == "" and migration.displayId ~= "" and display.id == migration.displayId then
    return true
  end

  if migration.url ~= "" and type(display.url) == "string" then
    if NormalizeUrl(display.url) == NormalizeUrl(migration.url) then
      return true
    end

    local sourceSlug = WagoSlug(migration.url)
    if sourceSlug ~= "" and WagoSlug(display.url) == sourceSlug then
      return true
    end
  end

  return false
end

local function ApplyMerfinMigrations()
  if type(MerfinPlusCompanionData) ~= "table" then
    return
  end
  if type(WeakAurasSaved) ~= "table" or type(WeakAurasSaved.displays) ~= "table" then
    return
  end

  local migrations = MerfinPlusCompanionData.Migrations
  if type(migrations) ~= "table" then
    return
  end

  for _, migration in ipairs(migrations) do
    if type(migration.slug) == "string" and migration.slug ~= "" then
      for _, display in pairs(WeakAurasSaved.displays) do
        if MigrationMatches(display, migration) then
          local installedVersion = tonumber(migration.installedVersion) or tonumber(display.version) or 1
          if installedVersion < 1 then
            installedVersion = 1
          end

          if not DisplayHasMerfinVersion(display, migration, installedVersion) then
            display.url = migration.installedUrl or ("https://wago.io/" .. migration.slug .. "/" .. tostring(installedVersion))
            display.version = installedVersion
            display.semver = display.semver or ("1.0." .. tostring(math.max(installedVersion - 1, 0)))
            display.merfinSlug = migration.slug
            display.merfinVersion = installedVersion
          end
        end
      end
    end
  end
end

local function InstallWeakAurasImportHook()
  if importHookInstalled or not WeakAuras or type(WeakAuras.Import) ~= "function" then
    return
  end

  originalWeakAurasImport = WeakAuras.Import
  WeakAuras.Import = function(inData, target, callbackFunc, linkedAuras)
    local slug = ImportSlugFromLinkedAuras(target, linkedAuras)

    if slug == "" then
      return originalWeakAurasImport(inData, target, callbackFunc, linkedAuras)
    end

    local function afterImport(success, importedId)
      if success then
        StampLinkedImport(target, linkedAuras, importedId, slug)
        if C_Timer and type(C_Timer.After) == "function" then
          C_Timer.After(0.1, function()
            StampLinkedImport(target, linkedAuras, importedId, slug)
          end)
        end
      end
      if type(callbackFunc) == "function" then
        callbackFunc(success, importedId)
      end
    end

    return originalWeakAurasImport(inData, target, afterImport, linkedAuras)
  end

  importHookInstalled = true
end

local function AddMerfinPlusCompanionData()
  if type(MerfinPlusCompanionData) ~= "table" then
    return
  end

  CheckRequiredMerfinPlusVersion()
  ApplyMerfinMigrations()

  local weakAurasData = MerfinPlusCompanionData.WeakAuras
  if weakAurasData and WeakAuras and type(WeakAuras.AddCompanionData) == "function" then
    RemoveCurrentCompanionStash(weakAurasData)
    WeakAuras.AddCompanionData(weakAurasData)
    InstallWeakAurasImportHook()
  end
end

function MerfinPlus:CompanionBridgeEnable()
  AddMerfinPlusCompanionData()

  if frame then
    return
  end

  frame = CreateFrame("Frame")
  frame:RegisterEvent("ADDON_LOADED")
  frame:SetScript("OnEvent", function(_, _, loadedAddonName)
    if loadedAddonName == "WeakAuras" or loadedAddonName == "MerfinPlus" then
      AddMerfinPlusCompanionData()
    end
  end)
end

Merfin = Merfin or {}
Merfin.Companion = Merfin.Companion or {}
Merfin.Companion.Refresh = AddMerfinPlusCompanionData
Merfin.Companion.CheckRequiredVersion = CheckRequiredMerfinPlusVersion
