package o11y

import (
	"context"
	"os"
	"time"

	"github.com/go-logr/logr"
	ctrl "sigs.k8s.io/controller-runtime"

	pkgconfig "github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/metrics"
	api "github.com/tidbcloud/observability/agent-control-server/pkg/o11y/api"
)

type method string

const (
	createMethod method = "create"
	getMethod    method = "get"
	deleteMethod method = "delete"
	pauseMethod  method = "pause"
	resumeMethod method = "resume"

	auth0ClientID     = "O11Y_AUTH0_CLIENT_ID"
	auth0ClientSecret = "O11Y_AUTH0_CLIENT_SECRET"
)

type O11yInfraAPI interface {
	Create(Params) (*api.ObservabilityCluster, error)
	Delete(Params) (*api.ObservabilityCluster, error)
	List(Params) ([]api.ClusterBaseInfo, error)
	Pause(Params) (*api.ObservabilityCluster, error)
	Resume(Params) (*api.ObservabilityCluster, error)
	GetByID(Params) (*api.ObservabilityCluster, error)
}

type Params struct {
	ctx                context.Context
	provider           string
	region             string
	tenantID           string
	projectID          string
	clusterID          string
	o11yInfraID        string
	agentAccessARN     string
	agentManagerARN    string
	agentAccessAccount string
	agentNextGen       bool
	agentSharedRole    bool
	mode               api.ShareMode
	// ignore following fields when call V1 interface method
	bizType     string
	alertTempID string
	monitorSpec string
	muteAlert   bool
	// this field is only used for NextGen
	skipEndpointServiceName bool
	byocVPCPeeringID        string
	byocDataplaneVPCCIDR    string
}

func (p Params) Vendor() string {
	return p.provider
}

func (p Params) Region() string {
	return p.region
}

func (p Params) AgentAccessAccount() string {
	return p.agentAccessAccount
}

func (p Params) SkipEndpointServiceName() bool {
	return p.skipEndpointServiceName
}

func (p Params) BYOCVPCPeering() (string, string) {
	return p.byocVPCPeeringID, p.byocDataplaneVPCCIDR
}

func (p Params) WithSkipEndpointServiceName(skip bool) Params {
	p.skipEndpointServiceName = skip
	return p
}

func (p Params) WithAgentAccessARN(arn string) Params {
	p.agentAccessARN = arn
	return p
}

func (p Params) WithAgentManagerARN(arn string) Params {
	p.agentManagerARN = arn
	return p
}

func (p Params) WithBYOCVPCPeering(peeringID, dataplaneVPCCIDR string) Params {
	p.byocVPCPeeringID = peeringID
	p.byocDataplaneVPCCIDR = dataplaneVPCCIDR
	return p
}

func CreateParamsV3(ctx context.Context,
	provider, region,
	bizType, tenantID, projectID, clusterID,
	agentAccessAccount,
	alertTempID string,
) Params {
	return Params{
		ctx:                ctx,
		provider:           provider,
		region:             region,
		bizType:            bizType,
		tenantID:           tenantID,
		projectID:          projectID,
		clusterID:          clusterID,
		agentAccessAccount: agentAccessAccount,
		mode:               api.SHARED,
		monitorSpec:        "vmalert.default",
		alertTempID:        alertTempID,
	}
}

func CreateParamsNextGen(ctx context.Context,
	provider, region,
	bizType, tenantID, clusterID, projectID,
	agentAccessAccount,
	alertTempID string,
	skipEndpointServiceName bool,
) Params {
	return Params{
		ctx:                     ctx,
		provider:                provider,
		region:                  region,
		bizType:                 bizType,
		tenantID:                tenantID,
		clusterID:               clusterID,
		projectID:               projectID,
		agentAccessAccount:      agentAccessAccount,
		mode:                    api.SHARED,
		monitorSpec:             "vmalert.default",
		alertTempID:             alertTempID,
		agentNextGen:            true,
		skipEndpointServiceName: skipEndpointServiceName,
	}
}

func GetParamsV2(ctx context.Context, provider, region, tenantID, projectID, clusterID,
	bizType string,
) Params {
	return Params{
		ctx:       ctx,
		provider:  provider,
		region:    region,
		tenantID:  tenantID,
		projectID: projectID,
		clusterID: clusterID,
		bizType:   bizType,
	}
}

func DeleteParamsV2(ctx context.Context, provider, region, tenantID, projectID, clusterID, bizType, o11yInfraID string) Params {
	return Params{
		ctx:         ctx,
		provider:    provider,
		region:      region,
		tenantID:    tenantID,
		projectID:   projectID,
		clusterID:   clusterID,
		bizType:     bizType,
		o11yInfraID: o11yInfraID,
	}
}

func PauseParams(ctx context.Context, provider, region, o11yInfraID string) Params {
	return Params{
		ctx:         ctx,
		provider:    provider,
		region:      region,
		o11yInfraID: o11yInfraID,
	}
}

func ResumeParams(ctx context.Context, provider, region, o11yInfraID string) Params {
	return Params{
		ctx:         ctx,
		provider:    provider,
		region:      region,
		o11yInfraID: o11yInfraID,
	}
}

type O11yInfraAPIClient struct {
	client api.Api
}

