package repomantenance

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

	"github.com/bombsimon/logrusr/v3"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	corev1api "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/vmware-tanzu/velero/internal/credentials"
	velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
	velerocli "github.com/vmware-tanzu/velero/pkg/client"
	"github.com/vmware-tanzu/velero/pkg/repository"
	"github.com/vmware-tanzu/velero/pkg/util/filesystem"
	"github.com/vmware-tanzu/velero/pkg/util/logging"

	repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
	"github.com/vmware-tanzu/velero/pkg/repository/maintenance"
	repomanager "github.com/vmware-tanzu/velero/pkg/repository/manager"
)

type Options struct {
	RepoName              string
	BackupStorageLocation string
	RepoType              string
	ResourceTimeout       time.Duration
	LogLevelFlag          *logging.LevelFlag
	FormatFlag            *logging.FormatFlag
}

func (o *Options) BindFlags(flags *pflag.FlagSet) {
	flags.StringVar(&o.RepoName, "repo-name", "", "namespace of the pod/volume that the snapshot is for")
	flags.StringVar(&o.BackupStorageLocation, "backup-storage-location", "", "backup's storage location name")
	flags.StringVar(&o.RepoType, "repo-type", velerov1api.BackupRepositoryTypeKopia, "type of the repository where the snapshot is stored")
	flags.Var(o.LogLevelFlag, "log-level", fmt.Sprintf("The level at which to log. Valid values are %s.", strings.Join(o.LogLevelFlag.AllowedValues(), ", ")))
	flags.Var(o.FormatFlag, "log-format", fmt.Sprintf("The format for log output. Valid values are %s.", strings.Join(o.FormatFlag.AllowedValues(), ", ")))
}

func NewCommand(f velerocli.Factory) *cobra.Command {
	o := &Options{
		LogLevelFlag: logging.LogLevelFlag(logrus.InfoLevel),
		FormatFlag:   logging.NewFormatFlag(),
	}
	cmd := &cobra.Command{
		Use:    "repo-maintenance",
		Hidden: true,
		Short:  "VELERO INTERNAL COMMAND ONLY - not intended to be run directly by users",
		Run: func(c *cobra.Command, args []string) {
			o.Run(f)
		},
	}

	o.BindFlags(cmd.Flags())
	return cmd
}

func (o *Options) Run(f velerocli.Factory) {
	logger := logging.DefaultLogger(o.LogLevelFlag.Parse(), o.FormatFlag.Parse())
	logger.SetOutput(os.Stdout)

	ctrl.SetLogger(logrusr.New(logger))

	pruneError := o.runRepoPrune(f, f.Namespace(), logger)
	defer func() {
		if pruneError != nil {
			os.Exit(1)
		}
	}()

	if pruneError != nil {
		os.Stdout.WriteString(fmt.Sprintf("%s%v", maintenance.TerminationLogIndicator, pruneError))
	}
}

func (o *Options) initClient(f velerocli.Factory) (client.Client, error) {
	scheme := runtime.NewScheme()
	err := velerov1api.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Wrap(err, "failed to add velero scheme")
	}

	err = corev1api.AddToScheme(scheme)
	if err != nil {
		return nil, errors.Wrap(err, "failed to add api core scheme")
	}

	config, err := f.ClientConfig()
	if err != nil {
		return nil, errors.Wrap(err, "failed to get client config")
	}

	cli, err := client.New(config, client.Options{
		Scheme: scheme,
	})
	if err != nil {
		return nil, errors.Wrap(err, "failed to create client")
	}

	return cli, nil
}

func initRepoManager(namespace string, cli client.Client, kubeClient kubernetes.Interface, logger logrus.FieldLogger) (repomanager.Manager, error) {
	// ensure the repo key secret is set up
	if err := repokey.EnsureCommonRepositoryKey(kubeClient.CoreV1(), namespace); err != nil {
		return nil, errors.Wrap(err, "failed to ensure repository key")
	}

	repoLocker := repository.NewRepoLocker()

	credentialFileStore, err := credentials.NewNamespacedFileStore(
		cli,
		namespace,
		credentials.DefaultStoreDirectory(),
		filesystem.NewFileSystem(),
	)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create namespaced file store")
	}

	credentialSecretStore, err := credentials.NewNamespacedSecretStore(cli, namespace)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create namespaced secret store")
	}

	return repomanager.NewManager(
		namespace,
		cli,
		repoLocker,
		credentialFileStore,
		credentialSecretStore,
		logger,
	), nil
}

func (o *Options) runRepoPrune(f velerocli.Factory, namespace string, logger logrus.FieldLogger) error {
	cli, err := o.initClient(f)
	if err != nil {
		return err
	}

	kubeClient, err := f.KubeClient()
	if err != nil {
		return err
	}

	var repo *velerov1api.BackupRepository
	retry := 10
	for {
		repo, err = repository.GetBackupRepository(context.Background(), cli, namespace,
			repository.BackupRepositoryKey{
				VolumeNamespace: o.RepoName,
				BackupLocation:  o.BackupStorageLocation,
				RepositoryType:  o.RepoType,
			}, true)
		if err == nil {
			break
		}

		retry--
		if retry == 0 {
			break
		}

		logger.WithError(err).Warn("Failed to retrieve backup repo, need retry")

		time.Sleep(time.Second)
	}

	if err != nil {
		return errors.Wrap(err, "failed to get backup repository")
	}

	manager, err := initRepoManager(namespace, cli, kubeClient, logger)
	if err != nil {
		return err
	}

	err = manager.PruneRepo(repo)
	if err != nil {
		return errors.Wrap(err, "failed to prune repo")
	}

	return nil
}
