package app

import (
	"bufio"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/pelletier/go-toml"
	"github.com/urfave/cli/v2"

	"github.com/erigontech/erigon/common/dir"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/state"
)

func stepRebase(cliCtx *cli.Context) error {
	logger := log.Root()

	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	dirs := datadir.Open(cliCtx.String("datadir"))
	settings, err := state.ResolveErigonDBSettings(dirs, logger, true)
	if err != nil {
		return err
	}

	currentStepSize := settings.StepSize
	newStepSize := cliCtx.Uint64("new-step-size")
	logger.Info("Rebasing step size", "current", currentStepSize, "new", newStepSize)

	if newStepSize == currentStepSize {
		logger.Info("Step size is already at the desired value; exiting", "new-step-size", newStepSize)
		return nil
	}
	if newStepSize > currentStepSize && newStepSize%currentStepSize != 0 {
		logger.Crit("Invalid step size", "new-step-size", newStepSize)
		return fmt.Errorf("new step size must be a multiple of %d", currentStepSize)
	} else if currentStepSize > newStepSize && currentStepSize%newStepSize != 0 {
		logger.Crit("Invalid step size", "new-step-size", newStepSize)
		return fmt.Errorf("new step size must be divisible from %d", currentStepSize)
	}

	factor := newStepSize / currentStepSize
	decr := newStepSize < currentStepSize
	if decr {
		factor = currentStepSize / newStepSize
		logger.Info("Decreasing step size", "factor", factor)
	} else {
		logger.Info("Increasing step size", "factor", factor)
	}

	// Look for files to be renamed
	re := regexp.MustCompile(`^v(\d+(?:\.\d+)?)-(.+)\.(\d+)-(\d+)\.([^.]+)$`)

	rens := make([]string, 0, 100)
	domains, err := collectRenameList(dirs.SnapDomain, re, decr, factor)
	if err != nil {
		return err
	}
	hists, err := collectRenameList(dirs.SnapHistory, re, decr, factor)
	if err != nil {
		return err
	}
	accessors, err := collectRenameList(dirs.SnapAccessors, re, decr, factor)
	if err != nil {
		return err
	}
	idxs, err := collectRenameList(dirs.SnapIdx, re, decr, factor)
	if err != nil {
		return err
	}
	rens = append(rens, domains...)
	rens = append(rens, hists...)
	rens = append(rens, accessors...)
	rens = append(rens, idxs...)

	// List files to be renamed
	for i := 0; i < len(rens); i += 2 {
		fmt.Printf("R: %s => %s\n", rens[i], rens[i+1])
	}

	// Collect and list .torrent files to be deleted
	dels := make([]string, 0, 100)
	domainTorrents, err := collectTorrentFiles(dirs.SnapDomain)
	if err != nil {
		return err
	}
	histTorrents, err := collectTorrentFiles(dirs.SnapHistory)
	if err != nil {
		return err
	}
	accessorTorrents, err := collectTorrentFiles(dirs.SnapAccessors)
	if err != nil {
		return err
	}
	idxTorrents, err := collectTorrentFiles(dirs.SnapIdx)
	if err != nil {
		return err
	}
	dels = append(dels, domainTorrents...)
	dels = append(dels, histTorrents...)
	dels = append(dels, accessorTorrents...)
	dels = append(dels, idxTorrents...)

	// include whole chaindata directory for deletion
	dels = append(dels, dirs.Chaindata) //nolint:gocritic

	// include erigondb.toml.torrent which is invalidated by the rebase
	dels = append(dels, filepath.Join(dirs.Snap, "erigondb.toml.torrent"))

	for _, f := range dels {
		fmt.Printf("D: %s\n", f)
	}

	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Proceed with these changes? [y/N]: ")
	answer, _ := reader.ReadString('\n')
	answer = strings.TrimSpace(strings.ToLower(answer))
	if answer != "y" && answer != "yes" {
		return fmt.Errorf("aborted by user")
	}

	// Perform renames
	for i := 0; i < len(rens); i += 2 {
		from := rens[i]
		to := rens[i+1]
		if err := os.Rename(from, to); err != nil {
			return fmt.Errorf("rename %s -> %s: %w", from, to, err)
		}
		fmt.Printf("Renamed: %s => %s\n", from, to)
	}

	// Perform deletions
	for _, p := range dels {
		if err := dir.RemoveAll(p); err != nil {
			return fmt.Errorf("delete %s: %w", p, err)
		}
		fmt.Printf("Deleted: %s\n", p)
	}

	// Write rebased settings
	settings.StepSize = newStepSize
	newFrozenSteps := settings.StepsInFrozenFile / factor
	if decr {
		newFrozenSteps = settings.StepsInFrozenFile * factor
	}
	settings.StepsInFrozenFile = newFrozenSteps

	settingsBytes, err := toml.Marshal(settings)
	if err != nil {
		return err
	}
	err = os.WriteFile(filepath.Join(dirs.Snap, state.ERIGONDB_SETTINGS_FILE), settingsBytes, 0644)
	if err != nil {
		return err
	}

	return nil
}

// collectRenameList walks the snapshot domain directory and builds a list of
// from/to file paths to be renamed according to the new step size rules.
func collectRenameList(domainPath string, re *regexp.Regexp, decr bool, factor uint64) ([]string, error) {
	renList := make([]string, 0, 100)

	walkErr := fs.WalkDir(os.DirFS(domainPath), ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !re.MatchString(path) {
			return nil
		}
		matches := re.FindStringSubmatch(path)
		from, err := strconv.ParseUint(matches[3], 10, 64)
		if err != nil {
			return err
		}
		to, err := strconv.ParseUint(matches[4], 10, 64)
		if err != nil {
			return err
		}

		// decreasing step size -> 1 step will contain more steps;
		// increasing step size -> 1 step will be a fraction of new steps -> datadir may not contain a full step!
		if decr {
			from *= factor
			to *= factor
		} else {
			from /= factor
			to /= factor
		}

		newPath := fmt.Sprintf("v%s-%s.%d-%d.%s", matches[1], matches[2], from, to, matches[5])
		renList = append(renList, filepath.Join(domainPath, path), filepath.Join(domainPath, newPath))

		return nil
	})

	if walkErr != nil {
		return nil, walkErr
	}
	return renList, nil
}

// collectTorrentFiles walks given root and returns absolute paths to files ending with .torrent
func collectTorrentFiles(root string) ([]string, error) {
	list := make([]string, 0, 100)
	walkErr := fs.WalkDir(os.DirFS(root), ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		if strings.HasSuffix(path, ".torrent") {
			list = append(list, filepath.Join(root, path))
		}
		return nil
	})
	if walkErr != nil {
		return nil, walkErr
	}
	return list, nil
}
