import { prisma } from '../../config/database';
import { redis } from '../../config/redis';
import { friendshipService } from './friendship.service';

interface Suggestion {
  userId: string;
  name: string | null;
  email: string;
  role: string;
  reason: string;
  score: number;
}

const SUGGESTIONS_CACHE_TTL = 300; // 5 minutes
const DISMISS_KEY_PREFIX = 'social:dismiss:';

export const suggestionsService = {
  /**
   * Get ranked friend suggestions for a user.
   * Sources: household members, shared shifts, shared campaigns, mutual friends.
   */
  async getSuggestions(userId: string, limit: number = 10): Promise<Suggestion[]> {
    const cacheKey = `social:suggestions:${userId}`;

    // Check cache
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Get IDs to exclude: self, existing friends, pending, blocked
    const [friendIds, pendingIds, blockedIds, dismissedIds] = await Promise.all([
      friendshipService.getFriendIds(userId),
      this.getPendingIds(userId),
      this.getBlockedIds(userId),
      this.getDismissedIds(userId),
    ]);

    const excludeSet = new Set([userId, ...friendIds, ...pendingIds, ...blockedIds, ...dismissedIds]);

    // Gather suggestions from multiple sources in parallel
    const [householdSuggestions, shiftSuggestions, campaignSuggestions, mutualSuggestions] = await Promise.all([
      this.getHouseholdSuggestions(userId, excludeSet),
      this.getSharedShiftSuggestions(userId, excludeSet),
      this.getSharedCampaignSuggestions(userId, excludeSet),
      this.getMutualFriendSuggestions(userId, friendIds, excludeSet),
    ]);

    // Merge by userId, summing scores and keeping highest-priority reason
    const scoreMap = new Map<string, Suggestion>();

    const addSuggestions = (items: Suggestion[]) => {
      for (const item of items) {
        const existing = scoreMap.get(item.userId);
        if (existing) {
          existing.score += item.score;
          // Keep the reason with higher base score
          if (item.score > existing.score - item.score) {
            existing.reason = item.reason;
          }
        } else {
          scoreMap.set(item.userId, { ...item });
        }
      }
    };

    addSuggestions(householdSuggestions);
    addSuggestions(mutualSuggestions);
    addSuggestions(shiftSuggestions);
    addSuggestions(campaignSuggestions);

    // Check privacy: filter out users who disabled friend requests
    const userIds = Array.from(scoreMap.keys());
    if (userIds.length > 0) {
      const privacyBlocked = await prisma.privacySettings.findMany({
        where: { userId: { in: userIds }, allowFriendRequests: false },
        select: { userId: true },
      });
      for (const p of privacyBlocked) {
        scoreMap.delete(p.userId);
      }
    }

    // Sort by score descending and take limit
    const suggestions = Array.from(scoreMap.values())
      .sort((a, b) => b.score - a.score)
      .slice(0, limit);

    // Cache for 5 minutes
    await redis.setex(cacheKey, SUGGESTIONS_CACHE_TTL, JSON.stringify(suggestions));

    return suggestions;
  },

  /** Dismiss a suggestion (stored in Redis set, expires in 30 days) */
  async dismissSuggestion(userId: string, suggestedUserId: string) {
    const key = `${DISMISS_KEY_PREFIX}${userId}`;
    await redis.sadd(key, suggestedUserId);
    await redis.expire(key, 30 * 24 * 60 * 60); // 30 days

    // Invalidate suggestions cache
    await redis.del(`social:suggestions:${userId}`);

    return { success: true };
  },

  // --- Source-specific suggestion generators ---

  /** Household members from CRM ContactConnections */
  async getHouseholdSuggestions(userId: string, excludeSet: Set<string>): Promise<Suggestion[]> {
    // Find the Contact linked to this user
    const contact = await prisma.contact.findFirst({
      where: { userId },
      select: { id: true },
    });
    if (!contact) return [];

    // Find contacts connected via HOUSEHOLD or FAMILY
    const connections = await prisma.contactConnection.findMany({
      where: {
        OR: [
          { fromContactId: contact.id, type: { in: ['HOUSEHOLD', 'FAMILY'] } },
          { toContactId: contact.id, type: { in: ['HOUSEHOLD', 'FAMILY'] } },
        ],
      },
      include: {
        fromContact: { select: { userId: true, displayName: true } },
        toContact: { select: { userId: true, displayName: true } },
      },
    });

    const suggestions: Suggestion[] = [];
    for (const conn of connections) {
      const otherContact = conn.fromContactId === contact.id ? conn.toContact : conn.fromContact;
      if (!otherContact.userId || excludeSet.has(otherContact.userId)) continue;

      const user = await prisma.user.findUnique({
        where: { id: otherContact.userId },
        select: { id: true, name: true, email: true, role: true },
      });
      if (!user) continue;

      suggestions.push({
        userId: user.id,
        name: user.name,
        email: user.email,
        role: user.role,
        reason: conn.type === 'HOUSEHOLD' ? 'Household member' : 'Family member',
        score: 100, // Highest priority
      });
    }

    return suggestions;
  },

  /** Users who signed up for the same shifts */
  async getSharedShiftSuggestions(userId: string, excludeSet: Set<string>): Promise<Suggestion[]> {
    // Get shift IDs this user signed up for (last 90 days)
    const since = new Date();
    since.setDate(since.getDate() - 90);

    const mySignups = await prisma.shiftSignup.findMany({
      where: { userId, signupDate: { gte: since } },
      select: { shiftId: true },
    });

    if (mySignups.length === 0) return [];
    const myShiftIds = mySignups.map((s) => s.shiftId);

    // Find other users who also signed up for those shifts
    const otherSignups = await prisma.shiftSignup.findMany({
      where: {
        shiftId: { in: myShiftIds },
        userId: { not: null },
      },
      select: { userId: true },
    });

    // Count co-occurrences
    const countMap = new Map<string, number>();
    for (const s of otherSignups) {
      if (!s.userId || excludeSet.has(s.userId)) continue;
      countMap.set(s.userId, (countMap.get(s.userId) || 0) + 1);
    }

    // Get user details for top suggestions
    const topUserIds = Array.from(countMap.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([id]) => id);

    if (topUserIds.length === 0) return [];

    const users = await prisma.user.findMany({
      where: { id: { in: topUserIds } },
      select: { id: true, name: true, email: true, role: true },
    });

    return users.map((u) => ({
      userId: u.id,
      name: u.name,
      email: u.email,
      role: u.role,
      reason: `${countMap.get(u.id)} shared shift${(countMap.get(u.id) || 0) > 1 ? 's' : ''}`,
      score: 30 + (countMap.get(u.id) || 0) * 5, // Base 30, +5 per shared shift
    }));
  },

  /** Users who participated in the same campaigns */
  async getSharedCampaignSuggestions(userId: string, excludeSet: Set<string>): Promise<Suggestion[]> {
    const since = new Date();
    since.setDate(since.getDate() - 90);

    const myEmails = await prisma.campaignEmail.findMany({
      where: { userId, sentAt: { gte: since } },
      select: { campaignId: true },
      distinct: ['campaignId'],
    });

    if (myEmails.length === 0) return [];
    const myCampaignIds = myEmails.map((e) => e.campaignId);

    const otherEmails = await prisma.campaignEmail.findMany({
      where: {
        campaignId: { in: myCampaignIds },
        userId: { not: null },
      },
      select: { userId: true },
      distinct: ['userId'],
    });

    const countMap = new Map<string, number>();
    for (const e of otherEmails) {
      if (!e.userId || excludeSet.has(e.userId)) continue;
      countMap.set(e.userId, (countMap.get(e.userId) || 0) + 1);
    }

    const topUserIds = Array.from(countMap.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([id]) => id);

    if (topUserIds.length === 0) return [];

    const users = await prisma.user.findMany({
      where: { id: { in: topUserIds } },
      select: { id: true, name: true, email: true, role: true },
    });

    return users.map((u) => ({
      userId: u.id,
      name: u.name,
      email: u.email,
      role: u.role,
      reason: `${countMap.get(u.id)} shared campaign${(countMap.get(u.id) || 0) > 1 ? 's' : ''}`,
      score: 20 + (countMap.get(u.id) || 0) * 5,
    }));
  },

  /** Users with the most mutual friends */
  async getMutualFriendSuggestions(userId: string, friendIds: string[], excludeSet: Set<string>): Promise<Suggestion[]> {
    if (friendIds.length === 0) return [];

    // For each friend, get their friends
    const friendOfFriendCounts = new Map<string, number>();

    // Get all friendships of our friends in one query
    const friendFriendships = await prisma.friendship.findMany({
      where: {
        OR: [
          { userId: { in: friendIds }, status: 'accepted' },
          { friendId: { in: friendIds }, status: 'accepted' },
        ],
      },
      select: { userId: true, friendId: true },
    });

    const friendSet = new Set(friendIds);
    for (const f of friendFriendships) {
      // The "other" person for each friendship
      const otherId = friendSet.has(f.userId) ? f.friendId : f.userId;
      if (excludeSet.has(otherId) || friendSet.has(otherId)) continue;
      friendOfFriendCounts.set(otherId, (friendOfFriendCounts.get(otherId) || 0) + 1);
    }

    const topUserIds = Array.from(friendOfFriendCounts.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([id]) => id);

    if (topUserIds.length === 0) return [];

    const users = await prisma.user.findMany({
      where: { id: { in: topUserIds } },
      select: { id: true, name: true, email: true, role: true },
    });

    return users.map((u) => ({
      userId: u.id,
      name: u.name,
      email: u.email,
      role: u.role,
      reason: `${friendOfFriendCounts.get(u.id)} mutual friend${(friendOfFriendCounts.get(u.id) || 0) > 1 ? 's' : ''}`,
      score: 50 + (friendOfFriendCounts.get(u.id) || 0) * 10,
    }));
  },

  // --- Helper methods ---

  async getPendingIds(userId: string): Promise<string[]> {
    const pending = await prisma.friendship.findMany({
      where: {
        OR: [
          { userId, status: 'pending' },
          { friendId: userId, status: 'pending' },
        ],
      },
      select: { userId: true, friendId: true },
    });
    return pending.map((p) => (p.userId === userId ? p.friendId : p.userId));
  },

  async getBlockedIds(userId: string): Promise<string[]> {
    const blocks = await prisma.userBlock.findMany({
      where: {
        OR: [
          { userId },
          { blockedUserId: userId },
        ],
      },
      select: { userId: true, blockedUserId: true },
    });
    const ids = new Set<string>();
    for (const b of blocks) {
      if (b.userId !== userId) ids.add(b.userId);
      if (b.blockedUserId !== userId) ids.add(b.blockedUserId);
    }
    return Array.from(ids);
  },

  async getDismissedIds(userId: string): Promise<string[]> {
    const key = `${DISMISS_KEY_PREFIX}${userId}`;
    return redis.smembers(key);
  },
};
