/******************************************************************************
 * Copyright (c)  2022 PingCAP, Inc.                                          *
 * Licensed under the Apache License, Version 2.0 (the "License");            *
 * you may not use this file except in compliance with the License.           *
 * You may obtain a copy of the License at                                    *
 *                                                                            *
 * http://www.apache.org/licenses/LICENSE-2.0                                 *
 *                                                                            *
 * Unless required by applicable law or agreed to in writing, software        *
 * distributed under the License is distributed on an "AS IS" BASIS,          *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 * See the License for the specific language governing permissions and        *
 * limitations under the License.                                             *
 ******************************************************************************/

/*******************************************************************************
 * @File: handler.go
 * @Description:
 * @Author: jiangxunyu@pingcap.com
 * @Version: 1.0.0
 * @Date: 2022/6/8 20:08
*******************************************************************************/

package globalapi

import (
	"context"
	"encoding/json"
	stderrors "errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/tidbcloud/observability/pkg/common/awsauth"
	"github.com/tidbcloud/observability/pkg/common/metrics"

	"github.com/ddliu/go-httpclient"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/globalservice"
	globalstructs "github.com/tidbcloud/observability/pkg/globalservice/structs"
	"go.uber.org/zap"
)

// defaultOptionalParam request regional server default optional parameters
var defaultOptionalParam = OptionalParam{}

const (
	paramKeyTenantID      = "tenantID"
	paramKeyBizType       = "bizType"
	paramKeyProjectID     = "projectID"
	paramKeyApplicationID = "applicationID"
)

var (
	paramKeyMap map[string]paramKeys = map[string]paramKeys{
		paramKeyTenantID: {
			pathParamKey:   paramKeyTenantID,
			headerParamKey: string(constants.OpenAPIHeaderOrgId),
		},
		paramKeyBizType: {
			pathParamKey:   paramKeyBizType,
			headerParamKey: string(constants.OpenAPIHeaderBizType),
		},
		paramKeyProjectID: {
			pathParamKey:   paramKeyProjectID,
			headerParamKey: string(constants.OpenAPIHeaderProjectID),
		},
		paramKeyApplicationID: {
			pathParamKey:   paramKeyApplicationID,
			headerParamKey: string(constants.OpenAPIHeaderApplicationID),
		},
	}
)

type paramKeys struct {
	pathParamKey   string
	headerParamKey string
}

type OptionalParam struct {
	Url string `json:"url"`
}

type ResponseResult struct {
	HttpCode int         `json:"httpCode"`
	Content  interface{} `json:"content"`
}

// AutoRouteRegionalServer
// @Description: automatic analysis of routing addresses for forwarding to the regional server
// @Parameter c
// @Parameter optionalParam
// @Parameter appends
func AutoRouteRegionalServer(c *gin.Context, optionalParam OptionalParam, appends ...func(c *gin.Context) error) {
	// handling user-injected logic
	for _, appender := range appends {
		err := appender(c)
		if err != nil {
			handleError(c, err)
			return
		}
	}

	// resolve dns address and request regional server api
	responseResult, err := resolveAndReqRegionalServer(c, optionalParam)
	if err != nil {
		handleError(c, err)
		return
	}
	c.JSON(responseResult.HttpCode, responseResult.Content)
	if responseResult.HttpCode != http.StatusOK {
		newCtx := context.WithValue(c.Request.Context(), constants.RequestResultCtxKey, responseResult.Content)
		c.Request = c.Request.WithContext(newCtx)
	}
}

// handleError
// @Description: uniform handling of errors
// @Parameter c
// @Parameter err
func handleError(c *gin.Context, err error) {
	if c == nil || c.Request == nil {
		return
	}

	var (
		status int
		result message.Result
	)

	if _, ok := err.(errors.Error); ok {
		status = err.(errors.Error).Code().GetHttpCode()
		result = message.Result{
			Code:    int(err.(errors.Error).Code()),
			Message: err.Error(),
		}
	} else {
		status = http.StatusInternalServerError
		result = message.Result{
			Code:    int(errors.UnrecognizedError),
			Message: err.Error(),
		}

	}

	c.JSON(status, result)
	newCtx := context.WithValue(c.Request.Context(), constants.RequestResultCtxKey, result)
	c.Request = c.Request.WithContext(newCtx)
}

