package server

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

	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
	"go.uber.org/zap/zapcore"

	"github.com/gin-gonic/gin"
	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	"github.com/pingcap/log"
	"github.com/tidbcloud/account/commons/config"
	openapiv1veta1 "github.com/tidbcloud/account/idl/pbgen/proto/v1beta1"
	"github.com/tidbcloud/account/internel/controllers/openapi"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/server"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.uber.org/zap"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	spb "google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
)

func StartOpenApiGrpcGateway(s *Server) {
	// 1.1. new grpc server
	grpcServer := tigrpc.NewServer(
		tigrpc.WithDefault(
			tigrpc.CustomizeZapOption(
				grpc_zap.WithMessageProducer(customizeInfoMessageProducer),
			),
		),
		tigrpc.WithUnaryInterceptors(
			grpc_middleware.ChainUnaryServer(
				openApiErrConvGrpcUnaryServerInterceptor(),
				auth.GrpcGatewayGetIdentityInfoUnaryInterceptor(&auth.IdentityAuthOption{
					EnableLog: true,
				}),
				auth.GrpcGatewayPermissionCheckUnaryServerInterceptor(&auth.PermissionCheckerCommonParam{
					IdentityF:                   auth.GrpcGatewayGetIdentityFunc,
					PathPattern2ResourceMap:     openApiPathPattern2ResourceMap,
					PathWhitelist:               openApiPathWhitelist,
					GenAccessParam4GrpcGatewayF: auth.GenAccessParam4GrpcGatewayWithPrefixOrgId,
					EnableLog:                   true,
				}),
			),
		))
	// 1.2 register grpc handler
	openapiv1veta1.RegisterApiKeyServiceServer(grpcServer, &openapi.ApiKeyService{})
	openapiv1veta1.RegisterOrgServiceServer(grpcServer, &openapi.OrgService{})
	openapiv1veta1.RegisterUserServiceServer(grpcServer, &openapi.UserService{})
	openapiv1veta1.RegisterInvitationServiceServer(grpcServer, &openapi.InvitationService{})
	openapiv1veta1.RegisterPartnerServiceServer(grpcServer, &openapi.PartnerService{})
	openapiv1veta1.RegisterFileUploadServiceServer(grpcServer, &openapi.FileUploadService{})
	// 1.3 start grpc server
	port := config.Global().ServerConfig.OpenApiGrpcPort
	grpcEndpoint := fmt.Sprintf("0.0.0.0:%d", port)
	go func() {
		tilog.PlainInfo("running open api grpc server", zap.Any("port", port))
		lis, err := net.Listen("tcp", grpcEndpoint)
		if err != nil {
			tilog.PlainPanic("failed to run open api grpc server listen", zap.Error(err))
		}
		if err = grpcServer.Serve(lis); err != nil {
			tilog.PlainPanic("failed to run open api grpc server", zap.Error(err))
		}
	}()

	// 2.1 config route to gateway
	grpcGwMux := s.HTTPGateway.Mux()
	mh := s.HTTPGateway.HttpMetricsHandler()
	eng := s.TiInf.GinEngine()
	eng.Any("/v1beta1/apiKeys/*any", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/apiKeys", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/org", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/users/*any", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/users", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/users:invite", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/projects/:project_id/*any", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))
	eng.Any("/v1beta1/partners/*any", gin.WrapH(mh.Handler(CorsAllowAll.Handler(grpcGwMux))))

	// 3.1 create grpc conn for gateway
	dialOpts := append(dialLogOptions(log.L(), ProdGatewayOptions),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithChainUnaryInterceptor(
			otelgrpc.UnaryClientInterceptor(),
		),
	)
	dialAddr := fmt.Sprintf("passthrough://0.0.0.0/%s", grpcEndpoint)
	conn, err := grpc.Dial(dialAddr, dialOpts...)
	if err != nil {
		tilog.PlainPanic("failed to dial open api grpc server", zap.Error(err))
	}
	ctx := context.Background()
	// 3.2 register gateway handler
	_ = openapiv1veta1.RegisterApiKeyServiceHandler(ctx, grpcGwMux, conn)
	_ = openapiv1veta1.RegisterOrgServiceHandler(ctx, grpcGwMux, conn)
	_ = openapiv1veta1.RegisterUserServiceHandler(ctx, grpcGwMux, conn)
	_ = openapiv1veta1.RegisterInvitationServiceHandler(ctx, grpcGwMux, conn)
	_ = openapiv1veta1.RegisterPartnerServiceHandler(ctx, grpcGwMux, conn)
	_ = openapi.RegisterFileUploadServiceHandler(ctx, grpcGwMux, conn)
}

func openApiErrConvGrpcUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		rsp, err := handler(ctx, req)
		if err == nil {
			return rsp, nil
		}
		tilog.Warn(ctx, "grpc failed with raw error", zap.Any("err", err))
		// handle ti error
		tierr, ok := errs.AsTiError(err)
		if ok {
			if tierr.HTTPCode() >= 500 {
				s := status.FromProto(&spb.Status{
					Code:    int32(tierr.HTTPCode()),
					Message: "Internal server error, please try again later",
				})
				ss, err := s.WithDetails(&errdetails.ErrorInfo{
					Reason: "INTERNAL",
					Metadata: map[string]string{
						"extraInfo": tierr.ErrMsg(),
					},
				})
				if err != nil {
					return rsp, s.Err()
				}
				return rsp, ss.Err()
			}
			return rsp, status.Error(codes.Code(tierr.HTTPCode()), tierr.ErrMsg())
		}

		// handle grpc status error
		grpcErr, ok := status.FromError(err)
		if ok {
			if grpcErr.Code() == codes.Unauthenticated {
				s := status.FromProto(&spb.Status{
					Code:    int32(http.StatusUnauthorized),
					Message: "Unauthorized",
				})
				ss, err := s.WithDetails(&errdetails.ErrorInfo{
					Reason: "Unauthorized",
					Metadata: map[string]string{
						"extraInfo": err.Error(),
					},
				})
				if err != nil {
					return rsp, s.Err()
				}
				return rsp, ss.Err()
			}
			if grpcErr.Code() == codes.PermissionDenied {
				s := status.FromProto(&spb.Status{
					Code:    int32(http.StatusForbidden),
					Message: "Forbidden",
				})
				ss, err := s.WithDetails(&errdetails.ErrorInfo{
					Reason: "Forbidden",
					Metadata: map[string]string{
						"extraInfo": err.Error(),
					},
				})
				if err != nil {
					return rsp, s.Err()
				}
				return rsp, ss.Err()
			}
			return rsp, grpcErr.Err()
		}

		return rsp, err
	}
}

func isOpenApiGrpcGatewayReq(r *http.Request) bool {
	path := r.URL.Path
	if strings.HasPrefix(path, "/v1beta1/apiKeys") {
		return true
	}
	if strings.HasPrefix(path, "/v1beta1/org") {
		return true
	}
	if strings.HasPrefix(path, "/v1beta1/users") {
		return true
	}
	if strings.HasPrefix(path, "/v1beta1/partners") {
		return true
	}
	matched, err := regexp.MatchString("/v1beta1/projects/\\d+/.+", path)
	if err != nil {
		tilog.Warn(r.Context(), "isOpenApiGrpcGatewayReq match path err", zap.Any("path", path), zap.Any("err", err))
		return false
	}
	return matched
}

//func grpcGatewayReceiveHttpHeader(ctx context.Context, r *http.Request) metadata.MD {
//	header := r.Header
//	for k, v := range header {
//		tilog.Info(ctx, fmt.Sprintf("grpc gateway receive http header: %s", k), zap.Any("value", v))
//	}
//	return metadata.New(map[string]string{})
//}

func customizeInfoMessageProducer(ctx context.Context, msg string, level zapcore.Level, code codes.Code, err error, duration zapcore.Field) {
	tigrpc.TraceIDMessageProducer(ctx, msg, zapcore.InfoLevel, code, err, duration)
}

var (
	openApiPathPattern2ResourceMap = map[string]string{
		// api key
		"get:/orgs/(\\d+)/v1beta1/apiKeys":         "account/openapi/orgs/{org_id}/list_apikey",
		"post:/orgs/(\\d+)/v1beta1/apiKeys":        "account/openapi/orgs/{org_id}/create_apikey",
		"get:/orgs/(\\d+)/v1beta1/apiKeys/\\w+":    "account/openapi/orgs/{org_id}/get_apikey",
		"delete:/orgs/(\\d+)/v1beta1/apiKeys/\\w+": "account/openapi/orgs/{org_id}/delete_apikey",
		"patch:/orgs/(\\d+)/v1beta1/apiKeys/\\w+":  "account/openapi/orgs/{org_id}/patch_apikey",
		"get:/orgs/(\\d+)/v1beta1/apiKeys/roles":   "account/openapi/orgs/{org_id}/list_apikey_roles",

		// org
		"get:/orgs/(\\d+)/v1beta1/org":           "account/openapi/orgs/{org_id}/get_org",
		"get:/orgs/(\\d+)/v1beta1/partners/orgs": "account/openapi/orgs/{org_id}/list_partner_orgs",
		// user
		"get:/orgs/(\\d+)/v1beta1/users/\\d+":                    "account/openapi/orgs/{org_id}/get_user",
		"patch:/orgs/(\\d+)/v1beta1/users/\\d+":                  "account/openapi/orgs/{org_id}/update_user",
		"delete:/orgs/(\\d+)/v1beta1/users/\\d+":                 "account/openapi/orgs/{org_id}/delete_org_user",
		"delete:/orgs/(\\d+)/v1beta1/projects/(\\d+)/users/\\d+": "account/openapi/orgs/{org_id}/projects/{project_id}/delete_project_user",
		"get:/orgs/(\\d+)/v1beta1/users":                         "account/openapi/orgs/{org_id}/list_user",
		"get:/orgs/(\\d+)/v1beta1/users/me":                      "account/openapi/orgs/{org_id}/get_user",
		"patch:/orgs/(\\d+)/v1beta1/users/me":                    "account/openapi/orgs/{org_id}/update_user",

		// invitation
		"post:/orgs/(\\d+)/v1beta1/users:invite":                 "account/openapi/orgs/{org_id}/create_org_invitation",
		"post:/orgs/(\\d+)/v1beta1/projects/(\\d+)/users:invite": "account/openapi/orgs/{org_id}/projects/{project_id}/create_project_invitation",
		// partner
		"post:/orgs/(\\d+)/v1beta1/partners/msp/signupUrl":   "account/openapi/orgs/{org_id}/create_partner_signup_url",
		"get:/orgs/(\\d+)/v1beta1/partners/customers":        "account/openapi/orgs/{org_id}/list_partner_customer",
		"get:/orgs/(\\d+)/v1beta1/partners/partnerProfile":   "account/openapi/orgs/{org_id}/get_partner_profile",
		"patch:/orgs/(\\d+)/v1beta1/partners/partnerProfile": "account/openapi/orgs/{org_id}/update_partner_profile",
		"post:/orgs/(\\d+)/v1beta1/partners/logo:upload":     "account/openapi/orgs/{org_id}/upload_partner_logo",
	}

	openApiPathWhitelist = []string{
		"/orgs/\\d+/healthz",
		"get:/orgs/(\\d+)/v1beta1/partners/orgs",
	}
)
