// MacroDeck — run a command automatically when files change in a watched
// folder. The automation primitive behind rules like "when a screenshot
// lands in Downloads, move it to Documents/Screenshots".
//
// Usage:
//
//	macrodeck watch <dir> --run "<command>" [--ext .png,.jpg] [--interval 2s]
//	                       [--run-existing] [--once]
//
// {file} in --run is replaced with the triggered file's path. MacroDeck
// polls rather than using OS file-event APIs, so it has no platform-specific
// dependencies and the same binary behaves identically on every OS —
// the tradeoff is latency bounded by --interval, not instant.
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

type fileState struct {
	modTime time.Time
	size    int64
}

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 "watch":
		cmdWatch(os.Args[2:])
	case "-h", "--help", "help":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "unknown command %q\n\n", os.Args[1])
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `MacroDeck — run a command when files change in a watched folder

Usage:
  macrodeck watch <dir> --run "<command>" [--ext .png,.jpg] [--interval 2s]
                         [--run-existing] [--once]

  {file} in --run is substituted with the triggered file's absolute path.
  --once does a single existing-files pass then exits (good for testing
  a rule or running it from cron/Task Scheduler instead of as a daemon).
`)
}

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 cmdWatch(args []string) {
	var dir, runCmd, extList, intervalStr string
	var runExisting, once bool
	positional := []string{}

	reordered := reorderFlags(args, map[string]bool{"run": true, "ext": true, "interval": true})
	for i := 0; i < len(reordered); i++ {
		a := reordered[i]
		switch a {
		case "--run":
			i++
			if i < len(reordered) {
				runCmd = reordered[i]
			}
		case "--ext":
			i++
			if i < len(reordered) {
				extList = reordered[i]
			}
		case "--interval":
			i++
			if i < len(reordered) {
				intervalStr = reordered[i]
			}
		case "--run-existing":
			runExisting = true
		case "--once":
			once = true
		default:
			if !strings.HasPrefix(a, "-") {
				positional = append(positional, a)
			}
		}
	}

	if len(positional) != 1 || runCmd == "" {
		fmt.Fprintln(os.Stderr, `usage: macrodeck watch <dir> --run "<command>" [--ext .png,.jpg] [--interval 2s] [--run-existing] [--once]`)
		os.Exit(1)
	}
	dir = positional[0]

	interval := 2 * time.Second
	if intervalStr != "" {
		d, err := time.ParseDuration(intervalStr)
		if err != nil {
			fmt.Fprintln(os.Stderr, "invalid --interval:", err)
			os.Exit(1)
		}
		interval = d
	}

	var exts []string
	if extList != "" {
		for _, e := range strings.Split(extList, ",") {
			exts = append(exts, strings.ToLower(strings.TrimSpace(e)))
		}
	}

	fmt.Printf("MacroDeck watching %s (interval %s)\n", dir, interval)
	if len(exts) > 0 {
		fmt.Printf("  filtering extensions: %s\n", strings.Join(exts, ", "))
	}
	fmt.Printf("  on trigger: %s\n\n", runCmd)

	known := map[string]fileState{}
	first := true
	for {
		entries, err := os.ReadDir(dir)
		if err != nil {
			fmt.Fprintln(os.Stderr, "error reading dir:", err)
			os.Exit(1)
		}
		for _, e := range entries {
			if e.IsDir() {
				continue
			}
			if len(exts) > 0 && !matchesExt(e.Name(), exts) {
				continue
			}
			path := filepath.Join(dir, e.Name())
			info, err := e.Info()
			if err != nil {
				continue
			}
			state := fileState{modTime: info.ModTime(), size: info.Size()}
			prev, seen := known[path]
			known[path] = state

			isNewOrChanged := !seen || prev != state
			shouldTrigger := isNewOrChanged && (!first || runExisting)
			if shouldTrigger {
				runTrigger(runCmd, path)
			}
		}
		first = false
		if once {
			break
		}
		time.Sleep(interval)
	}
}

func matchesExt(name string, exts []string) bool {
	lower := strings.ToLower(filepath.Ext(name))
	for _, e := range exts {
		if lower == e {
			return true
		}
	}
	return false
}

func runTrigger(cmdTemplate, filePath string) {
	command := strings.ReplaceAll(cmdTemplate, "{file}", filePath)
	fmt.Printf("[trigger] %s -> %s\n", filePath, command)

	var cmd *exec.Cmd
	if runtime.GOOS == "windows" {
		cmd = exec.Command("cmd", "/C", command)
	} else {
		cmd = exec.Command("sh", "-c", command)
	}
	cmd.Env = append(os.Environ(), "MACRODECK_FILE="+filePath)
	out, err := cmd.CombinedOutput()
	if len(out) > 0 {
		fmt.Print(string(out))
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "  command failed: %v\n", err)
	}
}
