"""
FrankenClaw — browser_task tool

Real-time browser automation using agent-browser (Vercel Labs). All actions
happen in ONE persistent, visible browser window. The user watches the agent
click, fill, navigate — like watching someone use a computer.

Session model:
  - 'open' starts a named session with a visible Chromium window
  - All subsequent actions (click, fill, snapshot, etc.) reuse that window
  - 'close' ends the session and closes the window
  - Default session name: 'default'. Multiple sessions supported.

The agent-browser daemon keeps the browser alive between calls. The --session
flag ties every CLI invocation to the same browser instance.
"""

import glob
import json
import os
import shutil
import subprocess

from config import load_config

DEFAULT_SESSION = "default"


def _find_binary() -> str:
    path = shutil.which("agent-browser")
    if not path:
        raise FileNotFoundError(
            "agent-browser not found on PATH. Install with: npm install -g agent-browser"
        )
    return path


def _build_base_args(cfg: dict, session: str, visible: bool) -> list[str]:
    """Build CLI flags: session, visibility, engine, sandbox."""
    args = ["--session", session]
    if visible:
        args.append("--headed")
    engine = cfg.get("browser_engine", "default")
    if engine and engine != "default":
        args += ["--engine", engine]
    executable = cfg.get("browser_executable")
    if executable:
        args += ["--executable-path", executable]
    # --no-sandbox required: MCP subprocess runs under stripped env without
    # Chrome's user namespace sandbox support (desktop session, not containerized)
    args += ["--args", "--no-sandbox"]
    return args


def _browser_env() -> dict:
    """Build environment for subprocess — ensures X11 vars are set for headed mode.

    MCP clients spawn FrankenClaw as a subprocess which inherits a stripped
    environment. DISPLAY and XAUTHORITY are often missing. We detect the active
    X session and inject the correct values so Chrome can open a visible window.
    """
    env = os.environ.copy()

    if "DISPLAY" not in env:
        # Detect from the active GNOME/X session
        display = _detect_x_display()
        env["DISPLAY"] = display or ":1"

    if "XAUTHORITY" not in env:
        # GDM puts it here; fallback to ~/.Xauthority
        gdm_xauth = f"/run/user/{os.getuid()}/gdm/Xauthority"
        home_xauth = os.path.expanduser("~/.Xauthority")
        if os.path.exists(gdm_xauth):
            env["XAUTHORITY"] = gdm_xauth
        elif os.path.exists(home_xauth):
            env["XAUTHORITY"] = home_xauth

    if "XDG_RUNTIME_DIR" not in env:
        env["XDG_RUNTIME_DIR"] = f"/run/user/{os.getuid()}"

    return env


def _detect_x_display() -> str:
    """Detect the DISPLAY of the active graphical session."""
    try:
        # Ask loginctl for the active session's display
        result = subprocess.run(
            ["loginctl", "show-session", "auto", "-p", "Display", "--value"],
            capture_output=True, text=True, timeout=3,
        )
        display = result.stdout.strip()
        if display:
            return f":{display}" if not display.startswith(":") else display
    except Exception:
        pass

    # Fallback: scan /tmp/.X*-lock files
    try:
        locks = sorted(glob.glob("/tmp/.X*-lock"))
        if locks:
            # /tmp/.X1-lock → :1
            num = locks[-1].split(".X")[1].split("-")[0]
            return f":{num}"
    except Exception:
        pass

    return ":1"


def _run(binary: str, base_args: list[str], cmd_args: list[str], timeout: int) -> dict:
    """Run an agent-browser command and return parsed JSON."""
    full_cmd = [binary] + base_args + cmd_args + ["--json"]
    result = subprocess.run(
        full_cmd,
        capture_output=True,
        text=True,
        timeout=timeout,
        env=_browser_env(),
    )
    try:
        return json.loads(result.stdout)
    except (json.JSONDecodeError, ValueError):
        if result.returncode != 0:
            stderr = result.stderr.strip() or result.stdout.strip()
            return {"success": False, "data": None, "error": f"Exit {result.returncode}: {stderr}"}
        return {"success": True, "data": result.stdout.strip(), "error": None}


