package api

import (
	"crypto/subtle"
	"encoding/base64"
	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/contextvar"
	"dataservices/commons/logger"
	dataapi "dataservices/internal/controller/dataapi_gateway"
	dto "dataservices/internal/dto/dataapi_gateway"
	"net/http"
	"strings"
	"time"

	"github.com/gin-contrib/pprof"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

func RouteDataApiGatewayAPI(router *gin.Engine) {
	router.Any(
		"/healthz", func(c *gin.Context) {
			c.Status(200)
		},
	)

	router.Use(func(c *gin.Context) {
		ctx := contextvar.WithRequestBeginTime(c.Request.Context(), time.Now())
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	})

	// Error
	router.Use(APIErrorJSONReporter())

	// Cors
	router.Use(Cors())

	// Metric
	router.Use(Metric())

	// Request Body Check
	router.Use(gatewayCheckReqBodySize)

	if !config.Global().IsAliCloudRegionalMode() {
		api := router.Group("/api/v1beta")
		{
			api.GET("/app/:external_app_id/endpoint/*path", dataapi.Advanced)
			api.POST("/app/:external_app_id/endpoint/*path", dataapi.Advanced)
			api.PUT("/app/:external_app_id/endpoint/*path", dataapi.Advanced)
			api.DELETE("/app/:external_app_id/endpoint/*path", dataapi.Advanced)

			api.PATCH("/app/:external_app_id/endpoint/*path", noRouter)
			api.HEAD("/app/:external_app_id/endpoint/*path", noRouter)
			api.OPTIONS("/app/:external_app_id/endpoint/*path", noRouter)
		}
		registerInnerDataServiceRoutes(router.Group("/inner/dataservice"))
		router.NoRoute(dataapi.HandleCustomDomainQuery)
		return
	}

	// AliCloud keeps the same regional handler paths as AWS. The extra
	// /dataservice prefix is only used by the upstream API gateway and stripped
	// before the request reaches this regional service.
	registerAliCloudInnerDataServiceRoutes(router.Group("/inner/dataservice"))
	router.NoRoute(noRouter)
}

func registerInnerDataServiceRoutes(innerApi *gin.RouterGroup) {
	innerApi.Use(innerDataServiceHostGuard())
	innerApi.Use(innerBasicAuthGuard())
	innerApi.POST("/v2/endpoint/test", dataapi.TestEndpointV2)
	innerApi.POST("/v2/exec/sqltemplate", dataapi.ExecSqlTemplateV2)
	innerApi.POST("/exec/sql", dataapi.ExecSql)
	innerApi.POST("/app_id_cluster_permission_check", dataapi.CheckAppIDClusterPermission)
	innerApi.POST("/database/meta", dataapi.GetDatabaseMeta)
	innerApi.POST("/session/create", dataapi.CreateSession)
	innerApi.POST("/session/close", dataapi.CloseSession)
	innerApi.POST("/parse/stmt", dataapi.ParseOneStmt)
	innerApi.POST("/v2/resolvehost", dataapi.ResolveHost)
}

func registerAliCloudInnerDataServiceRoutes(innerApi *gin.RouterGroup) {
	innerApi.Use(innerDataServiceHostGuard())
	innerApi.Use(innerBasicAuthGuard())
	innerApi.POST("/v2/exec/sqltemplate", dataapi.ExecSqlTemplateV2)
	innerApi.POST("/exec/sql", dataapi.ExecSql)
	innerApi.POST("/database/meta", dataapi.GetDatabaseMeta)
	innerApi.POST("/session/create", dataapi.CreateSession)
	innerApi.POST("/session/close", dataapi.CloseSession)
	innerApi.POST("/parse/stmt", dataapi.ParseOneStmt)
}

func innerDataServiceHostGuard() gin.HandlerFunc {
	return func(c *gin.Context) {
		if !isValidInnerDataServiceHost(c.Request.Host) {
			logger.Warn(c.Request.Context(), "invalid request host", zap.String("host", c.Request.Host))
			c.Status(404)
			c.Abort()
			return
		}
	}
}

type innerAuthInfo struct {
	service string
	envType string
	authStr string
}

func innerBasicAuthGuard() gin.HandlerFunc {
	return func(c *gin.Context) {
		if !shouldUseInnerBasicAuth() {
			c.Next()
			return
		}

		authInfo, ok := getInnerAuthInfo(c)
		if !ok {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}
		if !validateInnerAuth(authInfo, config.Global().ServerConf.Env, config.Global().ServerConf.CrossRegionToken) {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}

		c.Request.Header.Del("Authorization")
		c.Request.Header.Set("X-Request-Type", "inner")
		c.Request.Header.Set("X-Request-Service", authInfo.service)
		c.Next()
	}
}

func shouldUseInnerBasicAuth() bool {
	cfg := config.Global()
	if !cfg.IsRegionMode() {
		return false
	}
	provider := strings.ToLower(strings.TrimSpace(cfg.ServerlessConf.Provider))
	return provider != "" && provider != constants.ClusterCloudProviderAWS
}

func getInnerAuthInfo(c *gin.Context) (*innerAuthInfo, bool) {
	header := c.GetHeader("Authorization")
	if header == "" {
		return nil, false
	}
	authStr := extractInnerBasicAuthorization(header)
	if authStr == "" {
		return nil, false
	}
	return parseInnerAuthKey(authStr)
}

func extractInnerBasicAuthorization(authorization string) string {
	authType, authStr, ok := strings.Cut(strings.TrimSpace(authorization), " ")
	if !ok || !strings.EqualFold(authType, "basic") {
		return ""
	}
	return strings.TrimSpace(authStr)
}

func parseInnerAuthKey(authStr string) (*innerAuthInfo, bool) {
	decoded, err := base64.StdEncoding.DecodeString(authStr)
	if err != nil {
		return nil, false
	}
	items := strings.Split(string(decoded), ":")
	if len(items) != 4 {
		return nil, false
	}
	return &innerAuthInfo{
		service: items[0],
		envType: items[1],
		authStr: authStr,
	}, true
}

func validateInnerAuth(authInfo *innerAuthInfo, expectedEnv, expectedAuthStr string) bool {
	if authInfo == nil || expectedAuthStr == "" {
		return false
	}
	if authInfo.envType != expectedEnv {
		return false
	}
	if len(authInfo.authStr) != len(expectedAuthStr) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(authInfo.authStr), []byte(expectedAuthStr)) == 1
}

