export async function fetchMyDecks(client, userId) {
  const { data, error } = await client
    .from('decks')
    .select('id, title, description, config, published, created_at, updated_at, copied_from_deck_id, version, copied_from_version')
    .eq('owner_id', userId)
    .order('updated_at', { ascending: false });
  if (error) throw error;
  const decks = data ?? [];
  if (decks.length === 0) return [];

  const deckIds = decks.map((d) => d.id);
  const sourceDeckIds = [...new Set(decks.map((d) => d.copied_from_deck_id).filter(Boolean))];
  const ratingDeckIds = [...new Set([...deckIds, ...sourceDeckIds])];

  const [questionCounts, ratingsRows, sourceTitlesRows] = await Promise.all([
    Promise.all(
      decks.map(async (d) => {
        const { count, error: e } = await client
          .from('questions')
          .select('*', { count: 'exact', head: true })
          .eq('deck_id', d.id);
        if (e) return 0;
        return count ?? 0;
      })
    ),
    ratingDeckIds.length > 0
      ? client.from('deck_ratings').select('deck_id, rating').in('deck_id', ratingDeckIds)
      : Promise.resolve({ data: [] }),
    sourceDeckIds.length > 0
      ? client.from('decks').select('id, title, version, updated_at').in('id', sourceDeckIds)
      : Promise.resolve({ data: [] }),
  ]);

  const ratingByDeck = new Map();
  for (const r of ratingsRows.data ?? []) {
    if (!ratingByDeck.has(r.deck_id)) ratingByDeck.set(r.deck_id, []);
    ratingByDeck.get(r.deck_id).push(r.rating);
  }
  const getRating = (deckId) => {
    const arr = ratingByDeck.get(deckId);
    if (!arr?.length) return { average_rating: 0, rating_count: 0 };
    const sum = arr.reduce((a, b) => a + b, 0);
    return { average_rating: Math.round((sum / arr.length) * 100) / 100, rating_count: arr.length };
  };

  const sourceById = new Map((sourceTitlesRows.data ?? []).map((d) => [d.id, d]));

  return decks.map((d, i) => {
    const showRatingDeckId = d.copied_from_deck_id || d.id;
    const rating = getRating(showRatingDeckId);
    const canRate = !!d.copied_from_deck_id;
    const sourceDeck = d.copied_from_deck_id ? sourceById.get(d.copied_from_deck_id) : null;
    const source_deck_title = sourceDeck?.title ?? 'Deck';
    const source_version = sourceDeck?.version ?? 1;
    const my_version = d.copied_from_version ?? 0;
    const versionOutdated = source_version > my_version;
    const sourceNewerByTime =
      sourceDeck?.updated_at &&
      d.updated_at &&
      new Date(sourceDeck.updated_at).getTime() > new Date(d.updated_at).getTime();
    const needs_update =
      !!d.copied_from_deck_id && (versionOutdated || sourceNewerByTime);
    return {
      ...d,
      question_count: questionCounts[i],
      ...rating,
      can_rate: canRate,
      rateable_deck_id: d.copied_from_deck_id || null,
      source_deck_title,
      source_version,
      needs_update,
    };
  });
}

/**
 * @param {string | null | undefined} [userId] - Current user id; when set, each deck gets user_has_this (true if user owns it or already has a copy).
 */
