package main

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

// guidedProbeTimeout bounds the one request the guided session makes, so an
// unresponsive server cannot leave the window looking frozen.
const guidedProbeTimeout = 15 * 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 questions
// 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.
//
// GrabFlow writes files, and a download started by a double-click — to a place
// nobody confirmed, over a file that may already be there — is exactly the
// surprise this session must not spring. So the guided session runs the
// read-only half of a download: the same check GrabFlow makes before it
// fetches anything, which asks the server how big the file is and whether it
// can be fetched in parallel pieces. It then prints the command that would do
// the download, for the reader to run when they are ready. It never creates a
// file.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  GrabFlow")
	fmt.Println("  A downloader that can fetch a large file in several pieces at once,")
	fmt.Println("  pick up where it left off, and check that what arrived is intact.")
	fmt.Println()
	fmt.Println("  This window checks a link and tells you what to expect from it. It")
	fmt.Println("  does not download anything and does not create any file.")
	fmt.Println()

	rawURL := askURL(in)
	if rawURL == "" {
		return
	}
	fmt.Println()
	folder := askFolder(in)
	if folder == "" {
		return
	}

	fmt.Println()
	fmt.Println("  Asking the server about that link.")
	fmt.Println()
	reached := checkLink(rawURL, folder)

	fmt.Println()
	if reached {
		fmt.Println("  Nothing was downloaded. Copy the command above into a Command")
		fmt.Println("  Prompt to fetch the file, or run: grabflow help")
	} else {
		fmt.Println("  Nothing was downloaded and no file was created. Check the link and")
		fmt.Println("  try again, or run: grabflow help")
	}
	pause(in)
}

// askURL asks for the link until it gets something that is actually a web
// address, returning "" only when stdin has closed.
func askURL(in *bufio.Scanner) string {
	suggested := suggestedURL()
	for {
		fmt.Println("  Which link shall I check?")
		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 a link to check. Try again, or close this window.")
			fmt.Println()
			continue
		}
		// A pasted address often arrives without its http:// on the front.
		if !strings.Contains(answer, "://") {
			answer = "https://" + answer
		}

		u, err := url.Parse(answer)
		if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
			fmt.Println()
			fmt.Printf("  %q does not look like a web link.\n", answer)
			fmt.Println("  It should look like https://example.com/something.zip — you can")
			fmt.Println("  copy one from your browser's address bar and paste it here.")
			fmt.Println()
			continue
		}
		return answer
	}
}

// askFolder asks where the file would be saved. GrabFlow needs an output path
// for a real download, so this is the second thing it genuinely needs — and
// knowing it lets the session print a command that is ready to run.
func askFolder(in *bufio.Scanner) string {
	suggested := suggestedFolder()
	for {
		fmt.Println("  And where would you want it saved?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Print("  > ")

		if !in.Scan() {
			return ""
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a folder. 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("  Tip: you can drag a folder from Explorer onto this window to")
			fmt.Println("  paste its location, then press Enter.")
			fmt.Println()
			continue
		case !info.IsDir():
			fmt.Println()
			fmt.Printf("  %q is a file, not a folder. Give me the folder to save into.\n", answer)
			fmt.Println()
			continue
		}
		return answer
	}
}

// checkLink runs exactly the probe GrabFlow runs before a download: a HEAD
// request for the size and one ranged request for a single byte, to find out
// whether the server can serve pieces. Nothing is saved. It reports whether
// the server answered at all, so the closing words can match what happened.
func checkLink(rawURL, folder string) bool {
	client := &http.Client{Timeout: guidedProbeTimeout}

	length, ranges, err := probeServer(client, rawURL)
	if err != nil {
		fmt.Println("  I could not reach that link.")
		fmt.Println("  The address may be wrong, the server may be down, or this machine")
		fmt.Println("  may have no way out to the internet right now.")
		return false
	}

	fmt.Printf("  Link:  %s\n", rawURL)
	if length >= 0 {
		fmt.Printf("  Size:  %s\n", humanBytes(length))
	} else {
		fmt.Println("  Size:  the server would not say in advance")
	}
	if ranges {
		fmt.Println("  Speed: this server hands out pieces of the file, so GrabFlow can")
		fmt.Println("         fetch several at once and can resume an interrupted download.")
	} else {
		fmt.Println("  Speed: this server sends the file as one stream only, so it would")
		fmt.Println("         be fetched in a single piece and could not be resumed.")
	}

	target := filepath.Join(folder, suggestedFilename(rawURL))
	if _, err := os.Stat(target); err == nil {
		fmt.Println()
		fmt.Printf("  Note:  %s already exists. Save the download under another name\n", target)
		fmt.Println("         unless you mean to replace it.")
	}

	fmt.Println()
	fmt.Println("  To download it, run:")
	if ranges {
		fmt.Printf("    grabflow get %s -o %q --segments 8\n", rawURL, target)
	} else {
		fmt.Printf("    grabflow get %s -o %q\n", rawURL, target)
	}
	return true
}

// suggestedFilename works out what the finished file would sensibly be called,
// from the last part of the link.
func suggestedFilename(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return "download"
	}
	name := path.Base(u.Path)
	if name == "" || name == "." || name == "/" {
		return "download"
	}
	return name
}

// suggestedURL offers a link the reader can check with one keypress. It is a
// domain the IANA reserves for examples, so pressing Enter cannot put load on
// somebody's real server by accident.
func suggestedURL() string {
	return "https://example.com/"
}

// suggestedFolder offers a place to save that is certain to exist, so the
// second question can be answered with one keypress too.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	for _, name := range []string{"Downloads", "Desktop"} {
		candidate := filepath.Join(home, name)
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			return candidate
		}
	}
	return home
}

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