// Command snapbeam is a receiver that stays open: it waits on your own Wi-Fi
// for a screenshot or a snippet of text from your phone and puts it straight
// on your desk.
//
// It speaks two languages. The first is the LocalSend protocol v2.2
// (https://github.com/localsend/protocol), so the official LocalSend app —
// a separate Apache-2.0 project, not ours — discovers this computer and sends
// to it with no setup at all. The second is a self-contained web page that
// SnapBeam serves itself, reached by pointing a phone camera at the QR code it
// draws in the terminal, for a phone with nothing installed.
//
// Both paths land in the same folder and print the same line.
package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"runtime"
	"strings"
	"time"
)

const appName = "snapbeam"

// stderr is a variable so tests can capture what the program complains about.
var stderr io.Writer = os.Stderr

// ---------------------------------------------------------------------------
// Shared Techlosoft UX helpers (verbatim across the tool line)
// ---------------------------------------------------------------------------

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...)
}

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])
}

// stamp is the clock prefix on every line the running receiver prints.
func stamp() string { return time.Now().Format("[15:04:05]") }

func isTooLarge(err error) bool { return errors.Is(err, errTooLarge) }

// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------

func usage(w io.Writer) {
	fmt.Fprintf(w, `%s - get a screenshot or a snippet of text off your phone and onto your desk
(Techlosoft Device Bridge Center)

USAGE
  %s start [--port 53317] [--dir <folder>] [--name "Study PC"]
                 [--max-size 256MB] [--host <addr>] [--allow-public]
                 [--no-localsend] [--no-code] [--qr blocks|ascii|invert]
                 [--discovery-port 53317]
  %s list  [--dir <folder>] [--json]
  %s peers [--seconds 5] [--port 53317] [--allow-public]
  %s send  <file> [<file> ...] --to <host[:port]> [--code 123456]
  %s help | -h | --help

COMMANDS
  start   Open the receiver and stay open. Prints the LAN address, a fresh
          six-digit pairing code and a QR code, then waits. Two ways in:

            LocalSend  The official LocalSend app (a separate Apache-2.0
                       project, https://localsend.org) finds this computer by
                       itself. Screenshot, Share, LocalSend, tap this PC.
            Browser    Point the phone camera at the QR code. Your own
                       computer serves the page; nothing is installed.

          Everything that arrives lands in the save folder with a timestamped
          name, is announced on this terminal, and - if it is text - is put on
          this computer's clipboard.

  list    What has arrived so far, newest first, with size and type.
  peers   Listen for LocalSend devices on this network and list them. Useful
          for checking that multicast works here before blaming SnapBeam.
  send    Send files TO a LocalSend device, using the same protocol.

FLAGS
  --port <n>       TCP port of the HTTP server (default 53317, the LocalSend
                   default). The announcement tells other devices which port
                   to use, so this may be changed on its own.
  --discovery-port UDP port of the multicast group (default 53317). Change it
                   only to keep two receivers on one machine apart; every
                   device that should see the other must use the same one.
  --dir <folder>   Where incoming items are saved (default: SnapBeam in your
                   home folder). Created if missing.
  --name <text>    The name your phone sees (default: this machine's hostname).
  --max-size <sz>  Largest single item accepted: 256MB, 1.5G, or raw bytes.
  --host <addr>    Bind to this address instead of the auto-detected LAN one.
  --allow-public   Required before SnapBeam will bind to an address that is
                   not in a private range. Read the README first.
  --no-localsend   Browser page only: do not join the multicast group and do
                   not serve the LocalSend API.
  --no-code        Run with NO pairing code. Anyone on the network can send to
                   you. Only sensible on a network you control completely.
  --qr <style>     blocks (default), ascii for terminals without block
                   characters, invert for a light-background terminal.
  --code <digits>  For "send": the pairing code the far end is asking for.
  --to <host>      For "send": the device to send to, "192.168.1.30:53317".
  --seconds <n>    For "peers": how long to listen.
  --json           Machine-readable output (list).

EXAMPLES
  %s start
  %s start --dir ~/Screenshots --name "Study PC" --port 53317
  %s list
  %s peers --seconds 8
  %s send shot.png --to 192.168.1.30 --code 123456

Flags may appear before or after positional arguments.

SnapBeam is compatible with LocalSend. It is not LocalSend, is not affiliated
with the LocalSend project, and contains none of its code - it implements the
published protocol. LocalSend is Apache-2.0: https://github.com/localsend
`, appName, appName, appName, appName, appName, appName,
		appName, appName, appName, appName, appName)
}

