"""Fetch Open Graph metadata for a single user-supplied URL.

Messages are E2E encrypted client-side, so the server never sees message
content — link previews are requested by the client, after decryption, for
one URL at a time. This makes the endpoint a straightforward SSRF surface
(arbitrary user-controlled URL, fetched server-side), so every step here is
built to fail closed: only http/https, only public/routable IPs, small size
cap, short timeout, and only HTML responses are parsed.
"""
import ipaddress
import socket
from html.parser import HTMLParser
from urllib.parse import urlparse

import requests

MAX_RESPONSE_BYTES = 1 * 1024 * 1024  # 1MB
FETCH_TIMEOUT_SECONDS = 5
ALLOWED_SCHEMES = {"http", "https"}


class LinkPreviewError(Exception):
    def __init__(self, message, status_code=400):
        super().__init__(message)
        self.message = message
        self.status_code = status_code


def _resolve_and_validate_host(hostname):
    """Raise if the hostname resolves to a private/loopback/link-local/
    reserved address — the standard SSRF-via-DNS mitigation."""
    try:
        infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        raise LinkPreviewError("Could not resolve host.")
    for info in infos:
        ip = ipaddress.ip_address(info[4][0])
        if (
            ip.is_private
            or ip.is_loopback
            or ip.is_link_local
            or ip.is_multicast
            or ip.is_reserved
            or ip.is_unspecified
        ):
            raise LinkPreviewError("URL resolves to a disallowed address.")


def _validate_url(url):
    parsed = urlparse(url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        raise LinkPreviewError("Only http/https URLs are supported.")
    if not parsed.hostname:
        raise LinkPreviewError("URL has no host.")
    _resolve_and_validate_host(parsed.hostname)
    return parsed


class _OpenGraphParser(HTMLParser):
    """Minimal og:*/title tag scraper — stdlib only, no new dependency."""

    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.meta = {}
        self.title = None
        self._in_title = False

    def handle_starttag(self, tag, attrs):
        attrs_dict = dict(attrs)
        if tag == "meta":
            prop = attrs_dict.get("property") or attrs_dict.get("name")
            content = attrs_dict.get("content")
            if prop and content and prop.startswith("og:"):
                self.meta.setdefault(prop, content)
        elif tag == "title":
            self._in_title = True

    def handle_endtag(self, tag):
        if tag == "title":
            self._in_title = False

    def handle_data(self, data):
        if self._in_title and self.title is None:
            self.title = data.strip()


def fetch_link_preview(url):
    """Return {url, title, description, image, site_name} for `url`.

    Raises LinkPreviewError (safe to surface to the client) on anything
    that isn't a plain, fetchable, public HTML page.
    """
    parsed = _validate_url(url)

    try:
        resp = requests.get(
            url,
            timeout=FETCH_TIMEOUT_SECONDS,
            stream=True,
            headers={"User-Agent": "Mozilla/5.0 (compatible; KhubLinkPreview/1.0)"},
            allow_redirects=True,
        )
    except requests.RequestException:
        raise LinkPreviewError("Could not fetch URL.", status_code=502)

    # A redirect can point at a private address even if the original URL
    # didn't — validate the final resolved URL too.
    if resp.url != url:
        _validate_url(resp.url)

    content_type = resp.headers.get("Content-Type", "")
    if "text/html" not in content_type:
        resp.close()
        raise LinkPreviewError("URL is not an HTML page.", status_code=415)

    body = b""
    for chunk in resp.iter_content(chunk_size=8192):
        body += chunk
        if len(body) > MAX_RESPONSE_BYTES:
            break
    resp.close()

    try:
        html = body.decode(resp.encoding or "utf-8", errors="replace")
    except (LookupError, UnicodeDecodeError):
        html = body.decode("utf-8", errors="replace")

    parser = _OpenGraphParser()
    try:
        parser.feed(html)
    except Exception:
        pass

    og = parser.meta
    return {
        "url": url,
        "title": og.get("og:title") or parser.title or parsed.hostname,
        "description": og.get("og:description"),
        "image": og.get("og:image"),
        "site_name": og.get("og:site_name") or parsed.hostname,
    }
