package api

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strconv"
	"strings"

	"pinepods/gpodder-api/internal/db"
	"pinepods/gpodder-api/internal/models"

	"github.com/gin-gonic/gin"
)

// Maximum number of items to return in listings
const MAX_DIRECTORY_ITEMS = 100

// Common tag categories for podcasts
var commonCategories = []models.Tag{
	{Title: "Technology", Tag: "technology", Usage: 530},
	{Title: "Society & Culture", Tag: "society-culture", Usage: 420},
	{Title: "Arts", Tag: "arts", Usage: 400},
	{Title: "News & Politics", Tag: "news-politics", Usage: 320},
	{Title: "Business", Tag: "business", Usage: 300},
	{Title: "Education", Tag: "education", Usage: 280},
	{Title: "Science", Tag: "science", Usage: 260},
	{Title: "Comedy", Tag: "comedy", Usage: 240},
	{Title: "Health", Tag: "health", Usage: 220},
	{Title: "Sports", Tag: "sports", Usage: 200},
	{Title: "History", Tag: "history", Usage: 180},
	{Title: "Religion & Spirituality", Tag: "religion-spirituality", Usage: 160},
	{Title: "TV & Film", Tag: "tv-film", Usage: 140},
	{Title: "Music", Tag: "music", Usage: 120},
	{Title: "Games & Hobbies", Tag: "games-hobbies", Usage: 100},
}

// getTopTags handles GET /api/2/tags/{count}.json
func getTopTags(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Parse count parameter
		countStr := c.Param("count")
		count, err := strconv.Atoi(countStr)
		if err != nil || count < 1 || count > MAX_DIRECTORY_ITEMS {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid count parameter: must be between 1 and %d", MAX_DIRECTORY_ITEMS)})
			return
		}

		var rows *sql.Rows

		if database.IsPostgreSQLDB() {
			// PostgreSQL specific query using array functions
			rows, err = database.Query(`
				WITH category_counts AS (
					SELECT
						unnest(string_to_array(Categories, ',')) as category,
						COUNT(*) as usage
					FROM "Podcasts"
					WHERE Categories IS NOT NULL AND Categories != ''
					GROUP BY category
				)
				SELECT
					category as tag,
					category as title,
					usage
				FROM category_counts
				ORDER BY usage DESC
				LIMIT $1
			`, count)
		} else {
			// MySQL equivalent - need to use different approach since MySQL doesn't have unnest
			// Using FIND_IN_SET with a subquery for each common category
			// This is a simplified approach - in a real implementation you might want to
			// use a more sophisticated method for MySQL to extract and count categories
			placeholders := make([]string, len(commonCategories))
			args := make([]interface{}, len(commonCategories)+1)
			args[0] = count // First arg is the LIMIT parameter

			for i, category := range commonCategories {
				placeholders[i] = fmt.Sprintf(`
					SELECT
						?,
						?,
						COUNT(*) as usage
					FROM Podcasts
					WHERE Categories IS NOT NULL AND FIND_IN_SET(?, Categories) > 0
				`)
				args[i+1] = category.Tag
				// In a real implementation, we would add more parameters here
			}

			// In a real implementation, this query would be more sophisticated
			// For now, we'll just return results from the commonCategories slice
			// and limit it by count
			rows = nil
			err = fmt.Errorf("MySQL implementation falls back to default categories")
		}

		// If query fails or returns no rows, use the default list
		if err != nil || rows == nil {
			log.Printf("Error querying categories, using default list: %v", err)
			result := commonCategories
			if len(result) > count {
				result = result[:count]
			}
			c.JSON(http.StatusOK, result)
			return
		}
		defer rows.Close()

		// Process database results
		tags := make([]models.Tag, 0, count)
		for rows.Next() {
			var tag models.Tag
			if err := rows.Scan(&tag.Tag, &tag.Title, &tag.Usage); err != nil {
				log.Printf("Error scanning tag row: %v", err)
				continue
			}
			// Clean the tag
			tag.Tag = strings.ToLower(strings.TrimSpace(tag.Tag))
			tag.Tag = strings.ReplaceAll(tag.Tag, " ", "-")
			// Format the title properly
			tag.Title = formatTagTitle(tag.Tag)
			tags = append(tags, tag)
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating tag rows: %v", err)
		}

		// If we got no results from the database, use the default list
		if len(tags) == 0 {
			result := commonCategories
			if len(result) > count {
				result = result[:count]
			}
			c.JSON(http.StatusOK, result)
			return
		}

		c.JSON(http.StatusOK, tags)
	}
}

