"""Browser automation tools — thin wrappers around playwright-cli.

Each function calls ``playwright-cli`` as a subprocess and returns
the CLI output. These are registered as agent tools so the LLM can
call them directly without going through the shell ``execute`` tool
(which requires HITL approval for every command).

The browser session must be opened first with ``browser_open`` before
any other commands will work.
"""

from __future__ import annotations

import atexit
import os
import shutil
import subprocess
import tempfile
from pathlib import Path

# Shared constants
BROWSER_TOKEN_FILE = Path.home() / ".icarus" / "browser_token"

_browser_workdir: str | None = None
_playwright_cli_path: str | None = None
_cached_browser_env: dict[str, str] | None = None


def strip_token_prefix(token: str) -> str:
    """Strip ``KEY=`` prefix from a pasted extension token."""
    if "=" in token:
        return token.split("=", 1)[1]
    return token


def _get_workdir() -> str:
    """Get or create the temp working directory for playwright-cli."""
    global _browser_workdir  # noqa: PLW0603
    if _browser_workdir is None:
        _browser_workdir = tempfile.mkdtemp(prefix="icarus_browser_")
    return _browser_workdir


def _find_playwright_cli() -> str:
    """Find the playwright-cli binary.

    Detection order:
    1. ICARUS_PLAYWRIGHT_CLI env override
    2. Bundled node-runtime/ in Tauri resource dir (production)
    3. System PATH (dev mode / global install)
    """
    global _playwright_cli_path  # noqa: PLW0603
    if _playwright_cli_path is not None:
        return _playwright_cli_path

    # 1. Env override
    env_path = os.environ.get("ICARUS_PLAYWRIGHT_CLI")
    if env_path and Path(env_path).is_file():
        _playwright_cli_path = env_path
        return _playwright_cli_path

    # 2. Bundled node-runtime/ — check common locations
    #    In production: Tauri resource dir (ICARUS_RESOURCE_DIR)
    #    In dev: desktop/src-tauri/node-runtime/
    suffix = ".cmd" if os.name == "nt" else ""
    candidates = []

    # Tauri resource dir (set by Rust sidecar manager)
    res_dir = os.environ.get("ICARUS_RESOURCE_DIR")
    if res_dir:
        candidates.append(Path(res_dir) / "node-runtime" / f"playwright-cli{suffix}")

    # Dev mode: relative to repo root
    # Walk up from this file to find desktop/src-tauri/node-runtime/
    this_file = Path(__file__).resolve()
    for parent in this_file.parents:
        candidate = parent / "desktop" / "src-tauri" / "node-runtime" / f"playwright-cli{suffix}"
        if candidate.is_file():
            candidates.append(candidate)
            break

    for candidate in candidates:
        if candidate.is_file():
            _playwright_cli_path = str(candidate)
            return _playwright_cli_path

    # 3. System PATH fallback
    _playwright_cli_path = f"playwright-cli{suffix}" if suffix else "playwright-cli"
    return _playwright_cli_path


def _get_browser_env() -> dict[str, str]:
    """Build environment for playwright-cli with the extension token.

    Caches the result so repeated tool calls don't re-copy the full
    environment or re-read the token file on every invocation.
    """
    global _cached_browser_env  # noqa: PLW0603
    if _cached_browser_env is not None:
        return _cached_browser_env

    env = os.environ.copy()
    token = env.get("PLAYWRIGHT_MCP_EXTENSION_TOKEN")
    if not token:
        if BROWSER_TOKEN_FILE.exists():
            raw = BROWSER_TOKEN_FILE.read_text().strip()
            token = strip_token_prefix(raw) if raw else None
    if token:
        env["PLAYWRIGHT_MCP_EXTENSION_TOKEN"] = token
    _cached_browser_env = env
    return env


def invalidate_browser_env() -> None:
    """Clear cached env so the next tool call re-reads the token file."""
    global _cached_browser_env  # noqa: PLW0603
    _cached_browser_env = None


def _run_cli(*args: str, timeout: int = 30) -> str:
    """Run a playwright-cli command and return its output.

    Runs in a temp directory so artifacts don't pollute the user's workspace.

    Returns:
        Combined stdout from the CLI command, or an error message.
    """
    cli = _find_playwright_cli()
    cmd = [cli, *args]
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            env=_get_browser_env(),
            cwd=_get_workdir(),
        )
        output = result.stdout.strip()
        if result.returncode != 0 and result.stderr:
            output += f"\nError: {result.stderr.strip()}"
        return output or "(no output)"
    except subprocess.TimeoutExpired:
        return f"Error: Command timed out after {timeout}s"
    except FileNotFoundError:
        return f"Error: playwright-cli not found at '{cli}'. Run dev/setup-node-runtime.sh or install globally."


