package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	discord "github.com/bwmarrin/discordgo"
	extism "github.com/extism/go-sdk"
	log "github.com/sirupsen/logrus"
	"github.com/tetratelabs/wazero"
)

// --- Configuration ---

// discordTokenEnv is the name of the environment variable holding the bot token.
const discordTokenEnv = "DISCORD_BOT_TOKEN"

// pluginDir is the directory where Wasm plugin files (.wasm) are stored.
// The application expects files named like <handlerKey>.wasm (e.g., ping.wasm)
const pluginDir = "./plugins"

// pluginFunctionName is the name of the function exported by the Wasm plugins
// that will handle the interaction logic.
const pluginFunctionName = "handle_interaction"

// commandDefinitionFuncName is the name of the function exported by the Wasm plugins
// that will provide the command definition.
const commandDefinitionFuncName = "get_command_definition" // Function plugins must export

// --- Structs ---

// PluginResponse defines the structure of the JSON data expected *from* the Wasm plugin.
// The plugin should return JSON that can be unmarshaled into this struct.
type PluginResponse struct {
	Content   string `json:"content"`
	Ephemeral bool   `json:"ephemeral"`

	// NEW: Components to include in the message response (buttons, selects, etc.)
	// Use a pointer to allow 'nil' when no components are needed.
	// Documentation Reference (MessageComponent): https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#MessageComponent
	Components *[]json.RawMessage `json:"components,omitempty"`

	// TODO: Add fields for Modals later if needed. Requires sending a different InteractionResponse Type.
	// Title       string                      `json:"title,omitempty"` // For Modals
	// ModalCustomID string                    `json:"modal_custom_id,omitempty"` // For Modals
}

// --- Main Application Logic ---

func main() {
	f, err := os.OpenFile("latest_run.json", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Println("Could not open file for logging.")
		os.Exit(1)
	}
	defer f.Close()
	multiWriter := io.MultiWriter(os.Stdout, f)
	log.SetOutput(multiWriter)
	log.SetLevel(log.InfoLevel)
	log.SetFormatter(&log.JSONFormatter{})

	token := os.Getenv(discordTokenEnv)
	if token == "" {
		log.Errorf(fmt.Sprintf("Bot token not found in environment variable %s", discordTokenEnv))
		os.Exit(1)
		// Documentation Reference: Exiting if token is missing.
	}

	dg, err := discord.New("Bot " + token)
	if err != nil {
		log.Errorf(fmt.Sprintf("Error creating Discord session: %s", err))
		return
	}

	extism.SetLogLevel(extism.LogLevelWarn)

	log.Infoln("Adding interaction handler...")
	// Add the central handler for all interaction types.
	// Documentation Reference: https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#Session.AddHandler
	dg.AddHandler(interactionCreateHandler)

	log.Infoln("Opening Discord session...")
	// Open a websocket connection to Discord and begin listening.
	// Documentation Reference: https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#Session.Open
	err = dg.Open()
	if err != nil {
		log.Fatalf(fmt.Sprintf("Error opening connection: %v", err))
	}

	// Wait here until CTRL-C or other term signal is received.
	log.Infof(fmt.Sprintf("Bot '%s' is now running. Press CTRL-C to exit.", dg.State.User.Username))

	// !!! REGISTER COMMANDS HERE !!!
	// Register for a specific guild for faster testing (replace "YourGuildID" or leave "" for global)
	//registerCommands(dg, "") // "" for global, or "YourGuildID"

	// !!! CALL THE DYNAMIC COMMAND REGISTRATION !!!
	// Pass the Guild ID for testing or "" for global commands.
	registerCommandsFromPlugins(dg, "") // "" for global, or "YourGuildID"

	stop := make(chan os.Signal, 1)
	signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
	<-stop // Block until signal

	log.Infoln("Shutting down bot...")
	// Cleanly close down the Discord session.
	// Documentation Reference: https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#Session.Close
	if err := dg.Close(); err != nil {
		log.Warnf(fmt.Sprintf("Error closing Discord session: %v", err))
	}
}

// --- Interaction Handling ---