// formatTagTitle formats a tag string into a proper title
func formatTagTitle(tag string) string {
	// Replace hyphens with spaces
	title := strings.ReplaceAll(tag, "-", " ")

	// Convert to title case (capitalize first letter of each word)
	words := strings.Fields(title)
	for i, word := range words {
		if len(word) > 0 {
			words[i] = strings.ToUpper(word[:1]) + word[1:]
		}
	}

	return strings.Join(words, " ")
}

// getPodcastsForTag handles GET /api/2/tag/{tag}/{count}.json
func getPodcastsForTag(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Parse parameters
		tag := c.Param("tag")
		countStr := c.Param("count")
		count, err := strconv.Atoi(countStr)
		if err != nil || count < 1 || count > MAX_DIRECTORY_ITEMS {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid count parameter: must be between 1 and %d", MAX_DIRECTORY_ITEMS)})
			return
		}

		// Format tag for searching
		searchTag := "%" + strings.ReplaceAll(tag, "-", " ") + "%"

		// Query podcasts with the given tag
		var rows *sql.Rows

		if database.IsPostgreSQLDB() {
			// PostgreSQL query with DISTINCT ON
			rows, err = database.Query(`
				SELECT DISTINCT ON (p.PodcastID)
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) OVER (PARTITION BY p.PodcastID) as subscribers
				FROM "Podcasts" p
				JOIN "Users" u ON p.UserID = u.UserID
				WHERE
					p.Categories ILIKE $1 OR
					p.PodcastName ILIKE $1 OR
					p.Description ILIKE $1
				ORDER BY p.PodcastID, subscribers DESC
				LIMIT $2
			`, searchTag, count)
		} else {
			// MySQL equivalent without DISTINCT ON and window functions
			rows, err = database.Query(`
				SELECT
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers
				FROM Podcasts p
				JOIN Users u ON p.UserID = u.UserID
				WHERE
					p.Categories LIKE ? OR
					p.PodcastName LIKE ? OR
					p.Description LIKE ?
				GROUP BY p.PodcastID, p.PodcastName, p.Author, p.Description,
						p.FeedURL, p.WebsiteURL, p.ArtworkURL
				ORDER BY subscribers DESC
				LIMIT ?
			`, searchTag, searchTag, searchTag, count)
		}

		if err != nil {
			log.Printf("Error querying podcasts by tag: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcasts for tag"})
			return
		}
		defer rows.Close()

		// Build podcast list
		podcasts := make([]models.Podcast, 0)
		for rows.Next() {
			var podcast models.Podcast
			var podcastID int
			var author, description, websiteURL, artworkURL sql.NullString
			var subscribers int
			if err := rows.Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
			); err != nil {
				log.Printf("Error scanning podcast: %v", err)
				continue
			}
			// Set optional fields if present
			if author.Valid {
				podcast.Author = author.String
			}
			if description.Valid {
				podcast.Description = description.String
			}
			if websiteURL.Valid {
				podcast.Website = websiteURL.String
			}
			if artworkURL.Valid {
				podcast.LogoURL = artworkURL.String
			}
			podcast.Subscribers = subscribers
			podcast.MygpoLink = fmt.Sprintf("/podcast/%d", podcastID)
			podcasts = append(podcasts, podcast)
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating podcast rows: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process podcasts"})
			return
		}

		c.JSON(http.StatusOK, podcasts)
	}
}

