// FindPilot — fast filename + content search across a directory tree.
//
// Usage:
//
//	findpilot search <root> [--name GLOB] [--content PATTERN] [--regex]
//	                         [--ignore-case] [--max N]
//
// Combines a filename glob filter with a grep-style content search in one
// pass: with both set, only files whose name matches --name are searched
// for --content. Binary files (a NUL byte in the first 8KB) are skipped
// automatically for content search.
package main

import (
	"bufio"
	"bytes"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// questions the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}
	switch os.Args[1] {
	case "search":
		cmdSearch(os.Args[2:])
	case "-h", "--help", "help":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `FindPilot — fast filename + content search

Usage:
  findpilot search <root> [--name GLOB] [--content PATTERN] [--regex] [--ignore-case] [--max N]

At least one of --name / --content is required.
`)
}

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

func cmdSearch(args []string) {
	fs := flag.NewFlagSet("search", flag.ExitOnError)
	namePattern := fs.String("name", "", "filename glob, e.g. *.go")
	contentPattern := fs.String("content", "", "content search pattern")
	useRegex := fs.Bool("regex", false, "treat --content as a regular expression")
	ignoreCase := fs.Bool("ignore-case", false, "case-insensitive content match")
	max := fs.Int("max", 200, "maximum matches to print")
	fs.Parse(reorderFlags(args, map[string]bool{"name": true, "content": true, "max": true}))
	pos := fs.Args()
	if len(pos) != 1 || (*namePattern == "" && *contentPattern == "") {
		fmt.Fprintln(os.Stderr, "usage: findpilot search <root> [--name GLOB] [--content PATTERN] [--regex] [--ignore-case] [--max N]")
		os.Exit(1)
	}
	root := pos[0]

	var re *regexp.Regexp
	var plain string
	if *contentPattern != "" {
		if *useRegex {
			pattern := *contentPattern
			if *ignoreCase {
				pattern = "(?i)" + pattern
			}
			var err error
			re, err = regexp.Compile(pattern)
			if err != nil {
				fmt.Fprintln(os.Stderr, "invalid regex:", err)
				os.Exit(1)
			}
		} else {
			plain = *contentPattern
			if *ignoreCase {
				plain = strings.ToLower(plain)
			}
		}
	}

	var filesScanned, filesMatchedByName, contentMatches int
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() || !info.Mode().IsRegular() {
			return nil
		}
		if contentMatches >= *max {
			return nil
		}
		if *namePattern != "" {
			ok, _ := filepath.Match(*namePattern, filepath.Base(path))
			if !ok {
				return nil
			}
		}
		filesMatchedByName++

		if *contentPattern == "" {
			fmt.Println(path)
			return nil
		}

		filesScanned++
		f, err := os.Open(path)
		if err != nil {
			return nil
		}
		defer f.Close()

		head := make([]byte, 8192)
		n, _ := f.Read(head)
		if bytes.IndexByte(head[:n], 0) != -1 {
			return nil // looks binary, skip content search
		}
		f.Seek(0, 0)

		scanner := bufio.NewScanner(f)
		scanner.Buffer(make([]byte, 64*1024), 1024*1024)
		lineNum := 0
		for scanner.Scan() {
			lineNum++
			line := scanner.Text()
			matched := false
			if re != nil {
				matched = re.MatchString(line)
			} else {
				hay := line
				if *ignoreCase {
					hay = strings.ToLower(hay)
				}
				matched = strings.Contains(hay, plain)
			}
			if matched {
				fmt.Printf("%s:%d: %s\n", path, lineNum, strings.TrimSpace(line))
				contentMatches++
				if contentMatches >= *max {
					break
				}
			}
		}
		return nil
	})
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}

	if *contentPattern != "" {
		fmt.Printf("\n%d content match(es) across %d file(s) scanned (%d matched --name filter)\n", contentMatches, filesScanned, filesMatchedByName)
		if contentMatches >= *max {
			fmt.Println("(hit --max, results truncated)")
		}
	} else {
		fmt.Printf("\n%d file(s) matched\n", filesMatchedByName)
	}
}