// interactionCreateHandler is the core function that receives all interaction events from discordgo.
func interactionCreateHandler(s *discord.Session, i *discord.InteractionCreate) {
	startTime := time.Now() // Track processing time
	log.Debugf(fmt.Sprintf("Interaction received: Type=%v ID=%s User=%s Guild=%s Channel=%s",
		i.Type, i.ID, formatUser(i), i.GuildID, i.ChannelID))

	var handlerKey string
	var interactionInput interface{} // Use interface{} to hold different data types easily for JSON marshaling

	// Determine the response type expected after plugin execution (can be overridden by plugin later)
	expectedResponseType := discord.InteractionResponseChannelMessageWithSource

	// Determine the handler key and input data based on interaction type
	// Documentation Reference (Interaction Types): https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#InteractionType
	switch i.Type {
	case discord.InteractionApplicationCommand:
		data := i.ApplicationCommandData()
		handlerKey = data.Name                                                     // Use the command name (e.g., "ping")
		interactionInput = i                                                       // Pass the whole interaction for now
		expectedResponseType = discord.InteractionResponseChannelMessageWithSource // Default for slash commands
		log.Debugf(fmt.Sprintf("-> Type=SlashCommand Name=/%s Options=%+v", data.Name, data.Options))

	case discord.InteractionMessageComponent:
		data := i.MessageComponentData()
		fullCustomID := data.CustomID                                   // CustomID pluginName:action:context
		interactionInput = i                                            // Pass the whole interaction
		expectedResponseType = discord.InteractionResponseUpdateMessage // Components usually update the message they are on
		log.Debugf(fmt.Sprintf("-> Type=Component CustomID=%s ComponentType=%v Values=%+v", data.CustomID, data.ComponentType, data.Values))

		// --- CustomID Parsing ---
		parts := strings.SplitN(fullCustomID, ":", 2) // Split into max 2 parts: "pluginName" and "action:context"
		if len(parts) > 0 {
			handlerKey = parts[0] // The first part is the plugin name
			log.Debugf(fmt.Sprintf("Parsed handler key '%s' from CustomID '%s'", handlerKey, fullCustomID))
		} else {
			log.Warnf(fmt.Sprintf("Could not parse handler key from CustomID: %s", fullCustomID))
			// Maybe fallback to using the full CustomID as key? Or handle as error.
			// handlerKey = fullCustomID // Fallback (less ideal)
			respondWithError(s, i.Interaction, "Invalid component ID format.", true)
			return
		}
		// The plugin itself will handle the rest of the CustomID ("action:context")

	case discord.InteractionModalSubmit:
		data := i.ModalSubmitData()
		fullCustomID := data.CustomID
		interactionInput = i
		// Modals usually send a new message or update a previous one after submission.
		expectedResponseType = discord.InteractionResponseChannelMessageWithSource // Can be overridden by plugin
		log.Debugf("-> Type=ModalSubmit CustomID=%s", fullCustomID)

		// --- CustomID Parsing (same logic as components) ---
		parts := strings.SplitN(fullCustomID, ":", 2)
		if len(parts) > 0 {
			handlerKey = parts[0]
			log.Debugf(fmt.Sprintf("Parsed handler key '%s' from CustomID '%s'", handlerKey, fullCustomID))
		} else {
			log.Warnf(fmt.Sprintf("Could not parse handler key from Modal CustomID: %s", fullCustomID))
			respondWithError(s, i.Interaction, "Invalid modal ID format.", true)
			return
		}
		// Plugin handles the rest of the CustomID and submitted data (data.Components)

	// Optional: Handle Autocomplete interactions if needed
	// case discordgo.InteractionApplicationCommandAutocomplete:
	// 	data := i.ApplicationCommandData()
	//  handlerKey = data.Name // Or potentially a combination with focused option
	// 	interactionInput = i
	// 	log.Printf("INFO: -> Type=Autocomplete Name=/%s Focused=%s", data.Name, getFocusedOption(data.Options))
	// 	// Autocomplete requires a specific response type (InteractionResponseAutocompleteResult)
	// 	// This might need a dedicated path or plugin convention. We'll skip detailed handling for now.
	//  handleAutocomplete(s, i) // Example separate function
	// 	return // Return early for autocomplete

	default:
		log.Warnf(fmt.Sprintf("Received unhandled interaction type: %d", i.Type))
		respondWithError(s, i.Interaction, "Sorry, I don't know how to handle this type of interaction yet.", true)
		return
	}

	if handlerKey == "" {
		log.Warnln("Could not determine handler key for the interaction.")
		respondWithError(s, i.Interaction, "Could not identify the action to perform.", true)
		return
	}

	// --- Plugin Lookup and Execution ---
	pluginPath := filepath.Join(pluginDir, handlerKey+".wasm")
	log.Printf(fmt.Sprintf("Attempting to load plugin for key '%s' at path: %s", handlerKey, pluginPath))

	// Check if the plugin file exists before trying to load
	// Documentation Reference (os.Stat): https://pkg.go.dev/os#Stat
	// Documentation Reference (os.IsNotExist): https://pkg.go.dev/os#IsNotExist
	if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
		log.Warnf(fmt.Sprintf("Plugin file not found: %s", pluginPath))
		respondWithError(s, i.Interaction, fmt.Sprintf("Sorry, the handler for '%s' is not available or implemented yet.", handlerKey), true)
		return
	}

	// Marshal the interaction data to JSON to pass to the plugin
	// Note: Passing the entire discordgo.InteractionCreate struct can be large and includes sensitive info (token).
	// Consider creating a more specific input struct derived from `i` for better security and efficiency later.
	inputBytes, err := json.Marshal(interactionInput)
	if err != nil {
		log.Errorf(fmt.Sprintf("Failed to marshal interaction data for key '%s': %v", handlerKey, err))
		respondWithError(s, i.Interaction, "Error: Could not prepare data for the handler.", true)
		return
	}
	// log.Printf("DEBUG: Marshaled Input Data (first 256 bytes): %s", truncateString(string(inputBytes), 256)) // Optional debug log

	// --- Extism Plugin Call ---
	// Documentation Reference (Extism Concepts): https://extism.org/docs/concepts/overview
	// Documentation Reference (Go SDK NewPlugin): https://pkg.go.dev/github.com/extism/go-sdk@v1.7.1#NewPlugin
	ctx := context.Background() // Use background context for plugin execution

	// Wazero Cache
	// Fixes issue with module[wasi_snapshot_preview1] not instantiated error
	cache := wazero.NewCompilationCache()
	defer cache.Close(ctx)

	// Define the source of the Wasm module
	manifest := extism.Manifest{
		Wasm: []extism.Wasm{
			extism.WasmFile{Path: pluginPath},
		},
		// AllowedHosts can restrict network access if the plugin needs it. Example:
		// AllowedHosts: []string{"discord.com", "api.example.com"},
		// Config allows passing static key-value pairs to the plugin on initialization. Example:
		// Config: map[string]string{"log_level": "info"},
	}

	// Configure plugin options
	pluginConfig := extism.PluginConfig{
		EnableWasi: true,
		//ModuleConfig:  wazero.NewModuleConfig(),
		//RuntimeConfig: wazero.NewRuntimeConfig().WithCompilationCache(cache),
	}

	log.Debugf(fmt.Sprintf("Initializing Extism plugin: %s", pluginPath))
	plugin, err := extism.NewPlugin(ctx, manifest, pluginConfig, nil) // `nil` for host functions for now
	if err != nil {
		log.Errorf(fmt.Sprintf("Failed to initialize Extism plugin for key '%s' from %s: %v", handlerKey, pluginPath, err))
		respondWithError(s, i.Interaction, "Error: Could not load the required handler module.", true)
		return
	}
	defer plugin.Close(ctx) // IMPORTANT: Ensure plugin resources are freed

	log.Printf(fmt.Sprintf("Calling function '%s' in plugin '%s'", pluginFunctionName, handlerKey))
	// Call the exported function in the Wasm plugin
	// Documentation Reference (Go SDK Call): https://pkg.go.dev/github.com/extism/go-sdk@v1.7.1#Plugin.Call
	exitCode, outputBytes, err := plugin.Call(pluginFunctionName, inputBytes)
	executionTime := time.Since(startTime) // Track execution time after call

	if err != nil {
		log.Errorf(fmt.Sprintf("Extism plugin call failed for key '%s': %v. Execution time: %s", handlerKey, err, executionTime))
		respondWithError(s, i.Interaction, "Error: Failed to execute the handler module.", true)
		return
	}

	log.Debugf(fmt.Sprintf("Plugin '%s' finished. ExitCode=%d OutputLength=%d ExecutionTime=%s", handlerKey, exitCode, len(outputBytes), executionTime))

	// Convention: A non-zero exit code from the plugin indicates an error during its execution.
	if exitCode != 0 {
		errorMsg := fmt.Sprintf("Handler Error (Code %d)", exitCode)
		if len(outputBytes) > 0 {
			// Try to use the output as a more specific error message
			errorMsg = fmt.Sprintf("Handler Error (Code %d): %s", exitCode, string(outputBytes))
		}
		log.Warnf(fmt.Sprintf("Plugin '%s' returned non-zero exit code. Message: %s", handlerKey, errorMsg))
		respondWithError(s, i.Interaction, errorMsg, true) // Show plugin error to user (maybe ephemeral)
		return
	}

	// Handle cases where the plugin successfully executes but returns no specific response data
	// (e.g., acknowledging a button click without updating the message).
	// --- Handle Empty Output ---
	if len(outputBytes) == 0 {
		log.Debugf(fmt.Sprintf("Plugin '%s' returned empty output.", handlerKey))
		// For components/modals, empty output might mean just acknowledge, do nothing visible.
		// For slash commands, it's unexpected unless deferred.
		// Send a simple acknowledgement based on the interaction type
		ackType := discord.InteractionResponseDeferredChannelMessageWithSource
		if i.Type == discord.InteractionMessageComponent {
			ackType = discord.InteractionResponseDeferredMessageUpdate // Ack the component click
		}
		err = s.InteractionRespond(i.Interaction, &discord.InteractionResponse{Type: ackType})
		if err != nil {
			log.Errorf(fmt.Sprintf("Failed to send acknowledgement for empty plugin output (key '%s'): %v", handlerKey, err))
		} else {
			log.Debugf(fmt.Sprintf("Sent acknowledgement for key '%s'", handlerKey))
		}
		return // Interaction acknowledged
	}

	// --- Process Plugin Response ---
	var pluginResp PluginResponse
	err = json.Unmarshal(outputBytes, &pluginResp)
	if err != nil {
		log.Errorf(fmt.Sprintf("Failed to unmarshal plugin response JSON for key '%s': %v. Raw output: %s", handlerKey, err, truncateString(string(outputBytes), 256)))
		respondWithError(s, i.Interaction, "Received an invalid response from the handler module.", true)
		return
	}

	var finalComponents []discord.MessageComponent
	if pluginResp.Components != nil && len(*pluginResp.Components) > 0 {
		finalComponents = make([]discord.MessageComponent, 0, len(*pluginResp.Components))
		for _, rawComponent := range *pluginResp.Components {
			// Assuming top-level components are always Action Rows based on Discord structure
			var actionRow discord.ActionsRow
			if err := json.Unmarshal(rawComponent, &actionRow); err != nil {
				log.Errorf(fmt.Sprintf("Failed to unmarshal component Action Row: %v. Raw: %s", err, string(rawComponent)))
				// Decide how to handle: skip component, return error to user?
				continue // Skip this component
			}
			finalComponents = append(finalComponents, actionRow)
		}
	}

	responseData := &discord.InteractionResponseData{
		Content: pluginResp.Content,
	}

	// Assign to responseData (check if nil or empty first)
	if len(finalComponents) > 0 {
		responseData.Components = finalComponents // Assign the correctly typed slice
	} else if pluginResp.Components != nil {
		// This case means we had input components, but failed to parse all of them
		log.Warnf("Plugin provided components, but failed to parse them into ActionRows.")
		// Optionally send an error or proceed without components
	}

	// --- Send Response to Discord ---
	log.Debugf(fmt.Sprintf("Sending plugin response for '%s': Ephemeral=%t Content='%s' Components?=%t",
		handlerKey, pluginResp.Ephemeral, truncateString(pluginResp.Content, 60), finalComponents != nil))

	if pluginResp.Ephemeral {
		responseData.Flags = discord.MessageFlagsEphemeral
	}

	// Determine response type: Use the default determined earlier, unless plugin overrides (TODO)
	responseType := expectedResponseType
	// If the input was a component/modal and the plugin provided content/components,
	// we usually want to update the message instead of sending a new one.
	if (i.Type == discord.InteractionMessageComponent || i.Type == discord.InteractionModalSubmit) && (pluginResp.Content != "" || pluginResp.Components != nil) {
		// Let's favor updating the original message for component/modal interactions if content is returned.
		// A plugin could potentially signal wanting a *new* message instead.
		if i.Type == discord.InteractionMessageComponent {
			responseType = discord.InteractionResponseUpdateMessage
		} else {
			// Modal submits often create new messages, keep default ChannelMessageWithSource unless plugin specified update
			// For now, let's keep it simple.
		}
	}

	// Send the response
	err = s.InteractionRespond(i.Interaction, &discord.InteractionResponse{
		Type: responseType,
		Data: responseData,
	})

	finalTime := time.Since(startTime)
	if err != nil {
		log.Errorf(fmt.Sprintf("Failed to send final interaction response for key '%s' (Type %d): %v. Total processing time: %s", handlerKey, responseType, err, finalTime))
	} else {
		log.Printf(fmt.Sprintf("Successfully processed and responded to interaction for key '%s' (Type %d). Total processing time: %s", handlerKey, responseType, finalTime))
	}
} // End of interactionCreateHandler

