import { AtpBaseClient } from '../client/index' import type { UserPreferencesForScoring } from './reviewUtils' import { cache, TTL } from '../services/cache' import { DEFAULT_PREFERENCES } from '../data/preferenceDefinitions' /** * Fetches another user's preference settings from their PDS. * Returns null if the fetch fails (private settings, not found, etc.) * Falls back to cache if available. */ export async function fetchUserPreferences( did: string, pdsUrl: string ): Promise { const cacheKey = `preferences:${did}` // Check cache first const cached = cache.get(cacheKey) if (cached) { return cached } try { // Create unauthenticated PDS client for public reads const pdsClient = new AtpBaseClient({ service: pdsUrl }) // Fetch the settings record (rkey is always 'self' for user settings) const response = await pdsClient.social.drydown.settings.get({ repo: did, rkey: 'self', }) if (!response.value) { return null } const settings = response.value const preferences: UserPreferencesForScoring = { presenceStyle: settings.presenceStyle, longevityPriority: settings.longevityPriority, complexityPreference: settings.complexityPreference, scoringApproach: settings.scoringApproach, } // Cache the result cache.set(cacheKey, preferences, TTL.SETTINGS) return preferences } catch (error) { // Settings not found, private, or network error - return null console.debug('Could not fetch user preferences:', error) return null } } /** * Calculates compatibility score between two users' preferences. * Returns a percentage from 0-100 indicating how aligned their preferences are. * * Algorithm: * 1. Calculate absolute distance for each of 4 preference values (each on 1-5 scale) * 2. Average the distances (max possible avg = 4) * 3. Convert to percentage: ((4 - avgDistance) / 4) * 100 * * Examples: * - Identical preferences: avgDistance = 0 → 100% * - Opposite preferences: avgDistance = 4 → 0% * - Mixed preferences: avgDistance = 0.75 → 81% */ export function calculateCompatibilityScore( userPrefs: UserPreferencesForScoring, otherPrefs: UserPreferencesForScoring ): number { // Use DEFAULT_PREFERENCES if any preference is undefined const distances = [ Math.abs((userPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle) - (otherPrefs.presenceStyle ?? DEFAULT_PREFERENCES.presenceStyle)), Math.abs((userPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority) - (otherPrefs.longevityPriority ?? DEFAULT_PREFERENCES.longevityPriority)), Math.abs((userPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference) - (otherPrefs.complexityPreference ?? DEFAULT_PREFERENCES.complexityPreference)), Math.abs((userPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach) - (otherPrefs.scoringApproach ?? DEFAULT_PREFERENCES.scoringApproach)), ] const avgDistance = distances.reduce((sum, d) => sum + d, 0) / 4 const compatibility = ((4 - avgDistance) / 4) * 100 return Math.round(compatibility) } /** * Returns a friendly label describing the compatibility level. */ export function getCompatibilityLabel(score: number): string { if (score >= 90) return 'Highly Similar' if (score >= 75) return 'Similar Tastes' if (score >= 50) return 'Somewhat Different' if (score >= 25) return 'Different Tastes' return 'Very Different' }