local API = rawget(_G, "LIDCOUNCIL_API")
if type(API) ~= "table" then
  API = {}
  _G.LIDCOUNCIL_API = API
end

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

local function Now()
  return (type(GetServerTime) == "function" and GetServerTime()) or (time and time() or 0)
end

local function GetApiFunction(name)
  local value = API[name]
  if type(value) == "function" then
    return value
  end
  value = rawget(_G, name)
  if type(value) == "function" then
    return value
  end
  return nil
end

local function RootDB()
  local provider = GetApiFunction("LIDCOUNCIL_RootDB")
  return provider and provider() or nil
end

local function GetCharacterKey()
  local provider = GetApiFunction("LIDCOUNCIL_GetCharacterKey")
  local value = provider and provider() or ""
  value = TrimText(value)
  if value ~= "" then
    return value
  end
  local name = type(UnitName) == "function" and UnitName("player") or ""
  local realm = type(GetRealmName) == "function" and GetRealmName() or ""
  name = TrimText(name)
  realm = TrimText(realm):gsub("%s+", "")
  if name ~= "" and realm ~= "" then
    return name .. "-" .. realm
  end
  return name ~= "" and name or "LocalPlayer"
end

local function GetCurrentSpecializationID()
  if type(GetSpecialization) ~= "function" or type(GetSpecializationInfo) ~= "function" then
    return nil
  end
  local specIndex = GetSpecialization()
  local ok, specID = false, nil
  if specIndex then
    ok, specID = pcall(GetSpecializationInfo, specIndex)
  end
  specID = ok and tonumber(specID) or nil
  return specID and specID > 0 and specID or nil
end

local function GetLootProfile(profileName)
  local provider = GetApiFunction("LIDCOUNCIL_GetLootProfile")
  if type(provider) ~= "function" then
    return nil, TrimText(profileName)
  end
  return provider(profileName)
end

local function ExtractItemID(value)
  local text = TrimText(value)
  local linkID = text:match("item:(%d+):") or text:match("item:(%d+)")
  if linkID then
    return tonumber(linkID)
  end
  return tonumber(text)
end

local function ResolveProfileStorageKey(profileName)
  local profile, resolvedName = GetLootProfile(profileName)
  local profileId = type(profile) == "table" and type(profile.meta) == "table" and TrimText(profile.meta.profileId) or ""
  if profileId ~= "" then
    return "profileId:" .. profileId
  end
  resolvedName = TrimText(resolvedName ~= "" and resolvedName or profileName)
  if resolvedName ~= "" then
    return "profileName:" .. string.lower(resolvedName)
  end
  return "profileName:unknown"
end

local function ResolveDifficultyStorageSuffix(options)
  if type(options) ~= "table" or options.perDifficulty ~= true then
    return ""
  end
  local difficultyID = tonumber(options.difficultyID or options.lootDifficultyID or options.difficulty)
  if difficultyID and difficultyID > 0 then
    return ":difficulty:" .. tostring(math.floor(difficultyID))
  end
  local difficultyName = TrimText(options.difficultyName)
  if difficultyName ~= "" then
    return ":difficultyName:" .. string.lower(difficultyName)
  end
  return ""
end

local function ResolveBaseItemStorageKey(itemID, itemLink, itemName)
  local resolvedItemID = ExtractItemID(itemID) or ExtractItemID(itemLink)
  if resolvedItemID then
    return "itemID:" .. tostring(resolvedItemID)
  end
  local link = TrimText(itemLink)
  if link ~= "" then
    return "link:" .. string.lower(link)
  end
  local name = TrimText(itemName)
  if name ~= "" then
    return "name:" .. string.lower(name)
  end
  return ""
end

local function ResolveItemStorageKey(itemID, itemLink, itemName, options)
  local itemKey = ResolveBaseItemStorageKey(itemID, itemLink, itemName)
  if itemKey == "" then
    return ""
  end
  return itemKey .. ResolveDifficultyStorageSuffix(options)
end

local function ResolveSpecStorageKey(options)
  local specID = type(options) == "table" and tonumber(options.specID) or nil
  return specID and specID > 0 and "specID:" .. tostring(math.floor(specID)) or nil
end

local function ResolveRecommendationCharacterKey(options)
  local requestedKey = type(options) == "table" and TrimText(options.characterKey) or ""
  return requestedKey ~= "" and requestedKey or GetCharacterKey()
end

