# File: cpp_python_debug/gui/main_window.py
# Provides the Tkinter GUI for interacting with the GDB session and automated profiles.

import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu
import logging
import os
import json
import re
import threading
import subprocess
import sys
from datetime import datetime
from typing import (
    Optional,
    Dict,
    Any,
    Callable,
    List,
    Tuple,
)

from cpp_python_debug.core.config_manager import AppSettings
from cpp_python_debug.core.gdb_controller import GDBSession
from cpp_python_debug.core.output_formatter import save_to_json, save_to_csv
from cpp_python_debug.core.profile_executor import ProfileExecutor, ExecutionLogEntry
from cpp_python_debug.core.file_utils import sanitize_filename_component
from cpp_python_debug.gui.config_window import ConfigWindow
from cpp_python_debug.gui.profile_manager_window import ProfileManagerWindow
from cpp_python_debug.gui.dump_analysis_tab import DumpAnalysisTab

logger = logging.getLogger(__name__)

APP_TITLE_NAME = "Cpp-Python GDB Debug Helper"
try:
    from cpp_python_debug import _version as app_version_module

    APP_VERSION_INFO_STRING = f"v{app_version_module.__version__}"
    if (
        hasattr(app_version_module, "GIT_COMMIT_HASH")
        and app_version_module.GIT_COMMIT_HASH != "Unknown"
    ):
        APP_VERSION_INFO_STRING += f" (commit {app_version_module.GIT_COMMIT_HASH[:7]})"
except ImportError:
    logger.warning(
        "cpp_python_debug._version.py not found, using default version display."
    )
    APP_VERSION_INFO_STRING = "(Development Version)"

MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER = "ManualDump"
MANUAL_DUMP_FILENAME_PATTERN = (
    "{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}{extension}"
)
MANUAL_DUMPS_SUBFOLDER = (
    "manual_gdb_dumps"  # Sottocartella dentro la directory dei log dell'app
)
GDB_DUMP_EXTENSION = ".gdbdump.json"


