import {
  Direction,
  EventType,
  type IContent,
  type IEventRelation,
  type IRoomEvent,
  type MatrixClient,
  type MatrixEvent,
  type Room,
  RelationType,
} from 'matrix-js-sdk';
import type { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { getMemberDisplayName, getSpaceChildren, isSpace } from '../../utils/room';
import type { ForumPost, ForumTopic } from './types';
import { annotatePostsWithRoomScope } from './forumTopicHelpers';
import { bodyHtmlFromMessageContent, titleFromFormattedBody } from './forumRichText';

type RawRelation = {
  rel_type?: string;
  event_id?: string;
  'm.in_reply_to'?: { event_id?: string };
};

function threadRelationFromContent(content: Record<string, unknown>): RawRelation | null {
  const relates = content['m.relates_to'];
  if (!relates || typeof relates !== 'object') return null;
  return relates as RawRelation;
}

function isAnnotationRoomMessage(content: Record<string, unknown>): boolean {
  const relation = threadRelationFromContent(content);
  if (!relation?.rel_type) return false;
  return (
    relation.rel_type === RelationType.Replace ||
    relation.rel_type === 'm.replace' ||
    relation.rel_type === RelationType.Annotation ||
    relation.rel_type === 'm.annotation'
  );
}

function isForumRootContent(content: Record<string, unknown>): boolean {
  if (typeof content['com.matrixsso.title'] === 'string') return true;
  const formatted = content.formatted_body;
  return typeof formatted === 'string' && /<h1[\s>]/i.test(formatted);
}

function syncForumRootContentAfterEdit(
  merged: Record<string, unknown>,
  baseContent: Record<string, unknown>
): Record<string, unknown> {
  if (!isForumRootContent(baseContent)) return merged;

  const formattedTitle =
    typeof merged.formatted_body === 'string'
      ? titleFromFormattedBody(merged.formatted_body)
      : undefined;
  const { title, body } = postTitleFromContent({
    ...merged,
    // Ignore stale title so edits can rename the post.
    'com.matrixsso.title': formattedTitle ?? undefined,
  });

  const next: Record<string, unknown> = {
    ...merged,
    'com.matrixsso.title': title,
  };

  if (typeof merged.body === 'string') {
    const plain = merged.body.trim();
    if (body && plain !== body && !plain.startsWith(`${title}\n`)) {
      next.body = `${title}\n\n${body}`;
    }
  }

  return next;
}

function getLatestEditContent(
  event: IRoomEvent,
  timelineEvents: IRoomEvent[]
): Record<string, unknown> {
  const baseContent = event.content as Record<string, unknown>;
  const targetId = event.event_id;
  if (!targetId) return baseContent;

  const edits = timelineEvents
    .filter((candidate) => {
      if (candidate.type !== EventType.RoomMessage || !candidate.content) return false;
      const rel = threadRelationFromContent(candidate.content as Record<string, unknown>);
      return rel?.rel_type === RelationType.Replace && rel.event_id === targetId;
    })
    .sort((a, b) => (b.origin_server_ts || 0) - (a.origin_server_ts || 0));

  const latestEdit = edits[0];
  if (!latestEdit?.content) return baseContent;

  const newContent = (latestEdit.content as Record<string, unknown>)['m.new_content'];
  if (!newContent || typeof newContent !== 'object') return baseContent;

  const merged = { ...baseContent, ...(newContent as Record<string, unknown>) };
  return syncForumRootContentAfterEdit(merged, baseContent);
}

function parseBundledThreadReplyCount(event: IRoomEvent): number | undefined {
  const relations = event.unsigned?.['m.relations'] as Record<string, unknown> | undefined;
  const thread = relations?.['m.thread'] as { count?: unknown } | undefined;
  const count = Number(thread?.count);
  if (!Number.isFinite(count) || count < 0) return undefined;
  return Math.floor(count);
}

function postTitleFromContent(content: Record<string, unknown>): { title: string; body: string } {
  const rawBody = typeof content.body === 'string' ? content.body : '';
  const customTitle =
    typeof content['com.matrixsso.title'] === 'string'
      ? String(content['com.matrixsso.title']).trim()
      : '';
  const htmlTitle =
    typeof content.formatted_body === 'string'
      ? titleFromFormattedBody(content.formatted_body)
      : undefined;
  const firstLine = rawBody.split(/\r?\n/, 1)[0]?.trim() ?? '';
  const title = (customTitle || htmlTitle || firstLine || '(untitled post)').slice(0, 160);

  let body = rawBody;
  if (customTitle && rawBody.startsWith(customTitle)) {
    body = rawBody.slice(customTitle.length).replace(/^\s+/, '');
  } else {
    const [leadingBlock, ...restBlocks] = rawBody.split(/\r?\n\r?\n/);
    if (restBlocks.length > 0 && leadingBlock.trim() === firstLine) {
      body = restBlocks.join('\n\n').trim();
    } else if (firstLine && rawBody.startsWith(firstLine)) {
      body = rawBody.slice(firstLine.length).replace(/^\s+/, '');
    }
  }

  return { title, body: body || rawBody };
}

/** Title + body for a forum matrix message payload (including edits). */
export function forumPostTitleFromContent(content: Record<string, unknown>): string {
  return postTitleFromContent(content).title;
}

function sortRepliesRecursively(posts: ForumPost[]) {
  posts.sort((a, b) => a.timestamp - b.timestamp);
  for (const post of posts) {
    if (post.replies.length > 0) sortRepliesRecursively(post.replies);
  }
}

function buildPosts(mx: MatrixClient, roomId: string, timelineEvents: IRoomEvent[]): ForumPost[] {
  const redactedIds = new Set(
    timelineEvents.filter((e) => e.unsigned?.redacted_because).map((e) => e.event_id)
  );

  const normalized = timelineEvents
    .filter(
      (event) =>
        event.type === EventType.RoomMessage &&
        event.event_id &&
        event.content &&
        !redactedIds.has(event.event_id)
    )
    .map((event) => {
      const rawContent = event.content as Record<string, unknown>;
      if (isAnnotationRoomMessage(rawContent)) return null;

      const content = getLatestEditContent(event, timelineEvents);
      const relation = threadRelationFromContent(content);
      const isThreadReply =
        relation?.rel_type === RelationType.Thread || relation?.rel_type === 'm.thread';

      if (typeof content.body !== 'string') return null;

      const { title, body } = postTitleFromContent(content);
      const customTitle =
        typeof content['com.matrixsso.title'] === 'string'
          ? String(content['com.matrixsso.title']).trim()
          : '';
      const bodyHtml = bodyHtmlFromMessageContent(content, {
        stripTitleHeading: Boolean(customTitle),
      });
      const room = mx.getRoom(roomId);
      const displayName = room
        ? getMemberDisplayName(room, event.sender || '') ?? event.sender
        : event.sender;

      return {
        eventId: event.event_id!,
        title,
        body,
        bodyHtml,
        sender: event.sender || 'unknown',
        senderDisplayName: displayName,
        timestamp: event.origin_server_ts || 0,
        relation,
        bundledReplyCount: !isThreadReply ? parseBundledThreadReplyCount(event) : undefined,
      };
    })
    .filter((e): e is NonNullable<typeof e> => e !== null)
    .sort((a, b) => a.timestamp - b.timestamp);

  const postsById = new Map<string, ForumPost>();
  for (const event of normalized) {
    postsById.set(event.eventId, {
      eventId: event.eventId,
      title: event.title,
      body: event.body,
      bodyHtml: event.bodyHtml,
      sender: event.sender,
      senderDisplayName: event.senderDisplayName,
      timestamp: event.timestamp,
      bundledReplyCount: event.bundledReplyCount,
      replies: [],
    });
  }

  const roots: ForumPost[] = [];
  const placedReplyIds = new Set<string>();

  for (const event of normalized) {
    const current = postsById.get(event.eventId);
    if (!current) continue;

    const relation = event.relation;
    let threadRootId: string | null = null;
    let parentEventId: string | null = null;

    if (relation?.rel_type === RelationType.Thread || relation?.rel_type === 'm.thread') {
      threadRootId = relation.event_id ? String(relation.event_id) : null;
    }
    if (relation?.['m.in_reply_to']?.event_id) {
      parentEventId = String(relation['m.in_reply_to'].event_id);
    }

    if (!threadRootId) {
      roots.push(current);
      continue;
    }

    if (placedReplyIds.has(event.eventId)) continue;

    if (parentEventId && parentEventId !== threadRootId) {
      const parent = postsById.get(parentEventId);
      if (parent) {
        current.replyToEventId = parentEventId;
        parent.replies.push(current);
        placedReplyIds.add(event.eventId);
        continue;
      }

      current.replyToEventId = parentEventId;
      current.replyToDeleted = true;
      const threadRoot = postsById.get(threadRootId);
      if (threadRoot) {
        threadRoot.replies.push(current);
        placedReplyIds.add(event.eventId);
      }
      continue;
    }

    const threadRoot = postsById.get(threadRootId);
    if (threadRoot) {
      threadRoot.replies.push(current);
      placedReplyIds.add(event.eventId);
    }
  }

  sortRepliesRecursively(roots);
  return roots.sort((a, b) => b.timestamp - a.timestamp);
}

export async function listForumTopics(mx: MatrixClient, spaceRoomId: string): Promise<ForumTopic[]> {
  const rooms: IHierarchyRoom[] = [];
  let nextBatch: string | undefined;
  for (let page = 0; page < 5; page += 1) {
    const hierarchy = await mx.getRoomHierarchy(spaceRoomId, 100, 3, false, nextBatch);
    rooms.push(...hierarchy.rooms);
    nextBatch = hierarchy.next_batch;
    if (!nextBatch) break;
  }

  const parentByRoomId = new Map<string, string>();
  for (const room of rooms) {
    if (!room?.room_id || !Array.isArray(room.children_state)) continue;
    for (const childState of room.children_state) {
      if (childState?.type !== 'm.space.child' || typeof childState.state_key !== 'string') continue;
      parentByRoomId.set(childState.state_key, room.room_id);
    }
  }

  const fromHierarchy = rooms
    .filter((room) => room?.room_id && room.room_id !== spaceRoomId)
    .map((room) => ({
      roomId: room.room_id,
      name: String(room.name || room.canonical_alias || room.room_id),
      topic: typeof room.topic === 'string' ? room.topic : undefined,
      canonicalAlias: typeof room.canonical_alias === 'string' ? room.canonical_alias : undefined,
      memberCount:
        typeof room.num_joined_members === 'number' ? room.num_joined_members : undefined,
      isSpace: room.room_type === 'm.space' || room.room_type === 'm.forum',
      parentRoomId: parentByRoomId.get(room.room_id),
    }));

  if (fromHierarchy.length > 0) {
    return fromHierarchy.sort((a, b) => a.name.localeCompare(b.name));
  }

  // Fallback: joined space children (when hierarchy API returns nothing useful)
  const topics: ForumTopic[] = [];
  const visited = new Set<string>();

  const collectFromSpace = (parentId: string, depth: number) => {
    if (depth > 3 || visited.has(parentId)) return;
    visited.add(parentId);
    const parentRoom = mx.getRoom(parentId);
    if (!parentRoom) return;

    for (const childId of getSpaceChildren(parentRoom)) {
      if (childId === spaceRoomId || visited.has(childId)) continue;
      const child = mx.getRoom(childId);
      const name = child?.name || childId;
      const childIsSpace = isSpace(child ?? null);
      topics.push({
        roomId: childId,
        name,
        isSpace: childIsSpace,
        parentRoomId: parentId,
      });
      if (childIsSpace) collectFromSpace(childId, depth + 1);
    }
  };

  collectFromSpace(spaceRoomId, 0);
  return topics.sort((a, b) => a.name.localeCompare(b.name));
}

export async function listTopicFeedPosts(
  mx: MatrixClient,
  roomId: string,
  options: { from?: string; minRoots?: number } = {}
): Promise<{ posts: ForumPost[]; nextBatch: string | null }> {
  const pageSize = 40;
  const minRoots = options.minRoots ?? 8;
  let merged: ForumPost[] = [];
  let nextBatch = options.from;
  const maxPages = options.from ? 5 : 10;

  for (let page = 0; page < maxPages; page += 1) {
    const response = await mx.createMessagesRequest(
      roomId,
      nextBatch ?? null,
      pageSize,
      Direction.Backward
    );
    const chunk = [...(response.chunk || [])].reverse();
    const pagePosts = buildPosts(mx, roomId, chunk);
    merged = page === 0 && !options.from ? pagePosts : mergeRootPosts(merged, pagePosts);
    nextBatch = response.end ?? undefined;
    if (!nextBatch || merged.length >= minRoots) break;
  }

  const localPosts = buildPosts(mx, roomId, collectLocalRootPosts(mx, roomId));
  merged = mergeRootPosts(merged, localPosts);

  return { posts: merged, nextBatch: nextBatch ?? null };
}

function mergeRootPosts(existing: ForumPost[], incoming: ForumPost[]): ForumPost[] {
  const byId = new Map(existing.map((post) => [post.eventId, post]));
  for (const post of incoming) {
    const previous = byId.get(post.eventId);
    if (!previous) {
      byId.set(post.eventId, post);
      continue;
    }
    const replyIds = new Set(previous.replies.map((reply) => reply.eventId));
    const mergedReplies = [
      ...previous.replies,
      ...post.replies.filter((reply) => !replyIds.has(reply.eventId)),
    ];
    byId.set(post.eventId, { ...post, replies: mergedReplies });
  }
  return [...byId.values()].sort((a, b) => b.timestamp - a.timestamp);
}

function mergeUniqueRoomEvents(...lists: IRoomEvent[][]): IRoomEvent[] {
  const seen = new Set<string>();
  const unique: IRoomEvent[] = [];
  for (const list of lists) {
    for (const event of list) {
      const id = event.event_id;
      if (!id || seen.has(id)) continue;
      seen.add(id);
      unique.push(event);
    }
  }
  return unique;
}

function isSecondaryMatrixEvent(mEvent: MatrixEvent): boolean {
  const relation = mEvent.getRelation();
  if (
    relation?.rel_type === RelationType.Replace ||
    relation?.rel_type === RelationType.Annotation
  ) {
    return true;
  }
  return isAnnotationRoomMessage(mEvent.getContent());
}

function eventBelongsToThread(mEvent: MatrixEvent, rootEventId: string): boolean {
  if (mEvent.getId() === rootEventId) return true;

  const relation = mEvent.getRelation();
  if (relation?.rel_type === RelationType.Thread && relation.event_id === rootEventId) {
    return true;
  }

  const contentRelation = threadRelationFromContent(mEvent.getContent());
  if (
    contentRelation?.rel_type === RelationType.Thread &&
    contentRelation.event_id === rootEventId
  ) {
    return true;
  }

  return false;
}

function matrixEventToRoomEvent(mEvent: MatrixEvent): IRoomEvent {
  return mEvent.event as IRoomEvent;
}

/** Thread events already in the local room timeline (includes unsent/local echo). */
export function collectLocalThreadEvents(
  mx: MatrixClient,
  roomId: string,
  rootEventId: string
): IRoomEvent[] {
  const rootId = rootEventId.trim();
  const room = mx.getRoom(roomId);
  if (!rootId || !room) return [];

  const seen = new Set<string>();
  const events: IRoomEvent[] = [];

  const add = (mEvent: MatrixEvent | undefined | null) => {
    if (!mEvent || mEvent.isRedacted()) return;
    if (mEvent.getType() !== EventType.RoomMessage) return;
    if (isSecondaryMatrixEvent(mEvent)) return;
    if (!eventBelongsToThread(mEvent, rootId)) return;
    const id = mEvent.getId();
    if (!id || seen.has(id)) return;
    seen.add(id);
    events.push(matrixEventToRoomEvent(mEvent));
  };

  add(room.findEventById(rootId));

  const thread = room.getThread(rootId);
  if (thread) {
    add(thread.rootEvent);
    for (const reply of thread.events) add(reply);
    for (const reply of thread.liveTimeline?.getEvents() ?? []) add(reply);
  }

  for (const mEvent of room.getUnfilteredTimelineSet().getLiveTimeline().getEvents()) {
    add(mEvent);
  }

  const targetIds = new Set(seen);
  events.push(...collectLocalReplaceEdits(room, targetIds));

  return events;
}

function collectLocalReplaceEdits(room: Room, targetIds: Set<string>): IRoomEvent[] {
  const events: IRoomEvent[] = [];
  const seen = new Set<string>();

  for (const mEvent of room.getUnfilteredTimelineSet().getLiveTimeline().getEvents()) {
    if (mEvent.isRedacted() || mEvent.getType() !== EventType.RoomMessage) continue;
    const relation = mEvent.getRelation();
    if (relation?.rel_type !== RelationType.Replace) continue;
    const targetId = relation.event_id;
    if (!targetId || !targetIds.has(targetId)) continue;
    const id = mEvent.getId();
    if (!id || seen.has(id)) continue;
    seen.add(id);
    events.push(matrixEventToRoomEvent(mEvent));
  }

  return events;
}

function collectLocalRootPosts(mx: MatrixClient, roomId: string): IRoomEvent[] {
  const room = mx.getRoom(roomId);
  if (!room) return [];

  const seen = new Set<string>();
  const events: IRoomEvent[] = [];

  for (const mEvent of room.getUnfilteredTimelineSet().getLiveTimeline().getEvents()) {
    if (mEvent.isRedacted() || mEvent.getType() !== EventType.RoomMessage) continue;
    if (isSecondaryMatrixEvent(mEvent)) continue;
    const relation = threadRelationFromContent(mEvent.getContent());
    const isThreadReply =
      relation?.rel_type === RelationType.Thread || relation?.rel_type === 'm.thread';
    if (isThreadReply) continue;
    const id = mEvent.getId();
    if (!id || seen.has(id)) continue;
    seen.add(id);
    events.push(matrixEventToRoomEvent(mEvent));
  }

  const rootIds = new Set(events.map((event) => event.event_id).filter(Boolean) as string[]);
  events.push(...collectLocalReplaceEdits(room, rootIds));

  return events;
}

export function loadPostThreadLocal(
  mx: MatrixClient,
  roomId: string,
  rootEventId: string
): ForumPost | null {
  const rootId = rootEventId.trim();
  const events = collectLocalThreadEvents(mx, roomId, rootId);
  if (events.length === 0) return null;
  const posts = buildPosts(mx, roomId, events);
  return posts.find((post) => post.eventId === rootId) ?? posts[0] ?? null;
}

async function fetchRootEvent(
  mx: MatrixClient,
  roomId: string,
  rootEventId: string
): Promise<IRoomEvent | null> {
  const rootId = rootEventId.trim();
  if (!rootId) return null;

  try {
    return (await mx.fetchRoomEvent(roomId, rootId)) as IRoomEvent;
  } catch {
    const room = mx.getRoom(roomId);
    const local = room?.findEventById(rootId);
    if (local) {
      return local.event as IRoomEvent;
    }
    return null;
  }
}

export async function loadPostThread(
  mx: MatrixClient,
  roomId: string,
  rootEventId: string
): Promise<ForumPost | null> {
  const rootId = rootEventId.trim();
  const localEvents = collectLocalThreadEvents(mx, roomId, rootId);

  const rootEvent = await fetchRootEvent(mx, roomId, rootId);
  const apiEvents: IRoomEvent[] = rootEvent?.event_id ? [rootEvent] : [];

  const collectRelations = async (
    relationType: RelationType | null,
    eventType: EventType | null
  ) => {
    let from: string | undefined;
    for (let page = 0; page < 20; page += 1) {
      const relPayload = await mx.fetchRelations(roomId, rootId, relationType, eventType, {
        from,
        limit: 100,
        dir: Direction.Backward,
        recurse: true,
      });
      if (relPayload.chunk?.length) {
        apiEvents.push(...(relPayload.chunk as IRoomEvent[]));
      }
      from = relPayload.next_batch ?? undefined;
      if (!from) break;
    }
  };

  if (rootEvent?.event_id) {
    try {
      await collectRelations(RelationType.Replace, EventType.RoomMessage);
    } catch {
      // Root may have no replace relations.
    }
    try {
      await collectRelations(RelationType.Thread, EventType.RoomMessage);
    } catch {
      try {
        await collectRelations(null, EventType.RoomMessage);
      } catch {
        // Still show root without replies.
      }
    }
  }

  const unique = mergeUniqueRoomEvents(localEvents, apiEvents);
  if (unique.length === 0) return null;

  const posts = buildPosts(mx, roomId, unique);
  return posts.find((post) => post.eventId === rootId) ?? posts[0] ?? null;
}

export async function loadAggregatedTopicFeed(
  mx: MatrixClient,
  scopes: Array<{ roomId: string; sectionTitle?: string; topicName?: string }>
): Promise<ForumPost[]> {
  const allPosts: ForumPost[] = [];
  for (const scope of scopes) {
    const { posts } = await listTopicFeedPosts(mx, scope.roomId, { minRoots: 5 });
    annotatePostsWithRoomScope(posts, scope.roomId, scope);
    allPosts.push(...posts);
  }
  return allPosts.sort((a, b) => b.timestamp - a.timestamp);
}

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

export type ForumNewPostPayload = {
  title: string;
  plainText: string;
  formattedHtml: string;
};

/** Top-level forum thread root (matrixsso-compatible). */
export async function sendForumPost(
  mx: MatrixClient,
  roomId: string,
  post: ForumNewPostPayload
): Promise<string> {
  const title = post.title.trim();
  const plainText = post.plainText.trim();
  if (!title) {
    throw new Error('Post title is required.');
  }
  if (title.length > 140) {
    throw new Error('Post title is too long.');
  }
  if (!plainText) {
    throw new Error('Post body is required.');
  }

  let formattedHtml = post.formattedHtml.trim().replace(/(<br\s*\/?>\s*)+$/gi, '');
  if (!formattedHtml) {
    formattedHtml = `<p>${escapeHtml(plainText)}</p>`;
  }

  const res = await mx.sendMessage(roomId, {
    msgtype: 'm.text',
    body: `${title}\n\n${plainText}`,
    'com.matrixsso.title': title,
    format: 'org.matrix.custom.html',
    formatted_body: `<h1>${escapeHtml(title)}</h1>${formattedHtml}`,
  });

  return res.event_id;
}

/** Image/file/video root post with a forum title. */
export async function sendForumMediaRoot(
  mx: MatrixClient,
  roomId: string,
  title: string,
  content: IContent
): Promise<string> {
  const trimmedTitle = title.trim();
  if (!trimmedTitle) {
    throw new Error('Post title is required.');
  }
  if (trimmedTitle.length > 140) {
    throw new Error('Post title is too long.');
  }

  const res = await mx.sendMessage(roomId, {
    ...content,
    'com.matrixsso.title': trimmedTitle,
  });
  return res.event_id;
}

/** Thread attachment (image, file, etc.) under a forum post root. */
export async function sendForumThreadAttachment(
  mx: MatrixClient,
  roomId: string,
  threadRootId: string,
  content: IContent,
  replyToEventId?: string
): Promise<string> {
  const rootId = threadRootId.trim();
  const parentId = replyToEventId?.trim() || rootId;
  if (!rootId) {
    throw new Error('Thread root event id is required.');
  }

  const res = await mx.sendMessage(roomId, {
    ...content,
    'm.relates_to': {
      rel_type: RelationType.Thread,
      event_id: rootId,
      is_falling_back: false,
      'm.in_reply_to': { event_id: parentId },
    } as IEventRelation,
  });
  return res.event_id;
}

export type ForumReplyPayload = {
  plainText: string;
  formattedHtml?: string;
};

/** Thread reply (matrixsso: m.thread + m.in_reply_to on parent message). */
export function sendForumReply(
  mx: MatrixClient,
  roomId: string,
  threadRootId: string,
  replyToEventId: string,
  message: string | ForumReplyPayload
): Promise<string> {
  const rootId = threadRootId.trim();
  const parentId = replyToEventId.trim() || rootId;
  if (!rootId) {
    throw new Error('Thread root event id is required.');
  }

  const plainText = (typeof message === 'string' ? message : message.plainText).trim();
  if (!plainText) {
    throw new Error('Reply message is required.');
  }

  let formattedHtml =
    typeof message === 'string' ? '' : String(message.formattedHtml || '').trim();
  formattedHtml = formattedHtml.replace(/(<br\s*\/?>\s*)+$/gi, '');

  const content: Record<string, unknown> = {
    msgtype: 'm.text',
    body: plainText,
    'm.relates_to': {
      rel_type: RelationType.Thread,
      event_id: rootId,
      is_falling_back: false,
      'm.in_reply_to': { event_id: parentId },
    },
  };

  if (formattedHtml) {
    content.format = 'org.matrix.custom.html';
    content.formatted_body = formattedHtml;
  }

  return mx.sendMessage(roomId, content).then((res) => res.event_id);
}

export function createForumPostFromPayload(
  mx: MatrixClient,
  roomId: string,
  eventId: string,
  payload: ForumNewPostPayload,
  meta: { topicRoomId?: string; sectionTitle?: string; topicName?: string } = {}
): ForumPost {
  const userId = mx.getUserId() || 'unknown';
  const room = mx.getRoom(roomId);
  const displayName = room
    ? getMemberDisplayName(room, userId) ?? userId
    : userId;
  const title = payload.title.trim();
  const plainText = payload.plainText.trim();
  let formattedHtml = payload.formattedHtml.trim().replace(/(<br\s*\/?>\s*)+$/gi, '');
  if (!formattedHtml) {
    formattedHtml = `<p>${escapeHtml(plainText)}</p>`;
  }

  return {
    eventId,
    title,
    body: plainText,
    bodyHtml: bodyHtmlFromMessageContent(
      {
        format: 'org.matrix.custom.html',
        formatted_body: `<h1>${escapeHtml(title)}</h1>${formattedHtml}`,
      },
      { stripTitleHeading: true }
    ),
    sender: userId,
    senderDisplayName: displayName,
    timestamp: Date.now(),
    topicRoomId: meta.topicRoomId ?? roomId,
    sectionTitle: meta.sectionTitle,
    topicName: meta.topicName,
    bundledReplyCount: 0,
    replies: [],
  };
}

export function createForumReplyFromPayload(
  mx: MatrixClient,
  roomId: string,
  eventId: string,
  replyToEventId: string,
  payload: ForumReplyPayload
): ForumPost {
  const userId = mx.getUserId() || 'unknown';
  const room = mx.getRoom(roomId);
  const displayName = room
    ? getMemberDisplayName(room, userId) ?? userId
    : userId;
  const plainText = payload.plainText.trim();
  let formattedHtml = String(payload.formattedHtml || '').trim().replace(/(<br\s*\/?>\s*)+$/gi, '');

  const content: Record<string, unknown> = { body: plainText };
  if (formattedHtml) {
    content.format = 'org.matrix.custom.html';
    content.formatted_body = formattedHtml;
  }

  return {
    eventId,
    title: '',
    body: plainText,
    bodyHtml: bodyHtmlFromMessageContent(content),
    sender: userId,
    senderDisplayName: displayName,
    timestamp: Date.now(),
    replyToEventId,
    replies: [],
  };
}
