# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Browser Automation Skill

Control a web browser using Playwright for:
- Navigating websites
- Filling forms
- Downloading files
- Scraping content
- Taking screenshots
- Handling logins

Requires: playwright
Install: pip install playwright && playwright install chromium

Security:
    Cookies are encrypted at rest using Fernet symmetric encryption.
    If cryptography is not installed, cookies are stored with restricted permissions.
"""

import asyncio
import base64
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path

logger = logging.getLogger(__name__)

# Storage paths - use centralized paths


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.paths import (
        COOKIES_DIR,
        DATA_DIR,
        DOWNLOADS_DIR,
        SCREENSHOTS_DIR,
        ensure_dir,
    )
except ImportError:
    DATA_DIR = _get_data_dir()
    SCREENSHOTS_DIR = DATA_DIR / "screenshots"
    DOWNLOADS_DIR = DATA_DIR / "downloads"
    COOKIES_DIR = DATA_DIR / "browser_cookies"

    def ensure_dir(path):
        path.mkdir(parents=True, exist_ok=True)
        return path


BROWSER_STATE_FILE = DATA_DIR / "browser_state.json"

# Ensure directories exist with restricted permissions
ensure_dir(SCREENSHOTS_DIR)
ensure_dir(DOWNLOADS_DIR)
COOKIES_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)


# ============================================================
# COOKIE ENCRYPTION
# ============================================================


def _get_encryption_key() -> bytes:
    """
    Get or create encryption key for cookie storage.
    Key is stored in a file with restricted permissions.
    """
    key_file = COOKIES_DIR / ".cookie_key"

    if key_file.exists():
        return key_file.read_bytes()

    # Generate new key
    try:
        from cryptography.fernet import Fernet

        key = Fernet.generate_key()
    except ImportError:
        # Fallback: generate a random key manually
        import secrets

        key = base64.urlsafe_b64encode(secrets.token_bytes(32))

    # Save with restricted permissions
    key_file.write_bytes(key)
    try:
        os.chmod(key_file, 0o600)
    except OSError:
        pass  # Windows may not support chmod

    return key


def _encrypt_cookies(cookies: list) -> bytes:
    """Encrypt cookies for storage."""
    try:
        from cryptography.fernet import Fernet

        key = _get_encryption_key()
        f = Fernet(key)
        data = json.dumps(cookies).encode("utf-8")
        return f.encrypt(data)
    except ImportError:
        # Fallback: base64 encode (not secure, but better than plaintext)
        logger.debug("cryptography not installed - cookies stored with basic encoding only")
        data = json.dumps(cookies).encode("utf-8")
        return base64.b64encode(data)


def _decrypt_cookies(encrypted: bytes) -> list:
    """Decrypt stored cookies."""
    try:
        from cryptography.fernet import Fernet

        key = _get_encryption_key()
        f = Fernet(key)
        data = f.decrypt(encrypted)
        return json.loads(data.decode("utf-8"))
    except ImportError:
        # Fallback: base64 decode
        data = base64.b64decode(encrypted)
        return json.loads(data.decode("utf-8"))
    except Exception as e:
        logger.error(f"Failed to decrypt cookies: {e}")
        return []


# Global browser instance (reused for session persistence)
_browser = None
_context = None
_page = None


def _get_event_loop():
    """Get or create event loop."""
    try:
        loop = asyncio.get_event_loop()
        if loop.is_closed():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
    return loop


def _run_async(coro):
    """Run async function synchronously."""
    loop = _get_event_loop()
    return loop.run_until_complete(coro)


async def _get_browser():
    """Get or create browser instance."""
    global _browser, _context, _page

    if _browser is None:
        try:
            from playwright.async_api import async_playwright

            playwright = await async_playwright().start()

            _browser = await playwright.chromium.launch(
                headless=True,
                args=["--no-sandbox", "--disable-dev-shm-usage"],  # For Pi/container
            )

            # Create context with persistent cookies
            _context = await _browser.new_context(
                viewport={"width": 1280, "height": 720},
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            )

            # Load saved cookies if they exist (encrypted)
            cookies_file = COOKIES_DIR / "cookies.enc"
            legacy_file = COOKIES_DIR / "cookies.json"

            if cookies_file.exists():
                try:
                    encrypted = cookies_file.read_bytes()
                    cookies = _decrypt_cookies(encrypted)
                    if cookies:
                        await _context.add_cookies(cookies)
                        logger.info("Loaded encrypted cookies")
                except Exception as e:
                    logger.warning(f"Failed to load cookies: {e}")
            elif legacy_file.exists():
                # Migrate old unencrypted cookies
                try:
                    cookies = json.loads(legacy_file.read_text())
                    await _context.add_cookies(cookies)
                    logger.info("Loaded legacy cookies (will encrypt on next save)")
                    # Delete the old file after successful load
                    legacy_file.unlink()
                except Exception as e:
                    logger.warning(f"Failed to load legacy cookies: {e}")

            _page = await _context.new_page()

            # Set default timeout
            _page.set_default_timeout(30000)

        except ImportError:
            from familiar.core.deps import BROWSER_PACKAGES, ensure_packages

            ok, _ = ensure_packages(BROWSER_PACKAGES)
            if ok:
                import subprocess as _sp

                _sp.run(
                    [sys.executable, "-m", "playwright", "install", "chromium"],
                    capture_output=True,
                    timeout=300,
                )
                # Retry after install
                return await _get_browser()
            raise RuntimeError("Browser automation setup failed. Check server logs.")

    return _page


async def _save_cookies():
    """Save cookies for session persistence (encrypted)."""
    # (read-only access to _context)
    if _context:
        try:
            cookies = await _context.cookies()
            if cookies:
                encrypted = _encrypt_cookies(cookies)
                cookies_file = COOKIES_DIR / "cookies.enc"
                cookies_file.write_bytes(encrypted)
                # Set restrictive permissions
                try:
                    os.chmod(cookies_file, 0o600)
                except OSError:
                    pass  # Windows may not support chmod
                logger.debug("Saved encrypted cookies")
        except Exception as e:
            logger.error(f"Failed to save cookies: {e}")


async def _close_browser():
    """Close browser instance."""
    global _browser, _context, _page

    if _context:
        await _save_cookies()

    if _browser:
        await _browser.close()
        _browser = None
        _context = None
        _page = None


# === Browser Actions ===


async def _navigate(url: str) -> dict:
    """Navigate to a URL."""
    page = await _get_browser()

    try:
        response = await page.goto(url, wait_until="networkidle")
        await _save_cookies()

        return {
            "success": True,
            "url": page.url,
            "title": await page.title(),
            "status": response.status if response else None,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _get_page_content() -> dict:
    """Get current page content."""
    page = await _get_browser()

    try:
        title = await page.title()
        url = page.url

        # Get text content (cleaned up)
        text = await page.evaluate("""() => {
            // Remove script and style elements
            const scripts = document.querySelectorAll('script, style, noscript');
            scripts.forEach(s => s.remove());

            // Get text
            return document.body.innerText;
        }""")

        # Truncate if too long
        if len(text) > 10000:
            text = text[:10000] + "\n... (truncated)"

        return {"success": True, "url": url, "title": title, "content": text}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _screenshot(name: str = None) -> dict:
    """Take a screenshot."""
    page = await _get_browser()

    try:
        if not name:
            name = datetime.now().strftime("%Y%m%d_%H%M%S")

        filename = f"{name}.png"
        filepath = SCREENSHOTS_DIR / filename

        await page.screenshot(path=str(filepath), full_page=False)

        return {"success": True, "path": str(filepath), "url": page.url}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _click(selector: str) -> dict:
    """Click an element."""
    page = await _get_browser()

    try:
        await page.click(selector, timeout=10000)
        await page.wait_for_load_state("networkidle")
        await _save_cookies()

        return {"success": True, "clicked": selector, "new_url": page.url}
    except Exception as e:
        return {"success": False, "error": str(e), "selector": selector}


async def _fill_field(selector: str, value: str) -> dict:
    """Fill a form field."""
    page = await _get_browser()

    try:
        await page.fill(selector, value, timeout=10000)
        return {"success": True, "filled": selector}
    except Exception as e:
        return {"success": False, "error": str(e), "selector": selector}


async def _type_text(selector: str, text: str) -> dict:
    """Type text (character by character, for JS-heavy inputs)."""
    page = await _get_browser()

    try:
        await page.type(selector, text, delay=50)
        return {"success": True, "typed": selector}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _select_option(selector: str, value: str) -> dict:
    """Select dropdown option."""
    page = await _get_browser()

    try:
        await page.select_option(selector, value)
        return {"success": True, "selected": value}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _get_elements(selector: str) -> dict:
    """Get info about elements matching selector."""
    page = await _get_browser()

    try:
        elements = await page.query_selector_all(selector)

        results = []
        for i, el in enumerate(elements[:20]):  # Limit to 20
            text = await el.inner_text()
            tag = await el.evaluate("el => el.tagName")
            href = await el.get_attribute("href")

            results.append(
                {"index": i, "tag": tag, "text": text[:100] if text else "", "href": href}
            )

        return {"success": True, "count": len(elements), "elements": results}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _wait_for(selector: str, timeout: int = 10000) -> dict:
    """Wait for element to appear."""
    page = await _get_browser()

    try:
        await page.wait_for_selector(selector, timeout=timeout)
        return {"success": True, "found": selector}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _download_file(url: str = None, click_selector: str = None, filename: str = None) -> dict:
    """Download a file (either by URL or by clicking a download link)."""
    page = await _get_browser()

    try:
        if click_selector:
            # Wait for download triggered by click
            async with page.expect_download() as download_info:
                await page.click(click_selector)

            download = await download_info.value

            if not filename:
                filename = download.suggested_filename

            filepath = DOWNLOADS_DIR / filename
            await download.save_as(str(filepath))

            return {"success": True, "path": str(filepath), "filename": filename}

        elif url:
            # Direct download via new request
            import urllib.request

            if not filename:
                filename = url.split("/")[-1].split("?")[0] or "download"

            filepath = DOWNLOADS_DIR / filename
            urllib.request.urlretrieve(url, filepath)

            return {"success": True, "path": str(filepath), "filename": filename}

        else:
            return {"success": False, "error": "Provide url or click_selector"}

    except Exception as e:
        return {"success": False, "error": str(e)}


async def _login(
    url: str,
    username_selector: str,
    password_selector: str,
    submit_selector: str,
    username: str,
    password: str,
) -> dict:
    """Perform a login."""
    page = await _get_browser()

    try:
        # Navigate to login page
        await page.goto(url, wait_until="networkidle")

        # Fill credentials
        await page.fill(username_selector, username)
        await page.fill(password_selector, password)

        # Submit
        await page.click(submit_selector)
        await page.wait_for_load_state("networkidle")

        # Save cookies for future sessions
        await _save_cookies()

        new_url = page.url
        title = await page.title()

        # Check if we're still on login page (might indicate failure)
        still_on_login = url in new_url or "login" in new_url.lower()

        return {
            "success": not still_on_login,
            "url": new_url,
            "title": title,
            "note": "May still be on login page - check for errors"
            if still_on_login
            else "Login appears successful",
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _extract_table() -> dict:
    """Extract table data from current page."""
    page = await _get_browser()

    try:
        tables = await page.evaluate("""() => {
            const tables = document.querySelectorAll('table');
            const results = [];

            tables.forEach((table, tableIndex) => {
                const rows = [];
                table.querySelectorAll('tr').forEach(tr => {
                    const cells = [];
                    tr.querySelectorAll('td, th').forEach(cell => {
                        cells.push(cell.innerText.trim());
                    });
                    if (cells.length > 0) rows.push(cells);
                });
                if (rows.length > 0) {
                    results.push({index: tableIndex, rows: rows.slice(0, 50)});
                }
            });

            return results;
        }""")

        return {"success": True, "tables": tables, "count": len(tables)}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def _find_links(pattern: str = None) -> dict:
    """Find links on page, optionally filtered by pattern."""
    page = await _get_browser()

    try:
        links = await page.evaluate("""() => {
            const links = [];
            document.querySelectorAll('a[href]').forEach(a => {
                links.push({
                    text: a.innerText.trim().slice(0, 100),
                    href: a.href
                });
            });
            return links;
        }""")

        # Filter if pattern provided
        if pattern:
            pattern_lower = pattern.lower()
            links = [
                link
                for link in links
                if pattern_lower in link["text"].lower() or pattern_lower in link["href"].lower()
            ]

        return {
            "success": True,
            "links": links[:50],  # Limit results
            "count": len(links),
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


# === Synchronous Wrappers (for tool handlers) ===


def navigate(data: dict) -> str:
    """Navigate to a URL."""
    url = data.get("url", "")
    if not url:
        return "Please provide a URL"

    if not url.startswith(("http://", "https://")):
        url = "https://" + url

    result = _run_async(_navigate(url))

    if result["success"]:
        return f"✓ Navigated to: {result['title']}\nURL: {result['url']}"
    else:
        return f"❌ Navigation failed: {result['error']}"


def get_page_content(data: dict) -> str:
    """Get current page content."""
    result = _run_async(_get_page_content())

    if result["success"]:
        return f"📄 {result['title']}\nURL: {result['url']}\n\n{result['content']}"
    else:
        return f"❌ Error: {result['error']}"


def take_screenshot(data: dict) -> str:
    """Take a screenshot."""
    name = data.get("name")
    result = _run_async(_screenshot(name))

    if result["success"]:
        return f"📸 Screenshot saved: {result['path']}"
    else:
        return f"❌ Error: {result['error']}"


def click_element(data: dict) -> str:
    """Click an element."""
    selector = data.get("selector", "")
    if not selector:
        return "Please provide a CSS selector"

    result = _run_async(_click(selector))

    if result["success"]:
        return f"✓ Clicked: {selector}\nNow at: {result['new_url']}"
    else:
        return f"❌ Could not click '{selector}': {result['error']}"


def fill_form_field(data: dict) -> str:
    """Fill a form field."""
    selector = data.get("selector", "")
    value = data.get("value", "")

    if not selector:
        return "Please provide a CSS selector"

    result = _run_async(_fill_field(selector, value))

    if result["success"]:
        return f"✓ Filled {selector}"
    else:
        return f"❌ Could not fill '{selector}': {result['error']}"


def find_elements(data: dict) -> str:
    """Find elements on page."""
    selector = data.get("selector", "")
    if not selector:
        return "Please provide a CSS selector"

    result = _run_async(_get_elements(selector))

    if result["success"]:
        if result["count"] == 0:
            return f"No elements found matching '{selector}'"

        lines = [f"Found {result['count']} elements:\n"]
        for el in result["elements"]:
            text = el["text"][:50] + "..." if len(el["text"]) > 50 else el["text"]
            href = f" → {el['href']}" if el["href"] else ""
            lines.append(f"  [{el['index']}] <{el['tag']}> {text}{href}")

        return "\n".join(lines)
    else:
        return f"❌ Error: {result['error']}"


def download_file(data: dict) -> str:
    """Download a file."""
    url = data.get("url")
    click_selector = data.get("click_selector")
    filename = data.get("filename")

    result = _run_async(_download_file(url, click_selector, filename))

    if result["success"]:
        return f"✓ Downloaded: {result['path']}"
    else:
        return f"❌ Download failed: {result['error']}"


def login_to_site(data: dict) -> str:
    """Log into a website."""
    url = data.get("url", "")
    username = data.get("username", "")
    password = data.get("password", "")
    username_selector = data.get(
        "username_selector",
        "input[type='email'], input[name='username'], input[name='email'], #username, #email",
    )
    password_selector = data.get("password_selector", "input[type='password'], #password")
    submit_selector = data.get(
        "submit_selector",
        "button[type='submit'], input[type='submit'], button:has-text('Log in'), button:has-text('Sign in')",
    )

    if not url or not username or not password:
        return "Please provide url, username, and password"

    result = _run_async(
        _login(url, username_selector, password_selector, submit_selector, username, password)
    )

    if result["success"]:
        return f"✓ Logged in successfully\nNow at: {result['title']}\n{result['url']}"
    else:
        if "error" in result:
            return f"❌ Login failed: {result['error']}"
        else:
            return f"⚠️ Login may have failed - still at: {result['url']}\n{result.get('note', '')}"


def extract_table_data(data: dict) -> str:
    """Extract table data from page."""
    result = _run_async(_extract_table())

    if result["success"]:
        if result["count"] == 0:
            return "No tables found on page"

        lines = [f"Found {result['count']} table(s):\n"]

        for table in result["tables"]:
            lines.append(f"\n--- Table {table['index']} ---")
            for row in table["rows"][:10]:  # Show first 10 rows
                lines.append("  " + " | ".join(str(cell)[:30] for cell in row))
            if len(table["rows"]) > 10:
                lines.append(f"  ... ({len(table['rows'])} rows total)")

        return "\n".join(lines)
    else:
        return f"❌ Error: {result['error']}"


def find_links_on_page(data: dict) -> str:
    """Find links on page."""
    pattern = data.get("pattern")
    result = _run_async(_find_links(pattern))

    if result["success"]:
        if result["count"] == 0:
            return "No links found" + (f" matching '{pattern}'" if pattern else "")

        lines = [
            f"Found {result['count']} links"
            + (f" matching '{pattern}':" if pattern else ":")
            + "\n"
        ]

        for link in result["links"]:
            text = link["text"][:40] or "(no text)"
            lines.append(f"  • {text}")
            lines.append(f"    {link['href']}")

        return "\n".join(lines)
    else:
        return f"❌ Error: {result['error']}"


def close_browser(data: dict) -> str:
    """Close browser and save session."""
    _run_async(_close_browser())
    return "✓ Browser closed, session saved"


# Tool definitions
TOOLS = [
    {
        "name": "browser_navigate",
        "description": (
            "Navigate the Playwright browser to a specific known URL. "
            "Use this when you have an exact URL to visit, NOT for general web searches. "
            "For searching the web by query, use the web_search tool instead. "
            "Returns the page title, final URL, and HTTP status code."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"url": {"type": "string", "description": "URL to navigate to"}},
            "required": ["url"],
        },
        "handler": navigate,
        "category": "browser",
    },
    {
        "name": "browser_read_page",
        "description": (
            "Get the cleaned text content of the page currently open in the browser. "
            "Use this after navigating to a URL to read the page body. "
            "Scripts, styles, and noscript elements are stripped; content is truncated at 10,000 characters."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": get_page_content,
        "category": "browser",
    },
    {
        "name": "browser_screenshot",
        "description": (
            "Take a PNG screenshot of the current browser viewport. "
            "Use this to capture visual state of a page for the user or for debugging. "
            "Saved to ~/.familiar/data/screenshots/ with a timestamped or custom filename."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Optional name for the screenshot file"}
            },
        },
        "handler": take_screenshot,
        "category": "browser",
    },
    {
        "name": "browser_click",
        "description": (
            "Click an element on the current browser page identified by a CSS selector. "
            "Use this to interact with buttons, links, or other clickable elements. "
            "Waits for network idle after clicking and returns the new URL."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "selector": {
                    "type": "string",
                    "description": "CSS selector for element to click (e.g., 'button.submit', '#login-btn', 'a:has-text(\"Download\")')",
                }
            },
            "required": ["selector"],
        },
        "handler": click_element,
        "category": "browser",
    },
    {
        "name": "browser_fill",
        "description": (
            "Fill a form input field with text using a CSS selector. "
            "Use this for text inputs, textareas, and other fillable form elements. "
            "Clears existing content before filling; use browser_click for submit buttons afterward."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "selector": {"type": "string", "description": "CSS selector for input field"},
                "value": {"type": "string", "description": "Text to enter"},
            },
            "required": ["selector", "value"],
        },
        "handler": fill_form_field,
        "category": "browser",
    },
    {
        "name": "browser_find_elements",
        "description": (
            "Find elements on the current page matching a CSS selector and return their details. "
            "Use this to discover available buttons, links, form fields, or other elements. "
            "Returns up to 20 matches with tag name, text content, and href attributes."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "selector": {
                    "type": "string",
                    "description": "CSS selector (e.g., 'button', 'a', 'input', '.classname', '#id')",
                }
            },
            "required": ["selector"],
        },
        "handler": find_elements,
        "category": "browser",
    },
    {
        "name": "browser_find_links",
        "description": (
            "Find all links on the page currently open in the browser, optionally filtered by a "
            "text or URL pattern. "
            "Use this to discover navigation targets on the current page. "
            "NOT for searching the web -- use web_search instead. Returns up to 50 links."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {
                    "type": "string",
                    "description": "Optional text to filter links by (searches link text and URL)",
                }
            },
        },
        "handler": find_links_on_page,
        "category": "browser",
    },
    {
        "name": "browser_download",
        "description": (
            "Download a file either by direct URL or by clicking a download link on the current page. "
            "Use this when the user needs to save a file from the web. "
            "Files are saved to ~/.familiar/data/downloads/ with the suggested or custom filename."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "Direct URL to download"},
                "click_selector": {
                    "type": "string",
                    "description": "CSS selector for download button/link to click",
                },
                "filename": {"type": "string", "description": "Optional filename to save as"},
            },
        },
        "handler": download_file,
        "category": "browser",
    },
    {
        "name": "browser_login",
        "description": (
            "Log into a website by filling username/password fields and clicking submit. "
            "Saves encrypted session cookies for future browser sessions. "
            "Auto-detects common login field selectors if not provided. "
            "Returns the post-login page title and URL to verify success."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "Login page URL"},
                "username": {"type": "string", "description": "Username or email"},
                "password": {"type": "string", "description": "Password"},
                "username_selector": {
                    "type": "string",
                    "description": "CSS selector for username field (auto-detected if not provided)",
                },
                "password_selector": {
                    "type": "string",
                    "description": "CSS selector for password field (auto-detected if not provided)",
                },
                "submit_selector": {
                    "type": "string",
                    "description": "CSS selector for submit button (auto-detected if not provided)",
                },
            },
            "required": ["url", "username", "password"],
        },
        "handler": login_to_site,
        "category": "browser",
    },
    {
        "name": "browser_extract_table",
        "description": (
            "Extract HTML table data from the current browser page as structured rows. "
            "Use this when a page contains tabular data you need to read or process. "
            "Returns up to 50 rows per table and reports the total number of tables found."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": extract_table_data,
        "category": "browser",
    },
    {
        "name": "browser_close",
        "description": (
            "Close the Playwright browser instance and save encrypted session cookies. "
            "Use this when browser interaction is complete to free resources. "
            "Cookies are persisted so the next browser_navigate call can resume the session."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": close_browser,
        "category": "browser",
    },
]
