"""A module to redirect image file loading to imgproxy for more
efficient and smaller image formats.

This module defines a util.imgproxy_url class that can be
used in place of download_url in img tags.

E.G. replace

    <img tal:attributes="src file/download_url; alt file/name"
        loading="lazy">

with:

    <img tal:attributes="src
       python:utils.imgproxy_url(file, ['rs:fit:400:300',
                                        'preset:mark' ]);
       alt file/name" loading="lazy">

to load the image at a size of 400x300 and have a watermark
placed on it.

It is controlled by the following settings in
extensions/config.ini:

-----
[imgproxy]

# URL for imgproxy serving up your tracker's images.
# Must have trailing /.
# If not set, regular download_url for the attached file is generated.
url = https://example.net/tracker/imgproxy/

# Hex encoded random key used for signature hash.
# Can be stored in a file. Use file://filename relative to
# extensions directory.
# Shell command to generate suitable string is:
#   echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')
#
# If not set, unsigned requests are generated for imgproxy.
# if Accepted by imgproxy, this allows attached files to
# be accessed by unauthorized clients.
key = 736563726574

# Hex encoded random salt used for signature hash.
# See key documentation for details.
salt = file://../secrets/imgproxy.salt

# Integer number of seconds that the image url is cached/valid
# (default 5 seconds). Must be > 0. If you are lazy loading
# the images on a page with a lot of images, you need to
# increase this time otherwise images will fail to load.
#ttl = 10

# Add a watermark to images. Assumes IMGPROXY_WATERMARK_DATA
# or other watermark source is defined in imgproxy environment
# (default False)
#watermark = on
------

See: https://imgproxy.net/
     https://github.com/imgproxy/imgproxy
"""

import binascii
import base64
import hashlib
import hmac
import logging
import time

from roundup.anypy.strings import b2s, bs2b
from roundup.anypy.urllib_ import quote
from roundup.configuration import BooleanOption, \
    IntegerNumberOption, \
    InvalidOptionError, \
    OptionValueError, \
    SecretOption, \
    WebUrlOption

try:
    fromhex = bytes.fromhex
except AttributeError:
    fromhex = binascii.unhexlify

logger = logging.getLogger('extension')


def make_signature(processing_url, key_hex, salt_hex):
    """Generate the signature for the processing_url with key/salt

       Used to limit access to the image url passed to
       imgproxy by signing the processing_url.
    """
    key = fromhex(key_hex)
    salt = fromhex(salt_hex)

    path = bs2b(processing_url)
    digest = hmac.new(key, msg=salt+path,
                      digestmod=hashlib.sha256).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b"=")


def get_setting(config, setting, default=""):
    """Get a setting from a detector or extension url with
       default value if not defined.
    """
    try:
        return config[setting]
    except InvalidOptionError:
        return default


