"""VM keyboard, mouse, and PowerShell interaction tools."""

from __future__ import annotations

import time
from typing import Annotated, Any

from fastmcp.exceptions import ToolError
from fastmcp.tools import tool
from pydantic import Field

from mcp_vm_blackbox._helpers import KEY_MAP, _load_vboxapi, _resolve, _vbox
from mcp_vm_blackbox.models import CommandResult, VmKeyComboResult, VmKeyResult, VmMouseClickResult, VmTypeResult
from mcp_vm_blackbox.tunnel import SshTunnel
from mcp_vm_blackbox.winrm_client import WinRMClient

# ---------------------------------------------------------------------------
# Marks cache — populated by vm_screenshot(som=True) via T04 / som_overlay.py
# ---------------------------------------------------------------------------

# Maps vm_name -> SoMMarksLegend (typed as Any until som_overlay.py lands in T03)
_marks_cache: dict[str, Any] = {}


def resolve_mark(vm_name: str, mark_id: int) -> tuple[int, int]:
    """Resolve a SoM mark ID to native VM coordinates.

    Args:
        vm_name: VirtualBox VM name.
        mark_id: 1-based mark ID from the SoM marks legend.

    Returns:
        Tuple of (native_x, native_y) in native VM resolution pixels.

    Raises:
        ToolError: When no marks exist for the VM or the mark_id is not found.
    """
    legend = _marks_cache.get(vm_name)
    if legend is None:
        raise ToolError(
            f"No marks cached for VM '{vm_name}'. Run vm_screenshot(som=True) first to populate the marks cache."
        )
    for entry in legend.marks:
        if entry.mark_id == mark_id:
            return entry.native_x, entry.native_y
    raise ToolError(f"Mark {mark_id} not found in cache for VM '{vm_name}'")


def update_marks_cache(vm_name: str, legend: Any) -> None:
    """Store a SoMMarksLegend in the module-level marks cache.

    Called by vm_screenshot(som=True) after successfully stamping marks onto
    the screenshot image.

    Args:
        vm_name: VirtualBox VM name (cache key).
        legend: SoMMarksLegend instance from som_overlay.stamp_marks().
    """
    _marks_cache[vm_name] = legend


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@tool
async def vm_powershell(
    vm_name: Annotated[str, Field(description="VirtualBox VM name (used for context).")],
    script: Annotated[str, Field(description="PowerShell script text to execute.")],
    target: Annotated[
        str, Field(description='Named target ("local", "ci") or raw SSH string ("user@host").')
    ] = "local",
    timeout_s: Annotated[
        int, Field(description="Intended WinRM call timeout in seconds (not yet enforced by WinRMClient).", ge=1)
    ] = 30,
    winrm_host: Annotated[str, Field(description="WinRM host address for the local tunnel endpoint.")] = "127.0.0.1",
) -> CommandResult:
    """Run a PowerShell script on a Windows VM via WinRM.

    Opens a short-lived SSH tunnel for remote targets and tears it down after
    the call completes.

    Note:
        ``timeout_s`` is accepted for API compatibility but is **not** currently
        honoured — ``WinRMClient.run_ps()`` does not expose a per-call timeout.
        The parameter is retained so callers can document intent.

    Args:
        vm_name: VirtualBox VM name (used for context; the WinRM host is
            derived from the target config).
        script: PowerShell script text to execute.
        target: Named target or raw SSH string.
        timeout_s: Intended WinRM call timeout in seconds (not yet enforced).
        winrm_host: Host address used for the local end of the WinRM tunnel.
            Defaults to ``"127.0.0.1"``.

    Returns:
        CommandResult with stdout, stderr, exit_code.  On failure, error is set.
    """
    cfg = _resolve(target)
    winrm_port = cfg.tunnel_local_port if cfg.is_remote else cfg.winrm_port

    async with SshTunnel(cfg):
        try:
            client = WinRMClient(host=winrm_host, port=winrm_port)
            stdout = client.run_ps(script)
        except RuntimeError as exc:
            return CommandResult(stdout="", stderr=str(exc), exit_code=1)
        except OSError as exc:
            return CommandResult(
                stdout="",
                stderr="",
                exit_code=-1,
                error=f"WinRM connection failed. Is the VM running and port 5985 reachable? ({exc})",
            )
        else:
            return CommandResult(stdout=stdout, stderr="", exit_code=0)


