package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// runGuided is what happens when somebody double-clicks the program instead of
// typing its name at a prompt.
//
// Without this, Explorer opens a console, main() finds no arguments, prints
// the usage text to stderr and exits — and Windows destroys the window in the
// same instant. From the other side of the screen that is indistinguishable
// from a crash. So when we know we were double-clicked, we ask the two things
// a search needs and stay on screen until the reader is done.
//
// Building an index rewrites a file on disk, so the guided session does not do
// that: it searches an index that already exists. Looking a word up touches
// nothing at all — not the index, not the files it describes.
//
// This path is entered ONLY when there are no arguments and both ends of the
// program are a real console. Any scripted or piped use takes exactly the same
// code path it always did.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  SearchForge")
	fmt.Println("  Find your files by what is inside them, instantly.")
	fmt.Println()
	fmt.Println("  SearchForge reads a folder once and remembers every word it found, in")
	fmt.Println("  a catalogue file. After that, searching is a look-up in that catalogue")
	fmt.Println("  rather than a fresh trawl through the disk, so answers come back at")
	fmt.Println("  once however big the folder is.")
	fmt.Println()
	fmt.Println("  This window searches a catalogue you already have. Nothing is changed:")
	fmt.Println("  the catalogue is only read, and your files are not opened at all.")
	fmt.Println()

	catalogue, ok := askCatalogue(in, suggestedCatalogue())
	if !ok {
		return
	}

	fmt.Println()
	fmt.Printf("  Searching %s\n", catalogue)

	terms, ok := askTerms(in)
	if !ok {
		return
	}

	fmt.Println()
	cmdQuery(append([]string{"--index", catalogue}, terms...))

	fmt.Println()
	fmt.Println("  Nothing was changed. SearchForge only read the catalogue.")
	fmt.Println("  To search again, to build a catalogue for another folder, or to bring")
	fmt.Println("  this one up to date, run SearchForge from a command prompt:")
	fmt.Println("  searchforge help")
	pause(in)
}

// askCatalogue asks which catalogue to search and keeps asking until it gets
// one that exists and can really be opened.
func askCatalogue(in *bufio.Scanner, suggested string) (string, bool) {
	for {
		fmt.Println("  Which catalogue shall I search?")
		fmt.Println("  It is the file SearchForge wrote when it read your folder. You can")
		fmt.Println("  drag it, or the folder holding it, from Explorer onto this window")
		fmt.Println("  to paste the location.")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Print("  > ")

		if !in.Scan() {
			// stdin closed on us; there is nothing sensible left to ask.
			return "", false
		}
		answer := strings.Trim(strings.TrimSpace(in.Text()), `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  There is no catalogue here yet. One has to be made before")
			fmt.Println("  anything can be searched, and that is a job for the command")
			fmt.Println("  prompt: searchforge help explains it. Try again, or close this")
			fmt.Println("  window.")
			fmt.Println()
			continue
		}

		info, err := os.Stat(answer)
		switch {
		case err != nil:
			fmt.Println()
			fmt.Printf("  I cannot find %q.\n", answer)
			fmt.Println("  Check the location and try again, or close this window.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged folder is a near miss worth rescuing rather than
			// scolding: look inside it for the catalogue they meant.
			if found := firstCatalogueIn(answer); found != "" {
				fmt.Println()
				fmt.Printf("  That is a folder, so I will use the catalogue inside it:\n  %s\n", found)
				return found, true
			}
			fmt.Println()
			fmt.Printf("  %q is a folder and there is no catalogue in it. Give me the\n", answer)
			fmt.Println("  catalogue file itself.")
			fmt.Println()
			continue
		}

		if !isCatalogue(answer) {
			fmt.Println()
			fmt.Printf("  %q is not a catalogue I can read.\n", answer)
			fmt.Println("  It has to be a file SearchForge wrote itself. Try again, or")
			fmt.Println("  close this window.")
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// askTerms asks what the reader is looking for. An empty answer is the one
// thing it cannot use, so it asks again rather than searching for nothing.
func askTerms(in *bufio.Scanner) ([]string, bool) {
	for {
		fmt.Println()
		fmt.Println("  What are you looking for?")
		fmt.Println("  One word, or several — a file has to contain all of them to match.")
		fmt.Print("  > ")

		if !in.Scan() {
			return nil, false
		}
		fields := strings.Fields(strings.Trim(strings.TrimSpace(in.Text()), `"`))
		if len(fields) == 0 {
			fmt.Println()
			fmt.Println("  I need at least one word to look for. Try again, or close this")
			fmt.Println("  window.")
			continue
		}
		return fields, true
	}
}

// suggestedCatalogue offers a catalogue the reader has probably already built,
// so the common case is one keypress. It returns "" when there is nothing to
// offer, and the prompt explains how a catalogue gets made.
func suggestedCatalogue() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if exe, err := os.Executable(); err == nil {
		dirs = append(dirs, filepath.Dir(exe))
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, home, filepath.Join(home, "Documents"))
	}
	for _, dir := range dirs {
		if found := firstCatalogueIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// firstCatalogueIn returns the first file in dir that SearchForge can actually
// open as a catalogue, or "" if there is none. Candidates are checked by
// opening them rather than by trusting the name, so a default is never offered
// that then fails on the reader's first keypress.
func firstCatalogueIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := strings.ToLower(e.Name())
		if strings.Contains(name, "index") || strings.Contains(name, "searchforge") {
			names = append(names, e.Name())
		}
	}
	sort.Strings(names)
	for _, name := range names {
		candidate := filepath.Join(dir, name)
		if isCatalogue(candidate) {
			return candidate
		}
	}
	return ""
}

// isCatalogue reports whether path really is one of SearchForge's own
// catalogues. Parsing alone is not enough to say so: the catalogue is JSON,
// and any other JSON file in the folder would parse into an empty one. A
// genuine catalogue always records the folder it was built from and when, so
// those are what get checked.
func isCatalogue(path string) bool {
	idx, err := loadIndex(path)
	if err != nil {
		return false
	}
	return idx.Root != "" && idx.BuiltAt > 0
}

// pause keeps the console window open. Explorer closes it the moment the
// process exits, so without this the reader never sees the output.
func pause(in *bufio.Scanner) {
	fmt.Println()
	fmt.Print("  Press Enter to close this window. ")
	in.Scan()
}