def imgproxy_url(file_obj, render_options=None):
    """Take a file object (with a download_url method) and an
       optional list of imgproxy processing
       options. Specified options take precedence over the
       ones added by this function (filename, watermark, expires).

       Return a url to the imgproxy image server that has
       access to the tracker's db/files/file directory
       tree. Processing directives and other doc at:
       https://docs.imgproxy.net/.
    """

    ext_config = file_obj._db.config.ext

    url_prefix = get_setting(ext_config, 'IMGPROXY_URL', None)

    if url_prefix is None:
        # can't use imgproxy without the url.
        # this is a safety since it should never happen
        # as this function is not called if url not defined.
        return file_obj.download_url()

    key = get_setting(ext_config, 'IMGPROXY_KEY', None)
    salt = get_setting(ext_config, 'IMGPROXY_SALT', None)
    ttl = get_setting(ext_config, 'IMGPROXY_TTL', 5)
    watermark = get_setting(ext_config, 'IMGPROXY_WATERMARK', False)

    if not render_options:
        # basic resize 400x300 no shrink or expand
        render_options = ["rs:fill:400:300:0:0"]

    # Insert default/configured options at start of
    # render_options. Options later in the URL take
    # precedence. So watermark can be overridden by
    # 'watermark:0' set in render_options passed to
    # imgproxy_url().

    # Set filename to basename of image file.
    # Suffix is added by imgproxy to match image format.
    name = file_obj._klass.get(file_obj._nodeid, 'name')
    render_options.insert(0, "filename:%s" % quote(name[:name.rindex('.')]))

    # Add the watermark. Assumes IMGPROXY_WATERMARK_DATA or
    # other watermark definition is defined in imgproxy
    # environment.
    if watermark:
        render_options.insert(0, "watermark:0.3:sowe")

    if key and salt:
        # Set URL expiration time.
        # If there is no signature hash, any user can change the
        # expire time and resend the URL. So don't bother
        # setting it.
        render_options.insert(0, "expires:%i" % (time.time() + ttl))

    rendering = "/".join(render_options)
    local_url = "local:///%s" % file_obj._db.subdirFilename(
        file_obj._klass.classname,
        file_obj._nodeid)

    processing_url = "/%(rendering)s/plain/%(local_url)s" % {
        "rendering": rendering, "local_url": local_url, }

    if key and salt:
        signature = b2s(make_signature(processing_url, key, salt))
    else:
        signature = "not_signed"

    imgproxy_url = "%(url_prefix)s%(signature)s%(processing_url)s" % {
        "url_prefix": url_prefix, "signature": signature,
        "processing_url": processing_url}

    return imgproxy_url


def init(instance):
    # verify options values meet standards.
    try:
        instance.config.ext.update_option(
            'IMGPROXY_URL', WebUrlOption,
            description="Url for imgproxy server.")
    except InvalidOptionError:
        # It's not set, that's ok. Just call download_url()
        # and skip everything else in this module.
        instance.registerUtil(
            'imgproxy_url',
            lambda file_obj, _ignore=None: file_obj.download_url())
        return

    try:
        option_name = 'IMGPROXY_KEY'
        instance.config.ext.update_option(
            option_name, SecretOption,
            description="Random HMAC key, hex encoded.")

        try:
            fromhex(instance.config.ext[option_name])
        except ValueError:
            raise OptionValueError(
                instance.config.ext.options[option_name],
                instance.config.ext[option_name],
                "in extensions/config.ini. Is not a valid hex encoded string."
            )
    except InvalidOptionError:
        # It's not set, that's ok.
        pass

    try:
        option_name = 'IMGPROXY_SALT'
        instance.config.ext.update_option(
            option_name, SecretOption,
            description="Random salt used with HMAC key, hex encoded.")

        try:
            fromhex(instance.config.ext[option_name])
        except ValueError:
            raise OptionValueError(
                instance.config.ext.options[option_name],
                instance.config.ext[option_name],
                "in extensions/config.ini. Is not a valid hex encoded string."
            )
    except InvalidOptionError:
        # It's not set, that's ok.
        pass

    try:
        instance.config.ext.update_option(
            'IMGPROXY_TTL', IntegerNumberOption, default=5,
            description="Lifetime in integer seconds of image URL.")
    except InvalidOptionError:
        # It's not set, that's ok. Default is 5 seconds.
        pass

    try:
        instance.config.ext.update_option(
            'IMGPROXY_WATERMARK', BooleanOption, default=False,
            description="Print watermark on images.")
    except InvalidOptionError:
        # It's not set, no watermark.
        pass

    instance.registerUtil('imgproxy_url', imgproxy_url)


if __name__ == '__main__':
    """ Test make_signature() using imgproxy example signature
        settings.
    """
    url = ('/rs:fill:300:400:0/g:sm/aHR0cDovL2V4YW1w/bGUuY29tL2l'
           'tYWdl/cy9jdXJpb3NpdHku/anBn.png')
    print(url)

    s = make_signature(url, '736563726574', '68656C6C6F')

    print(s)

    assert b2s(s) == "oKfUtW34Dvo2BGQehJFR4Nr0_rIjOtdtzJ3QFsUcXH8"

    print("Test passed, no errors")
