package api

import (
	"dataservices/commons/config"
	"dataservices/commons/errs"
	"dataservices/commons/ginparser"
	"dataservices/commons/grpc"
	"dataservices/commons/metrics"
	dto "dataservices/internal/dto/dataapi"
	service "dataservices/internal/service/central"
	"net/http"
	"strconv"
	"strings"
	"time"

	e "errors"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	errors "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.uber.org/zap"

	account "github.com/tidbcloud/account/idl/pbgen/proto/account"
)

const (
	MAX = 1024 * 1024
)

var (
	chat2queryAppAllowedPath = map[string]bool{
		// controller.CreateDataApp
		"POST:/orgs/:org_id/projects/:project_id/apps": true,
		// controller.DeleteDataApp
		"DELETE:/orgs/:org_id/projects/:project_id/apps/:app_id": true,
		// controller.ListDataApps
		"GET:/orgs/:org_id/projects/:project_id/apps": true,
		// controller.GetDataApp
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id": true,
		// controller.UpdateDataApp
		"PUT:/orgs/:org_id/projects/:project_id/apps/:app_id": true,
		// controller.CreateAppCluster
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/clusters": true,
		// controller.DeleteAppCluster
		"DELETE:/orgs/:org_id/projects/:project_id/apps/:app_id/clusters/:cluster_id": true,
		// controller.ListAppCluster
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/clusters": true,
		// controller.GetClusterLinkedAPP
		"GET:/orgs/:org_id/projects/:project_id/clusters/:cluster_id/apps": true,
		// controller.CreateApiKey
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/apikeys": true,
		// controller.DeleteApiKey
		"DELETE:/orgs/:org_id/projects/:project_id/apps/:app_id/apikeys/:apikey_id": true,
		// controller.UpdateApiKey
		"PUT:/orgs/:org_id/projects/:project_id/apps/:app_id/apikeys/:apikey_id": true,
		// controller.ListApiKeys
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/apikeys": true,
		// controller.GetEndpoint
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id": true,
		// controller.PatchEndpoint
		"PATCH:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id": true,
		// controller.ListEndpoints
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints": true,
		// controller.CodeExample
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/example": true,
		// controller.EndpointBaseUrl
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/baseurl": true,
		// controller.ListDataAppUsage
		"GET:/orgs/:org_id/projects/:project_id/list_dataapp_usage": true,
		// controller.GetOpenApiSpecification
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/oas": true,
		// controller.GetOpenApiSpecificationByExternalAppId
		"GET:/orgs/:org_id/projects/:project_id/external-apps/:external_app_id/oas": true,
		// controller.DownloadOpenApiSpecification
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/oas/download": true,
		// controller.AuthorizeOpenApiSpecification
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/oas/authorize": true,
	}

	systemDataEndpointAllowedPath = map[string]bool{
		// Get Endpoint
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id": true,
		// Patch Endpoint
		"PATCH:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id": true,
		// Delete Endpoint
		"DELETE:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id": true,
		// Get Code Example
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/example": true,
		// Get Baseurl
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/baseurl": true,
		// Create Session
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/sessions": true,
		// Run Sql
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/statements": true,
		// Test Endpoint
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/test": true,
		// Deploy
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/online": true,
		// Check Deploy Status
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/version/:version/status": true,
		// Get Deploy Version List
		"GET:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/versions": true,
		// Discard One Endpoint
		"POST:/orgs/:org_id/projects/:project_id/apps/:app_id/endpoints/:endpoint_id/discard": true,
	}
)

// Metric metric middleware
func Metric() gin.HandlerFunc {
	return func(c *gin.Context) {
		tBegin := time.Now()
		c.Next()

		duration := float64(time.Since(tBegin)) / float64(time.Millisecond)

		//path := parsePath(c.Request.URL.Path)
		path := c.FullPath()

		httpStatus := c.Writer.Status()
		if httpStatus == http.StatusOK {
			innerErrors := c.Errors.ByType(gin.ErrorTypeAny)
			if len(innerErrors) > 0 {
				httpStatus = http.StatusInternalServerError
				err := innerErrors[0].Err
				if cerr, ok := err.(dto.CommonError); ok {
					err = cerr.Raw()
				}
				if te, ok := errors.AsTiError(err); ok {
					httpStatus = te.HTTPCode()
				}
			}
		}

		metrics.HTTPReqTotal.With(prometheus.Labels{
			"method": c.Request.Method,
			"path":   path,
			"status": strconv.Itoa(httpStatus),
		}).Inc()

		metrics.HTTPReqDuration.With(prometheus.Labels{
			"method": c.Request.Method,
			"path":   path,
		}).Observe(duration)
	}
}

