# Generated by the bali-cli.  DO NOT EDIT!
# Please updated the version when utils changed.

# bali-cli utils `v20220513`

import hashlib
from datetime import datetime, date
from urllib.parse import quote

from grpc import insecure_channel
from google.protobuf import json_format
from google.protobuf.empty_pb2 import Empty

from ._config import GRPC_ADDRESS

# `GRPC_ADDRESS` is Generic gRPC channel target
# `GRPC_ADDRESS_EXTENDS` is gRPC channel target specified by service abbreviation
try:
    from .._config import GRPC_ADDRESS_EXTENDS
except ImportError:
    GRPC_ADDRESS_EXTENDS = {}

# gRPC connect channel options
try:
    from .._config import GRPC_CHANNEL_OPTIONS as channel_options
except ImportError:
    channel_options = None

# All RPC clients inherited from the `ClientMixin`
class ClientMixin:
    def _get_channel_target(self):
        target_settings = GRPC_ADDRESS
        if self._service_abbr in GRPC_ADDRESS_EXTENDS:
            target_settings = GRPC_ADDRESS_EXTENDS[self._service_abbr]
        return f"{target_settings['host']}:{target_settings['port']}"

    def check_cache(self, service, rpc_name, options, *args, **kwargs):
        cache_timeout = options.get('timeout')
        refresh = options.get('refresh')
        if not self.cache:
            return None, None

        cache_key = make_cache_key(service, rpc_name, *args, **kwargs)
        if refresh:
            self.cache.delete(cache_key)
            self.logger.info("gRPC client of %s.%s cache cleared: %s", service, rpc_name, cache_key)
        # check cache
        if cache_timeout:
            cached = self.cache.get(cache_key)
            if cached is not None:
                self.logger.info("gRPC client of %s.%s received(hit cache): %s", service, rpc_name, cached)
                return cached, cache_key

        return None, None

    def set_cache(self, cache_key, result, cache_timeout):
        if self.cache and cache_timeout:
            self.cache.set(cache_key, result, timeout=cache_timeout)

    def request(self, service, rpc_name, request_protobuf, schema, response_schema_cls, fail_silently, cache_options):
        """rpc request entry

        :param service: Service name like `UserService`
        :param rpc_name: Service name like `GetUser`
        :param request_protobuf:
        :param schema: RPC request schema objects, it's None when message type is Empty
        :param response_schema_cls:
        :param fail_silently: failed silently
        :param cache_options: cache options include `timeout` and `refresh`
        :return:
        """
        if schema:
            cached, cache_key = self.check_cache(service, rpc_name, cache_options, schema)
        else:
            cached, cache_key = self.check_cache(service, rpc_name, cache_options)
        if cached is not None:
            return cached

        stub_cls = getattr(self.pb2_grpc, f'{service}Stub')
        request_data = ParseDict(schema.dict(), getattr(self.pb2, request_protobuf)()) if schema else Empty()

        channel_target = self._get_channel_target()
        with insecure_channel(channel_target, options=channel_options) as channel:
            stub = stub_cls(channel)
            try:
                self.logger.info("gRPC client of %s.%s send: %s. <%s>", service, rpc_name, request_data, type(request_data))
                response = getattr(stub, rpc_name)(request_data)
            except Exception as ex:
                self.logger.error("gRPC client of %s.%s exception: %s", service, rpc_name, ex)
                if not fail_silently:
                    raise
                self.logger.warning(repr(ex))
                reply = {}
            else:
                reply = MessageToDict(
                    response,
                    including_default_value_fields=True,
                    preserving_proto_field_name=True,
                )

            self.logger.info("gRPC client of %s.%s received: %s", service, rpc_name, reply)

            result = response_schema_cls(**reply)

            cache_timeout = cache_options.get('timeout')
            self.set_cache(cache_key, result, cache_timeout)
            return result


