package status

import (
	"context"
	"strconv"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"github.com/pkg/errors"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/bilibili/kratos/pkg/ecode"
	"github.com/bilibili/kratos/pkg/net/rpc/warden/internal/pb"
)

// togRPCCode convert ecode.Codo to gRPC code
func togRPCCode(code ecode.Codes) codes.Code {
	switch code.Code() {
	case ecode.OK.Code():
		return codes.OK
	case ecode.RequestErr.Code():
		return codes.InvalidArgument
	case ecode.NothingFound.Code():
		return codes.NotFound
	case ecode.Unauthorized.Code():
		return codes.Unauthenticated
	case ecode.AccessDenied.Code():
		return codes.PermissionDenied
	case ecode.LimitExceed.Code():
		return codes.ResourceExhausted
	case ecode.MethodNotAllowed.Code():
		return codes.Unimplemented
	case ecode.Deadline.Code():
		return codes.DeadlineExceeded
	case ecode.ServiceUnavailable.Code():
		return codes.Unavailable
	}
	return codes.Unknown
}

func toECode(gst *status.Status) ecode.Code {
	gcode := gst.Code()
	switch gcode {
	case codes.OK:
		return ecode.OK
	case codes.InvalidArgument:
		return ecode.RequestErr
	case codes.NotFound:
		return ecode.NothingFound
	case codes.PermissionDenied:
		return ecode.AccessDenied
	case codes.Unauthenticated:
		return ecode.Unauthorized
	case codes.ResourceExhausted:
		return ecode.LimitExceed
	case codes.Unimplemented:
		return ecode.MethodNotAllowed
	case codes.DeadlineExceeded:
		return ecode.Deadline
	case codes.Unavailable:
		return ecode.ServiceUnavailable
	case codes.Unknown:
		return ecode.String(gst.Message())
	}
	return ecode.ServerErr
}

// FromError convert error for service reply and try to convert it to grpc.Status.
func FromError(svrErr error) (gst *status.Status) {
	var err error
	svrErr = errors.Cause(svrErr)
	if code, ok := svrErr.(ecode.Codes); ok {
		// TODO: deal with err
		if gst, err = gRPCStatusFromEcode(code); err == nil {
			return
		}
	}
	// for some special error convert context.Canceled to ecode.Canceled,
	// context.DeadlineExceeded to ecode.DeadlineExceeded only for raw error
	// if err be wrapped will not effect.
	switch svrErr {
	case context.Canceled:
		gst, _ = gRPCStatusFromEcode(ecode.Canceled)
	case context.DeadlineExceeded:
		gst, _ = gRPCStatusFromEcode(ecode.Deadline)
	default:
		gst, _ = status.FromError(svrErr)
	}
	return
}

func gRPCStatusFromEcode(code ecode.Codes) (*status.Status, error) {
	var st *ecode.Status
	switch v := code.(type) {
	// compatible old pb.Error remove it after nobody use pb.Error.
	case *pb.Error:
		return status.New(codes.Unknown, v.Error()).WithDetails(v)
	case *ecode.Status:
		st = v
	case ecode.Code:
		st = ecode.FromCode(v)
	default:
		st = ecode.Error(ecode.Code(code.Code()), code.Message())
		for _, detail := range code.Details() {
			if msg, ok := detail.(proto.Message); ok {
				st.WithDetails(msg)
			}
		}
	}
	// gst := status.New(togRPCCode(st), st.Message())
	// NOTE: compatible with PHP swoole gRPC put code in status message as string.
	// gst := status.New(togRPCCode(st), strconv.Itoa(st.Code()))
	gst := status.New(codes.Unknown, strconv.Itoa(st.Code()))
	pbe := &pb.Error{ErrCode: int32(st.Code()), ErrMessage: gst.Message()}
	// NOTE: server return ecode.Status will be covert to pb.Error details will be ignored
	// and put it at details[0] for compatible old client
	return gst.WithDetails(pbe, st.Proto())
}

// ToEcode convert grpc.status to ecode.Codes
func ToEcode(gst *status.Status) ecode.Codes {
	details := gst.Details()
	// reverse range details, details may contain three case,
	// if details contain pb.Error and ecode.Status use eocde.Status first.
	//
	// Details layout:
	// pb.Error [0: pb.Error]
	// both pb.Error and ecode.Status [0: pb.Error, 1: ecode.Status]
	// ecode.Status [0: ecode.Status]
	for i := len(details) - 1; i >= 0; i-- {
		detail := details[i]
		// compatible with old pb.Error.
		if pe, ok := detail.(*pb.Error); ok {
			st := ecode.Error(ecode.Code(pe.ErrCode), pe.ErrMessage)
			if pe.ErrDetail != nil {
				dynMsg := new(ptypes.DynamicAny)
				// TODO deal with unmarshalAny error.
				if err := ptypes.UnmarshalAny(pe.ErrDetail, dynMsg); err == nil {
					st, _ = st.WithDetails(dynMsg.Message)
				}
			}
			return st
		}
		// convert detail to status only use first detail
		if pb, ok := detail.(proto.Message); ok {
			return ecode.FromProto(pb)
		}
	}
	return toECode(gst)
}