// internal/api/routes.go
package api

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"regexp"

	"github.com/gin-gonic/gin"
	"github.com/nbd-wtf/go-nostr"
	"git.sovbit.dev/Enki/nostr-poster/internal/auth"
	"git.sovbit.dev/Enki/nostr-poster/internal/models"
	"git.sovbit.dev/Enki/nostr-poster/internal/scheduler"
	"git.sovbit.dev/Enki/nostr-poster/internal/media/upload/blossom"
    "git.sovbit.dev/Enki/nostr-poster/internal/media/upload/nip94"
	"go.uber.org/zap"
)

// API represents the HTTP API for managing bots
type API struct {
	router      *gin.Engine
	logger      *zap.Logger
	botService  *BotService
	authService *auth.Service
	scheduler   *scheduler.Scheduler
}

// NewAPI creates a new API instance
func NewAPI(
	logger *zap.Logger,
	botService *BotService,
	authService *auth.Service,
	scheduler *scheduler.Scheduler,
) *API {
	router := gin.Default()
	
	api := &API{
		router:      router,
		logger:      logger,
		botService:  botService,
		authService: authService,
		scheduler:   scheduler,
	}
	
	// Set up routes
	api.setupRoutes()
	
	return api
}

// SetupRoutes configures the API routes
func (a *API) setupRoutes() {
	// Public routes
	a.router.GET("/health", a.healthCheck)
	
	// API routes
	apiGroup := a.router.Group("/api")
	
	// Authentication
	apiGroup.POST("/auth/login", a.login)
	apiGroup.GET("/auth/verify", a.requireAuth, a.verifyAuth)
	
	// Bot management
	botGroup := apiGroup.Group("/bots")
	botGroup.Use(a.requireAuth)
	{
		botGroup.GET("", a.listBots)
		botGroup.POST("", a.createBot)
		botGroup.GET("/:id", a.getBot)
		botGroup.PUT("/:id", a.updateBot)
		botGroup.DELETE("/:id", a.deleteBot)
		
		// Bot configuration
		botGroup.GET("/:id/config", a.getBotConfig)
		botGroup.PUT("/:id/config", a.updateBotConfig)
		
		// Relay management
		botGroup.GET("/:id/relays", a.getBotRelays)
		botGroup.PUT("/:id/relays", a.updateBotRelays)
		
		// Actions
		botGroup.POST("/:id/profile/publish", a.publishBotProfile)
		botGroup.POST("/:id/run", a.runBotNow)
		botGroup.POST("/:id/enable", a.enableBot)
		botGroup.POST("/:id/disable", a.disableBot)
		
		// NEW: Manual post creation
		botGroup.POST("/:id/post", a.createManualPost)
	}
	
	// Content management
	contentGroup := apiGroup.Group("/content")
	contentGroup.Use(a.requireAuth)
	{
		contentGroup.GET("/:botId", a.listBotContent)
		contentGroup.POST("/:botId/upload", a.uploadContent)
		contentGroup.DELETE("/:botId/:filename", a.deleteContent)
		
		// NEW: Media server upload
		contentGroup.POST("/:botId/uploadToMediaServer", a.uploadToMediaServer)
	}
	
	// Stats
	statsGroup := apiGroup.Group("/stats")
	statsGroup.Use(a.requireAuth)
	{
		statsGroup.GET("", a.getStats)
		statsGroup.GET("/:botId", a.getBotStats)
	}
	
	// Serve the web UI
	a.router.StaticFile("/", "./web/index.html")
	a.router.StaticFile("/content.html", "./web/content.html")
	a.router.Static("/assets", "./web/assets")
	
	// Handle 404s for SPA
	a.router.NoRoute(func(c *gin.Context) {
		c.File("./web/index.html")
	})
}

// Run starts the API server
func (a *API) Run(addr string) error {
	return a.router.Run(addr)
}

// Middleware for requiring authentication
func (a *API) requireAuth(c *gin.Context) {
	token := c.GetHeader("Authorization")
	if token == "" {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
			"error": "Authentication required",
		})
		return
	}
	
	// Verify the auth token
	pubkey, err := a.authService.VerifyToken(token)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
			"error": "Invalid authentication token",
		})
		return
	}
	
	// Store the pubkey in the context
	c.Set("pubkey", pubkey)
	c.Next()
}

// API Handlers

// healthCheck responds with a simple health check
func (a *API) healthCheck(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"status": "ok",
	})
}