class ProtobufParser(json_format._Parser):  # noqa
    def _ConvertValueMessage(self, value, message):
        """Convert a JSON representation into Value message."""
        if isinstance(value, dict):
            self._ConvertStructMessage(value, message.struct_value)
        elif isinstance(value, list):
            self._ConvertListValueMessage(value, message.list_value)
        elif isinstance(value, (datetime, date)):
            message.string_value = value.isoformat()
        elif value is None:
            message.null_value = 0
        elif isinstance(value, bool):
            message.bool_value = value
        elif isinstance(value, json_format.six.string_types):
            message.string_value = value
        elif isinstance(value, json_format._INT_OR_FLOAT):  # noqa
            message.number_value = value
        else:
            raise json_format.ParseError(
                'Value {0} has unexpected type {1}.'.format(value, type(value))
            )


class ProtobufPrinter(json_format._Printer):  # noqa
    def _FieldToJsonObject(self, field, value):
        """Converts field value according to Proto3 JSON Specification."""
        if field.cpp_type == json_format.descriptor.FieldDescriptor.CPPTYPE_MESSAGE:
            return self._MessageToJsonObject(value)
        elif field.cpp_type == json_format.descriptor.FieldDescriptor.CPPTYPE_ENUM:
            if self.use_integers_for_enums:
                return value
            if field.enum_type.full_name == 'google.protobuf.NullValue':
                return None
            enum_value = field.enum_type.values_by_number.get(value, None)
            if enum_value is not None:
                return enum_value.name
            else:
                if field.file.syntax == 'proto3':
                    return value
                raise json_format.SerializeToJsonError(
                    'Enum field contains an integer value '
                    'which can not mapped to an enum value.'
                )
        elif field.cpp_type == json_format.descriptor.FieldDescriptor.CPPTYPE_STRING:
            if field.type == json_format.descriptor.FieldDescriptor.TYPE_BYTES:
                # Use base64 Data encoding for bytes
                return json_format.base64.b64encode(value).decode('utf-8')
            else:
                return value
        elif field.cpp_type == json_format.descriptor.FieldDescriptor.CPPTYPE_BOOL:
            return bool(value)
        elif field.cpp_type in json_format._INT64_TYPES:  # noqa
            return str(value)
        elif field.cpp_type in json_format._FLOAT_TYPES:  # noqa
            if json_format.math.isinf(value):
                if value < 0.0:
                    return json_format._NEG_INFINITY  # noqa
                else:
                    return json_format._INFINITY  # noqa
            if json_format.math.isnan(value):
                return json_format._NAN  # noqa
            if self.float_format:
                return float(format(value, self.float_format))
            else:
                converted_i = int(value)
                converted_f = float(value)
                return converted_i if converted_f == converted_i else converted_f

        return value


def MessageToDict(  # noqa
        message,
        including_default_value_fields=False,
        preserving_proto_field_name=False,
        use_integers_for_enums=False,
        descriptor_pool=None,
        float_precision=None
):
    printer = ProtobufPrinter(
        including_default_value_fields,
        preserving_proto_field_name,
        use_integers_for_enums,
        descriptor_pool,
        float_precision=float_precision
    )
    return printer._MessageToJsonObject(message)  # noqa


def ParseDict(  # noqa
    js_dict, message, ignore_unknown_fields=False, descriptor_pool=None
):
    parser = ProtobufParser(ignore_unknown_fields, descriptor_pool)
    parser.ConvertMessage(js_dict, message)
    return message


def make_cache_key(service, method, *args, **kwargs):
    """Make cache key

    ref: https://github.com/peterbe/django-cache-memoize/blob/master/src/cache_memoize/__init__.py
    cache_memoize._default_make_cache_key
    """
    cache_key = ":".join(
        [quote(str(x)) for x in args]
        + [quote("{}={}".format(k, v)) for k, v in kwargs.items()]
    )
    hashed = hashlib.md5(
        bytes("cache_memoize" + (method + cache_key), encoding='utf-8')
    ).hexdigest()
    return 'grpc_cache:%s:%s:%s' % (service, method, hashed)
