"""MCP tools that bridge the browser layer.

Six tools, split by purpose:

- ``open_debug_window`` — the ONLY tool that can launch a Chromium window.
  Costs a Control Room session slot. Refused when the launch budget says no
  (crash-loop guard) and when the unsaved-input guard fires on the existing
  window.
- ``close_debug_window`` — terminate the live window. Releases the slot.
- ``inspect_console`` / ``inspect_network`` / ``screenshot_debug`` /
  ``get_computed_style`` — read-only inspection over the live window via
  Playwright-over-CDP. Structurally unable to open a window; they surface a
  clear "call open_debug_window first" error when none exists.

All browser tools return a structured dict. Errors from the browser layer
(``BrowserUnavailable``, ``NoDebugWindow``, ``LaunchBudgetExceeded``,
``UnsavedInputBlocked``, ``WindowLaunchError``, ``InspectError``) are caught
HERE and turned into ``{success: False, error, error_type, ...}`` dicts —
the server pipeline never sees them as exceptions. ``A360AuthError`` is NOT
caught: it must propagate per the Phase 1 contract.
"""

from __future__ import annotations

import logging

from pydantic import BaseModel, Field

from ..auth.auth_manager import AuthManager
from ..utils.config import ServerConfig
from ..utils.registry import register_tool

logger = logging.getLogger(__name__)

# How long inspect_console / inspect_network listen when no duration is given.
DEFAULT_LISTEN_SECONDS = 3.0
MAX_LISTEN_SECONDS = 30.0


# --------------------------------------------------------------------- models


class OpenDebugWindowParams(BaseModel):
    start_url: str | None = Field(
        default=None,
        description="Optional URL to open. Defaults to the Control Room home.",
    )


class CloseDebugWindowParams(BaseModel):
    force: bool = Field(
        default=False,
        description="Close even with unsaved form input on the page.",
    )


class InspectConsoleParams(BaseModel):
    duration_seconds: float | None = Field(
        default=None,
        description="Seconds to listen for console messages. Default 3, max 30.",
    )


class InspectNetworkParams(BaseModel):
    duration_seconds: float | None = Field(
        default=None,
        description="Seconds to listen for XHR/fetch traffic. Default 3, max 30.",
    )


class ScreenshotDebugParams(BaseModel):
    output_path: str | None = Field(
        default=None,
        description="Path to save the PNG. Omitted: inline base64, or disk if too large.",
    )
    full_page: bool = Field(default=True, description="Capture the full scrollable page.")


class GetComputedStyleParams(BaseModel):
    selector: str = Field(description="CSS selector of the element to inspect.")
    properties: list[str] | None = Field(
        default=None,
        description="CSS property names to read. Default: a compact summary.",
    )


# ----------------------------------------------------------------- helpers


def _error(*, tool: str, error: str, error_type: str, **extra) -> dict:
    payload = {"success": False, "tool": tool, "error": error, "error_type": error_type}
    payload.update(extra)
    return payload


def _resolve_window(config: ServerConfig):
    """Return ``(window_state, error_dict)`` — exactly one is None."""
    from ..browser import find_window

    state = find_window(
        control_room_url=config.control_room_url,
        username=config.auth.username,
    )
    if state is None:
        return None, _error(
            tool="browser",
            error="No live debug window. Call open_debug_window first.",
            error_type="no_debug_window",
        )
    return state, None


# ----------------------------------------------------------------- tools


