package services

import (
	"context"
	"fmt"
	"strings"

	"go.uber.org/zap"
	"gorm.io/gorm"
	"veza-backend-api/internal/models"
)

// FullTextSearchService provides full-text search using PostgreSQL tsvector/tsquery
// BE-SVC-006: Implement search service using PostgreSQL full-text search
type FullTextSearchService struct {
	db     *gorm.DB
	logger *zap.Logger
}

// NewFullTextSearchService creates a new full-text search service
func NewFullTextSearchService(db *gorm.DB, logger *zap.Logger) *FullTextSearchService {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &FullTextSearchService{
		db:     db,
		logger: logger,
	}
}

// SearchParams represents search parameters
type SearchParams struct {
	Query    string   // Search query text
	Types    []string // Types to search: "track", "user", "playlist"
	Page     int      // Page number (default: 1)
	Limit    int      // Results per page (default: 20, max: 100)
	MinScore float64  // Minimum relevance score (0.0-1.0)
}

// FullTextSearchResult represents unified full-text search results
type FullTextSearchResult struct {
	Tracks    []TrackSearchResult    `json:"tracks"`
	Users     []UserSearchResult     `json:"users"`
	Playlists []PlaylistSearchResult `json:"playlists"`
	Total     int64                  `json:"total"`
	Page      int                    `json:"page"`
	Limit     int                    `json:"limit"`
}

// TrackSearchResult represents a track search result with relevance score
type TrackSearchResult struct {
	models.Track
	Relevance float64 `json:"relevance"`
}

// UserSearchResult represents a user search result with relevance score
type UserSearchResult struct {
	models.User
	Relevance float64 `json:"relevance"`
}

// PlaylistSearchResult represents a playlist search result with relevance score
type PlaylistSearchResult struct {
	models.Playlist
	Relevance float64 `json:"relevance"`
}

// Search performs a full-text search across tracks, users, and playlists
func (s *FullTextSearchService) Search(ctx context.Context, params SearchParams) (*FullTextSearchResult, error) {
	result := &FullTextSearchResult{
		Tracks:    []TrackSearchResult{},
		Users:     []UserSearchResult{},
		Playlists: []PlaylistSearchResult{},
		Page:      params.Page,
		Limit:     params.Limit,
	}

	// Validate and set defaults
	if params.Page < 1 {
		params.Page = 1
	}
	if params.Limit < 1 {
		params.Limit = 20
	}
	if params.Limit > 100 {
		params.Limit = 100
	}
	if params.MinScore < 0 {
		params.MinScore = 0
	}
	if params.MinScore > 1 {
		params.MinScore = 1
	}

	// Build search types - if empty, search all
	searchAll := len(params.Types) == 0
	searchTracks := searchAll || containsString(params.Types, "track")
	searchUsers := searchAll || containsString(params.Types, "user")
	searchPlaylists := searchAll || containsString(params.Types, "playlist")

	// Prepare search query for PostgreSQL tsquery
	searchQuery := s.prepareSearchQuery(params.Query)

	// Search tracks
	if searchTracks && params.Query != "" {
		tracks, err := s.searchTracks(ctx, searchQuery, params)
		if err != nil {
			s.logger.Warn("Failed to search tracks", zap.Error(err))
		} else {
			result.Tracks = tracks
		}
	}

	// Search users
	if searchUsers && params.Query != "" {
		users, err := s.searchUsers(ctx, searchQuery, params)
		if err != nil {
			s.logger.Warn("Failed to search users", zap.Error(err))
		} else {
			result.Users = users
		}
	}

	// Search playlists
	if searchPlaylists && params.Query != "" {
		playlists, err := s.searchPlaylists(ctx, searchQuery, params)
		if err != nil {
			s.logger.Warn("Failed to search playlists", zap.Error(err))
		} else {
			result.Playlists = playlists
		}
	}

	// Calculate total
	result.Total = int64(len(result.Tracks) + len(result.Users) + len(result.Playlists))

	return result, nil
}

