package branch

import (
	"fmt"
	"strconv"
	"time"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/aws-shared-provider/pkg/o11y"
	"github.com/tidbcloud/aws-shared-provider/pkg/o11y/metric_agent"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/labels"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	reconcileerr "github.com/tidbcloud/infra-api/controllers/common/error"
	infraconsts "github.com/tidbcloud/infra-api/pkg/consts"
	o11ymetric "github.com/tidbcloud/infra-api/pkg/o11y/metric_agent"
	infrao11y "github.com/tidbcloud/observability/agent-control-server/pkg/o11y/api"
)

const (
	defaultWaitInterval = 2 * time.Second
)

func (r *reconcileContext) getCloudProvider() string {
	if r.ProviderType == types.ProviderTypeAliCloudFreeTier {
		return metric_agent.AliCloud
	}
	return metric_agent.AWSFreeTier
}

// Metric agent is the replacement of db-monitor in TiDB cluster.

func (r *reconcileContext) ensureO11yCluster() (errResult error) {
	r.Logger.Info("ensure branch o11y cluster")
	if r.isO11yClusterAlready() {
		r.Logger.Info("o11y cluster is ready, skip to ensure")
		return nil
	}

	o11yCluster, err := r.createO11yInfraCluster()
	if err != nil {
		return err
	}

	err = r.ensureO11yRemoteWrite(o11yCluster)
	if err != nil {
		return err
	}
	r.Logger.Info("ensure o11y cluster successful")
	return
}

func (r *reconcileContext) createO11yInfraCluster() (*infrao11y.ObservabilityCluster, error) {
	// Create o11y infra via autonomous global openapi.
	tenantID, projectID, branchID, err := getIDsFromBranchCRD(r.branch)
	if err != nil {
		return nil, errors.Annotate(err, "failed to get tenantID, projectID, branchID from branch CRD")
	}

	r.Logger.Info("create o11y cluster", "tenantID", tenantID, "projectID", projectID,
		"branchID", branchID)
	o11yCluster, err := r.O11yApi.CreateO11yInfraV2Serverless(
		infrao11y.WithKubernetesID(r.ctx, labels.GetKubeClusterFromLabels(r.branch.Labels)), r.O11yAgentAccessArn, o11y.AgentManagerARN, r.getCloudProvider(),
		r.branch.Labels[cloudv1.RegionLabel], tenantID, projectID, branchID,
		generateO11yBizType(o11y.DevTier, r.O11yEnv), generateAlterTempID(r.O11yEnv), r.O11yGlobalClusterID,
		strconv.FormatUint(uint64(r.keyspaceID), 10), infrao11y.SHARED, true /*r.serverlessEnabled()*/)

	if err != nil {
		return o11yCluster, errors.Annotate(err, "failed to create o11y infra cluster")
	}

	if o11yCluster.Status == infrao11y.CREATING {
		return o11yCluster, errors.Errorf("o11y cluster is %s, not ready, please try again later", o11yCluster.Status)
	}

	return o11yCluster, nil
}

func (r *reconcileContext) ensureO11yRemoteWrite(o11yCluster *infrao11y.ObservabilityCluster) error {
	clusterRemoteWrite := o11ymetric.NewWriteURLBuilder(*o11yCluster.MonitorInfo.DnsName).ForAWS().
		BuildRemoteWrite(o11yCluster.ID)

	addClusterRemoteWriteToBranchAnn(r.branch,
		clusterRemoteWrite, o11yCluster.ID, r.O11yConfig.Version)

	if err := r.InfraClient.Update(r.ctx, r.branch); err != nil {
		return errors.Annotate(err, "failed add o11y cluster remote write to branch CRD annotations")
	}

	return nil
}

func (r *reconcileContext) isO11yClusterAlready() bool {
	// get o11y cluster id
	if _, ok := r.branch.Annotations[infraconsts.O11yIDAnnotationKey]; !ok {
		r.Logger.Info("failed to get o11y id in branch CRD annotation")
		return false
	}

	// get o11y cluster remote write
	if _, ok := r.branch.Annotations[o11y.O11yClusterRemoteWriteAnnotationKey]; !ok {
		r.Logger.Info("failed to get o11y cluster remote write in branch CRD annotation")
		return false
	}
	return true
}

