package openapi

import (
	"context"
	"fmt"
	"time"

	"braces.dev/errtrace"
	"github.com/gin-gonic/gin"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"

	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/app/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/app/openapi/region"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/service/auth"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/service/rbac"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/infra/accountimpl"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/infra/ipipcli"
	tidbmgmtservice "github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/infra/mgmtservicecmock"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/apimiddleware/reqmodifier"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/allowlist"
)

func SetupOpenApiHandler(eng *gin.Engine, timeout time.Duration) error {
	address := config.Global().TiDBMgmtServiceConfig.Address
	fakeGRPC := config.Global().TiDBMgmtServiceConfig.LocalMode

	// For local development, we can use fake GRPC server to avoid the dependency on the real GRPC server.
	if fakeGRPC {
		tidbmgmtservice.SetupFakeMgmtGRPCServer(address)
		return nil
	}

	conn, err := common.CreateGrpcConn(address, timeout)
	if err != nil {
		return errtrace.Wrap(fmt.Errorf("failed to dial server: %w", err))
	}

	openApiMiddlewares, uiApiMiddlewares := initCommonMiddlewares()

	if err := registerHandlers(eng, conn, openApiMiddlewares, uiApiMiddlewares); err != nil {
		return errtrace.Wrap(fmt.Errorf("failed to register gateway: %w", err))
	}

	return nil
}

func initCommonMiddlewares() (openApiMiddlewares, uiApiMiddlewares []gin.HandlerFunc) {
	accountClient := accountimpl.NewAccount()

	var allowList *allowlist.AllowLockList
	allowList, err := allowlist.NewAllowList(
		context.Background(),
		config.Global().AllowAndLockList.Enable,
		config.Global().AllowAndLockList.OneConfigName,
		accountClient.GetAccountClient(),
		time.Second*time.Duration(config.Global().AllowAndLockList.ReloadInterval),
	)
	if err != nil {
		panic("Init allow list config failed. " + err.Error())
	}

	authSvc := auth.NewAuthService(accountClient)
	rbacSvc := rbac.NewRBACService(accountClient, allowList)

	return []gin.HandlerFunc{
			authSvc.CheckUserAndPermission(), // check auth by auth0 and api key (authentication)
			authSvc.TransformAuthnCtx,        // set grpc metadata (authentication data processing in ctx)
			rbacSvc.CheckAllowList,           // check allow list
			rbacSvc.CheckRBAC,                // rbac middleware (authorization)
		}, []gin.HandlerFunc{
			authSvc.RemoveApiKeyHeader(),
			authSvc.CheckUserAndPermission(), // check auth by auth0 and api key (authentication)
			authSvc.TransformAuthnCtx,        // set grpc metadata (authentication data processing in ctx)
			rbacSvc.CheckAllowList,           // check allow list
			rbacSvc.CheckRBAC,                // rbac middleware (authorization)
		}
}

func registerHandlers(
	eng *gin.Engine,
	conn *grpc.ClientConn,
	openApiMiddlewares, uiApiMiddlewares []gin.HandlerFunc,
) error {
	// serverless
	if err := registerServerlessV1beta1Handlers(eng, conn, openApiMiddlewares, uiApiMiddlewares); err != nil {
		return errtrace.Wrap(fmt.Errorf("failed to register serverless handlers: %w", err))
	}

	// dedicated
	if err := registerDedicatedV1beta1Handlers(eng, conn, openApiMiddlewares, uiApiMiddlewares); err != nil {
		return errtrace.Wrap(fmt.Errorf("failed to register dedicated handlers: %w", err))
	}

	// fs
	if err := registerFsV1beta2Handlers(eng, conn, openApiMiddlewares, uiApiMiddlewares); err != nil {
		return errtrace.Wrap(fmt.Errorf("failed to register fs handlers: %w", err))
	}

	return nil
}

