package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"streamdeck-lets-go/internal/config"
)

func ExecuteAction(a *config.Action, deck *Deck, pm *PageManager) error {
	if a == nil {
		return nil
	}

	slog.Debug("execute action", "type", a.Type)

	switch a.Type {
	case "command":
		return execCommand(a)
	case "builtin":
		return execBuiltin(a, deck, pm)
	case "script":
		return execScript(a)
	case "page":
		if pm != nil {
			return pm.ActivatePage(a.Page)
		}
		return fmt.Errorf("page manager not available")
	case "keyboard":
		return execKeyboard(a)
	default:
		return fmt.Errorf("unknown action type: %s", a.Type)
	}
}

func execCommand(a *config.Action) error {
	cmd := exec.Command("sh", "-c", a.Command)

	if a.Background {
		cmd.Stdout = nil
		cmd.Stderr = nil
		if err := cmd.Start(); err != nil {
			return fmt.Errorf("start command: %w", err)
		}
		go func() {
			if err := cmd.Wait(); err != nil {
				slog.Warn("command finished", "cmd", a.Command, "error", err)
			}
		}()
		return nil
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("run command: %w", err)
	}
	return nil
}

func execScript(a *config.Action) error {
	cmd := exec.Command(a.Script)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("run script: %w", err)
	}
	return nil
}

func execDisplayCapture(d *config.DisplayCfg, timeout time.Duration) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	var cmd *exec.Cmd
	if d.Command != "" {
		cmd = exec.CommandContext(ctx, "sh", "-c", d.Command)
	} else {
		cmd = exec.CommandContext(ctx, d.Script)
	}
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	output := stdout.String()
	if stderr.Len() > 0 {
		if output != "" {
			output += "\n"
		}
		output += stderr.String()
	}
	return output, err
}

func execDynamicKeys(dg *config.DynamicKeyGen, timeout time.Duration, keyCount int) ([]config.KeyConfig, error) {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	var cmd *exec.Cmd
	if dg.Command != "" {
		cmd = exec.CommandContext(ctx, "sh", "-c", dg.Command)
	} else {
		script := dg.Script
		if !filepath.IsAbs(script) {
			script = filepath.Join(configDir(), script)
		}
		cmd = exec.CommandContext(ctx, script)
	}

	// Set environment variables for the script
	cmd.Env = append(os.Environ(),
		fmt.Sprintf("STREAMDECK_KEY_COUNT=%d", keyCount),
		fmt.Sprintf("STREAMDECK_CONFIG_DIR=%s", configDir()),
	)

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()

	// Log stderr separately (don't merge into stdout for JSON parsing)
	if stderr.Len() > 0 {
		slog.Debug("dynamic_keys script stderr", "output", stderr.String())
	}

	if err != nil {
		return nil, fmt.Errorf("execute dynamic_keys: %w", err)
	}

	// Parse JSON from stdout
	var keys []config.KeyConfig
	output := stdout.String()
	if err := json.Unmarshal([]byte(output), &keys); err != nil {
		return nil, fmt.Errorf("parse dynamic_keys JSON: %w", err)
	}

	return keys, nil
}

func execBuiltin(a *config.Action, deck *Deck, pm *PageManager) error {
	parts := strings.SplitN(a.Builtin, ":", 2)
	if len(parts) < 1 {
		return fmt.Errorf("invalid builtin: %s", a.Builtin)
	}

	category := parts[0]
	action := ""
	if len(parts) > 1 {
		action = parts[1]
	}

	slog.Debug("builtin", "category", category, "action", action)

	switch category {
	case "media":
		return mediaAction(action)
	case "volume":
		return volumeAction(action)
	case "brightness":
		return brightnessAction(action)
	case "page":
		if pm != nil {
			return pageBuiltinAction(action, pm)
		}
		return fmt.Errorf("page manager not available")
	case "deck":
		return deckBuiltinAction(action, deck)
	default:
		return fmt.Errorf("unknown builtin category: %s", category)
	}
}

func mediaAction(action string) error {
	var cmd string
	switch action {
	case "playpause":
		cmd = "playerctl play-pause"
	case "next":
		cmd = "playerctl next"
	case "prev":
		cmd = "playerctl previous"
	case "stop":
		cmd = "playerctl stop"
	default:
		return fmt.Errorf("unknown media action: %s", action)
	}
	return exec.Command("sh", "-c", cmd).Run()
}

