local Cache = require("lazy.core.cache")

local M = {}

---@type table<string, {file: string, hash?:string}>
M.modules = {}

function M.add(modname, file)
  if not M.modules[modname] then
    M.modules[modname] = { file = file }
  end
end

---@param modname string
function M.load(modname)
  if type(package.loaded[modname]) == "table" then
    return package.loaded[modname]
  end

  local info = M.modules[modname]
  if info then
    local err
    ---@type string|fun()|nil
    local chunk = Cache.get(modname)

    if not chunk then
      vim.schedule(function()
        vim.notify("loading " .. modname)
      end)
      chunk, err = loadfile(info.file)
      if chunk then
        Cache.set(modname, string.dump(chunk))
        info.hash = info.hash or Cache.hash(info.file)
      end
    end

    if type(chunk) == "string" then
      chunk, err = loadstring(chunk --[[@as string]], "@" .. info.file)
    end

    if not chunk then
      error(err)
    end

    ---@type table
    local mod = chunk()
    package.loaded[modname] = mod
    return mod
  end
end

local function _add_module(dir, modname)
  local d = vim.loop.fs_opendir(dir, nil, 100)
  if d then
    ---@type {name: string, type: "file"|"directory"|"link"}[]
    local entries = vim.loop.fs_readdir(d)
    while entries do
      for _, entry in ipairs(entries) do
        local path = dir .. "/" .. entry.name
        if entry.type == "directory" then
          _add_module(path, modname and (modname .. "." .. entry.name) or entry.name)
        else
          local childname = entry.name:match("^(.*)%.lua$")
          if childname then
            local child = entry.name == "init.lua" and modname or modname and (modname .. "." .. childname) or childname
            if child then
              M.add(child, path)
            end
          end
        end
      end
      entries = vim.loop.fs_readdir(d)
    end
    vim.loop.fs_closedir(d)
  end
end

function M.add_module(path)
  if path:find("/lua/?$") then
    return _add_module(path)
  end
  ---@type string
  local modname = path:match("/lua/(.*)/?")
  assert(modname)
  modname = modname:gsub("/", ".")
  if vim.loop.fs_stat(path .. ".lua") then
    M.add(modname, path .. ".lua")
  end
  _add_module(path, modname)
end

function M.setup()
  -- load cache
  local value = Cache.get("cache.modules")
  if value then
    M.modules = vim.json.decode(value)
    for k, v in pairs(M.modules) do
      if Cache.hash(v.file) ~= v.hash then
        Cache.del(k)
        M.changed = true
        M.modules[k] = nil
      end
    end
  end

  -- preload core modules
  local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h")
  for _, name in ipairs({ "util", "config", "loader", "state" }) do
    local modname = "lazy.core." .. name
    M.add(modname, root .. "/core/" .. name:gsub("%.", "/") .. ".lua")
  end

  table.insert(package.loaders, 2, function(modname)
    if M.modules[modname] then
      return function()
        return M.load(modname)
      end
    end
  end)
  return M
end

function M.save()
  local value = {}
  for k, v in pairs(M.modules) do
    if v.hash then
      value[k] = v
    end
  end
  Cache.set("cache.modules", vim.json.encode(value))
end

return M