package clustergateway

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
)

const (
	proxyApiPrefix        = "/proxy/cluster"
	clusterAgentApiPrefix = "/cluster-agent"

	sharedPoolIdParameter       = "sharedPoolId"
	logicalClusterIdParameter   = "logicalClusterId"
	dmClusterIdParameter        = "dmClusterId"
	keySpaceParameter           = "keySpace"
	dataplaneNamespaceParameter = "dataplaneNamespace"
	tibrCRNameParameter         = "tibrCRName"
	restoreIdParameter          = "restoreId"
	backupQueryParameter        = "query"

	// Notice: please don't change the values since it's query string parameter
	pdClusterIdQueryParameter    = "cluster_id"
	fromPrefixParameter          = "from_prefix"
	loadDetailsParameter         = "load_details"
	maxCountQueryParameter       = "max_count"
	keySpaceQueryParameter       = "keyspace"
	backupIdQueryParameter       = "backup_id"
	backupNameQueryParameter     = "backup_name"
	sourceKeySpaceQueryParameter = "source_keyspace"
	pointInTimeQueryParameter    = "point_in_time"

	// Pack & Restore Packed Backup query parameters
	backupNameTaskQueryParameter  = "backup_name"
	keyspaceTaskQueryParameter    = "keyspace"
	useS3BucketQueryParameter     = "use_s3_bucket"
	usePrefixQueryParameter       = "use_prefix"
	useEndpointQueryParameter     = "use_endpoint"
	exoticBackupEndpointParameter = "exotic_backup_endpoint"
	exoticBackupQueryParameter    = "exotic_backup"
	copyToQueryParameter          = "copy_to"
	maxThroughputQueryParameter   = "max_throughput"
	clusterIdTaskQueryParameter   = "cluster_id"
	taskIdParameter               = "taskId"

	// Cluster Agent Ops query parameters
	includeTikvUsageParameter = "include_tikv_usage"

	componentTypeParameter = "componentType"
	podNameParameter       = "podName"
)

var (
	tibrApiPrefix         = fmt.Sprintf("/proxy/cluster/{%s}/tibr/{%s}", sharedPoolIdParameter, tibrCRNameParameter)
	tibrRestoreApiPattern = fmt.Sprintf("%s/api/v1/restore_keyspace/{%s}", tibrApiPrefix, restoreIdParameter)
)

var operationMap = map[string]string{
	// PD Service
	"ListKeySpaces":            fmt.Sprintf("%s/{%s}/pd/pd/api/v2/keyspaces", proxyApiPrefix, sharedPoolIdParameter),
	"CreateKeySpace":           fmt.Sprintf("%s/{%s}/pd/pd/api/v2/keyspaces", proxyApiPrefix, sharedPoolIdParameter),
	"GetKeySpace":              fmt.Sprintf("%s/{%s}/pd/pd/api/v2/keyspaces/{%s}", proxyApiPrefix, sharedPoolIdParameter, keySpaceParameter),
	"UpdateKeySpaceState":      fmt.Sprintf("%s/{%s}/pd/pd/api/v2/keyspaces/{%s}/state", proxyApiPrefix, sharedPoolIdParameter, keySpaceParameter),
	"GetCluster":               fmt.Sprintf("%s/{%s}/pd/pd/api/v1/cluster", proxyApiPrefix, sharedPoolIdParameter),
	"SetKeySpaceResourceGroup": fmt.Sprintf("%s/{%s}/pd/resource-manager/api/v1/config/keyspace/service-limit/{%s}", proxyApiPrefix, sharedPoolIdParameter, keySpaceParameter),
	"SetKeySpaceRuVersion":     fmt.Sprintf("%s/{%s}/pd/resource-manager/api/v1/config/controller/ru-version/{%s}", proxyApiPrefix, sharedPoolIdParameter, keySpaceParameter),
	// DM
	"ListDMSources": fmt.Sprintf("%s/{%s}/dm/{%s}/api/v1/sources", proxyApiPrefix, sharedPoolIdParameter, dmClusterIdParameter),
	// TiBR
	"GetLightweightBackup":          fmt.Sprintf("%s/api/v1/x/backup", tibrApiPrefix),
	"ListLightweightBackups":        fmt.Sprintf("%s/api/v1/x/backups", tibrApiPrefix),
	"CreateLightweightBackup":       fmt.Sprintf("%s/api/v1/x/cluster/lightweight_backup", tibrApiPrefix),
	"CreateLightweightRestore":      tibrRestoreApiPattern,
	"GetLightweightRestore":         tibrRestoreApiPattern,
	"DeleteLightweightRestore":      tibrRestoreApiPattern,
	"CreatePackBackupTask":          fmt.Sprintf("%s/api/v1/x/task/pack_backup/{%s}", tibrApiPrefix, taskIdParameter),
	"CreateCopyPackedBackupTask":    fmt.Sprintf("%s/api/v1/x/task/copy_backup/{%s}", tibrApiPrefix, taskIdParameter),
	"CreateRestorePackedBackupTask": fmt.Sprintf("%s/api/v1/x/task/restore_packed/{%s}", tibrApiPrefix, taskIdParameter),
	"GetTask":                       fmt.Sprintf("%s/api/v1/x/task/{%s}", tibrApiPrefix, taskIdParameter),
	"DeleteTask":                    fmt.Sprintf("%s/api/v1/x/task/{%s}", tibrApiPrefix, taskIdParameter),
	"FlushWAL":                      fmt.Sprintf("%s/api/v1/x/cluster/flush_wal", tibrApiPrefix),
	"GetExoticBackupInfo":           fmt.Sprintf("%s/api/v1/x/exotic_backup", tibrApiPrefix),
	"GetBackupStatus":               fmt.Sprintf("%s/api/v1/x/backup/status", tibrApiPrefix),
	// Cluster Agent Ops
	"ClusterAgentOpsRestart":              fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}:restart", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"GetClusterTopology":                  fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/topology", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"DeleteCloudAdmin":                    fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/cloud_admin", clusterAgentApiPrefix, dataplaneNamespaceParameter, keySpaceParameter),
	"ListPods":                            fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/pods", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"GetComponentResourceMetrics":         fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/components:resource-metrics", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"GetComponentRawMetrics":              fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/components/{%s}/{%s}/metrics", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter, componentTypeParameter, podNameParameter),
	"GetComponentNodeMetrics":             fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}/components/{%s}/{%s}/node-metrics", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter, componentTypeParameter, podNameParameter),
	"MarkTiProxyPodUnhealthyLegacy":       fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}:mark_tiproxy_pod_unhealthy_legacy", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"ProbeTiProxyHealthAPI":               fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}:probe_tiproxy_health_api", clusterAgentApiPrefix, dataplaneNamespaceParameter, sharedPoolIdParameter),
	"ListPlanReplayerFiles":               fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}:list_plan_replayer_files", clusterAgentApiPrefix, dataplaneNamespaceParameter, logicalClusterIdParameter),
	"GeneratePlanReplayerFileDownloadURL": fmt.Sprintf("%s/{%s}/api/v1/clusters/{%s}:generate_plan_replayer_file_download_url", clusterAgentApiPrefix, dataplaneNamespaceParameter, logicalClusterIdParameter),
}

