// streamdeck-init — Interactive TUI for configuring Stream Deck keys.
//
// This is a config builder, not an installer. It reads and appends to
// ~/.config/streamdeck-go/config.yaml (or a custom path via -config).
// It does NOT touch services, binaries, udev rules, or dependencies.
//
// ## How it works
//
// 1. Bootstrap: ensures config.yaml and modules.yaml exist (creates from
//    defaults/embedded example if missing).
// 2. Loads both files using the same internal/config and internal/modules
//    packages the daemon uses.
// 3. Renders an 8x4 key grid showing which slots are free vs occupied.
// 4. Walks the user through a series of huh forms:
//    - Pick a key slot (0-31)
//    - Choose action type: module function or raw shell command
//    - If module: pick module → function → customize params (with defaults)
//    - If command: type a shell command
//    - Pick an icon from icons_dir (or type a custom filename)
//    - Confirm and append to config.yaml
// 5. Loops: asks "Add another key?" after each addition.
//
// ## Expanding this tool
//
// To add new TUI steps (e.g. toggle key setup with poll config, or a
// "delete key" flow), add a new function following the pattern of
// configureModuleKey/configureCommandKey: build huh forms, collect values
// into a keyEntry, return it for the confirm/append step.
//
// To support new key types in the YAML output, update keyEntry and
// renderKeySnippet() in yaml.go.
//
// The grid rendering is in grid.go — it's hardcoded to 8x4 (XL). To
// support other models, make gridCols/gridRows dynamic based on the
// device config's product_id.
//
// ## Dependencies
//
// - github.com/charmbracelet/huh — form/prompt library (select, input, confirm)
// - github.com/charmbracelet/lipgloss — terminal styling (grid rendering, summary output)
// - internal/config — shared config loader and DefaultConfigPath()
// - internal/modules — shared module registry loader
// - internal/defaults — embedded modules.example.yaml for bootstrapping
package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/WoodardDigital/streamdeck-go/internal/config"
	"github.com/WoodardDigital/streamdeck-go/internal/defaults"
	"github.com/WoodardDigital/streamdeck-go/internal/modules"
	"github.com/charmbracelet/huh"
	"github.com/charmbracelet/lipgloss"
)

func main() {
	cfgPath := flag.String("config", config.DefaultConfigPath(), "path to config file")
	flag.Parse()

	// Bootstrap: ensure config dir, config.yaml, and modules.yaml exist.
	// On a fresh install this creates everything from scratch so the user
	// doesn't need to manually create files before running the TUI.
	if err := bootstrap(*cfgPath); err != nil {
		fmt.Fprintf(os.Stderr, "setup error: %v\n", err)
		os.Exit(1)
	}

	// Main loop — each iteration configures one key, then asks to continue.
	for {
		if err := runOnce(*cfgPath); err != nil {
			fmt.Fprintf(os.Stderr, "error: %v\n", err)
			os.Exit(1)
		}

		var again bool
		form := huh.NewForm(
			huh.NewGroup(
				huh.NewConfirm().
					Title("Add another key?").
					Value(&again),
			),
		)
		if err := form.Run(); err != nil {
			break
		}
		if !again {
			break
		}
	}

	fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true).Render("✓") +
		" Done — the daemon will auto-reload your config.")
}

// bootstrap ensures the config directory, config.yaml, and modules.yaml exist.
//
// Creates:
//   - ~/.config/streamdeck-go/         (config dir)
//   - ~/.config/streamdeck-go/icons/   (icon storage)
//   - ~/.config/streamdeck-go/config.yaml  (from inline default if missing)
//   - ~/.config/streamdeck-go/modules.yaml (from embedded example if missing)
//
// The embedded modules.example.yaml comes from internal/defaults, which uses
// //go:embed so the init binary works standalone without the repo present.
func bootstrap(cfgPath string) error {
	dir := filepath.Dir(cfgPath)
	iconsDir := filepath.Join(dir, "icons")
	if err := os.MkdirAll(iconsDir, 0755); err != nil {
		return err
	}

	// Create a minimal config.yaml if one doesn't exist yet.
	// Uses "keys: {}" (empty map) which appendKeyToConfig handles specially —
	// it replaces the {} with actual key entries on first append.
	if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
		defaultCfg := fmt.Sprintf(`# streamdeck-go configuration
icons_dir: %s
brightness: 70

device:
  vendor_id: 0x0fd9
  product_id: 0x00ba

keys: {}
`, iconsDir)
		if err := os.WriteFile(cfgPath, []byte(defaultCfg), 0644); err != nil {
			return err
		}
		fmt.Printf("Created %s\n", cfgPath)
	}

	// Copy the embedded modules.example.yaml so module functions are available
	// immediately. Users can edit this file to add custom modules later.
	modPath := config.ModulesPath(cfgPath)
	if _, err := os.Stat(modPath); os.IsNotExist(err) {
		if err := os.WriteFile(modPath, defaults.ModulesExampleYAML, 0644); err != nil {
			return err
		}
		fmt.Printf("Created %s\n", modPath)
	}

	return nil
}