class GDBGui(tk.Tk):
    # --- MODIFICA: Aggiornato costruttore per accettare percorsi ---
    def __init__(self, app_base_path: str, log_directory_path: str):
        super().__init__()

        self.app_base_path: str = app_base_path
        self.log_directory_path: str = log_directory_path
        logger.info(
            f"GDBGui initialized with base_path: {self.app_base_path}, log_dir: {self.log_directory_path}"
        )

        # AppSettings ora potrebbe usare app_base_path se modificato per non usare più appdirs per il file principale
        # Se AppSettings è già stato modificato per usare get_app_base_path(), non serve passarlo.
        # Per ora, assumiamo che AppSettings sappia come trovare il suo file di config.
        self.app_settings = AppSettings(
            app_base_path_override=self.app_base_path
        )  # Passa la base se AppSettings la usa
        # --- FINE MODIFICA ---

        self.gui_log_handler: Optional[ScrolledTextLogHandler] = None

        self.title(f"{APP_TITLE_NAME} - {APP_VERSION_INFO_STRING}")
        logger.info(f"Settings file in use: {self.app_settings.config_filepath}")

        self.geometry(
            self.app_settings.get_setting("gui", "main_window_geometry", "850x800")
        )

        self.gdb_session: Optional[GDBSession] = None
        self.last_manual_gdb_dump_filepath: Optional[str] = None
        self.program_started_once: bool = False

        self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...")
        self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...")

        default_general_settings = self.app_settings._get_default_settings().get(
            "general", {}
        )
        self.exe_path_var = tk.StringVar(
            value=self.app_settings.get_setting(
                "general",
                "last_target_executable_path",
                default_general_settings.get("last_target_executable_path", ""),
            )
        )
        self.breakpoint_var = tk.StringVar(
            value=self.app_settings.get_setting(
                "general",
                "default_breakpoint",
                default_general_settings.get("default_breakpoint", "main"),
            )
        )
        self.variable_var = tk.StringVar(
            value=self.app_settings.get_setting(
                "general",
                "default_variable_to_dump",
                default_general_settings.get("default_variable_to_dump", ""),
            )
        )
        self.params_var = tk.StringVar(
            value=self.app_settings.get_setting(
                "general",
                "default_program_parameters",
                default_general_settings.get("default_program_parameters", ""),
            )
        )

        self.profile_executor_instance: Optional[ProfileExecutor] = None
        self.available_profiles_map: Dict[str, Dict[str, Any]] = {}
        self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.")
        self.produced_files_tree: Optional[ttk.Treeview] = None
        self.last_run_output_path: Optional[str] = None
        self.manual_dumps_output_path: Optional[str] = None
        self.profile_progressbar: Optional[ttk.Progressbar] = None
        self.status_bar_widget: Optional[ttk.Label] = None
        self.status_var = tk.StringVar(value="Ready.")

        self._prepare_manual_dumps_directory()
        self._create_menus()
        self._create_widgets()
        self._setup_logging_redirect_to_gui()
        self._check_critical_configs_and_update_gui()
        self._load_and_populate_profiles_for_automation_tab()

        self.protocol("WM_DELETE_WINDOW", self._on_closing_window)
        logger.info(
            f"{APP_TITLE_NAME} GUI initialized. Version: {APP_VERSION_INFO_STRING}"
        )

    def _prepare_manual_dumps_directory(self):
        # --- MODIFICA: Usa self.log_directory_path come base per MANUAL_DUMPS_SUBFOLDER ---
        if not self.log_directory_path:  # Dovrebbe essere sempre impostato da __init__
            logger.error(
                "Log directory path not set in GDBGui, cannot prepare manual dumps directory."
            )
            self.manual_dumps_output_path = None
            if (
                hasattr(self, "open_manual_dumps_folder_button")
                and self.open_manual_dumps_folder_button
            ):
                self.open_manual_dumps_folder_button.config(state=tk.DISABLED)
            return

        self.manual_dumps_output_path = os.path.join(
            self.log_directory_path, MANUAL_DUMPS_SUBFOLDER
        )
        # --- FINE MODIFICA ---
        try:
            os.makedirs(self.manual_dumps_output_path, exist_ok=True)
            logger.info(
                f"Manual dumps directory ensured/created: {self.manual_dumps_output_path}"
            )
            if (
                hasattr(self, "open_manual_dumps_folder_button")
                and self.open_manual_dumps_folder_button
            ):
                self.open_manual_dumps_folder_button.config(state=tk.NORMAL)
        except OSError as e:
            logger.error(
                f"Could not create manual dumps directory '{self.manual_dumps_output_path}': {e}"
            )
            self.manual_dumps_output_path = None
            if (
                hasattr(self, "open_manual_dumps_folder_button")
                and self.open_manual_dumps_folder_button
            ):
                self.open_manual_dumps_folder_button.config(state=tk.DISABLED)

    def _create_menus(self):  # (Invariato)
        self.menubar = Menu(self)
        self.config(menu=self.menubar)
        options_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Options", menu=options_menu)
        options_menu.add_command(
            label="Configure Application...", command=self._open_config_window
        )
        options_menu.add_separator()
        options_menu.add_command(label="Exit", command=self._on_closing_window)
        profiles_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="Profiles", menu=profiles_menu)
        profiles_menu.add_command(
            label="Manage Profiles...", command=self._open_profile_manager_window
        )

    def _open_config_window(self):  # (Invariato)
        logger.debug("Opening configuration window.")
        config_win = ConfigWindow(self, self.app_settings)
        self.wait_window(config_win)
        logger.debug("Configuration window closed.")
        self._check_critical_configs_and_update_gui()
        self._prepare_manual_dumps_directory()

    def _open_profile_manager_window(self):  # (Invariato)
        logger.info("Opening Profile Manager window.")
        profile_win = ProfileManagerWindow(self, self.app_settings)
        self.wait_window(profile_win)
        logger.info("Profile Manager window closed.")
        self._load_and_populate_profiles_for_automation_tab()

    def _check_critical_configs_and_update_gui(self):  # (Invariato)
        logger.info("Checking critical configurations and updating GUI status...")
        gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
        dumper_script_path = self.app_settings.get_setting(
            "general", "gdb_dumper_script_path"
        )
        gdb_ok = False
        if gdb_exe_path and os.path.isfile(gdb_exe_path):
            self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)")
            gdb_ok = True
        elif gdb_exe_path:
            self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)")
        else:
            self.gdb_exe_status_var.set(
                "GDB: Not Configured! Set in Options > Configure."
            )
        if dumper_script_path and os.path.isfile(dumper_script_path):
            self.gdb_dumper_status_var.set(
                f"Dumper: {os.path.basename(dumper_script_path)} (OK)"
            )
        elif dumper_script_path:
            self.gdb_dumper_status_var.set(
                f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)"
            )
        else:
            self.gdb_dumper_status_var.set(
                "Dumper: Not Configured (Optional for JSON dump)."
            )
        is_profile_currently_running = bool(
            self.profile_executor_instance and self.profile_executor_instance.is_running
        )
        if hasattr(self, "start_gdb_button"):
            can_start_gdb = gdb_ok and not is_profile_currently_running
            self.start_gdb_button.config(
                state=tk.NORMAL if can_start_gdb else tk.DISABLED
            )
        if not gdb_ok and not is_profile_currently_running:
            self._reset_gui_to_stopped_state()

    def _create_widgets(self):  # (Invariato)
        main_frame = ttk.Frame(self, padding="10")
        main_frame.grid(row=0, column=0, sticky="nsew")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        main_frame.rowconfigure(0, weight=0)
        main_frame.rowconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=8)
        main_frame.rowconfigure(3, weight=0)
        main_frame.columnconfigure(0, weight=1)
        self._create_config_status_widgets(main_frame)
        self._create_mode_notebook_widgets(main_frame)
        self._create_output_log_widgets(main_frame)
        self._create_status_bar(main_frame)

    def _create_config_status_widgets(self, parent_frame: ttk.Frame):  # (Invariato)
        config_status_frame = ttk.LabelFrame(
            parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)
        )
        config_status_frame.grid(row=0, column=0, sticky="ew", pady=5, padx=0)
        config_status_frame.columnconfigure(1, weight=1)
        config_status_frame.columnconfigure(3, weight=1)
        ttk.Label(config_status_frame, text="GDB:").grid(
            row=0, column=0, sticky=tk.W, padx=(5, 0), pady=5
        )
        self.gdb_exe_status_label = ttk.Label(
            config_status_frame,
            textvariable=self.gdb_exe_status_var,
            relief="sunken",
            padding=(5, 2),
            anchor=tk.W,
        )
        self.gdb_exe_status_label.grid(
            row=0, column=1, sticky="ew", padx=(0, 10), pady=5
        )
        ttk.Label(config_status_frame, text="Dumper:").grid(
            row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5
        )
        self.gdb_dumper_status_label = ttk.Label(
            config_status_frame,
            textvariable=self.gdb_dumper_status_var,
            relief="sunken",
            padding=(5, 2),
            anchor=tk.W,
        )
        self.gdb_dumper_status_label.grid(
            row=0, column=3, sticky="ew", padx=(0, 10), pady=5
        )
        ttk.Button(
            config_status_frame, text="Configure...", command=self._open_config_window
        ).grid(row=0, column=4, padx=(5, 5), pady=5, sticky=tk.E)

    def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame):  # (Invariato)
        mode_notebook = ttk.Notebook(parent_frame)
        mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0)
        self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10")
        mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution")
        self._populate_automated_execution_tab(self.automated_exec_frame)
        manual_debug_frame = ttk.Frame(mode_notebook, padding="5")
        mode_notebook.add(manual_debug_frame, text="Manual Debug")
        self._populate_manual_debug_tab(manual_debug_frame)

        # Add the Dump Analysis Tab
        dump_analysis_frame = DumpAnalysisTab(mode_notebook, app_settings=self.app_settings, log_directory_path=self.log_directory_path)
        mode_notebook.add(dump_analysis_frame, text="Dump Analysis")

    def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame):  # (Invariato)
        parent_tab_frame.columnconfigure(0, weight=1)
        manual_target_settings_frame = ttk.LabelFrame(
            parent_tab_frame, text="Target & Debug Session Settings", padding="10"
        )
        manual_target_settings_frame.grid(row=0, column=0, sticky="ew", pady=5)
        manual_target_settings_frame.columnconfigure(1, weight=1)
        row_idx = 0
        ttk.Label(manual_target_settings_frame, text="Target Executable:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
        )
        ttk.Entry(
            manual_target_settings_frame, textvariable=self.exe_path_var, width=70
        ).grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2)
        ttk.Button(
            manual_target_settings_frame,
            text="Browse...",
            command=self._browse_target_exe,
        ).grid(row=row_idx, column=2, padx=5, pady=2)
        row_idx += 1
        ttk.Label(manual_target_settings_frame, text="Program Parameters:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
        )
        ttk.Entry(manual_target_settings_frame, textvariable=self.params_var).grid(
            row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2
        )
        row_idx += 1
        ttk.Label(manual_target_settings_frame, text="Breakpoint Location:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
        )
        ttk.Entry(manual_target_settings_frame, textvariable=self.breakpoint_var).grid(
            row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2
        )
        row_idx += 1
        ttk.Label(
            manual_target_settings_frame,
            text="Examples: main, file.cpp:123, MyClass::foo",
            foreground="gray",
            font=("TkDefaultFont", 8),
        ).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 5))
        row_idx += 1
        ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(
            row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
        )
        ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(
            row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2
        )
        manual_session_control_frame = ttk.LabelFrame(
            parent_tab_frame, text="Session Control", padding="10"
        )
        manual_session_control_frame.grid(row=1, column=0, sticky="ew", pady=(10, 5))
        button_flow_frame = ttk.Frame(manual_session_control_frame)
        button_flow_frame.pack(fill=tk.X, expand=True)
        self.start_gdb_button = ttk.Button(
            button_flow_frame,
            text="1. Start GDB",
            command=self._start_gdb_session_action,
            state=tk.DISABLED,
        )
        self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
        self.set_bp_button = ttk.Button(
            button_flow_frame,
            text="2. Set BP",
            command=self._set_gdb_breakpoint_action,
            state=tk.DISABLED,
        )
        self.set_bp_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
        self.run_button = ttk.Button(
            button_flow_frame,
            text="3. Run",
            command=self._run_or_continue_gdb_action,
            state=tk.DISABLED,
        )
        self.run_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
        self.dump_var_button = ttk.Button(
            button_flow_frame,
            text="4. Dump Var",
            command=self._dump_gdb_variable_action,
            state=tk.DISABLED,
        )
        self.dump_var_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
        self.stop_gdb_button = ttk.Button(
            button_flow_frame,
            text="Stop GDB",
            command=self._stop_gdb_session_action,
            state=tk.DISABLED,
        )
        self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
        manual_save_data_frame = ttk.LabelFrame(
            parent_tab_frame, text="Save/Manage Dumped Data", padding="10"
        )
        manual_save_data_frame.grid(row=2, column=0, sticky="ew", pady=5)
        self.save_json_button = ttk.Button(
            manual_save_data_frame,
            text="Save as JSON",
            command=lambda: self._save_dumped_data("json"),
            state=tk.DISABLED,
        )
        self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5)
        self.save_csv_button = ttk.Button(
            manual_save_data_frame,
            text="Save as CSV",
            command=lambda: self._save_dumped_data("csv"),
            state=tk.DISABLED,
        )
        self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5)
        self.open_manual_dumps_folder_button = ttk.Button(
            manual_save_data_frame,
            text="Open Dumps Folder",
            command=self._open_manual_dumps_folder,
            state=(
                tk.NORMAL
                if self.manual_dumps_output_path
                and os.path.isdir(self.manual_dumps_output_path)
                else tk.DISABLED
            ),
        )
        self.open_manual_dumps_folder_button.pack(side=tk.LEFT, padx=5, pady=5)

    def _populate_automated_execution_tab(
        self, parent_tab_frame: ttk.Frame
    ) -> None:  # (Invariato)
        parent_tab_frame.columnconfigure(0, weight=1)
        parent_tab_frame.rowconfigure(0, weight=0)
        parent_tab_frame.rowconfigure(1, weight=0)
        parent_tab_frame.rowconfigure(2, weight=1)
        parent_tab_frame.rowconfigure(3, weight=0)
        auto_control_frame = ttk.LabelFrame(
            parent_tab_frame, text="Profile Execution Control", padding="10"
        )
        auto_control_frame.grid(row=0, column=0, sticky="ew", pady=5)
        auto_control_frame.columnconfigure(1, weight=1)
        ttk.Label(auto_control_frame, text="Select Profile:").grid(
            row=0, column=0, padx=(5, 2), pady=5, sticky="w"
        )
        self.profile_selection_combo = ttk.Combobox(
            auto_control_frame, state="readonly", width=35, textvariable=tk.StringVar()
        )
        self.profile_selection_combo.grid(
            row=0, column=1, padx=(0, 5), pady=5, sticky="ew"
        )
        self.run_profile_button = ttk.Button(
            auto_control_frame,
            text="Run Profile",
            command=self._run_selected_profile_action,
            state=tk.DISABLED,
        )
        self.run_profile_button.grid(row=0, column=2, padx=(0, 2), pady=5, sticky="ew")
        self.stop_profile_button = ttk.Button(
            auto_control_frame,
            text="Stop Profile",
            command=self._stop_current_profile_action,
            state=tk.DISABLED,
        )
        self.stop_profile_button.grid(row=0, column=3, padx=(0, 5), pady=5, sticky="ew")
        progress_status_frame = ttk.Frame(parent_tab_frame)
        progress_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
        progress_status_frame.columnconfigure(0, weight=1)
        progress_status_frame.rowconfigure(0, weight=1)
        progress_status_frame.rowconfigure(1, weight=0)
        self.profile_exec_status_label_big = ttk.Label(
            progress_status_frame,
            textvariable=self.profile_exec_status_var,
            font=("TkDefaultFont", 10, "bold"),
            anchor=tk.NW,
            justify=tk.LEFT,
        )
        self.profile_exec_status_label_big.grid(
            row=0, column=0, sticky="new", padx=5, pady=(0, 2)
        )

        def _configure_wraplength_for_status_label(event):
            new_width = event.width - 15
            if (
                new_width > 20
                and hasattr(self, "profile_exec_status_label_big")
                and self.profile_exec_status_label_big.winfo_exists()
            ):
                self.profile_exec_status_label_big.config(wraplength=new_width)

        progress_status_frame.bind(
            "<Configure>", _configure_wraplength_for_status_label
        )
        self.profile_progressbar = ttk.Progressbar(
            progress_status_frame, orient=tk.HORIZONTAL, mode="indeterminate"
        )
        produced_files_frame = ttk.LabelFrame(
            parent_tab_frame, text="Produced Files Log", padding="10"
        )
        produced_files_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0))
        produced_files_frame.columnconfigure(0, weight=1)
        produced_files_frame.rowconfigure(0, weight=1)
        self.produced_files_tree = ttk.Treeview(
            produced_files_frame,
            columns=(
                "timestamp",
                "breakpoint_spec",
                "variable",
                "file",
                "status",
                "details",
            ),
            show="headings",
            selectmode="browse",
        )
        self.produced_files_tree.grid(row=0, column=0, sticky="nsew")
        headings = {
            "timestamp": "Time",
            "breakpoint_spec": "Breakpoint Spec",
            "variable": "Variable",
            "file": "File Produced",
            "status": "Status",
            "details": "Details",
        }
        widths = {
            "timestamp": 130,
            "breakpoint_spec": 150,
            "variable": 150,
            "file": 180,
            "status": 100,
            "details": 600,
        }
        minwidths = {
            "timestamp": 120,
            "breakpoint_spec": 100,
            "variable": 100,
            "file": 150,
            "status": 60,
            "details": 150,
        }
        stretches = {
            "timestamp": False,
            "breakpoint_spec": True,
            "variable": True,
            "file": True,
            "status": False,
            "details": True,
        }
        for col, text in headings.items():
            self.produced_files_tree.heading(col, text=text, anchor=tk.W)
            self.produced_files_tree.column(
                col, width=widths[col], minwidth=minwidths[col], stretch=stretches[col]
            )
        tree_scrollbar_y = ttk.Scrollbar(
            produced_files_frame,
            orient=tk.VERTICAL,
            command=self.produced_files_tree.yview,
        )
        tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
        tree_scrollbar_x = ttk.Scrollbar(
            produced_files_frame,
            orient=tk.HORIZONTAL,
            command=self.produced_files_tree.xview,
        )
        tree_scrollbar_x.grid(row=1, column=0, sticky="ew")
        self.produced_files_tree.configure(
            yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set
        )
        folder_button_frame = ttk.Frame(parent_tab_frame)
        folder_button_frame.grid(row=3, column=0, sticky="e", pady=(5, 0))
        self.open_output_folder_button = ttk.Button(
            folder_button_frame,
            text="Open Profile Output Folder",
            command=self._open_last_run_output_folder,
            state=tk.DISABLED,
        )
        self.open_output_folder_button.pack(side=tk.RIGHT, padx=5, pady=0)

    def _load_and_populate_profiles_for_automation_tab(self):  # (Invariato)
        self.available_profiles_map.clear()
        profiles_list = self.app_settings.get_profiles()
        profile_display_names = []
        for profile_item in profiles_list:
            name = profile_item.get("profile_name")
            self.available_profiles_map[name] = profile_item if name else {}
            profile_display_names.append(name if name else "Unnamed Profile")  # type: ignore
        sorted_names = sorted(profile_display_names)
        self.profile_selection_combo["values"] = sorted_names
        is_profile_currently_running = bool(
            self.profile_executor_instance and self.profile_executor_instance.is_running
        )
        gdb_is_ok = self.app_settings.get_setting(
            "general", "gdb_executable_path"
        ) and os.path.isfile(
            self.app_settings.get_setting("general", "gdb_executable_path")
        )
        if sorted_names:
            self.profile_selection_combo.set(sorted_names[0])
            self.run_profile_button.config(
                state=(
                    tk.NORMAL
                    if not is_profile_currently_running and gdb_is_ok
                    else tk.DISABLED
                )
            )
            self.profile_exec_status_var.set(
                f"Ready to run profile: {self.profile_selection_combo.get()}"
            )
        else:
            self.profile_selection_combo.set("")
            self.run_profile_button.config(state=tk.DISABLED)
            self.profile_exec_status_var.set(
                "No profiles defined. Create/manage via 'Profiles > Manage Profiles...'."
            )

    def _create_output_log_widgets(self, parent_frame: ttk.Frame):  # (Invariato)
        output_log_notebook = ttk.Notebook(parent_frame)
        output_log_notebook.grid(
            row=2, column=0, columnspan=1, sticky="nsew", pady=(5, 0), padx=0
        )
        log_text_height = 12
        self.gdb_raw_output_text = scrolledtext.ScrolledText(
            output_log_notebook,
            wrap=tk.WORD,
            height=log_text_height,
            state=tk.DISABLED,
            font=("Consolas", 9),
        )
        output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output")
        self.parsed_json_output_text = scrolledtext.ScrolledText(
            output_log_notebook,
            wrap=tk.WORD,
            height=log_text_height,
            state=tk.DISABLED,
            font=("Consolas", 9),
        )
        output_log_notebook.add(
            self.parsed_json_output_text, text="Parsed JSON/Status Output"
        )
        self.app_log_text = scrolledtext.ScrolledText(
            output_log_notebook,
            wrap=tk.WORD,
            height=log_text_height,
            state=tk.DISABLED,
            font=("Consolas", 9),
        )
        output_log_notebook.add(self.app_log_text, text="Application Log")

    def _create_status_bar(self, parent_frame: ttk.Frame):  # (Invariato)
        self.status_bar_widget = ttk.Label(
            parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
        )
        self.status_bar_widget.grid(
            row=3, column=0, columnspan=1, sticky="ew", pady=(5, 0), ipady=2, padx=0
        )

    def _setup_logging_redirect_to_gui(self):  # (Invariato)
        if not hasattr(self, "app_log_text") or not self.app_log_text:
            logger.error("app_log_text not found.")
            return
        self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text)
        formatter = logging.Formatter(
            "%(asctime)s - %(name)-30s - [%(levelname)-7s] %(message)s",
            datefmt="%H:%M:%S",
        )
        self.gui_log_handler.setFormatter(formatter)
        self.gui_log_handler.setLevel(logging.INFO)
        logging.getLogger().addHandler(self.gui_log_handler)
        logger.info("GUI logging handler initialized and added to root logger.")

    def _browse_file(
        self,
        title: str,
        target_var: tk.StringVar,
        filetypes: Optional[List[Tuple[str, Any]]] = None,
    ):  # (Invariato)
        current_path = target_var.get()
        initial_dir = (
            os.path.dirname(current_path)
            if current_path and os.path.exists(os.path.dirname(current_path))
            else None
        )
        path = filedialog.askopenfilename(
            title=title,
            filetypes=filetypes or [("All files", "*.*")],
            initialdir=initial_dir,
            parent=self,
        )
        if path:
            target_var.set(path)
        if target_var == self.exe_path_var:
            self.app_settings.set_setting(
                "general", "last_target_executable_path", path
            )
            logger.info(f"Target executable path updated: {path}")

    def _browse_target_exe(self):
        self._browse_file(
            "Select Target Executable",
            self.exe_path_var,
            [("Executable files", ("*.exe", "*")), ("All files", "*.*")],
        )  # (Invariato)

    def _update_gdb_raw_output(self, text: str, append: bool = True):  # (Invariato)
        if (
            not hasattr(self, "gdb_raw_output_text")
            or not self.gdb_raw_output_text.winfo_exists()
        ):
            return
        self.gdb_raw_output_text.config(state=tk.NORMAL)
        if append:
            self.gdb_raw_output_text.insert(tk.END, str(text) + "\n")
        else:
            self.gdb_raw_output_text.delete("1.0", tk.END)
            self.gdb_raw_output_text.insert("1.0", str(text))
        self.gdb_raw_output_text.see(tk.END)
        self.gdb_raw_output_text.config(state=tk.DISABLED)

    def _update_parsed_json_output(self, data_to_display: Any):  # (Invariato)
        if (
            not hasattr(self, "parsed_json_output_text")
            or not self.parsed_json_output_text.winfo_exists()
        ):
            return
        self.parsed_json_output_text.config(state=tk.NORMAL)
        self.parsed_json_output_text.delete("1.0", tk.END)
        if data_to_display is None:
            self.parsed_json_output_text.insert("1.0", "<No data to display>")
        elif isinstance(data_to_display, dict):
            if "status" in data_to_display and (
                "filepath_written" in data_to_display
                or "message" in data_to_display
                or "variable_dumped" in data_to_display
            ):
                status_text = f"Status: {data_to_display.get('status', 'N/A')}\nVar: {data_to_display.get('variable_dumped', 'N/A')}\nFile Written: {data_to_display.get('filepath_written', 'N/A')}\nFmt Req: {data_to_display.get('target_format_requested', 'N/A')}\nMsg: {data_to_display.get('message', 'N/A')}\n"
                if data_to_display.get("details"):
                    status_text += f"Details: {data_to_display.get('details')}\n"
                if data_to_display.get("raw_gdb_output"):
                    status_text += f"Raw GDB Output Snippet: {str(data_to_display.get('raw_gdb_output'))[:200]}...\n"
                self.parsed_json_output_text.insert("1.0", status_text)
            else:
                try:
                    self.parsed_json_output_text.insert(
                        "1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False)
                    )
                except Exception as e:
                    self.parsed_json_output_text.insert(
                        "1.0", f"Err JSON dict: {e}\nRaw: {str(data_to_display)}"
                    )
        elif isinstance(data_to_display, list):
            try:
                self.parsed_json_output_text.insert(
                    "1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False)
                )
            except Exception as e:
                self.parsed_json_output_text.insert(
                    "1.0", f"Err JSON list: {e}\nRaw: {str(data_to_display)}"
                )
        else:
            self.parsed_json_output_text.insert("1.0", str(data_to_display))
        self.parsed_json_output_text.see("1.0")
        self.parsed_json_output_text.config(state=tk.DISABLED)

    def _update_status_bar(self, message: str, is_error: bool = False):  # (Invariato)
        if hasattr(self, "status_var") and self.status_var is not None:
            self.status_var.set(message)

    def _handle_gdb_operation_error(
        self, op_name: str, err_details: Any
    ):  # (Invariato)
        msg = f"Error GDB op '{op_name}': {err_details}"
        logger.error(msg, exc_info=isinstance(err_details, Exception))
        self._update_gdb_raw_output(f"GDB_OPERATION_ERROR: {msg}\n", True)
        self._update_status_bar(f"Error: GDB op '{op_name}' failed.", True)
        if self.winfo_exists():
            messagebox.showerror("GDB Operation Error", msg, parent=self)

    def _start_gdb_session_action(self):
        if self.profile_executor_instance and self.profile_executor_instance.is_running:
            messagebox.showwarning(
                "Profile Running", "Profile running. Stop first.", parent=self
            )
            return

        gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
        target_exe = self.exe_path_var.get()
        gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path")

        if not gdb_exe or not os.path.isfile(gdb_exe):
            messagebox.showerror("Config Error", "GDB path incorrect.", parent=self)
            self._check_critical_configs_and_update_gui()
            return
        if not target_exe or not os.path.exists(target_exe):
            messagebox.showerror(
                "Input Error", "Target exe required/not found.", parent=self
            )
            return

        dumper_opts = self.app_settings.get_category_settings("dumper_options", {})

        if self.gdb_session and self.gdb_session.is_alive():
            messagebox.showwarning(
                "Session Active", "GDB active. Stop first.", parent=self
            )
            return

        self._update_status_bar("Starting GDB...")
        self._update_gdb_raw_output("Starting GDB session...\n", False)
        self._update_parsed_json_output(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", 30)
        )
        quit_timeout_no_sym = self.app_settings.get_setting(
            "timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10)
        )

        try:
            self.gdb_session = GDBSession(
                gdb_path=gdb_exe,
                executable_path=target_exe,
                gdb_script_full_path=gdb_script,
                dumper_options=dumper_opts,
                app_log_directory_path=self.log_directory_path,
            )
            self.gdb_session.start(timeout=startup_timeout)
            self._update_gdb_raw_output(
                f"GDB started for '{os.path.basename(target_exe)}'.\n"
            )

            if not self.gdb_session.symbols_found:
                self._update_gdb_raw_output(
                    "ERROR: No debug symbols. Session terminated.\n", True
                )
                if self.winfo_exists():
                    messagebox.showwarning(
                        "No Debug Symbols",
                        f"No symbols in '{os.path.basename(target_exe)}'. Aborted.",
                        parent=self,
                    )
                self._update_status_bar("GDB aborted: No symbols.", True)
                if self.gdb_session.is_alive():
                    self.gdb_session.quit(timeout=quit_timeout_no_sym)
                self.gdb_session = None
                self._reset_gui_to_stopped_state()
                self._check_critical_configs_and_update_gui()
                return

            # --- BLOCCO MODIFICATO ---
            if gdb_script and os.path.isfile(gdb_script):
                if self.gdb_session.gdb_script_sourced_successfully:
                    self._update_gdb_raw_output(
                        f"Dumper '{os.path.basename(gdb_script)}' sourced.\n", True
                    )
                    self._update_status_bar(
                        f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded."
                    )
                    self.gdb_dumper_status_var.set(
                        f"Dumper: {os.path.basename(gdb_script)} (Loaded)"
                    )
                else: # gdb_script_sourced_successfully è False
                    self._update_gdb_raw_output(
                        f"Warn: Dumper '{os.path.basename(gdb_script)}' FAILED load.\n",
                        True,
                    )
                    self._update_status_bar(
                        f"GDB active. Dumper load issue.", is_error=True
                    )
                    # Sposta il messagebox e l'aggiornamento dello status var QUI DENTRO L'ELSE
                    if self.winfo_exists():
                        messagebox.showwarning(
                            "Dumper Issue",
                            f"Dumper '{os.path.basename(gdb_script)}' failed load.",
                            parent=self,
                        )
                    self.gdb_dumper_status_var.set(
                        f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)"
                    )
            # --- FINE BLOCCO MODIFICATO ---
            elif gdb_script: # gdb_script specificato ma non è un file valido
                self._update_gdb_raw_output(
                    f"Warn: Dumper path '{gdb_script}' invalid.\n", True
                )
                self._update_status_bar(
                    f"GDB active. Dumper path invalid.", is_error=True
                )
                self.gdb_dumper_status_var.set(
                    f"Dumper: {os.path.basename(gdb_script) if gdb_script else 'N/A'} (Path Invalid!)"
                )
            else: # Nessun gdb_script specificato
                self._update_gdb_raw_output(
                    "No dumper script configured. JSON dump via 'dump_json' command unavailable.\n", True
                )
                self._update_status_bar("GDB active. No dumper script.")
                self.gdb_dumper_status_var.set("Dumper: Not Configured.")

            self.start_gdb_button.config(state=tk.DISABLED)
            self.set_bp_button.config(state=tk.NORMAL)
            self.run_button.config(state=tk.DISABLED, text="3. Run Program")
            self.dump_var_button.config(state=tk.DISABLED)
            self.stop_gdb_button.config(state=tk.NORMAL)

            if hasattr(self, "run_profile_button"):
                self.run_profile_button.config(state=tk.DISABLED) # type: ignore
            if hasattr(self, "profile_selection_combo"):
                self.profile_selection_combo.config(state=tk.DISABLED) # type: ignore

            self.program_started_once = False
            self.last_manual_gdb_dump_filepath = None
            self._disable_save_buttons()

        except (FileNotFoundError, ConnectionError, TimeoutError) as e:
            self._handle_gdb_operation_error("start GDB session", e)
            self.gdb_session = None
            self._reset_gui_to_stopped_state()
            self._check_critical_configs_and_update_gui()
        except Exception as e:
            logger.critical(
                f"MAIN CATCH start GDB: {type(e).__name__}: '{e}'", exc_info=True
            )
            self._handle_gdb_operation_error("start (unexpected)", e)
            self.gdb_session = None
            self._reset_gui_to_stopped_state()
            self._check_critical_configs_and_update_gui()

    def _set_gdb_breakpoint_action(self):
        # (Come prima, usa i default corretti per i timeout)
        if not self.gdb_session or not self.gdb_session.is_alive():
            messagebox.showerror("Error", "GDB not active.", parent=self)
            return
        bp_loc = self.breakpoint_var.get().strip()
        if not bp_loc:
            messagebox.showerror("Input Error", "BP location empty.", parent=self)
            return
        self._update_status_bar(f"Setting BP at '{bp_loc}'...")
        try:
            default_timeouts = self.app_settings._get_default_settings().get(
                "timeouts", {}
            )
            cmd_timeout = self.app_settings.get_setting(
                "timeouts", "gdb_command", default_timeouts.get("gdb_command", 30)
            )
            output = self.gdb_session.set_breakpoint(bp_loc, timeout=cmd_timeout)
            self._update_gdb_raw_output(output, True)
            bp_disp = bp_loc[:20] + "..." if len(bp_loc) > 20 else bp_loc
            if (
                "Breakpoint" in output
                and "not defined" not in output.lower()
                and "pending" not in output.lower()
            ):
                self.set_bp_button.config(text=f"BP: {bp_disp} (Set)")
                self.run_button.config(state=tk.NORMAL)
                self._update_status_bar(f"BP set at '{bp_loc}'.")
            elif "pending" in output.lower():
                self.set_bp_button.config(text=f"BP: {bp_disp} (Pend)")
                self.run_button.config(state=tk.NORMAL)
                self._update_status_bar(f"BP '{bp_loc}' pending.")
                messagebox.showinfo(
                    "BP Pending", f"BP '{bp_loc}' pending.", parent=self
                )
            else:
                self._update_status_bar(f"Issue setting BP '{bp_loc}'.", True)
                if self.winfo_exists():
                    messagebox.showwarning(
                        "BP Error",
                        f"Failed to set BP at '{bp_loc}'. Check GDB Output.",
                        parent=self,
                    )
        except (ConnectionError, TimeoutError) as e:
            self._handle_gdb_operation_error(f"set BP '{bp_loc}'", e)
        except Exception as e:
            self._handle_gdb_operation_error(f"set BP '{bp_loc}' (unexpected)", e)

    def _run_or_continue_gdb_action(self):
        # (Come prima, usa i default corretti per i timeout)
        if not self.gdb_session or not self.gdb_session.is_alive():
            messagebox.showerror("Error", "GDB not active.", parent=self)
            return
        self._update_parsed_json_output(None)
        self._disable_save_buttons()
        try:
            output = ""
            default_timeouts = self.app_settings._get_default_settings().get(
                "timeouts", {}
            )
            run_timeout = self.app_settings.get_setting(
                "timeouts",
                "program_run_continue",
                default_timeouts.get("program_run_continue", 120),
            )
            dumper_ok = self.gdb_session.gdb_script_sourced_successfully
            if not self.program_started_once:
                params = self.params_var.get()
                self._update_status_bar(f"Running with params: '{params}'...")
                self._update_gdb_raw_output(f"run {params}\n", True)
                output = self.gdb_session.run_program(params, timeout=run_timeout)
            else:
                self._update_status_bar("Continuing...")
                self._update_gdb_raw_output("continue\n", True)
                output = self.gdb_session.continue_execution(timeout=run_timeout)
            self._update_gdb_raw_output(output, True)
            dump_btn_state = tk.NORMAL if dumper_ok else tk.DISABLED
            if "Breakpoint" in output or re.search(
                r"Hit Breakpoint \d+", output, re.IGNORECASE
            ):
                self._update_status_bar("BP hit. Ready to dump.")
                self.dump_var_button.config(state=dump_btn_state)
                self.program_started_once = True
                self.run_button.config(text="3. Continue")
            elif "Program exited normally" in output or "exited with code" in output:
                self._update_status_bar("Program exited.")
                self.dump_var_button.config(state=tk.DISABLED)
                self.run_button.config(text="3. Run (Restart)")
                self.program_started_once = False
            elif (
                "received signal" in output.lower()
                or "segmentation fault" in output.lower()
            ):
                self._update_status_bar("Program signal/crash.", True)
                self.dump_var_button.config(state=dump_btn_state)
                self.program_started_once = True
                self.run_button.config(text="3. Continue (Risky)")
            else:
                self._update_status_bar("Program running/unknown.")
                self.dump_var_button.config(state=dump_btn_state)
                self.program_started_once = True
                self.run_button.config(text="3. Continue")
        except (ConnectionError, TimeoutError) as e:
            self._handle_gdb_operation_error("run/continue", e)
        except Exception as e:
            self._handle_gdb_operation_error("run/continue (unexpected)", e)

    def _dump_gdb_variable_action(self):
        # (Come prima, usa i default corretti per i timeout e sanitize_filename_component importato)
        if not self.gdb_session or not self.gdb_session.is_alive():
            messagebox.showerror("Error", "GDB session not active.", parent=self)
            return
        if not self.gdb_session.gdb_script_sourced_successfully:
            messagebox.showwarning(
                "Dumper Script Error",
                "GDB dumper script not loaded. JSON dump unavailable.",
                parent=self,
            )
            self._check_critical_configs_and_update_gui()
            return
        var_expr = self.variable_var.get().strip()
        if not var_expr:
            messagebox.showerror(
                "Input Error", "Variable/Expression to dump empty.", parent=self
            )
            return
        if not self.manual_dumps_output_path or not os.path.isdir(
            self.manual_dumps_output_path
        ):
            messagebox.showerror(
                "Directory Error", "Manual dumps output directory not set.", parent=self
            )
            self._prepare_manual_dumps_directory()
        if not self.manual_dumps_output_path or not os.path.isdir(
            self.manual_dumps_output_path
        ):
            return
        self._update_status_bar(f"Dumping '{var_expr}' to file...")
        self._update_gdb_raw_output(f"Attempting file dump of: {var_expr}\n", True)
        self.last_manual_gdb_dump_filepath = None
        self._disable_save_buttons()
        timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
        app_name_manual = sanitize_filename_component(
            os.path.basename(
                self.exe_path_var.get() if self.exe_path_var.get() else "manual_app"
            )
        )
        breakpoint_manual = (
            sanitize_filename_component(self.breakpoint_var.get())
            if self.breakpoint_var.get()
            else "manual_bp_not_set"
        )
        variable_manual = sanitize_filename_component(var_expr)
        gdb_dump_ext_name_part = GDB_DUMP_EXTENSION.lstrip(".")
        manual_placeholders = {
            "{profile_name}": MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER,
            "{app_name}": app_name_manual,
            "{breakpoint}": breakpoint_manual,
            "{variable}": variable_manual,
            "{timestamp}": timestamp_str,
            "{extension}": gdb_dump_ext_name_part,
        }
        gdb_target_dump_filename = MANUAL_DUMP_FILENAME_PATTERN
        for ph, val in manual_placeholders.items():
            gdb_target_dump_filename = gdb_target_dump_filename.replace(ph, val)
        if not gdb_target_dump_filename.endswith(GDB_DUMP_EXTENSION):
            base_n, _ = os.path.splitext(gdb_target_dump_filename)
            gdb_target_dump_filename = base_n + GDB_DUMP_EXTENSION
        gdb_target_dump_filepath = os.path.join(
            self.manual_dumps_output_path, gdb_target_dump_filename
        )
        try:
            default_timeouts = self.app_settings._get_default_settings().get(
                "timeouts", {}
            )
            dump_timeout = self.app_settings.get_setting(
                "timeouts", "dump_variable", default_timeouts.get("dump_variable", 60)
            )
            status_payload = self.gdb_session.dump_variable_to_json(
                var_expr,
                timeout=dump_timeout,
                target_output_filepath=gdb_target_dump_filepath,
                target_output_format="json",
            )
            self._update_parsed_json_output(status_payload)
            if status_payload.get("status") == "success":
                filepath_written = status_payload.get("filepath_written")
                if filepath_written and os.path.exists(filepath_written):
                    self.last_manual_gdb_dump_filepath = filepath_written
                    self._update_status_bar(
                        f"Dumped '{var_expr}' to temp: {os.path.basename(filepath_written)}."
                    )
                    self._enable_save_buttons_if_data()
                else:
                    self._update_status_bar(
                        f"Dumper success, but temp file for '{var_expr}' missing.", True
                    )
            else:
                error_msg = status_payload.get(
                    "details", status_payload.get("message", "Unknown dumper error")
                )
                self._update_status_bar(
                    f"Error dumping '{var_expr}': {error_msg}", True
                )
        except (ConnectionError, TimeoutError) as e:
            self._handle_gdb_operation_error(f"dump var '{var_expr}'", e)
        except Exception as e:
            self._handle_gdb_operation_error(f"dump var '{var_expr}' (unexpected)", e)

    def _stop_gdb_session_action(self):
        # (Come prima, usa i default corretti per i timeout)
        if self.gdb_session and self.gdb_session.is_alive():
            self._update_status_bar("Stopping GDB session...")
            try:
                default_timeouts = self.app_settings._get_default_settings().get(
                    "timeouts", {}
                )
                kill_timeout = self.app_settings.get_setting(
                    "timeouts", "kill_program", default_timeouts.get("kill_program", 20)
                )
                quit_timeout = self.app_settings.get_setting(
                    "timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10)
                )
                if self.program_started_once:
                    kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
                    self._update_gdb_raw_output(f"Kill output:\n{kill_output}\n", True)
                self.gdb_session.quit(timeout=quit_timeout)
                self._update_gdb_raw_output("GDB quit sent.\n", True)
            except Exception as e:
                self._handle_gdb_operation_error("stop GDB session", e)
            finally:
                self.gdb_session = None
                self._reset_gui_to_stopped_state()
                self._load_and_populate_profiles_for_automation_tab()
        else:
            self._reset_gui_to_stopped_state()
            self._load_and_populate_profiles_for_automation_tab()

    def _reset_gui_to_stopped_state(self):  # (Invariato)
        gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
        gdb_is_ok = bool(gdb_exe_path and os.path.isfile(gdb_exe_path))
        is_prof_running = (
            self.profile_executor_instance and self.profile_executor_instance.is_running
        )
        if hasattr(self, "start_gdb_button"):
            self.start_gdb_button.config(
                state=tk.NORMAL if gdb_is_ok and not is_prof_running else tk.DISABLED
            )
        if hasattr(self, "set_bp_button"):
            self.set_bp_button.config(state=tk.DISABLED, text="2. Set BP")
        if hasattr(self, "run_button"):
            self.run_button.config(state=tk.DISABLED, text="3. Run Program")
        if hasattr(self, "dump_var_button"):
            self.dump_var_button.config(state=tk.DISABLED)
        if hasattr(self, "stop_gdb_button"):
            self.stop_gdb_button.config(state=tk.DISABLED)
        if hasattr(self, "save_json_button"):
            self._disable_save_buttons()
        if hasattr(self, "run_profile_button") and hasattr(
            self, "profile_selection_combo"
        ):
            can_run_profile = (
                gdb_is_ok
                and not is_prof_running
                and bool(self.profile_selection_combo.get())
            )
            self.run_profile_button.config(
                state=tk.NORMAL if can_run_profile else tk.DISABLED
            )
            self.profile_selection_combo.config(state="readonly" if not is_prof_running else tk.DISABLED)  # type: ignore
        self.program_started_once = False
        self.last_manual_gdb_dump_filepath = None
        if (
            not is_prof_running
            and hasattr(self, "status_var")
            and self.status_var is not None
        ):
            self._update_status_bar("GDB session stopped or not active.")

    def _enable_save_buttons_if_data(self):  # (Invariato)
        can_save = bool(
            self.last_manual_gdb_dump_filepath
            and os.path.exists(self.last_manual_gdb_dump_filepath)
        )
        if hasattr(self, "save_json_button"):
            self.save_json_button.config(state=tk.NORMAL if can_save else tk.DISABLED)
        if hasattr(self, "save_csv_button"):
            self.save_csv_button.config(state=tk.NORMAL if can_save else tk.DISABLED)

    def _disable_save_buttons(self):  # (Invariato)
        if hasattr(self, "save_json_button"):
            self.save_json_button.config(state=tk.DISABLED)
        if hasattr(self, "save_csv_button"):
            self.save_csv_button.config(state=tk.DISABLED)

    def _save_dumped_data(self, final_format_type: str):  # (Invariato)
        if not self.last_manual_gdb_dump_filepath or not os.path.exists(
            self.last_manual_gdb_dump_filepath
        ):
            messagebox.showwarning(
                "No Data", "No temp dump file to save from.", parent=self
            )
            return
        file_ext = f".{final_format_type.lower()}"
        file_types = [
            (f"{final_format_type.upper()} files", f"*{file_ext}"),
            ("All files", "*.*"),
        ]
        var_sugg = (
            sanitize_filename_component(self.variable_var.get())
            if self.variable_var.get()
            else "manual_dump_var"
        )
        timestamp_from_temp_file = "data"
        try:
            temp_basename = os.path.basename(self.last_manual_gdb_dump_filepath)
            match_ts = re.search(r"(\d{8}_\d{6}_\d{3})", temp_basename)

            if match_ts:
                timestamp_from_temp_file = match_ts.group(1)
        except Exception:
            pass
        default_fname_base = f"{var_sugg}_{timestamp_from_temp_file}"
        final_save_filepath = filedialog.asksaveasfilename(
            defaultextension=file_ext,
            filetypes=file_types,
            title=f"Save Manual Dump as {final_format_type.upper()}",
            initialfile=f"{default_fname_base}{file_ext}",
            parent=self,
        )
        if not final_save_filepath:
            return
        self._update_status_bar(
            f"Saving data as {final_format_type.upper()} to {os.path.basename(final_save_filepath)}..."
        )
        try:
            with open(
                self.last_manual_gdb_dump_filepath, "r", encoding="utf-8"
            ) as f_temp:
                data_from_gdb_dump = json.load(f_temp)
            if final_format_type == "json":
                save_to_json(data_from_gdb_dump, final_save_filepath)
            elif final_format_type == "csv":
                data_csv = data_from_gdb_dump
                if isinstance(data_csv, dict) and not isinstance(data_csv, list):
                    data_csv = [data_csv]
                elif not isinstance(data_csv, list):
                    data_csv = [{"value": data_csv}]
                elif (
                    isinstance(data_csv, list)
                    and data_csv
                    and not all(isinstance(i, dict) for i in data_csv)
                ):
                    data_csv = [{"value": i} for i in data_csv]
                save_to_csv(data_csv, final_save_filepath)
            messagebox.showinfo(
                "Save Successful", f"Data saved to:\n{final_save_filepath}", parent=self
            )
            self._update_status_bar(
                f"Data saved to {os.path.basename(final_save_filepath)}."
            )
            try:
                os.remove(self.last_manual_gdb_dump_filepath)
                logger.info(
                    f"Deleted temp manual dump: {self.last_manual_gdb_dump_filepath}"
                )
                self.last_manual_gdb_dump_filepath = None
                self._disable_save_buttons()
            except Exception as e_del:
                logger.error(
                    f"Could not delete temp manual dump '{self.last_manual_gdb_dump_filepath}': {e_del}"
                )
        except Exception as e:
            logger.error(f"Error saving manual dump: {e}", exc_info=True)
            messagebox.showerror("Save Error", f"Failed to save: {e}", parent=self)
            self._update_status_bar(f"Error saving.", True)

    def _gui_status_update(self, message: str) -> None:  # (Invariato)
        if (
            hasattr(self, "profile_exec_status_var")
            and self.profile_exec_status_var is not None
        ):
            self.profile_exec_status_var.set(message)
        logger.info(f"ProfileExecutor Status Update (via callback): {message}")

    def _gui_gdb_output_update(self, message: str) -> None:
        self._update_gdb_raw_output(message, append=True)  # (Invariato)

    def _gui_json_data_update(self, data: Any) -> None:
        self._update_parsed_json_output(data)  # (Invariato)

    def _gui_add_execution_log_entry(
        self, entry: ExecutionLogEntry
    ) -> None:  # (Invariato)
        if self.produced_files_tree and self.winfo_exists():
            try:
                values = (
                    entry.get("timestamp", ""),
                    entry.get("breakpoint_spec", "N/A"),
                    entry.get("variable", "N/A"),
                    entry.get("file_produced", "N/A"),
                    entry.get("status", "N/A"),
                    entry.get("details", ""),
                )
                item_id = self.produced_files_tree.insert("", tk.END, values=values)
                self.produced_files_tree.see(item_id)
            except Exception as e:
                logger.error(f"Failed add to tree: {e}. Entry: {entry}")

    def _clear_produced_files_tree(self) -> None:  # (Invariato)
        if self.produced_files_tree:
            for item in self.produced_files_tree.get_children():
                self.produced_files_tree.delete(item)

    def _run_selected_profile_action(self) -> None:
        # (Come prima, ma con il passaggio di log_directory_path a ProfileExecutor)
        selected_profile_name = self.profile_selection_combo.get()
        if not selected_profile_name:
            messagebox.showwarning(
                "No Profile Selected", "Please select a profile to run.", parent=self
            )
            return
        if self.profile_executor_instance and self.profile_executor_instance.is_running:
            messagebox.showwarning(
                "Profile Already Running",
                "A profile is already in execution.",
                parent=self,
            )
            return
        if self.gdb_session and self.gdb_session.is_alive():
            messagebox.showerror(
                "GDB Session Active",
                "A manual GDB session is active. Stop it first.",
                parent=self,
            )
            return

        profile_data_to_run = self.available_profiles_map.get(selected_profile_name)
        if not profile_data_to_run:
            messagebox.showerror(
                "Profile Error",
                f"Could not retrieve data for '{selected_profile_name}'.",
                parent=self,
            )
            return

        self.profile_exec_status_var.set(
            f"STARTING PROFILE '{selected_profile_name}'..."
        )
        if self.profile_progressbar and hasattr(
            self.profile_exec_status_label_big, "master"
        ):
            parent_frame_for_pb = self.profile_exec_status_label_big.master  # type: ignore
            self.profile_progressbar.grid(
                row=1,
                column=0,
                columnspan=1,
                sticky="ew",
                padx=5,
                pady=(2, 5),
                in_=parent_frame_for_pb,
            )
            self.profile_progressbar.start(15)

        self.run_profile_button.config(state=tk.DISABLED)
        self.stop_profile_button.config(state=tk.NORMAL)
        self.profile_selection_combo.config(state=tk.DISABLED)
        try:
            if self.menubar.winfo_exists():
                self.menubar.entryconfig("Profiles", state=tk.DISABLED)
                self.menubar.entryconfig("Options", state=tk.DISABLED)
        except tk.TclError:
            logger.warning("TclError disabling menubar.")
        self.start_gdb_button.config(state=tk.DISABLED)
        self.set_bp_button.config(state=tk.DISABLED)
        self.run_button.config(state=tk.DISABLED)
        self.dump_var_button.config(state=tk.DISABLED)
        self.stop_gdb_button.config(state=tk.DISABLED)
        self.last_run_output_path = None
        self.open_output_folder_button.config(state=tk.DISABLED)  # type: ignore

        # --- MODIFICA: Passa self.log_directory_path ---
        self.profile_executor_instance = ProfileExecutor(
            profile_data_to_run,
            self.app_settings,
            app_log_directory_path=self.log_directory_path,
            status_update_callback=self._gui_status_update,
            gdb_output_callback=self._gui_gdb_output_update,
            json_output_callback=self._gui_json_data_update,
            execution_log_callback=self._gui_add_execution_log_entry,
        )
        # --- FINE MODIFICA ---

        self._clear_produced_files_tree()
        self._update_gdb_raw_output("", False)
        self._update_parsed_json_output(None)
        executor_thread = threading.Thread(
            target=self._profile_executor_thread_target, daemon=True
        )
        executor_thread.start()

    def _profile_executor_thread_target(self):  # (Invariato)
        if self.profile_executor_instance:
            try:
                self.profile_executor_instance.run()
            finally:
                if self.winfo_exists():
                    self.after(0, self._on_profile_execution_finished)

    def _on_profile_execution_finished(self):  # (Invariato)
        if not self.winfo_exists():
            logger.warning("Profile exec finished callback, but window gone.")
            return
        if self.profile_progressbar:
            self.profile_progressbar.stop()
            self.profile_progressbar.grid_remove()
        final_status_message = (
            "Profile execution finished (status unknown if executor missing)."
        )
        if self.profile_executor_instance:
            if hasattr(self.profile_executor_instance, "current_run_output_path"):
                self.last_run_output_path = (
                    self.profile_executor_instance.current_run_output_path
                )
            if (
                self.last_run_output_path
                and os.path.isdir(self.last_run_output_path)
                and hasattr(self, "open_output_folder_button")
            ):
                self.open_output_folder_button.config(state=tk.NORMAL)  # type: ignore
            else:
                if hasattr(self, "open_output_folder_button"):
                    self.open_output_folder_button.config(state=tk.DISABLED)  # type: ignore
                logger.warning(
                    f"Profile output folder invalid: {self.last_run_output_path}"
                )
            current_gui_status = self.profile_exec_status_var.get()
            if (
                "STARTING PROFILE" in current_gui_status
                or "Requesting profile stop" in current_gui_status
                or "Status:" in current_gui_status
            ):
                if hasattr(self.profile_executor_instance, "profile_execution_summary"):
                    exec_final_status = (
                        self.profile_executor_instance.profile_execution_summary.get(
                            "status", "Unknown status from executor"
                        )
                    )
                    if (
                        "Error" in exec_final_status
                        or "failed" in exec_final_status.lower()
                        or "Interrupted" in exec_final_status
                    ):
                        final_status_message = (
                            f"Profile finished with issues: {exec_final_status}"
                        )
                    elif exec_final_status not in [
                        "Initialized",
                        "Pending",
                        "Processing Dumps",
                    ]:
                        final_status_message = (
                            f"Profile run completed. State: {exec_final_status}"
                        )
            elif (
                "Error:" in current_gui_status or "failed" in current_gui_status.lower()
            ):
                final_status_message = (
                    f"Profile finished with issues: {current_gui_status}"
                )
            else:
                final_status_message = (
                    f"Profile run completed. Last status: {current_gui_status}"
                )
        self.profile_exec_status_var.set(final_status_message)
        gdb_is_ok = self.app_settings.get_setting(
            "general", "gdb_executable_path"
        ) and os.path.isfile(
            self.app_settings.get_setting("general", "gdb_executable_path")
        )
        if hasattr(self, "profile_selection_combo") and self.profile_selection_combo.get():  # type: ignore
            if hasattr(self, "run_profile_button") and gdb_is_ok:
                self.run_profile_button.config(state=tk.NORMAL)  # type: ignore
            else:
                if hasattr(self, "run_profile_button"):
                    self.run_profile_button.config(state=tk.DISABLED)  # type: ignore
        else:
            if hasattr(self, "run_profile_button"):
                self.run_profile_button.config(state=tk.DISABLED)  # type: ignore
        if hasattr(self, "stop_profile_button"):
            self.stop_profile_button.config(state=tk.DISABLED)  # type: ignore
        if hasattr(self, "profile_selection_combo"):
            self.profile_selection_combo.config(state="readonly")  # type: ignore
        try:
            if self.menubar.winfo_exists():
                self.menubar.entryconfig("Profiles", state=tk.NORMAL)
                self.menubar.entryconfig("Options", state=tk.NORMAL)
        except tk.TclError as e_menu_enable:
            logger.warning(f"TclError re-enabling menubar: {e_menu_enable}")
        self._check_critical_configs_and_update_gui()
        self.profile_executor_instance = None
        logger.info("Profile execution GUI updates completed.")

    def _stop_current_profile_action(self) -> None:  # (Invariato)
        if self.profile_executor_instance and self.profile_executor_instance.is_running:
            self.profile_exec_status_var.set("Requesting profile stop...")
            self.profile_executor_instance.request_stop()
        if hasattr(self, "stop_profile_button"):
            self.stop_profile_button.config(state=tk.DISABLED)
        else:
            self.profile_exec_status_var.set("No profile running to stop.")

    def _open_last_run_output_folder(self) -> None:
        self._open_folder_path(
            self.last_run_output_path, "Profile Output Folder"
        )  # (Invariato)

    def _open_manual_dumps_folder(self) -> None:
        self._open_folder_path(
            self.manual_dumps_output_path, "Manual Dumps Folder"
        )  # (Invariato)

    def _open_folder_path(
        self, folder_path: Optional[str], folder_desc: str
    ) -> None:  # (Invariato)
        logger.info(f"Attempting to open {folder_desc}: '{folder_path}'")
        if not folder_path or not os.path.isdir(folder_path):
            messagebox.showwarning(
                f"No {folder_desc}",
                f"Path for '{folder_desc}' not set/exist: {folder_path}",
                parent=self,
            )
        else:
            try:
                if sys.platform == "win32":
                    os.startfile(os.path.normpath(folder_path))
                elif sys.platform == "darwin":
                    subprocess.run(["open", folder_path], check=True)
                else:
                    subprocess.run(["xdg-open", folder_path], check=True)
            except Exception as e:
                logger.error(
                    f"Failed to open {folder_desc} at '{folder_path}': {e}",
                    exc_info=True,
                )
                messagebox.showerror(
                    "Error Opening Folder",
                    f"Could not open {folder_desc.lower()}: {folder_path}\nError: {e}",
                    parent=self,
                )

    def _on_closing_window(self):  # (Invariato)
        logger.info("Main window closing sequence initiated.")
        active_profile_stop_requested = False
        if self.profile_executor_instance and self.profile_executor_instance.is_running:
            response = messagebox.askyesnocancel(
                "Profile Running",
                "Profile running. Stop & exit?",
                default=messagebox.CANCEL,
                parent=self,
            )
            if response is True:
                self._stop_current_profile_action()
                active_profile_stop_requested = True
            elif response is None:
                logger.info("User cancelled exit.")
                return
        self.app_settings.set_setting("gui", "main_window_geometry", self.geometry())
        self.app_settings.set_setting(
            "general", "last_target_executable_path", self.exe_path_var.get()
        )
        self.app_settings.set_setting(
            "general", "default_breakpoint", self.breakpoint_var.get()
        )
        self.app_settings.set_setting(
            "general", "default_variable_to_dump", self.variable_var.get()
        )
        self.app_settings.set_setting(
            "general", "default_program_parameters", self.params_var.get()
        )
        if not self.app_settings.save_settings() and self.winfo_exists():
            messagebox.showwarning(
                "Settings Error", "Could not save settings.", parent=self
            )
        should_destroy = True
        if self.gdb_session and self.gdb_session.is_alive():
            if self.winfo_exists() and messagebox.askokcancel(
                "GDB Session Active", "Manual GDB active. Stop & exit?", parent=self
            ):
                self._stop_gdb_session_action()
            elif self.winfo_exists():
                should_destroy = False
                logger.info("User cancelled exit (manual GDB active).")
            elif not self.winfo_exists():
                self._stop_gdb_session_action()
        if should_destroy:
            logger.info("Proceeding with window destruction.")
            if self.gui_log_handler:
                logging.getLogger().removeHandler(self.gui_log_handler)
                self.gui_log_handler.close()
                self.gui_log_handler = None
            if active_profile_stop_requested:
                logger.debug("Main window closing. Profile executor asked to stop.")
            self.destroy()
            logger.info("Main Tkinter window destroyed. App will exit.")
        else:
            logger.info("Window destruction aborted by user.")


