--[[ Core/HousingResult.lua - turning Blizzard's result codes into sentences.

     Every housing call answers with an Enum.HousingResult: 0 is Success and
     the other 111 say what went wrong. The addon used to throw those away.
     Import showed "Import failed (83)" and then guessed at a reason - "make
     sure you're at your house, and if you are, the code may no longer be
     valid" - which is two guesses, and on 2026-08-18 a player wrote on
     CurseForge that he tried two imports, both failed, and deleted the addon.
     Code 83 is NotInsideHouse. The client knew exactly what was wrong the
     whole time and we did not pass it on.

     Only the codes a player can actually hit while importing are written out.
     Anything else falls back to its enumerator name, which is still a real
     answer someone can search for or paste into a bug report - far better
     than a bare number.

     Names come from Enum.HousingResult at runtime where the client exposes
     it, and from the table below otherwise, so a client that renames a code
     does not leave us printing a stale name. ]]--

local _, ns = ...
local R = {}
ns.Result = R

-- The ones worth a sentence of their own. Second person, present tense, and
-- each one says what to DO - a player who reads "NotOnOwnedPlot" still has to
-- work out that they should go home.
local EXPLAINED = {
  [0]   = nil,   -- Success needs no words
  [2]   = "You can't import while you're in combat.",
  [3]   = "This share code isn't valid any more. The builder may have replaced it.",
  [6]   = "The server refused the import without saying why. Worth trying again in a moment.",
  [7]   = "You can't import a blueprint here. Stand inside your own house or on your plot.",
  [9]   = "The server doesn't know this blueprint. The code may have expired.",
  [10]  = "Your house doesn't meet this blueprint's requirements.",
  [11]  = "This blueprint needs a room to go in. Place the room first, then import.",
  [12]  = "This blueprint isn't the right type for where you're standing.",
  [13]  = "Wrong place for this type of blueprint: an interior has to be imported inside, an exterior outside.",
  [15]  = "This blueprint was made in an older version of the game and can't be imported.",
  [20]  = "You can't afford this import.",
  [33]  = "This blueprint is built for a different type of house than yours.",
  [51]  = "This blueprint belongs to the other faction.",
  [64]  = "Your room doesn't have enough decor budget left for this blueprint.",
  [67]  = "You've reached the limit for placed decor. Remove something first.",
  [70]  = "Your decor storage is full.",
  [72]  = "You're missing a dye this blueprint uses.",
  [73]  = "This blueprint needs an expansion you don't have.",
  [80]  = "Open Edit Mode on your house first, then import.",
  [82]  = "Open the layout editor first, then import.",
  [83]  = "You need to be inside your house to import this.",
  [84]  = "You need to be standing on a plot you own.",
  [87]  = "You don't have permission to change this house.",
  [92]  = "You need a house of your own before you can import a blueprint.",
  [99]  = "Housing services are unavailable right now. This one is Blizzard's end, not yours.",
  [104] = "Too many requests at once. Wait a few seconds and try again.",
  [107] = "You haven't collected this house type yet.",
  [108] = "You haven't collected a room this blueprint needs.",
  [110] = "You haven't collected a room theme this blueprint uses.",
}