// login handles user login with NIP-07 signature
func (a *API) login(c *gin.Context) {
	var req struct {
		Pubkey    string `json:"pubkey" binding:"required"`
		Signature string `json:"signature" binding:"required"`
		Event     string `json:"event" binding:"required"`
	}
	
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}
	
	// Verify the signature
	token, err := a.authService.Login(req.Pubkey, req.Signature, req.Event)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"token": token,
	})
}

// verifyAuth verifies the current auth token
func (a *API) verifyAuth(c *gin.Context) {
	pubkey, exists := c.Get("pubkey")
	if !exists {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"pubkey": pubkey,
	})
}

// listBots lists all bots owned by the user
func (a *API) listBots(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	
	bots, err := a.botService.ListUserBots(pubkey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list bots"})
		a.logger.Error("Failed to list bots", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, bots)
}

func (a *API) createBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	a.logger.Info("Bot creation request received", zap.String("owner_pubkey", pubkey))
	
	var bot models.Bot
	if err := c.ShouldBindJSON(&bot); err != nil {
		a.logger.Error("Invalid bot data", zap.Error(err))
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot data: " + err.Error()})
		return
	}
	
	// Log received data
	a.logger.Info("Received bot creation data", 
		zap.String("name", bot.Name), 
		zap.String("display_name", bot.DisplayName),
		zap.Bool("has_privkey", bot.EncryptedPrivkey != ""),
		zap.Bool("has_pubkey", bot.Pubkey != ""))
	
	// Set the owner
	bot.OwnerPubkey = pubkey
	
	// Create the bot
	createdBot, err := a.botService.CreateBot(&bot)
	if err != nil {
		a.logger.Error("Failed to create bot", zap.Error(err))
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	
	a.logger.Info("Bot created successfully", 
		zap.Int64("bot_id", createdBot.ID), 
		zap.String("name", createdBot.Name))

	createdBot.EncryptedPrivkey = ""
	
	c.JSON(http.StatusCreated, createdBot)
}

// getBot gets a specific bot by ID
func (a *API) getBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Get the bot
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	c.JSON(http.StatusOK, bot)
}

// updateBot updates a bot
func (a *API) updateBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	var bot models.Bot
	if err := c.ShouldBindJSON(&bot); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot data"})
		return
	}
	
	// Set the ID and owner
	bot.ID = botID
	bot.OwnerPubkey = pubkey
	
	// Update the bot
	updatedBot, err := a.botService.UpdateBot(&bot)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bot"})
		a.logger.Error("Failed to update bot", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, updatedBot)
}

// deleteBot deletes a bot
func (a *API) deleteBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Delete the bot
	err = a.botService.DeleteBot(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bot"})
		a.logger.Error("Failed to delete bot", zap.Error(err))
		return
	}
	
	c.Status(http.StatusNoContent)
}

// getBotConfig gets a bot's configuration
func (a *API) getBotConfig(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Get the bot with config
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"post_config": bot.PostConfig,
		"media_config": bot.MediaConfig,
	})
}

// updateBotConfig updates a bot's configuration
func (a *API) updateBotConfig(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	var config struct {
		PostConfig  *models.PostConfig  `json:"post_config"`
		MediaConfig *models.MediaConfig `json:"media_config"`
	}
	
	if err := c.ShouldBindJSON(&config); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid config data"})
		return
	}
	
	// Set the bot ID
	if config.PostConfig != nil {
		config.PostConfig.BotID = botID
	}
	if config.MediaConfig != nil {
		config.MediaConfig.BotID = botID
	}
	
	// Update the configs
	err = a.botService.UpdateBotConfig(botID, pubkey, config.PostConfig, config.MediaConfig)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update config"})
		a.logger.Error("Failed to update bot config", zap.Error(err))
		return
	}
	
	// Get the updated bot
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	// Update the scheduler if needed
	if config.PostConfig != nil && config.PostConfig.Enabled {
		err = a.scheduler.UpdateBotSchedule(bot)
		if err != nil {
			a.logger.Error("Failed to update bot schedule", 
				zap.Int64("botID", botID),
				zap.Error(err))
		}
	} else if config.PostConfig != nil && !config.PostConfig.Enabled {
		a.scheduler.UnscheduleBot(botID)
	}
	
	c.JSON(http.StatusOK, gin.H{
		"post_config": bot.PostConfig,
		"media_config": bot.MediaConfig,
	})
}