func checkUserPermission(c *gin.Context) error {
	ctx := c.Request.Context()
	params := ginparser.NewCommonParamsByGin(c)
	validateKeys := &[]string{ginparser.PARAM_ORG_ID, ginparser.PARAM_PROJECT_ID}
	validateErr := params.ValidateUrl(validateKeys)
	if validateErr != nil {
		return validateErr
	}

	// check org access
	listOrgsByUserReq := &account.ListOrgsByUserRequest{UserId: params.User.Id}
	orgFlag := false
	orgs, err := grpc.ListOrgsByUser(ctx, listOrgsByUserReq)
	if err != nil {
		return errors.NewInternalServerError("ListOrgsByUser failed.", err)
	}
	for _, v := range orgs.Orgs {
		if v.Id == params.OrgID {
			orgFlag = true
			break
		}
	}
	if !orgFlag {
		return errors.NewForbidden("Check org failed. ", nil)
	}

	// check project access
	projectFlag := false
	listProjectsByOrgsInUserRequest := &account.ListProjectsByOrgsInUserRequest{
		UserId: params.User.Id,
		OrgIds: []uint64{params.OrgID},
	}
	projects, err := grpc.ListProjectsByOrgsInUser(ctx, listProjectsByOrgsInUserRequest)
	if err != nil {
		return errors.NewInternalServerError("ListProjectsByOrgsInUser failed.", err)
	}
	for _, v1 := range projects.Projects {
		for _, v2 := range v1.Projects {
			if v2.Id == params.ProjectID {
				projectFlag = true
				break
			}
		}
	}
	if !projectFlag {
		return errors.NewForbidden("Check project failed. ", nil)
	}

	/*
		// check org role
		user, ok := c.MustGet(ginparser.CONTEXT_ACCOUNT_USER).(*account.User)
		if !ok || user == nil {
			return errors.NewUnauthorized("User not found", nil)
		}
		role, err := grpc.GetAuthUser(ctx, &account.GetAuthUserRequest{UserEmail: user.Email, OrgId: params.OrgID})
		if err != nil {
			tilog.Error(ctx, "GetAuthUser failed", zap.String("user", user.Email), zap.Uint64("org_id", params.OrgID), zap.Error(err))
			return err
		}
		roleId := role.OrgRole.Number()
		if roleId != acl.OrgRole_ORG_ROLE_MEMBER.Number() &&
			roleId != acl.OrgRole_ORG_ROLE_OWNER.Number() {
			tilog.Error(ctx, "user has no permission", zap.Int32("role_id", int32(roleId)), zap.String("user", user.Email))
			return errors.NewForbidden("Unauthorized account role, only owner or member account has permission", nil)
		}
	*/
	return nil
}

func abortWithError(c *gin.Context, err error) {
	c.Error(err)
	c.Abort()
}

func CheckUser(c *gin.Context) {
	if IsTestRequest(c) {
		return
	}

	auth0Token, err := getToken(c)
	if err != nil {
		abortWithError(c, errors.NewUnauthorized("Auth token is invalid.", nil))
		return
	}
	c.Set(ginparser.CONTEXT_AUTH0_TOKEN, auth0Token)

	authUser, err := getUser(c, auth0Token)
	if err != nil {
		abortWithError(c, errors.NewUnauthorized("User not found.", err))
		return
	}
	c.Set(ginparser.CONTEXT_ACCOUNT_USER, authUser)
	c.Set(ginparser.CONTEXT_CURRENT_USER, authUser)

	err = checkUserPermission(c)
	if err != nil {
		abortWithError(c, err)
		return
	}
	c.Next()
}