export async function fetchPublishedDecks(client, userId) {
  const { data, error } = await client
    .from('decks')
    .select('id, title, description, config, published, created_at, updated_at, owner_id')
    .eq('published', true)
    .order('updated_at', { ascending: false });
  if (error) throw error;
  const decks = data ?? [];
  if (decks.length === 0) return [];

  const deckIds = decks.map((d) => d.id);
  const ownerIds = [...new Set(decks.map((d) => d.owner_id).filter(Boolean))];

  const promises = [
    Promise.all(
      decks.map(async (d) => {
        const { count, error: e } = await client
          .from('questions')
          .select('*', { count: 'exact', head: true })
          .eq('deck_id', d.id);
        if (e) return 0;
        return count ?? 0;
      })
    ),
    ownerIds.length > 0
      ? client.from('profiles').select('id, display_name, email').in('id', ownerIds)
      : Promise.resolve({ data: [] }),
    client.from('deck_ratings').select('deck_id, rating').in('deck_id', deckIds),
  ];

  if (userId) {
    promises.push(
      client
        .from('decks')
        .select('copied_from_deck_id')
        .eq('owner_id', userId)
        .not('copied_from_deck_id', 'is', null)
    );
  }

  const results = await Promise.all(promises);
  const [questionCounts, profilesRows, ratingsRows, myDecksRows] = results;

  const myCopiedFromIds = new Set();
  if (userId && myDecksRows?.data) {
    for (const row of myDecksRows.data) {
      if (row.copied_from_deck_id) myCopiedFromIds.add(row.copied_from_deck_id);
    }
  }

  const profilesById = new Map(
    (profilesRows.data ?? []).map((p) => [p.id, { display_name: p.display_name, email: p.email }])
  );
  const ratingByDeck = new Map();
  for (const r of ratingsRows.data ?? []) {
    if (!ratingByDeck.has(r.deck_id)) ratingByDeck.set(r.deck_id, []);
    ratingByDeck.get(r.deck_id).push(r.rating);
  }
  const getRating = (deckId) => {
    const arr = ratingByDeck.get(deckId);
    if (!arr?.length) return { average_rating: 0, rating_count: 0 };
    const sum = arr.reduce((a, b) => a + b, 0);
    return { average_rating: Math.round((sum / arr.length) * 100) / 100, rating_count: arr.length };
  };

  return decks.map((d, i) => {
    const profile = profilesById.get(d.owner_id);
    const owner_email = profile?.email ?? profile?.display_name ?? 'User';
    const user_has_this =
      !!userId && (d.owner_id === userId || myCopiedFromIds.has(d.id));
    return {
      ...d,
      question_count: questionCounts[i],
      owner_display_name: profile?.display_name ?? 'User',
      owner_email,
      ...getRating(d.id),
      user_has_this,
    };
  });
}

/**
 * @param {string} ownerId - Owner of the decks to list.
 * @param {string | null | undefined} [viewerId] - Current user id; when set, each deck gets user_has_this (true if viewer owns it or already has a copy).
 */
