"""Browser automation tool using Playwright.

Provides interactive browser control for the agent: navigation, clicking,
typing, form filling, screenshots, content extraction, and JavaScript execution.
"""

import asyncio
import ipaddress
import json
import platform
import time
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from loguru import logger

from flowly.agent.tools.base import Tool

# ---------------------------------------------------------------------------
# SSRF protection
# ---------------------------------------------------------------------------

_BLOCKED_HOSTS = frozenset({
    "localhost",
    "metadata.google.internal",
    "metadata.google",
    "169.254.169.254",
    "metadata",
})

_BROWSER_ARGS = [
    "--disable-dev-shm-usage",
    "--no-sandbox",
    "--disable-gpu",
    "--disable-extensions",
    "--disable-background-networking",
    "--disable-default-apps",
    "--disable-sync",
    "--disable-translate",
    "--no-first-run",
]


def _find_chromium_browsers() -> list[str]:
    """Discover Chromium-based browsers installed on the system."""
    import shutil

    candidates: list[str] = []
    system = platform.system().lower()

    if system == "darwin":
        mac_apps = [
            "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
            "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
            "/Applications/Arc.app/Contents/MacOS/Arc",
            "/Applications/Chromium.app/Contents/MacOS/Chromium",
            "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
            "/Applications/Opera.app/Contents/MacOS/Opera",
            "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
        ]
        candidates.extend(p for p in mac_apps if Path(p).exists())
    elif system == "linux":
        linux_bins = [
            "brave-browser", "brave", "google-chrome", "google-chrome-stable",
            "chromium-browser", "chromium", "vivaldi", "opera",
            "microsoft-edge", "microsoft-edge-stable",
        ]
        for b in linux_bins:
            found = shutil.which(b)
            if found:
                candidates.append(found)
    elif system == "windows":
        import os
        local = os.environ.get("LOCALAPPDATA", "")
        prog = os.environ.get("PROGRAMFILES", "C:\\Program Files")
        prog86 = os.environ.get("PROGRAMFILES(X86)", "C:\\Program Files (x86)")
        win_paths = [
            f"{local}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
            f"{prog}\\Google\\Chrome\\Application\\chrome.exe",
            f"{prog86}\\Google\\Chrome\\Application\\chrome.exe",
            f"{prog}\\Microsoft\\Edge\\Application\\msedge.exe",
            f"{local}\\Vivaldi\\Application\\vivaldi.exe",
        ]
        candidates.extend(p for p in win_paths if Path(p).exists())

    return candidates


def _is_ssrf_target(url: str) -> bool:
    """Return True if *url* targets a private, loopback, or metadata address."""
    parsed = urlparse(url)
    hostname = (parsed.hostname or "").lower()

    if not hostname:
        return True

    if hostname in _BLOCKED_HOSTS:
        return True

    # Block non-HTTP schemes
    if parsed.scheme not in ("http", "https"):
        return True

    # Block credentials in URL
    if parsed.username or parsed.password:
        return True

    try:
        addr = ipaddress.ip_address(hostname)
        return (
            addr.is_private
            or addr.is_loopback
            or addr.is_link_local
            or addr.is_reserved
            or addr.is_multicast
        )
    except ValueError:
        pass  # Not an IP literal — allow DNS names

    return False


# ---------------------------------------------------------------------------
# Browser session manager
# ---------------------------------------------------------------------------

