# 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

"""
Proxy Service for Private Browser.

Fetches web pages on behalf of the user, hiding their IP address.
Supports direct connections and Tor routing.
"""

import asyncio
import logging
import ssl
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

# Optional aiohttp for async HTTP
try:
    import aiohttp

    HAS_AIOHTTP = True
except ImportError:
    HAS_AIOHTTP = False
    logger.debug("aiohttp not installed - using urllib")

# Optional Tor support
try:
    import aiohttp_socks

    HAS_TOR = True
except ImportError:
    HAS_TOR = False


class ProxyService:
    """
    HTTP proxy service for private browsing.

    All web requests go through this service, so the user's
    IP address is never exposed to websites.
    """

    def __init__(self, config):
        self.config = config
        self._session: Optional[aiohttp.ClientSession] = None

        # Request stats
        self._total_requests = 0
        self._total_bytes = 0

    async def _get_session(self) -> aiohttp.ClientSession:
        """Get or create HTTP session."""
        if self._session is None or self._session.closed:
            # Configure connector
            if self.config.tor_enabled and HAS_TOR:
                # Tor SOCKS proxy
                connector = aiohttp_socks.ProxyConnector.from_url(
                    f"socks5://127.0.0.1:{self.config.tor_socks_port}"
                )
                logger.info("Using Tor for requests")
            else:
                # Direct connection with SSL
                connector = aiohttp.TCPConnector(
                    ssl=ssl.create_default_context(),
                    limit=10,
                    limit_per_host=5,
                )

            # Default headers
            headers = {
                "User-Agent": self.config.user_agent,
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Language": "en-US,en;q=0.5",
                "Accept-Encoding": "gzip, deflate",
                "DNT": "1",
                "Connection": "keep-alive",
                "Upgrade-Insecure-Requests": "1",
            }

            timeout = aiohttp.ClientTimeout(total=30, connect=10)

            self._session = aiohttp.ClientSession(
                connector=connector,
                headers=headers,
                timeout=timeout,
            )

        return self._session

    async def fetch(
        self,
        url: str,
        follow_redirects: bool = True,
        max_redirects: int = 10,
    ) -> Dict[str, Any]:
        """
        Fetch a URL.

        Args:
            url: URL to fetch
            follow_redirects: Follow HTTP redirects
            max_redirects: Maximum redirects to follow

        Returns:
            {
                'url': final URL after redirects,
                'status': HTTP status code,
                'content': response body,
                'content_type': Content-Type header,
                'headers': response headers,
            }
        """
        # SSRF protection: block requests to private/internal networks
        from familiar.core.sanitization import check_ssrf

        is_safe, ssrf_msg = check_ssrf(url)
        if not is_safe:
            return {
                "url": url,
                "status": 0,
                "content": f"<html><body><h1>Blocked</h1><p>{ssrf_msg}</p></body></html>",
                "content_type": "text/html",
                "headers": {},
                "error": ssrf_msg,
            }

        if not HAS_AIOHTTP:
            return await self._fetch_urllib(url)

        session = await self._get_session()

        try:
            async with session.get(
                url,
                allow_redirects=follow_redirects,
                max_redirects=max_redirects,
            ) as response:
                content = await response.text()

                self._total_requests += 1
                self._total_bytes += len(content)

                return {
                    "url": str(response.url),
                    "status": response.status,
                    "content": content,
                    "content_type": response.headers.get("Content-Type", "text/html"),
                    "headers": dict(response.headers),
                }

        except aiohttp.ClientError as e:
            logger.error(f"Fetch error for {url}: {e}")
            return {
                "url": url,
                "status": 0,
                "content": f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>",
                "content_type": "text/html",
                "headers": {},
                "error": str(e),
            }

    async def post(
        self,
        url: str,
        data: Dict[str, str],
    ) -> Dict[str, Any]:
        """
        POST data to a URL.

        Args:
            url: URL to post to
            data: Form data

        Returns:
            Same as fetch()
        """
        if not HAS_AIOHTTP:
            return await self._post_urllib(url, data)

        session = await self._get_session()

        try:
            async with session.post(url, data=data) as response:
                content = await response.text()

                self._total_requests += 1
                self._total_bytes += len(content)

                return {
                    "url": str(response.url),
                    "status": response.status,
                    "content": content,
                    "content_type": response.headers.get("Content-Type", "text/html"),
                    "headers": dict(response.headers),
                }

        except aiohttp.ClientError as e:
            logger.error(f"POST error for {url}: {e}")
            return {
                "url": url,
                "status": 0,
                "content": f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>",
                "content_type": "text/html",
                "headers": {},
                "error": str(e),
            }

    async def fetch_binary(
        self,
        url: str,
    ) -> Dict[str, Any]:
        """
        Fetch binary content (images, PDFs, etc.).

        Returns:
            {
                'url': URL,
                'content': bytes,
                'content_type': Content-Type,
            }
        """
        session = await self._get_session()

        try:
            async with session.get(url) as response:
                content = await response.read()

                self._total_requests += 1
                self._total_bytes += len(content)

                return {
                    "url": str(response.url),
                    "status": response.status,
                    "content": content,
                    "content_type": response.headers.get(
                        "Content-Type", "application/octet-stream"
                    ),
                }

        except aiohttp.ClientError as e:
            logger.error(f"Binary fetch error for {url}: {e}")
            return {
                "url": url,
                "status": 0,
                "content": b"",
                "content_type": "application/octet-stream",
                "error": str(e),
            }

    async def head(self, url: str) -> Dict[str, Any]:
        """
        HEAD request to get headers only.
        """
        session = await self._get_session()

        try:
            async with session.head(url, allow_redirects=True) as response:
                self._total_requests += 1

                return {
                    "url": str(response.url),
                    "status": response.status,
                    "headers": dict(response.headers),
                }

        except aiohttp.ClientError as e:
            return {
                "url": url,
                "status": 0,
                "headers": {},
                "error": str(e),
            }

    async def _fetch_urllib(self, url: str) -> Dict[str, Any]:
        """Fallback fetch using urllib."""
        import urllib.error
        import urllib.request

        def _do_fetch():
            req = urllib.request.Request(url, headers={"User-Agent": self.config.user_agent})

            try:
                with urllib.request.urlopen(req, timeout=30) as response:
                    content = response.read().decode("utf-8", errors="replace")
                    return {
                        "url": response.url,
                        "status": response.status,
                        "content": content,
                        "content_type": response.headers.get("Content-Type", "text/html"),
                        "headers": dict(response.headers),
                    }
            except urllib.error.URLError as e:
                return {
                    "url": url,
                    "status": 0,
                    "content": f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>",
                    "content_type": "text/html",
                    "headers": {},
                    "error": str(e),
                }

        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, _do_fetch)

    async def _post_urllib(self, url: str, data: Dict[str, str]) -> Dict[str, Any]:
        """Fallback POST using urllib."""
        import urllib.error
        import urllib.parse
        import urllib.request

        def _do_post():
            encoded = urllib.parse.urlencode(data).encode("utf-8")
            req = urllib.request.Request(
                url, data=encoded, headers={"User-Agent": self.config.user_agent}
            )

            try:
                with urllib.request.urlopen(req, timeout=30) as response:
                    content = response.read().decode("utf-8", errors="replace")
                    return {
                        "url": response.url,
                        "status": response.status,
                        "content": content,
                        "content_type": response.headers.get("Content-Type", "text/html"),
                        "headers": dict(response.headers),
                    }
            except urllib.error.URLError as e:
                return {
                    "url": url,
                    "status": 0,
                    "content": f"<html><body><h1>Error</h1><p>{str(e)}</p></body></html>",
                    "content_type": "text/html",
                    "headers": {},
                    "error": str(e),
                }

        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, _do_post)

    async def close(self):
        """Close the session."""
        if self._session and not self._session.closed:
            await self._session.close()

    def get_stats(self) -> Dict[str, Any]:
        """Get proxy statistics."""
        return {
            "total_requests": self._total_requests,
            "total_bytes": self._total_bytes,
            "total_mb": round(self._total_bytes / (1024 * 1024), 2),
            "tor_enabled": self.config.tor_enabled,
        }
