-- main.lua
-- Trash management system for Yazi

--=========== Plugin Settings =================================================
local isDebugEnabled = false
local M = {}
local PLUGIN_NAME = "recycle-bin"
local USER_ID = ya.uid()
local XDG_RUNTIME_DIR = os.getenv("XDG_RUNTIME_DIR") or ("/run/user/" .. USER_ID)

--=========== Compiled Patterns (Performance Optimization) ==================
-- Pre-compiled string patterns for better performance
local PATTERNS = {
  filename = "([^/]+)$",
  trash_list = "^(%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d) (.+)$",
  line_break = "[^\n]+",
  line_break_crlf = "[^\r\n]+",
  size_info = "^(%S+)",
  whitespace_cleanup = "[\r\n]+",
  trailing_space = "%s+$",
  first_word = "%l",
  upper_first = "^%l",
}

--=========== Plugin State ===========================================================
---@enum
local STATE_KEY = {
  CONFIG = "CONFIG",
}

--================= Notify / Logger ===========================================
local TIMEOUTS = {
  error = 8,
  warn = 8,
  info = 3,
}
local Notify = {}
---@param level "info"|"warn"|"error"|nil
---@param s string
---@param ... any
function Notify._send(level, s, ...)
  debug(s, ...)
  local content = Notify._parseContent(s, ...)
  local entry = {
    title = PLUGIN_NAME,
    content = content,
    timeout = TIMEOUTS[level] or 3,
    level = level,
  }
  ya.notify(entry)
end

function Notify._parseContent(s, ...)
  local ok, content = pcall(string.format, s, ...)
  if not ok then content = s end
  content = tostring(content):gsub(PATTERNS.whitespace_cleanup, " "):gsub(PATTERNS.trailing_space, "")
  return content
end

function Notify.error(...)
  ya.err(...)
  Notify._send("error", ...)
end
function Notify.warn(...)
  Notify._send("warn", ...)
end
function Notify.info(...)
  Notify._send("info", ...)
end
function debug(...)
  if isDebugEnabled then
    local msg = Notify._parseContent(...)
    ya.dbg(msg)
  end
end

