# File: cpp_python_debug/core/gdb_interactive_inspector.py
import logging
import os
import re
import time
import json
import pexpect
from typing import Dict, List, Optional, Any, Callable, Union

from cpp_python_debug.core.gdb_mi_session import GDBMISession
from cpp_python_debug.core.config_manager import (
    AppSettings,
)

logger = logging.getLogger(__name__)


class GDBInteractiveInspector:
    """
    Provides methods to interactively inspect a GDB session for specific details
    like variables in scope at a breakpoint. Manages temporary GDB MI sessions.
    """

    def __init__(self, gdb_executable_path: str, app_settings: AppSettings):
        self.gdb_exe_path: str = gdb_executable_path
        self.app_settings: AppSettings = app_settings
        self._mi_session: Optional[GDBMISession] = None

    def _get_timeout(self, category: str, key: str, default: int) -> int:
        if not self.app_settings:
            logger.warning(
                f"AppSettings not available in GDBInteractiveInspector, using default timeout {default} for {category}/{key}"
            )
            return default
        return self.app_settings.get_setting(category, key, default)

    def _cleanup_session(self):
        if self._mi_session and self._mi_session.is_alive():
            try:
                quit_timeout = self._get_timeout("timeouts", "gdb_quit", 10)
                self._mi_session.quit(timeout=quit_timeout)
            except Exception as e:
                logger.error(
                    f"Error quitting GDB MI session in inspector: {e}", exc_info=True
                )
        self._mi_session = None

    def _parse_mi_var_list_payload(self, payload_value: Any) -> List[str]:
        names: List[str] = []
        if isinstance(payload_value, list):
            for item in payload_value:
                if isinstance(item, dict) and "name" in item:
                    name_val = item["name"]
                    if (
                        isinstance(name_val, str)
                        and name_val.startswith('"')
                        and name_val.endswith('"')
                    ):
                        name_val = name_val[1:-1]
                    names.append(name_val)
        elif isinstance(payload_value, str):
            logger.debug(
                f"Parsing string payload for var names (fallback): {payload_value}"
            )
            for match in re.finditer(r'name="((?:[^"\\]|\\.)*?)"', payload_value):
                var_name = match.group(1).replace("\\\\", "\\").replace('\\"', '"')
                names.append(var_name)
        return list(set(names))

    def get_variables_in_scope(
        self,
        target_executable: str,
        breakpoint_location: str,
        program_args: str = "",
        status_callback: Optional[Callable[[str], None]] = None,
    ) -> Dict[str, List[str]]:
        _status_callback: Callable[[str], None] = (
            status_callback
            if status_callback
            else lambda msg: logger.info(f"Inspector Status Update: {msg}")
        )

        result: Dict[str, List[str]] = {"locals": [], "args": []}

        if not os.path.isfile(target_executable):
            _status_callback(f"Error: Target executable not found: {target_executable}")
            logger.error(
                f"Target executable not found for scope inspection: {target_executable}"
            )
            return result

        default_timeouts = self.app_settings._get_default_settings().get("timeouts", {})
        startup_timeout = self._get_timeout("timeouts", "gdb_start", default_timeouts.get("gdb_start", 30))
        cmd_timeout = self._get_timeout("timeouts", "gdb_command", default_timeouts.get("gdb_command", 30))
        inspection_cmd_timeout = self._get_timeout("timeouts", "dump_variable", default_timeouts.get("dump_variable", 30))
        run_timeout = self._get_timeout("timeouts", "program_run_continue", default_timeouts.get("program_run_continue", 120))

        _status_callback(
            f"Initializing GDB for scope inspection at '{breakpoint_location}'..."
        )
        self._mi_session = GDBMISession(
            gdb_path=self.gdb_exe_path, executable_path=target_executable
        )

        try:
            self._mi_session.start(timeout=startup_timeout)
            if not self._mi_session.symbols_found:
                msg = "Warning: No debugging symbols found. Scope inspection may be incomplete or fail."
                _status_callback(msg)
                logger.warning(msg)

            _status_callback(f"Setting breakpoint via MI at: {breakpoint_location}")
            bp_output = self._mi_session.send_mi_cmd(
                f"-break-insert {breakpoint_location}", timeout=cmd_timeout
            )
            bp_mi_result = self._mi_session._parse_mi_result(bp_output)

            if not (
                bp_mi_result.get("result_type") == "^done"
                and "bkpt" in bp_mi_result.get("payload", {})
            ):
                err_msg = f"Error: Could not set or parse breakpoint at '{breakpoint_location}'."
                _status_callback(err_msg + f" GDB output:\n{bp_output[:500]}...")
                logger.error(f"{err_msg} Raw: {bp_output}, Parsed: {bp_mi_result}")
                self._cleanup_session()
                return result
            logger.info(f"Breakpoint set successfully via MI. Response: {bp_mi_result}")

            if program_args and program_args.strip():
                set_args_command = f"-exec-arguments {program_args.strip()}"
                logger.info(f"Setting program arguments with MI command: [{set_args_command}]")
                args_set_output = self._mi_session.send_mi_cmd(set_args_command, timeout=cmd_timeout)
                args_set_mi_result = self._mi_session._parse_mi_result(args_set_output)
                if args_set_mi_result.get("result_type") != "^done":
                    err_msg_args = "Error: Could not set program arguments via MI."
                    _status_callback(err_msg_args + f" GDB output:\n{args_set_output[:500]}...")
                    logger.error(f"{err_msg_args} Raw: {args_set_output}, Parsed: {args_set_mi_result}")
                    self._cleanup_session()
                    return result
                logger.info(f"Program arguments set successfully via MI. Response: {args_set_mi_result}")

            _status_callback(f"Running program to hit breakpoint '{breakpoint_location}'...")
            exec_command = "-exec-run --thread-group i1"
            logger.info(f"Constructed GDB MI exec command: [{exec_command}]")

            if not self._mi_session or not self._mi_session.child:
                raise ConnectionError("GDB MI child process not available before -exec-run.")
            
            self._mi_session.child.sendline(exec_command)
            
            breakpoint_hit_pattern = re.compile(r'^\*stopped,reason="breakpoint-hit".*?thread-id="(\d+)"', re.MULTILINE)
            program_exited_pattern = re.compile(r'^\*stopped,reason="exited(?:-normally|-signalled)?"', re.MULTILINE)
            
            patterns_to_expect = [
                breakpoint_hit_pattern,
                program_exited_pattern,
                pexpect.EOF,
                pexpect.TIMEOUT,
            ]

            logger.info(f"Waiting for program event (breakpoint hit or exit) with timeout: {run_timeout}s")
            index = self._mi_session.child.expect(patterns_to_expect, timeout=run_timeout)
            output_buffer = str(self._mi_session.child.before or "") + str(self._mi_session.child.after or "")
            logger.debug(f"Inspector Run Event Output (Index {index}):\n{output_buffer.strip()}")

            hit_thread_id: Optional[str] = None
            
            if index == 0:
                match = breakpoint_hit_pattern.search(output_buffer)
                if match:
                    hit_thread_id = match.group(1)
                    _status_callback(f"Breakpoint hit on GDB thread ID: {hit_thread_id}.")
                    logger.info(f"Breakpoint hit detected on thread {hit_thread_id}.")
                else:
                    _status_callback("Error: Breakpoint hit event detected, but could not parse thread ID.")
                    logger.error("Breakpoint hit pattern matched, but regex group for thread ID failed.")
                    self._cleanup_session()
                    return result
            elif index == 1:
                _status_callback("Program exited before/without hitting the specified breakpoint.")
                logger.warning("Program exited during run, before hitting breakpoint for scope inspection.")
                self._cleanup_session()
                return result
            elif index == 2:
                _status_callback("Program exited (EOF detected in MI run loop).")
                logger.warning(f"EOF detected during MI program run. Output buffer:\n{output_buffer}")
                self._cleanup_session()
                return result
            elif index == 3:
                _status_callback(f"Timeout ({run_timeout}s) waiting for breakpoint or program exit (MI).")
                logger.warning(f"Timeout waiting for MI breakpoint. Output buffer:\n{output_buffer}")
                self._cleanup_session()
                return result
            else:
                _status_callback(f"Unexpected event (index {index}) from GDB.")
                logger.error(f"expect returned unexpected index {index}.")
                self._cleanup_session()
                return result

            if hit_thread_id:
                if not self._mi_session or not self._mi_session.child:
                    logger.error("GDB MI session or child became None before fetching locals/args.")
                    self._cleanup_session()
                    return result

                _status_callback("Fetching local variables via MI...")
                locals_cmd = f"-stack-list-locals --thread {hit_thread_id} --frame 0 0"
                logger.info(f"Sending MI locals command: [{locals_cmd}]")
                locals_output = self._mi_session.send_mi_cmd(locals_cmd, timeout=inspection_cmd_timeout)
                mi_locals_result = self._mi_session._parse_mi_result(locals_output)
                if mi_locals_result.get("result_type") == "^done":
                    locals_payload = mi_locals_result.get("payload", {}).get("locals")
                    if locals_payload is not None:
                        result["locals"] = self._parse_mi_var_list_payload(locals_payload)
                logger.info(f"Found locals via MI: {result['locals']}")

                _status_callback("Fetching arguments via MI...")
                args_cmd = f"-stack-list-arguments --thread {hit_thread_id} --frame 0 1"
                logger.info(f"Sending MI arguments command: [{args_cmd}]")
                args_output = self._mi_session.send_mi_cmd(args_cmd, timeout=inspection_cmd_timeout)
                mi_args_result = self._mi_session._parse_mi_result(args_output)
                if mi_args_result.get("result_type") == "^done":
                    stack_args_payload = mi_args_result.get("payload", {}).get("stack-args")
                    if isinstance(stack_args_payload, list) and stack_args_payload:
                        frame_zero_args_data = None
                        for frame_data in stack_args_payload:
                            if (isinstance(frame_data, dict) and frame_data.get("frame", {}).get("level") == "0"):
                                frame_zero_args_data = frame_data.get("args")
                                break
                        if frame_zero_args_data is not None:
                            result["args"] = self._parse_mi_var_list_payload(frame_zero_args_data)
                logger.info(f"Found args via MI: {result['args']}")
                _status_callback("Scope inspection complete (MI).")

        except (FileNotFoundError, ConnectionError, TimeoutError) as e_session:
            _status_callback(f"GDB MI Session Error: {e_session}")
            logger.error(f"Inspector GDB MI Session Error: {e_session}", exc_info=True)
        except pexpect.TIMEOUT as e_ptimeout:
            _status_callback(f"GDB MI Communication Timeout: {e_ptimeout}")
            logger.error(f"Inspector pexpect.TIMEOUT (MI): {e_ptimeout}", exc_info=True)
        except Exception as e_general:
            _status_callback(f"Unexpected error during MI inspection: {e_general}")
            logger.error(f"Unexpected error in GDB MI inspector: {e_general}", exc_info=True)
        finally:
            self._cleanup_session()
            logger.info("GDB Interactive Inspector (MI mode) session finished.")

        return result

    def test_action_validity(
        self,
        target_executable: str,
        action_config: Dict[str, Any],
        program_args: str = "",
    ) -> Dict[str, str]:
        logger.warning(
            "test_action_validity is not yet implemented in GDBInteractiveInspector."
        )
        return {
            "status": "not_implemented",
            "details": "This feature is pending implementation.",
        }