package postgres

import (
	"context"
	"fmt"
	"strings"

	"chat-switchboard/models"
)

// RecallHybrid returns active memories using vector similarity + keyword search.
// If queryVec is nil/empty, falls back to keyword-only Recall.
func (s *MemoryStore) RecallHybrid(ctx context.Context, userID string, personaID *string, query string, queryVec []float64, limit int) ([]models.Memory, error) {
	if len(queryVec) == 0 {
		return s.Recall(ctx, userID, personaID, query, limit)
	}
	if limit <= 0 || limit > 100 {
		limit = 30
	}

	// Run semantic recall
	semantic, err := s.recallSemantic(ctx, userID, personaID, queryVec, limit)
	if err != nil {
		// Fall back to keyword
		return s.Recall(ctx, userID, personaID, query, limit)
	}

	// Run keyword recall if query provided
	var keyword []models.Memory
	if query != "" {
		keyword, _ = s.Recall(ctx, userID, personaID, query, limit)
	}

	return mergeResults(semantic, keyword, limit), nil
}

// recallSemantic uses pgvector cosine distance to find similar memories.
func (s *MemoryStore) recallSemantic(ctx context.Context, userID string, personaID *string, queryVec []float64, limit int) ([]models.Memory, error) {
	vecStr := vectorToString(queryVec)

	parts := []string{}
	args := []interface{}{}
	idx := 1

	// User-scope
	userPart := fmt.Sprintf(`
		SELECT id, scope, owner_id, user_id, key, value,
		       source_channel_id, confidence, status, created_at, updated_at,
		       (embedding <=> $%d::vector) AS distance
		FROM memories
		WHERE scope = 'user' AND owner_id = $%d AND status = 'active'
		  AND embedding IS NOT NULL
	`, idx, idx+1)
	args = append(args, vecStr, userID)
	idx += 2
	parts = append(parts, userPart)

	if personaID != nil && *personaID != "" {
		// Persona-scope
		personaPart := fmt.Sprintf(`
			SELECT id, scope, owner_id, user_id, key, value,
			       source_channel_id, confidence, status, created_at, updated_at,
			       (embedding <=> $%d::vector) AS distance
			FROM memories
			WHERE scope = 'persona' AND owner_id = $%d AND status = 'active'
			  AND embedding IS NOT NULL
		`, idx, idx+1)
		args = append(args, vecStr, *personaID)
		idx += 2
		parts = append(parts, personaPart)

		// Persona+user scope
		puPart := fmt.Sprintf(`
			SELECT id, scope, owner_id, user_id, key, value,
			       source_channel_id, confidence, status, created_at, updated_at,
			       (embedding <=> $%d::vector) AS distance
			FROM memories
			WHERE scope = 'persona_user' AND owner_id = $%d AND user_id = $%d AND status = 'active'
			  AND embedding IS NOT NULL
		`, idx, idx+1, idx+2)
		args = append(args, vecStr, *personaID, userID)
		idx += 3
		parts = append(parts, puPart)
	}

	args = append(args, limit)
	fullQuery := fmt.Sprintf(`
		SELECT id, scope, owner_id, user_id, key, value,
		       source_channel_id, confidence, status, created_at, updated_at
		FROM (%s) AS combined
		WHERE distance < 0.7
		ORDER BY distance ASC, confidence DESC
		LIMIT $%d
	`, strings.Join(parts, " UNION ALL "), idx)

	rows, err := DB.QueryContext(ctx, fullQuery, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	return scanMemories(rows)
}

// mergeResults deduplicates and merges semantic + keyword results.
func mergeResults(semantic, keyword []models.Memory, limit int) []models.Memory {
	seen := make(map[string]bool, len(semantic))
	result := make([]models.Memory, 0, limit)

	// Semantic results first (higher relevance)
	for _, m := range semantic {
		if len(result) >= limit {
			break
		}
		seen[m.ID] = true
		result = append(result, m)
	}

	// Then keyword results not already included
	for _, m := range keyword {
		if len(result) >= limit {
			break
		}
		if !seen[m.ID] {
			seen[m.ID] = true
			result = append(result, m)
		}
	}

	return result
}
