import { useEffect, useState } from 'react';
import { MatrixError, Method } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';

const PROFILE_FIELD = 'io.lotus.avatar_decoration';

// Module-level cache — survives re-renders, lives for the app session.
// userId → slug | null (null = fetched, no decoration set)
const cache = new Map<string, string | null>();
// Callbacks waiting for a userId's result
const pending = new Map<string, Array<(val: string | null) => void>>();
// Transient-failure attempt counts (userId → n) so a flaky federated lookup
// can retry a couple of times, then gives up for the session.
const failures = new Map<string, number>();
// Mounted hooks per userId, so an invalidation (e.g. you change your own
// decoration) re-fetches live instead of waiting for a remount.
const listeners = new Map<string, Set<() => void>>();

function subscribeDecoration(userId: string, cb: () => void): () => void {
  let set = listeners.get(userId);
  if (!set) {
    set = new Set();
    listeners.set(userId, set);
  }
  set.add(cb);
  return () => {
    set.delete(cb);
    if (set.size === 0) listeners.delete(userId);
  };
}

function fetchDecoration(
  authedRequest: (method: Method, path: string) => Promise<Record<string, string>>,
  userId: string,
): Promise<string | null> {
  if (cache.has(userId)) return Promise.resolve(cache.get(userId) ?? null);

  // De-duplicate in-flight requests for the same userId
  if (pending.has(userId)) {
    return new Promise((resolve) => {
      pending.get(userId)!.push(resolve);
    });
  }

  const waiters: Array<(val: string | null) => void> = [];
  pending.set(userId, waiters);

  // Fetch the WHOLE profile, not the single `/{field}` sub-resource: an unset
  // field returns 404, which the browser logs as a failed request — a console
  // 404 for every user without a decoration. The full profile returns 200 with
  // all fields (incl. custom MSC4133 ones); read the decoration out of it.
  return authedRequest(Method.Get, `/profile/${encodeURIComponent(userId)}`)
    .then((res) => {
      const val = (res[PROFILE_FIELD] as string | undefined) ?? null;
      cache.set(userId, val);
      return val;
    })
    .catch((err: unknown) => {
      const status = err instanceof MatrixError ? err.httpStatus : undefined;
      // Definitive rejections (404 unknown user / 403 can't view / 400) — cache
      // "no decoration" so we never refetch a profile we can't read (otherwise
      // every avatar mount re-floods our HS with failing federated lookups).
      if (status === 404 || status === 403 || status === 400) {
        cache.set(userId, null);
      } else {
        // Transient (429 rate-limit / 5xx / network). Allow a couple of retries
        // — a single 429 in a member-list burst shouldn't permanently hide a
        // decoration — then give up for the session so a persistently-failing
        // federated link (e.g. a 502'ing remote server) can't loop forever.
        const attempts = (failures.get(userId) ?? 0) + 1;
        failures.set(userId, attempts);
        if (attempts >= 2) cache.set(userId, null);
      }
      return null;
    })
    .finally(() => {
      const v = cache.get(userId) ?? null;
      pending.delete(userId);
      waiters.forEach((cb) => cb(v));
    });
}

export function invalidateDecorationCache(userId: string): void {
  cache.delete(userId);
  // Also clear the give-up counter so the next fetch starts fresh.
  failures.delete(userId);
  // Notify mounted avatars for this user so they re-fetch immediately.
  listeners.get(userId)?.forEach((cb) => cb());
}

export function useAvatarDecoration(userId: string): string | null {
  const mx = useMatrixClient();
  const [slug, setSlug] = useState<string | null>(() => cache.get(userId) ?? null);

  useEffect(() => {
    let cancelled = false;
    const load = () => {
      fetchDecoration(
        (method, path) => mx.http.authedRequest<Record<string, string>>(method, path),
        userId,
      ).then((val) => {
        if (!cancelled) setSlug(val);
      });
    };
    load();
    // Re-run on invalidation (fetchDecoration re-fetches since the cache entry
    // was cleared; concurrent mounts for the same user still de-dupe via
    // `pending`). Keeps live avatars in sync when the decoration changes.
    const unsubscribe = subscribeDecoration(userId, load);
    return () => {
      cancelled = true;
      unsubscribe();
    };
  }, [mx, userId]);

  return slug;
}
