import { useEffect, useState } from 'react';
import { CallEmbed } from '../plugins/call';
import { isUserId } from '../utils/matrix';
import { useCallMembers, useCallSession } from './useCall';
import { useCallJoined } from './useCallEmbed';

/**
 * Returns the set of Matrix user IDs currently speaking in the Element Call
 * iframe.
 *
 * EC renders each participant's video tile with a `[data-video-fit]` wrapper.
 * When a participant is speaking, EC draws a speaking indicator via the tile's
 * `::before` pseudo-element `background-image` (anything other than `none`).
 * The participant's Matrix user ID is exposed on the first descendant carrying
 * an `aria-label`.
 *
 * We watch the whole iframe document so tiles added/removed mid-call are picked
 * up automatically, and on every relevant mutation we re-scan ALL `[data-video-fit]`
 * tiles and rebuild the set from the full current DOM state (rather than just the
 * tiles in the mutation batch).
 */
export const useCallSpeakers = (callEmbed: CallEmbed): Set<string> => {
  const [speakers, setSpeakers] = useState(new Set<string>());
  const callSession = useCallSession(callEmbed.room);
  const callMembers = useCallMembers(callSession);
  const joined = useCallJoined(callEmbed);

  // C-L5 — depend on a STABLE boolean, not the callMembers array (whose identity
  // changes on every membership change). The MutationObserver + io.lotus.call_state
  // subscription below already track tiles joining/leaving live, so rebuilding
  // them on each membership change is pure churn.
  const hasCallMembers = callMembers.length > 0;

  useEffect(() => {
    if (!hasCallMembers || !joined) {
      setSpeakers(new Set<string>());
      return undefined;
    }

    const getDoc = (): Document | undefined =>
      callEmbed.iframe.contentDocument ?? callEmbed.iframe.contentWindow?.document ?? undefined;

    const syncState = (): void => {
      // [lotus #2] Prefer the fork's io.lotus.call_state events over scraping
      // EC's rendered DOM. Falls back to the DOM path below when the fork hasn't
      // sent yet (null) OR sent a spurious empty list (you're always present in
      // your own joined call, so [] means "no usable data", not "nobody").
      const lotus = callEmbed.getLotusParticipants();
      if (lotus !== null && lotus.length > 0) {
        const ls = new Set<string>();
        lotus.forEach((p) => {
          if (p.speaking && isUserId(p.userId)) ls.add(p.userId);
        });
        setSpeakers(ls);
        return;
      }
      const doc = getDoc();
      if (!doc) {
        setSpeakers(new Set<string>());
        return;
      }
      const s = new Set<string>();
      // Re-scan every tile on each mutation and build the set from the full
      // current DOM state, not just the tiles that mutated this batch.
      const tiles = doc.querySelectorAll<HTMLElement>('[data-video-fit]');
      tiles.forEach((el) => {
        const style = callEmbed.iframe.contentWindow?.getComputedStyle(el, '::before');
        if (!style) return;
        const tileBackgroundImage = style.getPropertyValue('background-image');
        const speaking = tileBackgroundImage !== 'none';
        if (!speaking) return;

        const speakerId = el.querySelector('[aria-label]')?.getAttribute('aria-label');
        if (speakerId && isUserId(speakerId)) {
          s.add(speakerId);
        }
      });
      setSpeakers(s);
    };

    let tileObserver: MutationObserver | undefined;

    const attachObserver = (): void => {
      const doc = getDoc();
      if (!doc) return;
      tileObserver?.disconnect();
      // Watch the whole document for attribute changes on tiles (which carry
      // the speaking indicator) and for new tiles being added/removed.
      tileObserver = new MutationObserver((mutations) => {
        const relevant = mutations.some(
          (m) =>
            m.type === 'attributes' ||
            (m.type === 'childList' &&
              (Array.from(m.addedNodes).some(
                (n) => n instanceof Element && n.querySelector('[data-video-fit]'),
              ) ||
                Array.from(m.removedNodes).some(
                  (n) => n instanceof Element && n.querySelector('[data-video-fit]'),
                ))),
        );
        if (relevant) syncState();
      });
      tileObserver.observe(doc.body, {
        subtree: true,
        childList: true,
        attributes: true,
        attributeFilter: ['class', 'style'],
      });
      syncState();
    };

    attachObserver();
    // [lotus #2] Re-derive whenever the fork pushes new call-state.
    const unsubLotus = callEmbed.onLotusCallState(syncState);

    // If iframe isn't ready yet, wait for body to be available.
    let bodyWatcher: MutationObserver | undefined;
    if (!getDoc()?.body) {
      bodyWatcher = new MutationObserver(() => {
        if (getDoc()?.body) {
          bodyWatcher?.disconnect();
          bodyWatcher = undefined;
          attachObserver();
        }
      });
      const doc = getDoc();
      if (doc) bodyWatcher.observe(doc, { childList: true });
    }

    return () => {
      tileObserver?.disconnect();
      bodyWatcher?.disconnect();
      unsubLotus();
    };
  }, [callEmbed, hasCallMembers, joined]);

  return speakers;
};

