package api

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

	"github.com/gin-gonic/gin"
	centralapi "github.com/tidbcloud/core-svc/idl/pbgen/proto/central"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/cors"
	"github.com/tidbcloud/tiinf/sdk/rbac/common"
	"github.com/tidbcloud/tiinf/sdk/rbac/useadapter"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/central"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/inner"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/types"
	apiutils "github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/utils"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/v1beta"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/auth"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/config"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/rbac"
	accountgrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/account"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/utils"
	"github.com/tidbcloud/tidb-management-service/utilities/allowlist"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func InitAPI(engine *gin.Engine, allowList *allowlist.AllowLockList) {
	// If there is no route, forward req to the core portal.
	engine.NoRoute(
		func(c *gin.Context) {
			// If the request is not v1beta Open API, forward it to the core portal directly.
			if !strings.HasPrefix(c.Request.URL.Path, "/api/v1beta") {
				proxy(c, config.Config().CorePortal.CorePortalEndpoint)
				c.Abort()
				return
			}

			// If the request is v1beta Open API, check api key role, then forward it to the core portal.
			c.Next()
		},
		tigin.CheckAuthByAuth0(accountgrpc.GetAccountProviderSvcCli()),
		auth.CheckApiKeyRole(),
		func(c *gin.Context) {
			proxy(c, config.Config().CorePortal.CorePortalEndpoint)
		},
	)

	// register allow list
	apiutils.SetAllowList(allowList)

	// Register old Open API v1beta handlers
	v1beta1Router := engine.Group("/api/v1beta")
	registerV1beta(v1beta1Router, allowList)

	// Register old UI API handlers
	centralRouter := engine.Group("/api/v1")
	registerCentral(centralRouter, allowList)

	testTokenRouter := engine.Group("/api")
	registerTestToken(testTokenRouter)

	innerMgmtV1Router := engine.Group("/inner/mgmt/v1")
	registerInnerMgmtV1(innerMgmtV1Router)

	// set default access decide complete exec func
	useadapter.DefaultAccessDecideCompleteExecFunc = GinCloudAccessDecideCompleteExecFunc
}

// @title			v1beta1 API
// @version		1.0
// @description	This is a v1beta1 OpenAPI server
// @BasePath		/api/v1beta/
func registerV1beta(g *gin.RouterGroup, allowList *allowlist.AllowLockList) {
	g.Use(
		cors.GinNew(utils.GetCorsOptions()),
		tigin.CheckAuthByAuth0(accountgrpc.GetAccountProviderSvcCli()),
		auth.SetGrpcMetaDataHeader(),
		checkIsLocked(allowList),
	)
	g.GET("/clusters/provider/regions", v1beta.ListRegions)
	g.GET("/projects/:project_id/clusters", v1beta.ListClusters)
	g.GET("/projects/:project_id/clusters/:cluster_id", v1beta.GetCluster)
	g.POST("/projects/:project_id/clusters", v1beta.CreateCluster)
	g.PATCH("/projects/:project_id/clusters/:cluster_id", v1beta.UpdateCluster)
	g.DELETE("/projects/:project_id/clusters/:cluster_id", v1beta.DeleteCluster)
}

// @title			central API
// @description	This is a central server
// @BasePath		/api/v1/orgs
func registerCentral(g *gin.RouterGroup, allowList *allowlist.AllowLockList) {
	g.Use(
		cors.GinNew(utils.GetCorsOptions()),
		tigin.CheckAuthByAuth0(accountgrpc.GetAccountProviderSvcCli()),
		tigin.CheckRBACPermission(
			accountgrpc.GetAccountProviderSvcCli(),
			rbac.RBACPathPattern2ResourceMap,
			rbac.RBACPathWhitelist,
		),
		auth.SetGrpcMetaDataHeader(),
		checkIsLocked(allowList),
	)
	g.POST("/orgs/:org_id/projects/:project_id/cloud_providers/query", central.ListRegions)
	g.GET("/orgs/:org_id/projects/:project_id/clusters", central.ListClusters)
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id", central.GetCluster)
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/privatelink_service", central.GetPrivateLinkService)
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/connections", central.GetPrivateEndpoint)
	g.POST("/orgs/:org_id/projects/:project_id/clustersv2", central.CreateCluster)
	g.DELETE("/orgs/:org_id/projects/:project_id/clusters/:cluster_id", central.DeleteCluster)
	g.PATCH("/orgs/:org_id/projects/:project_id/clusters/:cluster_id", central.UpdateCluster)
	g.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/connections", central.CreatePrivateEndpoint)
	g.POST("/orgs/:org_id/projects/:project_id/clusters/names", central.GenerateName)
	g.PUT("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/password", central.ChangePassword)
	g.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/pause", central.PauseCluster)
	g.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/resume", central.ResumeCluster)
	g.PUT("/orgs/:org_id/projects/:project_id/clustersv2/:cluster_id/components", central.ScaleCluster)
	g.POST("/orgs/:org_id/projects/:project_id/node_profiles/query", central.ListNodeProfiles)
	g.POST("/orgs/:org_id/projects/:project_id/cluster_fee/query", central.CalClusterFee)

	// nodes
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/tidb_nodes", central.GetTiDBNodes)
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/tikv_nodes", central.GetTiKVNodes)
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/tiflash_nodes", central.GetTiFlashNodes)

	// db auditlog
	g.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log", central.GetAuditLogStatus)
	g.PUT("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log", central.UpdateAuditLogStatus)
	g.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log/precheck", central.PrecheckAuditLogConfig)
	g.GET(
		"/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log/plugin_status",
		central.CheckAuditLogPluginStatus,
	)
	g.GET(
		"/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log/access_records",
		central.GetAuditLogAccessRecords,
	)
	g.POST(
		"/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log/access_records",
		central.CreateAuditLogAccessRecord,
	)
	g.DELETE(
		"/orgs/:org_id/projects/:project_id/clusters/:cluster_id/audit_log/access_records/:id",
		central.DeleteAuditLogAccessRecord,
	)

	if config.Config().Biz.CreateClusterV3 {
		g.POST("/orgs/:org_id/projects/:project_id/clustersv3", central.CreateClusterWithConfig)
	} else {
		g.POST("/orgs/:org_id/projects/:project_id/clustersv3", func(ctx *gin.Context) {
			types.BadRequestResp(ctx, fmt.Errorf("not enable clusterv3"))
		})
	}

	g.GET("/orgs/:org_id/projects/:project_id/three_arch", central.GetThreeArch)

	registerDedicatedCIDR(g)
}

