"""
Browser Automation - LLM-controlled browser for OSINT operations

Provides headless browser automation through Tor proxy for dark web reconnaissance.
Inspired by agent-zero's browser-use integration.

Reliability Features (REL-005):
- Context managers for all browser resources
- Explicit cleanup in finally blocks
- Resource tracking and leak prevention
"""

import asyncio
import base64
import logging
import os
import shutil
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Optional

from groknroll.osint.tor_manager import TorManager


# REL-005: Configure logger for resource tracking
logger = logging.getLogger(__name__)

# Optional browser-use dependency
try:
    import browser_use

    BROWSER_USE_AVAILABLE = True
except ImportError:
    BROWSER_USE_AVAILABLE = False

# Optional playwright dependency
try:
    from playwright.async_api import Browser, BrowserContext, Page, async_playwright

    PLAYWRIGHT_AVAILABLE = True
except ImportError:
    PLAYWRIGHT_AVAILABLE = False


@dataclass
class BrowserState:
    """Current browser state snapshot"""

    url: str
    title: str
    screenshot_base64: Optional[str] = None
    page_content: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.utcnow)
    cookies: list[dict[str, Any]] = field(default_factory=list)


@dataclass
class BrowserAction:
    """Browser action to execute"""

    action_type: str  # navigate, click, type, scroll, screenshot, extract
    target: Optional[str] = None  # selector or URL
    value: Optional[str] = None  # text to type or other value
    wait_after: float = 1.0  # seconds to wait after action


@dataclass
class BrowserResult:
    """Result of browser automation task"""

    success: bool
    message: str
    screenshots: list[str] = field(default_factory=list)  # base64 encoded
    extracted_content: Optional[str] = None
    final_url: Optional[str] = None
    actions_taken: int = 0
    error: Optional[str] = None


