package lint

import (
	"fmt"
	"regexp"
	"strings"
)

// FileFilter filters file to exclude some files for a rule.
// Supports the following:
//   - File or directory names: pkg/mypkg/my.go
//   - Globs: **/*.pb.go,
//   - Regexes (with ~ prefix): ~-tmp\.\d+\.go
//   - Special test marker `TEST` (treated as `~_test\.go`).
type FileFilter struct {
	// raw definition of filter inside config
	raw string
	// don't care what was at start, will use regexes inside
	rx *regexp.Regexp
	// marks filter as matching everything
	matchesAll bool
	// marks filter as matching nothing
	matchesNothing bool
}

// ParseFileFilter creates a [FileFilter] for the given raw filter.
// If the string is empty, it matches nothing.
// If the string is `*` or `~`, it matches everything.
// If the regular expression is invalid, it returns a compilation error.
func ParseFileFilter(rawFilter string) (*FileFilter, error) {
	rawFilter = strings.TrimSpace(rawFilter)
	result := new(FileFilter)
	result.raw = rawFilter
	result.matchesNothing = result.raw == ""
	result.matchesAll = result.raw == "*" || result.raw == "~"
	if !result.matchesAll && !result.matchesNothing {
		if err := result.prepareRegexp(); err != nil {
			return nil, err
		}
	}
	return result, nil
}

func (ff *FileFilter) String() string { return ff.raw }

// MatchFileName checks if the file name matches the filter.
func (ff *FileFilter) MatchFileName(name string) bool {
	if ff.matchesAll {
		return true
	}
	if ff.matchesNothing {
		return false
	}
	name = strings.ReplaceAll(name, "\\", "/")
	return ff.rx.MatchString(name)
}

var (
	fileFilterInvalidGlobRegexp = regexp.MustCompile(`[^/]\*\*[^/]`)
	escapeRegexSymbols          = ".+{}()[]^$"
)

func (ff *FileFilter) prepareRegexp() error {
	var err error
	src := ff.raw
	if src == "TEST" {
		src = "~_test\\.go"
	}
	if strings.HasPrefix(src, "~") {
		ff.rx, err = regexp.Compile(src[1:])
		if err != nil {
			return fmt.Errorf("invalid file filter [%s], regexp compile error: [%w]", ff.raw, err)
		}
		return nil
	}
	/* globs */
	if strings.Contains(src, "*") {
		if fileFilterInvalidGlobRegexp.MatchString(src) {
			return fmt.Errorf("invalid file filter [%s], invalid glob pattern", ff.raw)
		}
		var rxBuild strings.Builder
		rxBuild.WriteByte('^')
		wasStar := false
		justDirGlob := false
		for _, c := range src {
			if c == '*' {
				if wasStar {
					rxBuild.WriteString(`[\s\S]*`)
					wasStar = false
					justDirGlob = true
					continue
				}
				wasStar = true
				continue
			}
			if wasStar {
				rxBuild.WriteString("[^/]*")
				wasStar = false
			}
			if strings.ContainsRune(escapeRegexSymbols, c) {
				rxBuild.WriteByte('\\')
			}
			rxBuild.WriteRune(c)
			if c == '/' && justDirGlob {
				rxBuild.WriteRune('?')
			}
			justDirGlob = false
		}
		if wasStar {
			rxBuild.WriteString("[^/]*")
		}
		rxBuild.WriteByte('$')
		ff.rx, err = regexp.Compile(rxBuild.String())
		if err != nil {
			return fmt.Errorf("invalid file filter [%s], regexp compile error after glob expand: [%w]", ff.raw, err)
		}
		return nil
	}

	// it's whole file mask, just escape dots and normalize separators
	fillRx := src
	fillRx = strings.ReplaceAll(fillRx, "\\", "/")
	fillRx = strings.ReplaceAll(fillRx, ".", `\.`)
	fillRx = "^" + fillRx + "$"
	ff.rx, err = regexp.Compile(fillRx)
	if err != nil {
		return fmt.Errorf("invalid file filter [%s], regexp compile full path: [%w]", ff.raw, err)
	}
	return nil
}
