package mgmtservicecmock

import (
	"context"
	"fmt"
	"sync"
	"time"

	"braces.dev/errtrace"
	"github.com/google/uuid"
	"github.com/mennanov/fmutils"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
)

func setupFakeServerlessGRPCServer(s *grpc.Server) {
	svc := &fakeTiDBMgmtServerlessService{
		clusters:        map[string]*openapiserverlessv1beta1.Cluster{},
		deletedClusters: map[string]*openapiserverlessv1beta1.Cluster{},
	}
	openapiserverlessv1beta1.RegisterClusterServiceServer(s, svc)
}

type fakeTiDBMgmtServerlessService struct {
	openapiserverlessv1beta1.UnimplementedClusterServiceServer

	mu sync.Mutex
	// name -> cluster
	clusters        map[string]*openapiserverlessv1beta1.Cluster
	deletedClusters map[string]*openapiserverlessv1beta1.Cluster
}

func (*fakeTiDBMgmtServerlessService) ListRegions(
	_ context.Context,
	_ *openapiserverlessv1beta1.ListRegionsRequest,
) (*openapiserverlessv1beta1.ListRegionsResponse, error) {
	regions := []*openapicommonv1beta1.Region{
		{
			Name:          "regions/aws-us-west-1",
			DisplayName:   "AWS US West 1",
			CloudProvider: openapicommonv1beta1.Region_aws,
		},
		{
			Name:          "regions/gcp-us-central1",
			DisplayName:   "GCP US Central 1",
			CloudProvider: openapicommonv1beta1.Region_gcp,
		},
	}

	response := &openapiserverlessv1beta1.ListRegionsResponse{
		Regions: regions,
	}

	return response, nil
}

func (f *fakeTiDBMgmtServerlessService) CreateCluster(
	_ context.Context,
	r *openapiserverlessv1beta1.CreateClusterRequest,
) (*openapiserverlessv1beta1.Cluster, error) {
	f.mu.Lock()
	defer f.mu.Unlock()

	id := uuid.New().String()
	cluster := r.Cluster
	cluster.Name = fmt.Sprintf("clusters/%s", id)
	cluster.ClusterId = id
	cluster.State = openapicommonv1beta1.ClusterState_CREATING
	cluster.CreateTime = timestamppb.New(time.Now())
	cluster.UpdateTime = cluster.CreateTime

	f.clusters[cluster.ClusterId] = cluster

	return cluster, nil
}

func (f *fakeTiDBMgmtServerlessService) ListClusters(
	_ context.Context,
	_ *openapiserverlessv1beta1.ListClustersRequest,
) (*openapiserverlessv1beta1.ListClustersResponse, error) {
	f.mu.Lock()
	defer f.mu.Unlock()

	resp := &openapiserverlessv1beta1.ListClustersResponse{
		TotalSize: uint32(len(f.clusters)),
	}
	for _, v := range f.clusters {
		resp.Clusters = append(resp.Clusters, v)
	}

	return resp, nil
}

func (f *fakeTiDBMgmtServerlessService) UpdateCluster(
	_ context.Context,
	r *openapiserverlessv1beta1.PartialUpdateClusterRequest,
) (*openapiserverlessv1beta1.Cluster, error) {
	f.mu.Lock()
	defer f.mu.Unlock()

	r.UpdateMask.Normalize()
	if !r.UpdateMask.IsValid(&openapiserverlessv1beta1.Cluster{}) {
		st, _ := status.New(codes.InvalidArgument, "invalid update mask").
			WithDetails(&errdetails.BadRequest_FieldViolation{
				Field:       "updateMask",
				Description: "invalid update mask",
			})
		return nil, errtrace.Wrap(st.Err())
	}

	cluster, ok := f.clusters[r.Cluster.ClusterId]
	if ok {
		fmutils.Filter(r.GetCluster(), r.UpdateMask.GetPaths())
		proto.Merge(cluster, r.GetCluster())
		cluster.UpdateTime = timestamppb.New(time.Now())

		return cluster, nil
	}

	st, _ := status.New(codes.NotFound, "cluster not found").
		WithDetails(&errdetails.ErrorInfo{
			Reason: "Unknown cluster id",
			Domain: "",
			Metadata: map[string]string{
				"cluster_id": r.Cluster.ClusterId,
			},
		})
	return nil, errtrace.Wrap(st.Err())
}

func (f *fakeTiDBMgmtServerlessService) DeleteCluster(
	_ context.Context,
	r *openapiserverlessv1beta1.DeleteClusterRequest,
) (*openapiserverlessv1beta1.Cluster, error) {
	f.mu.Lock()
	defer f.mu.Unlock()

	cluster, ok := f.clusters[r.ClusterId]
	delete(f.clusters, r.ClusterId)

	if ok {
		cluster.UpdateTime = timestamppb.New(time.Now())
		f.deletedClusters[r.ClusterId] = cluster
		return cluster, nil
	}

	st, _ := status.New(codes.NotFound, "cluster not found").
		WithDetails(&errdetails.ErrorInfo{
			Reason: "Unknown cluster id",
			Domain: "",
			Metadata: map[string]string{
				"cluster_id": r.ClusterId,
			},
		})
	return nil, errtrace.Wrap(st.Err())
}