class _BrowserSession:
    """Manages a singleton Playwright browser with inactivity timeout."""

    def __init__(self, config: Any):
        self._config = config
        self._playwright: Any | None = None
        self._browser: Any | None = None
        self._context: Any | None = None
        self._page: Any | None = None
        self._last_activity: float = 0
        self._lock = asyncio.Lock()
        self._watchdog_task: asyncio.Task | None = None

    # -- public --------------------------------------------------------------

    async def get_page(self) -> Any:
        """Return the active page, launching browser if needed."""
        async with self._lock:
            if self._page and not self._page.is_closed():
                self._touch()
                return self._page
            await self._launch()
            assert self._page is not None
            return self._page

    @property
    def is_active(self) -> bool:
        return self._browser is not None and self._browser.is_connected()

    async def close(self) -> None:
        """Tear down everything: page → context → browser → playwright."""
        if self._watchdog_task and not self._watchdog_task.done():
            self._watchdog_task.cancel()
            self._watchdog_task = None

        for resource, name in [
            (self._page, "page"),
            (self._context, "context"),
            (self._browser, "browser"),
        ]:
            if resource is not None:
                try:
                    await resource.close()
                except Exception as exc:
                    logger.debug(f"Browser {name} close error: {exc}")

        self._page = self._context = self._browser = None

        if self._playwright is not None:
            try:
                await self._playwright.stop()
            except Exception as exc:
                logger.debug(f"Playwright stop error: {exc}")
            self._playwright = None

        logger.info("Browser session closed")

    # -- private -------------------------------------------------------------

    def _touch(self) -> None:
        self._last_activity = time.monotonic()

    async def _launch(self) -> None:
        """Launch Playwright with system browser, fallback to bundled Chromium."""
        try:
            from playwright.async_api import async_playwright
        except ImportError:
            raise RuntimeError(
                "playwright is not installed. "
                "Run: pip install playwright && playwright install chromium"
            )

        self._playwright = await async_playwright().start()

        # 1. Try Chromium-based browsers via executable_path (Brave, Arc, etc.)
        for path in _find_chromium_browsers():
            try:
                self._browser = await self._playwright.chromium.launch(
                    headless=self._config.headless,
                    executable_path=path,
                    args=_BROWSER_ARGS,
                )
                logger.info("Browser launched via {}", path)
                break
            except Exception:
                continue
        else:
            # 2. Try Playwright channels (Chrome, Edge)
            for channel in ("chrome", "msedge"):
                try:
                    self._browser = await self._playwright.chromium.launch(
                        headless=self._config.headless,
                        channel=channel,
                        args=_BROWSER_ARGS,
                    )
                    logger.info("Browser launched via channel: {}", channel)
                    break
                except Exception:
                    continue
            else:
                # 3. Try bundled Chromium (playwright install chromium)
                try:
                    self._browser = await self._playwright.chromium.launch(
                        headless=self._config.headless,
                        args=_BROWSER_ARGS,
                    )
                    logger.info("Browser launched via bundled Chromium")
                except Exception as exc:
                    await self._playwright.stop()
                    self._playwright = None
                    raise RuntimeError(
                        f"No browser found: {exc}. "
                        "Install Chrome, Brave, Edge, or run: playwright install chromium"
                    ) from exc

        self._context = await self._browser.new_context(
            viewport={"width": 1280, "height": 720},
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/131.0.0.0 Safari/537.36"
            ),
        )

        # Block heavy resource types for performance
        blocked = set(self._config.block_resources)
        if blocked:
            async def _route_handler(route: Any) -> None:
                if route.request.resource_type in blocked:
                    await route.abort()
                else:
                    await route.continue_()

            await self._context.route("**/*", _route_handler)

        self._page = await self._context.new_page()
        self._touch()
        self._start_watchdog()
        logger.info("Browser session started (headless={})", self._config.headless)

    def _start_watchdog(self) -> None:
        if self._watchdog_task and not self._watchdog_task.done():
            self._watchdog_task.cancel()
        self._watchdog_task = asyncio.create_task(self._inactivity_watchdog())

    async def _inactivity_watchdog(self) -> None:
        timeout_s = self._config.session_timeout_minutes * 60
        try:
            while True:
                await asyncio.sleep(30)
                if not self.is_active:
                    break
                if time.monotonic() - self._last_activity >= timeout_s:
                    logger.info(
                        "Browser idle for {}m — auto-closing",
                        self._config.session_timeout_minutes,
                    )
                    await self.close()
                    break
        except asyncio.CancelledError:
            pass


# ---------------------------------------------------------------------------
# BrowserTool
# ---------------------------------------------------------------------------

