# File: cpp_python_debug/core/gdb_mi_session.py
import pexpect
import logging
import re
import os
import json
import time
import sys
import subprocess
from tkinter import messagebox
from typing import (
    Optional,
    Dict,
    Any,
    List,
    Pattern,
    Tuple,
    Union,
)

# Conditional import for pexpect depending on the platform
if sys.platform == "win32":
    from pexpect.popen_spawn import PopenSpawn
    spawn = PopenSpawn
else:
    spawn = pexpect.spawn

logger = logging.getLogger(__name__)

DEFAULT_MI_OPERATION_TIMEOUT = 30
DEFAULT_MI_LONG_OPERATION_TIMEOUT = 120


def _mi_symbol_sort_key(
    item_dict: Dict[str, Any],
) -> Tuple[str, Union[float, int], str]:
    """Helper function to sort symbol dictionaries consistently."""
    file_val = item_dict.get("file", "Z")
    if not file_val or not isinstance(file_val, str):
        file_val = "Z" # Sort items without file last
    
    line_val = item_dict.get("line")
    line_sort_val: Union[float, int]
    if isinstance(line_val, (int, float)):
        line_sort_val = line_val
    elif isinstance(line_val, str) and line_val.isdigit():
        try:
            line_sort_val = int(line_val)
        except ValueError:
            line_sort_val = float("inf") # Sort unparsable lines last
    else:
        line_sort_val = float("inf")

    name_val = item_dict.get("name", "Z")
    if not name_val or not isinstance(name_val, str):
        name_val = "Z" # Sort items without name last
        
    return (file_val.lower(), line_sort_val, name_val.lower())