// deleteO11yCluster delete o11y cluster
func (r *reconcileContext) deleteO11yCluster() error {
	r.Logger.Info("delete o11y cluster")
	// delete o11y cluster
	if err := r.deleteO11yInfraCluster(); err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to delete o11y infra cluster"))
	}
	r.Logger.Info("delete o11y cluster successful")
	return nil
}

func (r *reconcileContext) deleteO11yInfraCluster() error {
	// Else o11yID is empty, try to fetch o11y id from api.
	tenantID, projectID, branchID, err := getIDsFromBranchCRD(r.branch)
	if err != nil {
		return errors.Annotate(err, "failed to get tenantID, projectID, branchID from branch CRD")
	}

	var err1 error
	ctx := infrao11y.WithKubernetesID(r.ctx, labels.GetKubeClusterFromLabels(r.branch.Labels))
	o11yID, ok := r.branch.Annotations[infraconsts.O11yIDAnnotationKey]
	if ok && o11yID != "" {
		// If o11yID annotation is not empty, delete o11y infra.
		if _, err := r.O11yApi.DeleteO11yInfra(ctx, r.getCloudProvider(),
			r.branch.Labels[cloudv1.RegionLabel], tenantID, projectID, branchID, generateO11yBizType(o11y.DevTier, r.O11yEnv), o11yID); err != nil {
			err1 = errors.Annotate(err, "failed to delete o11y infra cluster with o11y openapi")
		}
	} else {
		//nolint:lll // ignore
		o11yInfra, err := r.O11yApi.ListO11yInfraV2(
			ctx, r.getCloudProvider(), r.branch.Labels[cloudv1.RegionLabel], tenantID, projectID, branchID, generateO11yBizType(o11y.DevTier, r.O11yEnv))
		switch {
		case err != nil:
			// Retry if api returns non 200 error.
			err1 = errors.Annotate(err, "failed to get o11y infra cluster with o11y openapi")
		case len(o11yInfra) == 1:
			// Only if exactly one o11y infra is found, delete o11y infra.
			//nolint:lll // ignore
			if _, err := r.O11yApi.DeleteO11yInfra(ctx, metric_agent.AWSFreeTier, r.branch.Labels[cloudv1.RegionLabel], tenantID, projectID, branchID, generateO11yBizType(o11y.DevTier, r.O11yEnv), o11yInfra[0].ID); err != nil {
				err1 = errors.Annotate(err, "failed to delete o11y infra cluster with o11y openapi")
			}
		default:
			// If err is nil and len(o11yInfra) is not 1, do nothing but log error.
			r.Logger.Info(
				"GetO11yInfra returns zero or more than one o11y infra cluster, should be exactly one" +
					" This could means that the o11y infra cluster is already deleted.")
		}
	}

	return err1
}

func generateO11yBizType(tierType, env string) string { //nolint:unparam // by design
	return fmt.Sprintf("%s-%s", env, tierType)
}

func generateAlterTempID(env string) string {
	if env == "prod" {
		return o11y.ClusterAlterTempID
	}
	return fmt.Sprintf("%s-%s", o11y.ClusterAlterTempID, "dev")
}

// getIDsFromBranchCRD tenantID fallbacks to "unknown", projectID fallbacks to "network.name"
// clusterID fallbacks to "cluster.Name"
func getIDsFromBranchCRD(b *cloudv1.Branch) (tenantID, projectID, branchID string, err error) {
	metadata, err := utils.GetBranchMetadata(b)
	if err != nil {
		return "", "", "", errors.Annotate(err, "failed to get branch metadata")
	}

	return strconv.FormatUint(metadata.TenantID, 10), strconv.FormatUint(metadata.ProjectID, 10), metadata.ID, nil
}

func addClusterRemoteWriteToBranchAnn(c *cloudv1.Branch, remoteWrite, o11yID string, o11yVersion int) {
	if c.Annotations == nil {
		c.Annotations = make(map[string]string)
	}
	c.Annotations[o11y.O11yClusterRemoteWriteAnnotationKey] = remoteWrite
	c.Annotations[infraconsts.O11yIDAnnotationKey] = o11yID
	c.Annotations[infraconsts.O11yVersionAnnotationKey] = strconv.Itoa(o11yVersion)
}
