// Command phonebridge is a local-network HTTP file bridge.
//
// It serves a file or folder from this computer over the LAN so a phone's
// browser can download it, or accepts an upload from a phone's browser back
// to this computer. No app, pairing, or cloud account required — just being
// on the same Wi-Fi / local network.
package main

import (
	"context"
	"flag"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"
)

func usage() {
	fmt.Fprint(os.Stderr, `phonebridge - local-network HTTP file bridge (no cables, no cloud)

Usage:
  phonebridge send <path> [--port 0] [--once] [--host <addr>]
  phonebridge receive <destdir> [--port 0] [--once] [--host <addr>]
  phonebridge help

Commands:
  send      Serve a file or directory over HTTP for a phone to download.
  receive   Serve an upload form over HTTP for a phone to upload files to.

Run "phonebridge send -h" or "phonebridge receive -h" for command help.
`)
}

func sendUsage() {
	fmt.Fprint(os.Stderr, `phonebridge send - serve a file or directory to your phone's browser

Usage:
  phonebridge send <path> [--port 0] [--once] [--host <addr>]

Arguments:
  <path>          File or directory to serve.

Flags:
  --port int      TCP port to bind (default 0 = let the OS pick a free port).
  --once          Shut down automatically after the first successful full
                   download completes. Without this flag the server runs
                   until interrupted (Ctrl+C).
  --host string   Force the host/IP printed in the URL instead of
                   auto-detecting the LAN address (useful on multi-NIC
                   machines where auto-detection guesses wrong).

Examples:
  phonebridge send ~/Downloads/report.pdf --once
  phonebridge send ~/Pictures/vacation --port 8080
`)
}

func receiveUsage() {
	fmt.Fprint(os.Stderr, `phonebridge receive - accept a file upload from your phone's browser

Usage:
  phonebridge receive <destdir> [--port 0] [--once] [--host <addr>]

Arguments:
  <destdir>       Directory to save uploaded files into (created if missing).
                   An upload never replaces a file already there: if the name
                   is taken, it is saved as "photo (2).jpg" and the name used
                   is printed.

Flags:
  --port int      TCP port to bind (default 0 = let the OS pick a free port).
  --once          Shut down automatically after the first successful upload.
                   Without this flag the server runs until interrupted
                   (Ctrl+C).
  --host string   Force the host/IP printed in the URL instead of
                   auto-detecting the LAN address (useful on multi-NIC
                   machines where auto-detection guesses wrong).

Examples:
  phonebridge receive ~/Incoming --once
`)
}

// reorderFlags works around a quirk of Go's flag package: it stops parsing
// flags at the first positional argument. This reorders args so all flags
// come before positional arguments, regardless of where the user put them.
func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

// humanBytes renders a byte count as a human-readable string (e.g. "1.5 MiB").
func humanBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for x := n / unit; x >= unit; x /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// detectLANIP makes a best-effort guess at this machine's LAN-facing IPv4
// address by iterating net.InterfaceAddrs() and returning the first
// non-loopback IPv4 address found. This is a heuristic: on machines with
// multiple network interfaces (VPNs, virtual adapters, multiple NICs) it may
// pick the "wrong" one. Use --host to override when that happens.
func detectLANIP() (string, error) {
	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return "", err
	}
	for _, a := range addrs {
		ipNet, ok := a.(*net.IPNet)
		if !ok || ipNet.IP.IsLoopback() {
			continue
		}
		ip4 := ipNet.IP.To4()
		if ip4 == nil {
			continue
		}
		return ip4.String(), nil
	}
	return "", fmt.Errorf("no non-loopback IPv4 address found")
}

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// one question the program needs and stay on screen. Printing usage
		// and exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}

	switch os.Args[1] {
	case "-h", "--help", "help":
		usage()
		return
	case "send":
		cmdSend(os.Args[2:])
	case "receive":
		cmdReceive(os.Args[2:])
	default:
		fmt.Fprintf(os.Stderr, "phonebridge: unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

// statusRecorder wraps http.ResponseWriter to capture the status code and
// number of bytes written, for logging and for deciding whether a request
// represents a "successful full download" when --once is set.
type statusRecorder struct {
	http.ResponseWriter
	status      int
	bytes       int64
	wroteHeader bool
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.wroteHeader = true
	r.ResponseWriter.WriteHeader(code)
}

func (r *statusRecorder) Write(b []byte) (int, error) {
	n, err := r.ResponseWriter.Write(b)
	r.bytes += int64(n)
	return n, err
}

func newStatusRecorder(w http.ResponseWriter) *statusRecorder {
	return &statusRecorder{ResponseWriter: w, status: http.StatusOK}
}

// resolveHostAndListener binds a TCP listener on 0.0.0.0:port (port 0 picks
// a free port) and resolves the host string to print in URLs: either the
// user-forced --host value, or a best-effort auto-detected LAN IP, falling
// back to loopback with a warning if nothing suitable is found.
func resolveHostAndListener(port int, forcedHost string) (net.Listener, string, error) {
	ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port))
	if err != nil {
		return nil, "", fmt.Errorf("failed to bind port %d: %w", port, err)
	}

	host := forcedHost
	if host == "" {
		ip, ipErr := detectLANIP()
		if ipErr != nil {
			fmt.Fprintf(os.Stderr, "phonebridge: warning: could not auto-detect a LAN IP (%v); falling back to 127.0.0.1 (only reachable from this machine, not your phone). Use --host to specify your LAN IP manually.\n", ipErr)
			host = "127.0.0.1"
		} else {
			host = ip
		}
	}
	return ln, host, nil
}

