package commontasks

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	"github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/o11y"
	"github.com/tidbcloud/observability/agent-control-server/pkg/o11y/api"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util"
)

type BackendTask struct {
	L                  logr.Logger
	Agent              client.Object
	O11yAPI            o11y.O11yInfraAPI
	PeeringNLB         string
	WithAgentAccessARN bool
	// AgentAccessARNKey is the annotation key to get agent access ARN (e.g., for GCP serviceAccount, Azure kubelet object ID)
	AgentAccessARNKey string
	// AgentManagerARNFunc is a function to get agent manager ARN from the agent spec (e.g., GCP project ID, Azure subscription ID)
	AgentManagerARNFunc func() string
}

func (t *BackendTask) Create(ctx context.Context, params o11y.Params, pause, couldSkip bool) deploy.Result {
	if couldSkip && t.couldSkipCreate(params) {
		t.L.Info("skip call o11y backend")
		return deploy.Done()
	}
	o11yCluster, err := t.O11yAPI.Create(params)
	if err != nil {
		return deploy.Panic(errors.Annotate(err, "failed to create o11y cluster"))
	}
	serviceDNSName, ok := t.resolveServiceDNSName(o11yCluster.MonitorInfo.DnsName)
	if !ok && !params.SkipEndpointServiceName() {
		return deploy.Panic(errors.New("o11y cluster is not ready"))
	}
	if t.PeeringNLB == "" && params.AgentAccessAccount() != "" && len(o11yCluster.MonitorInfo.EndpointServiceName) == 0 {
		return deploy.Panic(errors.New("o11y cluster is not ready"))
	}
	bucket, storageAccount, err := getLogStorageInfo(o11yCluster.StorageInfo)
	if err != nil {
		return deploy.Panic(err)
	}
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yID, o11yCluster.ID)
	if len(o11yCluster.MonitorInfo.EndpointServiceName) > 0 {
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yEndpointServiceName, o11yCluster.MonitorInfo.EndpointServiceName)
	}
	if serviceDNSName != "" {
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName, serviceDNSName)
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceOriginDNSName, serviceDNSName)
	}
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yVMAccountID, o11yCluster.MonitorInfo.VMAccountID)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yVMProjectID, o11yCluster.MonitorInfo.VMProjectID)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yLogBucketName, bucket)
	if storageAccount != "" {
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yLogStorageAccount, storageAccount)
	}
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yAgentRoleArn, o11yCluster.AgentInfo.RoleARN)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yClusterStatus, string(o11yCluster.Status))
	if isPausing(string(o11yCluster.Status)) && !pause {
		// Resume o11y cluster
		params = o11y.ResumeParams(ctx, params.Vendor(), params.Region(), o11yCluster.ID)
		_, err = t.O11yAPI.Resume(params)
		if err != nil {
			return deploy.Panic(fmt.Errorf("failed to resume o11y cluster: %w", err))
		}
	}
	if !isPausing(string(o11yCluster.Status)) && pause {
		// Pause o11y cluster
		params = o11y.PauseParams(ctx, params.Vendor(), params.Region(), o11yCluster.ID)
		_, err = t.O11yAPI.Pause(params)
		if err != nil {
			return deploy.Panic(fmt.Errorf("failed to pause o11y cluster: %w", err))
		}
	}
	return deploy.Done()
}

func (t *BackendTask) PostCreate(ctx context.Context, pause bool) deploy.Result {
	status := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yClusterStatus)
	if isPausing(status) && !pause {
		return deploy.Panic(fmt.Errorf("expect o11y cluster status to be resumed, but got %s", status))
	} else if !isPausing(status) && pause {
		return deploy.Panic(fmt.Errorf("expect o11y cluster status to be paused, but got %s", status))
	} else if !isRunning(status) && !pause {
		return deploy.Panic(fmt.Errorf("o11y cluster status is %s", status))
	}
	return deploy.Done()
}

