package api

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

	"github.com/pingcap/errors"
)

const (
	auth0ClientIDEenKey  = "O11Y_AUTH0_CLIENT_ID"
	auth0ClientSecretEnv = "O11Y_AUTH0_CLIENT_SECRET"
)

var apiTimeout = 10 * time.Second

type Api interface {
	// CreateAwsO11yInfra is idempotent, if the cluster with the tenant/project/cluster ID already exists,
	// it will return the existed cluster info.
	CreateAwsO11yInfra(ctx context.Context, agentAccessARN, agentManagerARN, provider, region, tenantID, projectID, clusterID string, mode ShareMode, sharedRole bool) (*ObservabilityCluster, error)
	DeleteO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID string) (*ObservabilityCluster, error)
	DeleteO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID, agentAccessARN, agentManagerARN string) (*ObservabilityCluster, error)
	GetO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID string) ([]ClusterBaseInfo, error)
	CreateO11yInfraV2(ctx context.Context, agentAccessARN, agentManagerARN, agentAccessAccount, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, monitorSpec string, mode ShareMode, muteAlert, sharedRole, agentNextGen bool, byocVPCPeeringID, byocDataplaneVPCCIDR string) (*ObservabilityCluster, error)
	CreateO11yInfraV2Serverless(ctx context.Context, agentAccessARN, agentManagerARN, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, parentClusterID, keySpaceID string, mode ShareMode, muteAlert bool) (*ObservabilityCluster, error)
	ListO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType string) ([]ClusterBaseInfo, error)
	GetO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType string) (*ObservabilityCluster, error)
	UpdateO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, o11yInfraID, bizType string, opts UpdateO11yInfraOptions) (*ObservabilityCluster, error)
	PauseO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error)
	ResumeO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error)
	GetO11yInfraByID(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error)
}

type apiImpl struct {
	mgr *clientManager
	env string
}

// InitApi initializes the o11y client. It will override auth0 client id and client secret if they are set in env.
func InitApi(auth0GetTokenURL, clientID, clientSecret, audience, o11yOpenAPIAddress, createO11yInfraPath, deleteO11yInfraPath, getO11yInfraPath, pauseO11yInfraPath, resumeO11yInfraPath, env string) (Api, error) {
	clientIDEnv := os.Getenv(auth0ClientIDEenKey)
	if clientIDEnv != "" {
		clientID = clientIDEnv
	}
	clientSecretEnv := os.Getenv(auth0ClientSecretEnv)
	if clientSecretEnv != "" {
		clientSecret = clientSecretEnv
	}
	if clientID == "" || clientSecret == "" {
		return nil, errors.New("failed to init o11y client, empty auth0 client ID or client secret")
	}

	mgr := &clientManager{
		auth0GetTokenURL: auth0GetTokenURL,
		auth0RequestBody: Auth0RequestBody{
			Audience:     audience,
			ClientID:     clientID,
			ClientSecret: clientSecret,
			GrantType:    grantType,
		},
		o11yOpenAPIAddress:  o11yOpenAPIAddress,
		createO11yInfraPath: createO11yInfraPath,
		deleteO11yInfraPath: deleteO11yInfraPath,
		getO11yInfraPath:    getO11yInfraPath,
		pauseO11yInfraPath:  pauseO11yInfraPath,
		resumeO11yInfraPath: resumeO11yInfraPath,
		Client:              &http.Client{Timeout: apiTimeout},
	}

	if err := mgr.refreshAccessToken(context.Background()); err != nil {
		return nil, errors.Annotate(err, "failed to init o11y client")
	}

	return &apiImpl{mgr: mgr, env: env}, nil
}

// CreateAwsO11yInfra is idempotent.
func (a *apiImpl) CreateAwsO11yInfra(ctx context.Context, agentAccessARN, agentManagerARN, provider, region, tenantID, projectID, clusterID string, mode ShareMode, sharedRole bool) (*ObservabilityCluster, error) {
	alertTempID := getAlertTmplByClusterAndEnv(clusterID, a.env)
	return a.mgr.createAWSO11yInfra(ctx, agentAccessARN, agentManagerARN, "", provider, region, tenantID, projectID, clusterID, getBizTypeByEnv(a.env), alertTempID, getMonitorSpecIDByEnv(a.env), "", "", mode, false, sharedRole, false, "", "")
}