// OpenAPI and WebUI API are served by the same handler, but have different base paths.
//
// Original request host and base path from outside:
// Serverless:
// - OpenAPI: https://serverless.tidbapi.com/v1beta1
// - WebUI API: https://us-west-2.prod.aws.tidbcloud.com/serverless/api/v1beta1
// Dedicated:
// - OpenAPI: https://dedicated.tidbapi.com/v1beta1
// - WebUI API: https://us-west-2.prod.aws.tidbcloud.com/dedicated/api/v1beta1
//
// After Kong pre-processing:
// Serverless:
// - OpenAPI: http://tidb-mgmt-portal-svc/serverless/v1beta1
// - WebUI API: http://tidb-mgmt-portal-svc/serverless/api/v1beta1
// Dedicated:
// - OpenAPI: http://tidb-mgmt-portal-svc/dedicated/v1beta1
// - WebUI API: http://tidb-mgmt-portal-svc/dedicated/api/v1beta1
//
// gRPC-Gateway serves handlers without base path.
//
// Thus, we strip the base paths of the Kong pre-processed requests, to use the same gRPC handler to serve both OpenAPI and WebUI API.
func registerServerlessV1beta1Handlers(
	eng *gin.Engine,
	conn *grpc.ClientConn,
	openApiMiddlewares, uiApiMiddlewares []gin.HandlerFunc,
) error {
	gwmux := common.MakeGatewayMux()

	if err := openapiserverlessv1beta1.RegisterClusterServiceHandler(context.Background(), gwmux, conn); err != nil {
		return errtrace.Wrap(err)
	}
	if err := openapiserverlessv1beta1.RegisterShadowPoolServiceHandler(context.Background(), gwmux, conn); err != nil {
		return errtrace.Wrap(err)
	}

	openapi := eng.Group("/serverless/v1beta1")
	oldUiapi := eng.Group("/serverless/api/v1beta1") // will deprecate
	uiapi := eng.Group("/api/v2023/serverless/v1beta1")

	oldUiapi.Use(region.GeoLocationHandler(ipipcli.NewIPIP())) //  serverless region geolocation middleware handler
	uiapi.Use(region.GeoLocationHandler(ipipcli.NewIPIP()))    //  serverless region geolocation middleware handler

	for _, apiGroup := range []*gin.RouterGroup{openapi} {
		apiGroup.Use(openApiMiddlewares...)
		// strip the base paths of the Kong pre-processed requests, to use the same gRPC handler to serve both OpenAPI and WebUI API.
		apiGroup.Use(reqmodifier.ServePathStartFromAny)
		apiGroup.Any("/*any", gin.WrapF(gwmux.ServeHTTP))
	}
	for _, apiGroup := range []*gin.RouterGroup{uiapi, oldUiapi} {
		apiGroup.Use(uiApiMiddlewares...)
		// strip the base paths of the Kong pre-processed requests, to use the same gRPC handler to serve both OpenAPI and WebUI API.
		apiGroup.Use(reqmodifier.ServePathStartFromAny)
		apiGroup.Any("/*any", gin.WrapF(gwmux.ServeHTTP))
	}

	return nil
}

func registerDedicatedV1beta1Handlers(
	eng *gin.Engine,
	conn *grpc.ClientConn,
	openApiMiddlewares, uiApiMiddlewares []gin.HandlerFunc,
) error {
	gwmux := common.MakeGatewayMux()

	// register dedicated service
	type registerHandlerFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error
	serviceHandlers := []registerHandlerFunc{
		openapidedicatedv1beta1.RegisterClusterServiceHandler,
		openapidedicatedv1beta1.RegisterRegionServiceHandler,
		openapidedicatedv1beta1.RegisterNetworkContainerServiceHandler,
		openapidedicatedv1beta1.RegisterDatabaseAuditLogServiceHandler,
		openapidedicatedv1beta1.RegisterPrivateEndpointConnectionServiceHandler,
		openapidedicatedv1beta1.RegisterCustomerManagedEncryptionKeyServiceHandler,
		openapidedicatedv1beta1.RegisterMaintenanceServiceHandler,
		openapidedicatedv1beta1.RegisterTidbNodeGroupServiceHandler,
	}

	for _, handler := range serviceHandlers {
		err := handler(context.Background(), gwmux, conn)
		if err != nil {
			return errtrace.Wrap(err)
		}
	}

	openapi := eng.Group("/dedicated/v1beta1")
	uiapi := eng.Group("/api/v2023/dedicated/v1beta1")

	for _, apiGroup := range []*gin.RouterGroup{openapi} {
		apiGroup.Use(openApiMiddlewares...)
		// strip the base paths of the Kong pre-processed requests, to use the same gRPC handler to serve both OpenAPI and WebUI API.
		apiGroup.Use(reqmodifier.ServePathStartFromAny)
		apiGroup.Any("/*any", gin.WrapF(gwmux.ServeHTTP))
	}

	for _, apiGroup := range []*gin.RouterGroup{uiapi} {
		apiGroup.Use(uiApiMiddlewares...)
		// strip the base paths of the Kong pre-processed requests, to use the same gRPC handler to serve both OpenAPI and WebUI API.
		apiGroup.Use(reqmodifier.ServePathStartFromAny)
		apiGroup.Any("/*any", gin.WrapF(gwmux.ServeHTTP))
	}

	return nil
}