// getPodcastData handles GET /api/2/data/podcast.json
func getPodcastData(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Get podcast URL from query parameter
		podcastURL := c.Query("url")
		if podcastURL == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "URL parameter is required"})
			return
		}

		// Query podcast data
		var podcast models.Podcast
		var podcastID int
		var author, description, websiteURL, artworkURL sql.NullString
		var subscribers int

		var err error

		if database.IsPostgreSQLDB() {
			// PostgreSQL query
			err = database.QueryRow(`
				SELECT
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers
				FROM "Podcasts" p
				JOIN "Users" u ON p.UserID = u.UserID
				WHERE p.FeedURL = $1
				GROUP BY p.PodcastID
				LIMIT 1
			`, podcastURL).Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
			)
		} else {
			// MySQL query
			err = database.QueryRow(`
				SELECT
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers
				FROM Podcasts p
				JOIN Users u ON p.UserID = u.UserID
				WHERE p.FeedURL = ?
				GROUP BY p.PodcastID
				LIMIT 1
			`, podcastURL).Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
			)
		}

		if err != nil {
			if err == sql.ErrNoRows {
				c.JSON(http.StatusNotFound, gin.H{"error": "Podcast not found"})
			} else {
				log.Printf("Error querying podcast data: %v", err)
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get podcast data"})
			}
			return
		}

		// Set optional fields if present
		if author.Valid {
			podcast.Author = author.String
		}
		if description.Valid {
			podcast.Description = description.String
		}
		if websiteURL.Valid {
			podcast.Website = websiteURL.String
		}
		if artworkURL.Valid {
			podcast.LogoURL = artworkURL.String
		}

		podcast.Subscribers = subscribers
		podcast.MygpoLink = fmt.Sprintf("/podcast/%d", podcastID)

		c.JSON(http.StatusOK, podcast)
	}
}

// isValidCallbackName checks if a JSONP callback name is valid and safe
func isValidCallbackName(callback string) bool {
	// Only allow alphanumeric characters, underscore, and period in callback names
	validCallbackRegex := regexp.MustCompile(`^[a-zA-Z0-9_.]+$`)
	return validCallbackRegex.MatchString(callback)
}

