package action

import (
	"context"
	"errors"
	"fmt"
	"path"
	"strings"
	"time"

	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/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	configcenter "github.com/tidbcloud/config-center/proto/config_center"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"gopkg.in/yaml.v2"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

var tidbPrivilegedTemplates = []string{
	"tidb_privileged_configmap",
	"tidb_privileged_deployment",
	"tidb_privileged_svc",
}

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

const ApplyTiDBPrivilegedActionName = "apply-tidb-privileged"

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

func (a *applyTiDBPrivilegedAction) Name() string {
	return ApplyTiDBPrivilegedActionName
}

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

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

	shouldApply, updateExisting := a.shouldApply(args, sharedPool)
	if !shouldApply {
		tilog.Info(ctx, "skip apply tidb privileged action")
		return nil
	}

	configuration, err := a.getPrivilegedTiDBConfig(ctx, sharedPool)
	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.TiDBPrivilegedType, compId, sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, clusterCert); err != nil {
		tilog.Error(ctx, "failed to create tidb privileged cluster cert", zap.Error(err))
		return err
	}

	serverCert := cert.TiDBPrivilegedServerCert(compId, sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, serverCert); err != nil {
		tilog.Error(ctx, "failed to create tidb privileged 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 privileged", zap.Error(err))
		return err
	}

	templArgs := map[string]string{
		"ClusterId":      sharedPool.GetId(),                                // 1233438346465886
		"SharedPoolId":   sharedPool.GetId(),                                // 1233438346465886
		"ComponentId":    compId,                                            // 3a219eb3
		"ComponentType":  entity.TiDBPrivilegedType,                         // privileged-tidb
		"ComponentGroup": tidbName,                                          // db-3a219eb3-privileged-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 privileged", zap.Error(err))
		return err
	}

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

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

func (a *applyTiDBPrivilegedAction) shouldApply(args actions.ActionArgs, sharedPool *entity.SharedPool) (bool, bool) {
	hasUpdateOptions := false
	shouldUpdate := false

	components := sharedPool.GetComponentGroups()
	if sharedPoolStr, err := args.GetString(workflow.SharedPoolKey); err == nil && sharedPoolStr != "" {
		var tempSharedPool entity.SharedPool
		if err := protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool); err == nil {
			components = tempSharedPool.GetComponentGroups()
		}
	}

	for _, comp := range components {
		optionsKey := entity.GetUpdateComponentKey(comp)
		if _, ok := args[optionsKey]; !ok {
			continue
		}

		optionsStr, _ := args.GetString(optionsKey)
		hasUpdateOptions = true
		if optionsStr == "" {
			continue
		}

		if comp.GetComponentType() != apiv1.ComponentType_TIDB_WORKER {
			continue
		}
		for _, option := range strings.Split(optionsStr, ",") {
			if strings.TrimSpace(option) == string(tioperator.OpsUpdateImage) {
				shouldUpdate = true
				break
			}
		}
	}

	// createSharedPoolDag invokes this action without component update options.
	// In that path, render and create missing privileged TiDB resources only;
	// existing resources are left unchanged because updateExisting is false.
	if !hasUpdateOptions {
		return true, false
	}
	return shouldUpdate, shouldUpdate
}

func (a *applyTiDBPrivilegedAction) applyResource(
	ctx context.Context,
	remoteCli client.Client,
	resource *unstructured.Unstructured,
	updateExisting bool,
) error {
	if !updateExisting || resource.GetKind() != "Deployment" {
		return k8s.CreateIfNotExist(ctx, remoteCli, resource)
	}

	toCr := resource.DeepCopy()
	err := remoteCli.Get(ctx, types.NamespacedName{Name: resource.GetName(), Namespace: resource.GetNamespace()}, toCr)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return remoteCli.Create(ctx, resource)
		}
		return err
	}

	_, err = controllerutil.CreateOrUpdate(ctx, remoteCli, toCr, func() error {
		toCr.Object["spec"] = resource.Object["spec"]
		toCr.SetAnnotations(resource.GetAnnotations())
		toCr.SetLabels(resource.GetLabels())
		return nil
	})
	return err
}

func (a *applyTiDBPrivilegedAction) 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 privileged 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 privileged tidb context infos success", zap.String("tidbName", tidbName))
	return
}

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