// prepareSearchQuery converts a search query to PostgreSQL tsquery format
func (s *FullTextSearchService) prepareSearchQuery(query string) string {
	if query == "" {
		return ""
	}

	// Remove special characters and split into words
	words := strings.Fields(strings.ToLower(query))

	// Build tsquery: word1 & word2 & word3 (AND search)
	// Use prefix matching with :* for better results
	tsquery := strings.Join(words, " & ")

	// Add prefix matching to last word if it's not too short
	if len(words) > 0 {
		lastWord := words[len(words)-1]
		if len(lastWord) > 2 {
			tsquery = strings.TrimSuffix(tsquery, lastWord) + lastWord + ":*"
		}
	}

	return tsquery
}

// searchTracks performs full-text search on tracks using PostgreSQL tsvector
func (s *FullTextSearchService) searchTracks(ctx context.Context, searchQuery string, params SearchParams) ([]TrackSearchResult, error) {
	offset := (params.Page - 1) * params.Limit

	// Build tsvector from title and artist
	// Use the GIN indexes created in migration 048_search_indexes.sql
	var results []struct {
		models.Track
		Relevance float64
	}

	query := s.db.WithContext(ctx).
		Model(&models.Track{}).
		Select(`
			tracks.*,
			ts_rank_cd(
				setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
				setweight(to_tsvector('english', COALESCE(artist, '')), 'B'),
				plainto_tsquery('english', ?)
			) as relevance
		`, searchQuery).
		Where("is_public = ? AND deleted_at IS NULL", true).
		Where(`
			to_tsvector('english', COALESCE(title, '')) ||
			to_tsvector('english', COALESCE(artist, '')) @@ plainto_tsquery('english', ?)
		`, searchQuery).
		Having("relevance >= ?", params.MinScore).
		Order("relevance DESC, created_at DESC").
		Offset(offset).
		Limit(params.Limit)

	if err := query.Scan(&results).Error; err != nil {
		return nil, fmt.Errorf("failed to search tracks: %w", err)
	}

	trackResults := make([]TrackSearchResult, len(results))
	for i, r := range results {
		trackResults[i] = TrackSearchResult{
			Track:     r.Track,
			Relevance: r.Relevance,
		}
	}

	return trackResults, nil
}

// searchUsers performs full-text search on users using PostgreSQL tsvector
func (s *FullTextSearchService) searchUsers(ctx context.Context, searchQuery string, params SearchParams) ([]UserSearchResult, error) {
	offset := (params.Page - 1) * params.Limit

	var results []struct {
		models.User
		Relevance float64
	}

	query := s.db.WithContext(ctx).
		Model(&models.User{}).
		Select(`
			users.*,
			ts_rank_cd(
				setweight(to_tsvector('english', COALESCE(username, '')), 'A') ||
				setweight(to_tsvector('english', COALESCE(first_name, '')), 'B') ||
				setweight(to_tsvector('english', COALESCE(last_name, '')), 'B'),
				plainto_tsquery('english', ?)
			) as relevance
		`, searchQuery).
		Where("deleted_at IS NULL").
		Where(`
			to_tsvector('english', COALESCE(username, '')) ||
			to_tsvector('english', COALESCE(first_name, '')) ||
			to_tsvector('english', COALESCE(last_name, '')) @@ plainto_tsquery('english', ?)
		`, searchQuery).
		Having("relevance >= ?", params.MinScore).
		Order("relevance DESC, created_at DESC").
		Offset(offset).
		Limit(params.Limit)

	if err := query.Scan(&results).Error; err != nil {
		return nil, fmt.Errorf("failed to search users: %w", err)
	}

	userResults := make([]UserSearchResult, len(results))
	for i, r := range results {
		// Exclude password hash
		r.User.PasswordHash = ""
		userResults[i] = UserSearchResult{
			User:      r.User,
			Relevance: r.Relevance,
		}
	}

	return userResults, nil
}

