// Package cleaner is a background process that compares the kubernetes namespace list with the
// folders in the local git home directory, deleting what's not in the namespace list.
package cleaner

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
	"github.com/drycc/builder/pkg/gitreceive"
	"github.com/drycc/builder/pkg/k8s"
	"github.com/drycc/builder/pkg/sys"
	"github.com/drycc/pkg/log"
	corev1 "k8s.io/api/core/v1"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	dotGitSuffix = ".git"
)

// localDirs returns all of the local directories immediately under gitHome that filter returns true for.
// filter will receive only the names of each of the top level directories (not their fully qualified paths), and should return true if it should be included in the output
func localDirs(gitHome string, filter func(string) bool) ([]string, error) {
	fileInfos, err := os.ReadDir(gitHome)
	if err != nil {
		return nil, err
	}
	var ret []string
	for _, fileInfo := range fileInfos {
		nm := fileInfo.Name()
		if len(nm) <= 0 || nm == "." || !fileInfo.IsDir() {
			continue
		}
		if filter(nm) {
			ret = append(ret, nm)
		}
	}
	return ret, nil
}

// getDiff gets the directories that are not in namespaceList
func getDiff(namespaceList []corev1.Namespace, dirs []string) []string {
	var ret []string

	// create a set of lowercase namespace names
	namespacesSet := make(map[string]struct{})
	for _, ns := range namespaceList {
		lowerName := strings.ToLower(ns.Name)
		namespacesSet[lowerName] = struct{}{}
	}

	// get dirs not in the namespaces set
	for _, dir := range dirs {
		lowerName := strings.ToLower(dir)
		if _, ok := namespacesSet[lowerName]; !ok {
			ret = append(ret, lowerName)
		}
	}

	return ret
}

func stripSuffixes(strs []string, suffix string) []string {
	ret := make([]string, len(strs))
	for i, str := range strs {
		idx := strings.LastIndex(str, suffix)
		if idx >= 0 {
			ret[i] = str[:idx]
		} else {
			ret[i] = str
		}
	}
	return ret
}

func dirHasGitSuffix(dir string) bool {
	return strings.HasSuffix(dir, dotGitSuffix)
}

func deleteFromStorage(app string, storageDriver storagedriver.StorageDriver) error {

	// delete all files matching app
	objs, err := storageDriver.List(context.Background(), "home")
	if err != nil {
		return err
	}

	// regex needs prepended / to match output of List()
	gitRegex, err := regexp.Compile(`^/` + fmt.Sprintf(gitreceive.GitKeyPattern, app, ".{8}") + "$")
	if err != nil {
		return err
	}

	for _, obj := range objs {
		if gitRegex.MatchString(obj) {
			log.Info("Cleaner deleting %s for app %s", obj, app)
			if err := storageDriver.Delete(context.Background(), obj); err != nil {
				return err
			}
		}
	}
	return nil
}

// Run starts the deleted app cleaner. Every pollSleepDuration, it compares the result of nsLister.List with the directories in the top level of gitHome on the local file system.
// On any error, it uses log messages to output a human readable description of what happened.
func Run(gitHome string, nsLister k8s.NamespaceLister, fs sys.FS, pollSleepDuration time.Duration, storageDriver storagedriver.StorageDriver) error {
	for {
		nsList, err := nsLister.List(context.TODO(), metav1.ListOptions{})
		if err != nil {
			log.Err("Cleaner error listing namespaces (%s)", err)
			continue
		}

		gitDirs, err := localDirs(gitHome, dirHasGitSuffix)
		if err != nil {
			log.Err("Cleaner error listing local git directories (%s)", err)
			continue
		}

		gitDirs = stripSuffixes(gitDirs, dotGitSuffix)

		appsToDelete := getDiff(nsList.Items, gitDirs)

		for _, appToDelete := range appsToDelete {
			dirToDelete := filepath.Join(gitHome, appToDelete+dotGitSuffix)
			if err := fs.RemoveAll(dirToDelete); err != nil {
				log.Err("Cleaner error removing local files for deleted app %s (%s)", dirToDelete, err)
			}
			if err := deleteFromStorage(appToDelete, storageDriver); err != nil {
				log.Err("Cleaner error removing object store files for deleted app %s (%s)", appToDelete, err)
			}
		}

		time.Sleep(pollSleepDuration)
	}
}