// resolveAndReqRegionalServer
// @Description: resolve regional server address and request regional server api
// @Parameter c
// @Parameter optionalParam
// @return result
// @return err
func resolveAndReqRegionalServer(c *gin.Context, optionalParam OptionalParam) (result ResponseResult, err error) {
	// get resolve parameter from gin context
	obj, ok := c.Get(structs.GlobalServerRequestParametersKey)
	if !ok {
		log.Error("get all request parameters failed")
		metrics.HandleGlobalBizErrorMetrics("get all request parameters failed")
		return result, errors.New(errors.GetRequestParamError, "get all request parameters failed")
	}
	reqParams := obj.(structs.RequestParameters)

	// Build routing context for logical region routing
	routingCtx := buildRoutingContext(reqParams, c.Request.URL.Path)

	// Resolve serverless child-cluster dimensions to parent-cluster dimensions
	// before route lookup. On failure, log the error and fall back to the
	// original routing context rather than rejecting the request.
	// mgmt call failures are propagated so the caller can retry.
	if shouldResolveServerlessChild(routingCtx) {
		resolved, resolveErr := resolveServerlessChild(c.Request.Context(), routingCtx)
		if resolveErr != nil {
			if stderrors.Is(resolveErr, ErrMgmtCallFailed) {
				metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("resolve serverless child mgmt call failed: %s", resolveErr.Error()))
				return result, resolveErr
			}
			log.Warn("resolve serverless child failed, falling back to original routing context",
				zap.Error(resolveErr),
				zap.String("applicationID", routingCtx.ApplicationID),
				zap.String("bizType", routingCtx.BizType))
			metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("resolve serverless child failed, fallback to original context. error: %s", resolveErr.Error()))
		} else {
			routingCtx = resolved
		}
	}

	// Try logical region routing first, fallback to physical region routing
	globalManager := globalservice.GetGlobalManager()
	var routeInfo *globalstructs.RegionalRouter
	routeInfo, err = globalManager.GetRegionalRouteInfo(c, routingCtx)
	if err != nil {
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("get regional route address failed. error: %s", err.Error()))
		return result, err
	}

	// Deprecated: Service Auth between regionalserver & globalserver is done via waf.
	// uniformly inject of authentication information in the request header
	token, err := awsauth.GetStsToken(c)
	if err != nil {
		// flush cache
		awsauth.GetStsTokenCache().Flush()
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("gen sts token failed. error: %s", err.Error()))
		return result, errors.New(errors.UnrecognizedError, err.Error())
	}
	reqParams.Headers[constants.AuthorizationHeader] = fmt.Sprintf("%s %s", constants.BearerPrefix, token)

	requestID, ok := c.Get(string(constants.OpenAPIHeaderRequestId))
	if ok {
		reqParams.Headers[string(constants.OpenAPIHeaderRequestId)] = requestID.(string)
	}

	// filling optional parameters
	if optionalParam.Url == "" {
		optionalParam.Url = c.Request.URL.String()
	}

	// constructing regional server access addresses
	regionalApiUrl := fmt.Sprintf("%s%s", routeInfo.DnsAddress, optionalParam.Url)

	// Add SigV4 authentication headers if AccessPrincipal is configured (for BYOC AWS API Gateway)
	if routeInfo.AccessPrincipal != "" {
		var body []byte
		if reqParams.BodyParams != "" {
			body = []byte(reqParams.BodyParams)
		}
		sigv4Headers, err := awsauth.GetSigV4Headers(c.Request.Context(), awsauth.SigV4AuthInfo{
			RoleArn:    routeInfo.AccessPrincipal,
			ExternalID: routeInfo.AccessExternalID,
			Region:     reqParams.Region,
		}, c.Request.Method, regionalApiUrl, body)
		if err != nil {
			log.Error("Failed to generate SigV4 headers", zap.Error(err))
			metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("failed to generate SigV4 headers: %s", err.Error()))
			return result, err
		}
		// Merge SigV4 headers into request headers
		for key, value := range sigv4Headers {
			reqParams.Headers[key] = value
		}
		log.Debug("Added SigV4 authentication headers for BYOC regional server",
			zap.String("roleArn", routeInfo.AccessPrincipal))
	}

	log.Debug("Constructing regional server URL",
		zap.String("Request_Method", c.Request.Method),
		zap.String("Regional_API_URL", regionalApiUrl),
	)

	log.Debug("Request parameters",
		zap.Any("Parameters", reqParams),
	)

	result, err = reqRegionalServer(c, reqParams, regionalApiUrl)
	AuditIfNeeded(c, result, err)
	return result, err
}

// buildRoutingContext
// @Description: build routing context from request parameters for logical region routing
// @Parameter reqParams
// @return globalstructs.RoutingContext
func buildRoutingContext(reqParams structs.RequestParameters, requestPath string) globalstructs.RoutingContext {
	logical := ""
	if shouldRecognizeLogicalRegionHeader(requestPath) {
		if v, ok := reqParams.Headers[string(constants.OpenAPIHeaderLogicalRegion)]; ok {
			logical = v
		}
	}

	projectID := extractParam(reqParams, paramKeyProjectID)
	applicationID := extractParam(reqParams, paramKeyApplicationID)

	// For POST/PUT requests (e.g., cluster creation), projectID and applicationID
	// may only be present in the request body, not in headers or path params.
	// Fall back to parsing the body so routing decisions have the full context.
	if projectID == "" || applicationID == "" {
		bodyProjectID, bodyApplicationID := extractDimsFromBody(reqParams.BodyParams)
		if projectID == "" {
			projectID = bodyProjectID
		}
		if applicationID == "" {
			applicationID = bodyApplicationID
		}
	}

	return globalstructs.RoutingContext{
		Vendor:        reqParams.Vendor,
		Region:        reqParams.Region,
		BizType:       extractParam(reqParams, paramKeyBizType),
		TenantID:      extractParam(reqParams, paramKeyTenantID),
		ProjectID:     projectID,
		ApplicationID: applicationID,
		ShowDeleted:   extractBoolParam(reqParams, "showDeleted"),
		KubernetesID:  reqParams.Headers[string(constants.OpenAPIHeaderKubernetesID)],
		LogicalRegion: logical,
	}
}