def _run_cli_with_snapshot(*args: str, timeout: int = 30) -> str:
    """Run a CLI command, inline snapshot content, then delete the artifact."""
    output = _run_cli(*args, timeout=timeout)
    workdir = Path(_get_workdir())
    for line in output.splitlines():
        if "[Snapshot](" in line:
            start = line.find("(")
            end = line.find(")", start + 1) if start != -1 else -1
            if start == -1 or end == -1:
                break
            rel_path = line[start + 1 : end]
            snapshot_path = workdir / rel_path
            try:
                content = snapshot_path.read_text()
                output += f"\n\n--- Snapshot ---\n{content}"
                snapshot_path.unlink(missing_ok=True)
            except OSError:
                pass
            break
    return output


def cleanup_browser_workdir() -> None:
    """Remove the temp working directory. Called on sidecar shutdown."""
    global _browser_workdir  # noqa: PLW0603
    if _browser_workdir and Path(_browser_workdir).exists():
        shutil.rmtree(_browser_workdir, ignore_errors=True)
        _browser_workdir = None


atexit.register(cleanup_browser_workdir)


# ── Session Management ─────────────────────────────────────────


def browser_open(url: str = "") -> str:
    """Open the browser and connect to the user's Chrome via the Playwright extension.

    This must be called before any other browser commands. It connects to
    the user's Chrome browser through the Playwright MCP Bridge extension,
    preserving their logged-in sessions, cookies, and extensions.

    Args:
        url: Optional URL to navigate to immediately after opening.

    Returns:
        Browser session info and initial page snapshot.
    """
    args = ["open", "--extension"]
    if url:
        args.append(url)
    return _run_cli_with_snapshot(*args, timeout=35)


def browser_close() -> str:
    """Close the browser session.

    Returns:
        Confirmation that the browser was closed.
    """
    return _run_cli("close")


# ── Navigation ─────────────────────────────────────────────────


def browser_goto(url: str) -> str:
    """Navigate to a URL in the browser.

    Args:
        url: The URL to navigate to (e.g. https://example.com).

    Returns:
        Page title, URL, and a snapshot of the page structure.
    """
    return _run_cli_with_snapshot("goto", url)


def browser_go_back() -> str:
    """Go back to the previous page in browser history.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("go-back")


def browser_go_forward() -> str:
    """Go forward to the next page in browser history.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("go-forward")


def browser_reload() -> str:
    """Reload the current page.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("reload")


# ── Page Reading ───────────────────────────────────────────────


def browser_snapshot() -> str:
    """Get the current page structure as an accessibility snapshot.

    Returns a YAML representation of all interactive elements on the page,
    each with a ref ID (like e3, e15) that you can use with click, fill,
    and other interaction commands.

    Always call this before interacting with a page to get current refs.

    Returns:
        Page URL, title, and YAML snapshot with element refs.
    """
    return _run_cli_with_snapshot("snapshot")


def browser_screenshot(ref: str = "") -> str:
    """Take a screenshot of the current page or a specific element.

    Use this for visual confirmation or when the snapshot doesn't
    capture the layout well (charts, images, complex dashboards).

    Args:
        ref: Optional element ref to screenshot (e.g. 'e5').
             If empty, screenshots the full page.

    Returns:
        Path to the saved screenshot file.
    """
    args = ["screenshot"]
    if ref:
        args.append(ref)
    return _run_cli(*args)


# ── Interaction ────────────────────────────────────────────────


def browser_click(ref: str) -> str:
    """Click an element on the page by its ref ID from the snapshot.

    Args:
        ref: Element ref from the snapshot (e.g. 'e3', 'e15').

    Returns:
        Updated page info and snapshot after clicking.
    """
    return _run_cli_with_snapshot("click", ref)


def browser_fill(ref: str, text: str) -> str:
    """Fill text into an input field, replacing any existing content.

    Use this for text inputs, textareas, and editable elements.
    This clears the field first, then types the new text.

    Args:
        ref: Element ref from the snapshot (e.g. 'e7').
        text: Text to fill into the field.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("fill", ref, text)