// runServer starts srv on ln, waits for either an interrupt signal or (if
// once is true) a signal on doneCh, then shuts the server down gracefully.
func runServer(srv *http.Server, ln net.Listener, once bool, doneCh <-chan struct{}) {
	serveErrCh := make(chan error, 1)
	go func() {
		serveErrCh <- srv.Serve(ln)
	}()

	if once {
		select {
		case <-doneCh:
		case err := <-serveErrCh:
			if err != nil && err != http.ErrServerClosed {
				fmt.Fprintf(os.Stderr, "phonebridge: server error: %v\n", err)
			}
			return
		}
	} else {
		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
		select {
		case <-sigCh:
		case err := <-serveErrCh:
			if err != nil && err != http.ErrServerClosed {
				fmt.Fprintf(os.Stderr, "phonebridge: server error: %v\n", err)
			}
			return
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_ = srv.Shutdown(ctx)
	<-serveErrCh
}

func cmdSend(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			sendUsage()
			return
		}
	}

	fs := flag.NewFlagSet("send", flag.ExitOnError)
	port := fs.Int("port", 0, "TCP port to bind (0 = pick a free port)")
	once := fs.Bool("once", false, "shut down after the first successful full download")
	host := fs.String("host", "", "force the host/IP printed in the URL")
	fs.Usage = sendUsage

	valueFlags := map[string]bool{"port": true, "host": true}
	fs.Parse(reorderFlags(args, valueFlags))

	if fs.NArg() < 1 {
		fmt.Fprintln(os.Stderr, "phonebridge send: missing <path>")
		sendUsage()
		os.Exit(1)
	}
	path := fs.Arg(0)

	info, err := os.Stat(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "phonebridge send: cannot access %q: %v\n", path, err)
		os.Exit(1)
	}

	ln, hostStr, err := resolveHostAndListener(*port, *host)
	if err != nil {
		fmt.Fprintf(os.Stderr, "phonebridge send: %v\n", err)
		os.Exit(1)
	}
	actualPort := ln.Addr().(*net.TCPAddr).Port

	dirMode := info.IsDir()

	mux := http.NewServeMux()
	doneCh := make(chan struct{})
	var closeOnce sync.Once

	// isDownload decides whether a completed request counts as "the
	// download" for --once purposes: in file mode any successful GET of
	// the root/filename is the download; in directory mode we require an
	// actual file path (not the auto-generated directory index) so that
	// merely opening the index page doesn't end the session early.
	isDownload := func(r *http.Request) bool {
		if !dirMode {
			return true
		}
		return r.URL.Path != "/" && !strings.HasSuffix(r.URL.Path, "/")
	}

	logAndMaybeFinish := func(w http.ResponseWriter, r *http.Request, handler func(rec *statusRecorder)) {
		rec := newStatusRecorder(w)
		handler(rec)
		fmt.Printf("[%s] %s %s -> %d (%s)\n", time.Now().Format("15:04:05"), r.Method, r.URL.Path, rec.status, humanBytes(rec.bytes))
		if *once && rec.status == http.StatusOK && isDownload(r) {
			closeOnce.Do(func() { close(doneCh) })
		}
	}

	var url string
	if dirMode {
		fileServer := http.FileServer(http.Dir(path))
		mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			logAndMaybeFinish(w, r, func(rec *statusRecorder) { fileServer.ServeHTTP(rec, r) })
		})
		url = fmt.Sprintf("http://%s:%d/", hostStr, actualPort)
	} else {
		filename := filepath.Base(path)
		serveIt := func(w http.ResponseWriter, r *http.Request) {
			logAndMaybeFinish(w, r, func(rec *statusRecorder) { http.ServeFile(rec, r, path) })
		}
		mux.HandleFunc("/", serveIt)
		mux.HandleFunc("/"+filename, serveIt)
		url = fmt.Sprintf("http://%s:%d/%s", hostStr, actualPort, filename)
	}

	fmt.Printf("phonebridge: serving %q\n", path)
	fmt.Printf("phonebridge: open this on your phone (same Wi-Fi):\n\n    %s\n\n", url)
	if *once {
		fmt.Println("phonebridge: will shut down after the first successful download (--once)")
	} else {
		fmt.Println("phonebridge: running until interrupted (Ctrl+C)")
	}

	srv := &http.Server{Handler: mux}
	runServer(srv, ln, *once, doneCh)
	fmt.Println("phonebridge: server stopped")
}

