"""VirtualBox VM dispatch: screenshots, keyboard, mouse, and guest control via VBoxManage."""

from __future__ import annotations

import tempfile
from typing import TYPE_CHECKING, ClassVar

import anyio

from mcp_vm_blackbox._helpers import _ssh_get_file, _ssh_run

_DEFAULT_GUEST_PASSWORD = "vagrant"  # noqa: S105

if TYPE_CHECKING:
    from mcp_vm_blackbox.targets import TargetConfig


class VBoxDispatch:
    """Dispatches computer-use actions to a VirtualBox VM via VBoxManage.

    Works on the local host or via SSH to a remote host using asyncssh.
    All commands are structured as lists — no shell=True.
    """

    _MAX_STRING_LEN = 256

    # Minimum parts in a keyboard combo (modifier + key)
    _MIN_COMBO_PARTS = 2

    # Full keyboard scancode table (PC/AT Set 1).
    # Format: (press_code, release_code) as hex strings.
    # Keys with E0 prefix use extended scancodes.
    _KEYS: ClassVar[dict[str, tuple[str, str]]] = {
        # Letters (a-z) - lowercase
        "a": ("1e", "9e"),
        "b": ("30", "b0"),
        "c": ("2e", "ae"),
        "d": ("20", "a0"),
        "e": ("12", "92"),
        "f": ("21", "a1"),
        "g": ("22", "a2"),
        "h": ("23", "a3"),
        "i": ("17", "97"),
        "j": ("24", "a4"),
        "k": ("25", "a5"),
        "l": ("26", "a6"),
        "m": ("32", "b2"),
        "n": ("31", "b1"),
        "o": ("18", "98"),
        "p": ("19", "99"),
        "q": ("10", "90"),
        "r": ("13", "93"),
        "s": ("1f", "9f"),
        "t": ("14", "94"),
        "u": ("16", "96"),
        "v": ("2f", "af"),
        "w": ("11", "91"),
        "x": ("2d", "ad"),
        "y": ("15", "95"),
        "z": ("2c", "ac"),
        # Numbers (0-9) - top row
        "0": ("0b", "8b"),
        "1": ("02", "82"),
        "2": ("03", "83"),
        "3": ("04", "84"),
        "4": ("05", "85"),
        "5": ("06", "86"),
        "6": ("07", "87"),
        "7": ("08", "88"),
        "8": ("09", "89"),
        "9": ("0a", "8a"),
        # Function keys (F1-F12)
        "f1": ("3b", "bb"),
        "f2": ("3c", "bc"),
        "f3": ("3d", "bd"),
        "f4": ("3e", "be"),
        "f5": ("3f", "bf"),
        "f6": ("40", "c0"),
        "f7": ("41", "c1"),
        "f8": ("42", "c2"),
        "f9": ("43", "c3"),
        "f10": ("44", "c4"),
        "f11": ("57", "d7"),
        "f12": ("58", "d8"),
        # Navigation and editing keys (extended scancodes with E0 prefix)
        "up": ("e048", "e0c8"),
        "down": ("e050", "e0d0"),
        "left": ("e04b", "e0cb"),
        "right": ("e04d", "e0cd"),
        "home": ("e047", "e0c7"),
        "end": ("e04f", "e0cf"),
        "pageup": ("e049", "e0c9"),
        "pagedown": ("e051", "e0d1"),
        "insert": ("e052", "e0d2"),
        "delete": ("e053", "e0d3"),
        # Special keys
        "Return": ("1c", "9c"),
        "enter": ("1c", "9c"),
        "Tab": ("0f", "8f"),
        "tab": ("0f", "8f"),
        "Escape": ("01", "81"),
        "escape": ("01", "81"),
        "space": ("39", "b9"),
        "BackSpace": ("0e", "8e"),
        "backspace": ("0e", "8e"),
        "printscreen": ("e037", "e0b7"),
        # Symbols (common)
        "minus": ("0c", "8c"),  # -
        "equals": ("0d", "8d"),  # =
        "bracketleft": ("1a", "9a"),  # [
        "bracketright": ("1b", "9b"),  # ]
        "backslash": ("2b", "ab"),  # \
        "semicolon": ("27", "a7"),  # ;
        "apostrophe": ("28", "a8"),  # '
        "grave": ("29", "a9"),  # `
        "comma": ("33", "b3"),  # ,
        "period": ("34", "b4"),  # .
        "slash": ("35", "b5"),  # /
    }

    # Modifier keys with left/right variants.
    # These are used for keyboard combos (ctrl+c, win+r, etc.)
    _MODIFIERS: ClassVar[dict[str, tuple[str, str]]] = {
        # Left modifiers (standard)
        "ctrl": ("1d", "9d"),
        "shift": ("2a", "aa"),
        "alt": ("38", "b8"),
        "win": ("e05b", "e0db"),  # Left Windows key (extended)
        # Right modifiers (use explicit "right_" prefix)
        "right_ctrl": ("e01d", "e09d"),
        "right_shift": ("36", "b6"),
        "right_alt": ("e038", "e0b8"),
        "right_win": ("e05c", "e0dc"),
    }

    # Backward-compatible alias for the old scancode table
    _SCANCODES: ClassVar[dict[str, tuple[str, str]]] = _KEYS

    def __init__(self, vm_name: str, cfg: TargetConfig) -> None:
        """Initialise the dispatcher for a named VM.

        Args:
            vm_name: VirtualBox VM name as shown in ``VBoxManage list vms``.
            cfg: Resolved target configuration (local or remote).
        """
        self.vm_name = vm_name
        self._cfg = cfg

    def _vbox_args(self, *args: str) -> list[str]:
        return ["VBoxManage", *args]

    def _screenshot_args(self, path: str) -> list[str]:
        return self._vbox_args("controlvm", self.vm_name, "screenshotpng", path)

    def _type_cmds(self, text: str) -> list[list[str]]:
        chunks = [text[i : i + self._MAX_STRING_LEN] for i in range(0, len(text), self._MAX_STRING_LEN)]
        return [self._vbox_args("controlvm", self.vm_name, "keyboardputstring", chunk) for chunk in chunks]

    def _click_cmds(self, x: int, y: int) -> list[list[str]]:
        return [
            self._vbox_args("controlvm", self.vm_name, "mousemove", str(x), str(y)),
            self._vbox_args("controlvm", self.vm_name, "mousebutton", "1"),
            self._vbox_args("controlvm", self.vm_name, "mousebutton", "0"),
        ]

    def _key_cmds(self, key: str) -> list[list[str]]:
        if key in self._KEYS:
            press, release = self._KEYS[key]
            return [self._vbox_args("controlvm", self.vm_name, "keyboardputscancode", press, release)]
        # Fall back to typing the key as a string
        return self._type_cmds(key)

    def _combo_cmds(self, combo: str) -> tuple[list[list[str]], str, list[str]]:
        """Parse a keyboard combo and generate scancode sequences.

        Parses combos like "ctrl+c", "win+r", "alt+f4", "shift+tab" into
        the proper scancode sequence: press modifiers, press key, release key,
        release modifiers.

        Args:
            combo: Combo string in "modifier+key" format, e.g., "ctrl+c", "win+r".

        Returns:
            Tuple of (commands, parsed_combo, scancodes_sent).
            - commands: List of VBoxManage command lists to execute.
            - parsed_combo: Normalized combo string.
            - scancodes_sent: List of hex scancodes that were sent.

        Raises:
            ValueError: When the combo cannot be parsed or contains unknown keys.
        """
        parts = [p.strip().lower() for p in combo.split("+")]
        if len(parts) < self._MIN_COMBO_PARTS:
            raise ValueError(f"Combo must have at least one modifier and one key: '{combo}'")

        # Separate modifiers from the final key
        modifier_names = parts[:-1]
        key_name = parts[-1]

        # Validate and collect modifier scancodes
        modifier_scancodes = []
        for mod in modifier_names:
            if mod not in self._MODIFIERS:
                raise ValueError(f"Unknown modifier: '{mod}'")
            press, _ = self._MODIFIERS[mod]
            modifier_scancodes.append(press)

        # Validate key
        if key_name not in self._KEYS:
            raise ValueError(f"Unknown key: '{key_name}'")
        key_press, key_release = self._KEYS[key_name]

        # Build the scancode sequence:
        # 1. Press all modifiers
        # 2. Press and release the key
        # 3. Release all modifiers (reverse order)
        commands: list[list[str]] = []
        scancodes_sent: list[str] = []

        # Press modifiers
        for press_code in modifier_scancodes:
            commands.append(self._vbox_args("controlvm", self.vm_name, "keyboardputscancode", press_code))
            scancodes_sent.append(press_code)

        # Press and release the key
        commands.append(self._vbox_args("controlvm", self.vm_name, "keyboardputscancode", key_press, key_release))
        scancodes_sent.extend([key_press, key_release])

        # Release modifiers (reverse order)
        for mod in reversed(modifier_names):
            _, release = self._MODIFIERS[mod]
            commands.append(self._vbox_args("controlvm", self.vm_name, "keyboardputscancode", release))
            scancodes_sent.append(release)

        parsed = "+".join([*modifier_names, key_name])
        return commands, parsed, scancodes_sent

    async def screenshot(self, path: str) -> bytes:
        """Capture a PNG screenshot of the VM display.

        Args:
            path: Path where the PNG will be written. For local targets this
                is a local path; for remote targets VBoxManage writes to this
                path on the remote host, then the file is copied to a local
                temp file for reading.

        Returns:
            Raw PNG bytes of the captured screenshot.

        Raises:
            RuntimeError: When VBoxManage returns non-zero exit code.
        """
        result = await _ssh_run(self._cfg, self._screenshot_args(path))
        if result.returncode != 0:
            raise RuntimeError(f"VBoxManage screenshot failed: {result.stderr.strip() or result.stdout}")
        if self._cfg.is_remote:
            with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
                local_path = tmp.name
            try:
                await _ssh_get_file(self._cfg, path, local_path)
                return await anyio.Path(local_path).read_bytes()
            finally:
                await anyio.Path(local_path).unlink(missing_ok=True)
        return await anyio.Path(path).read_bytes()

    async def click(self, x: int, y: int) -> None:
        """Send a left mouse click at the given VM display coordinates.

        Args:
            x: Horizontal pixel coordinate on the VM display.
            y: Vertical pixel coordinate on the VM display.

        Raises:
            RuntimeError: When a command returns non-zero.
        """
        for cmd in self._click_cmds(x, y):
            result = await _ssh_run(self._cfg, cmd)
            if result.returncode != 0:
                raise RuntimeError(f"VBoxManage click failed: {result.stderr.strip() or result.stdout}")

    async def type_text(self, text: str) -> None:
        """Type a string into the VM using keyboard input.

        Args:
            text: Text to type into the VM.  Long strings are chunked
                to stay within VBoxManage's per-call limit.

        Raises:
            RuntimeError: When a command returns non-zero.
        """
        for cmd in self._type_cmds(text):
            result = await _ssh_run(self._cfg, cmd)
            if result.returncode != 0:
                raise RuntimeError(f"VBoxManage type_text failed: {result.stderr.strip() or result.stdout}")

    async def key(self, key_name: str) -> None:
        """Press a named key on the VM keyboard.

        Recognised key names map to scan codes (``Return``, ``Tab``,
        ``Escape``, ``space``, ``BackSpace``).  Any other value is passed
        through :meth:`type_text`.

        Args:
            key_name: Name of the key to press.

        Raises:
            RuntimeError: When a command returns non-zero.
        """
        for cmd in self._key_cmds(key_name):
            result = await _ssh_run(self._cfg, cmd)
            if result.returncode != 0:
                raise RuntimeError(f"VBoxManage key failed: {result.stderr.strip() or result.stdout}")

    async def key_combo(self, combo: str) -> tuple[str, list[str]]:
        """Send a keyboard combo (modifier+key) to the VM.

        Parses combo strings like "ctrl+c", "win+r", "alt+f4" into the proper
        scancode sequence: press modifiers, press key, release key, release modifiers.

        Args:
            combo: Combo string in "modifier+key" format.

        Returns:
            Tuple of (parsed_combo, scancodes_sent).

        Raises:
            ValueError: When the combo cannot be parsed or contains unknown keys.
            RuntimeError: When a VBoxManage command returns non-zero.
        """
        commands, parsed, scancodes = self._combo_cmds(combo)
        for cmd in commands:
            result = await _ssh_run(self._cfg, cmd)
            if result.returncode != 0:
                raise RuntimeError(f"VBoxManage key_combo failed: {result.stderr.strip() or result.stdout}")
        return parsed, scancodes

    async def start_vm(self, mode: str = "headless") -> None:
        """Start the VM in the specified mode.

        Args:
            mode: Start mode: 'headless' (default), 'gui', or 'separate'.

        Raises:
            RuntimeError: When VBoxManage returns non-zero.
        """
        cmd = self._vbox_args("startvm", self.vm_name, "--type", mode)
        result = await _ssh_run(self._cfg, cmd)
        if result.returncode != 0:
            raise RuntimeError(f"VBoxManage startvm failed: {result.stderr.strip() or result.stdout}")

    async def stop_vm(self, mode: str = "acpi") -> None:
        """Stop the VM using the specified method.

        Args:
            mode: Stop mode: 'acpi' (graceful), 'poweroff' (hard), or 'savestate'.

        Raises:
            RuntimeError: When VBoxManage returns non-zero.
        """
        # Map user-facing mode names to VBoxManage controlvm commands
        mode_map = {"acpi": "acpipowerbutton", "poweroff": "poweroff", "savestate": "savestate"}
        vbox_cmd = mode_map.get(mode, mode)
        result = await _ssh_run(self._cfg, self._vbox_args("controlvm", self.vm_name, vbox_cmd))
        if result.returncode != 0:
            raise RuntimeError(f"VBoxManage controlvm {vbox_cmd} failed: {result.stderr.strip() or result.stdout}")

    async def copy_to_vm(
        self, local_src: str, guest_dst: str, username: str = "vagrant", password: str = _DEFAULT_GUEST_PASSWORD
    ) -> None:
        """Copy a file from the host into the guest VM.

        Args:
            local_src: Path to the source file on the host.
            guest_dst: Destination path inside the guest VM.
            username: Guest OS username for authentication.
            password: Guest OS password for authentication.

        Raises:
            RuntimeError: When VBoxManage returns non-zero.
        """
        cmd = self._vbox_args(
            "guestcontrol", self.vm_name, "copyto", "--username", username, "--password", password, local_src, guest_dst
        )
        result = await _ssh_run(self._cfg, cmd)
        if result.returncode != 0:
            raise RuntimeError(f"VBoxManage copyto failed: {result.stderr.strip() or result.stdout}")

    async def copy_from_vm(
        self, guest_src: str, local_dst: str, username: str = "vagrant", password: str = _DEFAULT_GUEST_PASSWORD
    ) -> None:
        """Copy a file from the guest VM to the host.

        Args:
            guest_src: Path to the source file inside the guest VM.
            local_dst: Destination path on the host.
            username: Guest OS username for authentication.
            password: Guest OS password for authentication.

        Raises:
            RuntimeError: When VBoxManage returns non-zero.
        """
        cmd = self._vbox_args(
            "guestcontrol",
            self.vm_name,
            "copyfrom",
            "--username",
            username,
            "--password",
            password,
            guest_src,
            local_dst,
        )
        result = await _ssh_run(self._cfg, cmd)
        if result.returncode != 0:
            raise RuntimeError(f"VBoxManage copyfrom failed: {result.stderr.strip() or result.stdout}")
