package regionalserver

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/go-resty/resty/v2"
	"google.golang.org/grpc/codes"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/httputil"
)

const (
	apiPrefix                          = "/api/premium/v1"
	clusterIdParameter                 = "clusterId"
	tidbGroupIdPathParameter           = "tidbGroupId"
	privateLinkServiceIdPathParameter  = "privateLinkServiceId"
	sharedPoolIdPathParameter          = "sharedPoolId"
	lightweightRestoreIdPathParameter  = "lightweightRestoreId"
	classicRestoreIdPathParameter      = "classicRestoreId"
	manualBackupIdPathParameter        = "manualBackupId"
	manualBackupRestoreIdPathParameter = "manualBackupRestoreId"
	dataplaneManagerApiPrefix          = "/dataplane-manager/api/v1"
	encryptionKeyIdPathParameter       = "encryptionKeyId"
	dataplaneIdParameter               = "dataplaneId"
	vpcPeeringIdPathParameter          = "vpcPeeringId"
	trustedDataplaneIdParameter        = "trusted_dataplane_id"
	exportIdPathParameter              = "exportId"
)

var (
	operationMap = map[string]string{
		// Ping service
		// "Ping": fmt.Sprintf("%s/ping", apiPrefix),

		// tidbCluster service
		"CreateCluster": fmt.Sprintf("%s/clusters", apiPrefix),
		"GetCluster": fmt.Sprintf(
			"%s/clusters/{%s}",
			apiPrefix,
			clusterIdParameter,
		),
		"DeleteCluster": fmt.Sprintf(
			"%s/clusters/{%s}",
			apiPrefix,
			clusterIdParameter,
		),
		"UpdateCluster": fmt.Sprintf(
			"%s/clusters/{%s}",
			apiPrefix,
			clusterIdParameter,
		),
		"ListClusters": fmt.Sprintf("%s/clusters", apiPrefix),
		"GetProviderRole": fmt.Sprintf(
			"%s/clusters/{%s}/providerRole",
			apiPrefix,
			clusterIdParameter,
		),
		"SetAuditLogSetting": fmt.Sprintf(
			"%s/clusters/{%s}:setAuditLogSetting",
			apiPrefix,
			clusterIdParameter,
		),
		"ForceUpdateGatewayIdentity": fmt.Sprintf(
			"%s/clusters/{%s}:forceUpdateGatewayIdentity",
			apiPrefix,
			clusterIdParameter,
		),

		// TidbGroup service
		"CreateTidbGroup": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups",
			apiPrefix,
			clusterIdParameter,
		),
		"UpdateTidbGroup": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
		),
		"ScaleTiDBGroup": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}:scale",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
		),
		"GetTidbGroup": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
		),
		"DeleteTidbGroup": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
		),
		"CreatePrivateLinkService": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}/privateLinkServices",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
		),
		"UpdatePrivateLinkService": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}/privateLinkServices/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
			privateLinkServiceIdPathParameter,
		),
		"GetPrivateLinkService": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}/privateLinkServices/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
			privateLinkServiceIdPathParameter,
		),
		"DeletePrivateLinkService": fmt.Sprintf(
			"%s/clusters/{%s}/tidbGroups/{%s}/privateLinkServices/{%s}",
			apiPrefix,
			clusterIdParameter,
			tidbGroupIdPathParameter,
			privateLinkServiceIdPathParameter,
		),

		// BR Service
		"ListLightweightBackups": fmt.Sprintf(
			"%s/sharedPools/{%s}/lightweightBackups",
			apiPrefix,
			sharedPoolIdPathParameter),
		"CreateLightweightRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/lightweightRestores",
			apiPrefix,
			sharedPoolIdPathParameter),
		"GetLightweightRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/lightweightRestores/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter,
			lightweightRestoreIdPathParameter,
		),
		"GetLightweightRestorePostActions": fmt.Sprintf(
			"%s/sharedPools/{%s}/lightweightRestores/{%s}:getPostActions",
			apiPrefix,
			sharedPoolIdPathParameter,
			lightweightRestoreIdPathParameter,
		),
		"DeleteLightweightRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/lightweightRestores/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter,
			lightweightRestoreIdPathParameter,
		),
		"CreateClassicRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/classicRestores",
			apiPrefix,
			sharedPoolIdPathParameter,
		),
		"GetClassicRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/classicRestores/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter,
			classicRestoreIdPathParameter,
		),
		"DeleteClassicRestore": fmt.Sprintf(
			"%s/sharedPools/{%s}/classicRestores/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter,
			classicRestoreIdPathParameter,
		),
		"TestClassicBackupAccess": fmt.Sprintf(
			"%s/restores:testClassicBackupAccess",
			apiPrefix,
		),
		"GetBackupGcSetting": fmt.Sprintf(
			"%s/sharedPools/{%s}/backupGcSetting",
			apiPrefix,
			sharedPoolIdPathParameter,
		),
		"UpdateBackupGcSetting": fmt.Sprintf(
			"%s/sharedPools/{%s}/backupGcSetting",
			apiPrefix,
			sharedPoolIdPathParameter,
		),
		"UpdateAutoBackupMetering": fmt.Sprintf(
			"%s/clusters/{%s}:updateAutoBackupMetering",
			apiPrefix,
			clusterIdParameter,
		),
		"ReportAutoBackupMetering": fmt.Sprintf(
			"%s/clusters/{%s}:reportAutoBackupMetering",
			apiPrefix,
			clusterIdParameter,
		),

		// Manual Backup
		"CreateManualBackup": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups",
			apiPrefix,
			clusterIdParameter,
		),
		"GetManualBackup": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
		),
		"DeleteManualBackup": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
		),

		// Manual Backup Restore
		"CreateManualBackupRestore": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}:restore",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
		),
		"GetManualBackupRestore": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}/restores/{%s}",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
			manualBackupRestoreIdPathParameter,
		),
		"GetManualBackupRestorePostActions": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}/restores/{%s}:getPostActions",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
			manualBackupRestoreIdPathParameter,
		),
		"DeleteManualBackupRestore": fmt.Sprintf(
			"%s/clusters/{%s}/manualBackups/{%s}/restores/{%s}",
			apiPrefix,
			clusterIdParameter,
			manualBackupIdPathParameter,
			manualBackupRestoreIdPathParameter,
		),

		// cluster agent service
		"GetClusterAgentPublicKey": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/public_key",
			clusterIdParameter,
			sharedPoolIdPathParameter,
		),
		// GetClusterAgentPublicKeyViaSharedPool is used for System TiDB which routes via shared pool
		"GetClusterAgentPublicKeyViaSharedPool": fmt.Sprintf(
			"/dataplane/proxy/sharedPools/{%s}/cluster-agent/tidb{%s}/api/v1/public_key",
			sharedPoolIdPathParameter,
			sharedPoolIdPathParameter,
		),
		// Note: The operation key is "ResetPassword" (matching the agent-side RPC name),
		// but the client interface uses "SetPassword" to better reflect the semantics
		// that this API can both set initial password and reset existing password.
		"SetPassword": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:reset_password",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		// SetPasswordViaSharedPool is used for System TiDB which routes via shared pool
		"SetPasswordViaSharedPool": fmt.Sprintf(
			"/dataplane/proxy/sharedPools/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:reset_password",
			sharedPoolIdPathParameter,
			sharedPoolIdPathParameter,
			sharedPoolIdPathParameter,
		),
		"ExecuteClusterAgentCommand": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:executeCommand",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"UpdateAuditLogSettingViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:update_audit_log_setting",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"SetGlobalCloudStorageURIViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:set_global_cloud_storage_uri",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"CreateAuditLogFilterRuleViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:create_audit_log_filter_rule",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"DeleteAuditLogFilterRuleViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:delete_audit_log_filter_rule",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"UpdateAuditLogFilterRuleViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:update_audit_log_filter_rule",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"UpdateLogRedactionPolicyViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:update_log_redaction_policy",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),
		"GetLogRedactionPolicyViaAgent": fmt.Sprintf(
			"/dataplane/proxy/clusters/{%s}/cluster-agent/tidb{%s}/api/v1/clusters/{%s}:get_log_redaction_policy",
			clusterIdParameter,
			sharedPoolIdPathParameter,
			clusterIdParameter,
		),

		// shared pool management Service
		"CreateSharedPool": fmt.Sprintf(
			"%s/sharedPools",
			apiPrefix),
		"GetSharedPool": fmt.Sprintf(
			"%s/sharedPools/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter),
		"ValidateSharedPoolPause": fmt.Sprintf(
			"%s/sharedPools/{%s}:validatePause",
			apiPrefix,
			sharedPoolIdPathParameter),
		"PauseSharedPool": fmt.Sprintf(
			"%s/sharedPools/{%s}:pause",
			apiPrefix,
			sharedPoolIdPathParameter),
		"ResumeSharedPool": fmt.Sprintf(
			"%s/sharedPools/{%s}:resume",
			apiPrefix,
			sharedPoolIdPathParameter),
		"GetPauseResumeOperation": fmt.Sprintf(
			"%s/sharedPools/{%s}/pauseResumeOperations/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter,
			"operationId"),
		"DeleteSharedPool": fmt.Sprintf(
			"%s/sharedPools/{%s}",
			apiPrefix,
			sharedPoolIdPathParameter),
		"UpdateSharedPoolVcpusLimit": fmt.Sprintf(
			"%s/sharedPools/{%s}:updateVcpusLimit",
			apiPrefix,
			sharedPoolIdPathParameter),
		"GetSharedPoolVcpusUsage": fmt.Sprintf(
			"%s/sharedPools/{%s}:getVcpusUsage",
			apiPrefix,
			sharedPoolIdPathParameter),
		"GetSharedPoolScaleConstraints": fmt.Sprintf(
			"%s/sharedPools/{%s}:getScaleConstraints",
			apiPrefix,
			sharedPoolIdPathParameter),
		"ScaleSharedPool": fmt.Sprintf(
			"%s/sharedPools/{%s}:scale",
			apiPrefix,
			sharedPoolIdPathParameter),
		"ListSharedPools": fmt.Sprintf(
			"%s/sharedPools",
			apiPrefix),
		"ListNodeSpecs": fmt.Sprintf(
			"%s/nodeSpecs",
			apiPrefix),
		"GetSharedPoolProvisionConfiguration": fmt.Sprintf(
			"%s/sharedPoolProvisionConfiguration",
			apiPrefix),
		"EvaluateSharedPoolNetworkCapacity": fmt.Sprintf(
			"%s/sharedPools:evaluateNetworkCapacity",
			apiPrefix),
		"UpdateSharedPoolCloudResourceTags": fmt.Sprintf(
			"%s/sharedPools/{%s}:updateCloudResourceTags",
			apiPrefix,
			sharedPoolIdPathParameter),
		"SetKongAuditLogSetting": fmt.Sprintf(
			"%s/kongAuditLogSettings",
			dataplaneManagerApiPrefix),
		"CheckKms": fmt.Sprintf(
			"/dataplane/proxy/sharedPools/{%s}/check",
			sharedPoolIdPathParameter),
		"CreateEncryptionKey": fmt.Sprintf(
			"%s/dataplanes/{%s}/keys",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter),
		"GetEncryptionKey": fmt.Sprintf(
			"%s/dataplanes/{%s}/keys/{%s}",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
			encryptionKeyIdPathParameter),
		"AddEncryptionKeyTrustedDataplane": fmt.Sprintf(
			"%s/dataplanes/{%s}/keys/{%s}:addTrustedDataplane",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
			encryptionKeyIdPathParameter),
		"GetEncryptionKeyTrustedDataplane": fmt.Sprintf(
			"%s/dataplanes/{%s}/keys/{%s}:getTrustedDataplane",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
			encryptionKeyIdPathParameter),
		"GetDataPlane": fmt.Sprintf(
			"%s/dataplanes/{%s}",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
		),
		"ListVpcPeerings": fmt.Sprintf(
			"%s/dataplanes/{%s}/vpcPeerings",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
		),
		"GetVpcPeering": fmt.Sprintf(
			"%s/dataplanes/{%s}/vpcPeerings/{%s}",
			dataplaneManagerApiPrefix,
			dataplaneIdParameter,
			vpcPeeringIdPathParameter,
		),
		"CreateExport": fmt.Sprintf(
			"%s/clusters/{%s}/exports",
			apiPrefix,
			clusterIdParameter,
		),
		"GetExport": fmt.Sprintf(
			"%s/clusters/{%s}/exports/{%s}",
			apiPrefix,
			clusterIdParameter,
			exportIdPathParameter,
		),
		"CancelExport": fmt.Sprintf(
			"%s/clusters/{%s}/exports/{%s}:cancel",
			apiPrefix,
			clusterIdParameter,
			exportIdPathParameter,
		),
		"CleanExport": fmt.Sprintf(
			"%s/clusters/{%s}/exports/{%s}:cleanExport",
			apiPrefix,
			clusterIdParameter,
			exportIdPathParameter,
		),
	}
)

