package utils

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/alcionai/clues"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	"github.com/alcionai/corso/src/cli/flags"
	"github.com/alcionai/corso/src/internal/events"
	"github.com/alcionai/corso/src/pkg/account"
	"github.com/alcionai/corso/src/pkg/config"
	"github.com/alcionai/corso/src/pkg/control"
	"github.com/alcionai/corso/src/pkg/logger"
	"github.com/alcionai/corso/src/pkg/path"
	"github.com/alcionai/corso/src/pkg/repository"
	"github.com/alcionai/corso/src/pkg/selectors"
	"github.com/alcionai/corso/src/pkg/storage"
)

type RepoDetailsAndOpts struct {
	Repo config.RepoDetails
	Opts control.Options
}

var ErrNotYetImplemented = clues.New("not yet implemented")

// GetAccountAndConnect is a wrapper for GetAccountAndConnectWithOverrides
// that automatically gets the storage provider and any storage provider specific
// flag overrides from the command line.
func GetAccountAndConnect(
	ctx context.Context,
	cmd *cobra.Command,
	pst path.ServiceType,
) (repository.Repositoryer, RepoDetailsAndOpts, error) {
	provider, overrides, err := GetStorageProviderAndOverrides(ctx, cmd)
	if err != nil {
		return nil, RepoDetailsAndOpts{}, clues.Stack(err)
	}

	return GetAccountAndConnectWithOverrides(ctx, pst, provider, overrides)
}

func GetAccountAndConnectWithOverrides(
	ctx context.Context,
	pst path.ServiceType,
	provider storage.ProviderType,
	overrides map[string]string,
) (repository.Repositoryer, RepoDetailsAndOpts, error) {
	cfg, err := config.ReadCorsoConfig(
		ctx,
		provider,
		true,
		true,
		overrides)
	if err != nil {
		return nil, RepoDetailsAndOpts{}, err
	}

	repoID := cfg.RepoID
	if len(repoID) == 0 {
		repoID = events.RepoIDNotFound
	}

	opts := ControlWithConfig(cfg)

	r, err := repository.New(
		ctx,
		cfg.Account,
		cfg.Storage,
		opts,
		repoID)
	if err != nil {
		return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "creating a repository controller")
	}

	if err := r.Connect(ctx, repository.ConnConfig{Service: pst}); err != nil {
		return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "connecting to the "+cfg.Storage.Provider.String()+" repository")
	}

	rdao := RepoDetailsAndOpts{
		Repo: cfg,
		Opts: opts,
	}

	return r, rdao, nil
}

func AccountConnectAndWriteRepoConfig(
	ctx context.Context,
	cmd *cobra.Command,
	pst path.ServiceType,
) (repository.Repositoryer, *account.Account, error) {
	r, rdao, err := GetAccountAndConnect(ctx, cmd, pst)
	if err != nil {
		logger.CtxErr(ctx, err).Info("getting and connecting account")
		return nil, nil, err
	}

	sc, err := rdao.Repo.Storage.StorageConfig()
	if err != nil {
		logger.CtxErr(ctx, err).Info("getting storage configuration")
		return nil, nil, err
	}

	m365Config, err := rdao.Repo.Account.M365Config()
	if err != nil {
		logger.CtxErr(ctx, err).Info("getting m365 configuration")
		return nil, nil, err
	}

	// repo config gets set during repo connect and init.
	// This call confirms we have the correct values.
	err = config.WriteRepoConfig(ctx, sc, m365Config, rdao.Opts.Repo, r.GetID())
	if err != nil {
		logger.CtxErr(ctx, err).Info("writing to repository configuration")
		return nil, nil, err
	}

	return r, &rdao.Repo.Account, nil
}

// CloseRepo handles closing a repo.
func CloseRepo(ctx context.Context, r repository.Repositoryer) {
	if err := r.Close(ctx); err != nil {
		fmt.Print("Error closing repository:", err)
	}
}

// HasNoFlagsAndShownHelp shows the Help output if no flags
// were provided to the command.  Returns true if the help
// was shown.
// Use for when the non-flagged usage of a command
// (ex: corso backup restore exchange) is expected to no-op.
func HasNoFlagsAndShownHelp(cmd *cobra.Command) bool {
	if cmd.Flags().NFlag() == 0 {
		cobra.CheckErr(cmd.Help())
		return true
	}

	return false
}

type cmdCfg struct {
	hidden     bool
	preRelease bool
	preview    bool
	debug      bool
}

type cmdOpt func(*cmdCfg)

