__all__ = ['get_url', 'download_bytes', 'download_image']

import io
import urllib.parse as _urlparse
import PIL as pil
import urllib as ul


_MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024  # 50 MB
_ALLOWED_SCHEMES = frozenset({'http', 'https'})


def get_url(url, max_bytes=_MAX_DOWNLOAD_BYTES):
    """Download the content at *url* and return it as bytes.

    Only ``http`` and ``https`` URLs are accepted.  ``file://``, ``ftp://``,
    and other schemes are rejected with :exc:`ValueError` to prevent
    server-side request forgery (SSRF) attacks.

    Uses a custom User-Agent header without modifying the global urllib opener.
    Raises urllib.error.URLError on network errors.  Reads at most *max_bytes*
    to avoid unbounded memory consumption on unexpectedly large responses.
    """
    parsed = _urlparse.urlparse(url)
    if parsed.scheme not in _ALLOWED_SCHEMES:
        raise ValueError(
            "Unsupported URL scheme '{0}'. Only 'http' and 'https' are "
            "allowed.".format(parsed.scheme))

    opener = ul.request.build_opener()

    opener.addheaders = [
        (
            'User-Agent',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
            '(KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36'
        )]

    response = opener.open(url, timeout=30)
    return response.read(max_bytes)


def download_bytes(url):
    return io.BytesIO(
        get_url(url))


def download_image(url):
    """Download an image from *url* and return a PIL Image.

    Raises:
        ValueError: if the URL scheme is not http or https.
        urllib.error.URLError: on network errors.
        RuntimeError: if the downloaded bytes cannot be decoded as an image.
    """
    try:
        return pil.Image.open(download_bytes(url))
    except Exception as exc:
        if isinstance(exc, ValueError):
            raise
        raise RuntimeError(
            "Failed to decode image from '{0}': {1}".format(url, exc)) from exc