-- Fallback names, so an unexplained code still reads as something. Kept short
-- deliberately: this is the list of what the client CAN answer, and only the
-- import-reachable half is spelled out above.
local NAMES = {
  [0] = "Success", [1] = "AccountBanned", [2] = "ActionLockedByCombat",
  [3] = "BlueprintCodeInvalid", [4] = "BlueprintDyeFailed",
  [5] = "BlueprintGenericExportError", [6] = "BlueprintGenericImportError",
  [7] = "BlueprintLocationInvalid", [8] = "BlueprintNameInvalid",
  [9] = "BlueprintNotFound", [10] = "BlueprintRequirementsUnmet",
  [11] = "BlueprintRoomPlacementRequired", [12] = "BlueprintTypeInvalid",
  [13] = "BlueprintTypeLocationInvalid", [14] = "BlueprintStorageLimit",
  [15] = "BlueprintVersionInvalid", [19] = "BoundToStartingArea",
  [20] = "CannotAfford", [22] = "CollisionInvalid", [23] = "DbError",
  [26] = "DecorNotFound", [27] = "DecorNotFoundInStorage",
  [29] = "FilterRejected", [33] = "FixtureHouseTypeMismatch",
  [37] = "GenericFailure", [41] = "HouseEditLockFailed", [50] = "HouseNotFound",
  [51] = "IncorrectFaction", [53] = "InvalidDistance", [56] = "InvalidHouse",
  [57] = "InvalidInstance", [58] = "InvalidInteraction", [61] = "InvalidMap",
  [63] = "InvalidRoomLayout", [64] = "InsufficientRoomBudget",
  [65] = "LockedByOtherPlayer", [67] = "MaxPlacedDecorReached",
  [70] = "MaxStorageDecorReached", [71] = "MissingCoreFixture",
  [72] = "MissingDye", [73] = "MissingExpansionAccess",
  [80] = "NotInDecorEditMode", [81] = "NotInFixtureEditMode",
  [82] = "NotInLayoutEditMode", [83] = "NotInsideHouse",
  [84] = "NotOnOwnedPlot", [85] = "OperationAborted", [87] = "PermissionDenied",
  [88] = "PlacementTargetInvalid", [91] = "PlotNotFound", [92] = "PlotNotVacant",
  [95] = "RoomNotFound", [98] = "RpcFailure", [99] = "ServiceNotAvailable",
  [101] = "TimeoutLimit", [103] = "TokenRequired", [104] = "TooManyRequests",
  [105] = "TransactionFailure", [107] = "UncollectedHouseType",
  [108] = "UncollectedRoom", [109] = "UncollectedRoomMaterial",
  [110] = "UncollectedRoomTheme",
}

-- Built once, from the client's own enum where it has one.
local liveNames
local function nameFor(code)
  if liveNames == nil then
    liveNames = false
    if type(Enum) == "table" and type(Enum.HousingResult) == "table" then
      local t = {}
      for k, v in pairs(Enum.HousingResult) do
        if type(v) == "number" then t[v] = k end
      end
      liveNames = t
    end
  end
  return (liveNames and liveNames[code]) or NAMES[code]
end

------------------------------------------------------------------------
-- Requirement flags. Every blueprint-contents reply carries
-- blockingRequirementFlags: a bitmask of what would stop THIS player
-- importing THIS build, computed by the server against their house. It was
-- stored and never read. Read from the client's own enum where present;
-- the fallback bits below were confirmed against Enum.
-- HousingBlueprintUnmetRequirementFlags on live, 2026-08-19.
------------------------------------------------------------------------
local FLAG_BITS = {
  InsufficientBudget = 1, MissingRoom = 2, MissingFixture = 4, MissingDecor = 8,
  MissingDye = 16, MismatchedExteriorFaction = 32, HouseTypeLocked = 64,
  HouseSizeLocked = 128,
}
local FLAG_TEXT = {
  InsufficientBudget        = "it costs more decor budget than your house has",
  MissingRoom               = "it needs a room your house doesn't have",
  MissingFixture            = "it needs a fixture your house doesn't have",
  MissingDecor              = "you don't own some of its decor",
  MissingDye                = "you're missing a dye it uses",
  MismatchedExteriorFaction = "it's built for the other faction's plot",
  HouseTypeLocked           = "it uses a house type you haven't unlocked",
  HouseSizeLocked           = "it uses a house size you haven't unlocked",
}

local function flagBits()
  local e = type(Enum) == "table" and Enum.HousingBlueprintUnmetRequirementFlags
  if type(e) == "table" then
    local t = {}
    for name, bit in pairs(e) do
      if type(bit) == "number" and bit > 0 then t[name] = bit end
    end
    return t
  end
  return FLAG_BITS
end

