/**
 * Warms the top-N conversations' recent image attachments (and video
 * thumbnails) into ExpoImage's memory+disk cache so tapping a chat
 * feels instant. Per-asset URL extraction + the actual `ExpoImage.prefetch`
 * call live in `prefetchMessageImages.ts` and are shared with the
 * single-message socket-receive prefetch — this module owns the
 * conversation ranking, round-robin, and budget logic; the rest is
 * delegated.
 */

import debounce from 'lodash.debounce';
import {Asset, ChatNotificationType} from '@modules/chats/interfaces';
import {useGroups} from '@modules/chats/store/zustand/useGroups';
import {
  getChatAvatar,
  getChatImage,
  getConversationAvatarPrefetchWidths,
  getPrefetchUrlsForAsset,
  prefetchImageUrls,
} from '@modules/chats/utils';
import {unreadsStore} from '@modules/chats/store/zustand/unreads';
import {persistedUserId} from '@services/localStorage';

const TOP_N_CONVERSATIONS = 5;
const MESSAGES_PER_CONVERSATION = 20;
const MAX_PREFETCH_PER_RUN = 50;
const DEBOUNCE_MS = 1_000;

type RankedMessage = {
  messageId: string;
  attachments: Asset[];
};
type RankedConversation = {
  channelId: string;
  messages: RankedMessage[];
};

const notMuted = (nt?: ChatNotificationType): boolean =>
  nt === undefined || nt === ChatNotificationType.All;

// Rank by pinned → unread → unmuted → recency.
const rankConversations = (): RankedConversation[] => {
  const groups = useGroups.getState().groups;
  const unreads = unreadsStore.getState().unreads;
  const channels = Object.keys(groups);

  const scored = channels
    .map(channelId => {
      const g = groups[channelId];
      if (!g?.groupInfo) return null;
      const {properties, latest} = g.groupInfo;
      const pinnedAt = properties?.pinned_at ?? 0;
      const isUnread = (unreads[channelId] ?? 0) > 0;
      const isNotMuted = notMuted(
        properties?.notification_status?.notification_type,
      );
      // ULID-style timestamps: lexicographic order == chronological.
      const latestTs = latest?.ts ?? '';
      return {channelId, g, pinnedAt, isUnread, isNotMuted, latestTs};
    })
    .filter((x): x is NonNullable<typeof x> => x !== null);

  scored.sort((a, b) => {
    if (a.pinnedAt !== b.pinnedAt) return (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0);
    if (a.isUnread !== b.isUnread) return a.isUnread ? -1 : 1;
    if (a.isNotMuted !== b.isNotMuted) return a.isNotMuted ? -1 : 1;
    return b.latestTs.localeCompare(a.latestTs);
  });

  const top = scored.slice(0, TOP_N_CONVERSATIONS);
  return top.map(({channelId, g}) => {
    const msgs = g.messagesWindow ?? [];
    const picked: RankedMessage[] = [];
    for (let i = msgs.length - 1; i >= 0; i--) {
      const m = msgs[i];
      if (!m.attachments?.length) continue;
      picked.push({
        messageId: m.client_msg_id,
        attachments: m.attachments,
      });
      if (picked.length >= MESSAGES_PER_CONVERSATION) break;
    }
    return {channelId, messages: picked};
  });
};

const runPrefetch = () => {
  const ranked = rankConversations();
  if (ranked.length === 0) return;

  // We ONLY prefetch image bytes — image attachments and video poster
  // thumbnails. Video files themselves are never prefetched.
  //
  // Build a per-conversation queue of (msg, asset, isBigSingle) tuples
  // in newest-first order, then ROUND-ROBIN across conversations so the
  // global budget is split fairly. Without this, a single gallery
  // message in the top-ranked conversation can hog all slots and the
  // user's lower-ranked chats get zero prefetched URLs.
  //
  // `attachmentCount` lets the prefetch loop ask the shared sizing
  // helper exactly the same widths the renderer will request.
  type Tuple = {asset: Asset; attachmentCount: number};
  const perConv: Tuple[][] = ranked.map(conv => {
    const tuples: Tuple[] = [];
    for (const msg of conv.messages) {
      const attachmentCount = msg.attachments.length;
      for (const asset of msg.attachments) {
        tuples.push({asset, attachmentCount});
      }
    }
    return tuples;
  });
  const maxLen = Math.max(0, ...perConv.map(t => t.length));

  const urlsToPrefetch: string[] = [];

  const pushIf = (url: string): boolean => {
    if (urlsToPrefetch.length >= MAX_PREFETCH_PER_RUN) return false;
    urlsToPrefetch.push(url);
    return true;
  };

  // Avatars first — tiny, identity-carrying, and displayed in the chat
  // list AND chat header. Widths come from the shared helper, which is
  // the SAME function the renderers use, so changing the avatar size in
  // one place propagates here automatically.
  const userId = persistedUserId ?? undefined;
  const avatarWidths = getConversationAvatarPrefetchWidths();
  for (const conv of ranked) {
    const g = useGroups.getState().groups[conv.channelId];
    const baseAvatar = g?.groupInfo
      ? getChatAvatar(g.groupInfo, userId).url
      : undefined;
    if (!baseAvatar) continue;
    for (const px of avatarWidths) {
      if (!pushIf(getChatImage.addSize(baseAvatar, px))) break;
    }
  }

  // Round-robin across conversations, asking `getPrefetchUrlsForAsset`
  // for the sized URLs each asset needs. The helper is the SAME one
  // the single-message socket-prefetch uses, so changing tile sizing
  // in one place propagates to both prefetch paths AND the renderer.
  let budgetExhausted = false;
  for (let i = 0; i < maxLen && !budgetExhausted; i++) {
    for (let c = 0; c < perConv.length && !budgetExhausted; c++) {
      const tuple = perConv[c][i];
      if (!tuple) continue;
      if (urlsToPrefetch.length >= MAX_PREFETCH_PER_RUN) {
        budgetExhausted = true;
        break;
      }
      const assetUrls = getPrefetchUrlsForAsset(
        tuple.asset,
        tuple.attachmentCount,
      );
      for (const url of assetUrls) {
        if (!pushIf(url)) {
          budgetExhausted = true;
          break;
        }
      }
    }
  }

  if (urlsToPrefetch.length > 0) {
    void prefetchImageUrls(urlsToPrefetch);
  }
};

const debouncedRun = debounce(runPrefetch, DEBOUNCE_MS, {
  leading: false,
  trailing: true,
});

let firstScheduleDone = false;

export const ConversationMediaPrefetcher = {
  // First call runs immediately; subsequent calls within DEBOUNCE_MS trail.
  schedule(): void {
    if (!firstScheduleDone) {
      firstScheduleDone = true;
      runPrefetch();
      return;
    }
    debouncedRun();
  },

  dispose(): void {
    debouncedRun.cancel();
    firstScheduleDone = false;
  },
};
