package sborepo

import (
	"fmt"
	"io/fs"
	"path/filepath"
	"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 iterates over the provided package names and applies f to each
// package found in the repository. It returns an error listing any package names
// that were not found.
func (repo RepoMap) IterOver(packageNames []string, f func(*Package)) error {
	var notFound []string
	for _, packageName := range packageNames {
		pkg, ok := repo.GetPkg(packageName)
		if !ok {
			notFound = append(notFound, packageName)
			continue
		}
		f(pkg)
	}
	if len(notFound) > 0 {
		return fmt.Errorf("packages not found in repository: %v", notFound)
	}
	return nil
}
