-- Check for windows
local is_windows = ya.target_family() == "windows"
-- Define flags and strings
local is_password, is_encrypted, is_level, cmd_password, cmd_level, default_extension = false, false, false, "", "", "zip"

-- Function to check valid filename
local function is_valid_filename(name)
    -- Trim whitespace from both ends
    name = name:match("^%s*(.-)%s*$")
    if name == "" then
        return false
    end
    if is_windows then
        -- Windows forbidden chars and reserved names
        if name:find('[<>:"/\\|%?%*]') then
            return false
        end
    else
        -- Unix forbidden chars
        if name:find("/") or name:find("%z") then
            return false
        end
    end
    return true
end

-- Function to send notifications
local function notify_error(message, urgency)
    ya.notify(
        {
            title = "Archive",
            content = message,
            level = urgency,
            timeout = 5
        }
    )
end

-- Function to check if command is available
local function is_command_available(cmd)
    local stat_cmd
    if is_windows then
        stat_cmd = string.format("where %s > nul 2>&1", cmd)
    else
        stat_cmd = string.format("command -v %s >/dev/null 2>&1", cmd)
    end
    local cmd_exists = os.execute(stat_cmd)
    if cmd_exists then
        return true
    else
        return false
    end
end

-- Function to change command arrays --> string -- Use first command available or first command
local function find_command_name(cmd_list)
    for _, cmd in ipairs(cmd_list) do
        if is_command_available(cmd) then
            return cmd
        end
    end
    return cmd_list[1] -- Return first command as fallback
end

-- Function to append filename to it's parent directory url
local function combine_url(path, file)
    path, file = Url(path), Url(file)
    return tostring(path:join(file))
end

-- Function to make a table of selected or hovered files: path = filenames
local selected_or_hovered =
    ya.sync(
    function()
        local tab, paths, names, path_fnames = cx.active, {}, {}, {}
        for _, u in pairs(tab.selected) do
            paths[#paths + 1] = tostring(u.parent)
            names[#names + 1] = tostring(u.name)
        end
        if #paths == 0 and tab.current.hovered then
            paths[1] = tostring(tab.current.hovered.url.parent)
            names[1] = tostring(tab.current.hovered.name)
        end
        for idx, name in ipairs(names) do
            if not path_fnames[paths[idx]] then
                path_fnames[paths[idx]] = {}
            end
            table.insert(path_fnames[paths[idx]], name)
        end
        return path_fnames, names, tostring(tab.current.cwd)
    end
)

-- Table of archive commands
local archive_commands = {
    ["%.zip$"] = {
        {command = "zip", args = {"-r"}, level_arg = "-", level_min = 0, level_max = 9, passwordable = true},
        {
            command = {"7z", "7zz", "7za"},
            args = {"a", "-tzip"},
            level_arg = "-mx=",
            level_min = 0,
            level_max = 9,
            passwordable = true
        },
        {
            command = {"tar", "bsdtar"},
            args = {"-caf"},
            level_arg = {"--option", "compression-level="},
            level_min = 1,
            level_max = 9
        }
    },
    ["%.7z$"] = {
        {
            command = {"7z", "7zz", "7za"},
            args = {"a"},
            level_arg = "-mx=",
            level_min = 0,
            level_max = 9,
            header_arg = "-mhe=on",
            passwordable = true
        }
    },
    ["%.rar$"] = {
        {
            command = "rar",
            args = {"a"},
            level_arg = "-m",
            level_min = 0,
            level_max = 5,
            header_arg = "-hp",
            passwordable = true
        }
    },
    ["%.tar.gz$"] = {
        {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "gzip"},
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-mx=",
            level_min = 1,
            level_max = 9,
            compress = "7z",
            compress_args = {"a", "-tgzip"}
        },
        {
            command = {"tar", "bsdtar"},
            args = {"-czf"},
            level_arg = {"--option", "gzip:compression-level="},
            level_min = 1,
            level_max = 9
        }
    },
    ["%.tar.xz$"] = {
        {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "xz"},
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-mx=",
            level_min = 1,
            level_max = 9,
            compress = "7z",
            compress_args = {"a", "-txz"}
        },
        {
            command = {"tar", "bsdtar"},
            args = {"-cJf"},
            level_arg = {"--option", "xz:compression-level="},
            level_min = 1,
            level_max = 9
        }
    },
    ["%.tar.bz2$"] = {
        {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "bzip2"},
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-mx=",
            level_min = 1,
            level_max = 9,
            compress = "7z",
            compress_args = {"a", "-tbzip2"}
        },
        {
            command = {"tar", "bsdtar"},
            args = {"-cjf"},
            level_arg = {"--option", "bzip2:compression-level="},
            level_min = 1,
            level_max = 9
        }
    },
    ["%.tar.zst$"] = {
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-",
            level_min = 1,
            level_max = 22,
            compress = "zstd",
            compress_args = {"--ultra"}
        }
    },
    ["%.tar.lz4$"] = {
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-",
            level_min = 1,
            level_max = 12,
            compress = "lz4"
        }
    },
    ["%.tar.lha$"] = {
        {
            command = {"tar", "bsdtar"},
            args = {"rpf"},
            level_arg = "-o",
            level_min = 5,
            level_max = 7,
            compress = "lha",
            compress_args = {"-a"}
        }
    },
    ["%.tar$"] = {
        {command = {"tar", "bsdtar"}, args = {"rpf"}}
    }
}

