package metabase

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config"
	engineconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/engine"
	shardconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/engine/shard"
	morphconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/morph"
	nodeconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/node"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	meta "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
	morphcontainer "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
	"github.com/spf13/cobra"
	"golang.org/x/sync/errgroup"
)

const (
	noCompactFlag = "no-compact"
)

var (
	errNoPathsFound          = errors.New("no metabase paths found")
	errNoMorphEndpointsFound = errors.New("no morph endpoints found")
)

var UpgradeCmd = &cobra.Command{
	Use:   "upgrade",
	Short: "Upgrade metabase to latest version",
	RunE:  upgrade,
}

func upgrade(cmd *cobra.Command, _ []string) error {
	configFile, err := cmd.Flags().GetString(commonflags.ConfigFlag)
	if err != nil {
		return err
	}
	configDir, err := cmd.Flags().GetString(commonflags.ConfigDirFlag)
	if err != nil {
		return err
	}
	appCfg := config.New(configFile, configDir, config.EnvPrefix)
	paths, err := getMetabasePaths(appCfg)
	if err != nil {
		return err
	}
	if len(paths) == 0 {
		return errNoPathsFound
	}
	cmd.Println("found", len(paths), "metabases:")
	for i, path := range paths {
		cmd.Println(i+1, ":", path)
	}
	mc, err := createMorphClient(cmd.Context(), appCfg)
	if err != nil {
		return err
	}
	defer mc.Close()
	civ, err := createContainerInfoProvider(mc)
	if err != nil {
		return err
	}
	noCompact, _ := cmd.Flags().GetBool(noCompactFlag)
	result := make(map[string]bool)
	var resultGuard sync.Mutex
	eg, ctx := errgroup.WithContext(cmd.Context())
	for _, path := range paths {
		eg.Go(func() error {
			var success bool
			cmd.Println("upgrading metabase", path, "...")
			if err := meta.Upgrade(ctx, path, !noCompact, civ, func(a ...any) {
				cmd.Println(append([]any{time.Now().Format(time.RFC3339), ":", path, ":"}, a...)...)
			}); err != nil {
				cmd.Println("error: failed to upgrade metabase", path, ":", err)
			} else {
				success = true
				cmd.Println("metabase", path, "upgraded successfully")
			}
			resultGuard.Lock()
			result[path] = success
			resultGuard.Unlock()
			return nil
		})
	}
	if err := eg.Wait(); err != nil {
		return err
	}
	for mb, ok := range result {
		if ok {
			cmd.Println(mb, ": success")
		} else {
			cmd.Println(mb, ": failed")
		}
	}
	return nil
}

func getMetabasePaths(appCfg *config.Config) ([]string, error) {
	var paths []string
	if err := engineconfig.IterateShards(appCfg, false, func(sc *shardconfig.Config) error {
		paths = append(paths, sc.Metabase().Path())
		return nil
	}); err != nil {
		return nil, fmt.Errorf("get metabase paths: %w", err)
	}
	return paths, nil
}

func createMorphClient(ctx context.Context, appCfg *config.Config) (*client.Client, error) {
	addresses := morphconfig.RPCEndpoint(appCfg)
	if len(addresses) == 0 {
		return nil, errNoMorphEndpointsFound
	}
	key := nodeconfig.Key(appCfg)
	cli, err := client.New(ctx,
		key,
		client.WithDialTimeout(morphconfig.DialTimeout(appCfg)),
		client.WithEndpoints(addresses...),
		client.WithSwitchInterval(morphconfig.SwitchInterval(appCfg)),
	)
	if err != nil {
		return nil, fmt.Errorf("create morph client:%w", err)
	}
	return cli, nil
}

func createContainerInfoProvider(cli *client.Client) (container.InfoProvider, error) {
	sh, err := cli.NNSContractAddress(client.NNSContainerContractName)
	if err != nil {
		return nil, fmt.Errorf("resolve container contract hash: %w", err)
	}
	cc, err := morphcontainer.NewFromMorph(cli, sh, 0, morphcontainer.TryNotary())
	if err != nil {
		return nil, fmt.Errorf("create morph container client: %w", err)
	}
	return container.NewInfoProvider(func() (container.Source, error) {
		return morphcontainer.AsContainerSource(cc), nil
	}), nil
}

func initUpgradeCommand() {
	flags := UpgradeCmd.Flags()
	flags.Bool(noCompactFlag, false, "Do not compact upgraded metabase file")
}