# File: cpp_python_debug/gui/action_editor_window.py
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import logging
import os
import threading
from typing import Dict, Any, Optional, List, Union, Tuple, Callable, TYPE_CHECKING

# Import assoluti
from cpp_python_debug.gui.dialogs import (
    FunctionSelectorDialog,
    SymbolListViewerDialog,
    SymbolAnalysisProgressDialog,
)
from cpp_python_debug.core.gdb_controller import (
    GDBSession,
)  # Usato in _browse_functions per query live

# from cpp_python_debug.core.config_manager import AppSettings # Passato come istanza, non importato direttamente se non per TYPE_CHECKING
from cpp_python_debug.core.gdb_interactive_inspector import GDBInteractiveInspector

# from cpp_python_debug.core.file_utils import sanitize_filename_component # Non usato qui

if TYPE_CHECKING:
    from cpp_python_debug.core.config_manager import AppSettings

    # from .main_window import GDBGui # Parent è tk.Widget, non GDBGui qui

logger = logging.getLogger(__name__)

# Default per una nuova azione, per coerenza se action_data è None
DEFAULT_ACTION_CONFIG_EDITOR = {
    "breakpoint_location": "main",
    "variables_to_dump": ["my_variable"],
    "output_format": "json",
    "output_directory": "./debug_dumps",
    "filename_pattern": "{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}.{format}",
    "continue_after_dump": True,
    "dump_on_every_hit": True,
}