// runOnce performs one complete key configuration cycle:
//
//	load config → show grid → pick slot → pick action → pick icon → confirm → write
//
// Config is re-loaded at the start of each cycle so that keys added in the
// previous iteration show up as occupied in the grid.
func runOnce(cfgPath string) error {
	// Re-read config each iteration to pick up keys added in previous cycles.
	cfg, err := config.Load(cfgPath)
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}

	// Load the module registry to enumerate available modules/functions.
	reg, err := modules.LoadRegistry(config.ModulesPath(cfgPath))
	if err != nil {
		return fmt.Errorf("load modules: %w", err)
	}

	// Show the 8x4 key grid so the user can see which slots are taken.
	fmt.Print(renderGrid(cfg.Keys))

	// Step 1: Pick a key slot (0-31).
	keyIdx, err := pickKeySlot(cfg.Keys)
	if err != nil {
		return err
	}

	// Step 2: Choose between module function or raw shell command.
	actionType, err := pickActionType()
	if err != nil {
		return err
	}

	// Step 3: Configure the key based on action type.
	var entry keyEntry
	entry.Index = keyIdx

	if actionType == "module" {
		// Module path: module → function → params (with defaults pre-filled).
		entry, err = configureModuleKey(keyIdx, reg)
	} else {
		// Command path: just a shell command string.
		entry, err = configureCommandKey(keyIdx)
	}
	if err != nil {
		return err
	}

	// Step 4: Pick an icon file.
	icon, err := pickIcon(cfg.IconsDir)
	if err != nil {
		return err
	}
	entry.Icon = icon

	// Step 5: Show summary and confirm before writing.
	fmt.Println()
	fmt.Println(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")).Render("  Summary:"))
	fmt.Printf("    Key:      %d\n", entry.Index)
	fmt.Printf("    Icon:     %s\n", entry.Icon)
	if entry.Module != "" {
		fmt.Printf("    Module:   %s\n", entry.Module)
		fmt.Printf("    Function: %s\n", entry.Function)
		if len(entry.Params) > 0 {
			fmt.Println("    Params:")
			for k, v := range entry.Params {
				fmt.Printf("      %s: %s\n", k, v)
			}
		}
	} else {
		fmt.Printf("    Command:  %s\n", entry.Command)
	}
	fmt.Println()

	var confirm bool
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewConfirm().
				Title("Write this key to config?").
				Value(&confirm),
		),
	)
	if err := form.Run(); err != nil {
		return err
	}
	if !confirm {
		fmt.Println("  Skipped.")
		return nil
	}

	// Step 6: Append the YAML snippet to config.yaml.
	// This uses raw text append (not marshal/unmarshal) to preserve comments.
	if err := appendKeyToConfig(cfgPath, entry); err != nil {
		return fmt.Errorf("write config: %w", err)
	}

	fmt.Println(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true).Render("  ✓") +
		fmt.Sprintf(" Key %d added to config.", entry.Index))
	return nil
}

// pickKeySlot presents a select list of all 32 key slots, showing which are
// occupied and what they're mapped to. Returns the selected key index.
//
// FUTURE: filter to show only free keys, or add a "overwrite?" confirmation
// when an occupied slot is picked.
func pickKeySlot(keys map[int]config.KeyConfig) (int, error) {
	options := make([]huh.Option[int], gridKeys)
	for i := 0; i < gridKeys; i++ {
		label := fmt.Sprintf("%-3d", i)
		if k, ok := keys[i]; ok {
			label += fmt.Sprintf("  (occupied — %s)", keyLabel(k))
		} else {
			label += "  (free)"
		}
		options[i] = huh.NewOption(label, i)
	}

	var keyIdx int
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[int]().
				Title("Pick a key slot").
				Options(options...).
				Value(&keyIdx),
		),
	)
	if err := form.Run(); err != nil {
		return 0, err
	}
	return keyIdx, nil
}

// pickActionType asks whether to configure a module function or a raw shell command.
//
// FUTURE: add "Toggle key (with poll)" as a third option, which would walk
// through icon_true/icon_false and poll config setup.
func pickActionType() (string, error) {
	var actionType string
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("What should this key do?").
				Options(
					huh.NewOption("Module function (Slack, etc.)", "module"),
					huh.NewOption("Shell command", "command"),
				).
				Value(&actionType),
		),
	)
	if err := form.Run(); err != nil {
		return "", err
	}
	return actionType, nil
}