func volumeAction(action string) error {
	var cmd string
	switch action {
	case "up":
		cmd = "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"
	case "down":
		cmd = "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
	case "mute":
		cmd = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
	default:
		return fmt.Errorf("unknown volume action: %s", action)
	}
	return exec.Command("sh", "-c", cmd).Run()
}

func brightnessAction(action string) error {
	var cmd string
	switch action {
	case "up":
		cmd = "brightnessctl s +10%"
	case "down":
		cmd = "brightnessctl s 10%-"
	default:
		return fmt.Errorf("unknown brightness action: %s", action)
	}
	return exec.Command("sh", "-c", cmd).Run()
}

func pageBuiltinAction(action string, pm *PageManager) error {
	switch action {
	case "next":
		pageNames := pm.PageNames()
		current := pm.ActivePageName()
		for i, name := range pageNames {
			if name == current {
				next := (i + 1) % len(pageNames)
				return pm.ActivatePage(pageNames[next])
			}
		}
		if len(pageNames) > 0 {
			return pm.ActivatePage(pageNames[0])
		}
	case "prev":
		pageNames := pm.PageNames()
		current := pm.ActivePageName()
		for i, name := range pageNames {
			if name == current {
				prev := (i - 1 + len(pageNames)) % len(pageNames)
				return pm.ActivatePage(pageNames[prev])
			}
		}
		if len(pageNames) > 0 {
			return pm.ActivatePage(pageNames[0])
		}
	default:
		return fmt.Errorf("unknown page action: %s", action)
	}
	return nil
}

func deckBuiltinAction(action string, deck *Deck) error {
	switch action {
	case "brightness-up":
		current := deck.Brightness()
		next := current + 25
		if next > 100 {
			next = 25
		}
		return deck.SetBrightness(next)
	case "brightness-down":
		current := deck.Brightness()
		next := current - 25
		if next < 25 {
			next = 100
		}
		return deck.SetBrightness(next)
	default:
		return fmt.Errorf("unknown deck action: %s", action)
	}
}

func keyboardTool() string {
	if os.Getenv("WAYLAND_DISPLAY") != "" {
		if _, err := exec.LookPath("ydotool"); err == nil {
			return "ydotool"
		}
		if _, err := exec.LookPath("wtype"); err == nil {
			return "wtype"
		}
		return ""
	}
	if _, err := exec.LookPath("xdotool"); err == nil {
		return "xdotool"
	}
	return ""
}

func execKeyboard(a *config.Action) error {
	tool := keyboardTool()
	if tool == "" {
		wayland := os.Getenv("WAYLAND_DISPLAY") != ""
		if wayland {
			return fmt.Errorf("keyboard action requires wtype — install it (e.g. apk add wtype) and restart")
		}
		return fmt.Errorf("keyboard action requires xdotool — install it (e.g. apk add xdotool) and restart")
	}

	keys := strings.ToLower(strings.TrimSpace(a.Keys))
	if keys == "" {
		return fmt.Errorf("keyboard action: keys is empty")
	}

	parts := strings.Split(keys, "+")
	if len(parts) == 0 {
		return fmt.Errorf("keyboard action: invalid keys format %q", a.Keys)
	}

	switch tool {
	case "ydotool":
		return execYDOTool(keys)
	case "wtype":
		return execWType(parts)
	case "xdotool":
		return execXDoTool(keys)
	default:
		return fmt.Errorf("keyboard action: unsupported tool %q", tool)
	}
}

func execWType(parts []string) error {
	mainKey := parts[len(parts)-1]
	mods := parts[:len(parts)-1]

	args := make([]string, 0, 2+len(mods)*2)

	for _, m := range mods {
		args = append(args, "-M", m)
	}
	args = append(args, "-P", mainKey, "-p", mainKey)
	for i := len(mods) - 1; i >= 0; i-- {
		args = append(args, "-m", mods[i])
	}

	cmd := exec.Command("wtype", args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func execXDoTool(keys string) error {
	cmd := exec.Command("xdotool", "key", keys)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func execYDOTool(keys string) error {
	// Use 'type' command instead of 'key' for better compatibility with Wine/games on Wayland
	cmd := exec.Command("ydotool", "type", keys)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (pm *PageManager) PageNames() []string {
	pm.mu.RLock()
	defer pm.mu.RUnlock()
	names := make([]string, 0, len(pm.pages))
	for name := range pm.pages {
		names = append(names, name)
	}
	return names
}