local function EnsureRecommendationBucket(profileName, options)
  local root = RootDB()
  if type(root) ~= "table" then
    return nil
  end
  root.ui = type(root.ui) == "table" and root.ui or {}
  root.ui.personalRecommendations = type(root.ui.personalRecommendations) == "table" and root.ui.personalRecommendations or {}
  local characterKey = ResolveRecommendationCharacterKey(options)
  root.ui.personalRecommendations[characterKey] = type(root.ui.personalRecommendations[characterKey]) == "table"
    and root.ui.personalRecommendations[characterKey]
    or {}
  local profileKey = ResolveProfileStorageKey(profileName)
  root.ui.personalRecommendations[characterKey][profileKey] = type(root.ui.personalRecommendations[characterKey][profileKey]) == "table"
    and root.ui.personalRecommendations[characterKey][profileKey]
    or {}
  local profileBucket = root.ui.personalRecommendations[characterKey][profileKey]
  local specKey = ResolveSpecStorageKey(options)
  if not specKey then
    return profileBucket
  end
  profileBucket.__specRecommendations = type(profileBucket.__specRecommendations) == "table" and profileBucket.__specRecommendations or {}
  profileBucket.__specRecommendations[specKey] = type(profileBucket.__specRecommendations[specKey]) == "table"
    and profileBucket.__specRecommendations[specKey]
    or {}
  return profileBucket.__specRecommendations[specKey]
end

local function GetRecommendationBucket(profileName, options)
  local root = RootDB()
  if type(root) ~= "table" or type(root.ui) ~= "table" or type(root.ui.personalRecommendations) ~= "table" then
    return nil
  end
  local characterBucket = root.ui.personalRecommendations[ResolveRecommendationCharacterKey(options)]
  if type(characterBucket) ~= "table" then
    return nil
  end
  local profileBucket = characterBucket[ResolveProfileStorageKey(profileName)]
  if type(profileBucket) ~= "table" then
    return nil
  end
  local specKey = ResolveSpecStorageKey(options)
  if not specKey then
    return profileBucket
  end
  local specBucket = type(profileBucket.__specRecommendations) == "table" and profileBucket.__specRecommendations[specKey] or nil
  return type(specBucket) == "table" and specBucket or nil
end

local function SetPersonalRecommendation(profileName, itemID, itemLink, itemName, priority, option, options)
  local itemKey = ResolveItemStorageKey(itemID, itemLink, itemName, options)
  local resolvedPriority = math.floor(tonumber(priority) or 0)
  if itemKey == "" then
    return false, "Missing item identity."
  end
  if resolvedPriority <= 0 then
    return API.LIDCOUNCIL_ClearPersonalRecommendation(profileName, itemID, itemLink, itemName, options)
  end
  local bucket = EnsureRecommendationBucket(profileName, options)
  if type(bucket) ~= "table" then
    return false, "Personal recommendation storage is unavailable."
  end
  bucket[itemKey] = {
    priority = resolvedPriority,
    optionKey = TrimText(type(option) == "table" and (option.key or option.optionKey) or ""),
    optionLabel = TrimText(type(option) == "table" and (option.label or option.key or option.optionKey) or ""),
    itemID = ExtractItemID(itemID) or ExtractItemID(itemLink),
    itemLink = TrimText(itemLink),
    itemName = TrimText(itemName),
    perDifficulty = type(options) == "table" and options.perDifficulty == true or false,
    difficultyID = type(options) == "table" and tonumber(options.difficultyID or options.lootDifficultyID or options.difficulty) or nil,
    difficultyName = type(options) == "table" and TrimText(options.difficultyName) or "",
    specID = type(options) == "table" and tonumber(options.specID) or nil,
    characterKey = ResolveRecommendationCharacterKey(options),
    updatedAt = Now(),
  }
  return true, bucket[itemKey]
end

local function ClearPersonalRecommendation(profileName, itemID, itemLink, itemName, options)
  local itemKey = ResolveItemStorageKey(itemID, itemLink, itemName, options)
  if itemKey == "" then
    return false, "Missing item identity."
  end
  local bucket = GetRecommendationBucket(profileName, options)
  if type(bucket) == "table" then
    bucket[itemKey] = nil
  end
  return true
end

local function GetPersonalRecommendation(profileName, itemID, itemLink, itemName, options)
  local itemKey = ResolveItemStorageKey(itemID, itemLink, itemName, options)
  if itemKey == "" then
    return nil
  end
  local bucket = GetRecommendationBucket(profileName, options)
  local stored = type(bucket) == "table" and bucket[itemKey] or nil
  return type(stored) == "table" and stored or nil