// getBotRelays gets a bot's relay configuration
func (a *API) getBotRelays(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Get the relays
	relays, err := a.botService.GetBotRelays(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get relays"})
		a.logger.Error("Failed to get bot relays", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, relays)
}

// updateBotRelays updates a bot's relay configuration
func (a *API) updateBotRelays(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	var relays []*models.Relay
	if err := c.ShouldBindJSON(&relays); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid relay data"})
		return
	}
	
	// Set the bot ID for each relay
	for _, relay := range relays {
		relay.BotID = botID
	}
	
	// Update the relays
	err = a.botService.UpdateBotRelays(botID, pubkey, relays)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update relays"})
		a.logger.Error("Failed to update bot relays", zap.Error(err))
		return
	}
	
	// Get the updated relays
	updatedRelays, err := a.botService.GetBotRelays(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updated relays"})
		a.logger.Error("Failed to get updated bot relays", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, updatedRelays)
}

// publishBotProfile publishes a bot's profile to Nostr
func (a *API) publishBotProfile(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Publish the profile
	err = a.botService.PublishBotProfile(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish profile"})
		a.logger.Error("Failed to publish bot profile", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"message": "Profile published successfully",
	})
}

// runBotNow runs a bot immediately
func (a *API) runBotNow(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Check if the bot belongs to the user
	_, err = a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	// Run the bot
	err = a.scheduler.RunNow(botID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to run bot"})
		a.logger.Error("Failed to run bot now", zap.Error(err))
		return
	}
	
	c.JSON(http.StatusOK, gin.H{
		"message": "Bot is running",
	})
}

// enableBot enables a bot
func (a *API) enableBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Get the bot
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	// Enable the bot
	bot.PostConfig.Enabled = true
	err = a.botService.UpdateBotConfig(botID, pubkey, bot.PostConfig, nil)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable bot"})
		a.logger.Error("Failed to enable bot", zap.Error(err))
		return
	}
	
	// Schedule the bot
	err = a.scheduler.ScheduleBot(bot)
	if err != nil {
		a.logger.Error("Failed to schedule bot", 
			zap.Int64("botID", botID),
			zap.Error(err))
	}
	
	c.JSON(http.StatusOK, gin.H{
		"message": "Bot enabled",
	})
}

// disableBot disables a bot
func (a *API) disableBot(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}
	
	// Get the bot
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}
	
	// Disable the bot
	bot.PostConfig.Enabled = false
	err = a.botService.UpdateBotConfig(botID, pubkey, bot.PostConfig, nil)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable bot"})
		a.logger.Error("Failed to disable bot", zap.Error(err))
		return
	}
	
	// Unschedule the bot
	a.scheduler.UnscheduleBot(botID)
	
	c.JSON(http.StatusOK, gin.H{
		"message": "Bot disabled",
	})
}

// listBotContent lists the content files for a bot
func (a *API) listBotContent(c *gin.Context) {
    pubkey := c.GetString("pubkey")

    botIDStr := c.Param("botId")
    botID, err := strconv.ParseInt(botIDStr, 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
        return
    }

    // Ensure the bot belongs to the user
    _, err = a.botService.GetBotByID(botID, pubkey)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
        return
    }

    // Build the path to the bot's content folder
    // e.g. content/bot_123
    contentDir := filepath.Join(a.scheduler.GetContentDir(), fmt.Sprintf("bot_%d", botID))
    // If the folder doesn't exist yet, return empty
    if _, err := os.Stat(contentDir); os.IsNotExist(err) {
        c.JSON(http.StatusOK, []string{})
        return
    }

    // Read the directory
    entries, err := os.ReadDir(contentDir)
    if err != nil {
        a.logger.Error("Failed to read content folder", zap.Error(err))
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read content folder"})
        return
    }

    // Collect filenames
    var files []string
    for _, entry := range entries {
        // skip subdirs if you want, or handle them
        if entry.IsDir() {
            continue
        }
        files = append(files, entry.Name())
    }

    c.JSON(http.StatusOK, files)
}

// uploadContent uploads content for a bot
func (a *API) uploadContent(c *gin.Context) {
    pubkey := c.GetString("pubkey")

    botIDStr := c.Param("botId")
    botID, err := strconv.ParseInt(botIDStr, 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
        return
    }

    // Ensure the bot belongs to user
    _, err = a.botService.GetBotByID(botID, pubkey)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
        return
    }

    // Get the file from the form data
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "No file in request"})
        return
    }

    // Build the path: e.g. content/bot_123
    contentDir := filepath.Join(a.scheduler.GetContentDir(), fmt.Sprintf("bot_%d", botID))
    // Ensure the directory exists
    if err := os.MkdirAll(contentDir, 0755); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bot folder"})
        return
    }

    // Destination path
    destPath := filepath.Join(contentDir, file.Filename)

    // Save the file
    if err := c.SaveUploadedFile(file, destPath); err != nil {
        a.logger.Error("Failed to save uploaded file", zap.Error(err))
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "File uploaded successfully", "filename": file.Filename})
}

