--!nocheck
--!nolint

type Range = { min: number?, max: number?, minExclusive: boolean?, maxExclusive: boolean? }

local check = {}
local ValidatorMt = {
  __index = {
    validate = function(self, v)
      local ok, err = pcall(check[self.kind], self, v)
      if ok then
        return true, nil
      end
      return false, err
    end,
  },
}

local function is_leaf_error(msg)
  return msg:match("^expected ")
    or msg:match("^out of ")
    or msg:match("^value ")
    or msg:match("^unexpected ")
    or msg:match("^string ")
end

local function inRange(v, r)
  if r.min and ((r.minExclusive and v <= r.min) or v < r.min) then
    return false
  end
  if r.max and ((r.maxExclusive and v >= r.max) or v > r.max) then
    return false
  end
  return true
end

local function compile(s)
  local m = {}
  for k, v in pairs(s) do
    m[k] = type(v) == "table" and rawget(v, "kind") and v or v
  end
  return m
end

function check.string(self, v)
  if type(v) ~= "string" then
    error("expected string, got " .. type(v), 0)
  end
  if self.p and not string.match(v, self.p) then
    error("string does not match pattern", 0)
  end
end

function check.number(self, v)
  if type(v) ~= "number" then
    error("expected number, got " .. type(v), 0)
  end
  if self.i and v % 1 ~= 0 then
    error("expected integer, got " .. tostring(v), 0)
  end
  if self.r and not inRange(v, self.r) then
    error("out of range", 0)
  end
end

function check.boolean(self, v)
  if type(v) ~= "boolean" then
    error("expected boolean, got " .. type(v), 0)
  end
end

check["nil"] = function(self, v)
  if v ~= nil then
    error("expected nil, got " .. type(v), 0)
  end
end

function check.struct(self, t)
  if type(t) ~= "table" then
    error("expected table, got " .. type(t), 0)
  end
  for k, f in pairs(self.m) do
    local ok, err = f:validate(t[k])
    if not ok then
      if is_leaf_error(err) then
        error(tostring(k) .. ": " .. err, 0)
      else
        error(tostring(k) .. "." .. err, 0)
      end
    end
  end
  for k in pairs(t) do
    if not self.m[k] then
      error("unexpected key: " .. tostring(k), 0)
    end
  end
end

function check.array(self, a)
  if type(a) ~= "table" then
    error("expected array, got " .. type(a), 0)
  end
  for i, v in ipairs(a) do
    local ok, err = self.t:validate(v)
    if not ok then
      if is_leaf_error(err) then
        error("[" .. tostring(i) .. "]: " .. err, 0)
      else
        error("[" .. tostring(i) .. "]." .. err, 0)
      end
    end
  end
end

function check.optional(self, v)
  if v == nil then
    return
  end
  local ok, err = self.t:validate(v)
  if not ok then
    error(err, 0)
  end
end

function check.union(self, v)
  for _, t in ipairs(self.s) do
    local ok = t:validate(v)
    if ok then
      return
    end
  end
  error("value did not match any union member", 0)
end

function check.literal(self, v)
  if v ~= self.v then
    error("expected " .. tostring(self.v) .. ", got " .. tostring(v), 0)
  end
end

function string(o: { default: string? }?): string
  local v = { kind = "string" }
  if o and o.default ~= nil then
    v.default = o.default
  end
  return setmetatable(v, ValidatorMt) :: string
end

function number(o: { integer: boolean?, range: Range?, default: number? }?): number
  local v = { kind = "number" }
  if o then
    if o.integer then
      v.i = true
    end
    if o.range then
      v.r = o.range
    end
    if o.default ~= nil then
      v.default = o.default
    end
  end
  return setmetatable(v, ValidatorMt) :: number
end

function integer(): number
  return number({ integer = true })
end

function boolean(o: { default: boolean? }?): boolean
  local v = { kind = "boolean" }
  if o and o.default ~= nil then
    v.default = o.default
  end
  return setmetatable(v, ValidatorMt) :: boolean
end

function none(): nil
  return setmetatable({ kind = "nil" }, ValidatorMt) :: nil
end

function struct<T>(s: T): T
  return setmetatable({ kind = "struct", m = compile(s) }, ValidatorMt) :: T
end

function array<T>(item: T): { T }
  return setmetatable({ kind = "array", t = item }, ValidatorMt) :: { T }
end

function optional<T>(inner: T): T?
  return setmetatable({ kind = "optional", t = inner, default = inner.default }, ValidatorMt) :: T?
end

function union<T, U>(a: T, b: U): T | U
  return setmetatable({ kind = "union", s = { a, b } }, ValidatorMt) :: T | U
end

function literal<T>(value: T): T
  return setmetatable({ kind = "literal", v = value }, ValidatorMt) :: T
end

function range(o: Range): number
  return number({ range = o })
end

function pattern(p: string): string
  return setmetatable({ kind = "string", p = p }, ValidatorMt) :: string
end

function validate(v: any, value: any): (boolean, string?)
  return v:validate(value)
end

function build<T>(s: T)
  local m = s.m
  return setmetatable({ schema = s }, {
    __call = function(_, data)
      if type(data) ~= "table" then
        error("expected table, got " .. type(data), 0)
      end
      local merged = {}
      for k, v in pairs(data) do
        merged[k] = v
      end
      if m then
        for k, f in pairs(m) do
          if merged[k] == nil and f.default ~= nil then
            merged[k] = f.default
          end
        end
      end
      local ok, err = s:validate(merged)
      if not ok then
        error(err, 0)
      end
      return merged
    end,
    __index = {
      type = function(self): T
        return self.schema :: T
      end,
      validate = function(self, value: any): (boolean, string?)
        return self.schema:validate(value)
      end,
    },
  })
end

type Regex = {
  captures: (regex: Regex, content: string) -> { { string } },
  replace: (regex: Regex, content: string, replacement: string, limit: number?) -> string,
  is_match: (regex: Regex, content: string) -> boolean,
}

function regex(expression: string): Regex
  return astra_internal__regex(expression)
end

return {
  types = {
    string = string,
    number = number,
    integer = integer,
    boolean = boolean,
    none = none,
    struct = struct,
    array = array,
    optional = optional,
    union = union,
    literal = literal,
    range = range,
    pattern = pattern,
    validate = validate,
    build = build,
  },
  regex = regex,
}