// podcastSearch handles GET /search.{format}
func podcastSearch(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Get query parameter
		query := c.Query("q")
		if query == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter 'q' is required"})
			return
		}

		// Get format parameter
		format := c.Param("format")
		if format == "" {
			format = "json" // Default format
		}

		// Parse optional parameters
		scaleLogo := c.Query("scale_logo")
		var scaleSize int
		if scaleLogo != "" {
			size, err := strconv.Atoi(scaleLogo)
			if err != nil || size < 1 || size > 256 {
				scaleSize = 64 // Default size
			} else {
				scaleSize = size
			}
		}

		// Limit search terms to prevent performance issues
		if len(query) > 100 {
			query = query[:100]
		}

		// Prepare search query terms for SQL
		searchTerms := "%" + strings.ReplaceAll(query, " ", "%") + "%"

		// Search podcasts
		var rows *sql.Rows
		var err error

		if database.IsPostgreSQLDB() {
			// PostgreSQL query with DISTINCT ON and window functions
			rows, err = database.Query(`
				SELECT DISTINCT ON (p.PodcastID)
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) OVER (PARTITION BY p.PodcastID) as subscribers,
					CASE
						WHEN p.PodcastName ILIKE $1 THEN 1
						WHEN p.Author ILIKE $1 THEN 2
						WHEN p.Description ILIKE $1 THEN 3
						ELSE 4
					END as match_priority
				FROM "Podcasts" p
				JOIN "Users" u ON p.UserID = u.UserID
				WHERE
					p.PodcastName ILIKE $1 OR
					p.Author ILIKE $1 OR
					p.Description ILIKE $1
				ORDER BY p.PodcastID, match_priority, subscribers DESC
				LIMIT $2
			`, searchTerms, MAX_DIRECTORY_ITEMS)
		} else {
			// MySQL query without DISTINCT ON and window functions
			rows, err = database.Query(`
				SELECT
					p.PodcastID, p.PodcastName, p.Author, p.Description,
					p.FeedURL, p.WebsiteURL, p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers,
					CASE
						WHEN p.PodcastName LIKE ? THEN 1
						WHEN p.Author LIKE ? THEN 2
						WHEN p.Description LIKE ? THEN 3
						ELSE 4
					END as match_priority
				FROM Podcasts p
				JOIN Users u ON p.UserID = u.UserID
				WHERE
					p.PodcastName LIKE ? OR
					p.Author LIKE ? OR
					p.Description LIKE ?
				GROUP BY p.PodcastID, p.PodcastName, p.Author, p.Description,
						p.FeedURL, p.WebsiteURL, p.ArtworkURL, match_priority
				ORDER BY match_priority, subscribers DESC
				LIMIT ?
			`, searchTerms, searchTerms, searchTerms, searchTerms, searchTerms, searchTerms, MAX_DIRECTORY_ITEMS)
		}

		if err != nil {
			log.Printf("Error searching podcasts: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search podcasts"})
			return
		}
		defer rows.Close()

		// Build podcast list
		podcasts := make([]models.Podcast, 0)
		for rows.Next() {
			var podcast models.Podcast
			var podcastID int
			var author, description, websiteURL, artworkURL sql.NullString
			var subscribers, matchPriority int

			if err := rows.Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
				&matchPriority,
			); err != nil {
				log.Printf("Error scanning podcast: %v", err)
				continue
			}

			// Set optional fields if present
			if author.Valid {
				podcast.Author = author.String
			}

			if description.Valid {
				podcast.Description = description.String
			}

			if websiteURL.Valid {
				podcast.Website = websiteURL.String
			}

			if artworkURL.Valid {
				podcast.LogoURL = artworkURL.String

				// Add scaled logo URL if requested
				if scaleLogo != "" {
					podcast.ScaledLogoURL = fmt.Sprintf("/logo/%d/%s", scaleSize, artworkURL.String)
				}
			}

			podcast.Subscribers = subscribers
			podcast.MygpoLink = fmt.Sprintf("/podcast/%d", podcastID)

			podcasts = append(podcasts, podcast)
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating podcast rows: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process search results"})
			return
		}

		// Return in requested format
		switch format {
		case "json":
			c.JSON(http.StatusOK, podcasts)
		case "jsonp":
			// JSONP callback
			callback := c.Query("jsonp")
			if callback == "" {
				callback = "callback" // Default callback name
			}

			// Validate callback name for security
			if !isValidCallbackName(callback) {
				c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSONP callback name"})
				return
			}

			// Convert to JSON using the standard json package
			jsonData, err := json.Marshal(podcasts)
			if err != nil {
				log.Printf("Error marshaling to JSON: %v", err)
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal to JSON"})
				return
			}

			// Wrap in callback
			c.Header("Content-Type", "application/javascript")
			c.String(http.StatusOK, "%s(%s);", callback, string(jsonData))
		case "txt":
			// Plain text format - just URLs
			var sb strings.Builder
			for _, podcast := range podcasts {
				sb.WriteString(podcast.URL)
				sb.WriteString("\n")
			}
			c.String(http.StatusOK, sb.String())
		case "opml":
			// OPML format
			opml := generateOpml(podcasts)
			c.Header("Content-Type", "text/xml")
			c.String(http.StatusOK, opml)
		case "xml":
			// XML format
			xml := generateXml(podcasts)
			c.Header("Content-Type", "text/xml")
			c.String(http.StatusOK, xml)
		default:
			c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"})
		}
	}
}