// searchPlaylists performs full-text search on playlists using PostgreSQL tsvector
func (s *FullTextSearchService) searchPlaylists(ctx context.Context, searchQuery string, params SearchParams) ([]PlaylistSearchResult, error) {
	offset := (params.Page - 1) * params.Limit

	var results []struct {
		models.Playlist
		Relevance float64
	}

	query := s.db.WithContext(ctx).
		Model(&models.Playlist{}).
		Select(`
			playlists.*,
			ts_rank_cd(
				setweight(to_tsvector('english', COALESCE(name, '')), 'A') ||
				setweight(to_tsvector('english', COALESCE(description, '')), 'B'),
				plainto_tsquery('english', ?)
			) as relevance
		`, searchQuery).
		Where("is_public = ? AND deleted_at IS NULL", true).
		Where(`
			to_tsvector('english', COALESCE(name, '')) ||
			to_tsvector('english', COALESCE(description, '')) @@ plainto_tsquery('english', ?)
		`, searchQuery).
		Having("relevance >= ?", params.MinScore).
		Order("relevance DESC, created_at DESC").
		Offset(offset).
		Limit(params.Limit)

	if err := query.Scan(&results).Error; err != nil {
		return nil, fmt.Errorf("failed to search playlists: %w", err)
	}

	playlistResults := make([]PlaylistSearchResult, len(results))
	for i, r := range results {
		playlistResults[i] = PlaylistSearchResult{
			Playlist:  r.Playlist,
			Relevance: r.Relevance,
		}
	}

	return playlistResults, nil
}

// SearchTracksOnly performs full-text search on tracks only
func (s *FullTextSearchService) SearchTracksOnly(ctx context.Context, query string, page, limit int) ([]TrackSearchResult, int64, error) {
	params := SearchParams{
		Query: query,
		Page:  page,
		Limit: limit,
		Types: []string{"track"},
	}

	tracks, err := s.searchTracks(ctx, s.prepareSearchQuery(query), params)
	if err != nil {
		return nil, 0, err
	}

	// Get total count
	var total int64
	countQuery := s.db.WithContext(ctx).
		Model(&models.Track{}).
		Where("is_public = ? AND deleted_at IS NULL", true).
		Where(`
			to_tsvector('english', COALESCE(title, '')) ||
			to_tsvector('english', COALESCE(artist, '')) @@ plainto_tsquery('english', ?)
		`, s.prepareSearchQuery(query))

	if err := countQuery.Count(&total).Error; err != nil {
		return nil, 0, fmt.Errorf("failed to count tracks: %w", err)
	}

	return tracks, total, nil
}

// SearchUsersOnly performs full-text search on users only
func (s *FullTextSearchService) SearchUsersOnly(ctx context.Context, query string, page, limit int) ([]UserSearchResult, int64, error) {
	params := SearchParams{
		Query: query,
		Page:  page,
		Limit: limit,
		Types: []string{"user"},
	}

	users, err := s.searchUsers(ctx, s.prepareSearchQuery(query), params)
	if err != nil {
		return nil, 0, err
	}

	// Get total count
	var total int64
	countQuery := s.db.WithContext(ctx).
		Model(&models.User{}).
		Where("deleted_at IS NULL").
		Where(`
			to_tsvector('english', COALESCE(username, '')) ||
			to_tsvector('english', COALESCE(first_name, '')) ||
			to_tsvector('english', COALESCE(last_name, '')) @@ plainto_tsquery('english', ?)
		`, s.prepareSearchQuery(query))

	if err := countQuery.Count(&total).Error; err != nil {
		return nil, 0, fmt.Errorf("failed to count users: %w", err)
	}

	return users, total, nil
}

// SearchPlaylistsOnly performs full-text search on playlists only
func (s *FullTextSearchService) SearchPlaylistsOnly(ctx context.Context, query string, page, limit int) ([]PlaylistSearchResult, int64, error) {
	params := SearchParams{
		Query: query,
		Page:  page,
		Limit: limit,
		Types: []string{"playlist"},
	}

	playlists, err := s.searchPlaylists(ctx, s.prepareSearchQuery(query), params)
	if err != nil {
		return nil, 0, err
	}

	// Get total count
	var total int64
	countQuery := s.db.WithContext(ctx).
		Model(&models.Playlist{}).
		Where("is_public = ? AND deleted_at IS NULL", true).
		Where(`
			to_tsvector('english', COALESCE(name, '')) ||
			to_tsvector('english', COALESCE(description, '')) @@ plainto_tsquery('english', ?)
		`, s.prepareSearchQuery(query))

	if err := countQuery.Count(&total).Error; err != nil {
		return nil, 0, fmt.Errorf("failed to count playlists: %w", err)
	}

	return playlists, total, nil
}

// containsString checks if a slice contains a string
func containsString(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}