var _ O11yInfraAPI = &O11yInfraAPIClient{}

// NewO11yInfraAPIClient initializes the o11y infra API client and return it when two required environment variables are set.
// Otherwise, it will return nil, which avoids the leakage of monitoring resources in the personal dev environment.
func NewO11yInfraAPIClient(config pkgconfig.ObservabilityInfra, logger logr.Logger) *O11yInfraAPIClient {
	_, ok1 := os.LookupEnv(auth0ClientID)
	_, ok2 := os.LookupEnv(auth0ClientSecret)

	if ok1 && ok2 {
		client, err := api.InitApi(
			config.Auth0GetTokenURL,
			config.Auth0ClientID,
			config.Auth0ClientSecret,
			config.Auth0Audience,
			config.OpenAPIAddress,
			config.CreateO11yInfraPath,
			config.DeleteO11yInfraPath,
			config.GetO11yInfraPath,
			config.PauseO11yInfraPath,
			config.ResumeO11yInfraPath,
			config.Env,
		)
		if err != nil {
			logger.Error(err, "unable to init o11y infra api client")
			os.Exit(1)
		}
		return &O11yInfraAPIClient{
			client: client,
		}
	}

	logger.Info("o11y auth config is not set correctly, will disable o11y agents")
	return nil
}

func (api *O11yInfraAPIClient) recordRequest(err error, p Params, m method, now time.Time) {
	labelValues := []string{p.tenantID, p.o11yInfraID, string(m)}
	if err != nil {
		labelValues = append(labelValues, "failed")
	} else {
		labelValues = append(labelValues, "success")
	}
	metrics.IncO11yApiRequestsCounter(labelValues)
	metrics.ObserveO11yApiRequestsDuration(labelValues, time.Since(now).Seconds())
}

func (api *O11yInfraAPIClient) Create(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, createMethod, time.Now()) }()
	return api.client.CreateAwsO11yInfra(p.ctx, p.agentAccessARN, p.agentManagerARN, p.provider, p.region, p.tenantID, p.projectID, p.clusterID, p.mode, p.agentSharedRole)
}

func (api *O11yInfraAPIClient) Delete(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, deleteMethod, time.Now()) }()
	return api.client.DeleteO11yInfra(p.ctx, p.provider, p.region, p.tenantID, p.projectID, p.clusterID, p.bizType, p.o11yInfraID)
}

// [Deprecated]
func (api *O11yInfraAPIClient) Get(p Params) ([]api.ClusterBaseInfo, error) {
	return api.List(p)
}

func (api *O11yInfraAPIClient) List(p Params) (_ []api.ClusterBaseInfo, err error) {
	defer func() { api.recordRequest(err, p, getMethod, time.Now()) }()
	return api.client.GetO11yInfra(p.ctx, p.provider, p.region, p.tenantID, p.projectID, p.clusterID)
}

func (api *O11yInfraAPIClient) GetByID(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, getMethod, time.Now()) }()
	return api.client.GetO11yInfraByID(p.ctx, p.provider, p.region, p.o11yInfraID)
}

func (api *O11yInfraAPIClient) Pause(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, pauseMethod, time.Now()) }()
	return api.client.PauseO11yInfra(p.ctx, p.provider, p.region, p.o11yInfraID)
}

func (api *O11yInfraAPIClient) Resume(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, resumeMethod, time.Now()) }()
	return api.client.ResumeO11yInfra(p.ctx, p.provider, p.region, p.o11yInfraID)
}

type O11yInfraAPIClientV2 struct {
	*O11yInfraAPIClient
}

func NewO11yInfraAPIClientV2(config pkgconfig.ObservabilityInfra) *O11yInfraAPIClientV2 {
	return &O11yInfraAPIClientV2{
		O11yInfraAPIClient: NewO11yInfraAPIClient(config, ctrl.Log),
	}
}

func (api *O11yInfraAPIClientV2) Create(p Params) (_ *api.ObservabilityCluster, err error) {
	return api.client.CreateO11yInfraV2(p.ctx,
		p.agentAccessARN,
		p.agentManagerARN,
		p.agentAccessAccount,
		p.provider,
		p.region,
		p.tenantID,
		p.projectID,
		p.clusterID,
		p.bizType,
		p.alertTempID,
		p.monitorSpec,
		p.mode,
		p.muteAlert,
		p.agentSharedRole,
		p.agentNextGen,
		p.byocVPCPeeringID,
		p.byocDataplaneVPCCIDR,
	)
}

func (api *O11yInfraAPIClientV2) List(p Params) (_ []api.ClusterBaseInfo, err error) {
	return api.client.ListO11yInfraV2(p.ctx,
		p.provider,
		p.region,
		p.tenantID,
		p.projectID,
		p.clusterID,
		p.bizType,
	)
}

func (api *O11yInfraAPIClientV2) Delete(p Params) (_ *api.ObservabilityCluster, err error) {
	defer func() { api.recordRequest(err, p, deleteMethod, time.Now()) }()
	return api.client.DeleteO11yInfraV2(p.ctx, p.provider, p.region, p.tenantID, p.projectID, p.clusterID, p.bizType, p.o11yInfraID, p.agentAccessARN, p.agentManagerARN)
}