class TorBrowserAutomation:
    """
    LLM-controlled browser automation through Tor proxy.

    Provides headless Chromium browser with:
    - Tor SOCKS5 proxy for .onion sites
    - Screenshot capture for LLM vision
    - DOM interaction (click, type, scroll)
    - Content extraction
    - Session persistence

    Usage:
        tor = TorManager()
        browser = TorBrowserAutomation(tor)
        async with browser:
            result = await browser.navigate("http://example.onion")
            screenshot = await browser.screenshot()
    """

    def __init__(
        self,
        tor_manager: TorManager,
        headless: bool = True,
        user_data_dir: Optional[str] = None,
        viewport_width: int = 1280,
        viewport_height: int = 1024,
        timeout: int = 60000,  # 60 seconds
    ):
        """
        Initialize Tor browser automation.

        Args:
            tor_manager: TorManager instance for proxy
            headless: Run in headless mode
            user_data_dir: Directory for persistent browser data
            viewport_width: Browser viewport width
            viewport_height: Browser viewport height
            timeout: Default timeout in milliseconds
        """
        if not PLAYWRIGHT_AVAILABLE:
            raise ImportError(
                "playwright is required for browser automation. "
                "Install with: pip install playwright && playwright install chromium"
            )

        self.tor_manager = tor_manager
        self.headless = headless
        self.user_data_dir = user_data_dir or tempfile.mkdtemp(prefix="osint_browser_")
        self.viewport = {"width": viewport_width, "height": viewport_height}
        self.timeout = timeout

        self._playwright = None
        self._browser: Optional[Browser] = None
        self._context: Optional[BrowserContext] = None
        self._page: Optional[Page] = None
        self._action_history: list[BrowserAction] = []

    async def __aenter__(self):
        """Async context manager entry."""
        await self.start()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        await self.close()

    async def start(self) -> None:
        """Start the browser with Tor proxy."""
        self._playwright = await async_playwright().start()

        # Configure browser with Tor proxy
        proxy_config = {
            "server": self.tor_manager.proxy_url.replace("socks5h://", "socks5://"),
        }

        self._browser = await self._playwright.chromium.launch(
            headless=self.headless,
            proxy=proxy_config,
            args=[
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--disable-dev-shm-usage",
                "--disable-accelerated-2d-canvas",
                "--disable-gpu",
                "--disable-web-security",
                "--disable-features=IsolateOrigins,site-per-process",
            ],
        )

        # Create persistent context
        self._context = await self._browser.new_context(
            viewport=self.viewport,
            user_agent=self.tor_manager.get_random_user_agent(),
            ignore_https_errors=True,
            java_script_enabled=True,
            accept_downloads=True,
            bypass_csp=True,
        )

        # Set default timeout
        self._context.set_default_timeout(self.timeout)

        # Create page
        self._page = await self._context.new_page()

        # Block unnecessary resources for faster loading
        await self._page.route(
            "**/*.{png,jpg,jpeg,gif,svg,mp3,mp4,avi,flac,ogg,wav,webm}",
            lambda route: route.abort(),
        )

    async def close(self) -> None:
        """
        Close browser and cleanup all resources.

        REL-005: Ensures all resources are cleaned up even if exceptions occur.
        """
        errors = []

        # Close page
        if self._page:
            try:
                await self._page.close()
            except Exception as e:
                logger.warning(f"Error closing page: {e}")
                errors.append(f"page: {e}")
            finally:
                self._page = None

        # Close context
        if self._context:
            try:
                await self._context.close()
            except Exception as e:
                logger.warning(f"Error closing context: {e}")
                errors.append(f"context: {e}")
            finally:
                self._context = None

        # Close browser
        if self._browser:
            try:
                await self._browser.close()
            except Exception as e:
                logger.warning(f"Error closing browser: {e}")
                errors.append(f"browser: {e}")
            finally:
                self._browser = None

        # Stop playwright
        if self._playwright:
            try:
                await self._playwright.stop()
            except Exception as e:
                logger.warning(f"Error stopping playwright: {e}")
                errors.append(f"playwright: {e}")
            finally:
                self._playwright = None

        # Cleanup user data directory if temporary
        if self.user_data_dir and self.user_data_dir.startswith(tempfile.gettempdir()):
            try:
                shutil.rmtree(self.user_data_dir, ignore_errors=True)
            except Exception as e:
                logger.warning(f"Error cleaning up user data dir: {e}")
                errors.append(f"user_data_dir: {e}")

        if errors:
            logger.debug(f"Browser cleanup completed with {len(errors)} errors")

    async def navigate(self, url: str, wait_until: str = "domcontentloaded") -> BrowserState:
        """
        Navigate to URL.

        Args:
            url: URL to navigate to
            wait_until: Wait condition (load, domcontentloaded, networkidle)

        Returns:
            BrowserState with current page info
        """
        if not self._page:
            raise RuntimeError("Browser not started. Call start() first.")

        try:
            await self._page.goto(url, wait_until=wait_until, timeout=self.timeout)
            self._action_history.append(BrowserAction("navigate", target=url))
            return await self.get_state()
        except Exception as e:
            return BrowserState(
                url=url,
                title=f"Error: {str(e)}",
                page_content=str(e),
            )

    async def click(self, selector: str, wait_after: float = 1.0) -> BrowserState:
        """
        Click an element.

        Args:
            selector: CSS selector or text selector
            wait_after: Seconds to wait after click

        Returns:
            BrowserState after click
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        try:
            # Try CSS selector first, then text
            try:
                await self._page.click(selector, timeout=5000)
            except Exception:
                # Try clicking by text
                await self._page.click(f"text={selector}", timeout=5000)

            await asyncio.sleep(wait_after)
            self._action_history.append(BrowserAction("click", target=selector))
            return await self.get_state()
        except Exception as e:
            state = await self.get_state()
            state.title = f"Click failed: {str(e)}"
            return state

    async def type_text(
        self,
        selector: str,
        text: str,
        clear_first: bool = True,
        press_enter: bool = False,
    ) -> BrowserState:
        """
        Type text into an input field.

        Args:
            selector: CSS selector for input
            text: Text to type
            clear_first: Clear existing text first
            press_enter: Press Enter after typing

        Returns:
            BrowserState after typing
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        try:
            if clear_first:
                await self._page.fill(selector, "")

            await self._page.type(selector, text)

            if press_enter:
                await self._page.press(selector, "Enter")
                await asyncio.sleep(2)  # Wait for page response

            self._action_history.append(
                BrowserAction("type", target=selector, value=text)
            )
            return await self.get_state()
        except Exception as e:
            state = await self.get_state()
            state.title = f"Type failed: {str(e)}"
            return state

    async def scroll(self, direction: str = "down", amount: int = 500) -> BrowserState:
        """
        Scroll the page.

        Args:
            direction: up or down
            amount: Pixels to scroll

        Returns:
            BrowserState after scroll
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        delta = amount if direction == "down" else -amount
        await self._page.mouse.wheel(0, delta)
        await asyncio.sleep(0.5)

        self._action_history.append(
            BrowserAction("scroll", value=f"{direction}:{amount}")
        )
        return await self.get_state()

    async def screenshot(self, full_page: bool = False) -> str:
        """
        Take a screenshot.

        Args:
            full_page: Capture entire page

        Returns:
            Base64 encoded screenshot
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        screenshot_bytes = await self._page.screenshot(full_page=full_page)
        return base64.b64encode(screenshot_bytes).decode("utf-8")

    async def extract_text(self, selector: Optional[str] = None) -> str:
        """
        Extract text content from page or element.

        Args:
            selector: CSS selector (optional, extracts body if not provided)

        Returns:
            Extracted text content
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        if selector:
            element = await self._page.query_selector(selector)
            if element:
                return await element.inner_text()
            return ""
        else:
            return await self._page.inner_text("body")

    async def extract_links(self) -> list[dict[str, str]]:
        """
        Extract all links from the page.

        Returns:
            List of dicts with 'url' and 'text' keys
        """
        if not self._page:
            raise RuntimeError("Browser not started.")

        links = await self._page.eval_on_selector_all(
            "a[href]",
            """elements => elements.map(e => ({
                url: e.href,
                text: e.innerText.trim().substring(0, 100)
            }))""",
        )
        return links

    async def get_state(self) -> BrowserState:
        """
        Get current browser state.

        Returns:
            BrowserState with current page info
        """
        if not self._page:
            return BrowserState(url="", title="Browser not started")

        try:
            url = self._page.url
            title = await self._page.title()
            content = await self.extract_text()[:5000]  # Limit content size

            return BrowserState(
                url=url,
                title=title,
                page_content=content,
            )
        except Exception as e:
            return BrowserState(
                url=self._page.url if self._page else "",
                title=f"Error getting state: {str(e)}",
            )

    async def execute_actions(
        self,
        actions: list[BrowserAction],
        on_action: Optional[Callable[[BrowserAction, BrowserState], None]] = None,
    ) -> BrowserResult:
        """
        Execute a sequence of browser actions.

        Args:
            actions: List of BrowserAction to execute
            on_action: Callback for each action completion

        Returns:
            BrowserResult with execution summary
        """
        screenshots = []
        actions_taken = 0

        try:
            for action in actions:
                if action.action_type == "navigate":
                    state = await self.navigate(action.target or "")
                elif action.action_type == "click":
                    state = await self.click(action.target or "")
                elif action.action_type == "type":
                    state = await self.type_text(action.target or "", action.value or "")
                elif action.action_type == "scroll":
                    direction = "down" if not action.value else action.value
                    state = await self.scroll(direction)
                elif action.action_type == "screenshot":
                    screenshot = await self.screenshot()
                    screenshots.append(screenshot)
                    state = await self.get_state()
                elif action.action_type == "extract":
                    text = await self.extract_text(action.target)
                    state = await self.get_state()
                    state.page_content = text
                else:
                    continue

                actions_taken += 1
                await asyncio.sleep(action.wait_after)

                if on_action:
                    on_action(action, state)

            final_state = await self.get_state()
            return BrowserResult(
                success=True,
                message="Actions completed successfully",
                screenshots=screenshots,
                extracted_content=final_state.page_content,
                final_url=final_state.url,
                actions_taken=actions_taken,
            )

        except Exception as e:
            return BrowserResult(
                success=False,
                message=f"Action failed: {str(e)}",
                screenshots=screenshots,
                actions_taken=actions_taken,
                error=str(e),
            )

    async def run_task(
        self,
        task: str,
        llm_callback: Optional[Callable[[str, BrowserState], list[BrowserAction]]] = None,
        max_steps: int = 20,
    ) -> BrowserResult:
        """
        Run an LLM-directed browser task.

        Args:
            task: Natural language task description
            llm_callback: Function that takes task and state, returns actions
            max_steps: Maximum number of action steps

        Returns:
            BrowserResult with task outcome
        """
        if not llm_callback:
            return BrowserResult(
                success=False,
                message="No LLM callback provided for task execution",
                error="Missing llm_callback",
            )

        screenshots = []
        total_actions = 0

        try:
            for step in range(max_steps):
                state = await self.get_state()

                # Take screenshot for LLM vision
                screenshot = await self.screenshot()
                screenshots.append(screenshot)
                state.screenshot_base64 = screenshot

                # Get next actions from LLM
                actions = llm_callback(task, state)

                if not actions:
                    # LLM indicates task is complete
                    break

                # Execute actions
                result = await self.execute_actions(actions)
                total_actions += result.actions_taken

                if not result.success:
                    return BrowserResult(
                        success=False,
                        message=f"Step {step + 1} failed: {result.error}",
                        screenshots=screenshots,
                        actions_taken=total_actions,
                        error=result.error,
                    )

            final_state = await self.get_state()
            return BrowserResult(
                success=True,
                message=f"Task completed in {total_actions} actions",
                screenshots=screenshots,
                extracted_content=final_state.page_content,
                final_url=final_state.url,
                actions_taken=total_actions,
            )

        except Exception as e:
            return BrowserResult(
                success=False,
                message=f"Task failed: {str(e)}",
                screenshots=screenshots,
                actions_taken=total_actions,
                error=str(e),
            )

    @property
    def page(self) -> Optional[Any]:
        """Get the current page (playwright Page object if available)."""
        return self._page

    @property
    def action_history(self) -> list[BrowserAction]:
        """Get history of actions taken."""
        return self._action_history.copy()


class BrowserAgentTask:
    """
    High-level browser automation task for OSINT.

    Combines TorBrowserAutomation with task-specific logic
    for common OSINT operations.
    """

    def __init__(self, tor_manager: TorManager):
        self.tor_manager = tor_manager
        self._browser: Optional[TorBrowserAutomation] = None

    async def search_and_scrape(
        self,
        search_engine_url: str,
        query: str,
        search_input_selector: str = "input[type='search'], input[type='text'], input[name='q']",
        result_selector: str = "a",
        max_results: int = 20,
    ) -> list[dict[str, str]]:
        """
        Search a dark web search engine and scrape results.

        Args:
            search_engine_url: URL of search engine
            query: Search query
            search_input_selector: CSS selector for search input
            result_selector: CSS selector for result links
            max_results: Maximum results to return

        Returns:
            List of result dicts with 'url' and 'title'
        """
        async with TorBrowserAutomation(self.tor_manager) as browser:
            # Navigate to search engine
            await browser.navigate(search_engine_url)

            # Type search query
            await browser.type_text(search_input_selector, query, press_enter=True)

            # Wait for results
            await asyncio.sleep(3)

            # Extract links
            links = await browser.extract_links()

            # Filter for .onion links
            onion_links = [
                link for link in links
                if ".onion" in link.get("url", "") and link.get("text")
            ]

            return onion_links[:max_results]

    async def scrape_page(
        self,
        url: str,
        extract_selectors: Optional[dict[str, str]] = None,
    ) -> dict[str, Any]:
        """
        Scrape content from a dark web page.

        Args:
            url: URL to scrape
            extract_selectors: Dict mapping names to CSS selectors

        Returns:
            Dict with extracted content
        """
        async with TorBrowserAutomation(self.tor_manager) as browser:
            state = await browser.navigate(url)

            result = {
                "url": state.url,
                "title": state.title,
                "content": state.page_content,
                "screenshot": await browser.screenshot(),
                "links": await browser.extract_links(),
            }

            if extract_selectors:
                for name, selector in extract_selectors.items():
                    try:
                        result[name] = await browser.extract_text(selector)
                    except Exception:
                        result[name] = None

            return result

    async def capture_evidence(
        self,
        urls: list[str],
        output_dir: str,
    ) -> list[dict[str, str]]:
        """
        Capture screenshots and content from multiple URLs.

        Args:
            urls: List of URLs to capture
            output_dir: Directory to save evidence

        Returns:
            List of evidence records
        """
        evidence = []
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        async with TorBrowserAutomation(self.tor_manager) as browser:
            for i, url in enumerate(urls):
                try:
                    state = await browser.navigate(url)
                    screenshot = await browser.screenshot(full_page=True)

                    # Save screenshot
                    screenshot_file = output_path / f"evidence_{i:03d}.png"
                    with open(screenshot_file, "wb") as f:
                        f.write(base64.b64decode(screenshot))

                    # Save content
                    content_file = output_path / f"evidence_{i:03d}.txt"
                    with open(content_file, "w", encoding="utf-8") as f:
                        f.write(f"URL: {state.url}\n")
                        f.write(f"Title: {state.title}\n")
                        f.write(f"Timestamp: {state.timestamp.isoformat()}\n")
                        f.write("-" * 50 + "\n")
                        f.write(state.page_content or "")

                    evidence.append({
                        "url": url,
                        "title": state.title,
                        "screenshot": str(screenshot_file),
                        "content": str(content_file),
                        "timestamp": state.timestamp.isoformat(),
                    })

                except Exception as e:
                    evidence.append({
                        "url": url,
                        "error": str(e),
                    })

        return evidence