func CheckRequestBodySize(c *gin.Context) {
	x := c.Request.ContentLength
	if x > MAX {
		tigin.AbortWithError(c, errors.NewBadRequest("request body is too large", nil))
		return
	}
	c.Next()
}

func getUser(c *gin.Context, auth0Token string) (*account.User, error) {
	req := &account.GetUserByTokenRequest{
		Token: auth0Token,
	}
	resp, err := grpc.GetUserByToken(c.Request.Context(), req)
	if err != nil {
		return nil, err
	}
	return resp.User, nil
}

func getToken(c *gin.Context) (string, error) {
	header := c.GetHeader("authorization")
	if header == "" {
		/*
		   since HTTP/2 use a lower case header, HTTP/1.x header is case-insensitive,
		   but generally we use a Authorization header in HTTP/1.x
		   LINK:
		   https://tools.ietf.org/html/rfc7540#section-8.1.2
		   https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
		*/
		header = c.GetHeader("Authorization")
	}

	if header == "" {
		return "", errors.NewUnauthorized("Auth token is required.", nil)
	}

	tokenSlice := strings.Split(header, " ")
	if len(tokenSlice) != 2 || strings.ToLower(tokenSlice[0]) != "bearer" {
		return "", errors.NewUnauthorized("Auth token is invalid.", nil)
	}

	return tokenSlice[1], nil
}

func IsTestRequest(c *gin.Context) bool {
	if !config.IsLocalMode() {
		return false
	}
	header := c.GetHeader("test-email")
	if header != "" {
		currentUser := &account.User{Email: header}
		c.Set(ginparser.CONTEXT_ACCOUNT_USER, currentUser)
		return true
	}
	return false
}