// --- DYNAMIC COMMAND REGISTRATION ---
// registerCommandsFromPlugins scans the plugin directory and registers commands based on plugin definitions.
func registerCommandsFromPlugins(s *discord.Session, guildID string) {
	log.Infoln("Starting dynamic command registration from plugins...")

	// Read all files in the plugin directory
	// Documentation Reference (os.ReadDir): https://pkg.go.dev/os#ReadDir
	files, err := os.ReadDir(pluginDir)
	if err != nil {
		log.Errorf(fmt.Sprintf("Failed to read plugin directory '%s': %v", pluginDir, err))
		return
	}

	registeredCount := 0
	for _, file := range files {
		// Skip directories and non-wasm files
		if file.IsDir() || !strings.HasSuffix(file.Name(), ".wasm") {
			continue
		}

		pluginPath := filepath.Join(pluginDir, file.Name())
		// Derive the expected command name from the filename (e.g., "ping.wasm" -> "ping")
		expectedCommandName := strings.TrimSuffix(file.Name(), ".wasm")

		log.Debugf(fmt.Sprintf("Processing plugin: %s (expected command: '%s')", pluginPath, expectedCommandName))

		// --- Load plugin temporarily to get definition ---
		ctx := context.Background()
		// Wazero Cache
		// Fixes issue with module[wasi_snapshot_preview1] not instantiated error
		cache := wazero.NewCompilationCache()
		defer cache.Close(ctx)

		manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: pluginPath}}}

		// WASI is likely not needed just for getting definition unless the function logs
		pluginConfig := extism.PluginConfig{
			EnableWasi: true,
			//ModuleConfig: wazero.NewModuleConfig(),
			//RuntimeConfig: wazero.NewRuntimeConfig().WithCompilationCache(cache)
		}

		plugin, err := extism.NewPlugin(ctx, manifest, pluginConfig, nil)
		if err != nil {
			log.Errorf(fmt.Sprintf("[%s] Failed to initialize plugin for definition check: %v", expectedCommandName, err))
			continue // Skip to next file
		}
		// Ensure plugin is closed even if errors occur later in the loop iteration
		closePlugin := func() {
			if err := plugin.Close(ctx); err != nil {
				log.Warnf(fmt.Sprintf("[%s] Error closing plugin after definition check: %v", expectedCommandName, err))
			}
		}

		// Check if the definition function exists
		// Documentation Reference (Extism FunctionExists): https://pkg.go.dev/github.com/extism/go-sdk@v1.7.1#Plugin.FunctionExists
		if !plugin.FunctionExists(commandDefinitionFuncName) {
			log.Warnf(fmt.Sprintf("[%s] Plugin does not export the required function '%s'. Skipping registration.", expectedCommandName, commandDefinitionFuncName))
			closePlugin()
			continue // Skip to next file
		}

		// Call the function to get the command definition JSON
		exitCode, outputBytes, err := plugin.Call(commandDefinitionFuncName, nil) // No input needed
		if err != nil {
			log.Errorf(fmt.Sprintf("[%s] Failed to call '%s': %v", expectedCommandName, commandDefinitionFuncName, err))
			closePlugin()
			continue
		}
		if exitCode != 0 {
			log.Errorf(fmt.Sprintf("[%s] Function '%s' returned non-zero exit code %d. Output: %s", expectedCommandName, commandDefinitionFuncName, exitCode, string(outputBytes)))
			closePlugin()
			continue
		}
		if len(outputBytes) == 0 {
			log.Warnf(fmt.Sprintf("[%s] Function '%s' returned empty output. Skipping registration.", expectedCommandName, commandDefinitionFuncName))
			closePlugin()
			continue
		}

		// Unmarshal the JSON output into the discordgo struct
		var cmdDef discord.ApplicationCommand
		// Documentation Reference (json.Unmarshal): https://pkg.go.dev/encoding/json#Unmarshal
		err = json.Unmarshal(outputBytes, &cmdDef)
		if err != nil {
			log.Errorf(fmt.Sprintf("[%s] Failed to unmarshal JSON definition from plugin: %v. Raw JSON: '%s'", expectedCommandName, err, string(outputBytes)))
			closePlugin()
			continue
		}

		// --- Validation ---
		if cmdDef.Name == "" {
			log.Warnf(fmt.Sprintf("[%s] Plugin provided command definition with empty 'Name'. Skipping registration.", expectedCommandName))
			closePlugin()
			continue
		}
		// Convention check: Does the name in the definition match the filename?
		if cmdDef.Name != expectedCommandName {
			log.Warnf(fmt.Sprintf("[%s] Plugin's defined command name ('%s') does not match filename convention ('%s'). Proceeding anyway.", expectedCommandName, cmdDef.Name, expectedCommandName))
			// You could choose to skip registration here if the convention is strict:
			// closePlugin()
			// continue
		}

		// --- Register the Command with Discord ---
		log.Infof(fmt.Sprintf("[%s] Registering command '%s' with Discord...", expectedCommandName, cmdDef.Name))
		cmd, err := s.ApplicationCommandCreate(s.State.User.ID, guildID, &cmdDef)
		if err != nil {
			// Check for specific errors like invalid characters if needed
			log.Errorf(fmt.Sprintf("[%s] Failed to register command '%s': %v", expectedCommandName, cmdDef.Name, err))
		} else {
			log.Infof(fmt.Sprintf("[%s] Registered command '%s' (ID: %s)", expectedCommandName, cmd.Name, cmd.ID))
			registeredCount++
		}

		// Close the plugin now that we're done with it for this loop iteration
		closePlugin()
	}

	log.Infof(fmt.Sprintf("Dynamic command registration finished. Registered %d commands.", registeredCount))

	// Optional: Fetch all currently registered commands and compare against found plugins
	// to identify and potentially delete commands corresponding to removed plugins.
	// This requires storing/managing command IDs.
}