func (cc *cmdCfg) populate(opts ...cmdOpt) {
	for _, opt := range opts {
		opt(cc)
	}
}

func HideCommand() cmdOpt {
	return func(cc *cmdCfg) {
		cc.hidden = true
	}
}

func MarkPreReleaseCommand() cmdOpt {
	return func(cc *cmdCfg) {
		cc.hidden = true
		cc.preRelease = true
	}
}

func MarkPreviewCommand() cmdOpt {
	return func(cc *cmdCfg) {
		cc.preview = true
	}
}

func MarkDebugCommand() cmdOpt {
	return func(cc *cmdCfg) {
		cc.hidden = true
		cc.debug = true
	}
}

// AddCommand adds a clone of the subCommand to the parent,
// and returns both the clone and its pflags.
func AddCommand(parent, c *cobra.Command, opts ...cmdOpt) (*cobra.Command, *pflag.FlagSet) {
	cc := &cmdCfg{}
	cc.populate(opts...)

	parent.AddCommand(c)
	c.Hidden = cc.hidden

	var deprecatedMsgOverride string

	switch true {
	case cc.preRelease:
		deprecatedMsgOverride = "THIS IS A PRE-RELEASE COMMAND THAT MAY NOT FUNCTION PROPERLY, OR AT ALL"
	case cc.preview:
		deprecatedMsgOverride = "THIS IS A FEATURE PREVIEW THAT MAY NOT FUNCTION PROPERLY AND MAY BREAK ACROSS RELEASES"
	case cc.debug:
		deprecatedMsgOverride = "THIS IS A DEBUGGING COMMAND - IT IS NOT PART OF STANDARD CORSO OPERATION"
	}

	if len(deprecatedMsgOverride) > 0 {
		// There is a default deprecated message that always shows so we do some terminal magic to overwrite it
		c.Deprecated = "\n\033[1F\033[K" +
			"==================================================================================================\n" +
			"\tWARNING!!! " + deprecatedMsgOverride + "\n" +
			"==================================================================================================\n"
	}

	c.Flags().SortFlags = false

	return c, c.Flags()
}

// separates the provided folders into two sets: folders that use a pathContains
// comparison (the default), and folders that use a pathPrefix comparison.
// Any element beginning with a path.PathSeparator (ie: '/') is moved to the prefix
// comparison set.  If folders is nil, returns only containsFolders with the any matcher.
func splitFoldersIntoContainsAndPrefix(folders []string) ([]string, []string) {
	var (
		containsFolders = []string{}
		prefixFolders   = []string{}
	)

	if len(folders) == 0 {
		return selectors.Any(), nil
	}

	// separate folder selection inputs by behavior.
	// any input beginning with a '/' character acts as a prefix match.
	for _, f := range folders {
		if len(f) == 0 {
			continue
		}

		if f[0] == path.PathSeparator {
			prefixFolders = append(prefixFolders, f)
		} else {
			containsFolders = append(containsFolders, f)
		}
	}

	return containsFolders, prefixFolders
}

// GetStorageProviderAndOverrides returns the storage provider type and
// any flags specified on the command line which are storage provider specific.
func GetStorageProviderAndOverrides(
	ctx context.Context,
	cmd *cobra.Command,
) (storage.ProviderType, map[string]string, error) {
	provider, err := config.GetStorageProviderFromConfigFile(ctx)
	if err != nil {
		return provider, nil, clues.Stack(err)
	}

	switch provider {
	case storage.ProviderS3:
		return provider, flags.S3FlagOverrides(cmd), nil
	case storage.ProviderFilesystem:
		return provider, flags.FilesystemFlagOverrides(cmd), nil
	}

	return provider, nil, clues.New("unknown storage provider: " + provider.String())
}

// MakeAbsoluteFilePath does directory path expansions & conversions, namely:
// 1. Expands "~" prefix to the user's home directory, and converts to absolute path.
// 2. Relative paths are converted to absolute paths.
// 3. Absolute paths are returned as-is.
// 4. Empty paths are not allowed, an error is returned.
func MakeAbsoluteFilePath(p string) (string, error) {
	if len(p) == 0 {
		return "", clues.New("empty path")
	}

	// Special case handling for "~". filepath.Abs will not expand it.
	if p[0] == '~' {
		homeDir, err := os.UserHomeDir()
		if err != nil {
			return "", clues.Wrap(err, "getting user home directory")
		}

		p = filepath.Join(homeDir, p[1:])
	}

	abs, err := filepath.Abs(p)
	if err != nil {
		return "", clues.Stack(err)
	}

	return abs, nil
}