end

local function GetPersonalRecommendationCharacterKeys()
  local root = RootDB()
  local keys = {}
  local seen = {}
  local function AddCharacterKey(value)
    local key = TrimText(value)
    if key ~= "" and not seen[key] then
      seen[key] = true
      keys[#keys + 1] = key
    end
  end

  local currentCharacterKey = GetCharacterKey()
  AddCharacterKey(currentCharacterKey)
  if type(root) == "table" then
    root.ui = type(root.ui) == "table" and root.ui or {}
    root.ui.personalRecommendationCharacters = type(root.ui.personalRecommendationCharacters) == "table"
      and root.ui.personalRecommendationCharacters
      or {}
    -- Older versions derived this menu from the shared session roster. Those
    -- entries cannot prove local ownership, so only an actual local login may
    -- register a character for this private, account-wide selector.
    root.ui.personalRecommendationCharactersMigrated = true
    if currentCharacterKey ~= "" then
      root.ui.personalRecommendationCharacters[currentCharacterKey] = true
    end
    for characterKey, isLocalCharacter in pairs(root.ui.personalRecommendationCharacters) do
      if isLocalCharacter == true then
        AddCharacterKey(characterKey)
      end
    end
  end
  table.sort(keys, function(left, right)
    return string.lower(left) < string.lower(right)
  end)
  return keys, currentCharacterKey
end

local function CopyRecommendationOptions(options, specID, perDifficulty)
  local copy = {}
  if type(options) == "table" then
    for key, value in pairs(options) do
      copy[key] = value
    end
  end
  copy.specID = tonumber(specID) or 0
  copy.perDifficulty = perDifficulty == true
  return copy
end

local function ResolveRecommendationForScope(profileName, itemID, itemLink, itemName, options, specID)
  local difficultyID = type(options) == "table" and tonumber(options.difficultyID or options.lootDifficultyID or options.difficulty) or nil
  local exactDifficulty = type(options) == "table" and options.exactDifficulty == true
  if (not difficultyID or difficultyID <= 0) and exactDifficulty then
    return nil
  end
  if difficultyID and difficultyID > 0 then
    local recommendation = GetPersonalRecommendation(profileName, itemID, itemLink, itemName, CopyRecommendationOptions(options, specID, true))
    if type(recommendation) == "table" or exactDifficulty then
      return recommendation
    end
  end
  return GetPersonalRecommendation(profileName, itemID, itemLink, itemName, CopyRecommendationOptions(options, specID, false))
end

local function ResolvePersonalRecommendation(profileName, itemID, itemLink, itemName, rollOptions, options)
  local hasRequestedSpec = type(options) == "table" and options.specID ~= nil
  local requestedSpecID = hasRequestedSpec and tonumber(options.specID) or nil
  local currentSpecID = nil
  if hasRequestedSpec then
    currentSpecID = requestedSpecID and requestedSpecID > 0 and requestedSpecID or nil
  else
    currentSpecID = GetCurrentSpecializationID()
  end
  local recommendation
  if currentSpecID then
    recommendation = ResolveRecommendationForScope(profileName, itemID, itemLink, itemName, options, currentSpecID)
  end
  if type(recommendation) ~= "table" then
    recommendation = ResolveRecommendationForScope(profileName, itemID, itemLink, itemName, options, 0)
  end
  if type(recommendation) ~= "table" then
    return nil, nil, "NONE"
  end
  local priority = math.floor(tonumber(recommendation.priority) or 0)
  if priority <= 0 then
    return recommendation, nil, "MISSING"
  end
  local option = type(rollOptions) == "table" and rollOptions[priority] or nil
  if type(option) ~= "table" then
    return recommendation, nil, "MISSING"
  end
  return recommendation, option, "OK"
end

API.LIDCOUNCIL_SetPersonalRecommendation = SetPersonalRecommendation
API.LIDCOUNCIL_ClearPersonalRecommendation = ClearPersonalRecommendation
API.LIDCOUNCIL_GetPersonalRecommendation = GetPersonalRecommendation
API.LIDCOUNCIL_ResolvePersonalRecommendation = ResolvePersonalRecommendation
API.LIDCOUNCIL_GetPersonalRecommendationCharacterKeys = GetPersonalRecommendationCharacterKeys
