package sborepo

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

	"gitlab.com/M0M097/pkg/cli"
	"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. It
// panics if the package is not found.
func (repo RepoMap) GetPkg(packageName string) *Package {
	if parentPath, ok := repo[packageName]; ok {
		return &Package{Name: packageName, ParentPath: parentPath}
	}
	panic(cli.Banner(
		"Fatal Error", "Package not found in the repository: "+packageName, 80))
}

// Contains checks if the package with the given name exists in the repository.
// It returns true if the package exists, false otherwise.
func (repo RepoMap) Contains(packageName string) bool { _, ok := repo[packageName]; return ok }

// IterOver iterates over the provided package names and applies the function
// f to each package. It panics if a package is not found in the repository.
func (repo RepoMap) IterOver(packageNames []string, f func(*Package)) {
	for _, packageName := range packageNames {
		f(repo.GetPkg(packageName))
	}
}
