package common

import (
	"context"
	"fmt"
	"time"

	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/agentcontrolutils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/approute/task"
	agentcontrolapi "github.com/tidbcloud/observability/agent-control-server/pkg/agentcontrol/api"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

type O11y struct {
	*task.BaseTask
}

var _ deploy.Task = &O11y{}

func (t *O11y) Name() string {
	return "O11y"
}

func NewO11y(ctx *task.Context) *O11y {
	return &O11y{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *O11y) Create(ctx context.Context) deploy.Result {
	if !t.Config.IsEnableO11yService() {
		t.Logger.Info("o11y service is disabled, skip creating o11y route")
		return deploy.Done()
	}
	t.BeginAt = util.Pointer(time.Now())
	cluster, err := getClusterFromAPPRoute(t.APPRoute)
	if err != nil {
		return deploy.Panic(err)
	}

	clusterID := t.APPRoute.Name
	labels := t.APPRoute.GetLabels()
	if len(labels) == 0 {
		return deploy.Panic(fmt.Errorf("approute %s/%s has no labels, cannot create o11y", t.APPRoute.Namespace, t.APPRoute.Name))
	}
	tenantID, hasTenant := labels[v1alpha1.TagTenantKey]
	projectID, hasProject := labels[v1alpha1.TagProjectKey]
	if !hasTenant || !hasProject {
		return deploy.Panic(fmt.Errorf("approute missing required labels: tenant/project"))
	}
	if config.FeatureEnabled(t.Config.FeatureGates, config.O11yNewAgentController, tenantID, projectID) {
		// Check if O11yRoute already exists.
		existingRoute := &v1alpha1.O11yRoute{}
		err := t.Local.Get(ctx, types.NamespacedName{
			Name:      t.APPRoute.Name,
			Namespace: t.APPRoute.Namespace},
			existingRoute)
		if err != nil {
			if !apierrors.IsNotFound(err) {
				return deploy.Panic(fmt.Errorf("failed to get o11y route: %w", err))
			}
			// O11yRoute doesn't exist, this means that this is a new cluster,
			// so redirected to the new path.
			dataplaneCfg, err := agentcontrolutils.BuildDataplaneConfig(ctx, t.Local, t.APPBox)
			if err != nil {
				return deploy.Panic(fmt.Errorf("failed to build dataplane config: %w", err))
			}
			_, err = t.AgentControlAPI.CreateTiDBAgent(ctx, &agentcontrolapi.CreateTiDBAgentRequest{
				TenantID:  tenantID,
				ProjectID: projectID,
				ClusterID: clusterID,
				Dataplane: *dataplaneCfg,
				TiDB: agentcontrolapi.TiDBClusterConfig{
					Version:  cluster.ClusterVersion.ClusterVersion,
					Topology: agentcontrolutils.BuildTiDBClusterTopology(cluster),
				},
			})
			if err != nil {
				return deploy.Panic(fmt.Errorf("failed to create o11y agent: %w", err))
			}
			return deploy.Done()
		}
	}

	o11yroute := &v1alpha1.O11yRoute{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.APPRoute.Name,
			Namespace: t.APPRoute.Namespace,
			// use APPRoute labels for O11yRoute
			Labels: t.APPRoute.GetLabels(),
		},
		Spec: v1alpha1.O11yRouteSpec{
			ApplicationID: t.APPRoute.Name,
			Agent: &v1alpha1.AgentSpec{
				TargetClusterVersion:  cluster.ClusterVersion.ClusterVersion,
				TargetClusterTopology: buildClusterTopologyFromCluster(cluster),
			},
			VPCRef: v1alpha1.Reference{
				Name:      t.APPBox.Spec.VPCRef.Name,
				Namespace: t.APPBox.Spec.VPCRef.Namespace,
			},
			APPBoxRef: v1alpha1.Reference{
				Name:      t.APPBox.Name,
				Namespace: t.APPBox.Namespace,
			},
			APPRouteRef: &v1alpha1.Reference{
				Name:      t.APPRoute.Name,
				Namespace: t.APPRoute.Namespace,
			},
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, o11yroute, func() error {
		return nil
	})

	return deploy.ErrorOrDone(err)
}

func (t *O11y) Delete(ctx context.Context) deploy.Result {
	o11yRoute := &v1alpha1.O11yRoute{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.APPRoute.Name,
			Namespace: t.APPRoute.Namespace,
		},
	}

	labels := t.APPRoute.GetLabels()
	if len(labels) > 0 {
		tenantID, hasTenant := labels[v1alpha1.TagTenantKey]
		projectID, hasProject := labels[v1alpha1.TagProjectKey]
		if hasTenant && hasProject {
			clusterID := t.APPRoute.Name
			if config.FeatureEnabled(t.Config.FeatureGates, config.O11yNewAgentController, tenantID, projectID) {
				if _, err := t.AgentControlAPI.DeleteTiDBAgent(ctx, &agentcontrolapi.DeleteTiDBAgentRequest{ClusterID: clusterID}); err != nil {
					return deploy.Panic(fmt.Errorf("failed to delete o11y agent: %w", err))
				}
			}
		}
	}

	// Just delete the O11yroute directly, and don't wait for the O11route to be deleted truly,
	// because the o11yroute controller will wait for the APPRoute to be deleted before deleting the O11yRoute.
	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, o11yRoute))
}

func buildClusterTopologyFromCluster(cluster *regionalpb.Cluster) *v1alpha1.ClusterTopology {
	topo := &v1alpha1.ClusterTopology{}
	for _, c := range cluster.ComponentNodeSpecs {
		switch c.ComponentType {
		case regionalpb.ComponentType_TIDB:
			topo.TiDB = v1alpha1.TiDB{
				Replicas: int(c.Count),
			}
		case regionalpb.ComponentType_PD:
			topo.PD = v1alpha1.PD{
				Replicas: int(c.Count),
			}
		case regionalpb.ComponentType_TIKV:
			topo.TiKV = v1alpha1.TiKV{
				Replicas: int(c.Count),
			}
		case regionalpb.ComponentType_TIFLASH:
			topo.TiFlash = &v1alpha1.TiFlash{
				Replicas: int(c.Count),
			}
		}
	}
	return topo
}
