package grpcerror

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/anypb"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/apimiddleware/response"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/consts"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

// GRPCGatewayErrorHandler maps gRPC error code to http status code, **won't** change error body.
// gRPC has its [standard error code](https://pkg.go.dev/google.golang.org/grpc/codes), and TiDB Mgmt Service defines additional biz error codes.
// When errors are lifted to the HTTP layer via gRPC-gateway, we need to map the gRPC error code to HTTP error code.
func GRPCGatewayErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaller runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	var customStatus *runtime.HTTPStatusError
	if errors.Is(err, customStatus) {
		err = customStatus.Err
	}

	s := status.Convert(err)
	if s == nil {
		tilog.Warn(ctx, "failed to convert error to grpc status error", zap.Error(err))
		s = status.New(codes.Internal, "Internal Server Error")
	}

	// Then, delegate the error to the default error handler.
	runtime.DefaultHTTPErrorHandler(ctx, mux, marshaller, w, r, generateGRPCError(ctx, r, s))
}

func generateGRPCError(ctx context.Context, r *http.Request, s *status.Status) error {
	// convert biz error code to http status code
	s = addRequestInfo(ctx, s)
	s = generateHttpRespCode(s)
	s = prettifyErrMsg(s)
	s = generateErrorDetails(ctx, r, s)

	// return http status error
	return &runtime.HTTPStatusError{
		HTTPStatus: int(s.Code()),
		Err:        s.Err(),
	}
}

func addRequestInfo(ctx context.Context, s *status.Status) *status.Status {
	bizCode := bizerr.ErrCode(s.Code())
	_, ok := bizerr.GrpcErrCode2BizErrCode[s.Code()]
	if ok {
		bizCode = bizerr.GrpcErrCode2BizErrCode[s.Code()]
	}

	ss, err := s.WithDetails(&errdetails.RequestInfo{
		RequestId:   consts.GetTraceID(ctx),
		ServingData: strconv.FormatInt(int64(bizCode), 10),
	})
	if err != nil {
		tilog.Warn(ctx, "failed to add error request info", zap.Error(err))
		return s
	}
	return ss
}

func generateHttpRespCode(s *status.Status) *status.Status {
	// generate http status code from grpc status code
	p := s.Proto()
	switch {
	case bizerr.ErrCode(s.Code()) == bizerr.ErrCodeOK:
		p.Code = int32(http.StatusOK)
	case bizerr.ErrCode(s.Code()) >= bizerr.RequestErrCodeStart:
		p.Code = int32(convertBizerrorToHTTPStatus(bizerr.ErrCode(s.Code())))
	case s.Code() >= codes.Internal:
		p.Code = int32(http.StatusInternalServerError)
	default:
		p.Code = int32(runtime.HTTPStatusFromCode(s.Code()))
	}

	return status.FromProto(p)
}

func convertBizerrorToHTTPStatus(code bizerr.ErrCode) int {
	if state, ok := bizerr.BizErrCode2HttpStatus[code]; ok {
		return state
	}
	if code >= bizerr.RequestErrCodeStart && code < bizerr.InternalErrCodeStart {
		return http.StatusBadRequest
	}

	if code >= bizerr.InternalErrCodeStart {
		return http.StatusInternalServerError
	}

	return http.StatusInternalServerError
}

func generateErrorDetails(ctx context.Context, r *http.Request, s *status.Status) *status.Status {
	details := make([]*anypb.Any, 0)
	p := s.Proto()

	// set details  "reason": "Internal"/ "domain": "tidbapi.com"
	if p.Code == int32(http.StatusInternalServerError) {
		// TODO: dicuss with the PM to decide whether to show the error message
		// if p.Message == "" {
		// 	p.Message = "Internal Server Error."
		// }
		p.Message = "Internal Server Error."

		// TBD: set domain for http rqeust path
		// errprInfo := &errdetails.ErrorInfo{
		// 	Reason:   "Internal",
		// 	Domain:   fmt.Sprintf("%s.%s", r.Header.Get(consts.DomainKey), consts.Domain),
		// 	Metadata: map[string]string{},
		// }
		// nd, err := anypb.New(errprInfo)
		// if err == nil {
		// 	details = append(details, nd)
		// }
	}

	for _, d := range p.Details {
		switch d.TypeUrl {
		// filter debug info in details
		case "type.googleapis.com/google.rpc.DebugInfo":
			continue
		// filter internal error info
		case "type.googleapis.com/google.rpc.ErrorInfo":
			if p.Code == int32(http.StatusInternalServerError) {
				continue
			}
			var errInfo errdetails.ErrorInfo
			if err := d.UnmarshalTo(&errInfo); err != nil {
				tilog.Warn(ctx, "failed to unmarshal error info", zap.Error(err))
				continue
			}
			errInfo.Domain = fmt.Sprintf("%s.%s", r.Header.Get(consts.DomainKey), consts.Domain)
			nd, err := anypb.New(&errInfo)
			if err == nil {
				d = nd
			}
			details = append(details, d)
		default:
			details = append(details, d)
		}
	}

	p.Details = details
	return status.FromProto(p)
}

// modifyGrpcErrorHandler modifies the response body of the gRPC error response.
// The gRPC error response is in the format of `google.rpc.Status`: types.google.com/google.rpc.Status
// The `google` prefix is not friendly to the TiDBCloud, so we need to replace it with `tidb`.
func ModifyGrpcErrorHandler(ctx *gin.Context) {
	ctx.Next()
	// modify grpc error response
	if ctx.Writer.Status() >= http.StatusMultipleChoices {
		if w, ok := ctx.Writer.(*response.WriterWrapper); ok {
			respBody := bytes.ReplaceAll(w.Body.Bytes(), []byte("google"), []byte("tidb"))
			respBody = bytes.ReplaceAll(respBody, []byte("apis"), []byte("api"))
			w.Body = bytes.NewBuffer(respBody)
		}
	}
}

// remove duplicate grpc error msg desc
func prettifyErrMsg(s *status.Status) *status.Status {
	p := s.Proto()

	if strings.Contains(p.Message, "desc = ") {
		er := strings.Split(p.Message, "desc =")
		p.Message = er[len(er)-1]
	}

	// remove space
	p.Message = strings.TrimSpace(p.Message)

	// capitalize the first letter of the error message
	p.Message = strings.ToUpper(p.Message[:1]) + p.Message[1:]

	// add . at the end of the error message
	if !strings.HasSuffix(p.Message, ".") {
		p.Message = fmt.Sprintf("%s.", p.Message)
	}
	return status.FromProto(p)
}
