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

from highlight_files import highlight_files  # Ensure this module is available


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)
        Updated to allow zero or more spaces before the index.
        """
        pattern = re.compile(r'^(?P<base>.+?)\s*(?:\((?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, its existence status, and its duplicate list string (semicolon-delimited).
        """
        results = []

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

        # 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:
                    # If directory can't be read, mark duplicates as error
                    for path in dir_paths:
                        results.append((path, False, "Error reading directory"))
                    continue  # Skip processing files in this directory

            for path in dir_paths:
                try:
                    exists = os.path.exists(path)
                    dup_str = self.get_duplicate_string(path, siblings, directory)
                    results.append((path, exists, dup_str))
                except Exception:
                    results.append((path, False, "Error processing file"))

        return results

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

        # Escape base and extension to handle any special characters
        escaped_base = re.escape(base)
        escaped_ext = re.escape(ext)

        # Pattern to match duplicates: basename + optional spaces + (index) + optional extension
        # This will match files like 'file(1).pdf', 'file   .pdf', 'file(2).pdf', etc.
        dup_pattern = re.compile(
            rf'^{escaped_base}\s*(?:\(\d{{1,3}}\))?{escaped_ext}$')

        duplicates = []

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

            if dup_pattern.match(sibling):
                dup_file_path = os.path.join(directory, sibling)
                duplicates.append(dup_file_path)

        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("1200x700")  # Increased width to accommodate horizontal scrollbar
        self.row_data = {}  # Map Treeview row IDs to their data
        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')

        # Define headings
        self.tree.heading("file_path", text="File Path")
        self.tree.heading("dup_list", text="Duplicate List")

        # Define column widths
        self.tree.column("file_path", width=800, anchor=tk.W)
        self.tree.column("dup_list", width=400, 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_y = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscroll=table_scrollbar_y.set)
        table_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)

        # Add horizontal scrollbar to the table
        table_scrollbar_x = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.tree.xview)
        self.tree.configure(xscroll=table_scrollbar_x.set)
        table_scrollbar_x.pack(fill=tk.X, padx=10)

        # Status bar
        self.status_var = tk.StringVar()
        self.status_var.set("Ready")
        status_bar = ttk.Label(self, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(fill=tk.X, side=tk.BOTTOM, ipady=2)

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

        # 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:
            self.status_var.set("No file paths entered.")
            return

        # Process the paths using DuplicateFinder
        results = self.finder.find_duplicates(paths)

        # Populate the table with results and map row data
        for file_path, exists, dup_str in results:
            row_id = self.tree.insert("", tk.END, values=(file_path, dup_str))
            self.row_data[row_id] = {
                "file_path": file_path,
                "exists": exists,
                "duplicates": dup_str
            }

        self.status_var.set("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:
            self.status_var.set("Import canceled.")
            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)
            self.status_var.set(f"Contents of '{os.path.basename(file_path)}' have been appended.")
        except Exception as e:
            self.status_var.set(f"Import error: {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:
            self.status_var.set("Clear and import canceled.")
            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)
            self.status_var.set(f"Contents of '{os.path.basename(file_path)}' have been loaded.")
        except Exception as e:
            self.status_var.set(f"Clear and Import error: {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():
            self.status_var.set("No data to deduplicate. Please run 'Find Duplicates' first.")
            return

        # Collect results
        results = []
        for item in self.tree.get_children():
            data = self.row_data.get(item, {})
            file_path = data.get("file_path")
            dup_str = data.get("duplicates")
            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:
            self.status_var.set("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():
                data = self.row_data.get(item, {})
                if data.get("file_path") == original:
                    self.tree.item(item, values=(new, data.get("duplicates")))
                    self.row_data[item]["file_path"] = new
                    break

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

        # Display summary in status bar
        self.status_var.set(summary)

    def on_row_double_click(self, event):
        """
        Callback for double-clicking a table row.
        Differentiates between 'File Path' and 'Duplicate List' columns.
        """
        # Identify the row and column
        region = self.tree.identify("region", event.x, event.y)
        if region != "cell":
            return  # Only act on cell double-clicks

        row_id = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)

        if not row_id:
            return  # Click was outside any row

        column_name = self.tree.heading(column)["text"]

        data = self.row_data.get(row_id, {})
        file_path = data.get("file_path")
        exists = data.get("exists")
        dup_str = data.get("duplicates")

        if column_name == "File Path":
            if not exists:
                # Exact match does not exist
                paths_to_highlight = []
            else:
                # Exact match exists
                paths_to_highlight = [file_path]

            # Add existing duplicates to highlight
            if dup_str and dup_str != "None":
                duplicates = dup_str.split(";")
                existing_duplicates = [dup for dup in duplicates if os.path.exists(dup)]
                paths_to_highlight.extend(existing_duplicates)

            if paths_to_highlight:
                highlight_files(paths_to_highlight)  # Highlight in Explorer
            # If no files to highlight, do nothing

        elif column_name == "Duplicate List":
            if dup_str and dup_str != "None":
                duplicates = dup_str.split(";")
            else:
                duplicates = []

            exact_match_text = f'"{file_path}"' if exists else "null"

            yaml_text = f'Exact match: {exact_match_text}\nDuplicates:'
            for dup in duplicates:
                yaml_text += f'\n- "{dup}"'

            self.show_text_dialog("Duplicate Files", yaml_text)

    def show_help(self):
        """
        Opens a Help window displaying information about the application.
        """
        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 `basename<0 or more spaces><(index)><.extension that may or may not exist>`.\n"
            " - This includes files like `file(1).txt`, `file   .txt`, `file(2).txt`, etc.\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 a 'File Path' cell to highlight the file and its existing duplicates.\n"
            "7. Double-click on a 'Duplicate List' cell to view detailed duplicate information.\n"
            "8. 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."
        )

        self.show_text_dialog("Help - Duplicate File Finder", help_text)

    def show_text_dialog(self, title, content):
        """
        Generic method to show a dialog with a read-only text area and an OK button.
        """
        dialog = tk.Toplevel(self)
        dialog.title(title)
        dialog.geometry("600x500")
        dialog.resizable(False, False)

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

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

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

        text_widget = tk.Text(content_frame, wrap=tk.WORD, state='normal')
        text_widget.grid(row=0, column=0, sticky="nsew")

        # Insert content and make it read-only
        text_widget.insert(tk.END, content)
        text_widget.configure(state='disabled')

        # Scrollbar for the text widget
        scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=text_widget.yview)
        scrollbar.grid(row=0, column=1, sticky="ns")
        text_widget.configure(yscrollcommand=scrollbar.set)

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

        ok_button = ttk.Button(button_frame, text="OK", command=dialog.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()
```

more features.

1. some file names have chained extension like foo (1).bar.baz.qux in this case the basename is foo (1). make sure the parse filename logic handles such,
2. the new pattern is foo<potential whitespaces><potential index><potential whitespaces><potential chained ext>
3. allow user to input pattern. the textbox contains the default pattern ^(?P<base>.+?)\s*(?:\((?P<index>\d{1,3})\))?(?P<ext>\.[^.]+)?$ by default
4. add a 3rd column Deduplicate with button Rename to the table
which change matches of `foo<potential whitespaces><potential index><potential whitespaces><potential chained ext>` to `foo<chained ext if originally exists>` if match count is exactly 1. notice if foo is exact match and it is the only 1 match then theres nothing to do since it is in its basename form.
5. Change the 2nd column, the duplicate list column to `Match Count` which displays number of matches
6. update the Match display dialog to show a table instead of text output
```
File Path Exact?
match1   Yes
match2   No
```
add a Deduplicate button before the table which does the same as 5
8. Deduplicate/rename buttons should be disabled if matches dont satisfy deduplication
9. the global `Deduplicate` button should be made to `Deduplicate All`
10. retain the double click actions for column 1 and column 2