package main

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

// The guided session takes a short, fixed measurement and then stops. These
// are the whole of its budget: at most guidedTargets addresses, guidedSamples
// connections each, and guidedTimeout for any one of them. Worst case the
// round takes guidedSamples * guidedTimeout, a few seconds, and it always
// ends by itself. There is no repeat, no watch mode and no loop.
const (
	guidedSamples = 3
	guidedTimeout = time.Second
	guidedTargets = 8
)

// 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 thing
// the program needs and stay on screen until the reader is done.
//
// The guided round is deliberately tiny: a handful of connections that are
// timed and closed again. It writes no report file and changes nothing on
// disk; the full agent command, which saves a report so several offices can be
// merged into one grid, stays at the command line.
//
// 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.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  RouteWatch")
	fmt.Println("  Time how long it takes this machine to reach a server.")
	fmt.Println()
	fmt.Println("  RouteWatch opens a connection to a server, times how long the")
	fmt.Println("  handshake took, closes it again, and repeats that a few times. The")
	fmt.Println("  spread between the quickest and the slowest attempt says more than any")
	fmt.Println("  single number does.")
	fmt.Println()
	fmt.Printf("  This round is a short one: %d attempts per server, each given %s to\n",
		guidedSamples, guidedTimeout)
	fmt.Println("  answer. It finishes by itself in a few seconds. Nothing is saved.")
	fmt.Println()

	targets, ok := askTargets(in, defaultTarget())
	if !ok {
		return
	}

	fmt.Println()
	fmt.Println("  Measuring.")
	fmt.Println()

	links := make([]Link, len(targets))
	done := make(chan struct{}, len(targets))
	for i, t := range targets {
		go func(i int, t Target) {
			links[i] = measureLink(context.Background(), t, guidedSamples, guidedTimeout)
			done <- struct{}{}
		}(i, t)
	}
	for range targets {
		<-done
	}

	fmt.Printf("  %-16s %-24s %8s %8s %8s  %s\n",
		"SERVER", "ADDRESS", "BEST ms", "TYPICAL", "WORST", "NOTE")
	for _, l := range links {
		if !l.Reachable {
			fmt.Printf("  %-16s %-24s %8s %8s %8s  %s\n",
				truncate(l.Target, 16), truncate(l.Addr, 24),
				markUnreachable, markUnreachable, markUnreachable,
				"no answer: "+l.Reason)
			continue
		}
		note := "all attempts answered"
		if l.Failed > 0 {
			note = fmt.Sprintf("%d of %d attempts got no answer", l.Failed, l.Samples)
		}
		fmt.Printf("  %-16s %-24s %8.1f %8.1f %8.1f  %s\n",
			truncate(l.Target, 16), truncate(l.Addr, 24),
			l.MinMS, l.MedianMS, l.MaxMS, note)
	}

	fmt.Println()
	fmt.Println("  Times are in milliseconds, and they measure the connection handshake")
	fmt.Println("  as this machine sees it — not a ping, and not any single hop along the")
	fmt.Println("  way. A server that is slow to accept looks the same as a slow line.")
	fmt.Println()
	fmt.Println("  One machine can only tell you that IT is slow. Run RouteWatch at each")
	fmt.Println("  office and it merges those measurements into a grid that names whose")
	fmt.Println("  fault it is. Run it from a command prompt: routewatch --help")
	pause(in)
}

// askTargets accepts either a saved target list or a single address typed by
// hand, and keeps asking until it has something it can actually measure.
func askTargets(in *bufio.Scanner, suggested string) ([]Target, bool) {
	for {
		fmt.Println("  Which server shall I measure?")
		fmt.Println("  Type an address as name-or-number and port, like intranet:443, or")
		fmt.Println("  give me a saved target list — you can drag that file from Explorer")
		fmt.Println("  onto this window to paste its location.")
		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 nil, false
		}
		answer := strings.Trim(strings.TrimSpace(in.Text()), `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need something to measure. Try again, or close this window.")
			fmt.Println()
			continue
		}

		// A real file on disk is a saved target list; anything else is meant
		// to be an address.
		if info, err := os.Stat(answer); err == nil {
			if info.IsDir() {
				fmt.Println()
				fmt.Printf("  %q is a folder. I need either a single address like\n", answer)
				fmt.Println("  intranet:443, or one target-list file.")
				fmt.Println()
				continue
			}
			list, err := loadTargets(answer)
			if err != nil {
				fmt.Println()
				fmt.Printf("  I could not use %q as a target list.\n", answer)
				fmt.Println("  Each entry in it needs a name and an address of the form")
				fmt.Println("  host:port. Try again, or type a single address instead.")
				fmt.Println()
				continue
			}
			if len(list) > guidedTargets {
				fmt.Println()
				fmt.Printf("  That list has %d servers in it. To keep this quick I will\n", len(list))
				fmt.Printf("  measure the first %d.\n", guidedTargets)
				list = list[:guidedTargets]
			}
			return list, true
		}

		if _, _, err := net.SplitHostPort(answer); err != nil {
			fmt.Println()
			fmt.Printf("  I cannot make sense of %q.\n", answer)
			fmt.Println("  An address needs a port on the end, like intranet:443 or")
			fmt.Println("  192.168.1.10:22. If you meant a file, check the location.")
			fmt.Println()
			continue
		}
		return []Target{{Name: hostPart(answer), Addr: answer}}, true
	}
}

// hostPart is the label a hand-typed address gets in the table: the host on
// its own reads better than host:port repeated in two columns.
func hostPart(addr string) string {
	host, _, err := net.SplitHostPort(addr)
	if err != nil || host == "" {
		return addr
	}
	return host
}

// defaultTarget is the one-keypress answer: a target list somebody has already
// left beside the program if there is one, otherwise a single address that is
// reachable from a normal machine and belongs to nobody — the name IANA
// reserves for exactly this kind of example. If it cannot be reached the round
// still says something useful, because "no answer" is a measurement too.
//
// It is always either a file that exists or a valid host:port, so pressing
// Enter can never greet the reader with an error.
func defaultTarget() string {
	if f := suggestedTargetFile(); f != "" {
		return f
	}
	return "example.com:443"
}

// suggestedTargetFile looks for a target list somebody has already put next to
// the program, so an office that has been handed one does not have to type its
// location. It returns "" when there is nothing to offer.
func suggestedTargetFile() string {
	var dirs []string
	if wd, err := os.Getwd(); err == nil {
		dirs = append(dirs, wd)
	}
	if exe, err := os.Executable(); err == nil {
		dirs = append(dirs, filepath.Dir(exe))
	}
	for _, dir := range dirs {
		for _, name := range []string{"targets.json", "routewatch-targets.json"} {
			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()
}
