// Package finder looks for files and directories in an {fs.Fs} filesystem.
package locafero

import (
	"errors"
	"io/fs"
	"path/filepath"
	"strings"

	"github.com/sourcegraph/conc/iter"
	"github.com/spf13/afero"
)

// Finder looks for files and directories in an [afero.Fs] filesystem.
type Finder struct {
	// Paths represents a list of locations that the [Finder] will search in.
	//
	// They are essentially the root directories or starting points for the search.
	//
	// Examples:
	//   - home/user
	//   - etc
	Paths []string

	// Names are specific entries that the [Finder] will look for within the given Paths.
	//
	// It provides the capability to search for entries with depth,
	// meaning it can target deeper locations within the directory structure.
	//
	// It also supports glob syntax (as defined by [filepat.Match]), offering greater flexibility in search patterns.
	//
	// Examples:
	//   - config.yaml
	//   - home/*/config.yaml
	//   - home/*/config.*
	Names []string

	// Type restricts the kind of entries returned by the [Finder].
	//
	// This parameter helps in differentiating and filtering out files from directories or vice versa.
	Type FileType
}

// Find looks for files and directories in an [afero.Fs] filesystem.
func (f Finder) Find(fsys afero.Fs) ([]string, error) {
	// Arbitrary go routine limit (TODO: make this a parameter)
	// pool := pool.NewWithResults[[]string]().WithMaxGoroutines(5).WithErrors().WithFirstError()

	type searchItem struct {
		path string
		name string
	}

	var searchItems []searchItem

	for _, searchPath := range f.Paths {
		searchPath := searchPath

		for _, searchName := range f.Names {
			searchName := searchName

			searchItems = append(searchItems, searchItem{searchPath, searchName})

			// pool.Go(func() ([]string, error) {
			// 	// If the name contains any glob character, perform a glob match
			// 	if strings.ContainsAny(searchName, "*?[]\\^") {
			// 		return globWalkSearch(fsys, searchPath, searchName, f.Type)
			// 	}
			//
			// 	return statSearch(fsys, searchPath, searchName, f.Type)
			// })
		}
	}

	// allResults, err := pool.Wait()
	// if err != nil {
	// 	return nil, err
	// }

	allResults, err := iter.MapErr(searchItems, func(item *searchItem) ([]string, error) {
		// If the name contains any glob character, perform a glob match
		if strings.ContainsAny(item.name, "*?[]\\^") {
			return globWalkSearch(fsys, item.path, item.name, f.Type)
		}

		return statSearch(fsys, item.path, item.name, f.Type)
	})
	if err != nil {
		return nil, err
	}

	var results []string

	for _, r := range allResults {
		results = append(results, r...)
	}

	// Sort results in alphabetical order for now
	// sort.Strings(results)

	return results, nil
}

func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
	var results []string

	err := afero.Walk(fsys, searchPath, func(p string, fileInfo fs.FileInfo, err error) error {
		if err != nil {
			return err
		}

		// Skip the root path
		if p == searchPath {
			return nil
		}

		var result error

		// Stop reading subdirectories
		// TODO: add depth detection here
		if fileInfo.IsDir() && filepath.Dir(p) == searchPath {
			result = fs.SkipDir
		}

		// Skip unmatching type
		if !searchType.matchFileInfo(fileInfo) {
			return result
		}

		match, err := filepath.Match(searchName, fileInfo.Name())
		if err != nil {
			return err
		}

		if match {
			results = append(results, p)
		}

		return result
	})
	if err != nil {
		return results, err
	}

	return results, nil
}

func statSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
	filePath := filepath.Join(searchPath, searchName)

	fileInfo, err := fsys.Stat(filePath)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	// Skip unmatching type
	if !searchType.matchFileInfo(fileInfo) {
		return nil, nil
	}

	return []string{filePath}, nil
}