func isValidInnerDataServiceHost(host string) bool {
	if !strings.HasSuffix(host, "tidbcloud.com") {
		return false
	}
	if config.Global().IsAliCloudRegionalMode() {
		// AliCloud regional host rules have evolved from the early
		// *.regional-api.*.aliyun.tidbcloud.com format to the current
		// regionalsvc01.<region>.<env>.alicloud.tidbcloud.com gateway host.
		// Keep both shapes accepted here so the regional service remains
		// compatible during rollout and environment migration.
		return (strings.HasPrefix(host, "regionalsvc01.") && strings.Contains(host, ".alicloud.")) ||
			(strings.Contains(host, ".regional-api.") && strings.Contains(host, ".aliyun."))
	}
	return strings.Contains(host, "inner")
}

func gatewayCheckReqBodySize(c *gin.Context) {
	x := c.Request.ContentLength
	if x > MAX {
		epResType := constants.EndpointResTypeSQL
		if strings.Contains(c.FullPath(), "/app/chat2qery-") {
			epResType = constants.EndpointResTypeChat2Data
		}
		res := dto.NewErrorAdvancedRes(http.StatusBadRequest, "Request body is too large", epResType)
		c.JSON(http.StatusBadRequest, res)
		return
	}
	c.Next()
}

func pprofRouter(router *gin.Engine) {
	if !config.IsProd() {
		pprof.Register(router)
	}
}

func noRouter(g *gin.Context) {
	g.AbortWithStatus(http.StatusNotFound)
}