// getToplist handles GET /toplist/{number}.{format}
func getToplist(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Parse count parameter
		countStr := c.Param("number")
		count, err := strconv.Atoi(countStr)
		if err != nil || count < 1 || count > MAX_DIRECTORY_ITEMS {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid number parameter: must be between 1 and %d", MAX_DIRECTORY_ITEMS)})
			return
		}

		// Get format parameter
		format := c.Param("format")
		if format == "" {
			format = "json" // Default format
		}

		// Parse optional parameters
		scaleLogo := c.Query("scale_logo")
		var scaleSize int
		if scaleLogo != "" {
			size, err := strconv.Atoi(scaleLogo)
			if err != nil || size < 1 || size > 256 {
				scaleSize = 64 // Default size
			} else {
				scaleSize = size
			}
		}

		// Query top podcasts
		var rows *sql.Rows

		if database.IsPostgreSQLDB() {
			// PostgreSQL query with CTE
			rows, err = database.Query(`
				WITH podcast_stats AS (
					SELECT
						p.PodcastID,
						p.PodcastName,
						p.Author,
						p.Description,
						p.FeedURL,
						p.WebsiteURL,
						p.ArtworkURL,
						COUNT(DISTINCT u.UserID) as subscribers,
						0 as position_last_week -- Placeholder for now
					FROM "Podcasts" p
					JOIN "Users" u ON p.UserID = u.UserID
					GROUP BY p.PodcastID
				)
				SELECT * FROM podcast_stats
				ORDER BY subscribers DESC, PodcastID
				LIMIT $1
			`, count)
		} else {
			// MySQL query without CTE
			rows, err = database.Query(`
				SELECT
					p.PodcastID,
					p.PodcastName,
					p.Author,
					p.Description,
					p.FeedURL,
					p.WebsiteURL,
					p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers,
					0 as position_last_week -- Placeholder for now
				FROM Podcasts p
				JOIN Users u ON p.UserID = u.UserID
				GROUP BY p.PodcastID, p.PodcastName, p.Author, p.Description,
						p.FeedURL, p.WebsiteURL, p.ArtworkURL
				ORDER BY subscribers DESC, PodcastID
				LIMIT ?
			`, count)
		}

		if err != nil {
			log.Printf("Error querying top podcasts: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get top podcasts"})
			return
		}
		defer rows.Close()

		// Build podcast list
		podcasts := make([]models.Podcast, 0)
		for rows.Next() {
			var podcast models.Podcast
			var podcastID int
			var author, description, websiteURL, artworkURL sql.NullString
			var subscribers, positionLastWeek int

			if err := rows.Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
				&positionLastWeek,
			); err != nil {
				log.Printf("Error scanning podcast: %v", err)
				continue
			}

			// Set optional fields if present
			if author.Valid {
				podcast.Author = author.String
			}

			if description.Valid {
				podcast.Description = description.String
			}

			if websiteURL.Valid {
				podcast.Website = websiteURL.String
			}

			if artworkURL.Valid {
				podcast.LogoURL = artworkURL.String

				// Add scaled logo URL if requested
				if scaleLogo != "" {
					podcast.ScaledLogoURL = fmt.Sprintf("/logo/%d/%s", scaleSize, artworkURL.String)
				}
			}

			podcast.Subscribers = subscribers
			podcast.MygpoLink = fmt.Sprintf("/podcast/%d", podcastID)

			podcasts = append(podcasts, podcast)
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating podcast rows: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process podcasts"})
			return
		}

		// Return in requested format (same as search)
		switch format {
		case "json":
			c.JSON(http.StatusOK, podcasts)
		case "jsonp":
			// JSONP callback
			callback := c.Query("jsonp")
			if callback == "" {
				callback = "callback" // Default callback name
			}

			// Validate callback name for security
			if !isValidCallbackName(callback) {
				c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSONP callback name"})
				return
			}

			// Convert to JSON using the standard json package
			jsonData, err := json.Marshal(podcasts)
			if err != nil {
				log.Printf("Error marshaling to JSON: %v", err)
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal to JSON"})
				return
			}

			// Wrap in callback
			c.Header("Content-Type", "application/javascript")
			c.String(http.StatusOK, "%s(%s);", callback, string(jsonData))
		case "txt":
			// Plain text format - just URLs
			var sb strings.Builder
			for _, podcast := range podcasts {
				sb.WriteString(podcast.URL)
				sb.WriteString("\n")
			}
			c.String(http.StatusOK, sb.String())
		case "opml":
			// OPML format
			opml := generateOpml(podcasts)
			c.Header("Content-Type", "text/xml")
			c.String(http.StatusOK, opml)
		case "xml":
			// XML format
			xml := generateXml(podcasts)
			c.Header("Content-Type", "text/xml")
			c.String(http.StatusOK, xml)
		default:
			c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"})
		}
	}
}