// deleteContent deletes content for a bot
func (a *API) deleteContent(c *gin.Context) {
    pubkey := c.GetString("pubkey")

    botIDStr := c.Param("botId")
    botID, err := strconv.ParseInt(botIDStr, 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
        return
    }

    filename := c.Param("filename")
    if filename == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "No filename"})
        return
    }

    // Ensure the bot belongs to user
    _, err = a.botService.GetBotByID(botID, pubkey)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
        return
    }

    // Build the path
    contentDir := filepath.Join(a.scheduler.GetContentDir(), fmt.Sprintf("bot_%d", botID))
    filePath := filepath.Join(contentDir, filename)

    // Remove the file
    if err := os.Remove(filePath); err != nil {
        if os.IsNotExist(err) {
            c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
        } else {
            a.logger.Error("Failed to delete file", zap.Error(err))
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"})
        }
        return
    }

    c.Status(http.StatusOK)
}

// uploadToMediaServer handles uploading a file from the server to a media server
func (a *API) uploadToMediaServer(c *gin.Context) {
    pubkey := c.GetString("pubkey")
    botIDStr := c.Param("botId")
    botID, err := strconv.ParseInt(botIDStr, 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
        return
    }

    // Look up the bot to get its details (including its pubkey and media config)
    bot, err := a.botService.GetBotByID(botID, pubkey)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
        return
    }

    // Parse request body.
    var req struct {
        Filename  string `json:"filename" binding:"required"`
        Service   string `json:"service" binding:"required"`
        ServerURL string `json:"serverURL"` // Optional: if provided, will override bot's media config URL.
    }
    if err := c.BindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
        return
    }

    // Build path to the file
    contentDir := filepath.Join(a.scheduler.GetContentDir(), fmt.Sprintf("bot_%d", botID))
    filePath := filepath.Join(contentDir, req.Filename)

    // Verify the file exists
    if _, err := os.Stat(filePath); os.IsNotExist(err) {
        c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
        return
    }

    // Create a new uploader instance that uses the bot's key.
    var uploader scheduler.MediaUploader
    if req.Service == "blossom" {
		// Get the base Blossom server URL
		serverURL := bot.MediaConfig.BlossomServerURL
		if req.ServerURL != "" {
			serverURL = req.ServerURL
		}
		
		// Log the URL for debugging purposes
		a.logger.Info("Creating Blossom uploader with server URL", 
			zap.String("original_url", serverURL))
			
		// According to BUD-02 specification, the upload endpoint should be /upload
		// Make sure the URL ends with /upload
		if !strings.HasSuffix(serverURL, "/upload") {
			serverURL = strings.TrimSuffix(serverURL, "/") + "/upload"
			a.logger.Info("Adding /upload endpoint to URL", 
				zap.String("complete_url", serverURL))
		}
		
		uploader = blossom.NewUploader(
			serverURL,
			a.logger,
			func(url, method string) (string, error) {
				privkey, err := a.botService.GetPrivateKey(bot.Pubkey)
				if err != nil {
					return "", err
				}
				return blossom.CreateBlossomAuthHeader(url, method, privkey)
			},
		)
	} else if req.Service == "nip94" {
        serverURL := bot.MediaConfig.Nip94ServerURL
        if req.ServerURL != "" {
            serverURL = req.ServerURL
        }
        uploader = nip94.NewUploader(
            serverURL,
            "",    // Download URL will be discovered
            nil,   // Supported types will be discovered
            a.logger,
            func(url, method string, payload []byte) (string, error) {
                privkey, err := a.botService.GetPrivateKey(bot.Pubkey)
                if err != nil {
                    return "", err
                }
                return nip94.CreateNIP98AuthHeader(url, method, payload, privkey)
            },
        )
    } else {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Unknown service"})
        return
    }

    caption := strings.TrimSuffix(req.Filename, filepath.Ext(req.Filename))
    altText := caption

    mediaURL, mediaHash, err := uploader.UploadFile(filePath, caption, altText)
    if err != nil {
        a.logger.Error("Failed to upload to media server",
            zap.String("file", filePath),
            zap.String("service", req.Service),
            zap.Error(err))
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload to media server: " + err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "url":     mediaURL,
        "hash":    mediaHash,
        "service": req.Service,
    })
}

