-- 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,
		position = { "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
