local Git = require("snacks.explorer.git")
local Tree = require("snacks.explorer.tree")

---@class snacks.explorer.diagnostic.Action: snacks.picker.Action
---@field severity? number
---@field up? boolean

local uv = vim.uv or vim.loop

local M = {}

---@param picker snacks.Picker
---@param path string
function M.reveal(picker, path)
  if picker.closed then
    return
  end
  for item, idx in picker:iter() do
    if item.file == path then
      picker.list:view(idx)
      return true
    end
  end
end

---@param prompt string
---@param fn fun()
function M.confirm(prompt, fn)
  Snacks.picker.select({ "No", "Yes" }, { prompt = prompt }, function(_, idx)
    if idx == 2 then
      fn()
    end
  end)
end

---@param picker snacks.Picker
---@param opts? {target?: boolean|string, refresh?: boolean}
function M.update(picker, opts)
  opts = opts or {}
  local cwd = picker:cwd()
  local target = type(opts.target) == "string" and opts.target or nil --[[@as string]]
  local refresh = opts.refresh or Tree:is_dirty(cwd, picker.opts)
  if target and not Tree:is_visible(cwd, target) then
    Tree:open(target)
    refresh = true
  end

  -- when searching, restore explorer view first
  if picker.input.filter.meta.searching then
    picker.input:set("", "")
    picker.list.win:focus()
    refresh = true
  end

  if not refresh and target then
    return M.reveal(picker, target)
  end
  if opts.target ~= false then
    picker.list:set_target()
  end
  picker:find({
    on_done = function()
      if target then
        M.reveal(picker, target)
      end
    end,
  })
end

---@class snacks.explorer.actions
---@field [string] snacks.picker.Action.spec
M.actions = {}

function M.actions.explorer_focus(picker)
  picker:set_cwd(picker:dir())
  picker:find()
end

function M.actions.explorer_open(_, item)
  if item then
    local _, err = vim.ui.open(item.file)
    if err then
      Snacks.notify.error("Failed to open `" .. item.file .. "`:\n- " .. err)
    end
  end
end

function M.actions.explorer_yank(picker)
  local files = {} ---@type string[]
  if vim.fn.mode():find("^[vV]") then
    picker.list:select()
  end
  for _, item in ipairs(picker:selected({ fallback = true })) do
    table.insert(files, Snacks.picker.util.path(item))
  end
  picker.list:set_selected() -- clear selection
  local value = table.concat(files, "\n")
  vim.fn.setreg(vim.v.register or "+", value, "l")
  Snacks.notify.info("Yanked " .. #files .. " files")
end

function M.actions.explorer_up(picker)
  picker:set_cwd(vim.fs.dirname(picker:cwd()))
  picker:find()
end

function M.actions.explorer_close(picker, item)
  if not item then
    return
  end
  local dir = picker:dir()
  if item.dir and not item.open then
    dir = vim.fs.dirname(dir)
  end
  Tree:close(dir)
  M.update(picker, { target = dir, refresh = true })
end

function M.actions.explorer_update(picker)
  Tree:refresh(picker:cwd())
  M.update(picker)
end

function M.actions.explorer_close_all(picker)
  Tree:close_all(picker:cwd())
  M.update(picker, { refresh = true })
end

function M.actions.explorer_git_next(picker, item)
  local node = Git.next(picker:cwd(), item and item.file)
  if node then
    M.update(picker, { target = node.path })
  end
end

function M.actions.explorer_paste(picker)
  local files = vim.split(vim.fn.getreg(vim.v.register or "+") or "", "\n", { plain = true })
  files = vim.tbl_filter(function(file)
    return file ~= "" and vim.fn.filereadable(file) == 1
  end, files)

  if #files == 0 then
    return Snacks.notify.warn(("The `%s` register does not contain any files"):format(vim.v.register or "+"))
  end
  local dir = picker:dir()
  Snacks.picker.util.copy(files, dir)
  Tree:refresh(dir)
  Tree:open(dir)
  M.update(picker, { target = dir })
end

function M.actions.explorer_git_prev(picker, item)
  local node = Git.next(picker:cwd(), item and item.file, true)
  if node then
    M.update(picker, { target = node.path })
  end
end

function M.actions.explorer_add(picker)
  Snacks.input({
    prompt = 'Add a new file or directory (directories end with a "/")',
  }, function(value)
    if not value or value:find("^%s$") then
      return
    end
    local path = svim.fs.normalize(picker:dir() .. "/" .. value)
    local is_file = value:sub(-1) ~= "/"
    local dir = is_file and vim.fs.dirname(path) or path
    if is_file and uv.fs_stat(path) then
      Snacks.notify.warn("File already exists:\n- `" .. path .. "`")
      return
    end
    vim.fn.mkdir(dir, "p")
    if is_file then
      io.open(path, "w"):close()
    end
    Tree:open(dir)
    Tree:refresh(dir)
    M.update(picker, { target = path })
  end)
end

function M.actions.explorer_rename(picker, item)
  if not item then
    return
  end
  Snacks.rename.rename_file({
    from = item.file,
    on_rename = function(new, old)
      Tree:refresh(vim.fs.dirname(old))
      Tree:refresh(vim.fs.dirname(new))
      M.update(picker, { target = new })
    end,
  })
end

function M.actions.explorer_move(picker)
  ---@type string[]
  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected())
  if #paths == 0 then
    Snacks.notify.warn("No files selected to move. Renaming instead.")
    return M.actions.explorer_rename(picker, picker:current())
  end
  local target = picker:dir()
  local what = #paths == 1 and vim.fn.fnamemodify(paths[1], ":p:~:.") or #paths .. " files"
  local t = vim.fn.fnamemodify(target, ":p:~:.")

  M.confirm("Move " .. what .. " to " .. t .. "?", function()
    for _, from in ipairs(paths) do
      local to = target .. "/" .. vim.fn.fnamemodify(from, ":t")
      Snacks.rename.rename_file({ from = from, to = to })
      Tree:refresh(vim.fs.dirname(from))
    end
    Tree:refresh(target)
    picker.list:set_selected() -- clear selection
    M.update(picker, { target = target })
  end)