// defaultSetPasswordTimeout is the fallback timeout for SetPassword /
// SetPasswordViaSharedPool when RegionalServerConfig.SetPasswordTimeoutInSec is
// unset or non-positive. Chosen to accommodate the cluster-agent V1 (privileged
// TiDB) fallback, which may wait ~30s for the privileged TiDB to deactivate
// from another keyspace before it can run the ALTER USER statement.
const defaultSetPasswordTimeout = 60 * time.Second

type regionalServerCliImpl struct {
	client *resty.Client
	// setPasswordClient is a dedicated resty client with an extended timeout
	// used only for SetPassword / SetPasswordViaSharedPool. resty's client-level
	// timeout is a hard cap on http.Client.Timeout that a per-request context
	// cannot extend, so a separate client is required to lengthen the cap for
	// these specific operations without affecting all regional RPCs.
	setPasswordClient *resty.Client
}

func NewRegionalServerCli(cfg *config.Config) regionalserver.Client {
	client := resty.New().
		SetTimeout(time.Duration(cfg.RegionalServerConfig.RequestTimeoutInSec) * time.Second).
		SetRetryWaitTime(time.Duration(cfg.RegionalServerConfig.RetryWaitTimeoutInSec) * time.Second).
		SetRetryCount(cfg.RegionalServerConfig.RetryCount)

	setPasswordTimeout := time.Duration(cfg.RegionalServerConfig.SetPasswordTimeoutInSec) * time.Second
	if setPasswordTimeout <= 0 {
		setPasswordTimeout = defaultSetPasswordTimeout
	}
	// Retries intentionally omitted: the operation is expensive (privileged TiDB
	// activate/deactivate cycle). The upstream caller can decide whether to retry.
	setPasswordClient := resty.New().SetTimeout(setPasswordTimeout)

	svc := &regionalServerCliImpl{
		client:            client,
		setPasswordClient: setPasswordClient,
	}
	return svc
}

