package ngm

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/tiinf/sdk/rbac/helperimpl"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/ngm/file_download"
	ngmutils "github.com/pingcap-inc/clinic/pkg/ngm/utils"
)

type dbaasHeader struct {
	CloudUser string `header:"X-CLOUD-USER" binding:"required"`
}

func dbaasAuthMw() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		var header dbaasHeader
		if err := ctx.ShouldBindHeader(&header); err != nil {
			utils.AuthError(ctx, "invalid dbaas user header")
			return
		}
		b64User, err := base64.StdEncoding.DecodeString(header.CloudUser)
		if err != nil {
			utils.AuthError(ctx, "invalid dbaas user header")
			return
		}
		var user session.CtxDbaasUser
		if err := json.Unmarshal(b64User, &user); err != nil {
			utils.AuthError(ctx, "invalid dbaas user header")
			return
		}
		ctx.Set(session.DbaasUserKey, &user)
		ctx.Next()
	}
}

// Clinic gateway can only guarantee users access to its own route. A :project
// path parameter is therefore authoritative. The new public NGM route omits
// :project for a shorter public URL; it uses x-project-id when supplied and
// falls back to the Clinic DB only when that header is empty.
// We pass the resolved values through headers for the existing NGM handlers.
// Refer to: https://github.com/pingcap-inc/clinic/pull/1009#discussion_r1119708607
func dbaasPathToHeaderMw(dbClient *gorm.DB) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		projectID := ctx.Param("project")
		if projectID == "" {
			projectID = strings.TrimSpace(ctx.GetHeader("x-project-id"))
		}
		if projectID == "" {
			clusterID := ctx.Param("cluster")
			var cluster db.Cluster
			if err := dbClient.WithContext(ctx.Request.Context()).Unscoped().Where("cluster_id = ?", clusterID).First(&cluster).Error; err != nil {
				if err != gorm.ErrRecordNotFound {
					log.Error("resolve NGM project ID from cluster failed", zap.String("cluster_id", clusterID), zap.Error(err))
				} else {
					log.Warn("resolve NGM project ID from cluster failed: cluster not found", zap.String("cluster_id", clusterID))
				}
				utils.ForbiddenError(ctx, "cluster not found")
				return
			}
			projectID = cluster.ProjectID
			if projectID == "" {
				log.Warn("resolve NGM project ID from cluster failed: project ID is empty", zap.String("cluster_id", clusterID))
				utils.ForbiddenError(ctx, "cluster project not found")
				return
			}
		}

		ctx.Request.Header.Set("x-org-id", ctx.Param("org"))
		ctx.Request.Header.Set("x-project-id", projectID)
		ctx.Request.Header.Set("x-cluster-id", ctx.Param("cluster"))
		if ctx.Param("project") == "" {
			ctx.Set(file_download.ExternalDownloadPathContextKey,
				fmt.Sprintf("/api/v1/orgs/%s/ngm/clusters/%s/oom_record/files", ctx.Param("org"), ctx.Param("cluster")))
		}
		ctx.Next()
	}
}

func clinicInternalAuthMw(
	permHelper helperimpl.AttributeSettablePermissionHelper,
	dbClient *gorm.DB,
	clinicClient *ngmutils.ClinicClient,
) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		path := ctx.Request.URL.Path
		if path == "/ngm/api/v1/continuous_profiling/download" { // little hack
			ctx.Next()
			return
		}

		data, err := base64.StdEncoding.DecodeString(ctx.GetHeader(session.CtxUserKey))
		if err != nil {
			utils.AuthError(ctx, "invalid user")
			return
		}
		var u session.CtxUser
		if err := json.Unmarshal(data, &u); err != nil {
			utils.AuthError(ctx, "invalid user")
			return
		}

		if !u.Internal {
			if !strings.Contains(path, "/topsql") && !strings.Contains(path, "/slow_query") && !strings.Contains(path, "/topru") {
				log.Warn("permission denied, invalid path for external user", zap.String("path", path))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}

			// check feature gate
			dbaasOrgIdStr := ctx.GetHeader("x-org-id")
			dbaasOrgId, err := strconv.ParseUint(dbaasOrgIdStr, 10, 64)
			if err != nil {
				log.Warn("permission denied, invalid dbaas org id", zap.String("dbaas_org_id", dbaasOrgIdStr))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			clinicOrgId, err := clinicClient.GetClinicOrgId(dbaasOrgId)
			if err != nil {
				log.Error("permission denied, get clinic org id failed", zap.Uint64("dbaas_org_id", dbaasOrgId), zap.Error(err))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			clusterIdStr := ctx.GetHeader("x-cluster-id")
			clusterId, err := strconv.ParseUint(clusterIdStr, 10, 64)
			if err != nil {
				log.Warn("permission denied, invalid cluster id", zap.String("cluster_id", clusterIdStr))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			cluster, err := clinicClient.GetCluster(clinicOrgId, clusterId)
			if err != nil {
				log.Error("permission denied, get cluster failed",
					zap.Uint64("dbaas_org_id", dbaasOrgId),
					zap.Uint64("clinic_org_id", clinicOrgId),
					zap.Uint64("cluster_id", clusterId),
					zap.Error(err))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			if strings.Contains(path, "/slow_query") && !cluster.FeatureGates.SlowQueryEnabled {
				log.Warn("permission denied, slow query feature gate is disabled", zap.Uint64("cluster_id", clusterId))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			if strings.Contains(path, "/topsql") && !cluster.FeatureGates.TopSQLEnabled {
				log.Warn("permission denied, topsql feature gate is disabled", zap.Uint64("cluster_id", clusterId))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			if strings.Contains(path, "/topru") && !cluster.FeatureGates.TopRUEnabled {
				log.Warn("permission denied, topru feature gate (topRUEnabled) is disabled", zap.Uint64("cluster_id", clusterId))
				utils.ForbiddenError(ctx, "permission denied")
				return
			}

			var user db.User
			err = dbClient.Transaction(func(txn *gorm.DB) error {
				err = txn.Unscoped().Where("id=?", u.UserID).First(&user).Error

				if err != nil && err != gorm.ErrRecordNotFound {
					return err
				}
				return nil
			})
			if err != nil || user.ExternalIdentifier == "" {
				utils.ForbiddenError(ctx, "permission denied")
				return
			}
			id := user.ExternalIdentifier
			if strings.HasPrefix(user.ExternalIdentifier, "tidbcloud-") {
				id = strings.Replace(id, "tidbcloud-", "", 1)
			}

			// check topsql permission
			if strings.Contains(path, "/topsql") {
				rUri := fmt.Sprintf("orgs/%d/ngm/topsql/read", dbaasOrgId)
				allow, err := permHelper.HasPermission(context.Background(), id, rUri)
				if err != nil {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
				if !allow {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
			}

			// check topru permission
			if strings.Contains(path, "/topru") {
				rUri := fmt.Sprintf("orgs/%d/ngm/topru/read", dbaasOrgId)
				allow, err := permHelper.HasPermission(context.Background(), id, rUri)
				if err != nil {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
				if !allow {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
			}

			// check slow query permission
			if strings.Contains(path, "/slow_query") {
				rUri := fmt.Sprintf("orgs/%d/ngm/slow_query/read", dbaasOrgId)
				allow, err := permHelper.HasPermission(context.Background(), id, rUri)
				if err != nil {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
				if !allow {
					utils.ForbiddenError(ctx, "permission denied")
					return
				}
			}
		}
		ctx.Set(session.CtxUserKey, &u)
		ctx.Next()
	}
}
