package sborepo

import (
	"fmt"
	"io/fs"
	"path/filepath"
	"regexp"
	"sync"

	"gitlab.com/M0M097/pkg/internal/must"
)

// RepoMap maps package names to their parent paths in a parsed SlackBuilds
// repository. The map value is the package's ParentPath; GetPkg reconstructs a
// Package from the map key and value. Callers may add packages directly by
// assigning repo[name] = parentPath.
type RepoMap map[string]string

// NewRepoMap creates a new RepoMap from the given repository path.
// repoPath must be the path to a directory two levels above the packages,
// i.e. if the packages live at /path/to/repo/categories/package_name,
// repoPath should be /path/to/repo/.
//
// Since this routine is usually the bottleneck, it is parallelized.
func NewRepoMap(repoPath string) RepoMap {

	var (
		wg   sync.WaitGroup
		mu   sync.Mutex
		repo = make(RepoMap, 10100)
	)

	for _, file := range must.ReadDir(repoPath) {
		// We parallelize over the `categories` in the repository
		wg.Add(1)
		go func(file fs.DirEntry) {
			defer wg.Done()

			if file.Name()[0] == '.' || !file.IsDir() {
				return
			}

			var (
				subdirPath = filepath.Join(repoPath, file.Name())
				subfiles   = must.ReadDir(subdirPath)
			)

			names := make([]string, 0, len(subfiles))
			for _, subfile := range subfiles {
				names = append(names, subfile.Name())
			}

			mu.Lock() // Merge
			for _, name := range names {
				repo[name] = subdirPath
			}
			mu.Unlock()
		}(file)
	}

	wg.Wait()
	return repo
}

// GetPkg returns the package with the given name from the repository and true
// if it was found in the RepoMap. Otherwise Package.ParentPath is empty and
// the second return value is false.
func (repo RepoMap) GetPkg(packageName string) (*Package, bool) {
	parentPath, ok := repo[packageName]
	return &Package{Name: packageName, ParentPath: parentPath}, ok
}

// IterOver returns a PkgIter over the provided package names and an error
// listing any package names that were not found.
func (repo RepoMap) IterOver(packageNames []string) (PkgIter, error) {
	var (
		found    = make([]*Package, 0, len(packageNames))
		notFound []string
	)
	for _, packageName := range packageNames {
		if pkg, ok := repo.GetPkg(packageName); !ok {
			notFound = append(notFound, packageName)
		} else {
			found = append(found, pkg)
		}
	}
	iter := func(f PkgFunc) {
		for _, pkg := range found {
			f(pkg)
		}
	}
	if len(notFound) > 0 {
		return iter, fmt.Errorf("packages not found in repository: %v", notFound)
	}
	return iter, nil
}

// Search returns an iterator over packages whose names match regex and an
// error if the regex cannot be compiled.
func (repo RepoMap) Search(regex string) (PkgIter, error) {
	re, err := regexp.Compile(regex)
	if err != nil {
		return nil, err
	}
	return func(f PkgFunc) {
		for packageName := range repo {
			if re.MatchString(packageName) {
				pkg, _ := repo.GetPkg(packageName)
				f(pkg)
			}
		}
	}, nil
}

// Installed returns an iterator over packages from repo that have entries in
// docDir.
func (repo RepoMap) Installed(docDir string) PkgIter {
	return func(f PkgFunc) {
		for _, file := range must.ReadDir(docDir) {
			pName := PackageNameFromFullName(file.Name())
			if pkg, ok := repo[pName]; ok {
				f(&Package{Name: pName, ParentPath: pkg})
			}
		}
	}
}