export async function fetchPublishedDecksByOwner(client, ownerId, viewerId) {
  const { data: decks, error } = await client
    .from('decks')
    .select('id, title, description, config, published, created_at, updated_at, owner_id')
    .eq('published', true)
    .eq('owner_id', ownerId)
    .order('updated_at', { ascending: false });
  if (error) throw error;
  const deckList = decks ?? [];
  if (deckList.length === 0) {
    const { data: profile } = await client.from('profiles').select('id, display_name, email').eq('id', ownerId).single();
    return {
      decks: [],
      owner_email: profile?.email ?? profile?.display_name ?? 'User',
      owner_display_name: profile?.display_name ?? profile?.email ?? 'User',
    };
  }

  const deckIds = deckList.map((d) => d.id);
  const promises = [
    Promise.all(
      deckList.map(async (d) => {
        const { count, error: e } = await client
          .from('questions')
          .select('*', { count: 'exact', head: true })
          .eq('deck_id', d.id);
        if (e) return 0;
        return count ?? 0;
      })
    ),
    client.from('deck_ratings').select('deck_id, rating').in('deck_id', deckIds),
  ];

  if (viewerId && viewerId !== ownerId) {
    promises.push(
      client
        .from('decks')
        .select('copied_from_deck_id')
        .eq('owner_id', viewerId)
        .not('copied_from_deck_id', 'is', null)
    );
  }

  const results = await Promise.all(promises);
  const [questionCounts, ratingsRows, myDecksRows] = results;

  const myCopiedFromIds = new Set();
  if (viewerId && viewerId !== ownerId && myDecksRows?.data) {
    for (const row of myDecksRows.data) {
      if (row.copied_from_deck_id) myCopiedFromIds.add(row.copied_from_deck_id);
    }
  }

  const ratingByDeck = new Map();
  for (const r of ratingsRows.data ?? []) {
    if (!ratingByDeck.has(r.deck_id)) ratingByDeck.set(r.deck_id, []);
    ratingByDeck.get(r.deck_id).push(r.rating);
  }
  const getRating = (deckId) => {
    const arr = ratingByDeck.get(deckId);
    if (!arr?.length) return { average_rating: 0, rating_count: 0 };
    const sum = arr.reduce((a, b) => a + b, 0);
    return { average_rating: Math.round((sum / arr.length) * 100) / 100, rating_count: arr.length };
  };

  const { data: profile } = await client.from('profiles').select('id, display_name, email').eq('id', ownerId).single();
  const owner_email = profile?.email ?? profile?.display_name ?? 'User';
  const owner_display_name = profile?.display_name ?? profile?.email ?? 'User';

  const decksWithMeta = deckList.map((d, i) => {
    const user_has_this =
      !!viewerId &&
      (viewerId === ownerId || myCopiedFromIds.has(d.id));
    return {
      ...d,
      question_count: questionCounts[i],
      owner_email,
      owner_display_name,
      ...getRating(d.id),
      user_has_this,
    };
  });

  return { decks: decksWithMeta, owner_email, owner_display_name };
}

export async function fetchDeckWithQuestions(client, deckId) {
  const { data: deck, error: deckError } = await client
    .from('decks')
    .select('*')
    .eq('id', deckId)
    .single();
  if (deckError || !deck) throw deckError || new Error('Deck not found');
  const { data: questions, error: qError } = await client
    .from('questions')
    .select('*')
    .eq('deck_id', deckId)
    .order('sort_order', { ascending: true });
  if (qError) throw qError;
  return { ...deck, questions: questions ?? [] };
}

/** Copy a published deck (and its questions) into the current user's account. New deck is unpublished. */
export async function copyDeckToUser(client, deckId, userId) {
  const source = await fetchDeckWithQuestions(client, deckId);
  if (!source.published) throw new Error('Deck is not available to copy');
  const questions = (source.questions ?? []).map((q) => ({
    prompt: q.prompt ?? '',
    explanation: q.explanation ?? '',
    answers: Array.isArray(q.answers) ? q.answers : [],
    correct_answer_indices: Array.isArray(q.correct_answer_indices) ? q.correct_answer_indices : [],
  }));
  return createDeck(client, userId, {
    title: source.title,
    description: source.description ?? '',
    config: source.config ?? {},
    questions,
    copiedFromDeckId: deckId,
    copiedFromVersion: source.version ?? 1,
  });
}

export async function createDeck(client, ownerId, { title, description, config, questions, copiedFromDeckId, copiedFromVersion }) {
  const row = {
    owner_id: ownerId,
    title: title.trim(),
    description: (description ?? '').trim(),
    config: config ?? {},
  };
  if (copiedFromDeckId != null) row.copied_from_deck_id = copiedFromDeckId;
  if (copiedFromVersion != null) row.copied_from_version = copiedFromVersion;
  const { data: deck, error: deckError } = await client
    .from('decks')
    .insert(row)
    .select('id')
    .single();
  if (deckError || !deck) throw deckError || new Error('Failed to create deck');
  if (questions && questions.length > 0) {
    const rows = questions.map((q, i) => ({
      deck_id: deck.id,
      sort_order: i,
      prompt: (q.prompt ?? '').trim(),
      explanation: (q.explanation ?? '').trim() || null,
      answers: Array.isArray(q.answers) ? q.answers : [],
      correct_answer_indices: Array.isArray(q.correct_answer_indices) ? q.correct_answer_indices : [],
    }));
    const { error: qError } = await client.from('questions').insert(rows);
    if (qError) throw qError;
  }
  return deck.id;
}