// extractDimsFromBody parses projectID and applicationID from a JSON request body.
// Returns empty strings if the body is empty or the fields are absent.
func extractDimsFromBody(body string) (projectID, applicationID string) {
	if body == "" {
		return
	}
	var parsed struct {
		ProjectID     string `json:"projectID"`
		ApplicationID string `json:"applicationID"`
	}
	if err := json.Unmarshal([]byte(body), &parsed); err != nil {
		return
	}
	return parsed.ProjectID, parsed.ApplicationID
}

func shouldRecognizeLogicalRegionHeader(requestPath string) bool {
	return pathMatchesWhitelist(requestPath, "/ops/api/v1") ||
		pathMatchesWhitelist(requestPath, "/internal/v1/routes") ||
		pathMatchesWhitelist(requestPath, "/api/v1/alertrule-template")
}

func pathMatchesWhitelist(requestPath, prefix string) bool {
	return requestPath == prefix || strings.HasPrefix(requestPath, prefix+"/")
}

// extractParam
// @Description: extract parameter value from various sources in request parameters.
// Lookup priority: header > path param > query param.
// @Parameter reqParams
// @Parameter paramName
// @return string
func extractParam(reqParams structs.RequestParameters, paramKey string) string {
	// Try to get from headers
	if value, exists := reqParams.Headers[paramKeyMap[paramKey].headerParamKey]; exists {
		return value
	}
	// Try to get from path params.
	for i, param := range reqParams.PathParams {
		if param.Key == paramKeyMap[paramKey].pathParamKey {
			return reqParams.PathParams[i].Value
		}
	}
	// Try to get from query params.
	if value, exists := reqParams.QueryParams[paramKeyMap[paramKey].pathParamKey]; exists {
		return value
	}

	// Return empty string if not found
	return ""
}

func extractBoolParam(reqParams structs.RequestParameters, paramName string) bool {
	if value, exists := reqParams.QueryParams[paramName]; exists {
		if parsed, err := strconv.ParseBool(value); err == nil {
			return parsed
		}
	}
	if value, exists := reqParams.FormParams[paramName]; exists {
		if parsed, err := strconv.ParseBool(value); err == nil {
			return parsed
		}
	}
	return false
}

// reqRegionalServer
// @Description: routing request regional server
// @Parameter c
// @Parameter reqParams
// @Parameter regionalApiUrl
// @return result
// @return err
func reqRegionalServer(c *gin.Context, reqParams structs.RequestParameters, regionalApiUrl string) (result ResponseResult, err error) {
	// creating regional server to route access requests
	var resp *httpclient.Response
	switch strings.ToUpper(c.Request.Method) {
	case http.MethodGet:
		resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).Get(regionalApiUrl)
	case http.MethodPost:
		if strings.Contains(reqParams.Headers["Content-Type"], "application/json") {
			resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).PostJson(regionalApiUrl, reqParams.BodyParams)
		} else if strings.Contains(reqParams.Headers["Content-Type"], "multipart/form-data") {
			// delete the content-type headers, regenerate the boundary and construct a new form-data request
			delete(reqParams.Headers, "Content-Type")
			resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).PostMultipart(regionalApiUrl, reqParams.FormParams)
		} else {
			resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).Post(regionalApiUrl, reqParams.FormParams)
		}
	case http.MethodPut:
		resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).PutJson(regionalApiUrl, reqParams.BodyParams)
	case http.MethodDelete:
		resp, err = httpclient.Begin().WithHeaders(reqParams.Headers).Delete(regionalApiUrl, reqParams.FormParams)
	default:
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("no support for %s method type request access", c.Request.Method))
		return result, errors.NewErrorf(errors.NoSupportRequestMethodError, "no support for %s method type request access", c.Request.Method)
	}
	if err != nil {
		log.Error("request regional server failed", zap.Error(err))
		// flush cache
		awsauth.GetStsTokenCache().Flush()
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("request regional server failed. error: %s", err.Error()))
		return result, errors.NewErrorf(errors.HttpClientResponseError, "%s", err.Error())
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("io read response body failed. error: %s", err.Error()))
		return result, errors.New(errors.IOReadFileError, err.Error())
	}
	var content interface{}
	if err = json.Unmarshal(body, &content); err != nil {
		log.Error("unmarshal json failed",
			zap.Error(err),
			zap.String("Content", string(body)))
		metrics.HandleGlobalBizErrorMetrics(fmt.Sprintf("unmarshal response body failed. error: %s", err.Error()))
		return result, errors.New(errors.UnmarshalError, err.Error())
	}
	result = ResponseResult{
		HttpCode: resp.StatusCode,
		Content:  content,
	}
	return result, nil
}
