import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { Icon, Icons, Text, as } from 'folds';
import {
  type IThreadBundledRelationship,
  type MatrixEvent,
  NotificationCountType,
  RelationType,
  type Room,
  RoomEvent,
  type Thread,
  ThreadEvent,
} from 'matrix-js-sdk';

import * as css from './ThreadSummaryCard.css';
import { Time } from '../../components/message';
import { getChannelsThreadPath } from '../../pages/pathUtils';
import { getMemberDisplayName } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
import { useUnreadThreadingEnabled } from '../../hooks/useClientConfig';

export type ThreadSummaryCardProps = {
  room: Room;
  rootEvent: MatrixEvent;
};

// Compact dark-pill summary rendered under thread roots in the channels
// timeline. Shows: dot indicator + reply count (bold, Primary.Main) +
// last reply time + chevron. Click navigates to
// `/channels/<space>/<room>/thread/<rootId>/` (M2 drawer).
//
// State model: tracks the SDK `Thread` object (created lazily on first
// reply, fired via `ThreadEvent.New` on the room) and re-renders on
// `ThreadEvent.Update` / `NewReply` for live count and time updates.
// Falls back to `mEvent.getServerAggregatedRelation('m.thread')` count
// + bundled `latest_event.origin_server_ts` for cold-load before the
// Thread is materialized.
//
// Returns `null` when there are no replies — mounting on every channel
// root is fine because of viewport virtualization in `RoomTimeline`.
export const ThreadSummaryCard = as<'button', ThreadSummaryCardProps>(
  ({ room, rootEvent, className, ...props }, ref) => {
    const { t } = useTranslation();
    const navigate = useNavigate();
    const { spaceIdOrAlias, roomIdOrAlias } = useParams();
    const rootId = rootEvent.getId();

    const [thread, setThread] = useState<Thread | null>(() =>
      rootId ? rootEvent.getThread() ?? room.getThread(rootId) ?? null : null
    );

    // Catch the moment SDK promotes the root into a Thread (first reply
    // arrives or relations come in via cold-load fetch). `ThreadEvent.New`
    // fires on `Room` (not Thread). Listener is one-shot — once we match
    // our rootId we detach so unrelated thread births in the same room
    // don't keep waking this row up. Skipping attach entirely when we
    // already have a thread keeps non-null instances listener-free.
    useEffect(() => {
      if (!rootId || thread) return undefined;
      const handleNew = (newThread: Thread) => {
        if (newThread.id === rootId) {
          setThread(newThread);
          room.off(ThreadEvent.New, handleNew);
        }
      };
      room.on(ThreadEvent.New, handleNew);
      // Race re-check: SDK may have created the Thread between the lazy
      // useState initializer and this effect mount. Cheap insurance —
      // mirrors the cold-load repair pattern in ThreadDrawer.tsx.
      const existing = room.getThread(rootId);
      if (existing) {
        setThread(existing);
        room.off(ThreadEvent.New, handleNew);
        return undefined;
      }
      return () => {
        room.off(ThreadEvent.New, handleNew);
      };
    }, [room, rootId, thread]);

    // Force a re-render on thread metadata changes. `ThreadEvent.NewReply`
    // is intentionally NOT subscribed: SDK emits Update right after every
    // NewReply (`models/thread.js::addEvent` → `updateThreadMetadata`), so
    // listening on both fires the tick twice per arrival. Capture the
    // emitter ref inside the effect for cleanup leak-safety against rare
    // snapshot replacements after timeline reset.
    const [, setVersion] = useState(0);
    useEffect(() => {
      if (!thread) return undefined;
      const localThread = thread;
      const tick = () => setVersion((v) => v + 1);
      localThread.on(ThreadEvent.Update, tick);
      return () => {
        localThread.off(ThreadEvent.Update, tick);
      };
    }, [thread]);

    // Per-thread unread refresh (M4). `RoomEvent.UnreadNotifications`
    // fires from three SDK call sites (matrix-js-sdk room.js:1354/1398
    // /1426): per-thread set (with threadId), per-thread reset from
    // /sync (NO threadId, room.js:1398), and main-timeline set (no
    // threadId, room.js:1426). Accept `firedThreadId === rootId` AND
    // the no-threadId reset/main-timeline cases so a /sync that zeros
    // all thread counts in one shot still ticks our card. The cost of
    // a no-op tick on unrelated main-timeline updates is negligible
    // because we only read a couple of cheap counters. Skipped under
    // the kill switch — pre-M4 cards don't surface unread state.
    const unreadThreadingEnabled = useUnreadThreadingEnabled();
    useEffect(() => {
      if (!unreadThreadingEnabled || !rootId) return undefined;
      const handle = (
        _count: unknown,
        firedThreadId: string | undefined
      ) => {
        if (firedThreadId === undefined || firedThreadId === rootId) {
          setVersion((v) => v + 1);
        }
      };
      room.on(RoomEvent.UnreadNotifications, handle);
      return () => {
        room.off(RoomEvent.UnreadNotifications, handle);
      };
    }, [room, rootId, unreadThreadingEnabled]);

    const bundled = rootId
      ? rootEvent.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread)
      : undefined;

    // When a Thread is materialized, trust its length even if it's 0 —
    // bundled aggregations don't shrink on redaction so they'd otherwise
    // surface stale counts on threads where every reply was redacted.
    // Bundled is the cold-load fallback only.
    const count = thread !== null ? thread.length : bundled?.count ?? 0;

    if (!rootId || count === 0) return null;

    const latestReply: MatrixEvent | undefined = thread?.replyToEvent ?? undefined;
    // Time source: prefer live Thread.replyToEvent timestamp; fall back to
    // bundled aggregation's `latest_event.origin_server_ts` so cold-loaded
    // cards still show a time before the SDK materializes the Thread.
    const lastTs =
      latestReply?.getTs() ??
      (typeof bundled?.latest_event?.origin_server_ts === 'number'
        ? bundled.latest_event.origin_server_ts
        : undefined);

    // Last-sender display name kept for screen-reader aria-label only —
    // visual surface is dot + count + time + chevron per current mockup.
    const senderId = latestReply?.getSender();
    const senderDisplayName = senderId
      ? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
      : null;

    // Per-thread unread state. Server-derived totals via SDK; only
    // consulted when the kill switch is on, so disabled-config or
    // MSC3773-less servers fall through to the static «read» dot.
    const unreadTotal = unreadThreadingEnabled
      ? room.getThreadUnreadNotificationCount(rootId, NotificationCountType.Total)
      : 0;
    const unreadHighlight = unreadThreadingEnabled
      ? room.getThreadUnreadNotificationCount(rootId, NotificationCountType.Highlight)
      : 0;
    let dotState: 'read' | 'unread' | 'highlight' = 'read';
    if (unreadHighlight > 0) dotState = 'highlight';
    else if (unreadTotal > 0) dotState = 'unread';

    const countLabel = t('Room.thread_summary_count', { count });
    const ariaParts = [t('Room.thread_summary_open_thread'), countLabel];
    if (unreadThreadingEnabled) {
      // Highlight (mention) is a strict superset of unread for screen
      // readers: prefer the mention phrasing when applicable so SR
      // users get parity with sighted users (red dot vs lavender dot).
      // Fall back to plain unread phrasing if total > 0 but no mention.
      if (unreadHighlight > 0) {
        ariaParts.push(t('Room.thread_summary_highlight', { count: unreadHighlight }));
      } else if (unreadTotal > 0) {
        ariaParts.push(t('Room.thread_summary_unread', { count: unreadTotal }));
      }
    }
    if (senderDisplayName) {
      ariaParts.push(t('Room.thread_summary_last_reply_by', { name: senderDisplayName }));
    }
    const ariaLabel = ariaParts.join(', ');

    const handleOpen = () => {
      if (!spaceIdOrAlias || !roomIdOrAlias) return;
      navigate(getChannelsThreadPath(spaceIdOrAlias, roomIdOrAlias, rootId));
    };

    return (
      <button
        type="button"
        ref={ref}
        className={classNames(css.Root, className)}
        onClick={handleOpen}
        aria-label={ariaLabel}
        data-event-id={rootId}
        data-unread-state={dotState}
        {...props}
      >
        <span className={css.Dot} data-unread-state={dotState} aria-hidden />
        <Text className={css.Count} as="span" size="T200">
          <b>{countLabel}</b>
        </Text>
        {lastTs && (
          <>
            <span className={css.Separator} aria-hidden>
              ·
            </span>
            <Time className={css.TimeText} ts={lastTs} compact size="T200" priority="300" />
          </>
        )}
        <Icon className={css.Chevron} src={Icons.ChevronRight} size="100" />
      </button>
    );
  }
);