func (a *applyTiDBPrivilegedAction) renderTemplates(ctx context.Context, templArgs map[string]string) (
	resources []*unstructured.Unstructured, err error) {
	for _, tmpl := range tidbPrivilegedTemplates {
		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
}

func findComponentByTypeForPrivileged(components []*entity.Component, componentType apiv1.ComponentType) *entity.Component {
	for _, comp := range components {
		if comp == nil {
			continue
		}
		if comp.GetComponentType() == componentType {
			return comp
		}
	}
	return nil
}

func hasComponentType(components []*entity.Component, componentType apiv1.ComponentType) bool {
	return findComponentByTypeForPrivileged(components, componentType) != nil
}

func getTiKVWorkerURL(sharedPool *entity.SharedPool) string {
	for _, comp := range sharedPool.GetComponentGroups() {
		if comp.GetComponentType() != apiv1.ComponentType_TIKV_WORKER {
			continue
		}
		if comp.GetNodeQuota().GetNodeCount() == 0 {
			continue
		}
		return fmt.Sprintf("https://%s.%s.svc:19000", entity.GetComponentSvcName(sharedPool, comp), sharedPool.GetDataPlaneNamespace())
	}
	return ""
}

func (a *applyTiDBPrivilegedAction) getPrivilegedTiDBConfig(ctx context.Context, sharedPool *entity.SharedPool) (*entity.ComponentConfiguration, error) {
	if shouldUseSharedPoolTiDBAsPrivilegedSource(sharedPool) {
		cfg, err := a.getSharedPoolTiDBConfigWithRetry(ctx, sharedPool)
		if err == nil {
			return cfg, nil
		}
		tilog.Warn(ctx, "failed to get shared-pool TIDB image for privileged TiDB, fallback to legacy source chain",
			zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
	}

	targetComp := &entity.Component{Id: sharedPool.GetDefaultComponenId(), ComponentType: apiv1.ComponentType_TIDB_WORKER}
	_, configuration, err := a.clusterConfigClient.GetComponentVersionAndConfig(ctx, sharedPool, targetComp)
	if err == nil {
		return configuration, nil
	}

	if !isGRPCNotFound(err) {
		return nil, err
	}

	tilog.Warn(ctx, "TIDB_WORKER config not found, fallback to TIKV image for privileged TiDB",
		zap.String("sharedPoolId", sharedPool.GetId()),
		zap.Error(err))

	_, tikvCfg, tikvErr := a.clusterConfigClient.GetComponentVersionAndConfig(ctx, sharedPool, &entity.Component{
		Id:            sharedPool.GetDefaultComponenId(),
		ComponentType: apiv1.ComponentType_TIKV,
	})
	if tikvErr != nil {
		return nil, fmt.Errorf("failed to get fallback tikv component config: %w", tikvErr)
	}

	return &entity.ComponentConfiguration{
		ImageRepo: tikvCfg.GetImageRepo(),
		ImageTag:  tikvCfg.GetImageTag(),
	}, nil
}

func (a *applyTiDBPrivilegedAction) getSharedPoolTiDBConfigWithRetry(ctx context.Context, sharedPool *entity.SharedPool) (*entity.ComponentConfiguration, error) {
	const (
		maxAttempts = 6
		retryDelay  = 3 * time.Second
	)

	var lastErr error
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		resp, err := a.clusterConfigClient.GetClient().GetClusterComponent(ctx, &configcenter.GetClusterComponentRequest{
			ClusterType:   configcenter.ClusterType_SHARED_POOL,
			ClusterId:     sharedPool.GetId(),
			ComponentName: strings.ToLower(apiv1.ComponentType_TIDB.String()),
		})
		if err == nil {
			if resp.GetBaseImage() == "" || resp.GetEffectiveImageTag() == "" {
				return nil, fmt.Errorf("tidb image not found in shared-pool meta")
			}
			return &entity.ComponentConfiguration{
				ImageRepo: resp.GetBaseImage(),
				ImageTag:  resp.GetEffectiveImageTag(),
			}, nil
		}

		lastErr = err
		if !isGRPCNotFound(err) || attempt == maxAttempts {
			break
		}

		tilog.Info(ctx, "shared-pool TIDB component not ready yet, retry for privileged TiDB image",
			zap.String("sharedPoolId", sharedPool.GetId()),
			zap.Int("attempt", attempt),
			zap.Int("maxAttempts", maxAttempts))

		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(retryDelay):
		}
	}

	return nil, fmt.Errorf("get shared-pool tidb image config: %w", lastErr)
}

func shouldUseSharedPoolTiDBAsPrivilegedSource(sharedPool *entity.SharedPool) bool {
	if sharedPool.GetCapacityMode() != apiv1.CapacityMode_TOPOLOGY_DRIVEN {
		return false
	}
	tidbWorkerComp := findComponentByTypeForPrivileged(sharedPool.GetComponentGroups(), apiv1.ComponentType_TIDB_WORKER)
	if tidbWorkerComp == nil {
		return true
	}
	return tidbWorkerComp.GetNodeQuota().GetNodeCount() == 0
}

func isGRPCNotFound(err error) bool {
	if err == nil {
		return false
	}
	var grpcStatus interface{ GRPCStatus() *status.Status }
	if !errors.As(err, &grpcStatus) {
		return false
	}
	return grpcStatus.GRPCStatus().Code() == codes.NotFound
}
