# 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 users, hiding their IP
and allowing server-side content filtering.
"""

from __future__ import annotations

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


class ProxyService:
    """
    HTTP proxy service that fetches pages for users.

    Features:
    - Async HTTP requests via aiohttp
    - Tor support for paranoid mode
    - Custom user agent
    - Cookie handling
    - Redirect following
    - Timeout handling
    """

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

        # Request stats
        self._requests = 0
        self._bytes_fetched = 0

    async def _get_session(self) -> aiohttp.ClientSession:
        """Get or create HTTP session."""
        if self._session is None or self._session.closed:
            # Configure connector
            connector_kwargs = {
                "limit": 20,
                "ttl_dns_cache": 300,
            }

            # Tor proxy for paranoid mode
            if self.config.tor_enabled:
                connector_kwargs["proxy"] = f"socks5://127.0.0.1:{self.config.tor_socks_port}"

            connector = aiohttp.TCPConnector(**connector_kwargs)

            # Session with default headers
            self._session = aiohttp.ClientSession(
                connector=connector,
                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),
                cookie_jar=aiohttp.CookieJar(unsafe=True),
            )

        return self._session

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

        Args:
            url: URL to fetch
            follow_redirects: Follow HTTP redirects
            max_redirects: Maximum redirects to follow
            timeout: Request timeout in seconds

        Returns:
            {
                'url': final URL after redirects,
                'status': HTTP status code,
                'content': page content,
                'content_type': MIME type,
                '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,
                timeout=aiohttp.ClientTimeout(total=timeout),
                ssl=self._get_ssl_context(),
            ) as response:
                # Read content
                content = await response.text(errors="replace")

                # Update stats
                self._requests += 1
                self._bytes_fetched += len(content)

                return {
                    "url": str(response.url),
                    "status": response.status,
                    "content": content,
                    "content_type": response.content_type or "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>{e}</p></body></html>",
                "content_type": "text/html",
                "headers": {},
                "error": str(e),
            }

        except asyncio.TimeoutError:
            logger.error(f"Timeout fetching {url}")
            return {
                "url": url,
                "status": 0,
                "content": "<html><body><h1>Timeout</h1><p>Request timed out</p></body></html>",
                "content_type": "text/html",
                "headers": {},
                "error": "timeout",
            }

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

        Args:
            url: URL to POST to
            data: Form data
            timeout: Request timeout

        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,
                timeout=aiohttp.ClientTimeout(total=timeout),
                ssl=self._get_ssl_context(),
            ) as response:
                content = await response.text(errors="replace")

                self._requests += 1
                self._bytes_fetched += len(content)

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

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

    async def fetch_binary(
        self,
        url: str,
        max_size: int = 10 * 1024 * 1024,  # 10MB
    ) -> Dict[str, Any]:
        """
        Fetch binary content (images, PDFs, etc.).

        Args:
            url: URL to fetch
            max_size: Maximum file size to download

        Returns:
            {
                'url': URL,
                'content': bytes,
                'content_type': MIME type,
            }
        """
        if not HAS_AIOHTTP:
            return {"url": url, "content": b"", "content_type": "", "error": "aiohttp required"}

        session = await self._get_session()

        try:
            async with session.get(url, ssl=self._get_ssl_context()) as response:
                # Check size
                size = int(response.headers.get("Content-Length", 0))
                if size > max_size:
                    return {
                        "url": url,
                        "content": b"",
                        "content_type": response.content_type,
                        "error": f"File too large: {size} bytes",
                    }

                content = await response.read()

                if len(content) > max_size:
                    return {
                        "url": url,
                        "content": b"",
                        "content_type": response.content_type,
                        "error": f"File too large: {len(content)} bytes",
                    }

                self._requests += 1
                self._bytes_fetched += len(content)

                return {
                    "url": str(response.url),
                    "content": content,
                    "content_type": response.content_type,
                }

        except Exception as e:
            logger.error(f"Binary fetch error for {url}: {e}")
            return {
                "url": url,
                "content": b"",
                "content_type": "",
                "error": str(e),
            }

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

        Args:
            url: URL to check

        Returns:
            Headers and status
        """
        if not HAS_AIOHTTP:
            return {"url": url, "status": 0, "headers": {}}

        session = await self._get_session()

        try:
            async with session.head(url, ssl=self._get_ssl_context()) as response:
                return {
                    "url": str(response.url),
                    "status": response.status,
                    "headers": dict(response.headers),
                    "content_type": response.content_type,
                    "content_length": response.headers.get("Content-Length"),
                }
        except Exception as e:
            return {
                "url": url,
                "status": 0,
                "headers": {},
                "error": str(e),
            }

    def _get_ssl_context(self):
        """Get SSL context."""
        # In paranoid mode, be stricter
        if self.config.privacy_mode == "paranoid":
            ctx = ssl.create_default_context()
            ctx.minimum_version = ssl.TLSVersion.TLSv1_2
            return ctx
        return None  # Use default

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

        def _sync_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(),
                        "headers": dict(response.headers),
                    }
            except urllib.error.URLError as e:
                return {
                    "url": url,
                    "status": 0,
                    "content": f"<html><body><h1>Error</h1><p>{e}</p></body></html>",
                    "content_type": "text/html",
                    "headers": {},
                    "error": str(e),
                }

        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, _sync_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 _sync_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(),
                        "headers": dict(response.headers),
                    }
            except urllib.error.URLError as e:
                return {
                    "url": url,
                    "status": 0,
                    "content": f"<html><body><h1>Error</h1><p>{e}</p></body></html>",
                    "content_type": "text/html",
                    "headers": {},
                    "error": str(e),
                }

        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, _sync_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 {
            "requests": self._requests,
            "bytes_fetched": self._bytes_fetched,
            "bytes_fetched_mb": round(self._bytes_fetched / (1024 * 1024), 2),
            "tor_enabled": self.config.tor_enabled,
        }