func (t *BackendTask) Delete(ctx context.Context, vendor, region, tenantID, projectID, appID, bizType string) deploy.Result {
	o11yID := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yID)
	if o11yID != "" {
		agentAccessARN := t.getAgentAccessARN()
		agentManagerARN := t.getAgentManagerARN()
		_, err := t.O11yAPI.Delete(o11y.DeleteParamsV2(ctx, vendor, region, tenantID, projectID, appID, bizType, o11yID).
			WithAgentAccessARN(agentAccessARN).
			WithAgentManagerARN(agentManagerARN))
		return deploy.PanicOrDone(errors.Annotate(err, "failed to delete o11y cluster"))
	}
	o11yCluster, err := t.O11yAPI.List(o11y.GetParamsV2(ctx, vendor, region, tenantID, projectID, appID, bizType))
	if err != nil {
		return deploy.PanicOrDone(errors.Annotate(err, "failed to list o11y cluster"))
	}
	if len(o11yCluster) == 1 {
		agentAccessARN := t.getAgentAccessARN()
		agentManagerARN := t.getAgentManagerARN()
		_, err := t.O11yAPI.Delete(o11y.DeleteParamsV2(ctx, vendor, region, tenantID, projectID, appID, bizType, o11yCluster[0].ID).
			WithAgentAccessARN(agentAccessARN).
			WithAgentManagerARN(agentManagerARN))
		return deploy.PanicOrDone(errors.Annotate(err, "failed to delete o11y cluster"))
	}
	if len(o11yCluster) == 0 {
		// Nothing to delete on the o11y side, continue the delete pipeline.
		t.L.Error(errors.New("o11y returns 0 cluster(s)"), "skip deleting o11y cluster, continue the delete pipeline")
		return deploy.Done()
	}
	// More than one o11y cluster is ambiguous. Panic to halt the pipeline and block
	// reconcile in a retry loop until manual intervention.
	errMsg := fmt.Sprintf("o11y returns %d cluster(s), should be exactly one", len(o11yCluster))
	t.L.Error(errors.New(errMsg), "waiting for manual handling", "clusters", fmt.Sprintf("%+v", o11yCluster))
	return deploy.Panic(errors.New(errMsg))
}

func (t *BackendTask) couldSkipCreate(params o11y.Params) bool {
	if util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yID) != "" &&
		util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName) != "" &&
		util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yVMAccountID) != "" &&
		util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yVMProjectID) != "" &&
		util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yLogBucketName) != "" &&
		util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yAgentRoleArn) != "" {
		if t.PeeringNLB != "" {
			return true
		}
		if params.SkipEndpointServiceName() {
			return true
		}
		return params.AgentAccessAccount() != "" && util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yEndpointServiceName) != ""
	}
	return false
}

func (t *BackendTask) resolveServiceDNSName(backendDNSName *string) (string, bool) {
	if t.PeeringNLB != "" {
		return t.PeeringNLB, true
	}
	if backendDNSName == nil || len(*backendDNSName) == 0 {
		return "", false
	}
	return *backendDNSName, true
}

func (t *BackendTask) getAgentAccessARN() string {
	if !t.WithAgentAccessARN {
		return ""
	}
	if t.AgentAccessARNKey != "" {
		return util.GetAnnotation(t.Agent, t.AgentAccessARNKey)
	}
	// Default to Azure kubelet object ID for backward compatibility
	return util.GetAnnotation(t.Agent, consts.AnnotationKeyAKSKubeletObjectID)
}

func (t *BackendTask) getAgentManagerARN() string {
	if t.AgentManagerARNFunc != nil {
		return t.AgentManagerARNFunc()
	}
	return ""
}

func getLogStorageInfo(storages []api.StorageInfo) (string, string, error) {
	for _, storage := range storages {
		if storage.Category == api.LOGS {
			return storage.BucketName, storage.StorageAccount, nil
		}
	}
	return "", "", errors.New("no log storage found")
}

func isRunning(status string) bool {
	switch status {
	case string(api.CREATING), string(api.UPDATING), string(api.RESTARTING), string(api.ACTIVE):
		return true
	default:
		return false
	}
}

func isPausing(status string) bool {
	switch status {
	case string(api.PAUSING), string(api.TO_BE_PAUSED), string(api.PAUSED):
		return true
	default:
		return false
	}
}