@register_tool(
    "open_debug_window",
    params=OpenDebugWindowParams,
    description="Open the shared Control Room debug window (costs a session slot).",
)
def open_debug_window(
    config: ServerConfig, auth_manager: AuthManager, params: OpenDebugWindowParams
) -> dict:
    tool = "open_debug_window"
    try:
        from ..browser import (
            LaunchBudgetExceeded,
            WindowLaunchError,
            bind_browser_slot,
            claim_browser_slot,
            find_window,
            launch_window,
            release_browser_slot,
        )
        from ..browser.session import SessionBudgetExceeded
    except ImportError as exc:
        return _error(
            tool=tool,
            error=f"Browser layer not installed: {exc}",
            error_type="browser_unavailable",
            action_required="pip install 'a360-mcp[browser]' && playwright install chromium",
        )

    # Reuse an already-live window: the user might call this again by mistake.
    # This short-circuit is also why opening runs no unsaved-input guard —
    # either a window exists and we touch nothing, or there is none to disrupt.
    # The guard lives on close_debug_window, which does tear a live page down.
    existing = find_window(
        control_room_url=config.control_room_url,
        username=config.auth.username,
    )
    if existing is not None:
        return {
            "success": True,
            "tool": tool,
            "already_open": True,
            "window": existing.to_payload(),
            "note": "A debug window is already open for this account.",
        }

    # Reserve a session slot first; do NOT launch if it would evict anyone.
    try:
        claim_browser_slot(
            control_room_url=config.control_room_url,
            username=config.auth.username,
        )
    except SessionBudgetExceeded as exc:
        return _error(
            tool=tool,
            error=str(exc),
            error_type="session_budget_exceeded",
            action_required="Close another Control Room session and retry.",
        )

    try:
        state = launch_window(
            control_room_url=config.control_room_url,
            username=config.auth.username,
            start_url=params.start_url,
        )
    except LaunchBudgetExceeded as exc:
        release_browser_slot(
            control_room_url=config.control_room_url,
            username=config.auth.username,
        )
        return _error(
            tool=tool,
            error=str(exc),
            error_type="launch_budget_exceeded",
            action_required="Wait for the cooldown window or use the existing window.",
        )
    except WindowLaunchError as exc:
        release_browser_slot(
            control_room_url=config.control_room_url,
            username=config.auth.username,
        )
        return _error(
            tool=tool,
            error=str(exc),
            error_type="window_launch_error",
            action_required="Check the Chromium install and the A360_BROWSER_BIN env var.",
        )

    # The slot was booked against *this* process before the launch. Re-stamp it
    # with Chromium's pid now that we have one — the window outlives the server,
    # and a slot carrying a dead server pid reads as free while the session
    # behind it is still live.
    bind_browser_slot(
        control_room_url=config.control_room_url,
        username=config.auth.username,
        pid=state.pid,
    )

    return {
        "success": True,
        "tool": tool,
        "already_open": False,
        "window": state.to_payload(),
        "note": (
            "Debug window opened. Sign in with your own credentials — the window "
            "keeps its OWN Control Room session, separate from the API."
        ),
    }


@register_tool(
    "close_debug_window",
    params=CloseDebugWindowParams,
    description="Close the shared debug window and release its session slot.",
)
def close_debug_window(
    config: ServerConfig, auth_manager: AuthManager, params: CloseDebugWindowParams
) -> dict:
    tool = "close_debug_window"
    try:
        from ..browser import close_window, find_window, release_browser_slot
        from ..browser.unsaved_guard import UnsavedInputBlocked, assert_safe_to_disrupt
    except ImportError as exc:
        return _error(
            tool=tool,
            error=f"Browser layer not installed: {exc}",
            error_type="browser_unavailable",
        )

    # Closing destroys whatever is on the page. Check for unsaved form input
    # against the *live* window before tearing it down. A dead or absent window
    # has nothing to lose, so the probe is skipped rather than run against None.
    if not params.force:
        existing = find_window(
            control_room_url=config.control_room_url,
            username=config.auth.username,
        )
        if existing is not None:
            try:
                assert_safe_to_disrupt(existing, action=tool)
            except UnsavedInputBlocked as exc:
                return _error(
                    tool=tool,
                    error=str(exc),
                    error_type="unsaved_input_blocked",
                    action_required="Save the form in the window, or retry with force=True.",
                )

    closed = close_window(
        control_room_url=config.control_room_url,
        username=config.auth.username,
    )
    release_browser_slot(
        control_room_url=config.control_room_url,
        username=config.auth.username,
    )
    return {"success": True, "tool": tool, "closed": closed}


@register_tool(
    "inspect_console",
    params=InspectConsoleParams,
    description="Capture console messages and page errors from the debug window.",
)
def inspect_console(
    config: ServerConfig, auth_manager: AuthManager, params: InspectConsoleParams
) -> dict:
    tool = "inspect_console"
    state, err = _resolve_window(config)
    if err is not None:
        return err
    try:
        from ..browser.inspect import (
            BrowserUnavailable,
            InspectError,
            NoDebugWindow,
        )
        from ..browser.inspect import inspect_console as _inspect
    except ImportError as exc:
        return _error(tool=tool, error=str(exc), error_type="browser_unavailable")
    try:
        return {
            "success": True,
            "tool": tool,
            **_inspect(state, duration_seconds=params.duration_seconds),
        }
    except NoDebugWindow as exc:
        return _error(tool=tool, error=str(exc), error_type="no_debug_window")
    except BrowserUnavailable as exc:
        return _error(
            tool=tool,
            error=str(exc),
            error_type="browser_unavailable",
            action_required="pip install 'a360-mcp[browser]' && playwright install chromium",
        )
    except InspectError as exc:
        return _error(tool=tool, error=str(exc), error_type="inspect_error")