--- Names of the flags set in a mask, in ascending bit order.
function R.FlagNames(mask)
  local out = {}
  if type(mask) ~= "number" or mask == 0 then return out end
  local bits = flagBits()
  local ordered = {}
  for name, bit in pairs(bits) do ordered[#ordered + 1] = { name, bit } end
  table.sort(ordered, function(a, b) return a[2] < b[2] end)
  for _, nb in ipairs(ordered) do
    if bit and bit.band then
      if bit.band(mask, nb[2]) ~= 0 then out[#out + 1] = nb[1] end
    elseif math.floor(mask / nb[2]) % 2 == 1 then
      out[#out + 1] = nb[1]
    end
  end
  return out
end

-- Short verdict per flag, for the line that leads the detail pane. Written
-- as things to reach, because all but the faction one ARE reachable - level
-- up, unlock a room, buy a dye - and a wall of "can't" is what made a level-1
-- player delete the addon.
local FLAG_HEAD = {
  InsufficientBudget        = "Needs more decor budget",
  MissingRoom               = "Needs a room you don't have yet",
  MissingFixture            = "Needs a fixture you don't have yet",
  MissingDecor              = "Needs decor you don't own yet",
  MissingDye                = "Needs a dye you don't have yet",
  MismatchedExteriorFaction = "Built for the other faction",
  HouseTypeLocked           = "Needs a house type you haven't unlocked",
  HouseSizeLocked           = "Needs a house size you haven't unlocked",
}
-- One word for the card chip, where "Locked" said only that something was.
local FLAG_CHIP = {
  InsufficientBudget = "Budget", MissingRoom = "Room", MissingFixture = "Fixture",
  MissingDecor = "Decor", MissingDye = "Dye", MismatchedExteriorFaction = "Faction",
  HouseTypeLocked = "House", HouseSizeLocked = "Size",
}
-- The one thing no amount of playing fixes.
local HARD = { MismatchedExteriorFaction = true }

--- For a blocking mask: head (short verdict), detail (the rest, or nil),
--- hard (true when nothing the player does will change it).
function R.FlagVerdict(mask)
  local names = R.FlagNames(mask)
  if #names == 0 then return nil end
  local hard = false
  for _, n in ipairs(names) do if HARD[n] then hard = true end end
  -- lead with a hard reason if there is one, else the first
  local lead = names[1]
  for _, n in ipairs(names) do if HARD[n] then lead = n; break end end
  local rest = {}
  for _, n in ipairs(names) do
    if n ~= lead then rest[#rest + 1] = FLAG_TEXT[n] or n end
  end
  local detail
  if #rest == 1 then detail = "also " .. rest[1]
  elseif #rest > 1 then
    detail = "also " .. table.concat(rest, ", ", 1, #rest - 1) .. ", and " .. rest[#rest]
  end
  return FLAG_HEAD[lead] or ("Can't import: " .. lead), detail, hard, FLAG_CHIP[lead] or "Locked"
end

--- One sentence for a blocking mask, or nil when nothing blocks.
--- "Can't import this: it costs more decor budget than your house has, and
--- it needs a room your house doesn't have."
function R.ExplainFlags(mask)
  local names = R.FlagNames(mask)
  if #names == 0 then return nil end
  local parts = {}
  for _, n in ipairs(names) do parts[#parts + 1] = FLAG_TEXT[n] or n end
  local joined
  if #parts == 1 then joined = parts[1]
  else joined = table.concat(parts, ", ", 1, #parts - 1) .. ", and " .. parts[#parts] end
  return "Can't import this: " .. joined .. "."
end

--- True when the code means the operation succeeded.
function R.IsSuccess(code)
  return code == 0 or code == nil
end

--- A sentence a player can act on. Never returns nil for a failure: an
--- unexplained code still comes back as its name, and an unknown one as a
--- number, because "we don't know" beats silence.
function R.Explain(code)
  if R.IsSuccess(code) then return nil end
  if type(code) ~= "number" then
    return "The import failed (" .. tostring(code) .. ")."
  end
  local said = EXPLAINED[code]
  if said then return said end
  local name = nameFor(code)
  if name then
    return "The import failed: " .. name .. " (" .. code .. ")."
  end
  return "The import failed with code " .. code .. "."
end
