package clusterproxy

import (
	"context"
	"fmt"
	"time"

	"github.com/patrickmn/go-cache"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	apierrors "k8s.io/apimachinery/pkg/api/errors"

	"github.com/tidbcloud/ticdc-service-ng/pkg/clusterproxy/api"
	"github.com/tidbcloud/ticdc-service-ng/pkg/clusterproxy/client"
	"github.com/tidbcloud/ticdc-service-ng/pkg/config"
	"github.com/tidbcloud/ticdc-service-ng/pkg/regionalservice"
)

const (
	// DefaultClusterProxyTimeout is the default timeout for cluster proxy
	DefaultClusterProxyTimeout = 60
	// CDCHeavyOperationTimeout for cdc create and update Request, cdc need to list all the tables, and it can spend much time
	// so we need a longer timeout
	CDCHeavyOperationTimeout = 60 * 5
)

// ClusterProxy is the interface for the cluster proxy
// it provides the PD client, SQL client and changefeed client
type ClusterProxy interface {
	Client() client.Client
	api.PDClientGetter
	api.SQLClientGetter
	api.ChangefeedClientGetter
}

// Model is the interface for the cluster proxy model
type Model interface {
	// GetClusterProxy returns the cluster proxy for the given cluster ID
	GetClusterProxy(ctx context.Context, clusterID string, timeout int) (ClusterProxy, error)
}

var _ Model = &clusterProxyModelImpl{}

// The clusterProxyModelImpl struct provides the implementation of the Model interface.
type clusterProxyModelImpl struct {
	cache           *cache.Cache
	clusterPwdCache *cache.Cache
	cfg             *config.Config
	opts            []client.Option
	regionalCli     regionalservice.Client
	clientCreator   client.Creator
}

func NewClusterProxyModelImpl(
	cfg *config.Config,
	regionalCli regionalservice.Client,
	clientCreator client.Creator,
	opts ...client.Option,
) Model {
	return &clusterProxyModelImpl{
		cfg:             cfg,
		cache:           cache.New(1*time.Minute, 30*time.Second),
		clusterPwdCache: cache.New(1*time.Minute, 30*time.Second),
		opts:            opts,
		regionalCli:     regionalCli,
		clientCreator:   clientCreator,
	}
}

func (p *clusterProxyModelImpl) GetClusterProxy(
	ctx context.Context, tidbClusterID string, timeout int,
) (ClusterProxy, error) {
	logger := tilog.WithContextL(ctx)
	// todo: cache the proxy or refresh token when the auth token is expired
	//cacheKey := tidbClusterId
	//if proxy, ok := p.cache.Get(cacheKey); ok {
	//	return proxy.(ClusterProxy), nil
	//}

	tidbGroup, err := p.regionalCli.GetOneTiDBGroupByClusterID(ctx, tidbClusterID)
	if err != nil || tidbGroup == nil {
		logger.Error("Failed to get tidbGroup", zap.Any("tidbGroup", tidbGroup), zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "tidbGroup %s not found", tidbClusterID)
		}
		return nil, status.Errorf(codes.Internal, "Failed to get tidbGroup %s: %s", tidbGroup, err)
	}

	dataplane, err := p.regionalCli.GetDataPlaneByID(ctx, tidbGroup.DataplaneName)
	if err != nil {
		logger.Error("Failed to get cluster", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound,
				"dataplane %s not found", dataplane.Id)
		}
		return nil, status.Errorf(codes.Internal,
			"Failed to get dataplane %s: %s", dataplane.Id, err)
	}
	if dataplane.ApiGateway == "" {
		logger.Error("can't get ApiGateway")
		return nil, status.Errorf(codes.Internal, "Failed to get ApiGateway for dataplane %s, please try again later", dataplane.Id)
	}
	opts := append(p.opts, client.WithTimeout(timeout))
	proxyClient, err := p.clientCreator.CreateClient(ctx, tidbGroup,
		dataplane.AuthToken, dataplane.ApiGateway, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to get proxy, %s", err.Error())
	}
	proxy := NewClusterProxy(proxyClient)
	//p.cache.SetDefault(cacheKey, proxy)
	return proxy, nil
}

var _ ClusterProxy = &clusterProxyImpl{}

type clusterProxyImpl struct {
	client client.Client
}

func NewClusterProxy(client client.Client) ClusterProxy {
	return &clusterProxyImpl{
		client: client,
	}
}

func (p *clusterProxyImpl) Client() client.Client {
	return p.client
}

func (p *clusterProxyImpl) ChangefeedClient() api.ChangefeedAPIV2 {
	return api.NewChangefeedAPIV2(p.client)
}

func (p *clusterProxyImpl) PDClient() api.PDClient {
	return api.NewPdClient(p.client)
}

func (p *clusterProxyImpl) SQLClient() api.SQLClient {
	return api.NewSQLClient(p.client)
}