func registerDedicatedCIDR(g *gin.RouterGroup) {
	g.POST("/orgs/:org_id/projects/:project_id/cidrs", central.CreateCidr)
	g.GET("/orgs/:org_id/projects/:project_id/cidrs", central.ListCidrs)
	g.GET("/orgs/:org_id/projects/:project_id/cidrs/:cidr_id", central.GetCidr)
	g.PATCH("/orgs/:org_id/projects/:project_id/cidrs/:cidr_id", central.UpdateCidr)
	g.DELETE("/orgs/:org_id/projects/:project_id/cidrs/:cidr_id", central.DeleteCidr)
	g.GET("/orgs/:org_id/projects/:project_id/defaultCidr", central.GetDefaultCidr)
}

// @title			inner mgmt v1 API
// @description	This is a inner mgmt server
// @BasePath		/inner/mgmt/v1
func registerInnerMgmtV1(g *gin.RouterGroup) {
	server := tigin.DefaultWithServerName("inner-mgmt-v1")
	handler := &customVerbRewriteRouter{
		verbs: map[string]string{
			":showComponentNodeInfo": "/showComponentNodeInfo",
			":countBillableClusters": "/countBillableClusters",
		},
		Handler: server,
	}
	server.GET("/clusters/:cluster_id/showComponentNodeInfo", inner.ShowComponentNodeInfo)
	server.POST("/clusters/countBillableClusters", inner.CountBillableClusters)
	server.POST("/checkThreeArch", inner.CheckThreeArch)

	g.Use(ServePathStartFromAny)
	g.Any("/*any", gin.WrapF(handler.ServeHTTP))
}

// @title			test-token API
// @description	This is a test-token server
// @BasePath		/api/v1/test-token
func registerTestToken(g *gin.RouterGroup) {
	g.Any("/v1/test-token", func(ctx *gin.Context) {
		proxy(ctx, config.Config().CorePortal.CorePortalEndpoint)
	})
}

func checkIsLocked(allowList *allowlist.AllowLockList) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if config.Config().AllowAndLockList.Enable {
			orgId, err := auth.GetOrgIDByHeader(ctx)
			if err != nil {
				tilog.Warn(ctx.Request.Context(), "invalid org id", zap.Error(err))
				ctx.JSON(http.StatusBadRequest, gin.H{
					"massage": "invalid org id",
					"error":   err.Error(),
				})
				ctx.Abort()
				return
			}

			if allowList.LockList.IsLockOrgId(orgId) &&
				!allowList.LockList.IsSuperUser(ctx.GetHeader(auth.GrpcGatewayUserEmailKey)) {
				// reject all requests
				ctx.JSON(http.StatusServiceUnavailable, gin.H{
					"massage": "The service is temporarily unavailable. Please try again later.",
				})
				tilog.Warn(ctx.Request.Context(), "org is locked", zap.Uint64("org_id", orgId))
				allowlist.OrgLockedCounter.WithLabelValues(ctx.GetHeader(auth.GrpcGatewayOrgIDKey)).Inc()
				ctx.Abort()
				return
			}
		}
	}
}

// marshalOptions is a configurable JSON format marshaller.
var marshalOptions = protojson.MarshalOptions{
	UseProtoNames:   true,
	EmitUnpopulated: true,
}

// web console base response
func GinCloudAccessDecideCompleteExecFunc(
	w http.ResponseWriter,
	r *http.Request,
	result *common.AccessResult,
	err error,
) (continueRun bool) {
	if err != nil || result.Code != common.RSP_CODE_RELEASE {
		tilog.Warn(r.Context(), "permission check failed. ", zap.Any("result", result), zap.Error(err))
		resp := &centralapi.DefaultResp{BaseResp: &centralapi.BaseResp{
			ErrMsg:  "permission denied",
			ErrCode: int64(bizerr.ErrCodePermission),
		}}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		body, _ := marshalOptions.Marshal(resp)
		_, err := w.Write(body)
		if err != nil {
			tilog.Error(r.Context(), "failed to write response", zap.Error(err))
		}
		return false
	}
	return true
}

func ServePathStartFromAny(ctx *gin.Context) {
	originalPath := ctx.Request.URL.Path
	ctx.Request.URL.Path = ctx.Param("any")
	ctx.Next()
	ctx.Request.URL.Path = originalPath
}
