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 one thing
// the program needs and stay on screen until the reader is done.
//
// SetupPilot can generate an answer file and can write a stripped copy of one,
// so the guided session does neither: it runs the check, which opens the file
// for reading and reports what it found. Producing a file stays a deliberate
// act at the command line.
//
// The check is run here rather than by calling the command directly, because
// the command ends the process when it finds a fault — which on Windows would
// take the console window with it and leave the reader staring at the same
// black flash this whole file exists to prevent. Same reading, same report,
// but the window survives a failing answer file.
//
// 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("  SetupPilot")
	fmt.Println("  Check a Windows answer file before you image a room full of machines.")
	fmt.Println()
	fmt.Println("  An answer file is what tells Windows Setup how to install itself with")
	fmt.Println("  nobody standing at the keyboard. Windows does not complain about a")
	fmt.Println("  section it cannot understand — it quietly skips it, and every machine")
	fmt.Println("  comes out wrong in the same way.")
	fmt.Println()
	fmt.Println("  SetupPilot reads the file and points at the faults that go unnoticed,")
	fmt.Println("  with the line each one is on. It only reads; your file is not altered.")
	fmt.Println()

	path, ok := askAnswerFile(in, suggestedAnswerFile())
	if !ok {
		return
	}

	fmt.Println()
	checkQuietly(path)

	fmt.Println()
	fmt.Println("  Your file was not changed.")
	fmt.Println("  A clean result means none of the faults SetupPilot knows how to find.")
	fmt.Println("  It is not a promise that the installation will work — always try one")
	fmt.Println("  machine before you image the rest.")
	fmt.Println()
	fmt.Println("  From a command prompt SetupPilot can also summarise a file, build a new")
	fmt.Println("  one, and write a copy with the secrets taken out: setuppilot --help")
	pause(in)
}

// checkQuietly runs exactly the audit the check command runs and prints the
// same report, but returns instead of ending the process so the window stays
// open long enough to read it.
func checkQuietly(path string) {
	data, size, err := readAnswerFile(path)
	if err != nil {
		fmt.Printf("  I could not read %s.\n", path)
		fmt.Println("  " + cleanErr(err))
		return
	}

	doc := parseDocument(path, data, size, false)
	f := validate(doc)
	errs, warns, infos := f.counts()

	fmt.Printf("Checking %s\n\n", path)
	printFindings(f)
	fmt.Printf("\n%d error(s), %d warning(s), %d note(s)\n", errs, warns, infos)
	if errs == 0 {
		fmt.Println("RESULT: PASS -- no structural errors found")
	} else {
		fmt.Println("RESULT: FAIL -- do not deploy this answer file")
	}
}

// askAnswerFile asks for the answer file and keeps asking until it gets one
// that exists and can be read.
func askAnswerFile(in *bufio.Scanner, suggested string) (string, bool) {
	for {
		fmt.Println("  Which answer file shall I check?")
		fmt.Println("  It is usually called unattend.xml or autounattend.xml. You can drag")
		fmt.Println("  it, or the folder holding it, from Explorer onto this window to")
		fmt.Println("  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("  I need an answer file to check. Try again, or close this 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 answer file they meant.
			if found := firstAnswerFileIn(answer); found != "" {
				fmt.Println()
				fmt.Printf("  That is a folder, so I will check the answer file inside it:\n  %s\n", found)
				return found, true
			}
			fmt.Println()
			fmt.Printf("  %q is a folder and there is no answer file in it. Give me the\n", answer)
			fmt.Println("  file itself.")
			fmt.Println()
			continue
		case !info.Mode().IsRegular():
			fmt.Println()
			fmt.Printf("  %q is not an ordinary file. I need the answer file itself.\n", answer)
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// suggestedAnswerFile offers an answer file the reader probably has beside
// them, so the common case is one keypress. It returns "" when there is
// nothing to offer, and the prompt simply asks for a location.
func suggestedAnswerFile() 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, "Downloads"),
			filepath.Join(home, "Documents"))
	}
	for _, dir := range dirs {
		if found := firstAnswerFileIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// firstAnswerFileIn returns the first answer file in dir, or "" if there is
// none. The well-known names come first, because those are the ones somebody
// actually meant; any other .xml is only a fallback.
func firstAnswerFileIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	known := map[string]bool{"unattend.xml": true, "autounattend.xml": true}
	var preferred, others []string
	for _, e := range entries {
		if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".xml") {
			continue
		}
		info, err := e.Info()
		if err != nil || !info.Mode().IsRegular() {
			continue
		}
		if known[strings.ToLower(e.Name())] {
			preferred = append(preferred, e.Name())
		} else {
			others = append(others, e.Name())
		}
	}
	sort.Strings(preferred)
	sort.Strings(others)
	for _, name := range append(preferred, others...) {
		candidate := filepath.Join(dir, name)
		if _, _, err := readAnswerFile(candidate); err == nil {
			return candidate
		}
	}
	return ""
}

// 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()
}
