local DR = select(2, ...)
local constants = DR.Modules["Core.Constants"]

local RollEngine = {}

local function resolveRange(category)
  return constants.ROLL.RANGE[category]
end

local function clampWeight(weight)
  local minWeight = DR.db.profile.minWeight
  local maxWeight = DR.db.profile.maxWeight
  if weight < minWeight then
    return minWeight
  end
  if weight > maxWeight then
    return maxWeight
  end
  return weight
end

function RollEngine:ClampWeight(weight)
  return clampWeight(tonumber(weight) or DR.db.profile.maxWeight)
end

function RollEngine:GetBaseRange(category)
  local range = resolveRange(category)
  if not range then
    return nil, nil
  end
  return range.min, range.max
end

function RollEngine:ComputeEffectiveScore(baseRoll, weight)
  return math.floor(baseRoll * (weight / 100))
end

function RollEngine:IsWeightedCategory(category)
  return constants.ROLL.WEIGHTED[tonumber(category)] == true
end

--- Score that competes: need rolls are multiplied by roll power; transmog,
--- offspec and other standard rolls use the raw roll.
function RollEngine:ComputeScoreFor(category, baseRoll, weight)
  if self:IsWeightedCategory(category) then
    return self:ComputeEffectiveScore(tonumber(baseRoll) or 0, tonumber(weight) or 100)
  end
  return math.floor(tonumber(baseRoll) or 0)
end

function RollEngine:GetCategoryPriority(category)
  return constants.ROLL.PRIORITY[tonumber(category)] or 0
end

function RollEngine:ApplyWinnerPenalty(weight)
  local factor = DR.db.profile.winnerPenaltyFactor
  local reduced = math.floor(weight * factor)
  return clampWeight(reduced)
end

--- amount defaults to the raider rate; the bench rate is passed explicitly.
function RollEngine:ApplyBossRegain(weight, amount)
  return clampWeight(weight + (tonumber(amount) or DR.db.profile.bossRegain))
end

--- Weights can be fractional (bench credit is +12.5); show one decimal only
--- when needed.
function RollEngine:FormatWeight(weight)
  weight = tonumber(weight) or 0
  if weight % 1 == 0 then
    return string.format("%d", weight)
  end
  return string.format("%.1f", weight)
end

function RollEngine:ApplyWeeklyRegain(weight)
  return clampWeight(weight + DR.db.profile.weeklyRegain)
end

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

  if not entry.guid or not entry.category or not entry.baseRoll then
    return nil
  end

  -- Passes are recorded so the leader can see who responded, but never compete.
  if tonumber(entry.category) == constants.ROLL.CATEGORY.PASS then
    return nil
  end

  local minWeight = DR.db.profile.minWeight
  local weight = tonumber(entry.weight) or minWeight

  return {
    guid = tostring(entry.guid),
    category = tonumber(entry.category),
    baseRoll = tonumber(entry.baseRoll),
    weight = clampWeight(weight),
  }
end

function RollEngine:ResolveWinner(submissions)
  local normalized = {}

  for _, entry in ipairs(submissions or {}) do
    local parsed = normalizeSubmission(entry)
    if parsed then
      parsed.effectiveScore = self:ComputeScoreFor(parsed.category, parsed.baseRoll, parsed.weight)
      normalized[#normalized + 1] = parsed
    end
  end

  table.sort(normalized, function(a, b)
    local pa, pb = self:GetCategoryPriority(a.category), self:GetCategoryPriority(b.category)
    if pa ~= pb then
      return pa > pb
    end
    if a.effectiveScore ~= b.effectiveScore then
      return a.effectiveScore > b.effectiveScore
    end
    return a.guid < b.guid
  end)

  if #normalized == 0 then
    return nil
  end

  local winner = normalized[1]
  local tieGuids = {}

  for i = 2, #normalized do
    local contender = normalized[i]
    if contender.category == winner.category and contender.effectiveScore == winner.effectiveScore then
      if #tieGuids == 0 then
        tieGuids[#tieGuids + 1] = winner.guid
      end
      tieGuids[#tieGuids + 1] = contender.guid
    else
      break
    end
  end

  local result = {
    winner = winner,
    sorted = normalized,
    tie = #tieGuids > 0,
    tieGuids = tieGuids,
  }

  return result
end

function RollEngine:RunRollOffRound(tiedEntries, category, roundNumber, rollProvider)
  local minRoll, maxRoll = self:GetBaseRange(category)
  local nextEntries = {}

  for _, entry in ipairs(tiedEntries or {}) do
    local nextRoll
    if rollProvider then
      nextRoll = rollProvider(entry.guid, minRoll, maxRoll, roundNumber)
    end
    nextRoll = tonumber(nextRoll) or math.random(minRoll, maxRoll)

    nextEntries[#nextEntries + 1] = {
      guid = entry.guid,
      category = category,
      baseRoll = nextRoll,
      weight = entry.weight,
    }
  end

  return self:ResolveWinner(nextEntries)
end

DR:RegisterModule("Domain.RollEngine", RollEngine)
