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
// question the program actually needs and stay on screen until the reader is
// done.
//
// 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.
//
// The guided session checks the recipe and never runs it. "apply" executes
// whatever shell commands the recipe lists — installers, registry edits,
// anything — and even its --dry-run still reaches out to run each step's check
// command. Nobody who double-clicked an icon has agreed to that. "validate"
// reads the file, works out the order the steps would run in, and stops there.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  WinImageKit")
	fmt.Println("  Checks a setup recipe before you trust a machine to it.")
	fmt.Println()
	fmt.Println("  A recipe is a file listing everything to install on a new PC, and")
	fmt.Println("  which steps have to happen before which. This works out whether")
	fmt.Println("  those instructions hang together — nothing pointing at a step that")
	fmt.Println("  does not exist, nothing waiting on itself in a circle — and shows")
	fmt.Println("  you the exact order the steps would run in.")
	fmt.Println()
	fmt.Println("  Nothing is installed and nothing is run. This only reads the recipe.")
	fmt.Println()

	suggested := suggestedManifest()
	for {
		fmt.Println("  Which recipe file shall I check?")
		fmt.Println("  (a .json file listing the steps)")
		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
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a recipe 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("  Tip: you can drag the file — or the folder holding it — from")
			fmt.Println("  Explorer onto this window to paste its location, then press")
			fmt.Println("  Enter.")
			fmt.Println()
			continue
		case info.IsDir():
			// A dragged-in folder is a common answer; look inside it rather
			// than sending the reader away to find the file themselves.
			found := manifestIn(answer)
			if found == "" {
				fmt.Println()
				fmt.Printf("  %q is a folder, and I cannot see a recipe file in it.\n", answer)
				fmt.Println("  Give me the .json file itself.")
				fmt.Println()
				continue
			}
			fmt.Printf("\n  Using the recipe I found in that folder: %s\n", found)
			answer = found
		}

		// loadManifest is the same reader "validate" uses, and it is where a
		// file that is not a recipe at all gets turned away — with a sentence
		// instead of a Go error, and without the process exiting on it.
		m, err := loadManifest(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  %q is not a recipe I can read.\n", filepath.Base(answer))
			fmt.Printf("  (%v)\n", err)
			fmt.Println("  A recipe is a .json file with a list of named steps in it.")
			fmt.Println()
			continue
		}

		fmt.Println()
		reportValidation(m)
		describeSteps(m)
		break
	}

	fmt.Println()
	fmt.Println("  Nothing was installed — this was a check of the instructions only.")
	fmt.Println("  The command-line version is the one that runs a recipe:")
	fmt.Println("  winimagekit --help")
	pause(in)
}

// describeSteps spells the graph out in words, because "valid dependency
// graph" answers a question the reader did not ask. What they want to know is
// what this recipe would do to the machine and in what order.
func describeSteps(m *Manifest) {
	fmt.Println()
	fmt.Printf("The %d step(s) in this recipe:\n\n", len(m.Steps))
	for _, s := range m.Steps {
		fmt.Printf("  %s\n", s.Name)
		if len(s.DependsOn) > 0 {
			fmt.Printf("      waits for: %s\n", strings.Join(s.DependsOn, ", "))
		} else {
			fmt.Printf("      waits for: nothing — it can start straight away\n")
		}
		if s.Install != "" {
			fmt.Printf("      would run: %s\n", s.Install)
		}
	}
}

// suggestedManifest offers a recipe the reader plausibly has in mind, so the
// common case is one keypress. Candidates are confirmed by actually loading
// them, so the offered default is one that will work.
func suggestedManifest() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, filepath.Join(home, "Desktop"),
			filepath.Join(home, "Downloads"),
			filepath.Join(home, "Documents"),
			home)
	}
	for _, dir := range dirs {
		if found := manifestIn(dir); found != "" {
			return found
		}
	}
	return ""
}

// manifestIn returns the first .json file directly inside dir that really is a
// WinImageKit recipe, or "" if there is none. Only the top level is searched:
// a guided default should be instant, not a disk crawl.
func manifestIn(dir string) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var names []string
	for _, e := range entries {
		if e.IsDir() || !strings.EqualFold(filepath.Ext(e.Name()), ".json") {
			continue
		}
		names = append(names, e.Name())
	}
	sort.Strings(names)
	for _, name := range names {
		path := filepath.Join(dir, name)
		if info, err := os.Stat(path); err != nil || info.Size() > 4<<20 {
			continue // not worth parsing a huge file to guess a default
		}
		if _, err := loadManifest(path); err == nil {
			return path
		}
	}
	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()
}
