# 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

"""
Familiar Private Browser Skill

Privacy-focused embedded web browser with:
- All requests proxied through Familiar server
- Tracker/ad blocking
- AI-powered summarization and assistance
- Reader mode
- Encrypted page archive
- No direct connection from user to websites
"""

import asyncio
import hashlib  # noqa: F401
import json
import logging
import re
import uuid  # noqa: F401
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import quote, unquote, urljoin, urlparse  # noqa: F401

logger = logging.getLogger(__name__)

from .ai_assist import BrowserAI  # noqa: E402
from .archive import PageArchive  # noqa: E402
from .filter import FilterEngine, FilterLevel  # noqa: F401, E402
from .proxy import ProxyService  # noqa: E402
from .reader import ReaderMode  # noqa: E402


@dataclass
class BrowserConfig:
    """Browser configuration."""

    # Privacy level: standard, strict, paranoid
    privacy_mode: str = "standard"

    # Filtering
    block_ads: bool = True
    block_trackers: bool = True
    block_scripts: bool = False  # True in strict/paranoid
    block_images: bool = False  # True in paranoid

    # Features
    reader_mode_default: bool = False
    auto_summarize: bool = False
    archive_enabled: bool = True

    # Storage
    storage_path: str = "~/.familiar/browser"
    max_archive_size_mb: int = 1000

    # Tor (paranoid mode)
    tor_enabled: bool = False
    tor_socks_port: int = 9050

    # User agent
    user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"

    def __post_init__(self):
        # Apply privacy mode presets
        if self.privacy_mode == "strict":
            self.block_scripts = True
        elif self.privacy_mode == "paranoid":
            self.block_scripts = True
            self.block_images = True
            self.tor_enabled = True


@dataclass
class BrowseResult:
    """Result of browsing to a URL."""

    url: str
    final_url: str  # After redirects
    title: str
    content_html: str
    content_text: str
    reader_html: Optional[str]
    summary: Optional[str]
    links: List[Dict[str, str]]
    images: List[Dict[str, str]]
    meta: Dict[str, str]
    blocked: Dict[str, int]  # Counts of blocked items
    load_time_ms: int
    timestamp: datetime = field(default_factory=datetime.utcnow)
    archived: bool = False
    archive_id: Optional[str] = None


