# 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 server-side
- AI-powered summarization and assistance
- Reader mode for clean reading
- Encrypted page archive
- No direct connection from user to websites
"""

import asyncio
import hashlib  # noqa: F401
import json  # noqa: F401
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_mode: str = "standard"  # standard, strict, paranoid
    block_ads: bool = True
    block_trackers: bool = True
    block_scripts: bool = False
    block_images: bool = False
    reader_mode_default: bool = False
    auto_summarize: bool = False
    archive_enabled: bool = True
    storage_path: str = "~/.familiar/browser"
    max_archive_size_mb: int = 1000
    tor_enabled: bool = False
    tor_socks_port: int = 9050
    user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"

    def __post_init__(self):
        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
    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]
    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.
    """

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

    def __init__(self, agent, config: Dict[str, Any]):
        self.agent = agent
        self.config = BrowserConfig(**config.get("private_browser", {}))
        self.storage_path = Path(self.config.storage_path).expanduser()
        self.storage_path.mkdir(parents=True, exist_ok=True)

        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

        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."""
        if hasattr(self.proxy, "close"):
            await self.proxy.close()

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

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

        response = await self.proxy.fetch(url)

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

        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"])

        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"])

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

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

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

        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,
        )

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

        self._add_to_history(result)
        return result

    async def search(self, query: str, engine: str = "duckduckgo") -> List[Dict[str, Any]]:
        """Search the web privately."""
        search_urls = {
            "duckduckgo": f"https://html.duckduckgo.com/html/?q={quote(query)}",
            "searx": f"https://searx.be/search?q={quote(query)}&format=json",
        }
        url = search_urls.get(engine, search_urls["duckduckgo"])
        response = await self.proxy.fetch(url)
        return self._parse_search_results(response["content"], engine)

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

        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."

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

    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:
            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")
        return await self.archive.save(result)

    async def get_archived(self, archive_id: str):
        """Retrieve an archived page."""
        return await self.archive.load(archive_id)

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

    async def search_archive(self, query: str):
        """Full-text search across archived pages."""
        return await self.archive.search(query)

    def get_history(self, limit: int = 20):
        """Get recent history."""
        return self.history[-limit:][::-1]

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

    def _add_to_history(self, result: BrowseResult):
        self.history.append(
            {
                "url": result.url,
                "final_url": result.final_url,
                "title": result.title,
                "timestamp": result.timestamp.isoformat(),
                "content_text": result.content_text[:1000],
            }
        )
        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history :]

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

    def _extract_meta(self, html: str) -> Dict[str, str]:
        meta = {}
        for pattern, key in [
            (
                r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']',
                "description",
            ),
            (r'<meta[^>]+property=["\']og:title["\'][^>]+content=["\']([^"\']+)["\']', "og:title"),
        ]:
            match = re.search(pattern, html, re.IGNORECASE)
            if match:
                meta[key] = match.group(1)
        return meta

    def _extract_links(self, html: str, base_url: str) -> List[Dict[str, str]]:
        links = []
        seen = set()
        for match in re.finditer(
            r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>([^<]*)</a>', html, re.IGNORECASE
        ):
            href, text = match.group(1), match.group(2).strip()
            if not href.startswith(("http://", "https://")):
                href = urljoin(base_url, href)
            if href.startswith(("http://", "https://")) and href not in seen:
                seen.add(href)
                links.append({"url": href, "text": text or href})
        return links[:100]

    def _extract_images(self, html: str, base_url: str) -> List[Dict[str, str]]:
        images = []
        seen = set()
        for match in re.finditer(r'<img[^>]+src=["\']([^"\']+)["\']', html, re.IGNORECASE):
            src = match.group(1)
            if not src.startswith(("http://", "https://", "data:")):
                src = urljoin(base_url, src)
            if not src.startswith("data:") and src not in seen:
                seen.add(src)
                images.append({"url": src, "alt": ""})
        return images[:50]

    def _html_to_text(self, html: str) -> str:
        text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
        text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.IGNORECASE)
        text = re.sub(r"<[^>]+>", " ", text)
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    def _parse_search_results(self, html: str, engine: str) -> List[Dict[str, Any]]:
        results = []
        if engine == "duckduckgo":
            for match in re.finditer(
                r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([^<]+)</a>', html
            ):
                results.append({"url": unquote(match.group(1)), "title": match.group(2).strip()})
        return results[:10]

    async def get_stats(self) -> Dict[str, Any]:
        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,
        }


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


__all__ = ["PrivateBrowserSkill", "BrowserConfig", "BrowseResult", "register"]
