package server

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"regexp"
	"strings"

	"braces.dev/errtrace"
	"github.com/gin-gonic/gin"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/pkg/errors"
	appconfig "github.com/tidbcloud/dataflow-service/app/config"
	"github.com/tidbcloud/dataflow-service/internal/cmd/bootstrap/server/wrapper"
	"github.com/tidbcloud/dataflow-service/internal/metrics"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/pkgv2/bizerr"
	"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"
)

var digitOnlyRegex = regexp.MustCompile("^[0-9]+$")

// 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,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	var customStatus *runtime.HTTPStatusError
	if errors.As(err, &customStatus) {
		err = customStatus.Err
	}

	s := status.Convert(err)
	functionName := getFunctionNameFromURI(r.RequestURI)
	if s == nil {
		tilog.Warn(ctx, "failed to convert error to grpc status error", zap.Error(err))
		metrics.HttpErrorResultCounter.WithLabelValues(functionName, r.Method, "-1").Inc()
		s = status.New(codes.Internal, "Internal Server Error")
	} else {
		metrics.HttpErrorResultCounter.WithLabelValues(functionName, r.Method, s.Code().String()).Inc()
	}

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

func getFunctionNameFromURI(URI string) string {
	withoutParameter := strings.Split(URI, "?")[0]
	paths := strings.Split(withoutParameter, "/")
	length := len(paths)
	functionName := ""
	pathLength := 0

	for i := length - 1; i >= 0; i-- {
		if !digitOnlyRegex.MatchString(paths[i]) {
			functionName = paths[i] + "/" + functionName
			pathLength++
			if pathLength == 2 {
				return functionName
			}
		}
	}

	return functionName
}

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 errtrace.Wrap(&runtime.HTTPStatusError{
		HTTPStatus: int(s.Code()),
		Err:        s.Err(),
	})
}

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

	ss, err := s.WithDetails(&errdetails.RequestInfo{
		RequestId:   log.GetLogID(ctx),
		ServingData: fmt.Sprintf("bizErrorCode: %d", bizCode),
	})
	if err != nil {
		tilog.Warn(ctx, "failed to add error request info", zap.Error(err))
		return s
	} else {
		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()) >= appconfig.RequestErrCodeStart:
		p.Code = int32(appconfig.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 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) {
		if p.Message == "" {
			p.Message = "Internal Server Error"
		}
		errprInfo := &errdetails.ErrorInfo{
			Reason: appconfig.Internal,
			//Domain:   consts.GetDomain(r.URL.Path),
			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
		// set domain for http rqeust path
		case "type.googleapis.com/google.rpc.ErrorInfo":
			// not keep internal error info
			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
			}
			// set domain for http rqeust path
			//errInfo.Domain = appconfig.GetDomain(r.URL.Path)
			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)
}

// 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]
	}
	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.(*wrapper.WriterWrapper); ok {
			respBody := bytes.ReplaceAll(w.Body.Bytes(), []byte("googleapis"), []byte("tidbapi"))
			respBody = bytes.ReplaceAll(respBody, []byte("google"), []byte("tidb"))
			// respBody = bytes.ReplaceAll(respBody, []byte("apis"), []byte("api"))
			w.Body = bytes.NewBuffer(respBody)
		}
	}
}