/**
 * Returns true when any REMOTE participant has their microphone muted in the
 * Element Call iframe.
 *
 * EC renders a mute-icon element per participant tile with a `data-muted`
 * attribute ("true" = muted, "false" = unmuted) and an `aria-label` set to
 * the participant's Matrix user ID.  We watch for attribute changes on all
 * `[data-muted]` elements, filter out the local user, and return true if any
 * remaining participant is muted.
 */
export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean => {
  const [muted, setMuted] = useState(false);

  useEffect(() => {
    if (!callEmbed) return undefined;

    const getDoc = (): Document | undefined =>
      callEmbed.iframe.contentDocument ?? callEmbed.iframe.contentWindow?.document ?? undefined;

    const localUserId = callEmbed.room.client?.getUserId() ?? '';

    const syncState = (): void => {
      // [lotus #2] Prefer the fork's io.lotus.call_state over DOM scraping;
      // ignore a spurious empty list (fall back to DOM).
      const lotus = callEmbed.getLotusParticipants();
      if (lotus !== null && lotus.length > 0) {
        const remote = lotus.filter((p) => p.userId !== localUserId);
        setMuted(remote.length > 0 && remote.every((p) => !p.audioEnabled));
        return;
      }
      const doc = getDoc();
      if (!doc) {
        setMuted(false);
        return;
      }
      // Each participant's mute icon has data-muted="true"|"false" and
      // aria-label set to their Matrix user ID.
      const muteIcons = doc.querySelectorAll<HTMLElement>('[data-muted]');
      let remoteCount = 0;
      let remoteMutedCount = 0;
      muteIcons.forEach((el) => {
        const userId = el.getAttribute('aria-label') ?? '';
        if (userId === localUserId) return;
        remoteCount += 1;
        if (el.getAttribute('data-muted') === 'true') remoteMutedCount += 1;
      });
      // "All muted" badge: true only when there is at least one remote
      // participant and every one of them is muted (not merely any single one).
      setMuted(remoteCount > 0 && remoteMutedCount === remoteCount);
    };

    let tileObserver: MutationObserver | undefined;

    const attachObserver = (): void => {
      const doc = getDoc();
      if (!doc) return;
      tileObserver?.disconnect();
      // Watch the whole document for attribute changes on data-muted elements
      // and for new tiles being added/removed.
      tileObserver = new MutationObserver((mutations) => {
        const relevant = mutations.some(
          (m) =>
            m.type === 'attributes' ||
            (m.type === 'childList' &&
              (Array.from(m.addedNodes).some(
                (n) => n instanceof Element && n.querySelector('[data-muted]'),
              ) ||
                Array.from(m.removedNodes).some(
                  (n) => n instanceof Element && n.querySelector('[data-muted]'),
                ))),
        );
        if (relevant) syncState();
      });
      tileObserver.observe(doc.body, {
        subtree: true,
        childList: true,
        attributes: true,
        attributeFilter: ['data-muted'],
      });
      syncState();
    };

    attachObserver();
    // [lotus #2] Re-derive whenever the fork pushes new call-state.
    const unsubLotus = callEmbed.onLotusCallState(syncState);

    // If iframe isn't ready yet, wait for body to be available.
    let bodyWatcher: MutationObserver | undefined;
    if (!getDoc()?.body) {
      bodyWatcher = new MutationObserver(() => {
        if (getDoc()?.body) {
          bodyWatcher?.disconnect();
          bodyWatcher = undefined;
          attachObserver();
        }
      });
      const doc = getDoc();
      if (doc) bodyWatcher.observe(doc, { childList: true });
    }

    return () => {
      tileObserver?.disconnect();
      bodyWatcher?.disconnect();
      unsubLotus();
    };
  }, [callEmbed]);

  return muted;
};