// --- Helper Functions ---

// respondWithError sends a standardized, usually ephemeral error message back to the user.
func respondWithError(s *discord.Session, interaction *discord.Interaction, message string, ephemeral bool) {
	log.Errorf(fmt.Sprintf("RESPONSE_ERR: InteractionID=%s Message='%s'", interaction.ID, message))
	responseData := &discord.InteractionResponseData{
		Content: message,
	}
	if ephemeral {
		responseData.Flags = discord.MessageFlagsEphemeral
	}
	err := s.InteractionRespond(interaction, &discord.InteractionResponse{
		Type: discord.InteractionResponseChannelMessageWithSource,
		Data: responseData,
	})
	if err != nil {
		// Log the error in sending the error response, but don't try to respond again
		log.Errorf(fmt.Sprintf("Failed to send error response for InteractionID=%s: %v", interaction.ID, err))
	}
}

// formatUser returns a string representation of the user initiating the interaction.
func formatUser(i *discord.InteractionCreate) string {
	if i.Member != nil && i.Member.User != nil {
		return fmt.Sprintf("%s#%s (%s)", i.Member.User.Username, i.Member.User.Discriminator, i.Member.User.ID)
	}
	if i.User != nil { // Happens for interactions in DMs
		return fmt.Sprintf("%s#%s (%s)", i.User.Username, i.User.Discriminator, i.User.ID)
	}
	return "Unknown User"
}

