package main

import (
	"bufio"
	"context"
	"fmt"
	"net"
	"os"
	"strings"
	"time"
)

// defaultHost is the one-keypress answer: the machine this is running on. It
// needs no network, no name server and no permission from anybody, so pressing
// Enter always produces a real result.
const defaultHost = "localhost"

// defaultPorts is the short list guided mode scans: the services a person is
// most likely to be asking about. Deliberately a handful, not a range — a
// double-clicked window should answer in a second or two.
const defaultPorts = "22,80,135,139,443,445,3306,3389,5432,8080"

// 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 action here is a single short port scan that finishes on its own. Guided
// mode never starts anything that waits, listens or loops.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  NetLens")
	fmt.Println("  Ask a machine on your network which of its doors are open.")
	fmt.Println()
	fmt.Println("  NetLens knocks on a short list of well-known ports and reports each")
	fmt.Println("  one as open, closed or blocked by something in between. It only")
	fmt.Println("  knocks: no data is sent and nothing is changed at either end.")
	fmt.Println()

	host, ok := askHost(in)
	if !ok {
		return
	}

	fmt.Println()
	fmt.Printf("  Knocking on %s at ports %s. This takes a moment.\n", host, defaultPorts)
	fmt.Println()
	runScan([]string{host, "--ports", defaultPorts})

	fmt.Println()
	fmt.Println("  Done. That was one pass and it has finished; nothing is left running.")
	fmt.Println("  The command-line version can scan any ports you like and measure")
	fmt.Println("  connection times too: netlens --help")
	pause(in)
}

// askHost asks which machine to look at and keeps asking until the name can
// actually be resolved to an address. It reports false only when stdin closes,
// at which point there is nothing sensible left to ask.
func askHost(in *bufio.Scanner) (string, bool) {
	for {
		fmt.Println("  Which machine shall I look at?")
		fmt.Println("  A name or an address is fine, for example 192.168.1.1 for your")
		fmt.Printf("  router. (press Enter for %s, this computer)\n", defaultHost)
		fmt.Print("  > ")

		if !in.Scan() {
			return "", false
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = defaultHost
		}
		if strings.ContainsAny(answer, " \t/\\") {
			fmt.Println()
			fmt.Printf("  %q does not look like a machine name or address.\n", answer)
			fmt.Println("  Try something like 192.168.1.1 or fileserver.")
			fmt.Println()
			continue
		}
		if !resolvable(answer) {
			fmt.Println()
			fmt.Printf("  I cannot work out an address for %q.\n", answer)
			fmt.Println("  Check the spelling, or type the numeric address instead.")
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// resolvable reports whether a name or address can be turned into an IP
// address within a couple of seconds, so a typo is caught before the scan
// rather than showing up as ten identical failures.
func resolvable(host string) bool {
	if net.ParseIP(host) != nil {
		return true
	}
	var r net.Resolver
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	addrs, err := r.LookupHost(ctx, host)
	return err == nil && len(addrs) > 0
}

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