package main

import (
	"bufio"
	"errors"
	"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.
//
// Everything RescueUSB does is reading, here and everywhere else: it opens the
// image file and nothing but the image file. It does not write to a USB stick,
// and guided mode offers nothing that could.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  RescueUSB")
	fmt.Println("  Check a downloaded disc image before you write it to a USB stick.")
	fmt.Println()
	fmt.Println("  Writing a broken download onto a stick wastes twenty minutes and")
	fmt.Println("  gives you a stick that will not start. This looks inside the file")
	fmt.Println("  first and tells you whether it arrived complete, whether it can")
	fmt.Println("  actually boot, and which kind of firmware it will boot on.")
	fmt.Println()
	fmt.Println("  Nothing is written anywhere. The image itself is only read.")
	fmt.Println()

	suggested := suggestedImage()
	for {
		fmt.Println("  Which disc image shall I check?")
		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 disc image — a .iso file — to check.")
			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 Enter.")
			fmt.Println()
			continue
		}

		info, err := os.Stat(answer)
		if 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 Enter.")
			fmt.Println()
			continue
		}
		if info.IsDir() {
			// A dragged folder is a near miss worth rescuing: if it holds
			// exactly one image, that is plainly the one meant.
			found := imagesIn(answer)
			if len(found) == 1 {
				fmt.Println()
				fmt.Printf("  That is a folder. Using the one image in it: %s\n", found[0])
				answer = found[0]
			} else {
				fmt.Println()
				fmt.Printf("  %q is a folder, not a disc image.\n", answer)
				if len(found) > 1 {
					fmt.Println("  It holds several. Give me one of these:")
					for i, f := range found {
						if i == 8 {
							fmt.Printf("      ... and %d more\n", len(found)-8)
							break
						}
						fmt.Printf("      %s\n", f)
					}
				}
				fmt.Println()
				continue
			}
		}

		fmt.Println()
		if err := cmdPreflight([]string{answer}); err != nil {
			// A pre-flight that says "no" reports itself in full and then
			// hands back the exit code a script would want. Guided mode has a
			// window to keep open, so that code is read and dropped here.
			var code exitCode
			if !errors.As(err, &code) {
				fmt.Println()
				fmt.Printf("  I could not check that file: %v\n", err)
				fmt.Println("  Try another, or close this window.")
				fmt.Println()
				continue
			}
		}
		break
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was written; the image is exactly as it was.")
	fmt.Println("  There is a command-line version too, which can also compare the")
	fmt.Println("  download against the checksums the publisher printed: rescueusb help")
	pause(in)
}

// suggestedImage offers a disc image that really exists, so the reader can get
// a useful answer by pressing one key. It returns "" rather than guessing when
// there is no image anywhere obvious.
func suggestedImage() string {
	var dirs []string
	if home, err := os.UserHomeDir(); err == nil {
		// A .iso is nearly always still sitting where the browser put it.
		dirs = append(dirs, filepath.Join(home, "Downloads"), home)
	}
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	for _, dir := range dirs {
		if found := imagesIn(dir); len(found) > 0 {
			return found[0]
		}
	}
	return ""
}

// imagesIn lists the disc images directly inside dir, largest first — the big
// one is nearly always the operating system somebody just downloaded, and the
// small ones are stray extras.
func imagesIn(dir string) []string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}
	type candidate struct {
		path string
		size int64
	}
	var found []candidate
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		ext := strings.ToLower(filepath.Ext(e.Name()))
		if ext != ".iso" && ext != ".img" {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		found = append(found, candidate{filepath.Join(dir, e.Name()), info.Size()})
	}
	sort.Slice(found, func(i, j int) bool {
		if found[i].size != found[j].size {
			return found[i].size > found[j].size
		}
		return found[i].path < found[j].path
	})
	paths := make([]string, 0, len(found))
	for _, c := range found {
		paths = append(paths, c.path)
	}
	return paths
}

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