package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

// 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.
//
// MacroDeck has no read-only command: "watch" exists to run somebody's command
// for them, and a watch loop never returns on its own. Guided mode therefore
// does the honest read-only half — it shows what is in the folder right now,
// which of those files a rule would fire on, and the exact command line that
// sets the rule up — and runs nothing at all.
func runGuided() {
	in := bufio.NewScanner(os.Stdin)

	fmt.Println()
	fmt.Println("  MacroDeck")
	fmt.Println("  Watch a folder and run a command of your choosing whenever a file")
	fmt.Println("  lands in it or changes.")
	fmt.Println()
	fmt.Println("  A rule looks like \"when a screenshot appears in Downloads, move it")
	fmt.Println("  into Documents\\Screenshots\". This window will show you what is in")
	fmt.Println("  a folder now and what a rule would see. It runs nothing, and it")
	fmt.Println("  changes nothing.")
	fmt.Println()

	folder, ok := askFolder(in)
	if !ok {
		return
	}
	previewFolder(folder)

	fmt.Println()
	fmt.Println("  To set a rule running, type this at a command prompt:")
	fmt.Printf("    macrodeck watch \"%s\" --run \"your-command {file}\"\n", folder)
	fmt.Println("  {file} is replaced with the full path of the file that triggered it.")
	fmt.Println("  Add --once to do a single pass instead of watching forever.")
	fmt.Println("  There is more detail in: macrodeck --help")
	pause(in)
}

// askFolder asks for the folder to look at and keeps asking until the answer
// is a folder that really exists. It reports false only when stdin closes, at
// which point there is nothing sensible left to ask.
func askFolder(in *bufio.Scanner) (string, bool) {
	suggested := suggestedFolder()
	for {
		fmt.Println("  Which folder would you like to watch?")
		if suggested != "" {
			fmt.Printf("  (press Enter for %s)\n", suggested)
		}
		fmt.Print("  > ")

		if !in.Scan() {
			return "", false
		}
		answer := strings.TrimSpace(in.Text())
		answer = strings.Trim(answer, `"`)
		if answer == "" {
			answer = suggested
		}
		if answer == "" {
			fmt.Println()
			fmt.Println("  I need a folder to look at. 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 it sits in.\n", answer)
			fmt.Println()
			continue
		}
		return answer, true
	}
}

// previewFolder lists what a watch rule would be looking at. It only reads.
func previewFolder(dir string) {
	fmt.Println()
	fmt.Printf("  Looking at %s\n", dir)
	fmt.Println()

	entries, err := os.ReadDir(dir)
	if err != nil {
		fmt.Println("  I could not read that folder. It may be in use, or you may not")
		fmt.Println("  have permission to look inside it.")
		return
	}

	type row struct {
		name string
		size int64
		mod  time.Time
	}
	var rows []row
	var folders int
	for _, e := range entries {
		if e.IsDir() {
			folders++
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		rows = append(rows, row{name: e.Name(), size: info.Size(), mod: info.ModTime()})
	}

	if len(rows) == 0 {
		fmt.Println("  There are no files in this folder yet. A rule would sit quietly")
		fmt.Println("  until the first one arrives.")
		if folders > 0 {
			fmt.Printf("  (%d sub-folder(s) here — MacroDeck watches this folder only.)\n", folders)
		}
		return
	}

	sort.Slice(rows, func(i, j int) bool { return rows[i].mod.After(rows[j].mod) })

	byExt := map[string]int{}
	for _, r := range rows {
		ext := strings.ToLower(filepath.Ext(r.name))
		if ext == "" {
			ext = "(no extension)"
		}
		byExt[ext]++
	}

	fmt.Printf("  %d file(s) here, newest first:\n\n", len(rows))
	const show = 15
	for i, r := range rows {
		if i == show {
			fmt.Printf("  ... and %d more\n", len(rows)-show)
			break
		}
		fmt.Printf("  %10s  %s  %s\n", humanBytes(r.size), r.mod.Format("2006-01-02 15:04"), r.name)
	}

	exts := make([]string, 0, len(byExt))
	for e := range byExt {
		exts = append(exts, e)
	}
	sort.Slice(exts, func(i, j int) bool {
		if byExt[exts[i]] != byExt[exts[j]] {
			return byExt[exts[i]] > byExt[exts[j]]
		}
		return exts[i] < exts[j]
	})
	fmt.Println()
	fmt.Print("  Kinds of file here: ")
	parts := make([]string, 0, len(exts))
	for _, e := range exts {
		parts = append(parts, fmt.Sprintf("%s x%d", e, byExt[e]))
	}
	fmt.Println(strings.Join(parts, ", "))
	fmt.Println("  A rule can be limited to some of those with --ext .png,.jpg")
	if folders > 0 {
		fmt.Printf("  (%d sub-folder(s) ignored — MacroDeck watches this folder only.)\n", folders)
	}
}

// humanBytes renders a byte count the way a person reads one.
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])
}

// suggestedFolder offers somewhere worth watching that is certain to exist, so
// the reader can get a useful answer by pressing one key.
func suggestedFolder() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	// Downloads is where the files people want automated usually land.
	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()
}