class PrivateBrowserSkill:
    """
    Privacy-focused embedded web browser.

    All web requests go through Familiar server - the user's device
    never connects directly to websites. This provides:

    - IP privacy (websites see Familiar server, not user)
    - Tracker blocking (stripped server-side)
    - Ad blocking (removed before content reaches user)
    - AI assistance (Familiar can read/summarize any page)
    - Encrypted archives (save pages locally)
    """

    name = "browser_proxy"
    description = "Privacy-focused web browser with AI assistance"

    def __init__(self, agent, config: Dict[str, Any]):
        self.agent = agent
        self.config = BrowserConfig(**config.get("browser_proxy", {}))

        # Storage path
        self.storage_path = Path(self.config.storage_path).expanduser()
        self.storage_path.mkdir(parents=True, exist_ok=True)

        # Components
        self.proxy = ProxyService(self.config)
        self.filter = FilterEngine(self.config)
        self.reader = ReaderMode()
        self.archive = PageArchive(self.storage_path / "archive")
        self.ai = BrowserAI(agent) if agent else None

        # History (in-memory, not persisted for privacy)
        self.history: List[Dict[str, Any]] = []
        self.max_history = 100

    async def start(self):
        """Initialize browser components."""
        await self.filter.load_blocklists()
        await self.archive.initialize()
        logger.info(f"Private browser initialized (mode: {self.config.privacy_mode})")

    async def stop(self):
        """Cleanup."""
        pass

    # ═══════════════════════════════════════════════════════════════
    # Core Browsing
    # ═══════════════════════════════════════════════════════════════

    async def browse(
        self,
        url: str,
        reader_mode: Optional[bool] = None,
        summarize: bool = False,
        archive: bool = False,
    ) -> BrowseResult:
        """
        Browse to a URL.

        Args:
            url: URL to browse
            reader_mode: Force reader mode (None = use default)
            summarize: Generate AI summary
            archive: Save to encrypted archive

        Returns:
            BrowseResult with content and metadata
        """
        start_time = asyncio.get_event_loop().time()

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

        # Fetch page through proxy
        response = await self.proxy.fetch(url)

        # Apply filters
        filtered = await self.filter.process(
            response["content"],
            response["url"],
            response["content_type"],
        )

        # Extract metadata
        title = self._extract_title(filtered["html"])
        meta = self._extract_meta(filtered["html"])
        links = self._extract_links(filtered["html"], response["url"])
        images = self._extract_images(filtered["html"], response["url"])

        # Reader mode
        use_reader = reader_mode if reader_mode is not None else self.config.reader_mode_default
        reader_html = None
        if use_reader:
            reader_html = await self.reader.extract(filtered["html"], response["url"])

        # Text extraction
        content_text = self._html_to_text(filtered["html"])

        # AI summary
        summary = None
        if summarize or self.config.auto_summarize:
            if self.ai:
                summary = await self.ai.summarize(content_text, title)

        # Calculate load time
        load_time = int((asyncio.get_event_loop().time() - start_time) * 1000)

        # Build result
        result = BrowseResult(
            url=url,
            final_url=response["url"],
            title=title,
            content_html=filtered["html"],
            content_text=content_text,
            reader_html=reader_html,
            summary=summary,
            links=links,
            images=images,
            meta=meta,
            blocked=filtered["blocked"],
            load_time_ms=load_time,
        )

        # Archive if requested
        if archive and self.config.archive_enabled:
            archive_id = await self.archive.save(result)
            result.archived = True
            result.archive_id = archive_id

        # Add to history
        self._add_to_history(result)

        return result

    async def search(
        self,
        query: str,
        engine: str = "duckduckgo",
    ) -> List[Dict[str, Any]]:
        """
        Search the web privately.

        Args:
            query: Search query
            engine: Search engine (duckduckgo, searx, google)

        Returns:
            List of search results
        """
        search_urls = {
            "duckduckgo": f"https://html.duckduckgo.com/html/?q={quote(query)}",
            "searx": f"https://searx.be/search?q={quote(query)}&format=json",
            "google": f"https://www.google.com/search?q={quote(query)}",
        }

        url = search_urls.get(engine, search_urls["duckduckgo"])

        response = await self.proxy.fetch(url)

        # Parse search results based on engine
        if engine == "searx":
            return self._parse_searx_results(response["content"])
        elif engine == "duckduckgo":
            return self._parse_ddg_results(response["content"])
        else:
            return self._parse_google_results(response["content"])

    # ═══════════════════════════════════════════════════════════════
    # AI Features
    # ═══════════════════════════════════════════════════════════════

    async def ask(
        self,
        question: str,
        url: Optional[str] = None,
    ) -> str:
        """
        Ask AI about current page or specific URL.

        Args:
            question: Question to ask
            url: URL to analyze (uses last visited if None)

        Returns:
            AI response
        """
        if not self.ai:
            return "AI not available"

        # Get page content
        if url:
            result = await self.browse(url)
            content = result.content_text
            title = result.title
        elif self.history:
            content = self.history[-1].get("content_text", "")
            title = self.history[-1].get("title", "")
        else:
            return "No page loaded. Please browse to a URL first."

        return await self.ai.answer_question(question, content, title)

    async def extract_data(
        self,
        url: str,
        data_type: str = "auto",
    ) -> Dict[str, Any]:
        """
        Extract structured data from a page.

        Args:
            url: URL to extract from
            data_type: Type of data (auto, prices, contacts, events, etc.)

        Returns:
            Extracted structured data
        """
        result = await self.browse(url)

        if self.ai:
            return await self.ai.extract_data(
                result.content_text,
                result.title,
                data_type,
            )

        # Fallback: basic extraction
        return {
            "title": result.title,
            "links": len(result.links),
            "images": len(result.images),
            "meta": result.meta,
        }

    async def fill_form(
        self,
        url: str,
        form_data: Dict[str, str],
    ) -> BrowseResult:
        """
        Fill and submit a form.

        Args:
            url: URL with the form
            form_data: Field names and values

        Returns:
            Result after form submission
        """
        # This would need Playwright for full form interaction
        # For now, just POST the data
        response = await self.proxy.post(url, form_data)

        filtered = await self.filter.process(
            response["content"],
            response["url"],
            response.get("content_type", "text/html"),
        )

        return BrowseResult(
            url=url,
            final_url=response["url"],
            title=self._extract_title(filtered["html"]),
            content_html=filtered["html"],
            content_text=self._html_to_text(filtered["html"]),
            reader_html=None,
            summary=None,
            links=[],
            images=[],
            meta={},
            blocked=filtered["blocked"],
            load_time_ms=0,
        )

    # ═══════════════════════════════════════════════════════════════
    # Archive Features
    # ═══════════════════════════════════════════════════════════════

    async def save_page(self, url: Optional[str] = None) -> str:
        """Save current or specified page to archive."""
        if url:
            result = await self.browse(url)
        elif self.history:
            # Reconstruct from history
            last = self.history[-1]
            result = BrowseResult(
                url=last["url"],
                final_url=last["final_url"],
                title=last["title"],
                content_html=last.get("content_html", ""),
                content_text=last.get("content_text", ""),
                reader_html=None,
                summary=None,
                links=[],
                images=[],
                meta={},
                blocked={},
                load_time_ms=0,
            )
        else:
            raise ValueError("No page to save")

        archive_id = await self.archive.save(result)
        return archive_id

    async def get_archived(self, archive_id: str) -> Optional[BrowseResult]:
        """Retrieve an archived page."""
        return await self.archive.load(archive_id)

    async def list_archived(
        self,
        limit: int = 50,
        search: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """List archived pages."""
        return await self.archive.list(limit, search)

    async def delete_archived(self, archive_id: str) -> bool:
        """Delete an archived page."""
        return await self.archive.delete(archive_id)

    async def search_archive(self, query: str) -> List[Dict[str, Any]]:
        """Full-text search across archived pages."""
        return await self.archive.search(query)

    # ═══════════════════════════════════════════════════════════════
    # History (In-Memory Only)
    # ═══════════════════════════════════════════════════════════════

    def get_history(self, limit: int = 20) -> List[Dict[str, Any]]:
        """Get recent history (session only, not persisted)."""
        return self.history[-limit:][::-1]

    def clear_history(self):
        """Clear browsing history."""
        self.history = []

    def _add_to_history(self, result: BrowseResult):
        """Add page to history."""
        self.history.append(
            {
                "url": result.url,
                "final_url": result.final_url,
                "title": result.title,
                "timestamp": result.timestamp.isoformat(),
                "content_text": result.content_text[:1000],  # Truncate for memory
            }
        )

        # Trim history
        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history :]

    # ═══════════════════════════════════════════════════════════════
    # Helper Methods
    # ═══════════════════════════════════════════════════════════════

    def _extract_title(self, html: str) -> str:
        """Extract page title."""
        match = re.search(r"<title[^>]*>([^<]+)</title>", html, re.IGNORECASE)
        if match:
            return match.group(1).strip()
        return "Untitled"

    def _extract_meta(self, html: str) -> Dict[str, str]:
        """Extract meta tags."""
        meta = {}

        # Description
        match = re.search(
            r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']',
            html,
            re.IGNORECASE,
        )
        if match:
            meta["description"] = match.group(1)

        # Keywords
        match = re.search(
            r'<meta[^>]+name=["\']keywords["\'][^>]+content=["\']([^"\']+)["\']',
            html,
            re.IGNORECASE,
        )
        if match:
            meta["keywords"] = match.group(1)

        # OG tags
        for tag in ["og:title", "og:description", "og:image", "og:type"]:
            match = re.search(
                r'<meta[^>]+property=["\']' + tag + r'["\'][^>]+content=["\']([^"\']+)["\']',
                html,
                re.IGNORECASE,
            )
            if match:
                meta[tag] = match.group(1)

        return meta

    def _extract_links(self, html: str, base_url: str) -> List[Dict[str, str]]:
        """Extract links from page."""
        links = []
        seen = set()

        for match in re.finditer(
            r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]*)</a>', html, re.IGNORECASE
        ):
            href = match.group(1)
            text = match.group(2).strip()

            # Make absolute
            if not href.startswith(("http://", "https://", "mailto:", "tel:", "javascript:")):
                href = urljoin(base_url, href)

            # Skip non-http
            if not href.startswith(("http://", "https://")):
                continue

            # Dedupe
            if href in seen:
                continue
            seen.add(href)

            links.append(
                {
                    "url": href,
                    "text": text or href,
                }
            )

        return links[:100]  # Limit

    def _extract_images(self, html: str, base_url: str) -> List[Dict[str, str]]:
        """Extract images from page."""
        images = []
        seen = set()

        for match in re.finditer(r'<img[^>]+src=["\']([^"\']+)["\']', html, re.IGNORECASE):
            src = match.group(1)

            # Make absolute
            if not src.startswith(("http://", "https://", "data:")):
                src = urljoin(base_url, src)

            # Skip data URLs and dupes
            if src.startswith("data:") or src in seen:
                continue
            seen.add(src)

            # Get alt text
            alt_match = re.search(r'alt=["\']([^"\']*)["\']', match.group(0))
            alt = alt_match.group(1) if alt_match else ""

            images.append(
                {
                    "url": src,
                    "alt": alt,
                }
            )

        return images[:50]  # Limit

    def _html_to_text(self, html: str) -> str:
        """Convert HTML to plain text."""
        # Remove scripts and styles
        text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
        text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.IGNORECASE)

        # Remove tags
        text = re.sub(r"<[^>]+>", " ", text)

        # Decode entities
        text = text.replace("&nbsp;", " ")
        text = text.replace("&amp;", "&")
        text = text.replace("&lt;", "<")
        text = text.replace("&gt;", ">")
        text = text.replace("&quot;", '"')

        # Normalize whitespace
        text = re.sub(r"\s+", " ", text)

        return text.strip()

    def _parse_ddg_results(self, html: str) -> List[Dict[str, Any]]:
        """Parse DuckDuckGo HTML results."""
        results = []

        for match in re.finditer(
            r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([^<]+)</a>.*?'
            r'<a[^>]+class="result__snippet"[^>]*>([^<]+)</a>',
            html,
            re.DOTALL,
        ):
            results.append(
                {
                    "url": unquote(match.group(1)),
                    "title": match.group(2).strip(),
                    "snippet": match.group(3).strip(),
                }
            )

        return results[:10]

    def _parse_searx_results(self, content: str) -> List[Dict[str, Any]]:
        """Parse Searx JSON results."""
        try:
            data = json.loads(content)
            return [
                {
                    "url": r.get("url"),
                    "title": r.get("title"),
                    "snippet": r.get("content"),
                }
                for r in data.get("results", [])[:10]
            ]
        except json.JSONDecodeError:
            return []

    def _parse_google_results(self, html: str) -> List[Dict[str, Any]]:
        """Parse Google HTML results (basic)."""
        results = []

        # Very basic parsing - Google's HTML is complex
        for match in re.finditer(r'<a[^>]+href="/url\?q=([^&"]+)', html):
            url = unquote(match.group(1))
            if url.startswith("http"):
                results.append(
                    {
                        "url": url,
                        "title": url,
                        "snippet": "",
                    }
                )

        return results[:10]

    # ═══════════════════════════════════════════════════════════════
    # Stats
    # ═══════════════════════════════════════════════════════════════

    async def get_stats(self) -> Dict[str, Any]:
        """Get browser statistics."""
        archive_stats = await self.archive.get_stats()
        filter_stats = self.filter.get_stats()

        return {
            "privacy_mode": self.config.privacy_mode,
            "session_pages_visited": len(self.history),
            "archive": archive_stats,
            "filter": filter_stats,
            "features": {
                "ads_blocked": self.config.block_ads,
                "trackers_blocked": self.config.block_trackers,
                "scripts_blocked": self.config.block_scripts,
                "tor_enabled": self.config.tor_enabled,
            },
        }


# Skill registration
def register(agent, config):
    """Register the private browser skill."""
    return PrivateBrowserSkill(agent, config)
