package main

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

// guidedTimeout is the patience of a single guided check. Short enough that a
// dead address does not look like the program has frozen.
const guidedTimeout = 5 * 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.
//
// LinkGuard's main job is to watch continuously, which is exactly the wrong
// thing to start in a window somebody double-clicked: a loop that prints
// forever and can only be stopped with a keystroke nobody mentioned is its own
// kind of "it hung". So the guided session runs ONE round of checks, prints
// what it found, and returns. It never starts the watch loop and never opens
// or writes a ledger.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  LinkGuard")
	fmt.Println("  Check whether something on the network is answering right now.")
	fmt.Println()
	fmt.Println("  Give it a web address, a server and port, or just a host name, and")
	fmt.Println("  it tries to reach it once and reports back how long that took.")
	fmt.Println()
	fmt.Println("  This is a single check that finishes on its own. Nothing is left")
	fmt.Println("  running and no file is written.")
	fmt.Println()

	suggested := suggestedTarget()
	for {
		fmt.Println("  What shall I check?")
		fmt.Println("  (a web address like https://example.com, a server like")
		fmt.Println("   db.example.com:5432, a host name, or a saved targets file)")
		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.")
			fmt.Println()
			continue
		}

		targets, ok := guidedTargets(answer)
		if !ok {
			continue
		}

		fmt.Println()
		fmt.Printf("  Checking. I will wait up to %s for an answer.\n", guidedTimeout)
		fmt.Println()
		checkOnce(targets)
		break
	}

	fmt.Println()
	fmt.Println("  That was one check, taken just now. Whether something is up at this")
	fmt.Println("  moment says nothing about how reliable it has been.")
	fmt.Println()
	fmt.Println("  The command-line version can keep watching a list of addresses and")
	fmt.Println("  build up the history that answers that: linkguard help")
	pause(in)
}

// guidedTargets turns one typed answer into the list to check. A path to a
// real file on disk is read as a saved targets file — which is what a reader
// gets by dragging one onto the window — and anything else is treated as a
// single address. It returns ok=false when the answer was a file that could
// not be used, so the caller asks again.
func guidedTargets(answer string) ([]Target, bool) {
	info, err := os.Stat(answer)
	if err != nil {
		// Not a path at all, which is the normal case: an address.
		return []Target{classifyTarget(answer)}, true
	}
	if info.IsDir() {
		fmt.Println()
		fmt.Printf("  %q is a folder. I need a web address, a server and port, or a\n", answer)
		fmt.Println("  saved targets file.")
		fmt.Println()
		return nil, false
	}

	cfg, err := loadConfig(answer)
	if err != nil {
		fmt.Println()
		fmt.Printf("  %q is a file, but not a LinkGuard targets file I can read.\n", answer)
		fmt.Println("  Give me a web address instead, such as https://example.com.")
		fmt.Println()
		return nil, false
	}
	fmt.Println()
	fmt.Printf("  Read %d target(s) from that file.\n", len(cfg.Targets))
	return cfg.Targets, true
}

// classifyTarget works out what kind of check one typed address deserves,
// using the same three kinds LinkGuard supports everywhere else.
func classifyTarget(addr string) Target {
	lower := strings.ToLower(addr)
	switch {
	case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"):
		return Target{Name: addr, Type: "http", Target: addr}
	case hasPort(addr):
		return Target{Name: addr, Type: "tcp", Target: addr}
	default:
		return Target{Name: addr, Type: "dns", Target: addr}
	}
}

// hasPort reports whether addr looks like host:port, which is the form a TCP
// connection check needs.
func hasPort(addr string) bool {
	i := strings.LastIndex(addr, ":")
	if i <= 0 || i == len(addr)-1 {
		return false
	}
	if strings.Contains(addr[:i], "/") {
		return false
	}
	port, err := strconv.Atoi(addr[i+1:])
	return err == nil && port > 0 && port <= 65535
}

// checkOnce runs exactly one round against the targets and prints the result.
// It is the probe command's behaviour: no ledger is opened, no loop is
// started, and the function returns as soon as the round is done.
func checkOnce(targets []Target) {
	if len(targets) == 0 {
		fmt.Println("  There was nothing in that file to check.")
		return
	}

	for _, t := range targets {
		switch t.Type {
		case "http":
			fmt.Printf("  %s: asking for the page and looking at what comes back.\n", t.Target)
		case "tcp":
			fmt.Printf("  %s: opening a connection to that port and closing it again.\n", t.Target)
		case "dns":
			fmt.Printf("  %s: looking the name up to see if it resolves to an address.\n", t.Target)
		}
	}
	fmt.Println()

	results := probeRound(context.Background(), targets, guidedTimeout)
	for _, r := range results {
		printResultLine(r)
	}

	var down int
	for _, r := range results {
		if r.State == stateDown {
			down++
		}
	}
	fmt.Println()
	switch {
	case down == 0 && len(results) == 1:
		fmt.Println("  It answered.")
	case down == 0:
		fmt.Println("  All of them answered.")
	case down == len(results):
		fmt.Println("  No answer. The reason is on the line above — a refused connection")
		fmt.Println("  means something is there but not listening, a name that does not")
		fmt.Println("  resolve usually means a typo, and a timeout means silence.")
	default:
		fmt.Printf("  %d of %d did not answer. The reason is on each line above.\n", down, len(results))
	}
}

// suggestedTarget offers something the reader can check with one keypress. It
// is a name reserved by the IANA for documentation and examples, so pressing
// Enter cannot poke somebody's real server by accident.
func suggestedTarget() string {
	return "https://example.com"
}

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