package main

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

// runGuided is what happens when somebody double-clicks DeployForge instead of
// typing its name at a prompt.
//
// Explorer opens a console, runs the program with no arguments, and destroys
// the window the instant the process exits — so printing usage and quitting
// looks exactly like a crash. When we know we were double-clicked we ask the
// one thing DeployForge needs and stay on screen until the reader is done.
//
// The guided session reads the recipe and reports on it. It does not run a
// single one of the commands in it — not the checks and certainly not the
// installs. Running a deployment is a deliberate command-line act, because it
// changes the machine.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  DeployForge")
	fmt.Println("  Check a setup recipe before you let it near a machine.")
	fmt.Println()
	fmt.Println("  A recipe is a list of steps for getting a computer ready. Each step")
	fmt.Println("  says how to tell whether the job is already done, and what to do if")
	fmt.Println("  it is not. Give me the recipe file and I will read it through, point")
	fmt.Println("  out anything wrong with it, and show you the steps in order.")
	fmt.Println()
	fmt.Println("  Reading only. No step is run here, so nothing on this machine changes.")
	fmt.Println()

	suggested := suggestedManifest()
	for {
		fmt.Println("  Where is your recipe file?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		} else {
			fmt.Println("  (it is a .json file listing the steps)")
		}
		fmt.Println("  Tip: you can drag the file, or the folder it lives in, from Explorer")
		fmt.Println("  onto this window to paste its location, then press Enter.")
		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 read. 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()
			continue
		case info.IsDir():
			// A dragged folder is an ordinary mistake: look for the obvious
			// file inside it before complaining about it.
			inside := filepath.Join(answer, "manifest.json")
			if fi, err := os.Stat(inside); err == nil && !fi.IsDir() {
				fmt.Println()
				fmt.Printf("  That is a folder — I will use %s inside it.\n", inside)
				answer = inside
				break // leaves the switch, and carries on with the file below
			}
			fmt.Println()
			fmt.Printf("  %q is a folder. I need the recipe file itself, which ends in .json.\n", answer)
			fmt.Println()
			continue
		}

		manifest, err := loadManifest(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  I could not read that as a recipe.\n")
			fmt.Printf("  %v\n", err)
			fmt.Println()
			continue
		}

		fmt.Println()
		describeManifest(manifest)
		break
	}

	fmt.Println()
	fmt.Println("  Done. Nothing was run and nothing was installed — that was a read of")
	fmt.Println("  the recipe, no more.")
	fmt.Println("  Actually running a recipe against this machine is a separate step on")
	fmt.Println("  the command line: run deployforge --help to see it.")
	pause(in)
}

// describeManifest reports what the recipe says: whether it is well formed,
// and then every step in the order it would run. It runs nothing.
func describeManifest(m *Manifest) {
	name := m.Name
	if strings.TrimSpace(name) == "" {
		name = "(unnamed)"
	}

	problems := validateManifest(m)
	if len(problems) == 0 {
		fmt.Printf("  Recipe %q looks sound: %d %s, and every one is complete.\n",
			name, len(m.Steps), plural(len(m.Steps), "step", "steps"))
	} else {
		fmt.Printf("  Recipe %q has %d %s to fix:\n", name, len(problems), plural(len(problems), "problem", "problems"))
		for _, p := range problems {
			fmt.Printf("    - %s\n", p)
		}
	}

	if len(m.Steps) == 0 {
		return
	}
	fmt.Println()
	fmt.Println("  The steps, in the order they would run:")
	for i, s := range m.Steps {
		label := s.Name
		if strings.TrimSpace(label) == "" {
			label = "(unnamed step)"
		}
		fmt.Printf("    %d. %s\n", i+1, label)
		fmt.Printf("       already done when this succeeds:  %s\n", orMissing(s.Check))
		fmt.Printf("       otherwise it would run:           %s\n", orMissing(s.Install))
	}
}

func orMissing(s string) string {
	if strings.TrimSpace(s) == "" {
		return "(nothing given)"
	}
	return s
}

// plural picks the right word for a count, so the report reads like English.
func plural(n int, one, many string) string {
	if n == 1 {
		return one
	}
	return many
}

// suggestedManifest offers a recipe the reader can accept with one keypress:
// the obvious one next to the program itself, or in the folder the console
// opened in. It returns "" when there is no such file, and the prompt copes
// with that.
func suggestedManifest() string {
	var dirs []string
	if exe, err := os.Executable(); err == nil {
		dirs = append(dirs, filepath.Dir(exe))
	}
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	for _, dir := range dirs {
		for _, name := range []string{"manifest.json", "deployforge.json", "deploy.json"} {
			candidate := filepath.Join(dir, name)
			if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
				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()
}
