package common

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/mitchellh/mapstructure"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

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

// waitPodPhase
type waitTargetReady struct {
	cli            client.Client
	cachedProvider dataplane.EnvProviderManager
}

type waitTargetReadyInputs struct {
	Namespace   string `mapstructure:"namespace" json:"namespace" validate:"required"`
	ClusterName string `mapstructure:"clusterName" json:"clusterName" validate:"required"`
	TargetName  string `mapstructure:"targetName" json:"targetName" validate:"required"`
	TargetType  string `mapstructure:"targetType" json:"targetType" validate:"required,oneof=pod component"`
	Phase       string `mapstructure:"phase" json:"phase" validate:"required"`
	// EnsureCreatedAfter is used to ensure the pod has been created after the specified time.
	// It should be in RFC3339 format or a variable in format '${fromCategory}.{fromStepName}.{fromWhere}.{fromKey}' likes
	// '$steps.step1.status.beginAt'
	EnsureCreatedAfter string `mapstructure:"ensureCreatedAfter" json:"ensureCreatedAfter" validate:"required"`
}

var (
	_ action.Action = &waitTargetReady{}
)

// NewWaitTargetReady wait for single pod or components-related pods running after the EnsureCreatedAfter time.
func NewWaitTargetReady(cli client.Client, cachedProvider dataplane.EnvProviderManager) action.Action {
	return &waitTargetReady{cli: cli, cachedProvider: cachedProvider}
}

func (p *waitTargetReady) Name() cloudv1.WorkflowActionName {
	return consts.WfActionWaitTargetReadyAction
}

func (p *waitTargetReady) PreCheck(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	inputs := &waitTargetReadyInputs{}
	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 (p *waitTargetReady) Handle(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	inputs := &waitTargetReadyInputs{}
	_ = mapstructure.Decode(step.Inputs.Object, inputs)

	var ensureCreatedAfter time.Time
	if inputs.EnsureCreatedAfter != "" {
		var err error
		ensureCreatedAfter, err = time.Parse(time.RFC3339, inputs.EnsureCreatedAfter)
		if err != nil {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("parse ensureCreatedAfter: %w", err))
		}
	}

	cluster := &cloudv1.Cluster{}
	if err := p.cli.Get(ctx, client.ObjectKey{Namespace: inputs.Namespace, Name: inputs.ClusterName}, cluster); err != nil {
		if apierrors.IsNotFound(err) {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("cluster %s not found", inputs.ClusterName))
		}
		return fmt.Errorf("get cluster: %w", err)
	}

	remoteCli, err := p.cachedProvider.GetClient(ctx, cluster)
	if err != nil {
		return fmt.Errorf("get dataplane cli: %w", err)
	}

	// to ensure there are expect count of pods created at specific time.
	var cntExpected int
	var targetPodsList corev1.PodList

	// collect the single pod or all pods related to the component.
	if inputs.TargetType == "pod" {
		cntExpected = 1

		pod := corev1.Pod{}
		// the pod may be in creating, so don't mark WF as failed if the pod is not found.
		if err := remoteCli.Get(ctx, client.ObjectKey{Namespace: cluster.PrefixedId(), Name: inputs.TargetName}, &pod); err != nil {
			return fmt.Errorf("get pod: %w", err)
		}
		targetPodsList.Items = append(targetPodsList.Items, pod)

	} else if inputs.TargetType == "component" {
		cntExpected, err = cluster.GetAllReplicasByComp(cloudv1.ComponentType(inputs.TargetName))
		if err != nil {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("failed get replicas of comp: %w", err))
		}

		if err := remoteCli.List(ctx, &targetPodsList,
			client.InNamespace(cluster.PrefixedId()),
			client.MatchingLabels{componentLabelKey: inputs.TargetName}); err != nil {
			return fmt.Errorf("list %s pods: %w", inputs.TargetName, err)
		}

	} else {
		return cloudv1.NewWFNonRetriableError(fmt.Errorf("target type %s not supported", inputs.TargetType))
	}

	var cntEnsured int
	var failedPods []string
	for _, pod := range targetPodsList.Items {
		if inputs.EnsureCreatedAfter != "" && pod.CreationTimestamp.Time.After(ensureCreatedAfter) && string(pod.Status.Phase) == inputs.Phase {
			cntEnsured++
			continue

		} else if inputs.EnsureCreatedAfter != "" && pod.CreationTimestamp.Time.Before(ensureCreatedAfter) {
			failedPods = append(failedPods, fmt.Sprintf("[%s]:not create after %s", pod.Name, inputs.EnsureCreatedAfter))

		} else if string(pod.Status.Phase) != inputs.Phase {
			failedPods = append(failedPods, fmt.Sprintf("[%s]:expect_phase=%s,actual=%s", pod.Name, inputs.Phase, pod.Status.Phase))
		}
	}

	if cntEnsured < cntExpected {
		return fmt.Errorf("expected %d pods ready, got %d: %s", cntExpected, cntEnsured, strings.Join(failedPods, "; "))
	}

	return nil
}
