--- logger.lua — A feature-rich logging library for Lua
-- Supports log levels, multiple handlers, formatting, and filtering.
--
-- Usage:
--   local Logger = require("logger")
--   local log = Logger.new("MyApp")
--   log:info("Server started on port %d", 8080)
--   log:warn("Disk usage at %d%%", 92)
--   log:error("Connection refused: %s", err)

local Logger = {}
Logger.__index = Logger

-- ── Level definitions ──────────────────────────────────────────────────────

Logger.LEVELS = {
  TRACE = 1,
  DEBUG = 2,
  INFO  = 3,
  WARN  = 4,
  ERROR = 5,
  FATAL = 6,
  OFF   = 7,   -- disable all output
}

-- Reverse map: number → name
local LEVEL_NAMES = {}
for name, val in pairs(Logger.LEVELS) do
  LEVEL_NAMES[val] = name
end

-- ANSI colour codes (used by the ConsoleHandler when colours are enabled)
local COLOURS = {
  [1] = "\27[36m",   -- TRACE  cyan
  [2] = "\27[34m",   -- DEBUG  blue
  [3] = "\27[32m",   -- INFO   green
  [4] = "\27[33m",   -- WARN   yellow
  [5] = "\27[31m",   -- ERROR  red
  [6] = "\27[35m",   -- FATAL  magenta
  RESET = "\27[0m",
}

-- ── Utility helpers ────────────────────────────────────────────────────────

--- Returns an ISO-8601-style timestamp: "2025-06-08 14:32:01"
local function timestamp()
  return os.date("%Y-%m-%d %H:%M:%S")
end

--- Safe string formatting — falls back gracefully when fmt is not a string
local function safe_format(fmt, ...)
  if select("#", ...) == 0 then
    return tostring(fmt)
  end
  local ok, result = pcall(string.format, tostring(fmt), ...)
  return ok and result or (tostring(fmt) .. " " .. table.concat({...}, " "))
end