func CheckUserAppPermission(c *gin.Context) {

	ctx := c.Request.Context()
	params := ginparser.NewCommonParamsByGin(c)
	createdBy, _ := params.GetUserEmail()
	if params.AppID > 0 {
		appInfo, err := service.GetDataApp(ctx, params.AppID)
		if err != nil {
			if errors.IsError(err, errors.ErrNotFound) {
				err = errors.NewBadRequest("DataAPP does not exist.", err)
			} else {
				err = errors.NewInternalDB("Query DataAPP detail failed. ", err)
			}
			abortWithError(c, err)
			tilog.Error(ctx, "CheckUserAppPermission with dataapp failed. ",
				zap.Error(err),
				zap.Uint64("app_id", params.AppID),
				zap.String("user", createdBy))
			return
		}
		if appInfo.ProjectId != params.ProjectID {
			abortWithError(c, errors.NewForbidden("The current user is not authorized to access the app.", nil))
			tilog.Error(ctx, "The current user is not authorized to access the app.",
				zap.Uint64("app_id", params.AppID),
				zap.String("user", createdBy))
			return
		}
		if params.EndpointID > 0 {
			endpoint, err := service.GetEndpoint(ctx, params.EndpointID)
			if err != nil {
				if e.Is(err, errors.ErrNotFound) {
					err = errors.NewNotFound("Endpoint does not exist", err)
				} else {
					err = errors.NewInternalDB("Query Endpoint detail failed. ", err)
				}
				abortWithError(c, err)
				tilog.Error(ctx, "CheckUserAppPermission with endpoint failed. ",
					zap.Error(err),
					zap.Uint64("app_id", params.AppID),
					zap.Uint64("endpoint_id", params.EndpointID),
					zap.String("user", createdBy))
				return
			}
			if endpoint.DataappId != params.AppID {
				abortWithError(c, errors.NewForbidden("The current user is not authorized to access the endpoint.", nil))
				tilog.Error(ctx, "The current user is not authorized to access the endpoint. ",
					zap.Uint64("app_id", params.AppID),
					zap.Uint64("endpoint_id", params.EndpointID),
					zap.String("user", createdBy))
				return
			}

			if endpoint.IsChat2QueryEndpoint() {
				method := c.Request.Method
				path := strings.TrimPrefix(c.FullPath(), "/api/v1/dataservices/central")
				if !chat2queryAppAllowedPath[method+":"+path] {
					err = errs.ErrOperationNotAllowed
					abortWithError(c, err)
					tilog.Error(ctx, "CheckUserAppPermission with chat2query endpoint type failed. ", zap.Error(err), zap.String("method", method), zap.String("path", path), zap.Uint64("app_id", params.AppID), zap.String("user", createdBy))
					return
				}
			}

			if endpoint.IsSystemDataEndpoint() {
				// if isHit, err := grpc.IsHitSystemEndpointWhitelist(c.Request.Context(), params.OrgID, createdBy); err != nil {
				// 	err = errors.NewInternalDB("Failed to check user system endpoint permission.", err)
				// 	abortWithError(c, err)
				// 	tilog.Error(ctx, "IsHitSystemEndpointWhitelist failed. ",
				// 		zap.Error(err),
				// 		zap.Uint64("app_id", params.AppID),
				// 		zap.Uint64("endpoint_id", params.EndpointID),
				// 		zap.String("user", createdBy))
				// 	return
				// } else if !isHit {
				// 	err = errors.NewBadRequest("Current user no permission to access system endpoint.", err)
				// 	abortWithError(c, err)
				// 	tilog.Error(ctx, "IsHitSystemEndpointWhitelist user no permission. ",
				// 		zap.Error(err),
				// 		zap.Uint64("app_id", params.AppID),
				// 		zap.Uint64("endpoint_id", params.EndpointID),
				// 		zap.String("user", createdBy))
				// 	return
				// }

				method := c.Request.Method
				path := strings.TrimPrefix(c.FullPath(), "/api/v1/dataservices/central")
				if !systemDataEndpointAllowedPath[method+":"+path] {
					err = errs.ErrOperationNotAllowed
					abortWithError(c, err)
					tilog.Error(ctx, "CheckUserAppPermission with system data endpoint type failed. ",
						zap.Error(err),
						zap.String("method", method),
						zap.String("path", path),
						zap.Uint64("app_id", params.AppID),
						zap.String("user", createdBy))
					return
				}
			}
		}
		if params.APIKeyID > 0 {
			apikeyList, err := service.RbacListApiKeys(ctx, params.AppID, true)
			if err != nil {
				err = errors.NewInternalDB("Check apikey failed. ", err)
				abortWithError(c, err)
				tilog.Error(ctx, "CheckUserAppPermission with apikey failed. ",
					zap.Error(err),
					zap.Uint64("app_id", params.AppID),
					zap.Uint64("apikey_id", params.APIKeyID),
					zap.String("user", createdBy))
				return
			}
			var exists bool
			for _, apikey := range apikeyList {
				if apikey.Id == params.APIKeyID {
					exists = true
					break
				}
			}
			if !exists {
				abortWithError(c, errors.NewForbidden("The current user is not authorized to access the apikey.", nil))
				tilog.Error(ctx, "The current user is not authorized to access the apikey. ",
					zap.Uint64("app_id", params.AppID),
					zap.Uint64("apikey_id", params.APIKeyID),
					zap.String("user", createdBy))
				return
			}
		}
		if params.DeployLogID > 0 {
			_, err := service.GetDataAppDeployLogs(ctx, params.AppID, params.DeployLogID)
			if err != nil {
				if tierr, ok := errors.AsTiError(err); !ok || tierr.HTTPCode() == http.StatusInternalServerError {
					err = errors.NewInternalDB("Check deploy log failed. ", err)
				}
				abortWithError(c, err)
				tilog.Error(ctx, "Failed to get dataapp deploy log. ",
					zap.Error(err),
					zap.Uint64("app_id", params.AppID),
					zap.Uint64("log_id", params.DeployLogID),
					zap.String("user", createdBy))
				return
			}
		}
	}
	c.Next()
}

func checkDataappJwt(c *gin.Context) {
	t := c.Query("token")
	if t == "" {
		abortWithError(c, errors.NewForbidden("token is required.", nil))
		return
	}
	var claims dto.DataappJWTClaims
	if err := claims.ParseJwt(t); err != nil {
		abortWithError(c, err)
		return
	}

	c.Set(ginparser.CONTEXT_DATAAPP_JWT_CLAIMS, &claims)
	c.Next()
}
