package backend

import (
	"context"
	"fmt"

	"github.com/blang/semver/v4"
	"github.com/gopasspw/gopass/pkg/debug"
)

// ErrNotSupported is returned by backends for unsupported calls.
var ErrNotSupported = fmt.Errorf("not supported")

// StorageBackend is a type of storage backend.
type StorageBackend int

const (
	// FS is a filesystem-backed storage.
	FS StorageBackend = iota
	// GitFS is a filesystem-backed storage with Git.
	GitFS
	// FossilFS is a filesystem-backed storage with Fossil.
	FossilFS
	// JJ is a filesystem-backed storage with Jujutsu.
	JJFS
	// CryptFS is a filename encrypting storage.
	CryptFS
)

func (s StorageBackend) String() string {
	if be, err := StorageRegistry.BackendName(s); err == nil {
		return be
	}

	return ""
}

// Storage is an storage backend.
type Storage interface {
	fmt.Stringer
	rcs
	Get(ctx context.Context, name string) ([]byte, error)
	Set(ctx context.Context, name string, value []byte) error
	Delete(ctx context.Context, name string) error
	Exists(ctx context.Context, name string) bool
	Move(ctx context.Context, from, to string, del bool) error
	List(ctx context.Context, prefix string) ([]string, error)
	IsDir(ctx context.Context, name string) bool
	Prune(ctx context.Context, prefix string) error
	Link(ctx context.Context, from, to string) error

	Name() string
	Path() string
	Version(context.Context) semver.Version
	Fsck(context.Context) error
}

// DetectStorage tries to detect the storage backend being used.
func DetectStorage(ctx context.Context, path string) (Storage, error) {
	// The call to HasStorageBackend is important since GetStorageBackend will always return FS
	// if nothing is found in the context.
	if be, err := StorageRegistry.Get(GetStorageBackend(ctx)); HasStorageBackend(ctx) && err == nil {
		debug.V(1).Log("Trying requested storage backend %q for %q", be, path)
		st, err := be.New(ctx, path)
		if err == nil {
			debug.Log("Successfully loaded requested storage backend %q for %q", be, path)

			return st, nil
		}
		debug.Log("Failed to use requested storage backend %q for %s: %q", be, path, err)

		// fallback to FS
		be, err := StorageRegistry.Get(FS)
		if err != nil {
			return nil, err
		}
		debug.Log("Using fallback %q for %q", be, path)

		return be.Init(ctx, path)
	}

	// Nothing requested in the context. Try to detect the backend.
	for _, be := range StorageRegistry.Prioritized() {
		debug.V(1).Log("Trying storage backend %q for %q", be, path)
		if err := be.Handles(ctx, path); err != nil {
			debug.Log("failed to use %s for %s: %s", be, path, err)

			continue
		}
		debug.Log("Detected storage backend %q for %q", be, path)

		return be.New(ctx, path)
	}

	// fallback to FS
	be, err := StorageRegistry.Get(FS)
	if err != nil {
		return nil, err
	}
	debug.Log("Using default fallback %q for %q", be, path)

	return be.Init(ctx, path)
}

// NewStorage initializes an existing storage backend.
func NewStorage(ctx context.Context, id StorageBackend, path string) (Storage, error) {
	if be, err := StorageRegistry.Get(id); err == nil {
		debug.Log("Using storage backend %q for %q", be, path)

		return be.New(ctx, path)
	}

	return nil, fmt.Errorf("unknown backend %q: %w", path, ErrNotFound)
}

// InitStorage initilizes a new storage location.
func InitStorage(ctx context.Context, id StorageBackend, path string) (Storage, error) {
	if be, err := StorageRegistry.Get(id); err == nil {
		debug.Log("Using %s for %s", be, path)

		return be.Init(ctx, path)
	}

	return nil, fmt.Errorf("unknown backend %q: %w", path, ErrNotFound)
}
