package checker

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

// StdClusterIdExtractor extracts cluster_id from requests that implement GetClusterId() directly.
func StdClusterIdExtractor(req any) (string, error) {
	type clusterRequest interface{ GetClusterId() string }
	r, ok := req.(clusterRequest)
	if !ok {
		return "", bizerr.New(bizerr.ErrCodeParams, "invalid request: req=%v", req)
	}
	return r.GetClusterId(), nil
}

// NewClusterPermissionChecker creates a permission checker that validates the requesting user
// owns the cluster. The extractClusterId function extracts the cluster ID string from the
// request — callers MUST provide an appropriate extractor for each method registered in the
// interceptor, so new APIs cannot silently skip permission checks.
func NewClusterPermissionChecker(extractClusterId func(req any) (string, error)) CheckFunc {
	return func(
		ctx context.Context,
		checkCtx *CheckContext,
		req any,
		info *grpc.UnaryServerInfo,
	) (context.Context, any, error) {
		reqMeta, err := metadata.ExtractRequestMeta(ctx)
		if err != nil {
			return ctx, req, bizerror.NewInternal(err, "failed to extract request meta")
		}

		clusterIdStr, err := extractClusterId(req)
		if err != nil {
			return ctx, req, err
		}

		clusterId, err := strconv.ParseUint(clusterIdStr, 10, 64)
		if err != nil {
			return ctx, req, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id: %s", clusterIdStr)
		}

		cluster, err := checkCtx.ClusterRepo.GetCluster(ctx, clusterId, true)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ctx, req, bizerror.NewNotFound(err, fmt.Sprintf("cluster %d not found", clusterId))
			}
			return ctx, req, bizerror.NewInternal(err, "failed to get cluster")
		}

		if err := reqMeta.CheckProjectIDsPermission(cluster.ProjectId); err != nil {
			return ctx, req, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
		}

		// Check if the cluster type is correct
		switch {
		case strings.Contains(info.FullMethod, "dedicated"):
			if cluster.DeployType != entity.DeployTypeDedicated {
				return ctx, req, bizerror.NewNotFound(errors.New("cluster type is not dedicated"), "cluster not found")
			}
		case strings.Contains(info.FullMethod, "serverless"):
			if cluster.DeployType != entity.DeployTypeServerless {
				return ctx, req, bizerror.NewNotFound(errors.New("cluster type is not serverless"), "cluster not found")
			}
		}

		reqMeta.ClusterID = cluster.ClusterId
		reqMeta.DefaultProjectID = cluster.ProjectId
		reqMeta.IsCheckPermission = lo.ToPtr(true)

		return metadata.AppendToIncomingMeta(ctx, reqMeta), req, nil
	}
}

// NewCreateClusterPermissionChecker creates a permission checker for cluster creation.
// The extractLabels function extracts the resource labels from the create request.
func NewCreateClusterPermissionChecker(extractLabels func(req any) (map[string]string, error)) CheckFunc {
	return func(
		ctx context.Context,
		_ *CheckContext,
		req any,
		info *grpc.UnaryServerInfo,
	) (context.Context, any, error) {
		reqMeta, err := metadata.ExtractRequestMeta(ctx)
		if err != nil {
			return ctx, req, bizerror.NewInternal(err, "failed to extract request meta")
		}

		projectId := reqMeta.DefaultProjectID
		labels, err := extractLabels(req)
		if err != nil {
			tilog.Error(ctx, "failed to get label from create request", zap.Error(err))
			return ctx, req, err
		}

		if labels != nil {
			if projectIdLabel, ok := labels[common.ProjectIDLabelKey]; ok {
				projectId, err = strconv.ParseUint(projectIdLabel, 10, 64)
				if err != nil {
					return ctx, req, bizerr.WrapErrorWithCode(
						err,
						bizerr.ErrCodeParams,
						"invalid %s label",
						common.ProjectIDLabelKey,
					)
				}
			}
		}

		if err := reqMeta.CheckProjectIDsPermission(projectId); err != nil {
			return ctx, req, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
		}

		reqMeta.DefaultProjectID = projectId
		reqMeta.IsCheckPermission = lo.ToPtr(true)

		return metadata.AppendToIncomingMeta(ctx, reqMeta), req, nil
	}
}
