package br

import (
	"context"
	"fmt"

	"github.com/mitchellh/mapstructure"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	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/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/provider"
	"github.com/tidbcloud/eks-provider/controllers/utils/configmanager"
	"github.com/tidbcloud/eks-provider/controllers/workflow/action"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	gitopssdk "github.com/tidbcloud/infra-provider/pkg/gitops-sdk"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

type createBackup struct {
	cli            client.Client
	cachedProvider dataplane.EnvProviderManager
}

type createBackupInputs struct {
	Name        string              `mapstructure:"name" validate:"required"`
	Namespace   string              `mapstructure:"namespace" validate:"required"`
	ClusterName string              `mapstructure:"clusterName" validate:"required"`
	Mode        string              `mapstructure:"mode" validate:"required,oneof=snapshot log"`
	BaseImage   string              `mapstructure:"baseImage" validate:"required"`
	ImageTag    string              `mapstructure:"imageTag" validate:"required"`
	Storage     string              `mapstructure:"storage" validate:"required"`
	Region      string              `mapstructure:"region" validate:"omitempty"`
	Config      string              `mapstructure:"config" validate:"omitempty"`
	Affinity    *corev1.Affinity    `mapstructure:"affinity" validate:"omitempty"`
	Tolerations []corev1.Toleration `mapstructure:"tolerations" validate:"omitempty"`
	TableFilter []string            `mapstructure:"tableFilter" validate:"omitempty"`
	CommitTs    string              `mapstructure:"commitTs" validate:"omitempty"`
	SecretName  string              `mapstructure:"secretName" validate:"omitempty"`
}

// nolint: unused
type createBackupOutputs struct {
	Size     int64  `mapstructure:"size" validate:"omitempty"`
	Status   string `mapstructure:"status" validate:"omitempty"`
	CommitTs string `mapstructure:"commitTs" validate:"omitempty"`
}

func NewCreateBackup(cli client.Client, cachedProvider dataplane.EnvProviderManager) action.Action {
	return &createBackup{cli: cli, cachedProvider: cachedProvider}
}

func (a *createBackup) Name() cloudv1.WorkflowActionName {
	return "CreateBackupAction"
}

func (a *createBackup) PreCheck(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	inputs := &createBackupInputs{}
	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 *createBackup) Handle(ctx context.Context, wf *cloudv1.Workflow, step cloudv1.WorkflowStep) error {
	logger := log.FromContext(ctx)
	stepStatus, _ := wf.StepStatus(ctx, step.Name)

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

	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 cloudv1.NewWFNonRetriableError(fmt.Errorf("cluster %s not found", inputs.ClusterName))
		}
		return fmt.Errorf("get cluster: %w", err)
	}
	// clusterNamer encodes the naming convention of TidbCluster in dataplane
	// We need it to retrieve cluster namings in dataplane
	clusterNamer := namer.NewNamer().WithClusterName(cluster.Name).WithTiDBNamespace(cluster.PrefixedId())
	remoteCli, err := a.cachedProvider.GetClient(ctx, cluster)
	if err != nil {
		return fmt.Errorf("get dataplane cli: %w", err)
	}
	storageProvider, err := provider.NewBRStorageProvider(inputs.Storage, inputs.Region, inputs.SecretName)
	if err != nil {
		return cloudv1.NewWFNonRetriableError(fmt.Errorf("get br storage provider: %w", err))
	}

	name := types.NamespacedName{Namespace: clusterNamer.GetTiDBNamespace(), Name: inputs.Name}
	backup := &opv1alpha1.Backup{}
	if err = remoteCli.Get(ctx, name, backup); err == nil {
		status := backup.Status.Phase
		logger.Info("backup phase", "Backup", name.String(), "Phase", status)
		switch status {
		case opv1alpha1.BackupComplete:
			return stepStatus.UpdateOutput(ctx, map[string]string{
				"size":     fmt.Sprintf("%d", backup.Status.BackupSize),
				"commitTs": backup.Status.CommitTs,
				"status":   string(status),
			})
		case opv1alpha1.BackupFailed:
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("backup failed"))
		default:
			// Wait until backup succeed or failed
			return fmt.Errorf("backup unfinished %s", status)
		}
	} else if !apierrors.IsNotFound(err) {
		return fmt.Errorf("get backup: %w", err)
	}

	// Backup not found, create a new Backup
	backup = &opv1alpha1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      inputs.Name,
			Namespace: clusterNamer.GetTiDBNamespace(),
		},
		Spec: opv1alpha1.BackupSpec{
			Type:        opv1alpha1.BackupTypeFull,
			TableFilter: inputs.TableFilter,
			// We always use Retain policy for tidb-operator and let dataflow-svc to clean data
			CleanPolicy: opv1alpha1.CleanPolicyTypeRetain,
			CommitTs:    inputs.CommitTs,
			Mode:        opv1alpha1.BackupMode(inputs.Mode),
			Affinity:    inputs.Affinity,
			Tolerations: inputs.Tolerations,
			BR: &opv1alpha1.BRConfig{
				Cluster:          clusterNamer.GetTiDBName(),
				ClusterNamespace: clusterNamer.GetTiDBNamespace(),
				StatusAddr:       "0.0.0.0:8089",
			},
			UseKMS: false,
		},
	}
	storageProvider.OverrideBackup(backup)
	if inputs.Config != "" {
		if err := configmanager.MergeBRConfig(backup.Spec.BR, []byte(inputs.Config)); err != nil {
			return cloudv1.NewWFNonRetriableError(fmt.Errorf("invalid BR config: %s, err: %w", inputs.Config, err))
		}
	}
	// override image
	baseImage := gitopssdk.OverrideBaseImage(cluster, inputs.BaseImage)
	backup.Spec.ToolImage = fmt.Sprintf("%s:%s", baseImage, inputs.ImageTag)
	logger.Info("create backup in dataplane for cluster", "cluster", fmt.Sprintf("%s/%s", inputs.Namespace, inputs.ClusterName))
	if err = remoteCli.Create(ctx, backup); err != nil {
		return fmt.Errorf("create backup in dataplane failed: %w", err)
	}
	logger.Info("waiting backup to complete", "Backup", name.String(), "Phase", "Running")
	return fmt.Errorf("backup created success, waiting to complete")
}