class BrowserTool(Tool):
    """Interactive browser automation tool with 12 actions."""

    def __init__(self, config: Any = None):
        from flowly.config.schema import BrowserConfig

        self._config = config or BrowserConfig()
        self._session = _BrowserSession(self._config)
        self._screenshots_dir = Path.home() / ".flowly" / "screenshots"

    # -- Tool interface ------------------------------------------------------

    @property
    def name(self) -> str:
        return "browser"

    @property
    def description(self) -> str:
        return (
            "Automate a web browser for interactive tasks.\n\n"
            "Actions:\n"
            "- navigate: Go to a URL (url required)\n"
            "- click: Click an element (selector required)\n"
            "- type: Type text into an input (selector, text required)\n"
            "- select: Select dropdown option (selector, value required)\n"
            "- scroll: Scroll page (direction=down/up/left/right, amount=500)\n"
            "- screenshot: Capture screenshot (selector optional, full_page=false)\n"
            "- extract: Extract text from page or element (selector optional)\n"
            "- fill_form: Fill multiple form fields (fields=[{selector, value}] required)\n"
            "- eval_js: Execute JavaScript (script required)\n"
            "- get_html: Get HTML of page or element (selector optional, outer=true)\n"
            "- wait: Wait for selector or time (selector or timeout in ms)\n"
            "- close: Close browser session\n\n"
            "The browser stays open between calls. It auto-closes after 10 min of inactivity."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "description": "The browser action to perform",
                    "enum": [
                        "navigate", "click", "type", "select", "scroll",
                        "screenshot", "extract", "fill_form", "eval_js",
                        "get_html", "wait", "close",
                    ],
                },
                "url": {
                    "type": "string",
                    "description": "URL to navigate to (for navigate action)",
                },
                "selector": {
                    "type": "string",
                    "description": "CSS selector for target element",
                },
                "text": {
                    "type": "string",
                    "description": "Text to type (for type action)",
                },
                "value": {
                    "type": "string",
                    "description": "Value to select (for select action)",
                },
                "fields": {
                    "type": "array",
                    "description": "Form fields [{selector, value}] (for fill_form)",
                    "items": {
                        "type": "object",
                        "properties": {
                            "selector": {"type": "string"},
                            "value": {"type": "string"},
                        },
                        "required": ["selector", "value"],
                    },
                },
                "script": {
                    "type": "string",
                    "description": "JavaScript code to execute (for eval_js)",
                },
                "direction": {
                    "type": "string",
                    "enum": ["up", "down", "left", "right"],
                    "description": "Scroll direction (default: down)",
                },
                "amount": {
                    "type": "integer",
                    "description": "Scroll amount in pixels (default: 500)",
                },
                "full_page": {
                    "type": "boolean",
                    "description": "Capture full scrollable page (default: false)",
                },
                "outer": {
                    "type": "boolean",
                    "description": "Include outer HTML vs inner (default: true)",
                },
                "timeout": {
                    "type": "integer",
                    "description": "Timeout in ms (for wait action, default: 5000)",
                },
            },
            "required": ["action"],
        }

    async def execute(self, action: str = "", **kwargs: Any) -> str:
        handlers = {
            "navigate": self._navigate,
            "click": self._click,
            "type": self._type,
            "select": self._select,
            "scroll": self._scroll,
            "screenshot": self._screenshot,
            "extract": self._extract,
            "fill_form": self._fill_form,
            "eval_js": self._eval_js,
            "get_html": self._get_html,
            "wait": self._wait,
            "close": self._close,
        }
        handler = handlers.get(action)
        if not handler:
            return json.dumps({"error": f"Unknown action: {action}. Valid: {list(handlers)}"})

        try:
            return await handler(**kwargs)
        except RuntimeError as exc:
            # Playwright not installed or browser launch failure
            return json.dumps({"error": str(exc)})
        except Exception as exc:
            logger.error("Browser {} error: {}", action, exc)
            return json.dumps({"error": str(exc), "action": action})

    # -- Actions -------------------------------------------------------------

    async def _navigate(self, url: str = "", **_: Any) -> str:
        if not url:
            return json.dumps({"error": "url is required for navigate action"})

        if _is_ssrf_target(url):
            return json.dumps({"error": "Blocked: URL targets a private or internal address"})

        page = await self._session.get_page()
        resp = await page.goto(
            url,
            timeout=self._config.navigation_timeout,
            wait_until="domcontentloaded",
        )

        return json.dumps({
            "action": "navigate",
            "url": page.url,
            "title": await page.title(),
            "status": resp.status if resp else None,
        })

    async def _click(self, selector: str = "", **_: Any) -> str:
        if not selector:
            return json.dumps({"error": "selector is required for click action"})

        page = await self._session.get_page()
        await page.click(selector, timeout=self._config.action_timeout)
        # Wait briefly for navigation or DOM changes
        try:
            await page.wait_for_load_state("domcontentloaded", timeout=3000)
        except Exception:
            pass

        return json.dumps({
            "action": "click",
            "selector": selector,
            "url": page.url,
            "title": await page.title(),
        })

    async def _type(self, selector: str = "", text: str = "", **_: Any) -> str:
        if not selector or not text:
            return json.dumps({"error": "selector and text are required for type action"})

        page = await self._session.get_page()
        # Clear existing value then type
        await page.fill(selector, "", timeout=self._config.action_timeout)
        await page.type(selector, text, timeout=self._config.action_timeout)

        return json.dumps({
            "action": "type",
            "selector": selector,
            "text_length": len(text),
            "url": page.url,
            "title": await page.title(),
        })

    async def _select(self, selector: str = "", value: str = "", **_: Any) -> str:
        if not selector or not value:
            return json.dumps({"error": "selector and value are required for select action"})

        page = await self._session.get_page()
        selected = await page.select_option(selector, value, timeout=self._config.action_timeout)

        return json.dumps({
            "action": "select",
            "selector": selector,
            "selected": selected,
            "url": page.url,
            "title": await page.title(),
        })

    async def _scroll(self, direction: str = "down", amount: int = 500, **_: Any) -> str:
        page = await self._session.get_page()

        dx, dy = 0, 0
        if direction == "down":
            dy = amount
        elif direction == "up":
            dy = -amount
        elif direction == "right":
            dx = amount
        elif direction == "left":
            dx = -amount

        await page.mouse.wheel(dx, dy)
        await asyncio.sleep(0.3)  # Let content render

        return json.dumps({
            "action": "scroll",
            "direction": direction,
            "amount": amount,
            "url": page.url,
            "title": await page.title(),
        })

    async def _screenshot(
        self, selector: str | None = None, full_page: bool = False, **_: Any
    ) -> str:
        page = await self._session.get_page()
        self._screenshots_dir.mkdir(parents=True, exist_ok=True)

        timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        filename = f"browser-{timestamp}.png"
        output_path = self._screenshots_dir / filename

        if selector:
            element = await page.query_selector(selector)
            if not element:
                return json.dumps({"error": f"Element not found: {selector}"})
            await element.screenshot(path=str(output_path))
        else:
            await page.screenshot(path=str(output_path), full_page=full_page)

        size_kb = round(output_path.stat().st_size / 1024, 1)

        return json.dumps({
            "action": "screenshot",
            "path": str(output_path),
            "size_kb": size_kb,
            "url": page.url,
            "title": await page.title(),
            "hint": f'Send to user with: message tool, media_paths=["{output_path}"]',
        })

    async def _extract(self, selector: str | None = None, **_: Any) -> str:
        page = await self._session.get_page()
        max_len = self._config.max_text_length

        if selector:
            element = await page.query_selector(selector)
            if not element:
                return json.dumps({"error": f"Element not found: {selector}"})
            text = (await element.inner_text()).strip()
        else:
            text = (await page.inner_text("body")).strip()

        truncated = len(text) > max_len
        if truncated:
            text = text[:max_len]

        return json.dumps({
            "action": "extract",
            "url": page.url,
            "title": await page.title(),
            "text": text,
            "length": len(text),
            "truncated": truncated,
        })

    async def _fill_form(self, fields: list[dict] | None = None, **_: Any) -> str:
        if not fields:
            return json.dumps({"error": "fields array is required for fill_form action"})

        page = await self._session.get_page()
        filled: list[str] = []
        errors: list[dict] = []

        for field in fields:
            sel = field.get("selector", "")
            val = field.get("value", "")
            if not sel:
                errors.append({"selector": sel, "error": "empty selector"})
                continue
            try:
                await page.fill(sel, val, timeout=self._config.action_timeout)
                filled.append(sel)
            except Exception as exc:
                errors.append({"selector": sel, "error": str(exc)})

        return json.dumps({
            "action": "fill_form",
            "filled_count": len(filled),
            "error_count": len(errors),
            "errors": errors if errors else None,
            "url": page.url,
            "title": await page.title(),
        })

    async def _eval_js(self, script: str = "", **_: Any) -> str:
        if not script:
            return json.dumps({"error": "script is required for eval_js action"})

        page = await self._session.get_page()
        result = await page.evaluate(script)

        # Serialize result
        try:
            result_str = json.dumps(result)
        except (TypeError, ValueError):
            result_str = str(result)

        max_len = self._config.max_text_length
        truncated = len(result_str) > max_len
        if truncated:
            result_str = result_str[:max_len]

        return json.dumps({
            "action": "eval_js",
            "result": result_str,
            "truncated": truncated,
            "url": page.url,
        })

    async def _get_html(
        self, selector: str | None = None, outer: bool = True, **_: Any
    ) -> str:
        page = await self._session.get_page()
        max_len = self._config.max_text_length

        if selector:
            element = await page.query_selector(selector)
            if not element:
                return json.dumps({"error": f"Element not found: {selector}"})
            if outer:
                html = await element.evaluate("el => el.outerHTML")
            else:
                html = await element.inner_html()
        else:
            html = await page.content()

        truncated = len(html) > max_len
        if truncated:
            html = html[:max_len]

        return json.dumps({
            "action": "get_html",
            "html": html,
            "length": len(html),
            "truncated": truncated,
            "url": page.url,
            "title": await page.title(),
        })

    async def _wait(
        self, selector: str = "", timeout: int = 5000, **_: Any
    ) -> str:
        page = await self._session.get_page()

        if selector:
            await page.wait_for_selector(selector, timeout=timeout)
            return json.dumps({
                "action": "wait",
                "waited_for": f"selector: {selector}",
                "url": page.url,
                "title": await page.title(),
            })
        else:
            await asyncio.sleep(timeout / 1000)
            return json.dumps({
                "action": "wait",
                "waited_for": f"{timeout}ms",
                "url": page.url,
                "title": await page.title(),
            })

    async def _close(self, **_: Any) -> str:
        was_active = self._session.is_active
        await self._session.close()
        return json.dumps({
            "action": "close",
            "was_active": was_active,
        })