// DeleteO11yInfra is idempotent.
func (a *apiImpl) DeleteO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID string) (*ObservabilityCluster, error) {
	return a.mgr.deleteO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID, "", "")
}

// DeleteO11yInfraV2 is idempotent. It also sends agentAccessARN and agentManagerARN for backend authorization cleanup.
func (a *apiImpl) DeleteO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID, agentAccessARN, agentManagerARN string) (*ObservabilityCluster, error) {
	return a.mgr.deleteO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID, agentAccessARN, agentManagerARN)
}

func (a *apiImpl) GetO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID string) ([]ClusterBaseInfo, error) {
	return a.mgr.listO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, getBizTypeByEnv(a.env))
}

// CreateO11yInfraV2 for agent controller
func (a *apiImpl) CreateO11yInfraV2(ctx context.Context, agentAccessARN, agentManagerARN, agentAccessAccount, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, monitorSpec string, mode ShareMode, muteAlert, sharedRole, agentNextGen bool, byocVPCPeeringID, byocDataplaneVPCCIDR string) (*ObservabilityCluster, error) {
	return a.mgr.createAWSO11yInfra(
		ctx, agentAccessARN, agentManagerARN, agentAccessAccount, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, monitorSpec, "", "", mode, muteAlert, sharedRole, agentNextGen, byocVPCPeeringID, byocDataplaneVPCCIDR)
}

// CreateO11yInfraV2Serverless for serverless
func (a *apiImpl) CreateO11yInfraV2Serverless(ctx context.Context, agentAccessARN, agentManagerARN, provider, region, tenantID, projectID, clusterID, bizType, alertTempID, parentClusterID, keySpaceID string, mode ShareMode, muteAlert bool) (*ObservabilityCluster, error) {
	return a.mgr.createAWSO11yInfra(
		ctx, agentAccessARN, agentManagerARN, "", provider, region, tenantID, projectID, clusterID, bizType, alertTempID, "", parentClusterID, keySpaceID, mode, muteAlert, false, false, "", "")
}

// ListO11yInfraV2 for dev-tier
func (a *apiImpl) ListO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType string) ([]ClusterBaseInfo, error) {
	return a.mgr.listO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, bizType)
}

// GetO11yInfraV2 for dev-tier
func (a *apiImpl) GetO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType string) (*ObservabilityCluster, error) {
	clusters, err := a.mgr.listO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, bizType)
	if err != nil {
		return nil, err
	}

	cluster := ClusterBaseInfo{ID: ""}
	for _, c := range clusters {
		if c.ApplicationID == clusterID {
			cluster = c
			break
		}
	}

	if cluster.ID == "" {
		return nil, errors.Errorf("failed to get o11y infra, not found error")
	}

	return a.mgr.getO11yInfra(ctx, provider, region, cluster.ID)
}

// PauseO11yInfra
func (a *apiImpl) PauseO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	return a.mgr.pauseO11yInfra(ctx, provider, region, o11yInfraID)
}

// ResumeO11yInfra
func (a *apiImpl) ResumeO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	return a.mgr.resumeO11yInfra(ctx, provider, region, o11yInfraID)
}

// UpdateO11yInfra updates the o11y infra with the given optional fields.
func (a *apiImpl) UpdateO11yInfraV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, o11yInfraID, bizType string, opts UpdateO11yInfraOptions) (*ObservabilityCluster, error) {
	return a.mgr.updateO11yInfra(ctx, provider, region, tenantID, projectID, clusterID, o11yInfraID, bizType, opts)
}

// GetO11yInfraByID
func (a *apiImpl) GetO11yInfraByID(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error) {
	return a.mgr.getO11yInfra(ctx, provider, region, o11yInfraID)
}

func getAlertTmplByClusterAndEnv(clusterID, env string) string {
	alertTempID := tidbAlertTempID
	if clusterID == infraClusterID {
		alertTempID = infraAlertTempID
	}
	if env != "prod" {
		// we use the same alert template for dev and staging
		alertTempID = fmt.Sprintf("%s-dev", alertTempID)
	}
	return alertTempID
}

func getBizTypeByEnv(env string) string {
	switch env {
	case "dev":
		return "dev-tidbcloud"
	case "staging":
		return "staging-tidbcloud"
	default:
		return bizTypeValue
	}
}

func getMonitorSpecIDByEnv(env string) string {
	switch env {
	case "dev", "staging":
		return "spec.for.test"
	default:
		return ""
	}
}
