# Copyright 2013-present Barefoot Networks, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import sys

from google.rpc import status_pb2, code_pb2
import grpc
from p4.v1 import p4runtime_pb2
from p4.v1 import p4runtime_pb2_grpc

# Used to indicate that the gRPC error Status object returned by the server has
# an incorrect format.
class P4RuntimeErrorFormatException(Exception):
    def __init__(self, message):
        super(P4RuntimeErrorFormatException, self).__init__(message)


# Parse the binary details of the gRPC error. This is required to print some
# helpful debugging information in tha case of batched Write / Read
# requests. Returns None if there are no useful binary details and throws
# P4RuntimeErrorFormatException if the error is not formatted
# properly. Otherwise, returns a list of tuples with the first element being the
# index of the operation in the batch that failed and the second element being
# the p4.Error Protobuf message.
def parseGrpcErrorBinaryDetails(grpc_error):
    if grpc_error.code() != grpc.StatusCode.UNKNOWN:
        return None

    error = None
    # The gRPC Python package does not have a convenient way to access the
    # binary details for the error: they are treated as trailing metadata.
    for meta in grpc_error.trailing_metadata():
        if meta[0] == "grpc-status-details-bin":
            error = status_pb2.Status()
            error.ParseFromString(meta[1])
            break
    if error is None:  # no binary details field
        return None
    if len(error.details) == 0:
        # binary details field has empty Any details repeated field
        return None

    indexed_p4_errors = []
    for idx, one_error_any in enumerate(error.details):
        p4_error = p4runtime_pb2.Error()
        if not one_error_any.Unpack(p4_error):
            raise P4RuntimeErrorFormatException(
                "Cannot convert Any message to p4.Error")
        if p4_error.canonical_code == code_pb2.OK:
            continue
        indexed_p4_errors += [(idx, p4_error)]

    return indexed_p4_errors


# P4Runtime uses a 3-level message in case of an error during the processing of
# a write batch. This means that some care is required when printing the
# exception if we do not want to end-up with a non-helpful message in case of
# failure as only the first level will be printed. In this function, we extract
# the nested error message when present (one for each operation included in the
# batch) in order to print error code + user-facing message. See P4Runtime
# documentation for more details on error-reporting.
def printGrpcError(grpc_error):
    print "gRPC Error", grpc_error.details(),
    status_code = grpc_error.code()
    print "({})".format(status_code.name),
    traceback = sys.exc_info()[2]
    print "[{}:{}]".format(
        traceback.tb_frame.f_code.co_filename, traceback.tb_lineno)
    if status_code != grpc.StatusCode.UNKNOWN:
        return
    p4_errors = parseGrpcErrorBinaryDetails(grpc_error)
    if p4_errors is None:
        return
    print "Errors in batch:"
    for idx, p4_error in p4_errors:
        code_name = code_pb2._CODE.values_by_number[
            p4_error.canonical_code].name
        print "\t* At index {}: {}, '{}'\n".format(
            idx, code_name, p4_error.message)
