-- ---------------------------------------------------------------------------
--  Small shared helpers. Kept deliberately thin.
-- ---------------------------------------------------------------------------

Util = {}

function Util.trim(s)
  if type(s) ~= 'string' then return '' end
  return (s:gsub('^%s+', ''):gsub('%s+$', ''))
end

function Util.clamp(v, lo, hi)
  if v < lo then return lo end
  if v > hi then return hi end
  return v
end

function Util.round(v, places)
  local m = 10 ^ (places or 0)
  return math.floor(v * m + 0.5) / m
end

--- Deep copy; cycles are not expected in config/state tables.
function Util.copy(t)
  if type(t) ~= 'table' then return t end
  local out = {}
  for k, v in pairs(t) do out[k] = Util.copy(v) end
  return out
end

function Util.count(t)
  local n = 0
  for _ in pairs(t) do n = n + 1 end
  return n
end

--- Group separated money, e.g. 1234567 -> "1,234,567"
function Util.money(n)
  local s = tostring(math.floor(math.abs(n or 0)))
  local out = s:reverse():gsub('(%d%d%d)', '%1,'):reverse()
  out = out:gsub('^,', '')
  return (n or 0) < 0 and ('-' .. out) or out
end

--- Names are stored capitalised regardless of how they were typed.
function Util.properName(s)
  s = Util.trim(s):lower()
  return (s:gsub("([^%s'-]+)", function(word)
    return word:sub(1, 1):upper() .. word:sub(2)
  end))
end

--- Validation shared by client (live feedback) and server (enforcement), so
--- the two can never disagree about what is acceptable.
Rules = {}

Rules.username = {
  min = 3, max = 20,
  pattern = '^[%w_]+$',
  hint = '3-20 characters, letters, numbers and underscore only',
}

Rules.password = {
  min = 8, max = 72,
  hint = 'at least 8 characters, with a letter and a number',
}

Rules.name = {
  min = 2, max = 20,
  pattern = "^[%a][%a'%-]*$",
  hint = "letters, apostrophes and hyphens",
}

function Rules.checkUsername(v)
  v = Util.trim(v or '')
  if #v < Rules.username.min then return false, 'Too short - ' .. Rules.username.hint end
  if #v > Rules.username.max then return false, 'Too long - ' .. Rules.username.hint end
  if not v:match(Rules.username.pattern) then return false, 'Only letters, numbers and underscore' end
  return true
end

function Rules.checkPassword(v)
  v = v or ''
  if #v < Rules.password.min then return false, 'At least 8 characters' end
  if #v > Rules.password.max then return false, 'Too long' end
  if not v:match('%a') then return false, 'Needs at least one letter' end
  if not v:match('%d') then return false, 'Needs at least one number' end
  return true
end

function Rules.checkName(v)
  v = Util.trim(v or '')
  if #v < Rules.name.min then return false, 'Too short' end
  if #v > Rules.name.max then return false, 'Too long' end
  if not v:match(Rules.name.pattern) then return false, "Letters, ' and - only" end
  return true
end

--- Date of birth: accepts YYYY-MM-DD, must be a real date and an adult age.
function Rules.checkDob(v)
  v = Util.trim(v or '')
  local y, m, d = v:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
  if not y then return false, 'Use YYYY-MM-DD' end
  y, m, d = tonumber(y), tonumber(m), tonumber(d)
  if m < 1 or m > 12 then return false, 'Month must be 01-12' end
  local mdays = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
  if (y % 4 == 0 and y % 100 ~= 0) or y % 400 == 0 then mdays[2] = 29 end
  if d < 1 or d > mdays[m] then return false, 'That day does not exist' end
  if y < 1930 or y > 2010 then return false, 'Year must be between 1930 and 2010' end
  return true
end
