// Avatar loader on top of the host's MSC4039 download_file. Module-level
// cache (mxc → objectURL promise) so a contact list re-render or tab switch
// never re-downloads, plus a small concurrency gate so opening a 200-contact
// list doesn't fire 200 parallel postMessage round-trips at once.
//
// Object URLs are kept for the iframe's lifetime — the widget document dies
// with the bot page, and the blobs are 96px thumbnails, so there's nothing
// worth revoking eagerly.

import { useEffect, useState } from 'preact/hooks';
import type { WidgetApi } from './widget-api';

const cache = new Map<string, Promise<string | null>>();

const MAX_CONCURRENT_DOWNLOADS = 4;
let active = 0;
const waiters: Array<() => void> = [];

const acquireSlot = async (): Promise<void> => {
  if (active >= MAX_CONCURRENT_DOWNLOADS) {
    await new Promise<void>((resolve) => {
      waiters.push(resolve);
    });
  }
  active += 1;
};

const releaseSlot = (): void => {
  active -= 1;
  waiters.shift()?.();
};

// Failed downloads stay cached only briefly: long enough that one list
// render can't hammer a dead media endpoint, short enough that a transient
// network blip doesn't pin initials for the rest of the session.
const FAILURE_RETRY_MS = 60_000;

const loadAvatar = async (api: WidgetApi, mxc: string): Promise<string | null> => {
  await acquireSlot();
  try {
    const blob = await api.downloadFile(mxc);
    return URL.createObjectURL(blob);
  } catch {
    // Missing media / capability denied / network — initials fallback now,
    // retry possible after the negative-cache window.
    window.setTimeout(() => cache.delete(mxc), FAILURE_RETRY_MS);
    return null;
  } finally {
    releaseSlot();
  }
};

const resolveAvatar = (api: WidgetApi, mxc: string): Promise<string | null> => {
  let promise = cache.get(mxc);
  if (!promise) {
    promise = loadAvatar(api, mxc);
    cache.set(mxc, promise);
  }
  return promise;
};

/** Resolve an mxc avatar URI to a local object URL (null while loading or on
 * failure — callers render the initials fallback in both cases). */
export const useMxcAvatar = (api: WidgetApi, mxc: string | undefined): string | null => {
  const [url, setUrl] = useState<string | null>(null);

  useEffect(() => {
    if (!mxc || !mxc.startsWith('mxc://')) {
      setUrl(null);
      return undefined;
    }
    let alive = true;
    setUrl(null);
    resolveAvatar(api, mxc).then((resolved) => {
      if (alive) setUrl(resolved);
    });
    return () => {
      alive = false;
    };
  }, [api, mxc]);

  return url;
};

/** Like useMxcAvatar, but walks a PRIORITY LIST of candidate mxc URIs and
 * returns the first one that actually downloads. Built for the own-profile
 * avatar, where the primary source (whoami profile.avatar) can be empty OR
 * point at media that no longer resolves — a dead first candidate must not
 * mask a live second one. */
export const useFirstAvatar = (
  api: WidgetApi,
  candidates: Array<string | undefined>
): string | null => {
  const [url, setUrl] = useState<string | null>(null);
  const key = candidates.filter(Boolean).join('|');

  useEffect(() => {
    let alive = true;
    setUrl(null);
    const list = key === '' ? [] : key.split('|');
    void (async () => {
      for (const mxc of list) {
        if (!mxc.startsWith('mxc://')) continue;
        // Sequential on purpose: candidates are ordered by trustworthiness
        // and the list is ≤3 entries, all cached after the first pass.
        // eslint-disable-next-line no-await-in-loop
        const resolved = await resolveAvatar(api, mxc);
        if (!alive) return;
        if (resolved) {
          setUrl(resolved);
          return;
        }
      }
    })();
    return () => {
      alive = false;
    };
  }, [api, key]);

  return url;
};