// configureModuleKey walks the user through: module → function → params.
//
// Modules and functions are enumerated dynamically from the loaded registry,
// so any module added to modules.yaml automatically appears in the TUI.
//
// For each param defined in the function's FunctionDef.Params, an input field
// is shown pre-filled with the default value. The user can accept defaults or
// override per-key. Functions with no params (e.g. clear_status) skip this step.
//
// FUTURE: show a description/help text for each function (would require adding
// a "description" field to FunctionDef in internal/modules/modules.go).
func configureModuleKey(keyIdx int, reg *modules.Registry) (keyEntry, error) {
	entry := keyEntry{Index: keyIdx}

	if reg == nil || len(reg.Modules) == 0 {
		return entry, fmt.Errorf("no modules defined in modules.yaml")
	}

	// Pick module — enumerate all top-level keys from modules.yaml.
	modNames := make([]string, 0, len(reg.Modules))
	for name := range reg.Modules {
		modNames = append(modNames, name)
	}
	sort.Strings(modNames)

	var modName string
	modOpts := make([]huh.Option[string], len(modNames))
	for i, name := range modNames {
		modOpts[i] = huh.NewOption(name, name)
	}

	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Pick a module").
				Options(modOpts...).
				Value(&modName),
		),
	)
	if err := form.Run(); err != nil {
		return entry, err
	}
	entry.Module = modName

	// Pick function — enumerate all functions within the chosen module.
	mod := reg.Modules[modName]
	fnNames := make([]string, 0, len(mod))
	for name := range mod {
		fnNames = append(fnNames, name)
	}
	sort.Strings(fnNames)

	var fnName string
	fnOpts := make([]huh.Option[string], len(fnNames))
	for i, name := range fnNames {
		fnOpts[i] = huh.NewOption(name, name)
	}

	form = huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title(fmt.Sprintf("Pick a function from %s", modName)).
				Options(fnOpts...).
				Value(&fnName),
		),
	)
	if err := form.Run(); err != nil {
		return entry, err
	}
	entry.Function = fnName

	// Customize params — one input per param, pre-filled with the module default.
	// paramPtrs is a []string (not []*string) so that &paramPtrs[i] gives a stable
	// pointer to each slice element — huh.NewInput().Value() needs a *string.
	fn := mod[fnName]
	if len(fn.Params) > 0 {
		paramKeys := make([]string, 0, len(fn.Params))
		for k := range fn.Params {
			paramKeys = append(paramKeys, k)
		}
		sort.Strings(paramKeys)

		paramPtrs := make([]string, len(paramKeys))
		paramFields := make([]huh.Field, len(paramKeys))
		for i, k := range paramKeys {
			paramPtrs[i] = fn.Params[k] // pre-fill with default
			paramFields[i] = huh.NewInput().
				Title(k).
				Value(&paramPtrs[i]).
				Placeholder(fn.Params[k])
		}

		form = huh.NewForm(
			huh.NewGroup(paramFields...),
		)
		if err := form.Run(); err != nil {
			return entry, err
		}

		// Collect the (possibly edited) values back into the entry.
		entry.Params = make(map[string]string, len(paramKeys))
		for i, k := range paramKeys {
			entry.Params[k] = paramPtrs[i]
		}
	}

	return entry, nil
}

// configureCommandKey asks for a raw shell command string.
// This is the non-module path — the command is written directly to config.yaml.
func configureCommandKey(keyIdx int) (keyEntry, error) {
	entry := keyEntry{Index: keyIdx}

	var cmd string
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Title("Shell command").
				Value(&cmd).
				Placeholder("e.g. open -a Firefox"),
		),
	)
	if err := form.Run(); err != nil {
		return entry, err
	}
	entry.Command = cmd
	return entry, nil
}

// pickIcon lets the user select an icon file.
//
// If icons_dir has image files, shows a select list of them plus a "type custom"
// option. If the directory is empty, falls back to a plain text input.
//
// Supported extensions: .png, .jpg, .jpeg, .gif, .svg
//
// FUTURE: show icon previews (would require a terminal image protocol like
// sixel or kitty graphics). For now, filenames are enough.
func pickIcon(iconsDir string) (string, error) {
	// Scan icons_dir for image files.
	entries, err := os.ReadDir(iconsDir)
	if err != nil && !os.IsNotExist(err) {
		return "", err
	}

	var iconFiles []string
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		ext := strings.ToLower(filepath.Ext(e.Name()))
		switch ext {
		case ".png", ".jpg", ".jpeg", ".gif", ".svg":
			iconFiles = append(iconFiles, e.Name())
		}
	}

	// No icons in directory — just ask for a filename.
	if len(iconFiles) == 0 {
		var icon string
		form := huh.NewForm(
			huh.NewGroup(
				huh.NewInput().
					Title("Icon filename").
					Value(&icon).
					Placeholder("e.g. myicon.png"),
			),
		)
		if err := form.Run(); err != nil {
			return "", err
		}
		return icon, nil
	}

	// Show available icons as a select list with a "custom" escape hatch.
	sort.Strings(iconFiles)
	options := make([]huh.Option[string], 0, len(iconFiles)+1)
	options = append(options, huh.NewOption("[ Type a custom filename ]", "__custom__"))
	for _, f := range iconFiles {
		options = append(options, huh.NewOption(f, f))
	}

	var icon string
	form := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title(fmt.Sprintf("Pick an icon from %s", iconsDir)).
				Options(options...).
				Value(&icon),
		),
	)
	if err := form.Run(); err != nil {
		return "", err
	}

	if icon == "__custom__" {
		form = huh.NewForm(
			huh.NewGroup(
				huh.NewInput().
					Title("Icon filename").
					Value(&icon).
					Placeholder("e.g. myicon.png"),
			),
		)
		if err := form.Run(); err != nil {
			return "", err
		}
	}

	return icon, nil
}