class ActionEditorWindow(tk.Toplevel):
    def __init__(
        self,
        parent: tk.Widget,  # Parent generico tk.Widget
        action_data: Optional[Dict[str, Any]] = None,
        is_new: bool = True,
        target_executable_path: Optional[str] = None,
        app_settings: Optional[
            "AppSettings"
        ] = None,  # Usa stringa per forward reference
        symbol_analysis_data: Optional[Dict[str, Any]] = None,
        program_parameters_for_scope: Optional[str] = "",
    ):
        super().__init__(parent)
        self.parent_window = parent
        self.is_new_action = is_new
        self.result: Optional[Dict[str, Any]] = None

        self.target_executable_path: Optional[str] = target_executable_path
        self.app_settings: Optional["AppSettings"] = app_settings
        self.symbol_analysis_data: Optional[Dict[str, Any]] = symbol_analysis_data
        self.program_parameters_for_scope: str = (
            program_parameters_for_scope
            if program_parameters_for_scope is not None
            else ""
        )

        title = "Add New Action" if self.is_new_action else "Edit Action"
        self.title(title)

        window_width = 700
        window_height = 680

        self.parent_window.update_idletasks()  # Assicura che la geometria del parent sia nota
        parent_x = self.parent_window.winfo_x()
        parent_y = self.parent_window.winfo_y()
        parent_width = self.parent_window.winfo_width()
        parent_height = self.parent_window.winfo_height()
        position_x = max(0, parent_x + (parent_width // 2) - (window_width // 2))
        position_y = max(0, parent_y + (parent_height // 2) - (window_height // 2))
        self.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}")

        self.resizable(False, False)
        self.transient(parent)
        self.grab_set()

        self.breakpoint_var = tk.StringVar()
        self.output_format_var = tk.StringVar()
        self.output_directory_var = tk.StringVar()
        self.filename_pattern_var = tk.StringVar()
        self.continue_after_dump_var = tk.BooleanVar()
        self.dump_on_every_hit_var = tk.BooleanVar()

        self._initial_action_data = (
            action_data.copy() if action_data else None
        )  # Salva una copia se necessario
        self.gdb_inspector_instance: Optional[GDBInteractiveInspector] = None
        self.scope_inspection_dialog: Optional[SymbolAnalysisProgressDialog] = None

        self._scope_variables_cache: Dict[
            Tuple[str, str, str], Dict[str, List[str]]
        ] = {}
        self._last_bp_for_scope_cache: str = ""

        self._create_widgets()
        self._load_action_data(action_data)
        self._update_browse_button_states()

        self.breakpoint_var.trace_add("write", self._on_breakpoint_var_change)
        self.protocol("WM_DELETE_WINDOW", self._on_cancel)
        # wait_window sarà chiamato dal ProfileManagerWindow

    def _on_breakpoint_var_change(self, *args):
        self._update_browse_button_states()

    def _create_widgets(self) -> None:
        main_frame = ttk.Frame(self, padding="15")
        main_frame.pack(expand=True, fill=tk.BOTH)
        main_frame.columnconfigure(1, weight=1)  # Colonna degli entry si espande

        row_idx = 0

        # Breakpoint Location
        ttk.Label(main_frame, text="Breakpoint Location:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
        )
        bp_input_frame = ttk.Frame(main_frame)
        bp_input_frame.grid(
            row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5
        )
        bp_input_frame.columnconfigure(0, weight=1)  # Entry si espande
        self.bp_entry = ttk.Entry(bp_input_frame, textvariable=self.breakpoint_var)
        self.bp_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5))
        self.browse_funcs_button = ttk.Button(
            bp_input_frame, text="Funcs...", command=self._browse_functions, width=8
        )
        self.browse_funcs_button.grid(row=0, column=1, sticky=tk.E, padx=(0, 2))
        self.browse_files_button = ttk.Button(
            bp_input_frame, text="Files...", command=self._browse_source_files, width=8
        )
        self.browse_files_button.grid(row=0, column=2, sticky=tk.E, padx=(0, 5))
        row_idx += 1
        ttk.Label(
            main_frame,
            text="(e.g., main, file.cpp:123, MyClass::foo)",
            foreground="gray",
        ).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10))
        row_idx += 1

        # Variables to Dump
        vars_label_frame = ttk.LabelFrame(
            main_frame, text="Variables to Dump", padding=(5, 2)
        )
        vars_label_frame.grid(
            row=row_idx, column=0, sticky="new", padx=5, pady=5, rowspan=2
        )  # rowspan per allineare con text box
        vars_label_frame.columnconfigure(0, weight=1)
        self.browse_globals_button = ttk.Button(
            vars_label_frame,
            text="Globals...",
            command=self._browse_global_variables,
            width=12,
        )
        self.browse_globals_button.pack(side=tk.TOP, anchor=tk.W, pady=(0, 3), padx=2)
        self.browse_scope_vars_button = ttk.Button(
            vars_label_frame,
            text="Scope Vars...",
            command=self._browse_scope_variables,
            width=12,
        )
        self.browse_scope_vars_button.pack(
            side=tk.TOP, anchor=tk.W, pady=(0, 3), padx=2
        )
        self.test_action_button = ttk.Button(
            vars_label_frame,
            text="Test Action...",
            command=self._test_action_placeholder,
            width=12,
            state=tk.DISABLED,
        )  # Funzionalità non implementata
        self.test_action_button.pack(side=tk.TOP, anchor=tk.W, pady=(0, 0), padx=2)

        self.variables_text = scrolledtext.ScrolledText(
            main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)
        )
        self.variables_text.grid(
            row=row_idx,
            column=1,
            columnspan=2,
            sticky="nsew",
            padx=5,
            pady=5,
            rowspan=2,
        )
        row_idx += 2
        ttk.Label(
            main_frame, text="(One per line, e.g., my_var, my_ptr@size, my_matrix@rows,cols)", foreground="gray"
        ).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10))
        row_idx += 1

        # Output Format
        ttk.Label(main_frame, text="Output Format:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
        )
        self.output_format_combo = ttk.Combobox(
            main_frame,
            textvariable=self.output_format_var,
            values=["json", "csv"],
            state="readonly",
            width=10,
        )
        self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5)
        row_idx += 1

        # Output Directory
        ttk.Label(main_frame, text="Output Directory:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
        )
        self.output_dir_entry = ttk.Entry(
            main_frame, textvariable=self.output_directory_var
        )
        self.output_dir_entry.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=5)
        ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(
            row=row_idx, column=2, padx=5, pady=5
        )
        row_idx += 1

        # Filename Pattern
        ttk.Label(main_frame, text="Filename Pattern:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
        )
        self.filename_pattern_entry = ttk.Entry(
            main_frame, textvariable=self.filename_pattern_var
        )
        self.filename_pattern_entry.grid(
            row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5
        )
        row_idx += 1
        ttk.Label(
            main_frame,
            text="(Placeholders: {profile_name}, {app_name}, {breakpoint}, {variable}, {timestamp}, {format})",
            foreground="gray",
        ).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10))
        row_idx += 1

        # Action Behavior Options
        behavior_frame = ttk.Frame(main_frame)
        behavior_frame.grid(
            row=row_idx, column=0, columnspan=3, sticky="ew", pady=(5, 0)
        )
        self.continue_check = ttk.Checkbutton(
            behavior_frame,
            text="Continue execution after this action completes",
            variable=self.continue_after_dump_var,
        )
        self.continue_check.pack(side=tk.LEFT, anchor=tk.W, padx=5, pady=2)
        self.dump_every_hit_check = ttk.Checkbutton(
            behavior_frame,
            text="Dump variables every time breakpoint is hit",
            variable=self.dump_on_every_hit_var,
        )
        self.dump_every_hit_check.pack(side=tk.LEFT, anchor=tk.W, padx=(20, 5), pady=2)
        row_idx += 1

        # OK/Cancel Buttons
        buttons_frame = ttk.Frame(main_frame)
        buttons_frame.grid(
            row=row_idx, column=0, columnspan=3, pady=(15, 5), sticky="e"
        )
        ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(
            side=tk.RIGHT, padx=5
        )
        ttk.Button(
            buttons_frame, text="Cancel", command=self._on_cancel, width=10
        ).pack(side=tk.RIGHT, padx=5)

    def _update_browse_button_states(self) -> None:
        analysis_available = bool(self.symbol_analysis_data)
        target_exe_str = (
            self.target_executable_path
            if isinstance(self.target_executable_path, str)
            else ""
        )
        target_is_valid_file = bool(target_exe_str and os.path.isfile(target_exe_str))

        gdb_exe_is_configured = False
        if self.app_settings:
            gdb_exe_path_setting = self.app_settings.get_setting(
                "general", "gdb_executable_path"
            )
            gdb_exe_is_configured = bool(
                gdb_exe_path_setting and os.path.isfile(gdb_exe_path_setting)
            )

        can_do_live_gdb_query = target_is_valid_file and gdb_exe_is_configured

        self.browse_funcs_button.config(
            state=(
                tk.NORMAL
                if analysis_available or can_do_live_gdb_query
                else tk.DISABLED
            )
        )

        can_browse_files = (
            analysis_available
            and isinstance(self.symbol_analysis_data, dict)
            and isinstance(self.symbol_analysis_data.get("symbols"), dict)
            and bool(self.symbol_analysis_data["symbols"].get("source_files"))
        )
        self.browse_files_button.config(
            state=tk.NORMAL if can_browse_files else tk.DISABLED
        )

        can_browse_globals = (
            analysis_available
            and isinstance(self.symbol_analysis_data, dict)
            and isinstance(self.symbol_analysis_data.get("symbols"), dict)
            and bool(self.symbol_analysis_data["symbols"].get("global_variables"))
        )
        self.browse_globals_button.config(
            state=tk.NORMAL if can_browse_globals else tk.DISABLED
        )

        self.browse_scope_vars_button.config(
            state=(
                tk.NORMAL
                if can_do_live_gdb_query and self.breakpoint_var.get().strip()
                else tk.DISABLED
            )
        )

    def _load_action_data(self, action_data: Optional[Dict[str, Any]]) -> None:
        # Usa la costante definita a livello di modulo
        defaults = DEFAULT_ACTION_CONFIG_EDITOR.copy()
        if (
            self.app_settings
        ):  # Sovrascrivi il default per output_directory se AppSettings è disponibile
            default_output_dir_from_settings = self.app_settings.get_setting(
                "general",
                "default_dump_output_dir",
                # Non fornire un altro fallback qui, se non c'è in AppSettings, il default di DEFAULT_ACTION_CONFIG_EDITOR
                # per output_directory verrà usato quando si accede a data_to_load.get(...)
            )
            if default_output_dir_from_settings:  # Se AppSettings fornisce un valore
                defaults["output_directory"] = default_output_dir_from_settings

        data_to_load = action_data if action_data else defaults

        self.breakpoint_var.set(
            data_to_load.get("breakpoint_location", defaults["breakpoint_location"])
        )
        variables_list = data_to_load.get(
            "variables_to_dump", defaults["variables_to_dump"]
        )
        if isinstance(variables_list, list):
            self.variables_text.insert(tk.END, "\n".join(variables_list))
        else:
            self.variables_text.insert(tk.END, str(variables_list))  # Fallback
        self.output_format_var.set(
            data_to_load.get("output_format", defaults["output_format"])
        )
        self.output_directory_var.set(
            data_to_load.get("output_directory", defaults["output_directory"])
        )
        self.filename_pattern_var.set(
            data_to_load.get("filename_pattern", defaults["filename_pattern"])
        )
        self.continue_after_dump_var.set(
            data_to_load.get("continue_after_dump", defaults["continue_after_dump"])
        )
        self.dump_on_every_hit_var.set(
            data_to_load.get("dump_on_every_hit", defaults["dump_on_every_hit"])
        )
        self._last_bp_for_scope_cache = self.breakpoint_var.get()

    def _browse_output_dir(self) -> None:
        current_path = self.output_directory_var.get()
        initial_dir = (
            current_path
            if current_path and os.path.isdir(current_path)
            else os.getcwd()
        )  # Fallback a CWD
        path = filedialog.askdirectory(
            title="Select Output Directory for Dumps",
            initialdir=initial_dir,
            parent=self,
        )
        if path:
            self.output_directory_var.set(path)

    def _validate_data(self) -> bool:
        if not self.breakpoint_var.get().strip():
            messagebox.showerror(
                "Validation Error", "Breakpoint Location cannot be empty.", parent=self
            )
            return False
        variables_str = self.variables_text.get("1.0", tk.END).strip()
        if not variables_str:
            messagebox.showerror(
                "Validation Error", "Variables to Dump cannot be empty.", parent=self
            )
            return False
        if not self.output_directory_var.get().strip():
            messagebox.showerror(
                "Validation Error", "Output Directory cannot be empty.", parent=self
            )
            return False
        if not self.filename_pattern_var.get().strip():
            messagebox.showerror(
                "Validation Error", "Filename Pattern cannot be empty.", parent=self
            )
            return False
        pattern_str = self.filename_pattern_var.get()
        if not any(
            ph in pattern_str
            for ph in ["{breakpoint}", "{variable}", "{timestamp}", "{app_name}"]
        ):
            messagebox.showwarning(
                "Pattern Warning",
                "Filename Pattern misses common placeholders (e.g., {breakpoint}, {variable}, {timestamp}, {app_name}). This may cause overwritten files.",
                parent=self,
            )
        if "{format}" not in pattern_str:
            messagebox.showwarning(
                "Pattern Warning",
                "Filename Pattern is missing {format}. File extension might be incorrect.",
                parent=self,
            )
        return True

    def _on_ok(self) -> None:
        if not self._validate_data():
            return
        variables_list = [
            line.strip()
            for line in self.variables_text.get("1.0", tk.END).strip().splitlines()
            if line.strip()
        ]
        if not variables_list:
            messagebox.showerror(
                "Validation Error",
                "Variables to Dump list is empty after processing.",
                parent=self,
            )
            return
        self.result = {
            "breakpoint_location": self.breakpoint_var.get().strip(),
            "variables_to_dump": variables_list,
            "output_format": self.output_format_var.get(),
            "output_directory": self.output_directory_var.get().strip(),
            "filename_pattern": self.filename_pattern_var.get().strip(),
            "continue_after_dump": self.continue_after_dump_var.get(),
            "dump_on_every_hit": self.dump_on_every_hit_var.get(),
        }
        self.destroy()

    def _on_cancel(self) -> None:
        self.result = None
        self.destroy()

    def get_result(self) -> Optional[Dict[str, Any]]:
        return self.result

    def _browse_functions(self) -> None:
        target_exe_str = (
            self.target_executable_path
            if isinstance(self.target_executable_path, str)
            else ""
        )
        functions_to_show_dicts: List[Dict[str, Any]] = []
        functions_to_show_names: List[str] = []
        source_of_functions = "live GDB query"

        if self.symbol_analysis_data and isinstance(self.symbol_analysis_data, dict):
            analyzed_exe = self.symbol_analysis_data.get("analyzed_executable_path")
            if (
                analyzed_exe
                and target_exe_str
                and os.path.normpath(analyzed_exe) == os.path.normpath(target_exe_str)
            ):
                cached_functions_data = self.symbol_analysis_data.get(
                    "symbols", {}
                ).get("functions", [])
                if isinstance(cached_functions_data, list) and cached_functions_data:
                    if all(isinstance(item, dict) for item in cached_functions_data):
                        functions_to_show_dicts = cached_functions_data
                        source_of_functions = "cached analysis (rich)"
                    elif all(isinstance(item, str) for item in cached_functions_data):
                        functions_to_show_names = cached_functions_data
                        source_of_functions = "cached analysis (names only)"

        if not functions_to_show_dicts and not functions_to_show_names:
            if not target_exe_str or not os.path.isfile(target_exe_str):
                messagebox.showerror(
                    "Error", "Target executable not set or invalid.", parent=self
                )
                return
            if not self.app_settings:
                messagebox.showerror(
                    "Error", "Application settings unavailable.", parent=self
                )
                return
            gdb_exe_path = self.app_settings.get_setting(
                "general", "gdb_executable_path"
            )
            if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
                messagebox.showerror(
                    "GDB Error",
                    f"GDB not configured or found: {gdb_exe_path}",
                    parent=self,
                )
                return

            self.config(cursor="watch")
            self.update_idletasks()
            temp_gdb_session: Optional[GDBSession] = None
            live_query_error = False
            try:
                logger.info(f"Live GDB query for functions. Target: {target_exe_str}")
                temp_gdb_session = GDBSession(gdb_exe_path, target_exe_str, None, {})
                default_timeouts = self.app_settings._get_default_settings().get(
                    "timeouts", {}
                )
                startup_timeout = self.app_settings.get_setting(
                    "timeouts", "gdb_start", default_timeouts.get("gdb_start", 15)
                )  # Shorter for non-interactive
                command_timeout = self.app_settings.get_setting(
                    "timeouts", "gdb_command", default_timeouts.get("gdb_command", 30)
                )
                temp_gdb_session.start(timeout=startup_timeout)
                if not temp_gdb_session.symbols_found:
                    messagebox.showwarning(
                        "No Symbols",
                        "GDB reported no debug symbols. Function list may be incomplete.",
                        parent=self,
                    )
                functions_to_show_names = temp_gdb_session.list_functions(
                    timeout=command_timeout
                )
            except Exception as e:
                logger.error(f"Error live function query: {e}", exc_info=True)
                messagebox.showerror(
                    "GDB Query Error", f"Could not retrieve functions: {e}", parent=self
                )
                live_query_error = True
            finally:
                if temp_gdb_session and temp_gdb_session.is_alive():
                    quit_timeout = self.app_settings.get_setting(
                        "timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 5)
                    )
                    try:
                        temp_gdb_session.quit(timeout=quit_timeout)
                    except Exception:
                        pass
                self.config(cursor="")
            if live_query_error:
                return

        selected_function_name: Optional[str] = None
        dialog_instance: Optional[
            Union[SymbolListViewerDialog, FunctionSelectorDialog]
        ] = None
        if functions_to_show_dicts:
            dialog_instance = SymbolListViewerDialog(
                self,
                functions_to_show_dicts,
                title=f"Select Function ({source_of_functions})",
                return_full_dict=False,
            )
        elif functions_to_show_names:
            dialog_instance = FunctionSelectorDialog(
                self,
                functions_to_show_names,
                title=f"Select Function ({source_of_functions})",
            )
        else:
            messagebox.showinfo(
                "No Functions",
                f"No functions found (source: {source_of_functions}).",
                parent=self,
            )
            return

        if dialog_instance:
            self.wait_window(dialog_instance)
            result_from_dialog = dialog_instance.result
        if isinstance(result_from_dialog, str):
            selected_function_name = result_from_dialog
        if selected_function_name:
            self.breakpoint_var.set(selected_function_name)
            self._last_bp_for_scope_cache = selected_function_name
            logger.info(f"BP location set to func: {selected_function_name}")

    def _browse_source_files(self) -> None:
        if (
            not self.symbol_analysis_data
            or not isinstance(self.symbol_analysis_data.get("symbols"), dict)
            or not self.symbol_analysis_data["symbols"].get("source_files")
        ):
            messagebox.showinfo(
                "No Source Data",
                "Symbol analysis data with source files is not available.",
                parent=self,
            )
            return
        source_files_list = self.symbol_analysis_data["symbols"]["source_files"]
        if not source_files_list:
            messagebox.showinfo(
                "No Source Files", "No source files found in analysis.", parent=self
            )
            return
        dialog = SymbolListViewerDialog(
            self,
            source_files_list,
            title="Select Source File (from analysis)",
            return_full_dict=False,
        )
        self.wait_window(dialog)
        selected_file_path = dialog.result
        if selected_file_path and isinstance(selected_file_path, str):
            normalized_path = os.path.normpath(selected_file_path)
            self.breakpoint_var.set(f"{normalized_path}:")
            self.bp_entry.focus_set()
            self.bp_entry.icursor(tk.END)
            self._last_bp_for_scope_cache = self.breakpoint_var.get()
            logger.info(f"BP location prefix set to file: {normalized_path}:")

    def _browse_global_variables(self) -> None:
        if (
            not self.symbol_analysis_data
            or not isinstance(self.symbol_analysis_data.get("symbols"), dict)
            or not self.symbol_analysis_data["symbols"].get("global_variables")
        ):
            messagebox.showinfo(
                "No Global Variables Data",
                "Symbol analysis data with global variables is not available.",
                parent=self,
            )
            return
        global_vars_list = self.symbol_analysis_data["symbols"]["global_variables"]
        if not global_vars_list:
            messagebox.showinfo(
                "No Global Variables",
                "No global variables found in analysis.",
                parent=self,
            )
            return
        dialog = SymbolListViewerDialog(
            self,
            global_vars_list,
            title="Select Global Variables to Dump (from analysis)",
            allow_multiple_selection=True,
            return_full_dict=False,
        )
        self.wait_window(dialog)
        selected_vars = dialog.result
        if selected_vars and isinstance(selected_vars, list):
            current_text = self.variables_text.get("1.0", tk.END).strip()
            current_list = [
                ln.strip() for ln in current_text.splitlines() if ln.strip()
            ]
            count = 0
            for var_item in selected_vars:
                if isinstance(var_item, str) and var_item not in current_list:
                    prefix = (
                        "\n" if current_text and not current_text.endswith("\n") else ""
                    )
                    self.variables_text.insert(tk.END, f"{prefix}{var_item}\n")
                    current_text = self.variables_text.get("1.0", tk.END).strip()
                    current_list.append(var_item)
                    count += 1
            if count > 0:
                self.variables_text.see(tk.END)
                logger.info(f"Added {count} global variables.")

    def _browse_scope_variables(self) -> None:
        bp_loc = self.breakpoint_var.get().strip()
        if not bp_loc:
            messagebox.showerror(
                "Input Error", "Breakpoint Location must be specified.", parent=self
            )
            return
        target_exe = (
            self.target_executable_path
            if isinstance(self.target_executable_path, str)
            else ""
        )
        if not target_exe or not os.path.isfile(target_exe):
            messagebox.showerror(
                "Config Error", "Valid Target Executable required.", parent=self
            )
            return
        if not self.app_settings:
            messagebox.showerror(
                "Config Error", "App settings unavailable.", parent=self
            )
            return
        gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
        if not gdb_exe or not os.path.isfile(gdb_exe):
            messagebox.showerror(
                "Config Error", "GDB not configured/found.", parent=self
            )
            return

        prog_args = self.program_parameters_for_scope
        cache_key = (target_exe, bp_loc, prog_args)
        if cache_key in self._scope_variables_cache:
            logger.info(f"Using cached scope vars for: {cache_key}")
            self._finalize_scope_variables_fetch(
                self._scope_variables_cache[cache_key],
                "Scope data from cache.",
                False,
                True,
            )
            return

        logger.info(f"No cache for scope vars: {cache_key}. Fetching live.")
        self._last_bp_for_scope_cache = bp_loc
        self.gdb_inspector_instance = GDBInteractiveInspector(
            gdb_exe, self.app_settings
        )
        self.scope_inspection_dialog = SymbolAnalysisProgressDialog(self)
        self.scope_inspection_dialog.title("Inspecting Scope")
        self.scope_inspection_dialog.set_status(f"Reaching BP '{bp_loc}'...")
        self.scope_inspection_dialog.log_message(
            "Starting GDB MI for scope inspection..."
        )
        self.attributes("-disabled", True)
        threading.Thread(
            target=self._fetch_scope_variables_thread,
            args=(target_exe, bp_loc, prog_args, cache_key),
            daemon=True,
        ).start()

    def _fetch_scope_variables_thread(
        self,
        target_exe: str,
        bp_loc: str,
        prog_args: str,
        cache_key: Tuple[str, str, str],
    ):
        results: Dict[str, List[str]] = {"locals": [], "args": []}
        error = False
        final_msg = "Scope inspection complete."
        fetched = False

        def inspector_status_cb(msg: str):
            if (
                self.scope_inspection_dialog
                and self.scope_inspection_dialog.winfo_exists()
            ):
                self.after(0, self.scope_inspection_dialog.log_message, msg)
                self.after(0, self.scope_inspection_dialog.set_status, msg)

        if self.gdb_inspector_instance:
            try:
                results = self.gdb_inspector_instance.get_variables_in_scope(
                    target_exe, bp_loc, prog_args, inspector_status_cb
                )
                if results.get("locals") or results.get("args"):
                    fetched = True
                    final_msg = "Scope inspection successful."
                elif (
                    self.scope_inspection_dialog
                    and self.scope_inspection_dialog.winfo_exists()
                    and not any(
                        ekw
                        in self.scope_inspection_dialog.status_label_var.get().lower()
                        for ekw in ["error", "fail", "timeout"]
                    )
                ):
                    final_msg = "No locals/args at BP, or not hit."
            except Exception as e:
                logger.error(
                    f"Exception in _fetch_scope_vars_thread: {e}", exc_info=True
                )
                final_msg = f"Error during scope inspection: {e}"
                error = True
        if self.winfo_exists():
            self.after(
                0,
                self._finalize_scope_variables_fetch,
                results,
                final_msg,
                error,
                fetched,
            )

    def _finalize_scope_variables_fetch(
        self,
        scope_vars_data: Dict[str, List[str]],
        status_msg: str,
        error_occurred: bool,
        data_was_fetched: bool,
    ):
        if self.scope_inspection_dialog and self.scope_inspection_dialog.winfo_exists():
            self.scope_inspection_dialog.analysis_complete_or_failed(
                not error_occurred and data_was_fetched
            )
            self.scope_inspection_dialog.set_status(status_msg)
        if self.winfo_exists():
            self.attributes("-disabled", False)
            self.focus_set()
        if error_occurred:
            messagebox.showerror("Scope Inspection Error", status_msg, parent=self)
            return

        all_scope_vars = scope_vars_data.get("args", []) + scope_vars_data.get(
            "locals", []
        )
        if not all_scope_vars and data_was_fetched:
            messagebox.showinfo(
                "Scope Variables",
                "No locals or args found at BP, or BP not hit.",
                parent=self,
            )
            return
        elif not data_was_fetched and not error_occurred:
            if not status_msg.startswith("Scope data from cache."):
                messagebox.showinfo("Scope Variables", status_msg, parent=self)
                return

        if all_scope_vars:
            scope_var_names = sorted(list(set(all_scope_vars)))
            dialog = SymbolListViewerDialog(
                self,
                scope_var_names,
                title="Select Variables from Scope",
                allow_multiple_selection=True,
                return_full_dict=False,
            )
            self.wait_window(dialog)
            selected_vars = dialog.result
            if selected_vars and isinstance(selected_vars, list):
                current_text = self.variables_text.get("1.0", tk.END).strip()
                current_list = [
                    ln.strip() for ln in current_text.splitlines() if ln.strip()
                ]
                count = 0
                for var_name in selected_vars:
                    if isinstance(var_name, str) and var_name not in current_list:
                        prefix = (
                            "\n"
                            if current_text and not current_text.endswith("\n")
                            else ""
                        )
                        self.variables_text.insert(tk.END, f"{prefix}{var_name}\n")
                        current_text = self.variables_text.get("1.0", tk.END).strip()
                        current_list.append(var_name)
                        count += 1
                if count > 0:
                    self.variables_text.see(tk.END)
                    logger.info(f"Added {count} scope variables.")

    def _test_action_placeholder(self) -> None:
        messagebox.showinfo(
            "Not Implemented",
            "The 'Test Action' feature is not yet implemented.",
            parent=self,
        )
