package main

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

// 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.
//
// What guided mode does NOT do is start the server. A running PhoneBridge waits
// for a phone that may never arrive, and a console window sitting on a server
// that only Ctrl+C can end is no better than the window that vanished. So this
// is the readiness check instead: it answers every question that has to be
// right before a hand-off works, in one pass, and returns on its own.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  PhoneBridge")
	fmt.Println("  Move a file between this computer and your phone over your own Wi-Fi.")
	fmt.Println()
	fmt.Println("  No cable, no app on the phone, no account: the phone's browser opens")
	fmt.Println("  an address on this computer and the file comes across.")
	fmt.Println()
	fmt.Println("  I will check that everything a hand-off needs is in place, and show")
	fmt.Println("  you the exact line to type when you want to send it. Nothing is")
	fmt.Println("  shared or opened to the network by this check.")
	fmt.Println()

	suggested := suggestedFolder()
	for {
		fmt.Println("  Which file or folder would you want to send to your phone?")
		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 file or folder. Try again, or close this window.")
			fmt.Println()
			continue
		}

		info, err := os.Stat(answer)
		if err != nil {
			fmt.Println()
			fmt.Printf("  I cannot find %q.\n", answer)
			fmt.Println("  Tip: you can drag a file or a folder from Explorer onto this")
			fmt.Println("  window to paste its location, then press Enter.")
			fmt.Println()
			continue
		}

		fmt.Println()
		readinessCheck(answer, info)
		break
	}

	fmt.Println()
	fmt.Println("  Nothing has been shared. The check opened a port for a moment to")
	fmt.Println("  prove it could, and closed it again.")
	fmt.Println()
	fmt.Println("  PhoneBridge can also receive: run it from a command prompt to accept")
	fmt.Println("  a file your phone sends the other way. Anything that arrives is added")
	fmt.Println("  to the folder you name - a file already in there is never replaced,")
	fmt.Println("  whatever the new one is called. phonebridge help")
	pause(in)
}

// readinessCheck answers, in one pass and without serving anything, the three
// questions that decide whether a hand-off will work: what would be sent, which
// address on this network the phone would have to open, and whether a port can
// be opened at all.
func readinessCheck(path string, info os.FileInfo) {
	abs, err := filepath.Abs(path)
	if err != nil {
		abs = path
	}

	fmt.Println("  Ready to send")
	fmt.Println("  -------------")
	if info.IsDir() {
		files, bytes, names := summarizeFolder(abs)
		fmt.Printf("    folder      %s\n", abs)
		fmt.Printf("    holding     %d file(s), %s\n", files, humanBytes(bytes))
		for _, n := range names {
			fmt.Printf("                  %s\n", n)
		}
		if files > len(names) {
			fmt.Printf("                  ... and %d more\n", files-len(names))
		}
		if files == 0 {
			fmt.Println("    note        that folder is empty, so there would be nothing to fetch")
		}
	} else {
		fmt.Printf("    file        %s\n", abs)
		fmt.Printf("    size        %s\n", humanBytes(info.Size()))
	}

	ip, ipErr := detectLANIP()
	if ipErr != nil {
		fmt.Println("    address     none found")
		fmt.Println("                This computer has no network address a phone could")
		fmt.Println("                reach. Connect it to the same Wi-Fi as the phone.")
	} else {
		fmt.Printf("    address     %s   (your phone must be on the same Wi-Fi)\n", ip)
	}

	// Binding proves the one thing that cannot be reasoned about: that this
	// machine will actually let PhoneBridge open a port. It is closed again
	// immediately — no request is ever answered on it.
	ln, err := net.Listen("tcp", "0.0.0.0:0")
	if err != nil {
		fmt.Printf("    ports       BLOCKED — this computer refused to open one (%v)\n", err)
	} else {
		port := ln.Addr().(*net.TCPAddr).Port
		ln.Close()
		fmt.Printf("    ports       fine — opened port %d as a test and closed it again\n", port)
		if ipErr == nil {
			fmt.Println()
			fmt.Printf("    Your phone would open an address like  http://%s:%d/\n", ip, port)
			fmt.Println("    (the real port is chosen when the hand-off actually starts)")
		}
	}

	fmt.Println()
	fmt.Println("  To send it for real, type this at a command prompt:")
	fmt.Println()
	fmt.Printf("      phonebridge send %s --once\n", quoteIfSpaced(abs))
	fmt.Println()
	fmt.Println("  That prints the address to open on the phone, hands the file over")
	fmt.Println("  once, and stops by itself.")
}

// summarizeFolder counts what is inside a folder and names the first few files,
// so the reader can see at a glance that they picked the right one.
func summarizeFolder(dir string) (count int, total int64, names []string) {
	const showAtMost = 5
	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return nil
		}
		count++
		total += info.Size()
		if len(names) < showAtMost {
			if rel, err := filepath.Rel(dir, path); err == nil {
				names = append(names, rel)
			} else {
				names = append(names, d.Name())
			}
		}
		return nil
	})
	return count, total, names
}

// quoteIfSpaced wraps a path in quotes when it needs them, so the line printed
// for the reader can be typed exactly as shown.
func quoteIfSpaced(p string) string {
	if strings.ContainsAny(p, " \t") {
		return `"` + p + `"`
	}
	return p
}

// suggestedFolder offers somewhere worth sending from that is certain to exist,
// so the reader can get a real answer by pressing one key.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	// What people hand to a phone is nearly always something they just
	// downloaded, or a picture.
	for _, name := range []string{"Downloads", "Pictures"} {
		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()
}