async def browser_task(
    task: str,
    url: str = "",
    action: str = "open",
    ref: str = "",
    session: str = "",
    visible: bool = True,
) -> str:
    """
    Control a real browser window on screen. All actions happen in one persistent
    window the user can watch in real time. The agent clicks, fills, navigates — the user sees it.

    Actions:
      open       — Open a URL in a visible browser window. Starts the session.
      snapshot   — Get the page's accessibility tree with element refs (@e1, @e2, etc.)
                   for identifying what to click or fill.
      click      — Click an element by ref (e.g. ref='@e2' from a snapshot).
      fill       — Fill a form field by ref. Put the text to type in 'task'.
      type       — Type text with real keystrokes (no element ref needed). Text goes in 'task'.
      press      — Press a key (Enter, Tab, Escape, etc.). Key name goes in 'task'.
      screenshot — Capture the current page to /tmp/frankenclaw-screenshot.png.
      eval       — Run JavaScript on the page. Put the JS expression in 'task'.
      scroll     — Scroll the page. Direction in 'task' (up, down, left, right).
      back       — Go back one page.
      forward    — Go forward one page.
      wait       — Wait for page to settle. Put milliseconds in 'task' (default: 3000).
      close      — Close the browser window and end the session.
      sessions   — List all active browser sessions.

    Args:
        task: Text to type (fill/type), key to press (press), JS expression (eval),
              scroll direction (scroll), wait ms (wait), or description of intent.
        url: Target URL (required for open).
        action: The browser action to perform (see list above).
        ref: Element ref from a snapshot (e.g. '@e2') for click/fill actions.
        session: Session name (default: 'default'). Use different names for parallel sessions.
        visible: If true (default), browser window is visible on screen. Set false for headless.

    Returns:
        JSON with result, action, session, and success fields.
    """
    cfg = load_config()
    timeout = cfg.get("browser_timeout", 30)
    binary = _find_binary()
    sess = session or DEFAULT_SESSION
    base_args = _build_base_args(cfg, sess, visible)

    try:
        if action == "open":
            if not url:
                return _result(False, "Error: 'open' requires a url.", action, sess)
            resp = _run(binary, base_args, ["open", url], timeout)

        elif action == "snapshot":
            resp = _run(binary, base_args, ["snapshot", "--compact"], timeout)

        elif action == "click":
            if not ref:
                return _result(False, "Error: 'click' requires a ref (e.g. '@e2').", action, sess)
            resp = _run(binary, base_args, ["click", ref], timeout)

        elif action == "fill":
            if not ref:
                return _result(False, "Error: 'fill' requires a ref (e.g. '@e3').", action, sess)
            resp = _run(binary, base_args, ["fill", ref, task], timeout)

        elif action == "type":
            resp = _run(binary, base_args, ["keyboard", "type", task], timeout)

        elif action == "press":
            key = task or "Enter"
            resp = _run(binary, base_args, ["press", key], timeout)

        elif action == "screenshot":
            path = "/tmp/frankenclaw-screenshot.png"
            resp = _run(binary, base_args, ["screenshot", path], timeout)
            if resp.get("success"):
                resp["data"] = {"path": path, "message": f"Screenshot saved to {path}"}

        elif action == "eval":
            resp = _run(binary, base_args, ["eval", task], timeout)

        elif action == "scroll":
            direction = task.lower() if task else "down"
            resp = _run(binary, base_args, ["scroll", direction], timeout)

        elif action == "back":
            resp = _run(binary, base_args, ["back"], timeout)

        elif action == "forward":
            resp = _run(binary, base_args, ["forward"], timeout)

        elif action == "wait":
            ms = task if task and task.isdigit() else "3000"
            resp = _run(binary, base_args, ["wait", ms], timeout)

        elif action == "close":
            resp = _run(binary, base_args, ["close"], timeout)

        elif action == "sessions":
            result = subprocess.run(
                [binary, "session", "list"],
                capture_output=True, text=True, timeout=10,
                env=_browser_env(),
            )
            return _result(True, result.stdout.strip(), action, sess)

        else:
            return _result(
                False,
                f"Unknown action '{action}'. Use: open, snapshot, click, fill, type, "
                f"press, screenshot, eval, scroll, back, forward, wait, close, sessions.",
                action, sess,
            )

        return _result(
            resp.get("success", False),
            resp.get("data") if resp.get("success") else resp.get("error", "Unknown error"),
            action, sess, url=url, ref=ref,
        )

    except subprocess.TimeoutExpired:
        return _result(False, f"Timed out after {timeout}s.", action, sess, url=url)
    except Exception as e:
        return _result(False, f"Failed: {e}", action, sess, url=url)


def _result(success: bool, data, action: str, session: str, url: str = "", ref: str = "") -> str:
    out = {
        "success": success,
        "result": data,
        "action": action,
        "session": session,
    }
    if url:
        out["url"] = url
    if ref:
        out["ref"] = ref
    return json.dumps(out, indent=2, default=str)