func (s *regionalServerCliImpl) callRegion(
	ctx context.Context,
	region *entity.Region,
	method, apiPath string,
	setParam func(*resty.Request) *resty.Request,
) (*resty.Response, bizerr.BizError) {
	return s.callRegionWithClient(ctx, s.client, region, method, apiPath, setParam)
}

func (s *regionalServerCliImpl) callRegionWithClient(
	ctx context.Context,
	client *resty.Client,
	region *entity.Region,
	method, apiPath string,
	setParam func(*resty.Request) *resty.Request,
) (*resty.Response, bizerr.BizError) {
	if region.State != globalcommonv1.Region_ACTIVE {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region is not active")
	}

	resp, err := httputil.DoRequest(ctx, client, method, region.ServiceAddress, apiPath, func(req *resty.Request) {
		req.ForceContentType("application/json")
		setParam(req)
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "call region service failed: %s", err.Error())
	}

	if (resp.StatusCode() < http.StatusOK || resp.StatusCode() >= http.StatusMultipleChoices) &&
		resp.StatusCode() != http.StatusConflict { // 409 means the resource already exists
		return resp, bizerr.New(bizerr.ErrCodeRegionalAPI, "call region service failed: %s", resp.String())
	}

	return resp, nil
}

func mapUnimplementedRuntimeMethod(resp *resty.Response, err bizerr.BizError) bizerr.BizError {
	if err == nil || resp == nil || resp.StatusCode() != http.StatusNotImplemented {
		return err
	}

	return bizerr.New(
		bizerr.ErrCode(codes.Unavailable),
		"regional runtime method is not available",
	)
}