@register_tool(
    "inspect_network",
    params=InspectNetworkParams,
    description="Capture XHR/fetch traffic from the debug window.",
)
def inspect_network(
    config: ServerConfig, auth_manager: AuthManager, params: InspectNetworkParams
) -> dict:
    tool = "inspect_network"
    state, err = _resolve_window(config)
    if err is not None:
        return err
    try:
        from ..browser.inspect import (
            BrowserUnavailable,
            InspectError,
            NoDebugWindow,
        )
        from ..browser.inspect import inspect_network as _inspect
    except ImportError as exc:
        return _error(tool=tool, error=str(exc), error_type="browser_unavailable")
    try:
        return {
            "success": True,
            "tool": tool,
            **_inspect(state, duration_seconds=params.duration_seconds),
        }
    except NoDebugWindow as exc:
        return _error(tool=tool, error=str(exc), error_type="no_debug_window")
    except BrowserUnavailable as exc:
        return _error(
            tool=tool,
            error=str(exc),
            error_type="browser_unavailable",
            action_required="pip install 'a360-mcp[browser]' && playwright install chromium",
        )
    except InspectError as exc:
        return _error(tool=tool, error=str(exc), error_type="inspect_error")


@register_tool(
    "screenshot_debug",
    params=ScreenshotDebugParams,
    description="Take a PNG screenshot of the debug window.",
)
def screenshot_debug(
    config: ServerConfig, auth_manager: AuthManager, params: ScreenshotDebugParams
) -> dict:
    tool = "screenshot_debug"
    state, err = _resolve_window(config)
    if err is not None:
        return err
    try:
        from ..browser.inspect import (
            BrowserUnavailable,
            InspectError,
            NoDebugWindow,
        )
        from ..browser.inspect import screenshot_debug as _screenshot
    except ImportError as exc:
        return _error(tool=tool, error=str(exc), error_type="browser_unavailable")
    try:
        return {
            "success": True,
            "tool": tool,
            **_screenshot(state, output_path=params.output_path, full_page=params.full_page),
        }
    except NoDebugWindow as exc:
        return _error(tool=tool, error=str(exc), error_type="no_debug_window")
    except BrowserUnavailable as exc:
        return _error(
            tool=tool,
            error=str(exc),
            error_type="browser_unavailable",
            action_required="pip install 'a360-mcp[browser]' && playwright install chromium",
        )
    except InspectError as exc:
        return _error(tool=tool, error=str(exc), error_type="inspect_error")


@register_tool(
    "get_computed_style",
    params=GetComputedStyleParams,
    description="Read computed CSS values for an element in the debug window.",
)
def get_computed_style(
    config: ServerConfig, auth_manager: AuthManager, params: GetComputedStyleParams
) -> dict:
    tool = "get_computed_style"
    state, err = _resolve_window(config)
    if err is not None:
        return err
    try:
        from ..browser.inspect import (
            BrowserUnavailable,
            InspectError,
            NoDebugWindow,
        )
        from ..browser.inspect import get_computed_style as _style
    except ImportError as exc:
        return _error(tool=tool, error=str(exc), error_type="browser_unavailable")
    try:
        return {
            "success": True,
            "tool": tool,
            **_style(state, selector=params.selector, properties=params.properties),
        }
    except NoDebugWindow as exc:
        return _error(tool=tool, error=str(exc), error_type="no_debug_window")
    except BrowserUnavailable as exc:
        return _error(
            tool=tool,
            error=str(exc),
            error_type="browser_unavailable",
            action_required="pip install 'a360-mcp[browser]' && playwright install chromium",
        )
    except InspectError as exc:
        return _error(tool=tool, error=str(exc), error_type="inspect_error")


__all__ = [
    "CloseDebugWindowParams",
    "GetComputedStyleParams",
    "InspectConsoleParams",
    "InspectNetworkParams",
    "OpenDebugWindowParams",
    "ScreenshotDebugParams",
    "close_debug_window",
    "get_computed_style",
    "inspect_console",
    "inspect_network",
    "open_debug_window",
    "screenshot_debug",
]
