```
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import re
from collections import defaultdict
from highlight_files import highlight_files


class DuplicateFinder:
    """
    Handles the logic for finding duplicate files based on naming conventions.
    """

    def __init__(self):
        self.dir_cache = {}  # Cache to store directory listings

    def parse_filename(self, filename):
        """
        Parses the filename into base, index (if any), and extension.
        Returns a tuple: (base, index, extension)
        """
        pattern = re.compile(r'^(?P<base>.+?)(?:\((?P<index>\d{1,3})\))?(?P<ext>\.[^.]+)?$')
        match = pattern.match(filename)
        if match:
            base = match.group('base')
            index = match.group('index')
            ext = match.group('ext') if match.group('ext') else ''
            return base, index, ext
        else:
            return filename, None, ''

    def find_duplicates(self, paths):
        """
        Given a list of file paths, returns a list of tuples containing
        the original file path and its duplicate list string.
        """
        results = []

        # Reset directory cache for each run
        self.dir_cache.clear()

        # Validate all paths first
        invalid_paths = [path for path in paths if not os.path.exists(path)]
        if invalid_paths:
            raise FileNotFoundError(f"The following paths do not exist:\n" + "\n".join(invalid_paths))

        # Group paths by their directory to utilize caching
        dir_to_files = defaultdict(list)
        for path in paths:
            directory, _ = os.path.split(path)
            dir_to_files[directory].append(path)

        # Process each directory
        for directory, dir_paths in dir_to_files.items():
            if directory in self.dir_cache:
                siblings = self.dir_cache[directory]
            else:
                try:
                    siblings = os.listdir(directory)
                    self.dir_cache[directory] = siblings
                except Exception as e:
                    for path in dir_paths:
                        results.append((path, "Error reading directory"))
                    continue  # Skip processing files in this directory

            for path in dir_paths:
                try:
                    dup_str = self.get_duplicate_string(path, siblings)
                    results.append((path, dup_str))
                except Exception as e:
                    results.append((path, f"Error processing file: {e}"))

        return results

    def get_duplicate_string(self, path, siblings):
        """
        Given a file path and the list of sibling files, returns the duplicate list string as specified.
        """
        directory, filename = os.path.split(path)
        base, index, ext = self.parse_filename(filename)

        if index:
            base_filename = f"{base}{ext}"
        else:
            base_filename = filename

        duplicates = []
        indices = []

        # If input has index, check if base file exists
        if index:
            if base_filename in siblings:
                duplicates.append("base")

        # Pattern to match duplicates
        # Escape base and extension to handle any special characters
        escaped_base = re.escape(base)
        escaped_ext = re.escape(ext)
        dup_pattern = re.compile(rf'^{escaped_base}\((\d{{1,3}})\){escaped_ext}$')

        for sibling in siblings:
            if sibling == filename:
                continue  # Skip the input file itself

            match = dup_pattern.match(sibling)
            if match:
                dup_index = match.group(1).lstrip('0') or '0'  # Remove leading zeros
                if dup_index.isdigit():
                    dup_index_int = int(dup_index)
                    if 0 <= dup_index_int <= 999:
                        indices.append(str(dup_index_int))

        # Sort indices numerically and remove duplicates
        unique_sorted_indices = sorted(set(indices), key=lambda x: int(x))

        if index:
            if unique_sorted_indices:
                duplicates.append(",".join(unique_sorted_indices))
        else:
            duplicates = unique_sorted_indices

        dup_list_str = ",".join(duplicates) if duplicates else "None"
        return dup_list_str


class DuplicateFinderApp(tk.Tk):
    """
    The main application class that handles the UI and user interactions.
    """

    def __init__(self, finder):
        super().__init__()
        self.finder = finder
        self.title("Duplicate File Finder")
        self.geometry("1000x700")  # Adjusted size for better layout with scrollable input
        self.create_widgets()

    def create_widgets(self):
        # Frame for input, buttons, and help
        input_frame = ttk.Frame(self)
        input_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=10)

        # Label for input
        input_label = ttk.Label(input_frame, text="Enter file paths (one per line):")
        input_label.pack(anchor=tk.W)

        # Frame for text input and its scrollbar
        text_frame = ttk.Frame(input_frame)
        text_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        # Text widget for multiline input
        self.text_input = tk.Text(text_frame, height=15, wrap=tk.NONE)
        self.text_input.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # Vertical scrollbar for the text widget
        text_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.text_input.yview)
        text_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # Configure the text widget to work with the scrollbar
        self.text_input.configure(yscrollcommand=text_scrollbar.set)

        # Frame for buttons
        button_frame = ttk.Frame(input_frame)
        button_frame.pack(fill=tk.X, pady=5)

        # Button to process input
        process_button = ttk.Button(button_frame, text="Find Duplicates", command=self.process_paths)
        process_button.pack(side=tk.LEFT, padx=(0, 5))

        # Import button
        import_button = ttk.Button(button_frame, text="Import", command=self.import_paths)
        import_button.pack(side=tk.LEFT, padx=(0, 5))

        # Clear and Import button
        clear_import_button = ttk.Button(button_frame, text="Clear and Import", command=self.clear_and_import_paths)
        clear_import_button.pack(side=tk.LEFT, padx=(0, 5))

        # Deduplicate button
        deduplicate_button = ttk.Button(button_frame, text="Deduplicate", command=self.deduplicate_files)
        deduplicate_button.pack(side=tk.LEFT, padx=(0, 5))

        # Help button
        help_button = ttk.Button(button_frame, text="Help", command=self.show_help)
        help_button.pack(side=tk.LEFT)

        # Separator
        separator = ttk.Separator(self, orient='horizontal')
        separator.pack(fill=tk.X, padx=10, pady=10)

        # Frame for the table
        table_frame = ttk.Frame(self)
        table_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Define columns
        columns = ("file_path", "dup_list")
        self.tree = ttk.Treeview(table_frame, columns=columns, show='headings')
        self.tree.heading("file_path", text="File Path")
        self.tree.heading("dup_list", text="Duplicate List")
        self.tree.column("file_path", width=700, anchor=tk.W)
        self.tree.column("dup_list", width=200, anchor=tk.W)
        self.tree.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)

        # Bind double-click event
        self.tree.bind("<Double-1>", self.on_row_double_click)

        # Add vertical scrollbar to the table
        table_scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscroll=table_scrollbar.set)
        table_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

    def process_paths(self):
        """
        Callback for the 'Find Duplicates' button.
        """
        # Clear existing table entries
        for item in self.tree.get_children():
            self.tree.delete(item)

        # Get input paths
        input_text = self.text_input.get("1.0", tk.END)
        paths = [line.strip() for line in input_text.strip().splitlines() if line.strip()]

        if not paths:
            messagebox.showerror("Input Error", "Please enter at least one file path.")
            return

        # Process the paths using DuplicateFinder
        try:
            results = self.finder.find_duplicates(paths)
        except FileNotFoundError as fe:
            messagebox.showerror("Invalid Paths", str(fe))
            return
        except Exception as e:
            messagebox.showerror("Error", f"An unexpected error occurred:\n{e}")
            return

        # Populate the table with results
        for file_path, dup_str in results:
            self.tree.insert("", tk.END, values=(file_path, dup_str))

        messagebox.showinfo("Processing Complete", "Duplicate analysis completed.")

    def import_paths(self):
        """
        Callback for the 'Import' button.
        Appends the content of a selected .txt file to the input textbox.
        """
        file_path = filedialog.askopenfilename(
            title="Select a Text File to Import",
            filetypes=[("Text Files", "*.txt")]
        )
        if not file_path:
            return  # User canceled the dialog

        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
            current_text = self.text_input.get("1.0", tk.END)
            if not current_text.endswith('\n'):
                content = '\n' + content
            self.text_input.insert(tk.END, content)
            messagebox.showinfo("Import Successful", f"Contents of '{os.path.basename(file_path)}' have been appended.")
        except Exception as e:
            messagebox.showerror("Import Error", f"An error occurred while importing the file:\n{e}")

    def clear_and_import_paths(self):
        """
        Callback for the 'Clear and Import' button.
        Clears the input textbox and inserts the content of a selected .txt file.
        """
        file_path = filedialog.askopenfilename(
            title="Select a Text File to Import",
            filetypes=[("Text Files", "*.txt")]
        )
        if not file_path:
            return  # User canceled the dialog

        try:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
            self.text_input.delete("1.0", tk.END)
            self.text_input.insert(tk.END, content)
            messagebox.showinfo("Import Successful", f"Contents of '{os.path.basename(file_path)}' have been loaded.")
        except Exception as e:
            messagebox.showerror("Import Error", f"An error occurred while importing the file:\n{e}")

    def deduplicate_files(self):
        """
        Callback for the 'Deduplicate' button.
        Renames files with no duplicates to their base name if conditions are met.
        """
        # First, ensure that duplicates have been found
        if not self.tree.get_children():
            messagebox.showerror("No Data", "Please run 'Find Duplicates' first.")
            return

        # Collect results
        results = []
        for item in self.tree.get_children():
            file_path, dup_str = self.tree.item(item, "values")
            results.append((file_path, dup_str))

        # Filter files with "None" duplication
        files_to_deduplicate = [fp for fp, dup in results if dup == "None"]

        if not files_to_deduplicate:
            messagebox.showinfo("No Files to Deduplicate", "There are no files without duplicates to deduplicate.")
            return

        rename_success = []
        rename_fail = []

        for file_path in files_to_deduplicate:
            directory, filename = os.path.split(file_path)
            base, index, ext = self.finder.parse_filename(filename)

            # Skip if there's no index to remove
            if not index:
                continue

            base_filename = f"{base}{ext}"
            base_file_path = os.path.join(directory, base_filename)

            # Ensure base file does not exist
            if os.path.exists(base_file_path):
                rename_fail.append((file_path, "Base file already exists."))
                continue

            # Proceed to rename since dup_str is "None" and base file doesn't exist
            try:
                os.rename(file_path, base_file_path)
                rename_success.append((file_path, base_file_path))
            except Exception as e:
                rename_fail.append((file_path, f"Rename failed: {e}"))

        # Update the table
        for original, new in rename_success:
            # Find the item in the tree and update it
            for item in self.tree.get_children():
                file_path, dup_str = self.tree.item(item, "values")
                if file_path == original:
                    self.tree.item(item, values=(new, dup_str))
                    break

        # Show summary
        summary = f"Deduplication Complete.\n\nSuccessfully renamed {len(rename_success)} file(s)."
        if rename_fail:
            summary += f"\n\nFailed to rename {len(rename_fail)} file(s):"
            for fail_file, reason in rename_fail:
                summary += f"\n- {fail_file}: {reason}"

        messagebox.showinfo("Deduplication Summary", summary)

    def on_row_double_click(self, event):
        """
        Callback for double-clicking a table row.
        Opens the file location in Windows Explorer and selects the file.
        """
        selected_item = self.tree.selection()
        if not selected_item:
            return
        item = selected_item[0]
        file_path = self.tree.item(item, "values")[0]
        if os.path.exists(file_path):
            # Use explorer to select the file
            try:
                # For paths with spaces or special characters, enclose in quotes
                highlight_files(os.path.normpath(file_path))
            except Exception as e:
                messagebox.showerror("Error", f"Could not open file location.\n{e}")
        else:
            messagebox.showerror("File Not Found", f"The file does not exist:\n{file_path}")

    def show_help(self):
        """
        Opens a Help window displaying information about the application.
        """
        help_window = tk.Toplevel(self)
        help_window.title("Help - Duplicate File Finder")
        help_window.geometry("600x500")
        help_window.resizable(False, False)

        # Center the help window relative to the main window
        self.center_window(help_window, 600, 500)

        # Frame for content
        content_frame = ttk.Frame(help_window, padding=10)
        content_frame.pack(fill=tk.BOTH, expand=True)

        # Label with help text
        help_text = (
            "Duplicate File Finder\n\n"
            "This application helps you identify duplicate files based on their naming conventions. "
            "You can enter multiple file paths, each on a new line, and the tool will analyze "
            "the specified directories to find duplicates with different numbered suffixes.\n\n"

            "**Input Format:**\n"
            " - Each file path should be on a separate line.\n"
            " - Example:\n"
            "   ```\n"
            "   C:/folder/file.txt\n"
            "   C:/folder/file(1).txt\n"
            "   ```\n\n"

            "**Duplicate Detection Logic:**\n"
            " - For a given file path, the application searches for duplicates in the same directory "
            "   following the pattern `file(<index>).extension`.\n"
            " - Handles files both with and without existing numbered indices.\n\n"

            "**How to Use:**\n"
            "1. Enter one or more valid file paths in the text area, each on a new line.\n"
            "2. Use the 'Import' button to append file paths from a `.txt` file.\n"
            "3. Use the 'Clear and Import' button to replace current file paths with those from a `.txt` file.\n"
            "4. Click the 'Find Duplicates' button to start the analysis.\n"
            "5. Results will be displayed in the table below, showing each file and its duplicates.\n"
            "6. Double-click on any row to open the file's location in Windows Explorer.\n"
            "7. Use the 'Deduplicate' button to rename files without duplicates to their base names.\n\n"

            "**Deduplication Conditions:**\n"
            "- The base file (`file.ext`) must not already exist.\n"
            "- Only files with an index (e.g., `file(1).ext`) are eligible for deduplication.\n\n"

            "For any issues or further assistance, please refer to the documentation or contact support."
        )

        # Scrollable Text widget for help content
        content_frame.columnconfigure(0, weight=1)
        content_frame.columnconfigure(1, weight=0)
        content_frame.rowconfigure(0, weight=1)

        # Scrollable Text widget for help content
        help_text_widget = tk.Text(content_frame, wrap=tk.WORD)
        help_text_widget.grid(row=0, column=0, sticky="nsew")

        # Insert help text and make it read-only
        help_text_widget.insert(tk.END, help_text)
        help_text_widget.configure(state='disabled')

        # Scrollbar for the help text
        help_scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=help_text_widget.yview)
        help_scrollbar.grid(row=0, column=1, sticky="ns")

        help_text_widget.configure(yscrollcommand=help_scrollbar.set)

        # OK button to close the help window
        button_frame = ttk.Frame(help_window, padding=10)
        button_frame.pack(fill=tk.X, pady=5)

        ok_button = ttk.Button(button_frame, text="OK", command=help_window.destroy)
        ok_button.pack()

    def center_window(self, win, width, height):
        """
        Centers a window on the screen.
        """
        screen_width = win.winfo_screenwidth()
        screen_height = win.winfo_screenheight()

        x = (screen_width // 2) - (width // 2)
        y = (screen_height // 2) - (height // 2)

        win.geometry(f'{width}x{height}+{x}+{y}')


def main():
    finder = DuplicateFinder()
    app = DuplicateFinderApp(finder)
    app.mainloop()


if __name__ == "__main__":
    main()
```
update the duplicate finding logic to
`path/to/basename<0 or more spaces><(index)><.extension that may or may not exist>` where `<>` is optional group

so `path/to/file(1).pdf` would scan `path/to/file.pdf`, `path/to/file(1).pdf`, `path/to/file    .pdf`, `path/to/file(2)` etc

also change Duplicate List to actual file list of duplicates, delimited by `;` like path
if the duplicate cell is double clicked, a window dialog contains a text input containing those duplicate will be shown, with ok button, this is the same dialog as the Help dialog so refactor this