// getSuggestions handles GET /suggestions/{count}.{format}
func getSuggestions(database *db.Database) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Get user ID from middleware
		userID, exists := c.Get("userID")
		if !exists {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
			return
		}

		// Parse count parameter
		countStr := c.Param("count")
		count, err := strconv.Atoi(countStr)
		if err != nil || count < 1 || count > MAX_DIRECTORY_ITEMS {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid count parameter: must be between 1 and %d", MAX_DIRECTORY_ITEMS)})
			return
		}

		// Get format parameter
		format := c.Param("format")
		if format == "" {
			format = "json" // Default format
		}

		// Get user's current subscriptions
		var rows *sql.Rows

		if database.IsPostgreSQLDB() {
			rows, err = database.Query(`
				SELECT FeedURL FROM "Podcasts" WHERE UserID = $1
			`, userID)
		} else {
			rows, err = database.Query(`
				SELECT FeedURL FROM Podcasts WHERE UserID = ?
			`, userID)
		}

		if err != nil {
			log.Printf("Error getting user subscriptions: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get subscriptions"})
			return
		}
		defer rows.Close()

		// Build map of current subscriptions
		currentSubs := make(map[string]bool)
		for rows.Next() {
			var url string
			if err := rows.Scan(&url); err != nil {
				log.Printf("Error scanning subscription URL: %v", err)
				continue
			}
			currentSubs[url] = true
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating subscription rows: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process subscriptions"})
			return
		}

		// Query for similar podcasts based on categories of current subscriptions
		if database.IsPostgreSQLDB() {
			rows, err = database.Query(`
				WITH user_categories AS (
					SELECT DISTINCT unnest(string_to_array(p.Categories, ',')) as category
					FROM "Podcasts" p
					WHERE p.UserID = $1 AND p.Categories IS NOT NULL AND p.Categories != ''
				),
				recommended_podcasts AS (
					SELECT DISTINCT ON (p.PodcastID)
						p.PodcastID,
						p.PodcastName,
						p.Author,
						p.Description,
						p.FeedURL,
						p.WebsiteURL,
						p.ArtworkURL,
						COUNT(DISTINCT u.UserID) as subscribers
					FROM "Podcasts" p
					JOIN "Users" u ON p.UserID = u.UserID
					WHERE EXISTS (
						SELECT 1 FROM user_categories uc
						WHERE p.Categories ILIKE '%' || uc.category || '%'
					)
					AND p.FeedURL NOT IN (
						SELECT FeedURL FROM "Podcasts" WHERE UserID = $1
					)
					GROUP BY p.PodcastID
					ORDER BY p.PodcastID, subscribers DESC
				)
				SELECT * FROM recommended_podcasts
				LIMIT $2
			`, userID, count)
		} else {
			// For MySQL, we use a simpler approach without CTEs and array functions
			rows, err = database.Query(`
				SELECT DISTINCT
					p.PodcastID,
					p.PodcastName,
					p.Author,
					p.Description,
					p.FeedURL,
					p.WebsiteURL,
					p.ArtworkURL,
					COUNT(DISTINCT u.UserID) as subscribers
				FROM Podcasts p
				JOIN Users u ON p.UserID = u.UserID
				JOIN (
					SELECT DISTINCT p.Categories
					FROM Podcasts p
					WHERE p.UserID = ? AND p.Categories IS NOT NULL AND p.Categories != ''
				) as user_cats
				WHERE p.Categories LIKE CONCAT('%', user_cats.Categories, '%')
				AND p.FeedURL NOT IN (
					SELECT FeedURL FROM Podcasts WHERE UserID = ?
				)
				GROUP BY p.PodcastID, p.PodcastName, p.Author, p.Description,
						p.FeedURL, p.WebsiteURL, p.ArtworkURL
				ORDER BY subscribers DESC, p.PodcastID
				LIMIT ?
			`, userID, userID, count)
		}

		if err != nil {
			log.Printf("Error querying suggested podcasts: %v", err)

			// If category-based query fails, fall back to popularity-based suggestions
			if database.IsPostgreSQLDB() {
				rows, err = database.Query(`
					SELECT
						p.PodcastID,
						p.PodcastName,
						p.Author,
						p.Description,
						p.FeedURL,
						p.WebsiteURL,
						p.ArtworkURL,
						COUNT(DISTINCT u.UserID) as subscribers
					FROM "Podcasts" p
					JOIN "Users" u ON p.UserID = u.UserID
					WHERE p.FeedURL NOT IN (
						SELECT FeedURL FROM "Podcasts" WHERE UserID = $1
					)
					GROUP BY p.PodcastID
					ORDER BY subscribers DESC, p.PodcastID
					LIMIT $2
				`, userID, count)
			} else {
				rows, err = database.Query(`
					SELECT
						p.PodcastID,
						p.PodcastName,
						p.Author,
						p.Description,
						p.FeedURL,
						p.WebsiteURL,
						p.ArtworkURL,
						COUNT(DISTINCT u.UserID) as subscribers
					FROM Podcasts p
					JOIN Users u ON p.UserID = u.UserID
					WHERE p.FeedURL NOT IN (
						SELECT FeedURL FROM Podcasts WHERE UserID = ?
					)
					GROUP BY p.PodcastID, p.PodcastName, p.Author, p.Description,
							p.FeedURL, p.WebsiteURL, p.ArtworkURL
					ORDER BY subscribers DESC, p.PodcastID
					LIMIT ?
				`, userID, count)
			}

			if err != nil {
				log.Printf("Error querying popular podcasts: %v", err)
				c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get suggestions"})
				return
			}
		}
		defer rows.Close()

		// Build podcast list
		podcasts := make([]models.Podcast, 0)
		for rows.Next() {
			var podcast models.Podcast
			var podcastID int
			var author, description, websiteURL, artworkURL sql.NullString
			var subscribers int

			if err := rows.Scan(
				&podcastID,
				&podcast.Title,
				&author,
				&description,
				&podcast.URL,
				&websiteURL,
				&artworkURL,
				&subscribers,
			); err != nil {
				log.Printf("Error scanning podcast: %v", err)
				continue
			}

			// Skip if already subscribed (double-check)
			if currentSubs[podcast.URL] {
				continue
			}

			// Set optional fields if present
			if author.Valid {
				podcast.Author = author.String
			}

			if description.Valid {
				podcast.Description = description.String
			}

			if websiteURL.Valid {
				podcast.Website = websiteURL.String
			}

			if artworkURL.Valid {
				podcast.LogoURL = artworkURL.String
			}

			podcast.Subscribers = subscribers
			podcast.MygpoLink = fmt.Sprintf("/podcast/%d", podcastID)

			podcasts = append(podcasts, podcast)
		}

		if err = rows.Err(); err != nil {
			log.Printf("Error iterating suggestion rows: %v", err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process suggestions"})
			return
		}

		// Return in requested format
		switch format {
		case "json":
			c.JSON(http.StatusOK, podcasts)
		case "txt":
			// Plain text format - just URLs
			var sb strings.Builder
			for _, podcast := range podcasts {
				sb.WriteString(podcast.URL)
				sb.WriteString("\n")
			}
			c.String(http.StatusOK, sb.String())
		case "opml":
			// OPML format
			opml := generateOpml(podcasts)
			c.Header("Content-Type", "text/xml")
			c.String(http.StatusOK, opml)
		default:
			c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported format"})
		}
	}
}