export async function updateDeck(client, deckId, { title, description, config, questions }) {
  const { data: current, error: fetchErr } = await client
    .from('decks')
    .select('version, published')
    .eq('id', deckId)
    .single();
  if (fetchErr) throw fetchErr;
  const bumpVersion = current?.published === true;
  const nextVersion = bumpVersion ? (current?.version ?? 1) + 1 : (current?.version ?? 1);
  const updatePayload = {
    title: title.trim(),
    description: (description ?? '').trim(),
    config: config ?? {},
    version: nextVersion,
  };
  const { error: deckError } = await client
    .from('decks')
    .update(updatePayload)
    .eq('id', deckId);
  if (deckError) throw deckError;
  const { error: delError } = await client.from('questions').delete().eq('deck_id', deckId);
  if (delError) throw delError;
  if (questions && questions.length > 0) {
    const rows = questions.map((q, i) => ({
      deck_id: deckId,
      sort_order: i,
      prompt: (q.prompt ?? '').trim(),
      explanation: (q.explanation ?? '').trim() || null,
      answers: Array.isArray(q.answers) ? q.answers : [],
      correct_answer_indices: Array.isArray(q.correct_answer_indices) ? q.correct_answer_indices : [],
    }));
    const { error: qError } = await client.from('questions').insert(rows);
    if (qError) throw qError;
  }
}

export async function togglePublished(client, deckId, published) {
  const { error } = await client.from('decks').update({ published }).eq('id', deckId);
  if (error) throw error;
}

export async function deleteDeck(client, deckId) {
  const { error } = await client.from('decks').delete().eq('id', deckId);
  if (error) throw error;
}

/**
 * Get source deck and copy deck with questions for the "Update from community" preview.
 * Copy must be owned by userId and have copied_from_deck_id.
 * @returns {{ source: object, copy: object, changes: string[] }}
 */
export async function getSourceUpdatePreview(client, copyDeckId, userId) {
  const { data: copy, error: copyErr } = await client
    .from('decks')
    .select('*')
    .eq('id', copyDeckId)
    .eq('owner_id', userId)
    .single();
  if (copyErr || !copy?.copied_from_deck_id) throw new Error('Deck not found or not a copy');
  const sourceId = copy.copied_from_deck_id;
  const [source, copyQuestionsRows] = await Promise.all([
    fetchDeckWithQuestions(client, sourceId),
    client.from('questions').select('*').eq('deck_id', copyDeckId).order('sort_order', { ascending: true }),
  ]);
  if (!source.published) throw new Error('Source deck is no longer available');
  const copyQuestions = copyQuestionsRows.data ?? [];
  const copyWithQuestions = { ...copy, questions: copyQuestions };

  const changes = [];
  if ((source.title ?? '').trim() !== (copy.title ?? '').trim()) {
    changes.push(`Title: "${(copy.title ?? '').trim()}" → "${(source.title ?? '').trim()}"`);
  }
  if ((source.description ?? '').trim() !== (copy.description ?? '').trim()) {
    changes.push('Description updated');
  }
  const srcLen = (source.questions ?? []).length;
  const copyLen = copyQuestions.length;
  if (srcLen !== copyLen) {
    changes.push(`Questions: ${copyLen} → ${srcLen}`);
  } else {
    const anyDifferent = (source.questions ?? []).some((sq, i) => {
      const cq = copyQuestions[i];
      if (!cq) return true;
      return (sq.prompt ?? '') !== (cq.prompt ?? '') || (sq.explanation ?? '') !== (cq.explanation ?? '');
    });
    if (anyDifferent) changes.push('Some question content updated');
  }
  if (changes.length === 0) changes.push('Content is in sync (version metadata will update)');

  return { source, copy: copyWithQuestions, changes };
}

