package hasher

import (
	"context"
	"errors"
	"fmt"
	"path"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/accounting"
	"github.com/rclone/rclone/fs/cache"
	"github.com/rclone/rclone/fs/fspath"
	"github.com/rclone/rclone/fs/hash"
	"github.com/rclone/rclone/fs/operations"
	"github.com/rclone/rclone/lib/kv"
)

// Command the backend to run a named command
//
// The command run is name
// args may be used to read arguments from
// opts may be used to read optional arguments from
//
// The result should be capable of being JSON encoded
// If it is a string or a []string it will be shown to the user
// otherwise it will be JSON encoded and shown to the user like that
func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (out any, err error) {
	switch name {
	case "drop":
		return nil, f.db.Stop(true)
	case "dump", "fulldump":
		return nil, f.dbDump(ctx, name == "fulldump", "")
	case "import", "stickyimport":
		sticky := name == "stickyimport"
		if len(arg) != 2 {
			return nil, errors.New("please provide checksum type and path to sum file")
		}
		return nil, f.dbImport(ctx, arg[0], arg[1], sticky)
	default:
		return nil, fs.ErrorCommandNotFound
	}
}

var commandHelp = []fs.CommandHelp{{
	Name:  "drop",
	Short: "Drop cache.",
	Long: `Completely drop checksum cache.

Usage example:

` + "```console" + `
rclone backend drop hasher:
` + "```",
}, {
	Name:  "dump",
	Short: "Dump the database.",
	Long:  "Dump cache records covered by the current remote.",
}, {
	Name:  "fulldump",
	Short: "Full dump of the database.",
	Long:  "Dump all cache records in the database.",
}, {
	Name:  "import",
	Short: "Import a SUM file.",
	Long: `Amend hash cache from a SUM file and bind checksums to files by size/time.

Usage example:

` + "```console" + `
rclone backend import hasher:subdir md5 /path/to/sum.md5
` + "```",
}, {
	Name:  "stickyimport",
	Short: "Perform fast import of a SUM file.",
	Long: `Fill hash cache from a SUM file without verifying file fingerprints.

Usage example:

` + "```console" + `
rclone backend stickyimport hasher:subdir md5 remote:path/to/sum.md5
` + "```",
}}

func (f *Fs) dbDump(ctx context.Context, full bool, root string) error {
	if root == "" {
		remoteFs, err := cache.Get(ctx, f.opt.Remote)
		if err != nil {
			return err
		}
		root = fspath.JoinRootPath(remoteFs.Root(), f.Root())
	}
	if f.db == nil {
		if f.opt.MaxAge == 0 {
			fs.Errorf(f, "db not found. (disabled with max_age = 0)")
		} else {
			fs.Errorf(f, "db not found.")
		}
		return kv.ErrInactive
	}
	op := &kvDump{
		full: full,
		root: root,
		path: f.db.Path(),
		fs:   f,
	}
	err := f.db.Do(false, op)
	if err == kv.ErrEmpty {
		fs.Infof(op.path, "empty")
		err = nil
	}
	return err
}

func (f *Fs) dbImport(ctx context.Context, hashName, sumRemote string, sticky bool) error {
	var hashType hash.Type
	if err := hashType.Set(hashName); err != nil {
		return err
	}
	if hashType == hash.None {
		return errors.New("please provide a valid hash type")
	}
	if !f.suppHashes.Contains(hashType) {
		return errors.New("unsupported hash type")
	}
	if !f.keepHashes.Contains(hashType) {
		fs.Infof(nil, "Need not import hashes of this type")
		return nil
	}

	_, sumPath, err := fspath.SplitFs(sumRemote)
	if err != nil {
		return err
	}
	sumFs, err := cache.Get(ctx, sumRemote)
	switch err {
	case fs.ErrorIsFile:
		// ok
	case nil:
		return fmt.Errorf("not a file: %s", sumRemote)
	default:
		return err
	}

	sumObj, err := sumFs.NewObject(ctx, path.Base(sumPath))
	if err != nil {
		return fmt.Errorf("cannot open sum file: %w", err)
	}
	hashes, err := operations.ParseSumFile(ctx, sumObj)
	if err != nil {
		return fmt.Errorf("failed to parse sum file: %w", err)
	}

	if sticky {
		rootPath := f.Fs.Root()
		for remote, hashVal := range hashes {
			key := path.Join(rootPath, remote)
			hashSums := operations.HashSums{hashName: hashVal}
			if err := f.putRawHashes(ctx, key, anyFingerprint, hashSums); err != nil {
				fs.Errorf(nil, "%s: failed to import: %v", remote, err)
			}
		}
		fs.Infof(nil, "Summary: %d checksum(s) imported", len(hashes))
		return nil
	}

	const longImportThreshold = 100
	if len(hashes) > longImportThreshold {
		fs.Infof(nil, "Importing %d checksums. Please wait...", len(hashes))
	}

	doneCount := 0
	err = operations.ListFn(ctx, f, func(obj fs.Object) {
		remote := obj.Remote()
		hash := hashes[remote]
		hashes[remote] = "" // mark as handled
		o, ok := obj.(*Object)
		if ok && hash != "" {
			if err := o.putHashes(ctx, hashMap{hashType: hash}); err != nil {
				fs.Errorf(nil, "%s: failed to import: %v", remote, err)
			}
			accounting.Stats(ctx).NewCheckingTransfer(obj, "importing").Done(ctx, err)
			doneCount++
		}
	})
	if err != nil {
		fs.Errorf(nil, "Import failed: %v", err)
	}
	skipCount := 0
	for remote, emptyOrDone := range hashes {
		if emptyOrDone != "" {
			fs.Infof(nil, "Skip vanished object: %s", remote)
			skipCount++
		}
	}
	fs.Infof(nil, "Summary: %d imported, %d skipped", doneCount, skipCount)
	return err
}
