package main

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

// guidedTimeout is how long guided mode waits for one machine to answer. Short
// on purpose: the reader is watching a window, not running a monitor.
const guidedTimeout = 2 * time.Second

// 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.
//
// Guided mode does ONE pass over the list and stops. The continuous watch that
// RemoteDeck normally runs never ends by itself, and a window with no way out
// of it is the very bug this whole file exists to fix.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  RemoteDeck")
	fmt.Println("  Check whether the machines you look after are answering.")
	fmt.Println()
	fmt.Println("  It knocks on each one in turn and reports which are up, which are")
	fmt.Println("  down, and how long each took to answer.")
	fmt.Println()
	fmt.Println("  I will do a single round of checks now and stop. Nothing on your")
	fmt.Println("  computer is read or changed beyond the list you point me at.")
	fmt.Println()

	suggested := suggestedTargetsFile()
	for {
		fmt.Println("  Which list of machines shall I check?")
		fmt.Println("  (a watchlist file, or just type one address like myserver:22)")
		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 something to check. Try again, or close this window.")
			printWatchlistHelp()
			continue
		}

		targets, err := guidedTargets(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  %s\n", err)
			printWatchlistHelp()
			continue
		}

		fmt.Println()
		fmt.Printf("  Checking %d machine(s). Anything that has not answered within\n", len(targets))
		fmt.Printf("  %s counts as down.\n", guidedTimeout)
		fmt.Println()
		// runOnce's return value is the exit code a scripted run would use.
		// Guided mode has a window to keep open, so it is deliberately not
		// turned into an os.Exit here.
		_ = runOnce(targets, guidedTimeout, false, nil)
		break
	}

	fmt.Println()
	fmt.Println("  That was one round of checks.")
	fmt.Println("  There is a command-line version too, which keeps watching and tells")
	fmt.Println("  you the moment anything changes: remotedeck help")
	pause(in)
}

// guidedTargets turns whatever the reader typed into a list to check: either a
// watchlist file on disk, or a single address typed straight at the prompt. The
// error it returns is written for a person, not a log file.
func guidedTargets(answer string) ([]Target, error) {
	info, statErr := os.Stat(answer)
	if statErr == nil && info.IsDir() {
		return nil, fmt.Errorf("%q is a folder. I need the watchlist file inside it, or one address.", answer)
	}
	if statErr == nil {
		targets, err := parseTargets(answer)
		if err != nil {
			return nil, fmt.Errorf("I found %q but could not read it as a watchlist.", answer)
		}
		return targets, nil
	}

	// A dragged path that is not there is a missing file, not an address —
	// worth saying so plainly. (Without this a Windows path would be read as
	// an address, because "C:\lists\hosts.txt" does split into a host and a
	// "port" as far as the parser is concerned.)
	if strings.ContainsAny(answer, `/\`) {
		return nil, fmt.Errorf("I cannot find %q.", answer)
	}

	// Not a file, so it should be an address. A bare host with no port is the
	// commonest slip, and worth naming exactly.
	if _, _, err := net.SplitHostPort(answer); err != nil {
		if !strings.Contains(answer, ":") {
			return nil, fmt.Errorf("%q has no port on the end. Try %s:22, or 443, or whichever port that machine listens on.", answer, answer)
		}
		return nil, fmt.Errorf("%q is neither a file I can find nor an address I can read.", answer)
	}
	return []Target{{Name: answer, Addr: answer}}, nil
}

func printWatchlistHelp() {
	fmt.Println()
	fmt.Println("  A watchlist is a plain text file with one machine per line:")
	fmt.Println()
	fmt.Println("      web1 10.0.0.5:443")
	fmt.Println("      db1  10.0.0.9:5432")
	fmt.Println()
	fmt.Println("  Tip: you can drag that file, or the folder holding it, from Explorer")
	fmt.Println("  onto this window to paste its location, then press Enter.")
	fmt.Println()
}

// suggestedTargetsFile offers a watchlist that really exists, so the reader can
// accept it with one keypress. There is no honest fallback if nobody has
// written one yet, so it returns "" and the prompt explains the format instead.
func suggestedTargetsFile() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		// The watchlist is nearly always kept beside the program.
		dirs = append(dirs, wd)
	}
	if home, err := os.UserHomeDir(); err == nil {
		dirs = append(dirs, home, filepath.Join(home, "Documents"))
	}
	for _, dir := range dirs {
		for _, name := range []string{"targets.txt", "watchlist.txt", "remotedeck-targets.txt"} {
			candidate := filepath.Join(dir, name)
			if info, err := os.Stat(candidate); err == nil && info.Mode().IsRegular() {
				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()
}
