package o11y

import (
	"context"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-api/pkg/k8s"
)

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) (*ObservabilityCluster, error)
	DeleteO11yInfra(ctx context.Context, provider, region, o11yInfraID string) (*ObservabilityCluster, error)
	GetO11yInfra(ctx context.Context, provider, region, tenantID, projectID, clusterID string) ([]ClusterBaseInfo, error)
	InstallAgentWithHelm(manifestDirectoryPrefix, chartRelativePath, releaseName, namespace, k8sVersion string, kubeConfig, agentChartValues []byte) (opRes controllerutil.OperationResult, renderedManifest string, err error)
	CreateO11yInfraV2(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)
	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) (*ObservabilityCluster, error) {
	alertTempID := tidbAlertTempID
	if clusterID == infraClusterID {
		alertTempID = infraAlertTempID
	}
	return a.mgr.createAWSO11yInfra(ctx, agentAccessARN, agentManagerARN, provider, region, tenantID, projectID, clusterID, getBizTypeByEnv(a.env), alertTempID, getMonitorSpecIDByEnv(a.env), "", "0", mode, false)
}

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

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))
}

func (a *apiImpl) InstallAgentWithHelm(manifestDirectoryPrefix, chartRelativePath, releaseName, namespace, k8sVersion string, kubeConfig, agentChartValues []byte) (opRes controllerutil.OperationResult, renderedManifest string, err error) {
	kubeConfigFilename, err := k8s.WriteTempFile(kubeConfig)
	if err != nil {
		return controllerutil.OperationResultNone, "", errors.Annotate(err, "failed to write kubeConfig to file")
	}
	defer os.Remove(kubeConfigFilename)
	chartValuesFilename, err := k8s.WriteTempFile(agentChartValues)
	if err != nil {
		return controllerutil.OperationResultNone, "", errors.Annotate(err, "failed to write chartValues to file")
	}
	defer os.Remove(chartValuesFilename)

	var (
		helm    = k8s.NewHelm().SetDir(manifestDirectoryPrefix).WithKubeConfig(kubeConfigFilename)
		kubectl = k8s.NewKubectl().SetDir(manifestDirectoryPrefix).WithKubeConfig(kubeConfigFilename)
	)

	chartPath := filepath.Join(manifestDirectoryPrefix, chartRelativePath)

	opRes, out, err := helm.Apply(
		kubectl.WithNamespace(namespace),
		releaseName,
		chartPath,
		"--namespace", namespace,
		"--kube-version", k8sVersion,
		"--values", chartValuesFilename,
	)
	if err != nil {
		return controllerutil.OperationResultNone, out, errors.Annotatef(err, "failed to install agent helm release, output: %s", out)
	}
	return opRes, out, nil
}

// CreateO11yInfraV2 for serverless
func (a *apiImpl) CreateO11yInfraV2(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)
}

// ListO11yInfraV2 for serverless
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 serverless
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)
}

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

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 ""
	}
}
