package vpcLogs

import (
	"context"
	"fmt"
	"strconv"

	"github.com/go-playground/validator/v10"
	"github.com/mitchellh/mapstructure"
	"github.com/samber/lo"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/workflow/action"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
)

var (
	_        action.Action = &modifyLoadBalanceLogs{}
	validate               = validator.New(validator.WithRequiredStructEnabled())
)

type modifyLoadBalanceLogs struct {
	cli            client.Client
	gcpCli         gcp.Client
	cachedProvider dataplane.EnvProviderManager
}

type modifyLoadBalanceLogsInputs struct {
	Namespace      string   `mapstructure:"namespace" json:"namespace" validate:"required"`
	ClusterName    string   `mapstructure:"clusterName" json:"clusterName" validate:"required"`
	Enabled        bool     `mapstructure:"enabled" json:"enabled" validate:"omitempty"`
	SampleRate     string   `mapstructure:"sampleRate" json:"sampleRate" validate:"omitempty"`
	MetadataMode   string   `mapstructure:"metadataMode" json:"metadataMode" validate:"omitempty"`
	MetadataFields []string `mapstructure:"metadataFields" json:"metadataFields" validate:"omitempty"`
}

func NewModifyLoadBalanceLogs(cli client.Client, gcpCli gcp.Client, cachedProvider dataplane.EnvProviderManager) action.Action {
	return &modifyLoadBalanceLogs{cli: cli, gcpCli: gcpCli, cachedProvider: cachedProvider}
}

func (a *modifyLoadBalanceLogs) Name() cloudv1.WorkflowActionName {
	return consts.WfActionModifyLoadBalanceLogsAction
}

func (a *modifyLoadBalanceLogs) PreCheck(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	inputs := &modifyLoadBalanceLogsInputs{}
	if err := mapstructure.Decode(step.Inputs.Object, inputs); err != nil {
		return fmt.Errorf("decode inputs: %w", err)
	}
	if err := validate.Struct(inputs); err != nil {
		return fmt.Errorf("validate inputs: %w", err)
	}
	return nil
}

func (a *modifyLoadBalanceLogs) Handle(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	logger := log.FromContext(ctx)

	inputs := &modifyLoadBalanceLogsInputs{}
	_ = mapstructure.Decode(step.Inputs.Object, inputs)

	envProvider, cluster, err := a.getEnvProviderAndCluster(ctx, inputs)
	if err != nil {
		return err
	}

	// Get provider configs to call GCP API
	remoteCli := envProvider.GetClient()
	region := envProvider.GetRegion()      // eg. us-west2
	gcpProject := envProvider.GetAccount() // eg. tidbcloud-dev-000

	// Collect backend service names from services of type LoadBalancer.
	backendServiceNames, err := a.getGCPBackendServices(ctx, remoteCli, cluster)
	if err != nil {
		return err
	}

	// parse sampleRate into float64
	var sr float64
	if inputs.SampleRate == "" {
		sr = 0.0
	} else {
		sr, err = strconv.ParseFloat(inputs.SampleRate, 64)
		if err != nil {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("failed parse sampleRate to float64: %w", err))
		}
	}

	// update log config for LB of each backend service
	for _, bsName := range backendServiceNames {
		req := &gcp.ModifyBackendServiceLoggingInputs{
			SampleRate:     sr,
			BackendService: bsName,
			GCPRegion:      region,
			GCPProject:     gcpProject,
			Enabled:        inputs.Enabled,
			MetadataMode:   inputs.MetadataMode,
			MetadataFields: inputs.MetadataFields,
		}

		opName, err := a.gcpCli.ModifyBackendServiceLogging(ctx, req)
		if err != nil {
			logger.Error(err, "failed modify backend service logs", "backendService", bsName, "req", req)
			return fmt.Errorf("failed modify backend service logs: %w", err)
		}

		logger.Info("successfully modified LB logging on BackendService", "backendService", bsName, "operation", opName, "req", req)
	}

	return nil
}

func (a *modifyLoadBalanceLogs) getEnvProviderAndCluster(ctx context.Context, inputs *modifyLoadBalanceLogsInputs) (dataplane.EnvProvider, *cloudv1.Cluster, error) {
	cluster := &cloudv1.Cluster{}
	if err := a.cli.Get(ctx, client.ObjectKey{Namespace: inputs.Namespace, Name: inputs.ClusterName}, cluster); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, nil, cloudv1.NewWFNonRetriableError(fmt.Errorf("cluster %s not found", inputs.ClusterName))
		}
		return nil, nil, fmt.Errorf("failed get cluster: %w", err)
	}

	envProvider, err := a.cachedProvider.GetEnvProvider(ctx, cluster)
	if err != nil {
		return nil, nil, fmt.Errorf("get envProvider: %w", err)
	}

	return envProvider, cluster, nil
}

func (a *modifyLoadBalanceLogs) getGCPBackendServices(ctx context.Context, remoteCli client.Client, cluster *cloudv1.Cluster) ([]string, error) {
	var svcList corev1.ServiceList
	if err := remoteCli.List(ctx, &svcList, client.InNamespace(cluster.PrefixedId()), client.MatchingLabels{
		// Only [Private-ILB] supports LB-Loggings.
		common.Component: common.PrivateLoadbalance,
		common.UsedBy:    common.CustomerTiDB,
	}); err != nil {
		return nil, fmt.Errorf("failed get %s/services: %w", cluster.PrefixedId(), err)
	}

	// Collect backend service names from services of type LoadBalancer.
	backendServiceNames := make([]string, 0, len(svcList.Items))
	for _, svc := range svcList.Items {
		if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
			// Get the first non-empty annotation
			if name, ok := lo.Coalesce(
				// The annotation exists if ILB subsetting enabled.
				svc.Annotations[consts.AnnoKeyGKESubsettingILBBackendService],
				// The origin annotation set in FullService task
				svc.Annotations[consts.AnnoKeyGKEOriginForwardingRuleName],
			); ok {
				backendServiceNames = append(backendServiceNames, name)
			}
		}
	}

	return backendServiceNames, nil
}