// Updated createManualPost function in routes.go to properly process kind 20 posts
func (a *API) createManualPost(c *gin.Context) {
	pubkey := c.GetString("pubkey")
	botIDStr := c.Param("id")
	botID, err := strconv.ParseInt(botIDStr, 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bot ID"})
		return
	}

	// Ensure the bot belongs to the user
	bot, err := a.botService.GetBotByID(botID, pubkey)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Bot not found"})
		return
	}

	// Parse request body
	var req struct {
		Kind     int      `json:"kind" binding:"required"`
		Content  string   `json:"content" binding:"required"`
		Title    string   `json:"title"`
		Alt      string   `json:"alt"` // Added to support alt text for images
		Hashtags []string `json:"hashtags"`
	}

	if err := c.BindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}

	// For kind 20 (picture post), title is required
	if req.Kind == 20 && req.Title == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Title is required for picture posts"})
		return
	}

	// Process content to extract media URLs
	var mediaURL string
	var mediaType string
	var mediaHash string
	
	// Check if content contains URLs
	re := regexp.MustCompile(`https?://[^\s]+`)
	matches := re.FindAllString(req.Content, -1)
	
	if len(matches) > 0 {
		mediaURL = matches[0] // Use the first URL found
		
		// Try to determine media type - this is a placeholder
		// In a real implementation, you might want to make an HTTP head request
		// or infer from URL extension
		mediaType = inferMediaTypeFromURL(mediaURL)
		
		// We don't have a hash yet, but we could calculate it if needed
		// This would require downloading the file, which might be too heavy
		mediaHash = "" 
	}

	// Create the appropriate event
	var event *nostr.Event
	var eventErr error
	
	switch req.Kind {
	case 1:
		// Standard text note
		// Create tags
		var tags []nostr.Tag
		for _, tag := range req.Hashtags {
			tags = append(tags, nostr.Tag{"t", tag})
		}
		
		event, eventErr = a.botService.eventMgr.CreateAndSignTextNoteEvent(bot.Pubkey, req.Content, tags)
	case 20:
		// Picture post
		event, eventErr = a.botService.eventMgr.CreateAndSignPictureEvent(
			bot.Pubkey,
			req.Title,
			req.Content,
			mediaURL,
			mediaType,
			mediaHash,
			req.Alt, // Use the alt text if provided
			req.Hashtags,
		)
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported post kind"})
		return
	}

	if eventErr != nil {
		a.logger.Error("Failed to create event", zap.Error(eventErr))
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create event: " + eventErr.Error()})
		return
	}

	// Configure relay manager
	for _, relay := range bot.Relays {
		if relay.Write {
			if err := a.botService.relayMgr.AddRelay(relay.URL, relay.Read, relay.Write); err != nil {
				a.logger.Warn("Failed to add relay",
					zap.String("url", relay.URL),
					zap.Error(err))
			}
		}
	}

	// Publish to relays
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	publishedRelays, err := a.botService.relayMgr.PublishEvent(ctx, event)
	if err != nil {
		a.logger.Error("Failed to publish event", zap.Error(err))
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish post: " + err.Error()})
		return
	}

	// Return success
	c.JSON(http.StatusOK, gin.H{
		"message":   "Post published successfully",
		"event_id":  event.ID,
		"relays":    publishedRelays,
	})
}

// Helper function to infer media type from URL
func inferMediaTypeFromURL(url string) string {
	// Basic implementation - check file extension
	lowerURL := strings.ToLower(url)
	
	// Image types
	if strings.HasSuffix(lowerURL, ".jpg") || strings.HasSuffix(lowerURL, ".jpeg") {
		return "image/jpeg"
	} else if strings.HasSuffix(lowerURL, ".png") {
		return "image/png"
	} else if strings.HasSuffix(lowerURL, ".gif") {
		return "image/gif"
	} else if strings.HasSuffix(lowerURL, ".webp") {
		return "image/webp"
	} else if strings.HasSuffix(lowerURL, ".svg") {
		return "image/svg+xml"
	} else if strings.HasSuffix(lowerURL, ".avif") {
		return "image/avif"
	}
	
	// Default to generic image type if we can't determine specifically
	return "image/jpeg"
}

// getStats gets overall statistics
func (a *API) getStats(c *gin.Context) {
	// This will be implemented when we add statistics
	c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}

// getBotStats gets statistics for a specific bot
func (a *API) getBotStats(c *gin.Context) {
	// This will be implemented when we add statistics
	c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented yet"})
}

// Helper function to extract hashtags from tags
func extractHashtags(tags []nostr.Tag) []string {
	var hashtags []string
	for _, tag := range tags {
		if tag[0] == "t" && len(tag) > 1 {
			hashtags = append(hashtags, tag[1])
		}
	}
	return hashtags
}