package checker

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

	"github.com/samber/lo"
	"google.golang.org/grpc"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"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/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

// StdTidbGroupIdExtractor extracts cluster_id and tidb_node_group_id from requests that
// implement both GetClusterId() and GetTidbNodeGroupId() directly.
func StdTidbGroupIdExtractor(req any) (clusterIdStr, tidbGroupIdStr string, err error) {
	type tidbGroupRequest interface {
		GetClusterId() string
		GetTidbNodeGroupId() string
	}
	r, ok := req.(tidbGroupRequest)
	if !ok {
		return "", "", bizerr.New(bizerr.ErrCodeParams, "invalid request: req=%v", req)
	}
	if r.GetTidbNodeGroupId() == "" {
		return "", "", bizerr.New(bizerr.ErrCodeParams, "empty tidb node group id")
	}
	return r.GetClusterId(), r.GetTidbNodeGroupId(), nil
}

// NewTidbGroupPermissionChecker creates a permission checker that validates the requesting user
// owns the cluster and tidb node group. The extractIds function extracts cluster ID and
// tidb group ID from the request.
func NewTidbGroupPermissionChecker(extractIds func(req any) (clusterIdStr, tidbGroupIdStr string, err 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, tidbGroupIdStr, err := extractIds(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)
		}

		if cluster.DeployType != entity.DeployTypeDedicated {
			return ctx, req, bizerror.NewNotFound(errors.New("cluster type is not dedicated"), "cluster not found")
		}

		if tidbGroupIdStr != "" {
			tidbGroupId, err := strconv.ParseUint(tidbGroupIdStr, 10, 64)
			if err != nil {
				return ctx, req, bizerr.WrapErrorWithCode(
					err,
					bizerr.ErrCodeParams,
					"invalid tidb node group id: %s",
					tidbGroupIdStr,
				)
			}

			resp, err := checkCtx.DedicatedCli.ListTidbNodeGroups(ctx, &globaldedicatedv1.ListTidbNodeGroupsRequest{
				ClusterIds: []uint64{cluster.ClusterId},
				Ids:        []uint64{tidbGroupId},
			})

			if err != nil {
				return ctx, req, bizerror.NewInternal(err, "failed to query tidb node group")
			}

			if len(resp.TidbNodeGroups) == 0 {
				return ctx, req, bizerror.NewNotFound(errors.New("tidb node group not found"), "tidb node group not found")
			}
		}

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

		if tidbGroupIdStr != "" {
			reqMeta.ResourceID = tidbGroupIdStr
		}

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