const uploadFormHTML = `<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1"><title>PhoneBridge - Upload</title></head>
<body>
<h1>PhoneBridge</h1>
<p>Send a file from this device to the computer running PhoneBridge.</p>
<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="file" multiple>
  <input type="submit" value="Upload">
</form>
</body>
</html>
`

// maxNameAttempts caps how far createWithoutReplacing will count before giving
// up. A folder holding ten thousand files that all want the same name is not a
// case worth serving, but it is worth refusing in a bounded number of steps
// rather than looping forever.
const maxNameAttempts = 10000

// createWithoutReplacing creates a new file called name in dir, and returns it
// along with the name it actually used. If that name is taken it counts up
// through variations of it — "photo.jpg", then "photo (2).jpg", then
// "photo (3).jpg" — until it finds one that is free.
//
// It exists because the obvious spelling of this, os.Create, truncates. An
// upload arriving from a phone carries whatever name the phone gave it, and
// phones hand out the same handful of names constantly: IMG_0001.JPG,
// image.jpg, document.pdf. With os.Create, a second phone uploading its own
// IMG_0001.JPG silently destroyed the first one, and there is no undo and no
// warning — the file was simply gone. Nothing about receiving a file should be
// able to lose one, so the only mode used here is O_EXCL, which fails rather
// than opens when the path already exists. That makes replacing a file
// impossible by construction rather than by remembering to check first.
func createWithoutReplacing(dir, name string) (*os.File, string, error) {
	ext := filepath.Ext(name)
	stem := strings.TrimSuffix(name, ext)
	// A name that is all extension (".gitignore") has no stem to count after,
	// so treat the whole thing as the stem and keep the counter at the end.
	if stem == "" {
		stem, ext = name, ""
	}

	for n := 1; n <= maxNameAttempts; n++ {
		candidate := name
		if n > 1 {
			candidate = fmt.Sprintf("%s (%d)%s", stem, n, ext)
		}
		f, err := os.OpenFile(filepath.Join(dir, candidate), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
		if err == nil {
			return f, candidate, nil
		}
		if !os.IsExist(err) {
			return nil, "", err
		}
	}
	return nil, "", fmt.Errorf("cannot find a free name for %q in %s: %q and %d numbered variations of it are all taken",
		name, dir, name, maxNameAttempts-1)
}

func cmdReceive(args []string) {
	for _, a := range args {
		if a == "-h" || a == "--help" || a == "help" {
			receiveUsage()
			return
		}
	}

	fs := flag.NewFlagSet("receive", flag.ExitOnError)
	port := fs.Int("port", 0, "TCP port to bind (0 = pick a free port)")
	once := fs.Bool("once", false, "shut down after the first successful upload")
	host := fs.String("host", "", "force the host/IP printed in the URL")
	fs.Usage = receiveUsage

	valueFlags := map[string]bool{"port": true, "host": true}
	fs.Parse(reorderFlags(args, valueFlags))

	if fs.NArg() < 1 {
		fmt.Fprintln(os.Stderr, "phonebridge receive: missing <destdir>")
		receiveUsage()
		os.Exit(1)
	}
	destDir := fs.Arg(0)

	if err := os.MkdirAll(destDir, 0o755); err != nil {
		fmt.Fprintf(os.Stderr, "phonebridge receive: cannot create %q: %v\n", destDir, err)
		os.Exit(1)
	}

	ln, hostStr, err := resolveHostAndListener(*port, *host)
	if err != nil {
		fmt.Fprintf(os.Stderr, "phonebridge receive: %v\n", err)
		os.Exit(1)
	}
	actualPort := ln.Addr().(*net.TCPAddr).Port
	url := fmt.Sprintf("http://%s:%d/", hostStr, actualPort)

	mux := http.NewServeMux()
	doneCh := make(chan struct{})
	var closeOnce sync.Once

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(w, uploadFormHTML)
	})

	mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		if err := r.ParseMultipartForm(64 << 20); err != nil {
			http.Error(w, "bad upload: "+err.Error(), http.StatusBadRequest)
			fmt.Printf("[%s] upload FAILED: %v\n", time.Now().Format("15:04:05"), err)
			return
		}
		if r.MultipartForm == nil || len(r.MultipartForm.File["file"]) == 0 {
			http.Error(w, "no file uploaded (expected field \"file\")", http.StatusBadRequest)
			return
		}

		var saved []string
		for _, fh := range r.MultipartForm.File["file"] {
			src, err := fh.Open()
			if err != nil {
				fmt.Printf("[%s] upload FAILED for %q: %v\n", time.Now().Format("15:04:05"), fh.Filename, err)
				continue
			}
			// filepath.Base strips any path components the client sent,
			// preventing writes outside destDir.
			safeName := filepath.Base(fh.Filename)
			if safeName == "." || safeName == "/" || safeName == "" {
				safeName = "upload.bin"
			}
			dst, usedName, err := createWithoutReplacing(destDir, safeName)
			if err != nil {
				src.Close()
				fmt.Printf("[%s] upload FAILED for %q: %v\n", time.Now().Format("15:04:05"), safeName, err)
				continue
			}
			n, err := io.Copy(dst, src)
			src.Close()
			dst.Close()
			if err != nil {
				fmt.Printf("[%s] upload FAILED for %q: %v\n", time.Now().Format("15:04:05"), usedName, err)
				continue
			}
			if usedName == safeName {
				fmt.Printf("[%s] received %q (%s)\n", time.Now().Format("15:04:05"), usedName, humanBytes(n))
			} else {
				fmt.Printf("[%s] received %q (%s), saved as %q because %q was already in this folder and has been left as it was\n",
					time.Now().Format("15:04:05"), safeName, humanBytes(n), usedName, safeName)
			}
			saved = append(saved, usedName)
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if len(saved) == 0 {
			http.Error(w, "upload failed", http.StatusInternalServerError)
			return
		}
		fmt.Fprintf(w, "<!DOCTYPE html><html><body><h1>Upload complete</h1><p>Received: %s</p></body></html>", strings.Join(saved, ", "))

		if *once {
			closeOnce.Do(func() { close(doneCh) })
		}
	})

	fmt.Printf("phonebridge: receiving into %q\n", destDir)
	fmt.Printf("phonebridge: open this on your phone (same Wi-Fi) and choose a file to upload:\n\n    %s\n\n", url)
	if *once {
		fmt.Println("phonebridge: will shut down after the first successful upload (--once)")
	} else {
		fmt.Println("phonebridge: running until interrupted (Ctrl+C)")
	}

	srv := &http.Server{Handler: mux}
	runServer(srv, ln, *once, doneCh)
	fmt.Println("phonebridge: server stopped")
}