// generateOpml generates an OPML format document from a list of podcasts
func generateOpml(podcasts []models.Podcast) string {
	var sb strings.Builder

	sb.WriteString(`<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
  <head>
    <title>gPodder Subscriptions</title>
  </head>
  <body>
`)

	for _, podcast := range podcasts {
		sb.WriteString(fmt.Sprintf(`    <outline text="%s" type="rss" xmlUrl="%s"`,
			escapeXml(podcast.Title), escapeXml(podcast.URL)))

		if podcast.Website != "" {
			sb.WriteString(fmt.Sprintf(` htmlUrl="%s"`, escapeXml(podcast.Website)))
		}

		if podcast.Description != "" {
			sb.WriteString(fmt.Sprintf(` description="%s"`, escapeXml(podcast.Description)))
		}

		sb.WriteString(" />\n")
	}

	sb.WriteString(`  </body>
</opml>`)

	return sb.String()
}

// generateXml generates an XML format document from a list of podcasts
func generateXml(podcasts []models.Podcast) string {
	var sb strings.Builder

	sb.WriteString(`<?xml version="1.0" encoding="utf-8"?>
<podcasts>
`)

	for _, podcast := range podcasts {
		sb.WriteString("  <podcast>\n")
		sb.WriteString(fmt.Sprintf("    <title>%s</title>\n", escapeXml(podcast.Title)))
		sb.WriteString(fmt.Sprintf("    <url>%s</url>\n", escapeXml(podcast.URL)))

		if podcast.Website != "" {
			sb.WriteString(fmt.Sprintf("    <website>%s</website>\n", escapeXml(podcast.Website)))
		}

		if podcast.MygpoLink != "" {
			sb.WriteString(fmt.Sprintf("    <mygpo_link>%s</mygpo_link>\n", escapeXml(podcast.MygpoLink)))
		}

		if podcast.Author != "" {
			sb.WriteString(fmt.Sprintf("    <author>%s</author>\n", escapeXml(podcast.Author)))
		}

		if podcast.Description != "" {
			sb.WriteString(fmt.Sprintf("    <description>%s</description>\n", escapeXml(podcast.Description)))
		}

		sb.WriteString(fmt.Sprintf("    <subscribers>%d</subscribers>\n", podcast.Subscribers))

		if podcast.LogoURL != "" {
			sb.WriteString(fmt.Sprintf("    <logo_url>%s</logo_url>\n", escapeXml(podcast.LogoURL)))
		}

		if podcast.ScaledLogoURL != "" {
			sb.WriteString(fmt.Sprintf("    <scaled_logo_url>%s</scaled_logo_url>\n", escapeXml(podcast.ScaledLogoURL)))
		}

		sb.WriteString("  </podcast>\n")
	}

	sb.WriteString("</podcasts>")

	return sb.String()
}

// escapeXml escapes special characters for XML output
func escapeXml(s string) string {
	s = strings.ReplaceAll(s, "&", "&amp;")
	s = strings.ReplaceAll(s, "<", "&lt;")
	s = strings.ReplaceAll(s, ">", "&gt;")
	s = strings.ReplaceAll(s, "\"", "&quot;")
	s = strings.ReplaceAll(s, "'", "&apos;")
	return s
}