func fail(format string, args ...any) {
	fmt.Fprintf(stderr, "%s: %s\n", appName, fmt.Sprintf(format, args...))
	os.Exit(1)
}

func usageErr(format string, args ...any) {
	fmt.Fprintf(stderr, "%s: %s\n\n", appName, fmt.Sprintf(format, args...))
	usage(stderr)
	os.Exit(1)
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

func main() {
	args := os.Args[1:]
	if len(args) == 0 {
		// 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(stderr)
		os.Exit(1)
	}
	switch args[0] {
	case "help", "-h", "--help":
		usage(os.Stdout)
		os.Exit(0)
	}
	cmd := args[0]
	rest := args[1:]
	for _, a := range rest {
		if a == "-h" || a == "--help" || a == "help" {
			usage(os.Stdout)
			os.Exit(0)
		}
	}
	switch cmd {
	case "start":
		cmdStart(rest)
	case "list":
		cmdList(rest)
	case "peers":
		cmdPeers(rest)
	case "send":
		cmdSend(rest)
	default:
		usageErr("unknown command %q", cmd)
	}
}

// ---------------------------------------------------------------------------
// Flag plumbing
// ---------------------------------------------------------------------------

var valueFlags = map[string]bool{
	"port": true, "p": true,
	"discovery-port": true,
	"dir":            true, "d": true,
	"name": true, "n": true,
	"max-size": true,
	"host":     true,
	"qr":       true,
	"code":     true,
	"to":       true,
	"seconds":  true,
}

func newFlagSet(name string) *flag.FlagSet {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	fs.SetOutput(stderr)
	fs.Usage = func() { usage(stderr) }
	return fs
}

// parseSize accepts "256MB", "1.5G", "2TiB" or a raw byte count. Suffixes are
// binary, exactly as in the rest of the Techlosoft line: 1KB == 1024 bytes.
func parseSize(s string) (int64, error) {
	t := strings.TrimSpace(s)
	if t == "" {
		return 0, errors.New("empty size")
	}
	t = strings.ReplaceAll(t, "_", "")
	t = strings.ReplaceAll(t, ",", "")
	up := strings.ToUpper(t)
	up = strings.TrimSuffix(up, "B")
	up = strings.TrimSuffix(up, "I")
	up = strings.TrimSpace(up)
	mult := int64(1)
	if up != "" {
		switch up[len(up)-1] {
		case 'K':
			mult = 1 << 10
		case 'M':
			mult = 1 << 20
		case 'G':
			mult = 1 << 30
		case 'T':
			mult = 1 << 40
		}
	}
	if mult != 1 {
		up = up[:len(up)-1]
	}
	up = strings.TrimSpace(up)
	if up == "" {
		return 0, fmt.Errorf("invalid size %q", s)
	}
	var value float64
	if _, err := fmt.Sscanf(up, "%g", &value); err != nil {
		return 0, fmt.Errorf("invalid size %q", s)
	}
	if value <= 0 {
		return 0, fmt.Errorf("size %q must be positive", s)
	}
	total := value * float64(mult)
	const maxSane = 1 << 43 // 8 TiB; beyond this the cap is not doing anything
	if total > maxSane {
		return 0, fmt.Errorf("size %q is larger than this program will accept", s)
	}
	return int64(total), nil
}

// deviceModelName is what the phone shows under the alias: the operating
// system, in the words the LocalSend UI expects.
func deviceModelName() string {
	switch runtime.GOOS {
	case "windows":
		return "Windows"
	case "darwin":
		return "macOS"
	case "linux":
		return "Linux"
	default:
		return strings.ToUpper(runtime.GOOS[:1]) + runtime.GOOS[1:]
	}
}

// defaultAlias is the name this computer announces: its hostname, which is
// what somebody looking at a phone will recognise.
func defaultAlias() string {
	if h, err := os.Hostname(); err == nil && strings.TrimSpace(h) != "" {
		if i := strings.IndexByte(h, '.'); i > 0 {
			h = h[:i]
		}
		return h
	}
	return "SnapBeam PC"
}
