import { useCallback, useEffect, useState } from 'react';
import {
  EventStatus,
  EventTimeline,
  MatrixClient,
  MatrixEvent,
  Room,
  RoomEvent,
  RoomEventHandlerMap,
  Thread,
  ThreadEvent,
} from 'matrix-js-sdk';
import { getLinkedTimelines } from '../RoomTimeline';
import { isPendingThreadReply } from './threadSummaryData';

/**
 * Resolve (or bootstrap) the live {@link Thread} for a root event.
 *
 * Uses the existing thread when present, otherwise creates one via
 * `room.createThread` — the SDK then auto-fetches the thread's events via
 * `/relations` and inserts the root at the top. If the root event isn't loaded
 * locally the Thread handles the root fetch itself, so passing `undefined` is
 * safe. Re-resolves when a matching thread later appears/updates on the room.
 */
export const useThreadInstance = (room: Room, threadRootId: string): Thread | undefined => {
  const getInstance = useCallback((): Thread | undefined => {
    const existing = room.getThread(threadRootId);
    if (existing) return existing;
    const rootEvent = room.findEventById(threadRootId);
    return room.createThread(threadRootId, rootEvent, [], false) ?? undefined;
  }, [room, threadRootId]);

  const [thread, setThread] = useState<Thread | undefined>(getInstance);

  useEffect(() => {
    setThread(getInstance());

    const handleThread: RoomEventHandlerMap[ThreadEvent.New] = (newThread) => {
      if (newThread.id === threadRootId) setThread(newThread);
    };
    const handleThreadUpdate: RoomEventHandlerMap[ThreadEvent.Update] = (updatedThread) => {
      if (updatedThread.id === threadRootId) setThread(updatedThread);
    };

    room.on(ThreadEvent.New, handleThread);
    room.on(ThreadEvent.Update, handleThreadUpdate);
    return () => {
      room.removeListener(ThreadEvent.New, handleThread);
      room.removeListener(ThreadEvent.Update, handleThreadUpdate);
    };
  }, [room, threadRootId, getInstance]);

  return thread;
};

/**
 * Build the ordered list of linked {@link EventTimeline}s for a thread's live
 * timeline and track readiness (`thread.initialEventsFetched`). Subscribes to
 * the Thread's re-emitted timeline events so callers repaginate/re-render as
 * the thread fills in.
 */
export const useThreadLinkedTimelines = (
  mx: MatrixClient,
  thread: Thread,
): { timelines: EventTimeline[]; ready: boolean; refresh: () => void } => {
  const [timelines, setTimelines] = useState<EventTimeline[]>(() =>
    getLinkedTimelines(thread.liveTimeline),
  );
  const [ready, setReady] = useState<boolean>(() => thread.initialEventsFetched);

  const refresh = useCallback(() => {
    setTimelines(getLinkedTimelines(thread.liveTimeline));
    setReady(thread.initialEventsFetched);
  }, [thread]);

  useEffect(() => {
    refresh();

    const handleTimeline = () => refresh();
    // Thread re-emits RoomEvent.Timeline / RoomEvent.TimelineReset from its
    // timelineSet, and fires ThreadEvent.Update as it (re)populates.
    thread.on(RoomEvent.Timeline, handleTimeline);
    thread.on(RoomEvent.TimelineReset, handleTimeline);
    thread.on(ThreadEvent.Update, handleTimeline);
    return () => {
      thread.removeListener(RoomEvent.Timeline, handleTimeline);
      thread.removeListener(RoomEvent.TimelineReset, handleTimeline);
      thread.removeListener(ThreadEvent.Update, handleTimeline);
    };
  }, [thread, refresh]);

  return { timelines, ready, refresh };
};

/**
 * Track in-flight (local echo) replies for a thread.
 *
 * Pending thread sends never enter the thread's timelineSet (chronological
 * pending ordering rejects them; `room.getPendingEvents()` THROWS in this
 * mode). We instead watch `RoomEvent.LocalEchoUpdated` on the room and keep our
 * own list of events that are pending replies to this thread and not yet in the
 * thread timeline. When an event's remote echo arrives (status flips to SENT,
 * or it lands in the thread) it drops out of the list.
 */
export const useThreadPendingEvents = (
  room: Room,
  threadRootId: string,
  thread: Thread | undefined,
): MatrixEvent[] => {
  const [pending, setPending] = useState<MatrixEvent[]>([]);

  useEffect(() => {
    setPending([]);

    const handleLocalEcho: RoomEventHandlerMap[RoomEvent.LocalEchoUpdated] = (event) => {
      const eventId = event.getId();
      setPending((prev) => {
        // Drop any previous entry for this event (same instance across the
        // temp-id -> real-id transition, or matched by id).
        const without = prev.filter((e) => e !== event && e.getId() !== eventId);

        const alreadyInThread =
          eventId !== undefined && thread?.findEventById(eventId) !== undefined;
        // Keep a tracked event through the SENT window too: the /send response
        // flips status to SENT before /sync delivers the event into the thread
        // timeline — dropping it there would make the message flash out of view.
        // It falls out on the next LocalEchoUpdated once findEventById sees it.
        const trackedAndAwaitingSync =
          event.status === EventStatus.SENT &&
          prev.some((e) => e === event || (eventId !== undefined && e.getId() === eventId));
        const stillPending =
          !alreadyInThread && (isPendingThreadReply(event, threadRootId) || trackedAndAwaitingSync);

        if (stillPending) return [...without, event];
        return without.length === prev.length ? prev : without;
      });
    };

    room.on(RoomEvent.LocalEchoUpdated, handleLocalEcho);
    return () => {
      room.removeListener(RoomEvent.LocalEchoUpdated, handleLocalEcho);
    };
  }, [room, threadRootId, thread]);

  return pending;
};

// markThreadAsRead moved to ./threadReceipt (pure + unit-tested); re-exported
// here for existing import sites.
export { markThreadAsRead } from './threadReceipt';