--========= Run terminal commands =======================================================
---@param cmd string
---@param args? string[]
---@param input? string  -- optional stdin input (e.g., password)
---@param is_silent? boolean
---@return string|nil, Output|nil
local function run_command(cmd, args, input, is_silent)
  debug("Executing command: " .. cmd .. (args and #args > 0 and (" " .. table.concat(args, " ")) or ""))
  local msgPrefix = "Command: " .. cmd .. " - "
  local cmd_obj = Command(cmd)

  -- Add arguments
  if type(args) == "table" and #args > 0 then
    for _, arg in ipairs(args) do
      cmd_obj:arg(arg)
    end
  end

  -- Set stdin mode if input is provided
  if input then
    cmd_obj:stdin(Command.PIPED)
  else
    cmd_obj:stdin(Command.INHERIT)
  end

  -- Set other streams
  cmd_obj:stdout(Command.PIPED):stderr(Command.PIPED):env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR)

  local child, cmd_err = cmd_obj:spawn()
  if not child then
    if not is_silent then Notify.error(msgPrefix .. "Failed to start. Error: %s", tostring(cmd_err)) end
    return cmd_err and tostring(cmd_err), nil
  end

  -- Send stdin input if available
  if input then
    local ok, err = child:write_all(input)
    if not ok then
      if not is_silent then Notify.error(msgPrefix .. "Failed to write, stdin: %s", tostring(err)) end
      return err and tostring(err), nil
    end

    local flushed, flush_err = child:flush()
    if not flushed then
      if not is_silent then Notify.error(msgPrefix .. "Failed to flush, stdin: %s", tostring(flush_err)) end
      return flush_err and tostring(flush_err), nil
    end
  end

  -- Read output
  local output, out_err = child:wait_with_output()
  if not output then
    if not is_silent then Notify.error(msgPrefix .. "Failed to get output, error: %s", tostring(out_err)) end
    return out_err and tostring(out_err), nil
  end

  -- Log outputs
  if output.stdout ~= "" and not is_silent then debug(msgPrefix .. "stdout: %s", output.stdout) end
  if output.status and output.status.code ~= 0 and not is_silent then
    debug(msgPrefix .. "Error code `%s`, success: `%s`", output.status.code, tostring(output.status.success))
  end

  -- Handle child output error
  if output.stderr ~= "" then
    if not is_silent then debug(msgPrefix .. "stderr: %s", output.stderr) end
    -- Only treat stderr as error if command actually failed
    if output.status and not output.status.success then return output.stderr, output end
  end

  return nil, output
end

--========= Sync helpers =======================================================
local set_state = ya.sync(function(state, key, value)
  state[key] = value
end)

local get_state = ya.sync(function(state, key)
  return state[key]
end)

--- Get and return string of the pwd/cwd
---@return string -- the current working directory
local get_cwd = ya.sync(function()
  return tostring(cx.active.current.cwd)
end)

---Get selected files from Yazi
---@return string[]
local get_selected_files = ya.sync(function()
  local tab, paths = cx.active, {}
  for _, u in pairs(tab.selected) do
    paths[#paths + 1] = tostring(u)
  end
  if #paths == 0 and tab.current.hovered then paths[1] = tostring(tab.current.hovered.url) end
  return paths
end)

---Clear the current selection in Yazi
local function clear_selection()
  -- Use Yazi's manager_emit to properly clear all selections
  ya.manager_emit("select_all", { state = false })
end

--=========== Utils =================================================
--- Deep merge two tables: overrides take precedence
---@param defaults table
---@param overrides table|nil
---@return table
local function deep_merge(defaults, overrides)
  if type(overrides) ~= "table" then return defaults end

  local result = {}

  for k, v in pairs(defaults) do
    if type(v) == "table" and type(overrides[k]) == "table" then
      result[k] = deep_merge(v, overrides[k])
    else
      result[k] = overrides[k] ~= nil and overrides[k] or v
    end
  end

  -- Include any keys in overrides not in defaults
  for k, v in pairs(overrides) do
    if result[k] == nil then result[k] = v end
  end

  return result
end

---Show an input box.
---@param title string
---@param is_password boolean?
---@param value string?
---@return string|nil
local function prompt(title, is_password, value)
  debug("Prompting user for `%s`, is password: `%s`", title, is_password)
  local input_value, input_event = ya.input({
    title = title,
    value = value or "",
    obscure = is_password or false,
    pos = { "center", y = 3, w = 60 },
  })

  if input_event ~= 1 then return nil end

  return input_value
end

---Create standardized ui.Text with common styling
---@param lines string|string[] Single line or array of lines
---@return table ui.Text object with standard alignment and wrapping
local function create_ui_list(lines)
  local line_objects = {}
  if type(lines) == "string" then
    table.insert(line_objects, ui.Line(lines))
  else
    for _, line in ipairs(lines) do
      table.insert(line_objects, ui.Line(line))
    end
  end
  return ui.Text(line_objects):align(ui.Align.LEFT):wrap(ui.Wrap.YES)
end

---Show a confirmation box.
---@param title string|table Confirmation title (string or structured ui.Line)
---@param body string|string[]|table? Confirmation body (string, string array, or structured ui.Text)
---@param posOpts ui.Pos? A table of position options (e.g. {"center", w = 70, h = 40, x = 0, y = 0})
---@return boolean
local function confirm(title, body, posOpts)
  local title_str = type(title) == "string" and title or tostring(title)
  debug("Confirming user action for `%s`", title_str)
  local pos = {
    posOpts and posOpts[1] or "center",
    w = posOpts and posOpts.w or 70,
    h = posOpts and posOpts.h or 40,
    x = posOpts and posOpts.x or 0,
    y = posOpts and posOpts.y or 0,
  }

  local confirmation_data = {
    title = type(title) == "string" and ui.Line(title) or title,
    pos = pos,
  }

  if body then
    -- Handle different body types
    if type(body) == "string" then
      confirmation_data.content = create_ui_list(body)
      confirmation_data.body = create_ui_list(body)
    elseif type(body) == "table" and body[1] and type(body[1]) == "string" then
      -- Array of strings
      confirmation_data.content = create_ui_list(body)
      confirmation_data.body = create_ui_list(body)
    else
      -- Structured UI component (ui.Text)
      confirmation_data.content = body
      confirmation_data.body = body
    end
  end

  local answer = ya.confirm(confirmation_data)
  return answer
end

---Present a simple which‑key style selector and return the chosen item (Max: 36 options).
---@param title string
---@param items string[]
---@return string|nil
local function choose_which(title, items)
  local keys = "1234567890abcdefghijklmnopqrstuvwxyz"
  local candidates = {}
  for i, item in ipairs(items) do
    if i > #keys then break end
    candidates[#candidates + 1] = { on = keys:sub(i, i), desc = item }
  end

  local idx = ya.which({ title = title, cands = candidates })
  return idx and items[idx]
end

--============== File helpers ====================================
---Check if a path exists and is a directory
---@param url Url
---@return boolean
local function is_dir(url)
  local cha, _ = fs.cha(url)
  return cha and cha.is_dir or false
end

---Get file size in bytes using fs.cha()
---@param file_path string Absolute path to the file
---@return integer|nil, string|nil -- size_in_bytes, error_message
local function get_file_size(file_path)
  local url = Url(file_path)
  local cha, err = fs.cha(url)

  if not cha then
    local error_msg = string.format("Failed to get file info for %s: %s", file_path, err or "unknown error")
    debug(error_msg)
    return nil, error_msg
  end

  if not cha.len then
    local error_msg = string.format("File size not available for %s", file_path)
    debug(error_msg)
    return nil, error_msg
  end

  return cha.len, nil
end

---Format bytes into human-readable format (B, KB, MB, GB, TB)
---@param bytes integer|nil Number of bytes to format
---@return string Formatted size string
local function format_file_size(bytes)
  if not bytes or bytes < 0 then return "0 B" end

  local units = { "B", "KB", "MB", "GB", "TB" }
  local size = bytes
  local unit_index = 1

  -- Convert to larger units while size >= 1024 and we have larger units
  while size >= 1024 and unit_index < #units do
    size = size / 1024
    unit_index = unit_index + 1
  end

  -- Format with appropriate decimal places
  if unit_index == 1 then
    -- Bytes - no decimal places
    return string.format("%d %s", size, units[unit_index])
  elseif size >= 100 then
    -- >= 100 units - no decimal places (e.g., "156 MB")
    return string.format("%.0f %s", size, units[unit_index])
  elseif size >= 10 then
    -- >= 10 units - one decimal place (e.g., "15.6 MB")
    return string.format("%.1f %s", size, units[unit_index])
  else
    -- < 10 units - two decimal places (e.g., "1.56 MB")
    return string.format("%.2f %s", size, units[unit_index])
  end
end

---Get file objects with size information for multiple files
---@param file_paths string[] Array of file paths/names to process
---@param base_dir string Base directory where files are located (e.g., "~/.local/share/Trash/files/")
---@return {name: string, size: string}[] Array of file objects with name and size
local function get_files_with_sizes(file_paths, base_dir)
  debug("Getting file sizes for %d files from base directory: %s", #file_paths, base_dir)

  local file_objects = {}

  -- Ensure base_dir ends with a slash for proper path construction
  local normalized_base_dir = base_dir
  if not normalized_base_dir:match("/$") then normalized_base_dir = normalized_base_dir .. "/" end

  for i, file_path in ipairs(file_paths) do
    -- Extract filename from the path
    local filename = file_path:match(PATTERNS.filename) or file_path

    -- Construct full path to the file in the base directory
    local full_path = normalized_base_dir .. filename

    -- Get file size using existing utility function
    local bytes, size_err = get_file_size(full_path)
    local formatted_size

    if size_err then
      -- Log the error but continue processing other files
      debug("Could not get size for file %s: %s", filename, size_err)
      formatted_size = "unknown size"
    else
      -- Format the size using existing utility function
      formatted_size = format_file_size(bytes)
    end

    -- Create file object with name and size
    file_objects[i] = {
      name = filename,
      size = formatted_size,
    }
  end

  debug("Successfully processed %d file objects", #file_objects)
  return file_objects
end

--=========== Trash helpers =================================================
---Get available trash directories from trash-cli
---@return string[], string|nil -- trash_dirs, error
local function get_trash_directories()
  local err, output = run_command("trash-list", { "--trash-dirs" }, nil, true)
  if err then return {}, err end

  local directories = {}
  if output and output.stdout ~= "" then
    for line in output.stdout:gmatch(PATTERNS.line_break) do
      local trimmed = line:gsub("^%s*(.-)%s*$", "%1") -- trim whitespace
      if trimmed ~= "" then
        -- Ensure directory path ends with /
        if not trimmed:match("/$") then trimmed = trimmed .. "/" end
        table.insert(directories, trimmed)
      end
    end
  end

  debug("Found %d trash directories: %s", #directories, table.concat(directories, ", "))
  return directories, nil
end

---Get the correct trash files directory path based on OS
---@param config table Configuration object containing trash_dir and os
---@return string -- trash_files_directory_path
local function get_trash_files_dir(config)
  local trash_files_dir = config.trash_dir
  -- On Linux, trash files are in a 'files' subdirectory
  if config.os ~= "macos" then
    -- Ensure trash_dir ends with / before adding 'files'
    if not trash_files_dir:match("/$") then trash_files_dir = trash_files_dir .. "/" end
    trash_files_dir = trash_files_dir .. "files"
  end
  return trash_files_dir
end

---Verify trash dir exists
---@param config table | nil
local function check_has_trash_directory(config)
  -- Get Config
  if not config then config = get_state(STATE_KEY.CONFIG) end
  -- Verify trash dir
  local trash_dir = config.trash_dir
  local trash_url = Url(trash_dir)

  if not is_dir(trash_url) then
    Notify.error("Trash directory not found: %s. Please check your configuration.", trash_dir)
    return false
  end

  return true
end

---Select trash directory from available options
---@param directories string[] Array of trash directory paths
---@return string|nil -- selected_directory
local function select_trash_directory(directories)
  if #directories == 0 then return nil end

  -- If only one directory, use it automatically
  if #directories == 1 then
    debug("Using single trash directory: %s", directories[1])
    return directories[1]
  end

  -- Multiple directories - present user with selection
  debug("Multiple trash directories found, prompting user selection")
  local selected_dir = choose_which("Select trash directory:", directories)

  if selected_dir then
    debug("User selected trash directory: %s", selected_dir)
  else
    debug("User cancelled trash directory selection")
  end

  return selected_dir
end

---Ensure trash directory is set, prompting user if needed
---@param config table
---@return boolean -- true if trash directory is available, false if cancelled or error
local function ensure_trash_directory(config)
  -- If trash directory is already set and exists, we're good
  if config.trash_dir and check_has_trash_directory(config) then return true end

  -- Get available trash directories
  local directories, dir_err = get_trash_directories()
  if dir_err then
    Notify.error(
      "Failed to discover trash directories: %s. Try 'trash-list --trash-dirs' manually to verify trash directories",
      dir_err
    )
    return false
  end

  if #directories == 0 then
    Notify.error("No trash directories found. Please check trash-cli installation.")
    return false
  end

  -- Let user select which trash directory to use
  local selected_dir = select_trash_directory(directories)
  if not selected_dir then
    Notify.info("Trash directory selection cancelled")
    return false
  end

  -- Save the selected trash directory to config for this session
  config.trash_dir = selected_dir
  set_state(STATE_KEY.CONFIG, config)
  debug("Updated trash_dir for this session: %s", selected_dir)

  return true
end

---Get mapping of filenames to original paths from trash-list
---@return table<string, string>, string|nil -- filename_to_path_map, error
local function get_trash_file_mappings()
  local err, output = run_command("trash-list", {})
  if err then return {}, err end

  local mappings = {}
  if output and output.stdout ~= "" then
    for line in output.stdout:gmatch(PATTERNS.line_break) do
      local timestamp, original_path = line:match(PATTERNS.trash_list)
      if timestamp and original_path then
        local filename = original_path:match(PATTERNS.filename) or original_path
        mappings[filename] = original_path
      end
    end
  end

  debug("Created %d trash file mappings", #mappings)
  return mappings, nil
end

---Get all files in trash with their sizes for display
---@param config table Configuration object
---@return {name: string, size: string}[], string|nil -- file_objects, error
local function get_trash_files_with_sizes(config)
  -- Get all files from trash-list
  local err, output = run_command("trash-list", {})
  if err then return {}, err end

  local file_names = {}
  if output and output.stdout ~= "" then
    for line in output.stdout:gmatch(PATTERNS.line_break) do
      local timestamp, original_path = line:match(PATTERNS.trash_list)
      if timestamp and original_path then
        local filename = original_path:match(PATTERNS.filename) or original_path
        table.insert(file_names, filename)
      end
    end
  end

  if #file_names == 0 then return {}, nil end

  -- Get file objects with sizes using existing function
  local trash_files_dir = get_trash_files_dir(config)
  local file_objects = get_files_with_sizes(file_names, trash_files_dir)

  debug("Retrieved %d trash files with sizes", #file_objects)
  return file_objects, nil
end

---Get trash files older than specified days with their sizes for display
---@param config table Configuration object
---@param days integer Number of days - files older than this will be included
---@return {name: string, size: string, deleted_date: string}[], string|nil -- file_objects, error
local function get_trash_files_older_than_days(config, days)
  -- Get all files from trash-list
  local err, output = run_command("trash-list", {})
  if err then return {}, err end

  -- Calculate cutoff time (days ago from now)
  local current_time = os.time()
  local cutoff_time = current_time - (days * 24 * 60 * 60) -- days * hours * minutes * seconds

  local old_files = {}
  if output and output.stdout ~= "" then
    for line in output.stdout:gmatch(PATTERNS.line_break) do
      local timestamp, original_path = line:match(PATTERNS.trash_list)
      if timestamp and original_path then
        -- Parse timestamp: "2025-08-28 20:27:38" format
        local year, month, day, hour, min, sec = timestamp:match("(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)")
        if year and month and day and hour and min and sec then
          local file_time = os.time({
            year = tonumber(year),
            month = tonumber(month),
            day = tonumber(day),
            hour = tonumber(hour),
            min = tonumber(min),
            sec = tonumber(sec),
          })

          -- If file is older than cutoff, include it
          if file_time < cutoff_time then
            local filename = original_path:match(PATTERNS.filename) or original_path
            table.insert(old_files, {
              filename = filename,
              deleted_date = timestamp,
            })
          end
        end
      end
    end
  end

  if #old_files == 0 then return {}, nil end

  -- Get file objects with sizes using existing function
  local trash_files_dir = get_trash_files_dir(config)
  local file_names = {}
  for _, file_info in ipairs(old_files) do
    table.insert(file_names, file_info.filename)
  end
  local file_objects = get_files_with_sizes(file_names, trash_files_dir)

  -- Add deleted date information to file objects
  for i, file_obj in ipairs(file_objects) do
    file_obj.deleted_date = old_files[i].deleted_date
  end

  debug("Retrieved %d trash files older than %d days", #file_objects, days)
  return file_objects, nil
end

--- Go to the trash directory
local function open_trash(config)
  -- Ensure we have a trash directory selected
  if not ensure_trash_directory(config) then return end

  local trash_files_dir = get_trash_files_dir(config)
  local trash_files_url = Url(trash_files_dir)

  -- Go to trash files directory if exists, fallback to trash root if not
  if is_dir(trash_files_url) then
    ya.emit("cd", { trash_files_url })
  else
    local trash_root_url = Url(config.trash_dir)
    if is_dir(trash_root_url) then
      ya.emit("cd", { trash_root_url })
      Notify.info("Trash files directory not found, navigated to trash root: %s", config.trash_dir)
    else
      Notify.error("Trash directory not found: %s", config.trash_dir)
    end
  end
end

---Offer to open trash directory when user attempts operations outside trash
---@param config table
---@return boolean -- true if user chose to navigate to trash and succeeded, false otherwise
local function offer_to_open_trash(config)
  local user_wants_to_navigate = confirm("Not in Trash Directory", {
    "This command can only be run from within a Trash directory.",
    "Would you like to open the Trash now?",
  }, { w = 70, h = 10, x = 0, y = 0 })

  if not user_wants_to_navigate then
    Notify.info("Operation cancelled")
    return false
  end

  -- Try to open trash directory
  open_trash(config)

  -- Check if we successfully navigated to a trash directory
  local current_dir = get_cwd()
  local trash_dirs, dir_err = get_trash_directories()
  if dir_err then
    Notify.error("Failed to verify navigation: %s", dir_err)
    return false
  end

  for _, trash_dir in ipairs(trash_dirs) do
    if current_dir:find(trash_dir, 1, true) == 1 then return true end
  end

  Notify.error("Failed to navigate to trash directory")
  return false
end

---Resolve a path that might be a symlink to its real path using realpath command
---@param path string The path to resolve
---@return string -- The resolved path (or original if resolution fails)
local function resolve_symlink(path)
  -- Use realpath command to resolve symlinks
  local err, output = run_command("realpath", { path }, nil, true)

  if not err and output and output.stdout then
    -- Remove trailing newline and return resolved path
    local resolved = output.stdout:gsub("[\r\n]+$", "")
    if resolved ~= "" then
      debug("Resolved symlink: %s -> %s", path, resolved)
      return resolved
    end
  end

  -- If resolution failed, return original path
  debug("Could not resolve symlink for %s, using original path", path)
  return path
end

---Check if current working directory is within a valid trash directory
---If not, offer to navigate to trash directory
---@param config table
---@return boolean -- true if in trash directory or successfully navigated to trash, false otherwise
local is_current_dir_in_trash = function(config)
  local current_dir = get_cwd()
  -- Resolve symlinks in the current directory path
  local resolved_dir = resolve_symlink(current_dir)

  local trash_dirs, dir_err = get_trash_directories()
  if dir_err then
    Notify.error(
      "Failed to find trash directories: %s. Check trash-cli installation with 'trash-list --version'",
      dir_err
    )
    return false
  end

  -- Check if already in trash directory (check both original and resolved paths)
  for _, trash_dir in ipairs(trash_dirs) do
    if current_dir:find(trash_dir, 1, true) == 1 or resolved_dir:find(trash_dir, 1, true) == 1 then return true end
  end

  -- Not in trash directory - offer to navigate there
  offer_to_open_trash(config)
  return false
end

---Get count of items in trash
---@return integer, string|nil -- count, error
local function get_trash_item_count()
  local err, output = run_command("trash-list", {})
  if err then return 0, err end

  local item_count = 0
  if output and output.stdout ~= "" then
    _, item_count = output.stdout:gsub(PATTERNS.line_break, "")
  end

  return item_count, nil
end

---Get size of trash files directory
---@param config table
---@return string, string|nil -- size_string, error
local function get_trash_size(config)
  local trash_files_dir = get_trash_files_dir(config)

  local err, output = run_command("du", { "-sh", trash_files_dir }, nil, true)
  if err or not output or output.stdout == "" then return "unknown size", err end

  local size_info = output.stdout:match(PATTERNS.size_info)
  return size_info or "unknown size", nil
end

---Get size of trash files directory
---@param config table
---@return {count: integer, size: string}|table, {type: string, msg: string}|nil
local function get_trash_data(config)
  -- Default values
  local count = 0
  local size = "0M"

  -- Get trash info
  local item_count, count_err = get_trash_item_count()
  if count_err then
    return { count, size }, { type = "error", msg = string.format("Failed to get trash contents: %s", count_err) }
  end

  if item_count == 0 then return { count, size }, { type = "info", msg = "Trash is already empty" } end

  local size_info, size_err = get_trash_size(config)
  if size_err then debug("Failed to get trash size: %s", size_err) end

  return {
    count = item_count,
    size = size_info,
  }, nil
end

--=========== Conflict Resolution =================================================
---Handle restore conflicts by checking if files exist at original locations
---@param restore_items table[] Array of restore items with original_path, filename, and size
---@return table[] non_conflicted_items, table[] conflicted_items
local function detect_restore_conflicts(restore_items)
  local conflicts = {}
  local non_conflicted_items = {}

  for _, item in ipairs(restore_items) do
    local original_url = Url(item.original_path)
    local cha, _ = fs.cha(original_url)

    if cha then -- File exists at original location
      table.insert(conflicts, {
        filename = item.filename,
        original_path = item.original_path,
        size = item.size,
      })
    else
      table.insert(non_conflicted_items, item)
    end
  end

  return non_conflicted_items, conflicts
end

---Create overwrite warning dialog and get user confirmation
---@param conflicts table[] Array of conflicted items
---@return boolean true if user confirms overwrite, false otherwise
local function create_overwrite_warning_dialog(conflicts)
  local overwrite_warning = {
    "⚠️  DESTRUCTIVE ACTION WARNING ⚠️",
    "",
    "You are about to PERMANENTLY OVERWRITE existing files:",
  }
  for _, conflict in ipairs(conflicts) do
    table.insert(overwrite_warning, string.format("  • %s", conflict.original_path))
  end
  table.insert(overwrite_warning, "")
  table.insert(overwrite_warning, "The existing files will be LOST FOREVER!")
  table.insert(overwrite_warning, "This action CANNOT BE UNDONE!")

  -- Create warning dialog with red styling
  local warning_components = {}
  for i, line in ipairs(overwrite_warning) do
    if i == 1 then
      -- Main warning title in red
      table.insert(warning_components, ui.Line(line):style(th.notify.title_error))
    elseif line:match("^  •") then
      -- File paths
      table.insert(warning_components, ui.Line(line):style(th.notify.content))
    elseif line:match("LOST FOREVER") or line:match("CANNOT BE UNDONE") then
      -- Critical warnings in red
      table.insert(warning_components, ui.Line(line):style(th.notify.title_error))
    else
      table.insert(warning_components, ui.Line(line))
    end
  end

  local warning_body = ui.Text(warning_components):align(ui.Align.LEFT):wrap(ui.Wrap.YES)
  return confirm(" CONFIRM DESTRUCTIVE ACTION ", warning_body, { w = 80, h = 20, x = 0, y = 0 })
end

---Handle overwrite choice confirmation
---@param conflicts table[] Array of conflicted items
---@return string "cancel"|"overwrite" User's final choice for overwrite action
local function handle_overwrite_choice(conflicts)
  local confirmed = create_overwrite_warning_dialog(conflicts)
  return confirmed and "overwrite" or "cancel"
end

---Present conflict resolution dialog to user and return their choice
---@param conflicts table[] Array of conflicted items
---@param non_conflicted_count integer Number of non-conflicted items
---@return string "cancel"|"skip"|"overwrite" User's choice
local function prompt_conflict_resolution(conflicts, non_conflicted_count)
  if non_conflicted_count > 0 then
    -- Offer choice between cancel, skip conflicts, or overwrite all
    local choices = {
      "Cancel restore",
      "Skip conflicts and restore others",
      "⚠️ Do not skip conflicts, restore ALL and OVERWRITE any conflicts",
    }
    local choice = choose_which("Resolve File Conflicts", choices)

    if choice == "Cancel restore" then
      return "cancel"
    elseif choice == "Skip conflicts and restore others" then
      return "skip"
    elseif choice == "⚠️ Do not skip conflicts, restore ALL and OVERWRITE any conflicts" then
      return handle_overwrite_choice(conflicts)
    else
      return "cancel" -- Default to cancel if no choice made
    end
  else
    -- All files have conflicts - offer overwrite option
    local choices = { "Cancel restore", "⚠️ Do not skip conflicts, restore ALL and OVERWRITE any conflicts" }
    local choice = choose_which("All Files Have Conflicts", choices)

    if choice == "⚠️ Do not skip conflicts, restore ALL and OVERWRITE any conflicts" then
      return handle_overwrite_choice(conflicts)
    else
      return "cancel"
    end
  end
end

---Delete a single conflicting file or directory at its original location
---@param original_path string The path to the conflicting file/directory
---@return boolean success, string|nil error_message
local function delete_conflict_file(original_path)
  local original_url = Url(original_path)

  -- Check if it's a file or directory to use the correct removal type
  local cha, cha_err = fs.cha(original_url)
  if not cha then
    local error_msg = string.format("Cannot access conflicting item %s: %s", original_path, cha_err or "unknown error")
    return false, error_msg
  end

  local remove_type = cha.is_dir and "dir_all" or "file"
  local delete_success, delete_err = fs.remove(remove_type, original_url)

  if delete_success then
    debug("Successfully deleted conflicting %s: %s", cha.is_dir and "directory" or "file", original_path)
    return true, nil
  else
    local error_msg = string.format(
      "Failed to delete existing %s %s: %s",
      cha.is_dir and "directory" or "file",
      original_path,
      delete_err or "unknown error"
    )
    return false, error_msg
  end
end

---Handle restore conflicts and return filtered items based on user choice
---@param restore_items table[] Original restore items
---@return table[]|nil filtered_items (nil if user cancelled)
local function handle_restore_conflicts(restore_items)
  local non_conflicted_items, conflicts = detect_restore_conflicts(restore_items)

  -- No conflicts found, proceed with all items
  if #conflicts == 0 then return restore_items end

  -- Present conflict resolution dialog
  local user_choice = prompt_conflict_resolution(conflicts, #non_conflicted_items)

  if user_choice == "cancel" then
    return nil
  elseif user_choice == "skip" then
    if #non_conflicted_items == 0 then
      Notify.info("No files to restore after skipping all conflicts")
      return nil
    end
    Notify.info("Skipping %d conflicted files, proceeding with %d files", #conflicts, #non_conflicted_items)
    return non_conflicted_items
  elseif user_choice == "overwrite" then
    -- Mark items that need overwrite and return all items
    -- The actual deletion will happen when user confirms the restore operation
    Notify.info("Selected overwrite option for %d conflicting files", #conflicts)

    -- Add overwrite metadata to restore items that have conflicts
    for _, item in ipairs(restore_items) do
      for _, conflict in ipairs(conflicts) do
        if item.original_path == conflict.original_path then
          item.needs_overwrite = true
          break
        end
      end
    end

    return restore_items -- Return all items, with overwrite flags set
  end

  -- Fallback to cancel
  return nil
end

--=========== File Selection =================================================
---Validates file selection and extracts filenames
---@param operation_name string The name of the operation (for logging/notifications)
---@return string[]|nil -- selected_paths
local function validate_and_get_selection(operation_name)
  -- Get selected files from Yazi
  local selected_paths = get_selected_files()
  if #selected_paths == 0 then
    Notify.warn("No files selected for " .. operation_name)
    return nil
  end
  debug("Selected paths for %s: %s", operation_name, table.concat(selected_paths, ", "))
  return selected_paths
end

--=========== Batch Operations =================================================
---Shows standardized confirmation dialog for batch operations
---@param verb string Action verb (e.g., "delete", "restore")
---@param items {name: string, size: string, needs_overwrite: boolean?}[] List of file objects with name, size, and optional overwrite flag
---@param warning string|nil Optional warning message
---@return boolean
local function confirm_batch_operation(verb, items, warning)
  local title = string.format("%s the following %d file(s):", verb:gsub(PATTERNS.upper_first, string.upper), #items)

  -- Create structured UI components for proper alignment and styling
  local body_components = {}

  -- Add each item as a formatted line with proper left alignment showing "fileName (size)"
  -- Show overwrite warning for files that will overwrite existing files
  local overwrite_count = 0
  for _, item in ipairs(items) do
    local display_text = string.format("%s (%s)", item.name, item.size)
    if item.needs_overwrite then
      overwrite_count = overwrite_count + 1
      -- Mark files that will overwrite with warning styling
      table.insert(
        body_components,
        ui.Line({
          ui.Span("  ⚠️  "),
          ui.Span(display_text .. " [WILL OVERWRITE]"),
        }):style(th.notify.title_warn)
      )
    else
      table.insert(body_components, ui.Line({ ui.Span("  "), ui.Span(display_text) }):align(ui.Align.LEFT))
    end
  end

  -- Add overwrite warning if any files need overwriting
  if overwrite_count > 0 then
    table.insert(body_components, ui.Line(""))
    table.insert(
      body_components,
      ui.Line(string.format("⚠️  %d existing file(s) will be permanently deleted!", overwrite_count))
        :style(th.notify.title_error)
    )
  end

  -- Add warning if provided with styling
  if warning then
    table.insert(body_components, ui.Line(""))
    table.insert(body_components, ui.Line(warning):style(th.notify.title_warn))
  end

  local structured_body = ui.Text(body_components):align(ui.Align.LEFT):wrap(ui.Wrap.YES)
  local confirmation = confirm(title, structured_body)
  if not confirmation then
    Notify.info(verb:gsub(PATTERNS.upper_first, string.upper) .. " cancelled")
    return false
  end

  return true
end

---Shows confirmation dialog for batch operations with deletion dates
---@param verb string Action verb (e.g., "delete")
---@param items {name: string, size: string, deleted_date: string}[] List of file objects with name, size, and deletion date
---@param days integer Number of days used for filtering
---@param warning string|nil Optional warning message
---@return boolean
local function confirm_batch_operation_with_dates(verb, items, days, warning)
  local title = string.format(
    "%s the following %d file(s) older than %d days:",
    verb:gsub(PATTERNS.upper_first, string.upper),
    #items,
    days
  )

  -- Create structured UI components for proper alignment and styling
  local body_components = {}

  -- Add each item as a formatted line showing "fileName (size) - deleted: date"
  for _, item in ipairs(items) do
    local display_text = string.format("%s (%s) - deleted: %s", item.name, item.size, item.deleted_date)
    table.insert(body_components, ui.Line({ ui.Span("  "), ui.Span(display_text) }):align(ui.Align.LEFT))
  end

  -- Add warning if provided with styling
  if warning then
    table.insert(body_components, ui.Line(""))
    table.insert(body_components, ui.Line(warning):style(th.notify.title_warn))
  end

  local structured_body = ui.Text(body_components):align(ui.Align.LEFT):wrap(ui.Wrap.YES)
  local confirmation = confirm(title, structured_body)
  if not confirmation then
    Notify.info(verb:gsub(PATTERNS.upper_first, string.upper) .. " cancelled")
    return false
  end

  return true
end

---Executes batch operation with progress tracking and error handling
---@param items table[] Array of items to process (can be strings, file objects, or restore items)
---@param operation_name string Name of operation for notifications
---@param operation_func function Function that takes an item and returns error_string|nil
---@return integer, integer -- success_count, failed_count
local function execute_batch_operation(items, operation_name, operation_func)
  local success_count = 0
  local failed_count = 0

  for _, item in ipairs(items) do
    local err = operation_func(item)
    if err then
      failed_count = failed_count + 1
    else
      success_count = success_count + 1
    end
  end

  return success_count, failed_count
end

---Reports standardized operation results
---@param operation_name string Name of the operation
---@param success_count integer Number of successful operations
---@param failed_count integer Number of failed operations
local function report_operation_results(operation_name, success_count, failed_count)
  local past_tense = operation_name == "deleting" and "deleted"
    or operation_name == "restoring" and "restored"
    or operation_name .. "d"

  if success_count > 0 and failed_count == 0 then
    Notify.info("Successfully %s %d file(s)", past_tense, success_count)
  elseif success_count > 0 and failed_count > 0 then
    Notify.warn(
      "%s %d file(s), failed %d",
      past_tense:gsub(PATTERNS.upper_first, string.upper),
      success_count,
      failed_count
    )
  else
    Notify.error("Failed to %s any files", operation_name:gsub("ing$", ""))
  end
end

--=========== api actions =================================================
local function cmd_open_trash(config)
  open_trash(config)
end

local function cmd_empty_trash(config)
  -- Ensure we have a trash directory selected
  if not ensure_trash_directory(config) then return end

  -- Get trash data
  local data, data_err = get_trash_data(config)
  if data_err then
    Notify[data_err.type](data_err.msg)
    return
  end

  -- Get all trash files with their sizes for detailed display
  local file_objects, file_err = get_trash_files_with_sizes(config)
  if file_err then
    Notify.error("Failed to get trash file list: %s", file_err)
    return
  end

  -- If no files found, show simple message
  if #file_objects == 0 then
    Notify.info("Trash is already empty")
    return
  end

  -- Show detailed confirmation dialog with file list and sizes
  if not confirm_batch_operation("permanently delete", file_objects, "This action cannot be undone!") then return end

  -- Execute trash-empty command
  local err, _ = run_command("trash-empty", {}, "y\n")
  if err then
    Notify.error("Failed to empty trash: %s. Try 'trash-empty' manually to debug", err)
    return
  end
  Notify.info("Trash emptied successfully (%d items, %s freed)", data.count, data.size)
end

local function cmd_empty_trash_by_days(config)
  -- Ensure we have a trash directory selected
  if not ensure_trash_directory(config) then return end

  -- Get trash data prior to the operation to calculate difference
  local begin_data, begin_err = get_trash_data(config)
  if begin_err then
    Notify[begin_err.type](begin_err.msg)
    return
  end

  -- Prompt user for number of days
  local days_input = prompt("Delete trash items older than (days)", false, "30")
  if not days_input then
    Notify.info("Empty trash by days cancelled")
    return
  end

  -- Validate input is a positive integer
  local days = tonumber(days_input)
  if not days or days <= 0 or math.floor(days) ~= days then
    Notify.error("Invalid input: please enter a positive integer for days")
    return
  end

  -- Get files older than specified days with sizes and deletion dates
  local file_objects, file_err = get_trash_files_older_than_days(config, days)
  if file_err then
    Notify.error("Failed to get trash file list: %s", file_err)
    return
  end

  -- If no files found that are older than the specified days
  if #file_objects == 0 then
    Notify.info("No items found that are older than %d days", days)
    return
  end

  -- Show detailed confirmation dialog with file list, sizes, and deletion dates
  if
    not confirm_batch_operation_with_dates("permanently delete", file_objects, days, "This action cannot be undone!")
  then
    return
  end

  -- Execute trash-empty command with days parameter
  local err, _ = run_command("trash-empty", { tostring(days) }, "y\n")
  if err then
    Notify.error("Failed to empty trash by days: %s", err)
    return
  end

  -- Get trash data after the operation to calculate difference
  local end_data, end_err = get_trash_data(config)
  if end_err then
    Notify[end_err.type](end_err.msg)
    return
  end

  -- Calculate items deleted
  local items_deleted = begin_data.count - end_data.count
  if items_deleted > 0 then
    Notify.info("Successfully removed %d trash items older than %d days", items_deleted, days)
  else
    Notify.info("No items found that are older than %d days", days)
  end
end

local function cmd_delete_selection(config)
  -- Ensure we have a trash directory selected
  if not ensure_trash_directory(config) then return end

  -- Check if current directory is within a valid trash directory
  if not is_current_dir_in_trash(config) then return end

  -- Validate selection and get filenames
  local selected_paths, _ = validate_and_get_selection("deletion")
  if not selected_paths then return end

  -- Get file objects with sizes for confirmation dialog
  local trash_files_dir = get_trash_files_dir(config)
  -- Ensure it ends with / for get_files_with_sizes
  if not trash_files_dir:match("/$") then trash_files_dir = trash_files_dir .. "/" end
  local file_objects = get_files_with_sizes(selected_paths, trash_files_dir)

  -- Confirm deletion from trash with warning
  if not confirm_batch_operation("permanently delete", file_objects, "This action cannot be undone!") then return end

  -- Create operation function for delete
  local function delete_operation(path)
    local filename = path:match(PATTERNS.filename) or path

    -- Use trash-rm with the filename as pattern
    -- trash-rm uses fnmatch patterns, so we pass the filename directly
    local delete_err, _ = run_command("trash-rm", { filename })
    if delete_err then
      Notify.error("Failed to delete %s: %s", filename, delete_err)
      return delete_err
    else
      return nil
    end
  end

  -- Execute batch operation
  local success_count, failed_count = execute_batch_operation(selected_paths, "permanently deleting", delete_operation)

  -- Clear selection after successful delete to prevent stale selections
  if success_count > 0 then clear_selection() end

  -- Report results
  report_operation_results("deleting", success_count, failed_count)
end

local function cmd_restore_selection(config)
  -- Ensure we have a trash directory selected
  if not ensure_trash_directory(config) then return end

  -- Check if current directory is within a valid trash directory
  if not is_current_dir_in_trash(config) then return end

  -- Validate selection and get filenames
  local selected_paths, _ = validate_and_get_selection("restoration")
  if not selected_paths then return end

  -- Get trash file mappings from trash-list
  local trash_mappings, mapping_err = get_trash_file_mappings()
  if mapping_err then
    Notify.error("Failed to get trash mappings: %s. Try 'trash-list' manually to verify trash contents", mapping_err)
    return
  end

  -- Prepare restore items with original paths and size information
  local restore_items = {}
  local trash_files_dir = get_trash_files_dir(config)
  local normalized_trash_files_dir = trash_files_dir
  if not normalized_trash_files_dir:match("/$") then normalized_trash_files_dir = normalized_trash_files_dir .. "/" end

  for _, path in ipairs(selected_paths) do
    local filename = path:match(PATTERNS.filename) or path
    local original_path = trash_mappings[filename]

    if original_path then
      -- Get file size from trash files directory
      local full_path = normalized_trash_files_dir .. filename
      local bytes, size_err = get_file_size(full_path)
      local formatted_size = size_err and "unknown size" or format_file_size(bytes)

      restore_items[#restore_items + 1] = {
        filename = filename,
        original_path = original_path,
        name = filename,
        size = formatted_size,
      }
    else
      Notify.warn("Could not find original path for file: %s", filename)
    end
  end

  if #restore_items == 0 then
    Notify.info("No files to restore in current trash directory")
    return
  end

  -- Handle potential conflicts at original file locations
  local filtered_items = handle_restore_conflicts(restore_items)
  if not filtered_items then
    -- User cancelled or no valid items after conflict resolution
    return
  end

  -- Update restore_items to use filtered list
  restore_items = filtered_items

  -- Confirm restoration
  if not confirm_batch_operation("restore", restore_items, nil) then return end

  -- Create operation function for restore using original paths
  local function restore_operation(item)
    debug("Restoring %s from original path: %s", item.filename, item.original_path)

    -- If this item needs overwrite, delete the existing file/directory first
    if item.needs_overwrite then
      local delete_success, delete_error = delete_conflict_file(item.original_path)
      if not delete_success then
        Notify.error(delete_error)
        return delete_error
      end
    end

    -- Use trash-restore with the original file path as argument and auto-select first match
    local restore_err, _ = run_command("trash-restore", { item.original_path }, "0\n")
    if restore_err then
      Notify.error("Failed to restore %s: %s", item.name, restore_err)
      return restore_err
    else
      return nil
    end
  end

  -- Execute batch operation
  local success_count, failed_count = execute_batch_operation(restore_items, "restoring", restore_operation)

  -- Clear selection after successful restore to prevent stale selections
  if success_count > 0 then clear_selection() end

  -- Report results
  report_operation_results("restoring", success_count, failed_count)
end

local function cmd_show_menu(config)
  local choice = ya.which({
    title = "Recycle Bin Menu",
    cands = {
      { on = "o", desc = "Open Trash" },
      { on = "r", desc = "Restore from Trash" },
      { on = "d", desc = "Delete from Trash" },
      { on = "e", desc = "Empty Trash" },
      { on = "D", desc = "Empty by Days" },
    },
  })

  if choice == 1 then
    cmd_open_trash(config)
  elseif choice == 2 then
    cmd_restore_selection(config)
  elseif choice == 3 then
    cmd_delete_selection(config)
  elseif choice == 4 then
    cmd_empty_trash(config)
  elseif choice == 5 then
    cmd_empty_trash_by_days(config)
  end
end

--=========== init requirements ================================================
---Verify all dependencies
local function check_dependencies()
  -- Check for trash-cli
  local trashcli_err, _ = run_command("trash-list", { "--version" }, nil, true)
  if trashcli_err then
    local path = os.getenv("PATH") or "(unset)"
    Notify.error("trashcli not found. Is it installed and in PATH? PATH=" .. path)
    return false
  end
  return true
end

---Initialize the plugin and verify dependencies
local function init()
  local initialized = get_state("is_initialized")
  if not initialized then
    if not check_dependencies() then return false end
    initialized = true
    set_state("is_initialized", true)
  end
  return initialized
end

--=========== Plugin start =================================================
-- Default configuration
local default_config = {
  trash_dir = nil, -- Will be auto-discovered from trash-list --trash-dirs
  os = ya.target_os(),
}

---Merges user‑provided configuration options into the defaults.
---@param user_config table|nil
local function set_plugin_config(user_config)
  local config = deep_merge(default_config, user_config or {})
  set_state(STATE_KEY.CONFIG, config)
end

---Setup
function M:setup(cfg)
  set_plugin_config(cfg)
end

---Entry
function M:entry(job)
  if not init() then return end

  -- Cache config to avoid multiple state access calls
  local config = get_state(STATE_KEY.CONFIG)
  local action = job.args[1]

  -- Pass config to functions that need it to avoid additional state calls
  if action == "menu" or not action then
    cmd_show_menu(config)
  elseif action == "open" then
    cmd_open_trash(config)
  elseif action == "delete" then
    cmd_delete_selection(config)
  elseif action == "restore" then
    cmd_restore_selection(config)
  elseif action == "emptyDays" then
    cmd_empty_trash_by_days(config)
  elseif action == "empty" then
    cmd_empty_trash(config)
  else
    Notify.error("Unknown action '%s'. Valid actions: menu, open, delete, restore, empty, emptyDays", tostring(action))
  end
end

return M
