package action

import (
	"context"
	"fmt"
	"path"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/cert"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var tidbDiagnosisTemplates = []string{
	"tidb_diagnosis_configmap",
	"tidb_diagnosis_deployment",
	"tidb_diagnosis_svc",
}

type applyTiDBDiagnosisAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	templateVersionSvc  *templateversion.Service
	imageConfig         *config.ImageConfig
	clusterConfigClient clusterconfig.Client
}

const ApplyTiDBDiagnoseActionName = "apply-tidb-diagnosis"

func NewApplyTiDBDiagnosisAction(
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	clusterConfigClient clusterconfig.Client,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
) actions.Action {
	return &applyTiDBDiagnosisAction{
		sharedPoolRepo:      sharedPoolRepo,
		dataplaneManager:    dataplaneManager,
		clusterConfigClient: clusterConfigClient,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
	}
}

func (a *applyTiDBDiagnosisAction) Name() string {
	return ApplyTiDBDiagnoseActionName
}

func (a *applyTiDBDiagnosisAction) Invoke(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply tidb diagnosis action")

	sharedPool, remoteCli, compId, tidbName, err := a.getContextInfos(args)
	if err != nil {
		tilog.Error(ctx, "failed to get apply tidb diagnosis context infos", zap.Error(err))
		return err
	}

	tidbWorkerComp := findComponentByTypeForPrivileged(sharedPool.GetComponentGroups(), apiv1.ComponentType_TIDB_WORKER)
	if tidbWorkerComp == nil || tidbWorkerComp.GetNodeQuota().GetNodeCount() == 0 {
		tilog.Info(ctx, "skip apply tidb diagnosis action because TIDB_WORKER node_count is 0",
			zap.String("sharedPoolId", sharedPool.GetId()))
		return nil
	}

	_, configuration, err := a.clusterConfigClient.GetComponentVersionAndConfig(
		ctx, sharedPool, &entity.Component{
			Id:            sharedPool.GetDefaultComponenId(),
			ComponentType: apiv1.ComponentType_TIDB_WORKER,
		}) // same version/image as TiDB Worker
	if err != nil {
		tilog.Error(ctx, "failed to get tidb cluster version and config", zap.Error(err))
		return err
	}
	if a.imageConfig.ImagePullPolicy == "" {
		// set default image pull policy if not set
		a.imageConfig.ImagePullPolicy = "IfNotPresent"
	}

	clusterCert := cert.ComponentCert(entity.TiDBDiagnosisType, compId, sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, clusterCert); err != nil {
		tilog.Error(ctx, "failed to create tidb diagnosis cluster cert", zap.Error(err))
		return err
	}

	serverCert := cert.TiDBDiagnoseServerCert(compId, sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, serverCert); err != nil {
		tilog.Error(ctx, "failed to create tidb diagnosis server cert", zap.Error(err))
		return err
	}

	// NOTE: other config items of auditlog will be set when activating this TiDB
	auditLogConfig := &entity.AuditLogConfig{
		LogDir:                  entity.AuditAndSlowLogPath,
		GracefulStopWaitSeconds: int32(20),
	}
	auditLogConfigYaml, err := yaml.Marshal(auditLogConfig)
	if err != nil {
		tilog.Error(ctx, "failed to marshal audit log config for tidb diagnosis", zap.Error(err))
		return err
	}

	templArgs := map[string]string{
		"ClusterId":      sharedPool.GetId(),                                // 1233438346465886
		"SharedPoolId":   sharedPool.GetId(),                                // 1233438346465886
		"ComponentId":    compId,                                            // 3a219eb3
		"ComponentType":  entity.TiDBDiagnosisType,                          // diagnosis-tidb
		"ComponentGroup": tidbName,                                          // db-3a219eb3-diagnosis-tidb
		"verifyCN":       utils.FnvHash(sharedPool.GetDataPlaneNamespace()), // 59accb4c
		"AuditLogConfig": string(auditLogConfigYaml),
		"Image": fmt.Sprintf("%s/%s/%s:%s",
			a.imageConfig.Registry,
			a.imageConfig.RepoPrefix,
			path.Base(configuration.GetImageRepo()),
			configuration.GetImageTag(),
		),
		"AuditLogImage":   fmt.Sprintf("%s/%s", a.imageConfig.Registry, a.imageConfig.AuditLogImage),
		"SlowLogImage":    fmt.Sprintf("%s/%s", a.imageConfig.Registry, a.imageConfig.SlowLogImage),
		"ImagePullPolicy": a.imageConfig.ImagePullPolicy,
	}

	// set tenant id
	if sharedPool.GetProvider().GetTags() != nil && sharedPool.GetProvider().GetTags()[schemav1.LabelUserTenantKey] != "" {
		templArgs["UserTenantId"] = sharedPool.GetProvider().GetTags()[schemav1.LabelUserTenantKey]
	}
	if tikvWorkerURL := getTiKVWorkerURL(sharedPool); tikvWorkerURL != "" {
		templArgs["TiKVWorkerURL"] = tikvWorkerURL
	}

	resources, err := a.renderTemplates(ctx, templArgs)
	if err != nil {
		tilog.Error(ctx, "failed to render templates for tidb diagnosis", zap.Error(err))
		return err
	}

	for _, resource := range resources {
		if err := k8s.CreateIfNotExist(ctx, remoteCli, resource); err != nil {
			tilog.Error(ctx, "failed to create resource", zap.String("resource", resource.GetName()), zap.Error(err))
			return err
		}
	}

	tilog.Info(ctx, "apply tidb diagnosis action success")
	return nil
}

func (a *applyTiDBDiagnosisAction) getContextInfos(args actions.ActionArgs) (
	sharedPool *entity.SharedPool, remoteCli client.Client, compId, tidbName string, err error) {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "get apply tidb diagnosis context infos...")
	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		err = fmt.Errorf("failed to get sharedPool id: %w", err)
		return
	}
	sharedPool, err = a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		err = fmt.Errorf("failed to load sharedPool: %w", err)
		return
	}

	remoteCli, err = a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		err = fmt.Errorf("failed to get remote client for shared pool: %w", err)
		return
	}
	compId = utils.FnvHash(sharedPoolId) // same component ID as many shared components
	tidbName = a.getTiDBName(compId)
	tilog.Info(ctx, "get apply diagnosis tidb context infos success", zap.String("tidbName", tidbName))
	return
}

func (a *applyTiDBDiagnosisAction) getTiDBName(compId string) string {
	return fmt.Sprintf("db-%s-diagnosis-tidb", compId)
}

func (a *applyTiDBDiagnosisAction) renderTemplates(ctx context.Context, templArgs map[string]string) (
	resources []*unstructured.Unstructured, err error) {
	for _, tmpl := range tidbDiagnosisTemplates {
		yamlStr, resolvedName, resolvedVersion, err := templateversion.RenderWithVersion(ctx, a.templateVersionSvc, tmpl, "", templArgs)
		if err != nil {
			return nil, fmt.Errorf("failed to render template %s: %w", tmpl, err)
		}
		resourceCr, _, err := k8s.GenerateUnstructuredFromYaml(yamlStr)
		if err != nil {
			return nil, fmt.Errorf("failed to generate unstructured from yaml: %w", err)
		}
		templateversion.SetVersionAnnotations(resourceCr, resolvedName, resolvedVersion)
		resources = append(resources, resourceCr)
	}
	return resources, nil
}