type clusterGatewayImpl struct {
	client *resty.Client
}

const (
	defaultTimeout = 10 * time.Second
)

func NewClusterGatewayImpl() Client {
	client := resty.New().
		SetTimeout(defaultTimeout).
		SetRetryWaitTime(time.Second).
		SetRetryCount(3).
		//nolint:gosec // skip G402
		SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})

	return &clusterGatewayImpl{
		client: client,
	}
}

func (c *clusterGatewayImpl) callTiBR(ctx context.Context, sharedPool *entity.SharedPool, method, apiPath string, setParam func(*resty.Request) *resty.Request) (*resty.Response, error) {
	return c.commonCallClusterProxy(ctx, sharedPool, method, apiPath, setParam, true)
}

func (c *clusterGatewayImpl) callClusterProxy(ctx context.Context, sharedPool *entity.SharedPool, method, apiPath string, setParam func(*resty.Request) *resty.Request) (*resty.Response, error) {
	return c.commonCallClusterProxy(ctx, sharedPool, method, apiPath, setParam, false)
}

func (c *clusterGatewayImpl) commonCallClusterProxy(ctx context.Context, sharedPool *entity.SharedPool, method, apiPath string, setParam func(*resty.Request) *resty.Request, retDirectly bool) (*resty.Response, error) {
	resp, err := utils.DoRequest(ctx, c.client, method, sharedPool.GetGatewayBaseUrl(), apiPath, func(req *resty.Request) {
		req.ForceContentType("application/json")
		if sharedPool.GetProvider().GetAuthToken() != "" {
			req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", sharedPool.GetProvider().GetAuthToken()))
		}
		setParam(req)
	})
	if err != nil {
		return nil, fmt.Errorf("call cluster gateway failed: %w", err)
	}

	if retDirectly {
		return resp, nil
	}

	if (resp.StatusCode() < http.StatusOK || resp.StatusCode() >= http.StatusMultipleChoices) &&
		// 409 means the resource already exists
		resp.StatusCode() != http.StatusConflict {
		return resp, fmt.Errorf("call cluster gateway failed: %s", resp.Body())
	}

	return resp, nil
}

func (c *clusterGatewayImpl) callClusterAgent(ctx context.Context,
	sharedPool *entity.SharedPool,
	method, apiPath string,
	setParam func(*resty.Request) *resty.Request,
) (*resty.Response, error) {
	return c.callClusterAgentOpts(ctx, sharedPool, method, apiPath, setParam, false)
}

func (c *clusterGatewayImpl) callClusterAgentNoRespLog(ctx context.Context,
	sharedPool *entity.SharedPool,
	method, apiPath string,
	setParam func(*resty.Request) *resty.Request,
) (*resty.Response, error) {
	return c.callClusterAgentOpts(ctx, sharedPool, method, apiPath, setParam, true)
}

func (c *clusterGatewayImpl) callClusterAgentOpts(ctx context.Context,
	sharedPool *entity.SharedPool,
	method, apiPath string,
	setParam func(*resty.Request) *resty.Request,
	noRespBodyLog bool,
) (*resty.Response, error) {
	doReq := utils.DoRequest
	if noRespBodyLog {
		doReq = utils.DoRequestNoRespLog
	}
	resp, err := doReq(ctx, c.client, method, sharedPool.GetGatewayBaseUrl(), apiPath, func(req *resty.Request) {
		req.ForceContentType("application/json")
		if sharedPool.GetProvider().GetAuthToken() != "" {
			req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", sharedPool.GetProvider().GetAuthToken()))
		}
		setParam(req)
	})
	if err != nil {
		return nil, fmt.Errorf("call cluster agent failed: %w", err)
	}
	return resp, nil
}

func IsNotFoundError(err error) bool {
	return errors.Is(err, ErrKeyspaceNotFound)
}