@tool
async def vm_type(
    vm_name: Annotated[str, Field(description="VirtualBox VM name.")],
    text: Annotated[str, Field(description="Text to type into the VM.")],
    target: Annotated[
        str, Field(description='Named target ("local", "ci") or raw SSH string ("user@host").')
    ] = "local",
) -> VmTypeResult:
    """Type text into a VM via keyboard emulation, chunked at 256 chars.

    Args:
        vm_name: VirtualBox VM name.
        text: Text to type into the VM.
        target: Named target or raw SSH string.

    Returns:
        VmTypeResult with status and chars_sent on success, or error on failure.
    """
    cfg = _resolve(target)
    try:
        await _vbox(cfg, vm_name).type_text(text)
        return VmTypeResult(status="ok", chars_sent=len(text))
    except RuntimeError as exc:
        return VmTypeResult(error=f"VM '{vm_name}' not found. Run vm_list to see available VMs. ({exc})")


@tool
async def vm_key(
    vm_name: Annotated[str, Field(description="VirtualBox VM name.")],
    key: Annotated[
        str,
        Field(
            description='Special key to send. One of: "enter", "tab", "escape", "space", "backspace", or letters a-z, numbers 0-9, f1-f12, arrow keys (up/down/left/right), home, end, pageup, pagedown, insert, delete, printscreen.'
        ),
    ],
    target: Annotated[
        str, Field(description='Named target ("local", "ci") or raw SSH string ("user@host").')
    ] = "local",
) -> VmKeyResult:
    """Send a special key to a VM.

    Args:
        vm_name: VirtualBox VM name.
        key: Key name (enter, tab, escape, space, backspace, a-z, 0-9, f1-f12,
            up/down/left/right, home, end, pageup, pagedown, insert, delete, printscreen).
        target: Named target or raw SSH string.

    Returns:
        VmKeyResult with status and key_sent on success, or error on failure.
    """
    # Normalize key name for lookup
    key_lower = key.lower()
    if key_lower not in KEY_MAP:
        return VmKeyResult(error=f"Unknown key '{key}'. Valid keys: {', '.join(sorted(KEY_MAP.keys()))}")
    cfg = _resolve(target)
    try:
        await _vbox(cfg, vm_name).key(KEY_MAP[key_lower])
    except RuntimeError as exc:
        return VmKeyResult(error=f"VM '{vm_name}' not found. Run vm_list to see available VMs. ({exc})")
    else:
        return VmKeyResult(status="ok", key_sent=key)


@tool
async def vm_key_combo(
    vm_name: Annotated[str, Field(description="VirtualBox VM name.")],
    combo: Annotated[
        str,
        Field(
            description='Keyboard combo in "modifier+key" format. Modifiers: ctrl, shift, alt, win. Examples: "ctrl+c", "win+r", "alt+f4", "shift+tab".'
        ),
    ],
    target: Annotated[
        str, Field(description='Named target ("local", "ci") or raw SSH string ("user@host").')
    ] = "local",
) -> VmKeyComboResult:
    """Send a keyboard combo (modifier+key) to a VM.

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

    Supported modifiers: ctrl, shift, alt, win (left-side modifiers only).

    Args:
        vm_name: VirtualBox VM name.
        combo: Keyboard combo in "modifier+key" format.
            Examples: "ctrl+c", "win+r", "alt+f4", "shift+tab".
        target: Named target or raw SSH string.

    Returns:
        VmKeyComboResult with status, combo_parsed, and scancodes_sent on success,
        or error on failure.
    """
    cfg = _resolve(target)
    try:
        parsed, scancodes = await _vbox(cfg, vm_name).key_combo(combo)
    except ValueError as exc:
        return VmKeyComboResult(status="error", combo_parsed=combo, error=f"Invalid combo: {exc}")
    except RuntimeError as exc:
        return VmKeyComboResult(
            status="error", combo_parsed=combo, error=f"VM '{vm_name}' not found or VBoxManage failed. ({exc})"
        )
    else:
        return VmKeyComboResult(status="ok", combo_parsed=parsed, scancodes_sent=scancodes)