--- Pads or truncates a string to a fixed width
local function pad(s, width)
  s = tostring(s)
  if #s >= width then return s:sub(1, width) end
  return s .. string.rep(" ", width - #s)
end

-- ── Built-in Handlers ──────────────────────────────────────────────────────

--- ConsoleHandler — writes to stdout (or stderr for ERROR+)
-- Options:
--   colours  (bool, default true)  — enable ANSI colours
--   stderr   (bool, default false) — always write to stderr
local function ConsoleHandler(opts)
  opts = opts or {}
  local use_colour = (opts.colours ~= false)
  local force_stderr = opts.stderr or false

  return function(record)
    local line = record.formatted
    if use_colour then
      local c = COLOURS[record.level] or ""
      line = c .. line .. COLOURS.RESET
    end
    local dest = (force_stderr or record.level >= Logger.LEVELS.ERROR) and io.stderr or io.stdout
    dest:write(line .. "\n")
    dest:flush()
  end
end

--- FileHandler — appends log records to a file
-- Options:
--   path     (string, required) — path to the log file
--   max_size (number, optional) — rotate when file exceeds this many bytes
--   max_files(number, optional) — keep this many rotated files (default 5)
local function FileHandler(opts)
  assert(opts and opts.path, "FileHandler requires opts.path")
  local path      = opts.path
  local max_size  = opts.max_size   -- nil = never rotate
  local max_files = opts.max_files or 5
  local file

  local function open_file()
    file = assert(io.open(path, "a"), "Cannot open log file: " .. path)
  end

  local function rotate()
    if file then file:close(); file = nil end
    -- shift old files: .4 → .5, .3 → .4, …, .1 → .2, base → .1
    for i = max_files - 1, 1, -1 do
      os.rename(path .. "." .. i, path .. "." .. (i + 1))
    end
    os.rename(path, path .. ".1")
    open_file()
  end

  open_file()

  return function(record)
    if not file then open_file() end
    file:write(record.formatted .. "\n")
    file:flush()
    if max_size then
      local pos = file:seek("cur")
      if pos and pos >= max_size then rotate() end
    end
  end
end

--- MemoryHandler — stores records in a Lua table (useful for testing)
-- Returns handler, get_records() function
local function MemoryHandler(opts)
  opts = opts or {}
  local limit = opts.limit or 10000
  local records = {}

  local handler = function(record)
    if #records >= limit then table.remove(records, 1) end
    table.insert(records, record)
  end

  local function get_records() return records end
  local function clear() records = {} end

  return handler, get_records, clear
end

-- Expose built-ins
Logger.handlers = {
  ConsoleHandler = ConsoleHandler,
  FileHandler    = FileHandler,
  MemoryHandler  = MemoryHandler,
}

-- ── Formatter ─────────────────────────────────────────────────────────────
-- Tokens in the format string:
--   {time}    — timestamp
--   {level}   — padded level name (5 chars)
--   {name}    — logger name
--   {message} — the log message

local DEFAULT_FORMAT = "[{time}] [{level}] [{name}] {message}"

local function escape_replacement(s)
  -- In Lua's gsub, '%' in the replacement string is special; escape it.
  return (s:gsub("%%", "%%%%"))
end

local function apply_format(fmt, record)
  return (fmt
    :gsub("{time}",    escape_replacement(record.time))
    :gsub("{level}",   escape_replacement(pad(record.level_name, 5)))
    :gsub("{name}",    escape_replacement(record.name))
    :gsub("{message}", escape_replacement(record.message)))
end

-- ── Logger constructor ─────────────────────────────────────────────────────

--- Create a new logger instance.
-- @param name    string   — logger name shown in output
-- @param opts    table    — optional configuration:
--   level    (number)  — minimum level, default INFO
--   format   (string)  — format string, default DEFAULT_FORMAT
--   handlers (table)   — list of handler functions; defaults to ConsoleHandler
function Logger.new(name, opts)
  opts = opts or {}
  local self = setmetatable({}, Logger)
  self.name      = name or "root"
  self.level     = opts.level   or Logger.LEVELS.INFO
  self.format    = opts.format  or DEFAULT_FORMAT
  self._handlers = opts.handlers or { ConsoleHandler() }
  self._filters  = {}   -- list of predicate functions
  return self
end

-- ── Configuration API ──────────────────────────────────────────────────────

--- Set the minimum log level (Logger.LEVELS.*)
function Logger:set_level(level)
  assert(type(level) == "number", "level must be a number from Logger.LEVELS")
  self.level = level
  return self
end

--- Add a handler function
function Logger:add_handler(handler)
  table.insert(self._handlers, handler)
  return self
end

--- Remove all handlers and replace with the given ones
function Logger:set_handlers(handlers)
  self._handlers = handlers
  return self
end

--- Add a filter predicate — a function(record) → bool
-- If ANY filter returns false the record is dropped.
function Logger:add_filter(fn)
  table.insert(self._filters, fn)
  return self
end

--- Set the output format string
function Logger:set_format(fmt)
  self.format = fmt
  return self
end

-- ── Core emit logic ────────────────────────────────────────────────────────

function Logger:_emit(level, fmt, ...)
  if level < self.level then return end

  local record = {
    time       = timestamp(),
    level      = level,
    level_name = LEVEL_NAMES[level] or ("LVL" .. level),
    name       = self.name,
    message    = safe_format(fmt, ...),
  }
  record.formatted = apply_format(self.format, record)

  -- run filters
  for _, filter in ipairs(self._filters) do
    if not filter(record) then return end
  end

  -- dispatch to handlers
  for _, handler in ipairs(self._handlers) do
    local ok, err = pcall(handler, record)
    if not ok then
      io.stderr:write("[logger] handler error: " .. tostring(err) .. "\n")
    end
  end
end

-- ── Convenience level methods ──────────────────────────────────────────────

function Logger:trace(fmt, ...) self:_emit(Logger.LEVELS.TRACE, fmt, ...) end
function Logger:debug(fmt, ...) self:_emit(Logger.LEVELS.DEBUG, fmt, ...) end
function Logger:info (fmt, ...) self:_emit(Logger.LEVELS.INFO,  fmt, ...) end
function Logger:warn (fmt, ...) self:_emit(Logger.LEVELS.WARN,  fmt, ...) end
function Logger:error(fmt, ...) self:_emit(Logger.LEVELS.ERROR, fmt, ...) end
function Logger:fatal(fmt, ...) self:_emit(Logger.LEVELS.FATAL, fmt, ...) end

--- Log at a given numeric level
function Logger:log(level, fmt, ...)
  self:_emit(level, fmt, ...)
end

--- Returns true if the given level would be emitted
function Logger:is_enabled(level)
  return level >= self.level
end

-- ── Child loggers ──────────────────────────────────────────────────────────

--- Create a child logger that inherits handlers/format but has its own name.
function Logger:child(name, opts)
  opts = opts or {}
  return Logger.new(self.name .. "." .. name, {
    level    = opts.level   or self.level,
    format   = opts.format  or self.format,
    handlers = opts.handlers or self._handlers,
  })
end

-- ── Module-level root logger ───────────────────────────────────────────────

Logger._root = Logger.new("root")

--- Get / lazily create a named logger backed by the root's handlers.
local _registry = {}
function Logger.get(name)
  if not _registry[name] then
    _registry[name] = Logger._root:child(name)
  end
  return _registry[name]
end

return Logger