// truncateString limits a string to a max length, adding "..." if truncated.
func truncateString(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	if maxLen <= 3 {
		return "..."
	}
	return s[:maxLen-3] + "..."
}

// registerCommands registers application commands with Discord.
// Call this function in main() after the session is created but before waiting for the stop signal.
//func registerCommands(s *discord.Session, guildID string) {
//	log.Println("INFO: Registering application commands...")
//
//	// Define the /ping command
//	pingCommand := &discord.ApplicationCommand{
//		Name:        "ping",
//		Description: "Checks the bot's responsiveness (handled by Wasm!)",
//		// Add options here if needed in the future
//	}
//
//	// Register the command.
//	// If guildID is empty "", it registers as a global command (can take ~1 hour to propagate).
//	// If guildID is provided, it registers instantly for that specific server (good for testing).
//	// Documentation Reference: https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.28.1#Session.ApplicationCommandCreate
//	cmd, err := s.ApplicationCommandCreate(s.State.User.ID, guildID, pingCommand)
//	if err != nil {
//		log.Errorf(fmt.Sprintf("Cannot create '/%s' command: %v", pingCommand.Name, err))
//	} else {
//		log.Printf("Registered command: /%s (ID: %s)", cmd.Name, cmd.ID)
//	}
//
//	// Add other command registrations here...
//}

// TODO: Add placeholder/example functions for command registration if desired
// func cleanupCommands(s *discordgo.Session) {}