end

function M.actions.explorer_copy(picker, item)
  if not item then
    return
  end
  ---@type string[]
  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected())
  -- Copy selection
  if #paths > 0 then
    local dir = picker:dir()
    Snacks.picker.util.copy(paths, dir)
    picker.list:set_selected() -- clear selection
    Tree:refresh(dir)
    Tree:open(dir)
    M.update(picker, { target = dir })
    return
  end
  Snacks.input({
    prompt = "Copy to",
  }, function(value)
    if not value or value:find("^%s$") then
      return
    end
    local dir = vim.fs.dirname(item.file)
    local to = svim.fs.normalize(dir .. "/" .. value)
    if uv.fs_stat(to) then
      Snacks.notify.warn("File already exists:\n- `" .. to .. "`")
      return
    end
    Snacks.picker.util.copy_path(item.file, to)
    Tree:refresh(vim.fs.dirname(to))
    M.update(picker, { target = to })
  end)
end

function M.actions.explorer_del(picker)
  ---@type string[]
  local paths = vim.tbl_map(Snacks.picker.util.path, picker:selected({ fallback = true }))
  if #paths == 0 then
    return
  end
  local what = #paths == 1 and vim.fn.fnamemodify(paths[1], ":p:~:.") or #paths .. " files"
  M.confirm("Delete " .. what .. "?", function()
    for _, path in ipairs(paths) do
      local ok, err = pcall(vim.fn.delete, path, "rf")
      if ok then
        Snacks.bufdelete({ file = path, force = true })
      else
        Snacks.notify.error("Failed to delete `" .. path .. "`:\n- " .. err)
      end
      Tree:refresh(vim.fs.dirname(path))
    end
    picker.list:set_selected() -- clear selection
    M.update(picker)
  end)
end

function M.actions.confirm(picker, item, action)
  if not item then
    return
  elseif picker.input.filter.meta.searching then
    M.update(picker, { target = item.file })
  elseif item.dir then
    Tree:toggle(item.file)
    M.update(picker, { refresh = true })
  else
    Snacks.picker.actions.jump(picker, item, action)
  end
end

function M.actions.explorer_diagnostic(picker, item, action)
  ---@cast action snacks.explorer.diagnostic.Action
  local node = Tree:next(picker:cwd(), function(node)
    if not node.severity then
      return false
    end
    return action.severity == nil or node.severity == action.severity
  end, { up = action.up, path = item and item.file })
  if node then
    M.update(picker, { target = node.path })
  end
end

M.actions.explorer_diagnostic_next = { action = "explorer_diagnostic" }
M.actions.explorer_diagnostic_prev = { action = "explorer_diagnostic", up = true }
M.actions.explorer_warn_next = { action = "explorer_diagnostic", severity = vim.diagnostic.severity.WARN }
M.actions.explorer_warn_prev = { action = "explorer_diagnostic", severity = vim.diagnostic.severity.WARN, up = true }
M.actions.explorer_error_next = { action = "explorer_diagnostic", severity = vim.diagnostic.severity.ERROR }
M.actions.explorer_error_prev = { action = "explorer_diagnostic", severity = vim.diagnostic.severity.ERROR, up = true }

return M
