"""
Copyright (C) 2021 Kaskada Inc. All rights reserved.

This package cannot be used, copied or distributed without the express 
written permission of Kaskada Inc.

For licensing inquiries, please contact us at info@kaskada.com.
"""

from kaskada.client import Client

import grpc
import os

from grpc_status import rpc_status
from google.rpc import error_details_pb2

KASKADA_DEFAULT_CLIENT = None

def init(**kwags):
    is_demo = kwags.pop('demo', False)
    if is_demo:
        demo_only(**kwags)
    else:
        client(**kwags)

def demo_only(**kwags):
    kwags['client_id'] = kwags.pop('client_id', 'lWYFx0020u4oulh7Z9UB8C5YXVRHNyk4')
    global KASKADA_DEFAULT_CLIENT
    KASKADA_DEFAULT_CLIENT = Client.demo_only(**kwags)
    return KASKADA_DEFAULT_CLIENT

def client(**kwags):
    client_id = kwags.pop('client_id', os.getenv('KASKADA_CLIENT_ID'))
    if client_id == None:
        raise ValueError("Unable to read Kaskada Client ID. Please provide client_id key word argument or set env var KASKADA_CLIENT_ID.")
    client_secret = kwags.pop('client_secret', os.getenv('KASKADA_CLIENT_SECRET'))
    if client_secret == None:
        raise ValueError("Unable to read Kaskada Client Secret. Please provide client_secret key word argument or set env var KASKADA_CLIENT_SECRET.")

    kwags['client_id'] = client_id
    kwags['client_secret'] = client_secret
    
    global KASKADA_DEFAULT_CLIENT
    KASKADA_DEFAULT_CLIENT = Client.authorized(**kwags)
    return KASKADA_DEFAULT_CLIENT

def handleGrpcError(rpc_error: grpc.Call):
    """
    All methods calling `handleGrpcError` do so after detecting a RpcError.  Currently
    all our grpc calls perform a `UnaryUnaryMultiCallable` method, docs linked here:
    https://grpc.github.io/grpc/python/_modules/grpc.html#UnaryUnaryMultiCallable

    Calling a `UnaryUnaryMultiCallable` method results in one of the following:
        Returns:
            The response value for the RPC.

        Raises:
            RpcError: Indicating that the RPC terminated with non-OK status. The
            raised RpcError will also be a grpc.Call for the RPC affording the RPC's
            metadata, status code, and details.

    Methods on a `grpc.Call` object include the following.  Full docs here:
    https://grpc.github.io/grpc/python/_modules/grpc.html#Call

        code(): The StatusCode value for the RPC.
        details(): The details string of the RPC.

    The `grpc_status.rpc_status.from_call(call)` method helps extract details
    objects from `grpc.Call` objects. Docs here: 
    https://grpc.github.io/grpc/python/_modules/grpc_status/rpc_status.html#from_call

        Args:
            call: A grpc.Call instance.

        Returns:
            A google.rpc.status.Status message representing the status of the RPC.

        Raises:
            ValueError: If the gRPC call's code or details are inconsistent with the
            status code and message inside of the google.rpc.status.Status.
    """
    errorMessage = "An error occurred in your request.\n\tError Code: {}\n".format(rpc_error.code().name)
    try:
        status = rpc_status.from_call(rpc_error)
        if status:
            unpacked = []
            errorMessage += "\tError Message: {}\n".format(status.message)
            for detail in status.details:
                try:
                    val = unpack_details(detail)
                    unpacked.append(val)
                except Exception:
                    # maybe report back to wren in the future
                    pass
            if len(unpacked) > 0:
                errorMessage += "\tDetails: {}\n".format(unpacked)
            raise Exception(errorMessage) from None
    except ValueError:
        # maybe report back to wren in the future
        pass
    errorMessage += "\tError Message: {}\n".format(rpc_error.details())
    raise Exception(errorMessage) from None

def unpack_details(grpc_detail):
    """Unpack a grpc status detail field (which is a 'google.protobuf.Any' type).
    `Unpack()` checks the descriptor of the passed-in message object against the stored one 
    and returns False if they don't match and does not attempt any unpacking; True otherwise.
        Source: 
            https://github.com/protocolbuffers/protobuf/blob/master/python/google/protobuf/internal/well_known_types.py#L81
            https://github.com/protocolbuffers/protobuf/blob/master/python/google/protobuf/internal/python_message.py#L1135

        Raises: 
            google.protobuf.message.DecodeError: If it can't deserialize the message object

    `Is()` checks if the `Any` message represents the given protocol buffer type.
    """
    if grpc_detail.Is(error_details_pb2.BadRequest.DESCRIPTOR):
        val = error_details_pb2.BadRequest()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.PreconditionFailure.DESCRIPTOR):
        val = error_details_pb2.PreconditionFailure()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.RetryInfo.DESCRIPTOR):
        val = error_details_pb2.RetryInfo()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.DebugInfo.DESCRIPTOR):
        val = error_details_pb2.DebugInfo()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.QuotaFailure.DESCRIPTOR):
        val = error_details_pb2.QuotaFailure()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.RequestInfo.DESCRIPTOR):
        val = error_details_pb2.RequestInfo()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.ResourceInfo.DESCRIPTOR):
        val = error_details_pb2.ResourceInfo()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.Help.DESCRIPTOR):
        val = error_details_pb2.Help()
        grpc_detail.Unpack(val)
        return val
    elif grpc_detail.Is(error_details_pb2.LocalizedMessage.DESCRIPTOR):
        val = error_details_pb2.LocalizedMessage()
        grpc_detail.Unpack(val)
        return val
    else:
        raise ValueError(grpc_detail.type_url)

def handleException(e: Exception):
    raise Exception("An exception occurred: {}".format(e)) from None

def validate_client(client: Client):
    if client == None:
        raise ValueError('Client must be provided')
    if client.computeStub == None:
        raise ValueError('Invalid client provided. Compute service stub was not initialized properly.')
    if client.viewStub == None:
        raise ValueError('Invalid client provided. View service stub was not initialized properly.')
    if client.tableStub == None:
        raise ValueError('Invalid client provided. Table service stubs was not initialized properly.')