return {
    entry = function(_, job)
        -- Parse flags and default extension
        if job.args ~= nil then
            for _, arg in ipairs(job.args) do
                if arg:match("^%-(%w+)$") then
                    -- Handle combined flags (e.g., -phl)
                    for flag in arg:sub(2):gmatch(".") do
                        if flag == "p" then
                            is_password = true
                        elseif flag == "h" then
                            is_encrypted = true
                        elseif flag == "l" then
                            is_level = true
                        end
                    end
                elseif arg:match("^%w[%w\\.]*$") then
                    -- Handle default extension (e.g., 7z, zip)
                    if archive_commands["%." .. arg .. "$"] then
                        default_extension = arg
                    else
                        notify_error(string.format("Unsupported extension: %s", arg), "warn")
                    end
                else
                      notify_error(string.format("Unknown argument: %s", arg), "warn")
                end
            end
        end

        -- Exit visual mode
        ya.emit("escape", {visual = true})
        -- Define file table and output_dir (pwd)
        local path_fnames, fnames, output_dir = selected_or_hovered()
        -- Get archive filename
        local output_name, event =
            ya.input(
            {
                title = "Create archive:",
                position = {"top-center", y = 3, w = 40}
            }
        )
        if event ~= 1 then
            return
        end

        -- Determine the default name for the archive
        local default_name = #fnames == 1 and fnames[1] or Url(output_dir).name
        output_name = output_name == "" and string.format("%s.%s", default_name, default_extension) or output_name

        -- Add default extension if none is specified
        if not output_name:match("%.%w+$") then
            output_name = string.format("%s.%s", output_name, default_extension)
        end

        -- Validate the final archive filename
        if not is_valid_filename(output_name) then
            notify_error("Invalid archive filename", "error")
            return
        end

        -- Match user input to archive command
        local archive_cmd,
            archive_args,
            archive_compress,
            archive_level_arg,
            archive_level_min,
            archive_level_max,
            archive_header_arg,
            archive_passwordable,
            archive_compress_args
        local matched_pattern = false
        for pattern, cmd_list in pairs(archive_commands) do
            if output_name:match(pattern) then
                matched_pattern = true -- Mark that file extension is correct
                for _, cmd in ipairs(cmd_list) do
                    -- Check if archive_cmd is available
                    local find_command = type(cmd.command) == "table" and find_command_name(cmd.command) or cmd.command
                    if is_command_available(find_command) then
                        -- Check if compress_cmd (if listed) is available
                        if cmd.compress == nil or is_command_available(cmd.compress) then
                            archive_cmd = find_command
                            archive_args = cmd.args
                            archive_compress = cmd.compress or ""
                            archive_level_arg = is_level and cmd.level_arg or ""
                            archive_level_min = cmd.level_min
                            archive_level_max = cmd.level_max
                            archive_header_arg = is_encrypted and cmd.header_arg or ""
                            archive_passwordable = cmd.passwordable or false
                            archive_compress_args = cmd.compress_args or {}
                            break
                        end
                    end
                end
                if archive_cmd then
                    break
                end
            end
        end

        -- Check if no archive command is available for the extension
        if not matched_pattern then
            notify_error("Unsupported file extension", "error")
            return
        end

        -- Check if no suitable archive program was found
        if not archive_cmd then
            notify_error("Could not find a suitable archive program for the selected file extension", "error")
            return
        end

        -- Check if archive command has multiple names
        if type(archive_cmd) == "table" then
            archive_cmd = find_command_name(archive_cmd)
        end

        -- Exit if archive command is not available
        if not is_command_available(archive_cmd) then
            notify_error(string.format("%s not available", archive_cmd), "error")
            return
        end

        -- Exit if compress command is not available
        if archive_compress ~= "" and not is_command_available(archive_compress) then
            notify_error(string.format("%s compression not available", archive_compress), "error")
            return
        end

        -- Add password arg if selected
        if archive_passwordable and is_password then
            local output_password, event =
                ya.input(
                {
                    title = "Enter password:",
                    obscure = true,
                    position = {"top-center", y = 3, w = 40}
                }
            )
            if event ~= 1 then
                return
            end
            if output_password ~= "" then
                cmd_password = "-P" .. output_password
                if archive_cmd == "rar" and is_encrypted then
                    cmd_password = archive_header_arg .. output_password -- Add archive arg for rar
                end
                table.insert(archive_args, cmd_password)
            end
        end

        -- Add header arg if selected for 7z
        if is_encrypted and archive_header_arg ~= "" and archive_cmd ~= "rar" then
            table.insert(archive_args, archive_header_arg)
        end

        -- Add level arg if selected
        if archive_level_arg ~= "" and is_level then
            local output_level, event =
                ya.input(
                {
                    title = string.format("Enter compression level (%s - %s)", archive_level_min, archive_level_max),
                    position = {"top-center", y = 3, w = 40}
                }
            )
            if event ~= 1 then
                return
            end
            -- Validate user input for compression level
            if
                output_level ~= "" and tonumber(output_level) ~= nil and tonumber(output_level) >= archive_level_min and
                    tonumber(output_level) <= archive_level_max
             then
                cmd_level =
                    type(archive_level_arg) == "table" and archive_level_arg[#archive_level_arg] .. output_level or
                    archive_level_arg .. output_level
                local target_args = archive_compress == "" and archive_args or archive_compress_args
                if type(archive_level_arg) == "table" then
                    -- Insert each element of archive_level_arg (except last) into target_args at the correct position
                    for i = 1, #archive_level_arg - 1 do
                        table.insert(target_args, i, archive_level_arg[i])
                    end
                    table.insert(target_args, #archive_level_arg, cmd_level) -- Add level at the end
                else
                    -- Insert the compression level argument at the start if not a table
                    table.insert(target_args, 1, cmd_level)
                end
            else
                notify_error("Invalid level specified. Using defaults.", "warn")
            end
        end

        -- Store the original output name for later use
        local original_name = output_name

        -- If compression is needed, adjust the output name to exclude extensions like ".tar"
        if archive_compress ~= "" then
            output_name = output_name:match("(.*%.tar)") or output_name
        end

        -- Create a temporary directory for intermediate files
        local temp_dir_name = ".tmp_compress"
        local temp_dir = combine_url(output_dir, temp_dir_name)
        local temp_dir, _ = tostring(fs.unique_name(Url(temp_dir)))

        -- Attempt to create the temporary directory
        local temp_dir_status, temp_dir_err = fs.create("dir_all", Url(temp_dir))
        if not temp_dir_status then
            -- Notify the user if the temporary directory creation fails
            notify_error(string.format("Failed to create temp directory, error code: %s", temp_dir_err), "error")
            return
        end

        -- Define the temporary output file path within the temporary directory
        local temp_output_url = combine_url(temp_dir, output_name)

        -- Add files to the output archive
        for filepath, filenames in pairs(path_fnames) do
            -- Execute the archive command for each path and its respective files
            local archive_status, archive_err =
                Command(archive_cmd):arg(archive_args):arg(temp_output_url):arg(filenames):cwd(filepath):spawn():wait()
            if not archive_status or not archive_status.success then
                -- Notify the user if the archiving process fails and clean up the temporary directory
                notify_error(string.format("Failed to create archive %s with '%s', error: %s", output_name, archive_cmd, archive_err), "error")
                local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
                if not cleanup_status then
                    notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
                end
                return
            end
        end

        -- If compression is required, execute the compression command
        if archive_compress ~= "" then
            local compress_status, compress_err =
                Command(archive_compress):arg(archive_compress_args):arg(temp_output_url):spawn():wait()
            if not compress_status or not compress_status.success then
                -- Notify the user if the compression process fails and clean up the temporary directory
                notify_error(string.format("Failed to compress archive %s with '%s', error: %s", output_name, archive_compress, compress_err), "error")
                local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
                if not cleanup_status then
                    notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
                end
                return
            end
        end

        -- Move the final file from the temporary directory to the output directory
        local final_output_url, temp_url_processed = combine_url(output_dir, original_name), combine_url(temp_dir, original_name)
        final_output_url, _ = tostring(fs.unique_name(Url(final_output_url)))
        local move_status, move_err = os.rename(temp_url_processed, final_output_url)
        if not move_status then
            -- Notify the user if the move operation fails and clean up the temporary directory
            notify_error(string.format("Failed to move %s to %s, error: %s", temp_url_processed, final_output_url, move_err), "error")
            local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
            if not cleanup_status then
                notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
            end
            return
        end

        -- Cleanup the temporary directory after successful operation
        local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
        if not cleanup_status then
            notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
        end
    end
}
