local M = {}

local uv = vim.uv or vim.loop

---@type {cmd:string[], args:string[], enabled?:boolean, available?:boolean|string}[]
local commands = {
  {
    cmd = { "fd", "fdfind" },
    args = { "--type", "f", "--type", "l", "--color", "never", "-E", ".git" },
  },
  {
    cmd = { "rg" },
    args = { "--files", "--no-messages", "--color", "never", "-g", "!.git" },
  },
  {
    cmd = { "find" },
    args = { ".", "-type", "f", "-not", "-path", "*/.git/*" },
    enabled = vim.fn.has("win-32") == 0,
  },
}

---@param cmd? string
---@return string? cmd, string[]? args
function M.get_cmd(cmd)
  local checked = {} ---@type string[]
  for _, command in ipairs(commands) do
    if command.enabled ~= false and command.available ~= false and (not cmd or vim.tbl_contains(command.cmd, cmd)) then
      if command.available then
        assert(type(command.available) == "string", "available must be a string")
        return command.available, vim.deepcopy(command.args)
      end
      for _, c in ipairs(command.cmd) do
        table.insert(checked, c)
        if vim.fn.executable(c) == 1 then
          command.available = c
          return c, vim.deepcopy(command.args)
        end
      end
      command.available = false
    end
  end
  checked = #checked == 0 and cmd and { cmd } or checked
  checked = vim.tbl_map(function(c)
    return "`" .. c .. "`"
  end, checked)
  Snacks.notify.error("No supported finder found:\n- " .. table.concat(checked, "\n-"))
end

function M.get_fd()
  return M.get_cmd("fd")
end

---@param opts snacks.picker.files.Config
---@param filter snacks.picker.Filter
local function get_cmd(opts, filter)
  local cmd, args = M.get_cmd(opts.cmd)
  if not cmd or not args then
    return
  end
  local is_fd, is_fd_rg, is_find, is_rg = cmd == "fd" or cmd == "fdfind", cmd ~= "find", cmd == "find", cmd == "rg"

  -- exclude
  for _, e in ipairs(opts.exclude or {}) do
    if is_fd then
      vim.list_extend(args, { "-E", e })
    elseif is_rg then
      vim.list_extend(args, { "-g", "!" .. e })
    elseif is_find then
      table.insert(args, "-not")
      table.insert(args, "-path")
      table.insert(args, e)
    end
  end

  -- extensions
  local ft = opts.ft or {}
  ft = type(ft) == "string" and { ft } or ft
  ---@cast ft string[]
  for _, e in ipairs(ft) do
    if is_fd then
      table.insert(args, "-e")
      table.insert(args, e)
    elseif is_rg then
      table.insert(args, "-g")
      table.insert(args, "*." .. e)
    elseif is_find then
      table.insert(args, "-name")
      table.insert(args, "*." .. e)
    end
  end

  -- hidden
  if opts.hidden and is_fd_rg then
    table.insert(args, "--hidden")
  elseif not opts.hidden and is_find then
    vim.list_extend(args, { "-not", "-path", "*/.*" })
  end

  -- ignored
  if opts.ignored and is_fd_rg then
    args[#args + 1] = "--no-ignore"
  end

  -- follow
  if opts.follow then
    args[#args + 1] = "-L"
  end

  -- extra args
  vim.list_extend(args, opts.args or {})

  -- file glob
  ---@type string?
  local pattern, pargs = Snacks.picker.util.parse(filter.search)
  vim.list_extend(args, pargs)

  pattern = pattern ~= "" and pattern or nil
  if pattern then
    if is_fd then
      table.insert(args, pattern)
    elseif is_rg then
      table.insert(args, "--glob")
      table.insert(args, pattern)
    elseif is_find then
      table.insert(args, "-name")
      table.insert(args, pattern)
    end
  end

  -- dirs
  local dirs = opts.dirs or {}
  if opts.rtp then
    vim.list_extend(dirs, Snacks.picker.util.rtp())
  end
  if #dirs > 0 then
    dirs = vim.tbl_map(svim.fs.normalize, dirs) ---@type string[]
    if is_fd and not pattern then
      args[#args + 1] = "."
    end
    if is_find then
      table.remove(args, 1)
      for _, d in pairs(dirs) do
        table.insert(args, 1, d)
      end
    else
      vim.list_extend(args, dirs)
    end
  end

  return cmd, args
end

---@param opts snacks.picker.files.Config
---@type snacks.picker.finder
function M.files(opts, ctx)
  local cwd = not (opts.rtp or (opts.dirs and #opts.dirs > 0))
      and svim.fs.normalize(opts and opts.cwd or uv.cwd() or ".")
    or nil
  local cmd, args = get_cmd(opts, ctx.filter)
  if not cmd then
    return function() end
  end
  if opts.debug.files then
    Snacks.notify(cmd .. " " .. table.concat(args or {}, " "))
  end
  return require("snacks.picker.source.proc").proc({
    opts,
    {
      cmd = cmd,
      args = args,
      notify = not opts.live,
      ---@param item snacks.picker.finder.Item
      transform = function(item)
        item.cwd = cwd
        item.file = item.text
      end,
    },
  }, ctx)
end

---@param opts snacks.picker.proc.Config
---@type snacks.picker.finder
function M.zoxide(opts, ctx)
  return require("snacks.picker.source.proc").proc({
    opts,
    {
      cmd = "zoxide",
      args = { "query", "--list" },
      ---@param item snacks.picker.finder.Item
      transform = function(item)
        item.file = item.text
        item.dir = true
      end,
    },
  }, ctx)
end

return M
