package aghos

import (
	"fmt"
	"io"
	"io/fs"

	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/stringutil"
)

// FileWalker is the signature of a function called for files in the file tree.
// As opposed to filepath.Walk it only walk the files (not directories) matching
// the provided pattern and those returned by function itself.  All patterns
// should be valid for fs.Glob.  If FileWalker returns false for cont then
// walking terminates.  Prefer using bufio.Scanner to read the r since the input
// is not limited.
//
// TODO(e.burkov, a.garipov):  Move into another package like aghfs.
//
// TODO(e.burkov):  Think about passing filename or any additional data.
type FileWalker func(r io.Reader) (patterns []string, cont bool, err error)

// checkFile tries to open and process a single file located on sourcePath in
// the specified fsys.  The path is skipped if it's a directory.
func checkFile(
	fsys fs.FS,
	c FileWalker,
	sourcePath string,
) (patterns []string, cont bool, err error) {
	var f fs.File
	f, err = fsys.Open(sourcePath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			// Ignore non-existing files since this may only happen when the
			// file was removed after filepath.Glob matched it.
			return nil, true, nil
		}

		return nil, false, err
	}
	defer func() { err = errors.WithDeferred(err, f.Close()) }()

	var fi fs.FileInfo
	if fi, err = f.Stat(); err != nil {
		return nil, true, err
	} else if fi.IsDir() {
		// Skip the directories.
		return nil, true, nil
	}

	return c(f)
}

// handlePatterns parses the patterns in fsys and ignores duplicates using
// srcSet.  srcSet must be non-nil.
func handlePatterns(
	fsys fs.FS,
	srcSet *stringutil.Set,
	patterns ...string,
) (sub []string, err error) {
	sub = make([]string, 0, len(patterns))
	for _, p := range patterns {
		var matches []string
		matches, err = fs.Glob(fsys, p)
		if err != nil {
			// Enrich error with the pattern because filepath.Glob
			// doesn't do it.
			return nil, fmt.Errorf("invalid pattern %q: %w", p, err)
		}

		for _, m := range matches {
			if srcSet.Has(m) {
				continue
			}

			srcSet.Add(m)
			sub = append(sub, m)
		}
	}

	return sub, nil
}

// Walk starts walking the files in fsys defined by patterns from initial.
// It only returns true if fw signed to stop walking.
func (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {
	// The slice of sources keeps the order in which the files are walked since
	// srcSet.Values() returns strings in undefined order.
	srcSet := stringutil.NewSet()
	var src []string
	src, err = handlePatterns(fsys, srcSet, initial...)
	if err != nil {
		return false, err
	}

	var filename string
	defer func() { err = errors.Annotate(err, "checking %q: %w", filename) }()

	for i := 0; i < len(src); i++ {
		var patterns []string
		var cont bool
		filename = src[i]
		patterns, cont, err = checkFile(fsys, fw, src[i])
		if err != nil {
			return false, err
		}

		if !cont {
			return true, nil
		}

		var subsrc []string
		subsrc, err = handlePatterns(fsys, srcSet, patterns...)
		if err != nil {
			return false, err
		}

		src = append(src, subsrc...)
	}

	return false, nil
}
