package recovery

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
)

var (
	reChanges  = regexp.MustCompile(`(\d+)\.changes`)
	reSnapshot = regexp.MustCompile(`(\d+)\.snapshot`)
)

func joinChangesFileName(dir string, logNumber int) string {
	return filepath.Join(dir, fmt.Sprintf("%d.changes", logNumber))
}

type RecoveryRecipe struct {
	Snapshot         string
	Changes          []string
	LogNumber        int
	ToDelete         []string
	CompleteSnapshot bool // флаг - что нужно завершить создание снапшота
}

type RecoveryAdvisor struct {
	dir            string
	verifySnapshot func(string) (bool, error) // (fileName) isVerified, error
}

type RecoveryAdvisorOptions struct {
	Dir            string
	VerifySnapshot func(string) (bool, error)
}

func NewRecoveryAdvisor(opt RecoveryAdvisorOptions) (*RecoveryAdvisor, error) {
	if opt.Dir == "" {
		return nil, errors.New("Dir option is required")
	}
	if opt.VerifySnapshot == nil {
		return nil, errors.New("VerifySnapshot option is required")
	}
	return &RecoveryAdvisor{
		dir:            opt.Dir,
		verifySnapshot: opt.VerifySnapshot,
	}, nil
}

type SnapshotChangesPair struct {
	SnapshotFileName string
	ChangesFileName  string
	LogNumber        int
}

func (s *RecoveryAdvisor) getSnapshotChangesPairs() (*SnapshotChangesPair, *SnapshotChangesPair, error) {
	var (
		numSet       = make(map[int]bool)
		changesSet   = make(map[int]bool)
		snapshotsSet = make(map[int]bool)
		pairs        []SnapshotChangesPair
	)

	entries, err := os.ReadDir(s.dir)
	if err != nil {
		return nil, nil, err
	}

	for _, entry := range entries {
		if entry.Type().IsRegular() {
			baseName := entry.Name()
			groups := reChanges.FindStringSubmatch(baseName)
			if len(groups) == 2 {
				num, _ := strconv.Atoi(groups[1])

				numSet[num] = true
				changesSet[num] = true
			}
			groups = reSnapshot.FindStringSubmatch(baseName)
			if len(groups) == 2 {
				num, _ := strconv.Atoi(groups[1])

				numSet[num] = true
				snapshotsSet[num] = true
			}
		}
	}

	for logNumber := range numSet {
		var (
			snapshotFileName string
			changesFileName  string
		)
		if changesSet[logNumber] {
			changesFileName = joinChangesFileName(s.dir, logNumber)
		}
		if snapshotsSet[logNumber] {
			snapshotFileName = filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", logNumber))
		}
		pairs = append(pairs, SnapshotChangesPair{
			ChangesFileName:  changesFileName,
			SnapshotFileName: snapshotFileName,
			LogNumber:        logNumber,
		})
	}

	if len(pairs) == 0 {
		return nil, nil, nil
	}

	sort.Slice(pairs, func(i, j int) bool {
		return pairs[i].LogNumber > pairs[j].LogNumber
	})

	pair := pairs[0]
	if pair.ChangesFileName == "" {
		return nil, nil, fmt.Errorf("has %d.shapshot file, but %d.changes file not found",
			pair.LogNumber, pair.LogNumber)
	}

	if len(pairs) > 1 {
		prevPair := pairs[1]
		if prevPair.SnapshotFileName == "" && prevPair.LogNumber != 1 {
			return &pair, nil, nil
		}

		if prevPair.ChangesFileName == "" && pair.SnapshotFileName == "" {
			return &pair, nil, nil
		}
		return &pair, &prevPair, nil
	} else {
		return &pair, nil, nil
	}
}

func (s *RecoveryAdvisor) GetRecipe() (*RecoveryRecipe, error) {
	pair, prevPair, err := s.getSnapshotChangesPairs()
	if err != nil {
		return nil, err
	}

	if pair == nil {
		return nil, nil
	}

	if pair.SnapshotFileName != "" {
		isVerified, err := s.verifySnapshot(pair.SnapshotFileName)
		if err != nil {
			return nil, fmt.Errorf("verifySnapshot %s: %s",
				pair.SnapshotFileName, err)
		}

		if isVerified {
			recipe := &RecoveryRecipe{
				Snapshot: pair.SnapshotFileName,
				Changes: []string{
					pair.ChangesFileName,
				},
				LogNumber: pair.LogNumber,
			}

			if prevPair != nil {
				if prevPair.ChangesFileName != "" {
					recipe.ToDelete = append(recipe.ToDelete, prevPair.ChangesFileName)
				}
				if prevPair.SnapshotFileName != "" {
					recipe.ToDelete = append(recipe.ToDelete, prevPair.SnapshotFileName)
				}
			}
			return recipe, nil
		}
		if prevPair != nil {
			return s.tryPrevPair(pair, prevPair)
		}
		return nil, fmt.Errorf("%d.shapshot is corrupted", pair.LogNumber)
	} else {
		if prevPair != nil {
			return s.tryPrevPair(pair, prevPair)
		} else {
			if pair.LogNumber == 1 {
				return &RecoveryRecipe{
					Changes: []string{
						pair.ChangesFileName,
					},
					LogNumber: pair.LogNumber,
				}, nil
			} else {
				return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber)
			}
		}
	}
}

func (s *RecoveryAdvisor) tryPrevPair(pair, prevPair *SnapshotChangesPair) (*RecoveryRecipe, error) {
	if prevPair.ChangesFileName == "" {
		if pair.SnapshotFileName != "" {
			return nil, fmt.Errorf("%d.shapshot is corrupted and %d.changes not found",
				pair.LogNumber, prevPair.LogNumber)
		} else {
			return nil, fmt.Errorf("%d.changes not found", prevPair.LogNumber)
		}
	}

	if prevPair.SnapshotFileName == "" {
		if prevPair.LogNumber == 1 {
			recipe := &RecoveryRecipe{
				Changes: []string{
					prevPair.ChangesFileName,
					pair.ChangesFileName,
				},
				LogNumber:        pair.LogNumber,
				CompleteSnapshot: true,
				ToDelete: []string{
					prevPair.ChangesFileName,
				},
			}
			return recipe, nil
		} else {
			if pair.SnapshotFileName != "" {
				return nil, fmt.Errorf("%d.shapshot is corrupted and %d.snapshot not found",
					pair.LogNumber, prevPair.LogNumber)
			} else {
				return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber)
			}
		}
	}

	isVerified, err := s.verifySnapshot(prevPair.SnapshotFileName)
	if err != nil {
		return nil, fmt.Errorf("verifySnapshot %s: %s",
			prevPair.SnapshotFileName, err)
	}

	if !isVerified {
		return nil, fmt.Errorf("%d.shapshot is corrupted", prevPair.LogNumber)
	}

	recipe := &RecoveryRecipe{
		Snapshot: prevPair.SnapshotFileName,
		Changes: []string{
			prevPair.ChangesFileName,
			pair.ChangesFileName,
		},
		LogNumber:        pair.LogNumber,
		CompleteSnapshot: true,
		ToDelete: []string{
			prevPair.ChangesFileName,
			prevPair.SnapshotFileName,
		},
	}
	return recipe, nil
}