@tool
def vm_mouse_click(
    vm_name: Annotated[str, Field(description="VirtualBox VM name.")],
    x: Annotated[
        int,
        Field(
            description=(
                "Absolute X coordinate (pixels from left edge, starts at 1). Required when mark_id is not provided."
            )
        ),
    ] = 0,
    y: Annotated[
        int,
        Field(
            description=(
                "Absolute Y coordinate (pixels from top edge, starts at 1). Required when mark_id is not provided."
            )
        ),
    ] = 0,
    target: Annotated[
        str,
        Field(description='Named target or raw SSH string. Only "local" is supported (vboxapi is local XPCOM only).'),
    ] = "local",
    button: Annotated[str, Field(description='Mouse button to click. One of: "left", "right", "middle".')] = "left",
    double_click: Annotated[bool, Field(description="If True, send two click pairs with 50ms sleep between.")] = False,
    mark_id: Annotated[
        int | None,
        Field(
            description=(
                "SoM mark ID from the marks_legend returned by vm_screenshot(som=True). "
                "When provided, coordinates are resolved from the marks cache and x/y are ignored. "
                "Takes priority over scale."
            ),
            ge=1,
        ),
    ] = None,
    scale: Annotated[
        float | None,
        Field(
            description=(
                "Scale factor from ScaleInfo returned by vm_screenshot. "
                "When provided (and mark_id is not set), native coordinates are computed as "
                "int(x / scale) and int(y / scale). Must be > 0.0 and <= 1.0."
            ),
            gt=0.0,
            le=1.0,
        ),
    ] = None,
) -> VmMouseClickResult:
    """Click a mouse button at absolute coordinates on a VM via the VirtualBox Python API.

    Uses vboxapi XPCOM directly (no subprocess).  Requires Guest Additions
    with ``absoluteSupported=True`` on the guest.

    Only works for ``target="local"`` because vboxapi uses local XPCOM.

    Coordinate resolution priority (highest to lowest):

    1. ``mark_id`` — resolves native coordinates from the SoM marks cache
       populated by ``vm_screenshot(som=True)``.
    2. ``scale`` — scales JPEG-space ``x``/``y`` to native resolution via
       ``int(x / scale)``, ``int(y / scale)``.
    3. Raw ``x``/``y`` — used as-is (backward-compatible default).

    Args:
        vm_name: VirtualBox VM name.
        x: Absolute X coordinate (1-based, pixels from left edge).
            Required when ``mark_id`` is not provided.
        y: Absolute Y coordinate (1-based, pixels from top edge).
            Required when ``mark_id`` is not provided.
        target: Named target or raw SSH string.  Must be ``"local"``.
        button: Mouse button to press.  One of ``"left"``, ``"right"``,
            ``"middle"``.
        double_click: When ``True``, sends two click pairs with a 50 ms
            pause between them.
        mark_id: SoM mark ID from ``marks_legend``.  Resolves to native
            coordinates via the in-process marks cache.  When set,
            ``x``/``y`` are ignored.
        scale: Scale factor from ``ScaleInfo``.  When set (and
            ``mark_id`` is ``None``), native coordinates are computed as
            ``int(x / scale)``, ``int(y / scale)``.

    Returns:
        VmMouseClickResult with status, x, y, button, scale_applied, and
        mark_id_resolved on success.

    Raises:
        ToolError: When ``target`` is not ``"local"``, vboxapi is not
            available, ``mark_id`` cannot be resolved, or ``x``/``y``
            are both zero without a ``mark_id``.
    """
    if target != "local":
        raise ToolError("vm_mouse_click only supports target='local' (vboxapi uses local XPCOM)")

    button_map: dict[str, int] = {"left": 0x01, "right": 0x02, "middle": 0x04}
    if button not in button_map:
        raise ToolError(f"Unknown button '{button}'. Valid values: {', '.join(button_map)}")
    button_mask = button_map[button]

    # Coordinate resolution: mark_id > scale > raw x/y
    scale_applied: float | None = None
    mark_id_resolved: int | None = None
    native_x: int
    native_y: int

    if mark_id is not None:
        native_x, native_y = resolve_mark(vm_name, mark_id)
        mark_id_resolved = mark_id
    elif scale is not None:
        if x <= 0 or y <= 0:
            raise ToolError(f"x and y must be > 0 when scale is provided (got x={x}, y={y})")
        native_x = int(x / scale)
        native_y = int(y / scale)
        scale_applied = scale
    else:
        if x <= 0 or y <= 0:
            raise ToolError(f"x and y must be > 0 when mark_id is not provided (got x={x}, y={y})")
        native_x = x
        native_y = y

    vbox_mgr_cls = _load_vboxapi()

    mgr = vbox_mgr_cls(None, None)
    try:
        vbox = mgr.getVirtualBox()
        machine = vbox.findMachine(vm_name)
        session = mgr.openMachineSession(machine, fPermitSharing=True)
        try:
            mouse = session.console.mouse

            def _click_once() -> None:
                mouse.putMouseEventAbsolute(native_x, native_y, 0, 0, button_mask)
                mouse.putMouseEventAbsolute(native_x, native_y, 0, 0, 0)
                mouse.putMouseEvent(0, 0, 0, 0, 0)

            _click_once()
            if double_click:
                time.sleep(0.05)
                _click_once()
        finally:
            mgr.closeMachineSession(session)
    except Exception as exc:
        raise ToolError(f"Mouse click failed on VM '{vm_name}': {exc}") from exc

    return VmMouseClickResult(
        status="ok",
        x=native_x,
        y=native_y,
        button=button,
        scale_applied=scale_applied,
        mark_id_resolved=mark_id_resolved,
    )