class GDBMISession:
    """
    Manages a GDB MI (Machine Interface) subprocess for structured interaction.
    """
    def __init__(
        self,
        gdb_path: str,
        executable_path: str,
        # These are kept for API compatibility but are not used by MI session for symbol analysis
        gdb_script_full_path: Optional[str] = None,
        dumper_options: Optional[Dict[str, Any]] = None,
    ):
        if not os.path.exists(gdb_path):
            msg = f"GDB executable not found: {gdb_path}"
            logger.error(msg)
            raise FileNotFoundError(msg)
        if not executable_path or not os.path.exists(executable_path):
            msg = f"Target executable not found: {executable_path}"
            logger.error(msg)
            raise FileNotFoundError(msg)
            
        self.gdb_path: str = os.path.normpath(gdb_path)
        self.executable_path: str = os.path.normpath(executable_path)
        self.child: Optional[pexpect.spawn] = None
        self.mi_prompt_textual: str = "(gdb) "
        self.symbols_found: bool = False
        self.gdb_banner: Optional[str] = None # Store GDB banner to avoid re-parsing
        logger.info(
            f"GDBMISession initialized. GDB: '{self.gdb_path}', Executable: '{self.executable_path}', Mode: MI"
        )

    def is_alive(self) -> bool:
        """
        Checks if the GDB subprocess is currently running.
        This is a cross-platform safe way to check the status.
        """
        if self.child is None:
            return False
        # The 'closed' attribute is a reliable way to check if the stream is dead.
        return not self.child.closed

    def _force_close_child(self):
        """
        Forces the child process to terminate in a cross-platform manner.
        """
        if not self.is_alive():
            return
            
        logger.warning("Forcing termination of GDB MI child process.")
        try:
            if hasattr(self.child, 'proc'):
                # This is a PopenSpawn object (Windows), use the underlying Popen process
                self.child.proc.terminate()
            else:
                # This is a standard pexpect.spawn object (Linux/macOS)
                self.child.close(force=True)
        except Exception as e:
            logger.error(f"Exception during force close of GDB MI process: {e}", exc_info=True)
        finally:
            self.child = None

    def _strip_mi_console_output(self, output: str) -> str:
        """Strips MI-specific prefixes from console stream output."""
        lines = output.splitlines()
        cleaned_lines = []
        for line in lines:
            s_line = line.strip()
            # Handle console stream output (~"...")
            if s_line.startswith('~"') and s_line.endswith('"'): # Ending can be just "
                content_raw = s_line[2:-1]
                if content_raw.endswith('\\n'):
                    content_raw = content_raw[:-2]
                content = (
                    content_raw
                    .replace("\\\\", "\\")
                    .replace('\\"', '"')
                    .replace("\\t", "\t")
                    .replace("\\r", "\r")
                )
                cleaned_lines.append(content)
            # Handle log stream output (&"...")
            elif s_line.startswith('&"') and s_line.endswith('"'):
                content_raw = s_line[2:-1]
                if content_raw.endswith('\\n'):
                    content_raw = content_raw[:-2]
                content = (
                    content_raw
                    .replace("\\\\", "\\")
                    .replace('\\"', '"')
                    .replace("\\t", "\t")
                    .replace("\\r", "\r")
                )
                cleaned_lines.append(content)
            # Filter out MI result/async/target records and the prompt
            elif not (
                s_line.startswith(("^", "*", "=", "+", "-"))
                or s_line == self.mi_prompt_textual
                or s_line.startswith('@"')
            ):
                if s_line:
                    cleaned_lines.append(line)
        return "\n".join(cleaned_lines)

    def _parse_mi_result(self, raw_output: str) -> Dict[str, Any]:
        """Parses a raw MI output string to extract the primary result record."""
        # Find the main result record (e.g., ^done, ^error)
        mi_record_match = re.search(
            r"^\s*([\^\*\+=])([a-zA-Z0-9_-]+)(?:,(.*))?$",
            raw_output.strip(),
            re.MULTILINE,
        )
        parsed_result: Dict[str, Any] = {
            "result_type": "^unparsed",
            "payload": {},
            "raw_output_head": raw_output[:200],
        }

        if mi_record_match:
            record_prefix = mi_record_match.group(1)
            record_type = mi_record_match.group(2)
            payload_str = mi_record_match.group(3) if mi_record_match.group(3) else ""

            parsed_result["result_type"] = record_prefix + record_type

            if payload_str:
                # Clean up console stream lines from the payload
                payload_str_cleaned = re.sub(
                    r"^[~&@].*?\n", "", payload_str, flags=re.MULTILINE
                ).strip()
                parsed_payload_data = {}
                
                # Regex to find key=value assignments. It handles quoted strings, complex values like lists/tuples, and simple values.
                assignments = re.findall(
                    r'([a-zA-Z0-9_-]+)=("((?:[^"\\]|\\.)*?)"|(\{.*?\s*\}|\[.*?\s*\]|[^,"{}\[\]\s]+))',
                    payload_str_cleaned,
                )
                for (key, _full_value_match, quoted_v_content, complex_or_simple_v) in assignments:
                    value_to_store: Any = (quoted_v_content if quoted_v_content else complex_or_simple_v)
                    # Simple type casting for non-complex values
                    if isinstance(value_to_store, str):
                        if (value_to_store.startswith("{") and value_to_store.endswith("}")) or \
                           (value_to_store.startswith("[") and value_to_store.endswith("]")):
                            # Keep complex structures as strings for now, could be parsed with another json.loads
                            pass
                        elif value_to_store.lower() == "true": value_to_store = True
                        elif value_to_store.lower() == "false": value_to_store = False
                        elif re.match(r"^-?\d+$", value_to_store): # Integer check
                            try: value_to_store = int(value_to_store)
                            except ValueError: pass
                        elif re.match(r"^-?\d*\.\d+$", value_to_store): # Float check
                            try: value_to_store = float(value_to_store)
                            except ValueError: pass
                    parsed_payload_data[key] = value_to_store
                
                if parsed_payload_data:
                    parsed_result["payload"] = parsed_payload_data
                else:
                    parsed_result["payload"] = {"raw_payload_string": payload_str_cleaned}

            logger.debug(f"Parsed MI Result: Type='{parsed_result['result_type']}', Payload Snippet='{str(parsed_result['payload'])[:200]}...'")
        else:
            logger.debug(f"No structured MI result record found in raw output: {raw_output[:200]}...")
            
        return parsed_result

    def send_mi_cmd(
        self,
        command: str,
        timeout: int = DEFAULT_MI_OPERATION_TIMEOUT,
    ) -> str:
        """
        Sends a command to GDB and reads the full response until the next prompt.
        This is a robust method to avoid synchronization issues.
        """
        if not self.is_alive():
            raise ConnectionError("GDB MI session is not active.")
            
        logger.debug(f"Sending GDB MI cmd: '{command}', timeout: {timeout}s")
        self.child.sendline(command.strip())
        
        full_buffer = ""
        while self.is_alive():
            try:
                # We always expect the prompt to terminate the output of a command
                self.child.expect_exact(self.mi_prompt_textual, timeout=timeout)
                # Combine what was before and the matched prompt itself
                full_buffer += str(self.child.before or "") + str(self.child.after or "")
                
                # Check if the buffer contains a result record. If so, command is done.
                # This handles cases where async records might appear before the result.
                if re.search(r"^\^.*?(,|$)", full_buffer, re.MULTILINE):
                    break
                # If only a prompt was found without a result, we might be reading an echo.
                # Continue reading if the command was not a simple no-op.
                # For simplicity, we break here assuming one command = one result.
                # More complex logic could be added for async notifications.
                break

            except pexpect.TIMEOUT:
                error_msg = f"Timeout ({timeout}s) waiting for prompt after MI command: '{command}'."
                logger.error(error_msg + f" Buffer so far: {full_buffer}")
                raise TimeoutError(error_msg)
            except pexpect.EOF:
                error_msg = f"GDB MI session ended (EOF) during command: '{command}'."
                logger.error(error_msg + f" Buffer so far: {full_buffer}")
                raise ConnectionError(error_msg)

        logger.debug(f"GDB MI raw output for '{command}' (len {len(full_buffer)}):\n{full_buffer.strip()[:1000]}...")
        return full_buffer

    def start(self, timeout: int = DEFAULT_MI_OPERATION_TIMEOUT) -> None:
        gdb_dir = os.path.dirname(self.gdb_path)
        cmd_test = [self.gdb_path, "--version"]
        logger.info(f"[SUBPROCESS_TEST_MI] GDB: {cmd_test}, cwd={gdb_dir if os.path.isdir(gdb_dir) else None}")
        try:
            proc = subprocess.Popen(
                cmd_test, cwd=gdb_dir if os.path.isdir(gdb_dir) else None,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
                creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0)
            )
            stdout_data, _ = proc.communicate(timeout=10)
            if proc.returncode != 0:
                raise RuntimeError(f"Subprocess GDB (MI) test failed with return code {proc.returncode}")
            # Capture the banner from the version check
            if stdout_data:
                self.gdb_banner = next((line for line in stdout_data.splitlines() if line.strip().startswith("GNU gdb")), None)
                logger.info(f"Captured GDB banner from pre-check: {self.gdb_banner}")

        except Exception as e:
            logger.error(f"[SUBPROCESS_TEST_MI] Exception: {type(e).__name__} - {e}", exc_info=True)
            raise RuntimeError(f"Subprocess GDB (MI) test error: {e}") from e

        logger.info("[SUBPROCESS_TEST_MI] Completed. Spawning GDB MI with pexpect...")
        cmd_args = [self.gdb_path, "--nx", "--quiet", "--interpreter=mi2", self.executable_path]
        spawn_cwd_val = gdb_dir if os.path.isdir(gdb_dir) else None
        logger.info(f"Spawning GDB MI: {cmd_args}, timeout: {timeout}s, cwd: {spawn_cwd_val or 'Default'}")
        
        try:
            if sys.platform == "win32":
                command_str = subprocess.list2cmdline(cmd_args)
                self.child = spawn(command_str, timeout=timeout, encoding='utf-8', cwd=spawn_cwd_val)
            else:
                self.child = spawn(cmd_args[0], args=cmd_args[1:], timeout=timeout, encoding='utf-8', cwd=spawn_cwd_val)

            self.child.expect_exact(self.mi_prompt_textual, timeout=timeout)
            startup_out = str(self.child.before or "") + str(self.child.after or "")
            logger.debug(f"Raw GDB MI startup output (incl. prompt):\n{startup_out.strip()[:1000]}...")
            
            clean_startup_text = self._strip_mi_console_output(startup_out)
            if "No debugging symbols found" in clean_startup_text:
                self.symbols_found = False
                logger.warning("'No debugging symbols found' message detected in MI startup output.")
            else:
                self.symbols_found = True
                logger.info("Debugging symbols appear to be loaded (MI).")
            logger.info("GDB MI session started, initial prompt received.")
            
            # Disable pagination to ensure full output for info commands
            pg_cmd = "-gdb-set pagination off"
            logger.info(f"Disabling GDB pagination via MI: '{pg_cmd}'")
            self.send_mi_cmd(pg_cmd, timeout=max(5, timeout // 2))
            logger.info("GDB MI pagination command sent and confirmed.")
        except pexpect.TIMEOUT as eto:
            err_det = f"Timeout ({timeout}s) occurred during GDB MI session startup with pexpect."
            logger.error(err_det, exc_info=False) 
            dbg_out = str(eto.get_trace())
            logger.error(f"GDB MI pexpect startup timeout details: {dbg_out.strip()}")
            self._force_close_child()
            raise TimeoutError(err_det) from eto
        except Exception as e_start:
            logger.error(f"An unexpected error occurred in GDBMISession.start(): {type(e_start).__name__}: {e_start}", exc_info=True)
            self._force_close_child()
            raise

    def get_gdb_version(self, timeout: int = DEFAULT_MI_OPERATION_TIMEOUT) -> Optional[str]:
        # Return the banner captured during start, it's more reliable
        if self.gdb_banner:
            return self.gdb_banner
            
        # Fallback if banner wasn't captured
        if not self.is_alive():
            logger.error("Cannot get GDB version: GDB MI session not active and no banner cached.")
            return None
        
        # This command is not ideal for getting the version string, but as a fallback:
        cmd = "-gdb-version"
        logger.info(f"Requesting GDB version via MI command (fallback): '{cmd}'")
        try:
            raw_output = self.send_mi_cmd(cmd, timeout=timeout)
            # The version info is usually in the console stream output
            text_output = self._strip_mi_console_output(raw_output)
            first_line = next((line.strip() for line in text_output.splitlines() if line.strip()), None)
            if first_line:
                logger.info(f"GDB version parsed from MI console stream: {first_line}")
                return first_line
            return None
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Error getting GDB version via MI: {e}", exc_info=True)
            return None

    def list_functions(
        self,
        regex_filter: Optional[str] = None,
        timeout: int = DEFAULT_MI_LONG_OPERATION_TIMEOUT,
    ) -> List[Dict[str, Any]]:
        if not self.is_alive():
            logger.error("Cannot list functions: GDB MI session not active.")
            return []
            
        cli_cmd = "info functions"
        if regex_filter:
            cli_cmd += f" {regex_filter.strip()}"
            
        mi_cmd = f'-interpreter-exec console "{cli_cmd}"'
        logger.info(f"Requesting GDB function list via MI command: '{mi_cmd}'")
        functions: List[Dict[str, Any]] = []
        try:
            raw_output = self.send_mi_cmd(mi_cmd, timeout=timeout)
            text_output = self._strip_mi_console_output(raw_output)
            
            current_file: Optional[str] = None
            in_non_debug_symbols_section = False
            
            debug_symbol_pattern = re.compile(
                r"^(?:(\d+):\s+)?(.*?)\s*\(([^)]*)\);?$", re.IGNORECASE
            )
            non_debug_symbol_pattern = re.compile(
                r"^\s*0x[0-9a-fA-F]+\s+([a-zA-Z_][\w:<>\.~\*\&\s-]*)$"
            )
            name_blacklist = {
                "void", "int", "char", "short", "long", "float", "double",
                "bool", "class", "struct", "enum", "union", "typename",
                "template", "static", "operator", "const",
            }
            partial_blacklist_regex = re.compile(
                r"^(std::|__gnu_cxx::|_GLOBAL__sub_I_|__static_initialization_and_destruction_|__tcf_|typeinfo for|vtable for|operator new|operator delete)"
            )
            
            for line in text_output.splitlines():
                stripped_line = line.strip()
                if not stripped_line or stripped_line.startswith(cli_cmd) or "All defined functions:" in stripped_line:
                    continue
                
                file_match = re.match(r"^(?:File\s+)?(.+):$", stripped_line)
                if file_match:
                    current_file = os.path.normpath(file_match.group(1).strip())
                    in_non_debug_symbols_section = False
                    continue
                
                if "Non-debugging symbols:" in stripped_line:
                    in_non_debug_symbols_section = True
                    continue

                entry: Optional[Dict[str, Any]] = None
                if in_non_debug_symbols_section:
                    match = non_debug_symbol_pattern.match(stripped_line)
                    if match:
                        name = match.group(1).strip()
                        if name and name not in name_blacklist and not partial_blacklist_regex.match(name):
                            entry = {"name": name, "file": "N/A (non-debug)", "line": "N/A", "signature": name}
                else:
                    match = debug_symbol_pattern.match(stripped_line)
                    if match:
                        line_num_str, full_decl, params = match.groups()
                        line_val = int(line_num_str) if line_num_str and line_num_str.isdigit() else "N/A"
                        full_decl = full_decl.strip()
                        
                        name_candidate_match = re.search(
                            r"((?:operator\s*[\<\>\+\-\*\/%&\|=!^~\\[\]]+)|[a-zA-Z_][\w:<>\~]*)(?:\s*<[^>]*>)?$",
                            full_decl,
                        )
                        name = name_candidate_match.group(1).strip() if name_candidate_match else full_decl
                        name = re.sub(r"\s*const$", "", name).strip()
                        
                        signature = f"{full_decl}({params})"
                        if name and name not in name_blacklist and not partial_blacklist_regex.match(name):
                            entry = {"name": name, "file": current_file or "N/A", "line": line_val, "signature": signature}
                
                if entry and entry["name"] not in [f["name"] for f in functions]:
                    functions.append(entry)
                    
            if functions:
                logger.info(f"Parsed {len(functions)} unique functions from MI output.")
                functions.sort(key=_mi_symbol_sort_key)
            else:
                warning_msg = (
                    f"No functions parsed from MI command. Output was: {text_output[:200]}..." if text_output 
                    else f"No textual output received for '{cli_cmd}'. Raw response: {raw_output[:200]}..."
                )
                logger.warning(warning_msg)
            return functions
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Error listing functions via MI: {e}", exc_info=True)
            return []

    # ... (the rest of the list_* methods remain the same as the previous version) ...
    def list_global_variables(
        self, timeout: int = DEFAULT_MI_LONG_OPERATION_TIMEOUT
    ) -> List[Dict[str, Any]]:
        if not self.is_alive():
            logger.error("Cannot list global variables: GDB MI session not active.")
            return []
        cli_cmd = "info variables"; mi_cmd = f'-interpreter-exec console "{cli_cmd}"'
        variables: List[Dict[str, Any]] = []
        try:
            raw_output = self.send_mi_cmd(mi_cmd, timeout=timeout)
            text_output = self._strip_mi_console_output(raw_output)
            current_file: Optional[str] = None
            in_non_debug_symbols_section = False
            debug_var_pattern = re.compile(
                r"^(?:(\d+):\s+)?(.*?)\s+([a-zA-Z_][\w:<>\[\]\*\&\d_]*?)(?:\s*[=;\[\(].*)?$"
            )
            non_debug_var_pattern = re.compile(
                r"^\s*0x[0-9a-fA-F]+\s+([a-zA-Z_][\w:<>\*\&]*)"
            )
            var_blacklist = {"void", "int", "char", "short", "long", "float", "double", "bool", "class", "struct", "enum", "union", "typename", "template", "static", "operator", "std", "__gnu_cxx", "func_ptr", "const"}
            linker_patterns = ["__CTOR_LIST__", "__DTOR_LIST__", "__sub_I_", "__init", "__fini", "__JCR_LIST__", "__JCR_END__", "__FRAME_END__", "__EH_FRAME_BEGIN__", "_data_start__", "_data_end__", "_bss_start__", "_bss_end__", "_IAT_start__", "_IAT_end__", "_RUNTIME_PSEUDO_RELOC_LIST_END__", "__mingw_", "__security_cookie", "__security_cookie_complement", "std::__ioinit", "std::piecewise_construct", "stdin", "stdout", "stderr"]

            for line in text_output.splitlines():
                stripped_line = line.strip()
                if not stripped_line or stripped_line.startswith(cli_cmd) or "All defined variables:" in stripped_line: continue
                file_match = re.match(r"^(?:File\s+)?(.+):$", stripped_line)
                if file_match:
                    current_file = os.path.normpath(file_match.group(1).strip())
                    in_non_debug_symbols_section = False
                    continue
                if "Non-debugging symbols:" in stripped_line:
                    in_non_debug_symbols_section = True
                    continue
                if "Debugging symbols:" in stripped_line:
                    in_non_debug_symbols_section = False
                    continue
                
                entry: Optional[Dict[str, Any]] = None
                if in_non_debug_symbols_section:
                    match = non_debug_var_pattern.match(stripped_line)
                    if match:
                        name = match.group(1).strip()
                        if name and name not in var_blacklist and not any(pat in name for pat in linker_patterns):
                            entry = {"name": name, "type": "N/A (non-debug)", "file": "N/A", "line": "N/A"}
                else:
                    match = debug_var_pattern.match(stripped_line)
                    if match:
                        line_str, type_field, name = match.groups()
                        line_val = int(line_str) if line_str and line_str.isdigit() else "N/A"
                        name = name.strip()
                        type_clean = re.sub(r"^(?:const|volatile|static|extern|typedef|struct|class|enum|union)\s+", "", type_field.strip()).strip()
                        if name and name not in var_blacklist and not any(pat in name for pat in linker_patterns):
                            entry = {"name": name, "type": type_clean, "file": current_file or "N/A", "line": line_val}
                if entry and entry["name"] not in [v["name"] for v in variables]:
                    variables.append(entry)
            
            if variables:
                variables.sort(key=_mi_symbol_sort_key)
            return variables
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Error listing global variables via MI: {e}", exc_info=True)
            return []

    def list_types(
        self, timeout: int = DEFAULT_MI_LONG_OPERATION_TIMEOUT
    ) -> List[Dict[str, Any]]:
        if not self.is_alive():
            logger.error("Cannot list types: GDB MI session not active.")
            return []
        cli_cmd = "info types"; mi_cmd = f'-interpreter-exec console "{cli_cmd}"'
        types_list: List[Dict[str, Any]] = []
        try:
            raw_output = self.send_mi_cmd(mi_cmd, timeout=timeout)
            text_output = self._strip_mi_console_output(raw_output)
            current_file: Optional[str] = None
            type_pattern = re.compile(r"^(?:(\d+):\s+)?(.+?)(?:;|\s*\{|\s*$)")
            type_blacklist = {"bool", "char", "short", "int", "long", "float", "double", "void", "sizetype", "std", "__gnu_cxx", "decltype(nullptr)", "func_ptr", "enum", "class", "struct", "union", "typedef", "typename", "const", "volatile", "static", "extern"}
            internal_patterns = [r"__alloctr_rebind", r"__ptrtr_not_void", r"allocator_traits", r"_Vector_base", r"basic_string<", r"char_traits", r"__normal_iterator", r"ios_base::Init", r"integral_constant", r"ix86_arch_indices", r"ix86_tune_indices", r"default_delete", r"unique_ptr", r"nullptr_t", r"_Iter_debug_offset", r"_List_node", r"__aligned_membuf"]
            
            for line in text_output.splitlines():
                stripped_line = line.strip()
                if not stripped_line or stripped_line.startswith(cli_cmd) or "All defined types:" in stripped_line: continue
                file_match = re.match(r"^(?:File\s+)?(.+):$", stripped_line)
                if file_match:
                    current_file = os.path.normpath(file_match.group(1).strip())
                    continue
                match = type_pattern.match(stripped_line)
                if match:
                    line_str, full_definition = match.groups()
                    line_val = int(line_str) if line_str and line_str.isdigit() else "N/A"
                    full_definition = full_definition.strip()
                    name_candidate = re.sub(r"^(?:const|volatile|struct|class|enum|union|typedef|typename)\s+", "", full_definition).strip()
                    name_clean = re.sub(r"\s*<.*?>\s*", "", name_candidate).replace("*", "").replace("&", "").strip()
                    if "::" in name_clean: name_clean = name_clean.split("::")[-1]
                    if name_clean and name_clean not in type_blacklist and not any(re.search(pat, full_definition, re.IGNORECASE) for pat in internal_patterns) and name_clean not in [t["name"] for t in types_list]:
                        types_list.append({"name": name_clean, "file": current_file or "N/A", "line": line_val, "definition": full_definition})
            
            if types_list:
                types_list.sort(key=_mi_symbol_sort_key)
            return types_list
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Error listing types via MI: {e}", exc_info=True)
            return []

    def list_source_files(
        self, timeout: int = DEFAULT_MI_LONG_OPERATION_TIMEOUT
    ) -> List[Dict[str, Any]]:
        if not self.is_alive():
            logger.error("Cannot list source files: GDB MI session not active.")
            return []
        cli_cmd = "info sources"; mi_cmd = f'-interpreter-exec console "{cli_cmd}"'
        source_files: List[Dict[str, Any]] = []
        try:
            raw_output = self.send_mi_cmd(mi_cmd, timeout=timeout)
            text_output = self._strip_mi_console_output(raw_output)
            in_read_symbols_section, in_on_demand_section = False, False
            for line in text_output.splitlines():
                stripped_line = line.strip()
                if not stripped_line or stripped_line.startswith(cli_cmd): continue
                if "Source files for which symbols have been read in:" in stripped_line:
                    in_read_symbols_section, in_on_demand_section = True, False
                    continue
                elif "Source files for which symbols will be read in on demand:" in stripped_line:
                    in_read_symbols_section, in_on_demand_section = False, True
                    continue
                
                status_label = "read" if in_read_symbols_section else ("on_demand" if in_on_demand_section else "unknown_section")
                path_candidates = [p.strip() for p in stripped_line.split(", ") if p.strip()]
                
                for path_item in path_candidates:
                    if path_item and not path_item.startswith("<") and not path_item.startswith("[") and path_item.lower() not in ["<built-in>", "<command-line>", "<compiler-generated>"] and not (path_item == "." or path_item == ".."):
                        if "\\" in path_item or "/" in path_item or (len(path_item) > 1 and path_item[1] == ":"):
                            normalized_path = os.path.normpath(path_item.replace("\\", os.sep))
                            if not any(entry["path"] == normalized_path for entry in source_files):
                                source_files.append({"path": normalized_path, "status": status_label, "line": "N/A", "name": os.path.basename(normalized_path)})
            
            if source_files:
                source_files.sort(key=lambda item: item.get("path", "Z").lower())
            return source_files
        except (ConnectionError, TimeoutError) as e:
            logger.error(f"Error listing source files via MI: {e}", exc_info=True)
            return []

    def quit(self, timeout: int = DEFAULT_MI_OPERATION_TIMEOUT) -> None:
        if self.is_alive():
            logger.info(f"Attempting graceful GDB MI quit, timeout {timeout}s.")
            try:
                self.child.sendline("-gdb-exit\n")
                logger.debug("Sent '-gdb-exit' command to GDB MI session.")
                # After sending exit, we expect GDB to close the connection, resulting in EOF
                self.child.expect(pexpect.EOF, timeout=max(1, timeout))
                logger.info("GDB MI session exited gracefully (EOF received after exit command).")
            except pexpect.TIMEOUT:
                logger.warning("GDB MI session did not close (EOF not received) after '-gdb-exit' command within timeout.")
            except (pexpect.EOF, ConnectionError):
                logger.info("GDB MI session was already closed or closed as expected during quit sequence.")
            except Exception as e_quit_general:
                logger.error(f"An unexpected exception occurred during GDB MI quit sequence: {e_quit_general}", exc_info=True)
            finally:
                if self.is_alive():
                    self._force_close_child()
                logger.info("GDB MI session resources have been released.")
        else:
            logger.info("Quit called, but no active GDB MI session.")