import { useEffect, useMemo, useState } from 'react';
import { MatrixEvent, MatrixEventEvent, MsgType, Room, ThreadEvent } from 'matrix-js-sdk';
import { trimReplyFromBody } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';

// One past conversation in an assistant bot's control DM. A conversation IS an m.thread
// root: the bot threads every top-level DM turn, so each chat is a thread the server
// enumerates for free via /rooms/{id}/threads.
export type BotConversation = {
  rootId: string;
  // First non-empty line of the thread root's body, reply-fallback stripped. "" when the
  // root isn't loaded yet or is media-only — the caller renders a localized placeholder.
  title: string;
  // Last-activity timestamp (latest reply, else the root), for recency ordering.
  ts: number;
};

// Derive a conversation title from its thread root WITHOUT storing anything: the title is
// the root message text (the §4.4 "root-text now" tier). A future account_data override
// (io.vojo.thread_titles) would layer on top of this at the call site.
const deriveTitle = (room: Room, rootId: string): string => {
  const rootEvent = room.getThread(rootId)?.rootEvent ?? room.findEventById(rootId);
  const content = rootEvent?.getContent();
  if (!content) return '';
  // Only text roots make a sensible title. For m.image/m.file/m.video the body is the
  // FILENAME (a non-empty string), so without this gate the list would leak "photo.png" as
  // a chat title; return "" so the caller shows the localized fallback instead. (The bot
  // never auto-threads media — it reacts "text only" first — but a manually-created media
  // thread in the same DM would otherwise surface here.)
  if (content.msgtype !== MsgType.Text && content.msgtype !== MsgType.Emote) return '';
  const { body } = content;
  if (typeof body !== 'string') return '';
  const firstLine = trimReplyFromBody(body)
    .split('\n')
    .find((line) => line.trim() !== '');
  return firstLine?.trim() ?? '';
};

// useBotConversations lists the control DM's conversations (thread roots), most-recent
// first, and re-renders as threads are created or get new replies. It triggers a one-shot
// server thread-list load (room.fetchRoomThreads) so cold conversations show after a
// reload without opening each thread. Read-only — never mutates room state.
export const useBotConversations = (room: Room): BotConversation[] => {
  const mx = useMatrixClient();
  const myUserId = mx.getUserId();
  const [tick, setTick] = useState(0);

  useEffect(() => {
    let cancelled = false;
    const bump = () => {
      if (!cancelled) setTick((n) => n + 1);
    };

    // Populate room.getThreads() from the server once; getThreads() then reflects it.
    // Best-effort — a failure just leaves the list at whatever /sync already materialized.
    room
      .fetchRoomThreads()
      .catch(() => undefined)
      .finally(bump);

    // ThreadEvent.New fires on the ROOM when a brand-new conversation roots (the SDK
    // excludes it from a thread's own emitter). ThreadEvent.Update is re-emitted onto the
    // room from each thread on every reply / latest-event change, so it covers both "a
    // reply landed" and the recency re-sort — we no longer also listen on RoomEvent.Timeline
    // (which fired an extra bump per reply plus one for every unrelated main-timeline event).
    room.on(ThreadEvent.New, bump);
    room.on(ThreadEvent.Update, bump);

    // In an encrypted DM a thread root can decrypt AFTER its last ThreadEvent.Update
    // (e.g. the bot's reply lands before the root is decrypted); without this the title
    // stays stuck on the localized fallback until the next reply. The client re-emits
    // Decrypted for every event, so re-derive when one lands in this room.
    const onDecrypted = (event: MatrixEvent) => {
      if (event.getRoomId() === room.roomId) bump();
    };
    mx.on(MatrixEventEvent.Decrypted, onDecrypted);

    return () => {
      cancelled = true;
      room.off(ThreadEvent.New, bump);
      room.off(ThreadEvent.Update, bump);
      mx.off(MatrixEventEvent.Decrypted, onDecrypted);
    };
  }, [room, mx]);

  return useMemo(() => {
    const conversations = room
      .getThreads()
      // Hide a not-yet-answered conversation from the list: a brand-new chat is the user's own
      // top-level message that the bot roots a thread on and answers a moment later. Until that
      // reply lands (or if the bot failed to answer at all), the thread has zero replies and a
      // self-authored root. Dropping those keeps stale/empty chats out of the history; a real
      // conversation reappears the instant the bot's threaded reply arrives — and at the moment
      // a chat is created the user is INSIDE it, not viewing this list. rootEvent unknown ⇒ keep.
      .filter((thread) => !(thread.length === 0 && thread.rootEvent?.getSender() === myUserId))
      .map<BotConversation>((thread) => {
        const last = thread.replyToEvent ?? thread.rootEvent;
        return {
          rootId: thread.id,
          title: deriveTitle(room, thread.id),
          ts: last?.getTs() ?? 0,
        };
      });
    conversations.sort((a, b) => b.ts - a.ts);
    return conversations;
    // `tick` is the reactivity trigger (room.getThreads() mutates in place); `room` and
    // `myUserId` are stable for the lifetime of this surface.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [room, tick]);
};