def browser_type(text: str) -> str:
    """Type text character by character into the currently focused element.

    Use this when you need to type into an element that is already focused,
    or for typing search queries and similar interactions.

    Args:
        text: Text to type.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("type", text)


def browser_select(ref: str, value: str) -> str:
    """Select an option in a dropdown by its value.

    Args:
        ref: Element ref of the select/dropdown (e.g. 'e9').
        value: The option value to select.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("select", ref, value)


def browser_check(ref: str) -> str:
    """Check a checkbox or radio button.

    Args:
        ref: Element ref from the snapshot.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("check", ref)


def browser_uncheck(ref: str) -> str:
    """Uncheck a checkbox.

    Args:
        ref: Element ref from the snapshot.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("uncheck", ref)


def browser_hover(ref: str) -> str:
    """Hover over an element to reveal tooltips or dropdown menus.

    Args:
        ref: Element ref from the snapshot.

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("hover", ref)


def browser_press(key: str) -> str:
    """Press a keyboard key.

    Args:
        key: Key to press (e.g. 'Enter', 'Tab', 'ArrowDown', 'Escape').

    Returns:
        Updated page info and snapshot.
    """
    return _run_cli_with_snapshot("press", key)


def browser_scroll(direction: str = "down") -> str:
    """Scroll the page up or down.

    Args:
        direction: 'up' or 'down' (default: 'down').

    Returns:
        Updated page info and snapshot.
    """
    dx, dy = (0, -500) if direction == "up" else (0, 500)
    return _run_cli_with_snapshot("mousewheel", str(dx), str(dy))


# ── Tabs ───────────────────────────────────────────────────────


def browser_tab_list() -> str:
    """List all open browser tabs.

    Returns:
        List of tabs with their index, title, and URL.
    """
    return _run_cli("tab-list")


def browser_tab_new(url: str = "") -> str:
    """Open a new browser tab.

    Args:
        url: Optional URL to open in the new tab.

    Returns:
        Info about the new tab.
    """
    args = ["tab-new"]
    if url:
        args.append(url)
    return _run_cli_with_snapshot(*args)


def browser_tab_select(index: int) -> str:
    """Switch to a browser tab by its index.

    Args:
        index: Tab index from tab_list (0-based).

    Returns:
        Updated page info and snapshot of the selected tab.
    """
    return _run_cli_with_snapshot("tab-select", str(index))


def browser_tab_close(index: int = -1) -> str:
    """Close a browser tab.

    Args:
        index: Tab index to close (-1 for current tab).

    Returns:
        Confirmation.
    """
    args = ["tab-close"]
    if index >= 0:
        args.append(str(index))
    return _run_cli(*args)


# ── JavaScript Evaluation ──────────────────────────────────────


def browser_eval(expression: str, ref: str = "") -> str:
    """Evaluate a JavaScript expression on the page or an element.

    Args:
        expression: JavaScript expression to evaluate.
        ref: Optional element ref to evaluate on.

    Returns:
        Result of the evaluation.
    """
    args = ["eval", expression]
    if ref:
        args.append(ref)
    return _run_cli(*args)


# ── Console & Network ──────────────────────────────────────────


def browser_console() -> str:
    """Get browser console messages (errors, warnings, logs).

    Returns:
        Console messages from the current page.
    """
    return _run_cli("console")


# ── Tool Collection ────────────────────────────────────────────


def find_playwright_cli() -> str:
    """Public wrapper for CLI discovery (used by sidecar status check)."""
    return _find_playwright_cli()


def is_browser_available() -> bool:
    """Return True if playwright-cli is installed and discoverable."""
    cli = _find_playwright_cli()
    p = Path(cli)
    return p.is_file() or bool(shutil.which(cli))


def create_browser_tools() -> list:
    """Return browser tools for agent registration.

    Returns an empty list when playwright-cli is not discoverable,
    so CLI/TUI agents don't expose tools that can never work.
    """
    if not is_browser_available():
        return []

    return [
        browser_open,
        browser_close,
        browser_goto,
        browser_go_back,
        browser_go_forward,
        browser_reload,
        browser_snapshot,
        browser_screenshot,
        browser_click,
        browser_fill,
        browser_type,
        browser_select,
        browser_check,
        browser_uncheck,
        browser_hover,
        browser_press,
        browser_scroll,
        browser_tab_list,
        browser_tab_new,
        browser_tab_select,
        browser_tab_close,
        browser_eval,
        browser_console,
    ]