/**
 * Update a community copy to match the current source deck (title, description, config, questions, copied_from_version).
 */
export async function applySourceUpdate(client, copyDeckId, userId) {
  const { data: copy, error: copyErr } = await client
    .from('decks')
    .select('id, copied_from_deck_id')
    .eq('id', copyDeckId)
    .eq('owner_id', userId)
    .single();
  if (copyErr || !copy?.copied_from_deck_id) throw new Error('Deck not found or not a copy');
  const source = await fetchDeckWithQuestions(client, copy.copied_from_deck_id);
  if (!source.published) throw new Error('Source deck is no longer available');

  const { error: deckError } = await client
    .from('decks')
    .update({
      title: source.title ?? '',
      description: source.description ?? '',
      config: source.config ?? {},
      copied_from_version: source.version ?? 1,
    })
    .eq('id', copyDeckId);
  if (deckError) throw deckError;

  const { error: delError } = await client.from('questions').delete().eq('deck_id', copyDeckId);
  if (delError) throw delError;
  const questions = source.questions ?? [];
  if (questions.length > 0) {
    const rows = questions.map((q, i) => ({
      deck_id: copyDeckId,
      sort_order: i,
      prompt: (q.prompt ?? '').trim(),
      explanation: (q.explanation ?? '').trim() || null,
      answers: Array.isArray(q.answers) ? q.answers : [],
      correct_answer_indices: Array.isArray(q.correct_answer_indices) ? q.correct_answer_indices : [],
    }));
    const { error: qError } = await client.from('questions').insert(rows);
    if (qError) throw qError;
  }
}

/** True if the user already has this deck (owns it or has added a copy). */
export async function userHasDeck(client, deckId, userId) {
  if (!userId) return false;
  const { data, error } = await client
    .from('decks')
    .select('id')
    .eq('owner_id', userId)
    .or(`id.eq.${deckId},copied_from_deck_id.eq.${deckId}`)
    .limit(1);
  if (error) throw error;
  return (data?.length ?? 0) > 0;
}

/** Get all reviews (ratings + comments) for a deck with reviewer display names. */
export async function getDeckReviews(client, deckId) {
  const { data: ratings, error: rError } = await client
    .from('deck_ratings')
    .select('user_id, rating, comment, created_at')
    .eq('deck_id', deckId)
    .order('created_at', { ascending: false });
  if (rError) throw rError;
  const list = ratings ?? [];
  if (list.length === 0) return [];
  const userIds = [...new Set(list.map((r) => r.user_id).filter(Boolean))];
  const { data: profiles } = await client
    .from('profiles')
    .select('id, display_name, email, avatar_url')
    .in('id', userIds);
  const byId = new Map((profiles ?? []).map((p) => [p.id, p]));
  return list.map((r) => {
    const p = byId.get(r.user_id);
    return {
      rating: r.rating,
      comment: r.comment || null,
      user_id: r.user_id,
      display_name: p?.display_name ?? null,
      email: p?.email ?? null,
      avatar_url: p?.avatar_url ?? null,
    };
  });
}

/** Get the current user's rating (and comment) for a deck, or null if none. */
export async function getMyDeckRating(client, deckId, userId) {
  if (!userId) return null;
  const { data, error } = await client
    .from('deck_ratings')
    .select('rating, comment')
    .eq('deck_id', deckId)
    .eq('user_id', userId)
    .maybeSingle();
  if (error) throw error;
  return data;
}

/** Submit or update rating (and optional comment) for a deck. Upserts by (deck_id, user_id). */
export async function submitDeckRating(client, deckId, userId, { rating, comment }) {
  const row = {
    deck_id: deckId,
    user_id: userId,
    rating: Math.min(5, Math.max(1, Math.round(rating))),
    comment: (comment ?? '').trim() || null,
  };
  const { error } = await client.from('deck_ratings').upsert(row, {
    onConflict: 'deck_id,user_id',
  });
  if (error) throw error;
}