class ScrolledTextLogHandler(logging.Handler):
    def __init__(self, text_widget: scrolledtext.ScrolledText):
        super().__init__()
        self.text_widget = text_widget
        self._active = True

    def emit(self, record: logging.LogRecord):
        if (
            not self._active
            or not hasattr(self.text_widget, "winfo_exists")
            or not self.text_widget.winfo_exists()
        ):
            return
        try:
            log_entry = self.format(record)
            self.text_widget.config(state=tk.NORMAL)
            self.text_widget.insert(tk.END, log_entry + "\n")
            self.text_widget.see(tk.END)
            self.text_widget.config(state=tk.DISABLED)
        except tk.TclError:
            self._active = False
            logger.debug("ScrolledTextLogHandler TclError, disabling.")
        except Exception as e:
            self._active = False
            print(f"FATAL ScrolledTextLogHandler ERROR: {e}", file=sys.stderr)


if __name__ == "__main__":
    if not logging.getLogger().hasHandlers():
        logging.basicConfig(
            level=logging.DEBUG,
            format="%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
        )
    logger.info("Running GDBGui directly for testing (main_window.py).")
    # Per testare GDBGui standalone, dobbiamo simulare i percorsi passati da __main__
    test_app_base_path = os.path.dirname(
        os.path.dirname(os.path.abspath(__file__))
    )  # Simula project_root
    test_log_dir = os.path.join(test_app_base_path, "logs_test_gui")
    try:
        os.makedirs(test_log_dir, exist_ok=True)
    except:
        pass
    app = GDBGui(app_base_path=test_app_base_path, log_directory_path=test_log_dir)
    app.mainloop()
