import WKSDK from "wukongimjssdk";
import { ChannelInfoListener } from "wukongimjssdk";
import {
  Channel,
  ChannelInfo,
  ChannelTypePerson,
  ChannelTypeGroup,
} from "wukongimjssdk";
import { ChannelTypeCommunityTopic } from "../../Service/Const";
import { parseThreadChannelId } from "../../Service/Thread";
import React, { Component } from "react";
import { Modal, Tag } from "@douyinfe/semi-ui";
import { ConversationWrap, MessageWrap } from "../../Service/Model";
import { getTimeStringAutoShort2 } from "../../Utils/time";
import classNames from "classnames";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

import "./index.css";

import WKApp from "../../App";
import { EndpointID } from "../../Service/Const";
import GroupIcon from "../Icons/GroupIcon";
import ThreadIcon from "../Icons/ThreadIcon";
import ContextMenus, {
  ContextMenusContext,
  ContextMenusData,
} from "../ContextMenus";
import { ChannelSettingManager } from "../../Service/ChannelSetting";
import { TypingListener, TypingManager } from "../../Service/TypingManager";
import { BeatLoader } from "react-spinners";
import { RevokeCell } from "../../Messages/Revoke";
import { FlameMessageCell } from "../../Messages/Flame";
import WKAvatar from "../WKAvatar";
import AiBadge from "../AiBadge";
import ConversationVM from "../Conversation/vm";
export type ConvFilter = "all" | "human" | "ai" | "group" | "dm";

// ── CompactGroupItem：群聊 Tab 紧凑 item，支持拖拽 ──────────────────────
interface CompactGroupItemProps {
  conversationWrap: ConversationWrap;
  selected: boolean;
  avatarKey?: string;
  onClick: () => void;
  onDoubleClick?: (e: React.MouseEvent) => void;
  onContextMenu: (e: React.MouseEvent) => void;
  /** 该群聊有子区，需要在 # icon 下方画竖线 */
  hasThreads?: boolean;
  onToggleThreads?: (e: React.MouseEvent) => void;
  /** 折叠时子区的未读数（展开时为 0） */
  threadUnread?: number;
}

const CompactGroupItem: React.FC<CompactGroupItemProps> = ({
  conversationWrap,
  selected,
  avatarKey,
  onClick,
  onDoubleClick,
  onContextMenu,
  hasThreads,
  onToggleThreads,
  threadUnread = 0,
}) => {
  const totalUnread = conversationWrap.unread + threadUnread;
  const channelInfo = conversationWrap.channelInfo;
  // channelInfo 未加载时主动拉取，加载完触发 re-render
  React.useEffect(() => {
    if (!channelInfo) {
      WKSDK.shared().channelManager.fetchChannelInfo(conversationWrap.channel);
    }
  }, [conversationWrap.channel.channelID]);

  const isThread =
    conversationWrap.channel.channelType === ChannelTypeCommunityTopic;

  // effectiveMute 对齐后端 allowPush 降级逻辑：
  // - 子区有显式设置（thread.mute != null）→ 只看子区自身
  // - 子区未设置（thread.mute == null）→ 继承父群组 mute
  // - 群组：只看自身 mute
  const parentGroupNo = isThread
    ? (channelInfo?.orgData?.parentGroupNo as string | undefined)
    : undefined;
  const parentChannelInfo = parentGroupNo
    ? WKSDK.shared().channelManager.getChannelInfo(
        new Channel(parentGroupNo, ChannelTypeGroup)
      )
    : undefined;
  const threadRawMute = isThread
    ? (channelInfo?.orgData?.thread as any)?.mute as number | null | undefined
    : undefined;
  const effectiveMute = isThread
    ? threadRawMute != null
      ? threadRawMute === 1          // 显式设置：只看子区自身
      : !!(parentChannelInfo?.mute)  // 未设置：继承父群
    : !!(channelInfo?.mute);         // 群组：只看自身

  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
    useSortable({
      // 同一分组内 sort 与跨分组 move 共用 sortable id：item::<channelType>::<channelID>
      // handleDragEnd 通过 over 是 item / cat 判断分支。
      id: `item::${conversationWrap.channel.channelType}::${conversationWrap.channel.channelID}`,
      data: {
        type: "item",
        channelType: conversationWrap.channel.channelType,
        channelID: conversationWrap.channel.channelID,
        isThread,
      },
      // 子区跟随父频道，不独立拖拽 / 排序
      disabled: isThread,
    });

  const style: React.CSSProperties = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.4 : undefined,
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={classNames(
        "wk-conv-compact-item",
        selected ? "wk-conv-compact-item--selected" : undefined,
        totalUnread > 0 ? "wk-conv-compact-item--unread" : undefined,
        isThread ? "wk-conv-compact-item--thread" : undefined,
        isDragging ? "wk-conv-compact-item--dragging" : undefined,
        hasThreads ? "wk-conv-compact-item--has-threads" : undefined,
        effectiveMute ? "wk-conv-compact-item--muted" : undefined
      )}
      onClick={onClick}
      onDoubleClick={onDoubleClick}
      onMouseDown={
        onDoubleClick
          ? (e) => {
              if (e.detail >= 2) e.preventDefault();
            }
          : undefined
      }
      onContextMenu={onContextMenu}
    >
      {/* 拖拽 handle（非子区才显示） */}
      {!isThread && (
        <span
          className="wk-conv-compact-drag-handle"
          {...attributes}
          {...listeners}
          onClick={(e) => e.stopPropagation()}
        >
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none">
            <circle cx="3" cy="3" r="1.2" fill="currentColor" />
            <circle cx="7" cy="3" r="1.2" fill="currentColor" />
            <circle cx="3" cy="7" r="1.2" fill="currentColor" />
            <circle cx="7" cy="7" r="1.2" fill="currentColor" />
            <circle cx="3" cy="11" r="1.2" fill="currentColor" />
            <circle cx="7" cy="11" r="1.2" fill="currentColor" />
          </svg>
        </span>
      )}
      <span
        className={`wk-conv-compact-icon${
          totalUnread > 0 ? " wk-conv-compact-icon--reddot" : ""
        }`}
      >
        {isThread ? (
          <ThreadIcon size={13} />
        ) : (
          <WKAvatar
            key={avatarKey}
            channel={conversationWrap.channel}
          />
        )}
      </span>
      {conversationWrap.isMentionMe && totalUnread > 0 && (
        <span className="wk-mention-badge">@我</span>
      )}
      <span className="wk-conv-compact-name">
        {channelInfo?.orgData.displayName ? (
          channelInfo.orgData.displayName
        ) : isThread ? (
          <span className="wk-conv-compact-name-skeleton" />
        ) : (
          conversationWrap.channel.channelID
        )}
      </span>
      {conversationWrap.channel.channelType === ChannelTypeGroup &&
        channelInfo?.orgData?.is_external_group === 1 && (
          <span className="wk-conv-compact-external-badge" aria-label="外部群">
            外部
          </span>
        )}
      {effectiveMute && (
        <span className="wk-conv-compact-mute-icon">
          <svg
            className="icon"
            viewBox="0 0 1131 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            width="12"
            height="12"
          >
            <path
              d="M914.688 892.736L64 236.224l38.784-50.88L271.36 315.648a300.288 300.288 0 0 1 246.976-157.952v-33.28c0-16.64 13.504-30.08 30.08-30.08h2.304c16.576 0 30.08 13.44 30.08 30.08v32.96a299.776 299.776 0 0 1 284.928 299.136v294.272l45.504 58.624 48.768 37.696-45.312 45.632zM234.624 480.384l506.88 391.232H140.416l94.272-121.536-0.064-269.696z"
              fill="#bfbfbf"
            />
          </svg>
        </span>
      )}
      {totalUnread > 0 && !effectiveMute && (
        <span className="wk-conv-compact-badges">
          <span className="wk-conv-compact-badge">
            {totalUnread > 99 ? "99+" : totalUnread}
          </span>
        </span>
      )}
      {hasThreads && (
        <span
          className="wk-conv-compact-thread-tag"
          aria-label="展开/收起子区"
          onClick={(e) => {
            e.stopPropagation();
            onToggleThreads?.(e);
          }}
        >
          <svg width="13" height="13" viewBox="0 0 24 24" fill="#6569E8">
            <path d="M12 2.81a1 1 0 0 1 0-1.41l.36-.36a1 1 0 0 1 1.41 0l9.2 9.2a1 1 0 0 1 0 1.4l-.7.7a1 1 0 0 1-1.3.13l-9.54-6.72a1 1 0 0 1-.08-1.58l1-1L12 2.8ZM12 21.2a1 1 0 0 1 0 1.41l-.35.35a1 1 0 0 1-1.41 0l-9.2-9.19a1 1 0 0 1 0-1.41l.7-.7a1 1 0 0 1 1.3-.12l9.54 6.72a1 1 0 0 1 .07 1.58l-1 1 .35.36ZM15.66 16.8a1 1 0 0 1-1.38.28l-8.49-5.66A1 1 0 1 1 6.9 9.76l8.49 5.65a1 1 0 0 1 .27 1.39ZM17.1 14.25a1 1 0 1 0 1.11-1.66L9.73 6.93a1 1 0 0 0-1.11 1.66l8.49 5.66Z" />
          </svg>
        </span>
      )}
    </div>
  );
};
// ────────────────────────────────────────────────────────────────────────────

export interface ConversationListProps {
  conversations: ConversationWrap[];
  select?: Channel;
  /** 外部控制过滤，不传则内部默认 'all' */
  filter?: ConvFilter;
  /** 紧凑模式：隐藏头像/消息预览/时间戳，显示 # icon，用于群聊 Tab */
  compact?: boolean;
  onClick?: (conversation: ConversationWrap) => void;
  onClearMessages?: (channel: Channel) => void;
  /** 点击 "+N 个子区" 时的回调，传入父群组 ID */
  onThreadOverflowClick?: (groupNo: string) => void;
  /** 外部注入的额外右键菜单项，追加到内置菜单之后 */
  extraContextMenus?: (
    conversation: ConversationWrap | undefined
  ) => ContextMenusData[];
  /** 隐藏右键菜单的「关闭聊天窗口」项（关注 tab 不展示该入口，PM #337） */
  hideCloseChat?: boolean;
  /** 关闭按 channelInfo.top 拆分置顶 / 普通两段的渲染逻辑。
   *  关注 tab 里会话顺序由 /v2/follow/sort 决定（sidebar 给的 follow_sort），
   *  pin 只是标记不影响位置，关闭后保持 caller 传入的顺序原样渲染。 */
  disablePinSplit?: boolean;
  /** 隐藏「置顶/取消置顶」菜单项 + 行尾图钉图标。关注 tab 用手动拖拽排序，
   *  pin 与拖拽语义冲突（pin 会强制顶到分组顶端覆盖手动顺序），所以在关注 tab 移除 pin 入口。
   *  最近 tab 仍保留 pin。 */
  hidePin?: boolean;
}

export interface ConversationListState {
  selectConversationWrap?: ConversationWrap;
  /** compact 模式：已展开全部子区的父群聊 ID 集合（点击 +N 后加入） */
  expandedGroupIds: Set<string>;
}

export default class ConversationList extends Component<
  ConversationListProps,
  ConversationListState
> {
  channelListener!: ChannelInfoListener;
  contextMenusContext!: ContextMenusContext;
  typingListener!: TypingListener;
  private _storageKey(): string {
    const uid = WKApp.loginInfo?.uid || 'unknown';
    const spaceId = WKApp.shared?.currentSpaceId || 'default';
    return `wk-thread-expanded-groups_${uid}_${spaceId}`;
  }

  constructor(props: ConversationListProps) {
    super(props);

    // 从 localStorage 恢复上次的展开状态
    let restoredIds: Set<string>;
    try {
      const raw = localStorage.getItem(this._storageKey());
      const parsed = raw ? JSON.parse(raw) : null;
      restoredIds = Array.isArray(parsed) ? new Set<string>(parsed) : new Set();
    } catch {
      restoredIds = new Set();
    }
    this.state = { expandedGroupIds: restoredIds };
  }

  componentDidMount() {
    this.channelListener = (channelInfo: ChannelInfo) => {
      this.setState({});
    };
    WKSDK.shared().channelManager.addListener(this.channelListener);

    this.typingListener = (channel: Channel, add: boolean) => {
      this.setState({});
    };
    TypingManager.shared.addTypingListener(this.typingListener);
  }

  componentWillUnmount() {
    WKSDK.shared().channelManager.removeListener(this.channelListener);
    TypingManager.shared.removeTypingListener(this.typingListener);
  }

  _handleScroll = () => {
    this.contextMenusContext.hide();
  };

  _toggleGroupExpand = (groupId: string) => {
    this.setState(
      (s) => {
        const next = new Set(s.expandedGroupIds);
        if (next.has(groupId)) next.delete(groupId);
        else next.add(groupId);
        return { expandedGroupIds: next };
      },
      () => {
        // setState 回调中执行副作用，避免在 updater 纯函数内写 localStorage
        try {
          localStorage.setItem(
            this._storageKey(),
            JSON.stringify([...this.state.expandedGroupIds])
          );
        } catch {
          // localStorage 不可用时静默忽略
        }
      }
    );
  };

  _handleContextMenu(
    conversationWrap: ConversationWrap,
    event: React.MouseEvent
  ) {
    this.contextMenusContext.show(event);
    this.setState({
      selectConversationWrap: conversationWrap,
    });
  }

  _getTypingUI(conversationWrap: ConversationWrap) {
    const { select } = this.props;
    const typing = TypingManager.shared.getTyping(conversationWrap.channel);
    const selected = select && select.isEqual(conversationWrap.channel);
    return (
      <div className="wk-typing">
        <BeatLoader
          size={4}
          margin={3}
          color={selected ? "white" : "var(--wk-color-theme)"}
        />
        &nbsp;&nbsp;
        {conversationWrap.channel.channelType !== ChannelTypePerson
          ? typing?.fromName
          : ""}
        正在输入
      </div>
    );
  }

  lastContent(conversationWrap: ConversationWrap) {
    if (!conversationWrap.lastMessage) {
      return;
    }
    const draft = conversationWrap.remoteExtra.draft;
    if (draft && draft !== "") {
      return draft;
    }
    // 检查是否有进行中的 AI 折叠 session
    const foldPreview = ConversationVM.foldSessionPreview.get(
      conversationWrap.channel.getChannelKey()
    );
    if (foldPreview) {
      return (
        <span className="wk-ai-collab-preview">
          <span className="wk-ai-collab-tag">
            <span className="wk-ai-collab-pulse" />
            AI协作中
          </span>
          <span className="wk-ai-collab-text">
            {foldPreview.participants.join(" × ")} · {foldPreview.count}条
          </span>
        </span>
      );
    }
    const lastMessage = new MessageWrap(conversationWrap.lastMessage);
    if (lastMessage.isDeleted) {
      return "";
    }
    if (lastMessage.revoke) {
      return RevokeCell.tip(lastMessage);
    }
    if (lastMessage.flame) {
      return FlameMessageCell.tip(lastMessage);
    }
    if (lastMessage.channel.channelType === ChannelTypePerson) {
      return lastMessage.content?.conversationDigest;
    } else {
      // 群组和子区频道都显示发送者名称
      let from = "";
      if (lastMessage.fromUID && lastMessage.fromUID !== "") {
        const fromChannel = new Channel(lastMessage.fromUID, ChannelTypePerson);
        const fromChannelInfo =
          WKSDK.shared().channelManager.getChannelInfo(fromChannel);
        if (fromChannelInfo) {
          from = `${fromChannelInfo.title}: `;
        } else {
          WKSDK.shared().channelManager.fetchChannelInfo(fromChannel);
        }
      }

      return `${from}${lastMessage.content?.conversationDigest || ""}`;
    }
  }

  getOnlineTip(channelInfo: ChannelInfo) {
    if (channelInfo.online) {
      return undefined;
    }
    const nowTime = new Date().getTime() / 1000;
    const btwTime = nowTime - channelInfo.lastOffline;
    if (btwTime < 60) {
      return "刚刚";
    }
    return `${(btwTime / 60).toFixed(0)}分钟`;
  }

  // 是否需要显示在线状态
  needShowOnlineStatus(channelInfo?: ChannelInfo) {
    if (!channelInfo) {
      return false;
    }
    if (channelInfo.online) {
      return true;
    }
    const nowTime = new Date().getTime() / 1000;
    const btwTime = nowTime - channelInfo.lastOffline;
    if (btwTime > 0 && btwTime < 60 * 60) {
      // 小于1小时才显示
      return true;
    }
    return false;
  }

  conversationItem(
    conversationWrap: ConversationWrap,
    hasThreads = false,
    threadUnread = 0
  ) {
    let channelInfo = conversationWrap.channelInfo;
    if (!channelInfo) {
      WKSDK.shared().channelManager.fetchChannelInfo(conversationWrap.channel);
    }

    const { compact } = this.props;
    const avatarKey = WKApp.shared.getChannelAvatarTag(
      conversationWrap.channel
    );
    const isThread =
      conversationWrap.channel.channelType === ChannelTypeCommunityTopic;
    // 非 compact（最近 tab）下子区按时间扁平展示，需要拿父频道 displayName 做面包屑、
    // 父频道头像作为左侧图标。父群信息可能还没拉到，主动 fetch，依赖 channelListener 触发重渲染。
    const parentGroupNo = isThread
      ? (channelInfo?.orgData?.parentGroupNo as string | undefined) ||
        parseThreadChannelId(conversationWrap.channel.channelID)?.groupNo
      : undefined;
    const parentChannel = parentGroupNo
      ? new Channel(parentGroupNo, ChannelTypeGroup)
      : undefined;
    const parentChannelInfo = parentChannel
      ? WKSDK.shared().channelManager.getChannelInfo(parentChannel)
      : undefined;
    if (parentChannel && !parentChannelInfo) {
      WKSDK.shared().channelManager.fetchChannelInfo(parentChannel);
    }

    // ── Compact 模式（群聊 Tab）：用 CompactGroupItem 函数组件（支持拖拽） ──
    if (compact) {
      const selected = !!(
        this.props.select && this.props.select.isEqual(conversationWrap.channel)
      );
      return (
        <CompactGroupItem
          key={conversationWrap.channel.getChannelKey()}
          conversationWrap={conversationWrap}
          selected={selected}
          avatarKey={avatarKey}
          hasThreads={hasThreads}
          threadUnread={threadUnread}
          onClick={() => {
            if (this.props.onClick) this.props.onClick(conversationWrap);
          }}
          onDoubleClick={
            conversationWrap.channel.channelType === ChannelTypeGroup &&
            hasThreads
              ? (e) => {
                  e.preventDefault();
                  this._toggleGroupExpand(conversationWrap.channel.channelID);
                }
              : undefined
          }
          onToggleThreads={
            conversationWrap.channel.channelType === ChannelTypeGroup &&
            hasThreads
              ? (e) => {
                  e.preventDefault();
                  this._toggleGroupExpand(conversationWrap.channel.channelID);
                }
              : undefined
          }
          onContextMenu={(e) => {
            this._handleContextMenu(conversationWrap, e);
          }}
        />
      );
    }

    const { select, onClick } = this.props;
    const typing = TypingManager.shared.getTyping(conversationWrap.channel);
    const selected = select && select.isEqual(conversationWrap.channel);
    // 父群下的子区折叠到 thread-overflow（默认不展开），父群 badge 必须把
    // 折叠中的子区未读一起显示，否则会出现「角标显示 N 但列表里看不到任何未读」。
    // 已展开时 threadUnread 由调用处传 0，自然只显示父群自身。
    const totalUnread = conversationWrap.unread + threadUnread;
    // 子区静音继承父群（与 CompactGroupItem 保持一致）：显式设置看自身，未设置看父群
    const threadRawMute = isThread
      ? ((channelInfo?.orgData?.thread as any)?.mute as number | null | undefined)
      : undefined;
    const effectiveMute = isThread
      ? threadRawMute != null
        ? threadRawMute === 1
        : !!parentChannelInfo?.mute
      : !!channelInfo?.mute;
    // 非 compact 下子区按 design v3.1 走扁平时间序，左侧用父频道头像，
    // 不再套 .wk-conversationlist-item-thread（避免缩进 + 树形连接线视觉嵌套）。
    const avatarChannel = isThread && parentChannel ? parentChannel : conversationWrap.channel;
    const isDM = avatarChannel.channelType === ChannelTypePerson;
    return (
      <div
        key={conversationWrap.channel.getChannelKey()}
        onClick={() => {
          if (onClick) {
            onClick(conversationWrap);
          }
        }}
        className={classNames(
          "wk-conversationlist-item",
          selected ? "wk-conversationlist-item-selected" : undefined,
          channelInfo?.top ? "wk-conversationlist-item-top" : undefined,
          totalUnread > 0
            ? "wk-conversationlist-item-unread"
            : undefined,
          effectiveMute ? "wk-conversationlist-item-muted" : undefined,
          isDM ? "wk-conversationlist-item-dm" : undefined
        )}
        onContextMenu={(e) => {
          this._handleContextMenu(conversationWrap, e);
        }}
      >
        <div className="wk-conversationlist-item-content">
          <div className="wk-conversationlist-item-left">
            <div className="wk-conversationlist-item-avatar-box">
              <WKAvatar
                channel={avatarChannel}
                key={avatarKey}
              ></WKAvatar>
              {hasThreads && (
                <div className="wk-conv-group-hash-badge">
                  <GroupIcon size={10} />
                </div>
              )}
              {!isThread && channelInfo && this.needShowOnlineStatus(channelInfo) ? (
                <OnlineStatusBadge
                  tip={this.getOnlineTip(channelInfo)}
                ></OnlineStatusBadge>
              ) : undefined}
              {totalUnread > 0 && !effectiveMute && (
                <span className="wk-conv-unread-num">
                  {totalUnread > 99 ? "99+" : totalUnread}
                </span>
              )}
              {totalUnread > 0 && effectiveMute && (
                <span className="wk-conv-unread-dot" />
              )}
            </div>
          </div>
          <div className="wk-conversationlist-item-right">
            {isThread && parentChannelInfo?.orgData?.displayName && (
              <div className="wk-conv-breadcrumb">
                {parentChannelInfo.orgData.displayName}
              </div>
            )}
            <div className="wk-conversationlist-item-right-first-line">
              <div className="wk-conversationlist-item-name">
                <h3>
                  {conversationWrap.channel.channelType ===
                    ChannelTypeCommunityTopic && (
                    <ThreadIcon
                      size={13}
                      className="wk-conv-channel-icon wk-conv-thread-icon"
                    />
                  )}
                  {channelInfo?.orgData.displayName}
                </h3>
                {conversationWrap.channel.channelType === ChannelTypeGroup &&
                  channelInfo?.orgData?.is_external_group === 1 && (
                    <Tag
                      size="small"
                      color="purple"
                      className="wk-conversationlist-item-external-tag"
                    >
                      外部
                    </Tag>
                  )}
                {channelInfo?.orgData?.robot === 1 && <AiBadge />}
                {channelInfo?.orgData.identityIcon ? (
                  <img
                    style={{
                      width: channelInfo?.orgData?.identitySize.width,
                      height: channelInfo?.orgData?.identitySize.height,
                    }}
                    src={channelInfo?.orgData.identityIcon}
                  ></img>
                ) : undefined}
                {effectiveMute && (
                  <span className="wk-conv-mute-icon" aria-label="免打扰">
                    <svg
                      viewBox="0 0 1131 1024"
                      width="11"
                      height="11"
                    >
                      <path
                        d="M914.688 892.736L64 236.224l38.784-50.88L271.36 315.648a300.288 300.288 0 0 1 246.976-157.952v-33.28c0-16.64 13.504-30.08 30.08-30.08h2.304c16.576 0 30.08 13.44 30.08 30.08v32.96a299.776 299.776 0 0 1 284.928 299.136v294.272l45.504 58.624 48.768 37.696-45.312 45.632zM234.624 480.384l506.88 391.232H140.416l94.272-121.536-0.064-269.696z"
                        fill="currentColor"
                      ></path>
                    </svg>
                  </span>
                )}
                <div className="wk-conversationlist-item-time">
                  <span>
                    {getTimeStringAutoShort2(
                      conversationWrap.timestamp * 1000,
                      true
                    )}
                  </span>
                </div>
              </div>
            </div>
            <div className="wk-conversationlist-item-right-second-line">
              <div className="wk-conversationlist-item-lastmsg">
                {!typing ? (
                  <label
                    className="wk-reminder"
                    style={{
                      display: conversationWrap.remoteExtra.draft
                        ? undefined
                        : "none",
                    }}
                  >
                    [草稿]
                  </label>
                ) : undefined}
                {conversationWrap.simpleReminders &&
                !typing &&
                conversationWrap.simpleReminders.length > 0
                  ? conversationWrap.simpleReminders
                      .filter((r) => r.done === false)
                      .map((r) => {
                        return (
                          <label key={r.reminderID} className="wk-reminder">
                            {r.text}
                          </label>
                        );
                      })
                  : undefined}
                {/* 静音 + 多条未读：预览前置 [N 条] 红字提示（design v3.1 低打扰） */}
                {effectiveMute && totalUnread > 1 && !typing && (
                  <span className="wk-conv-count-hint">
                    [{totalUnread > 99 ? "99+" : totalUnread} 条]
                  </span>
                )}
                {typing
                  ? this._getTypingUI(conversationWrap)
                  : this.lastContent(conversationWrap)}
              </div>
              {conversationWrap.isMentionMe && totalUnread > 0 && !effectiveMute && (
                <span className="wk-mention-badge">@我</span>
              )}
            </div>
          </div>
        </div>
      </div>
    );
  }

  onTop(channelInfo: ChannelInfo) {
    ChannelSettingManager.shared.top(!channelInfo.top, channelInfo.channel);
  }

  onMute(channelInfo: ChannelInfo) {
    this.onMuteWithValue(!channelInfo.mute, channelInfo)
  }

  onMuteWithValue(value: boolean, channelInfo: ChannelInfo) {
    ChannelSettingManager.shared.mute(value, channelInfo.channel)
      .then(() => {
        // 直接重拉（不删缓存），新数据覆盖旧缓存，避免删除期间出现 loading 骨架
        WKSDK.shared().channelManager.fetchChannelInfo(channelInfo.channel)
          .then(() => this.setState({}))
      })
  }

  onCloseChat(channel: Channel) {
    // 关闭聊天
    WKApp.conversationProvider.deleteConversation(channel);
  }

  async onClearMessages(channel: Channel) {
    if (this.props.onClearMessages) {
      this.props.onClearMessages(channel);
    }
  }

  filterConversation(conv: ConversationWrap): boolean {
    const filter = this.props.filter ?? "all";
    if (filter === "all") return true;
    const channelInfo = conv.channelInfo;
    // 群组和子区频道都归类到 group 过滤器
    if (filter === "group")
      return (
        conv.channel.channelType === ChannelTypeGroup ||
        conv.channel.channelType === ChannelTypeCommunityTopic
      );
    // dm = human + ai（私聊 Tab 包含所有私聊会话）
    if (filter === "dm") return conv.channel.channelType === ChannelTypePerson;
    if (filter === "ai") {
      if (conv.channel.channelType !== ChannelTypePerson) return false;
      // channelInfo 未加载时隐藏，等 channelInfoListener 触发重渲后再显示
      if (!channelInfo) return false;
      return channelInfo.orgData?.robot === 1;
    }
    if (filter === "human") {
      if (conv.channel.channelType !== ChannelTypePerson) return false;
      // channelInfo 未加载时暂时归入 human，channelInfoListener 更新后自动修正
      if (!channelInfo) return true;
      return channelInfo.orgData?.robot !== 1;
    }
    return true;
  }

  // 收集每个父群组下的子区列表（不重排），compact 模式下基于此再做嵌套；
  // 非 compact（最近 tab）只用它给父群组打 hash badge / 算菜单展开态。
  private buildThreadsByParent(
    convs: ConversationWrap[]
  ): Map<string, ConversationWrap[]> {
    const threadsByParent = new Map<string, ConversationWrap[]>();
    for (const conv of convs) {
      if (conv.channel.channelType === ChannelTypeCommunityTopic) {
        const parentGroupNo =
          conv.channelInfo?.orgData?.parentGroupNo ||
          parseThreadChannelId(conv.channel.channelID)?.groupNo;
        if (parentGroupNo) {
          const list = threadsByParent.get(parentGroupNo) || [];
          list.push(conv);
          threadsByParent.set(parentGroupNo, list);
        }
      }
    }
    return threadsByParent;
  }

  // 将子区放在父群组后面，默认全部收起（MAX_VISIBLE_THREADS=0），展开后显示全部
  private groupThreadsWithParent(convs: ConversationWrap[]): {
    items: Array<
      | ConversationWrap
      | {
          type: "thread-overflow";
          parentGroupId: string;
          count: number;
          unreadCount: number;
        }
    >;
    threadsByParent: Map<string, ConversationWrap[]>;
  } {
    const MAX_VISIBLE_THREADS = 0;

    // 分离群组和子区
    const threads: ConversationWrap[] = [];

    for (const conv of convs) {
      if (conv.channel.channelType === ChannelTypeCommunityTopic) {
        threads.push(conv);
      }
    }

    // 按父群组分组子区
    const threadsByParent = new Map<string, ConversationWrap[]>();
    for (const thread of threads) {
      const parentGroupNo =
        thread.channelInfo?.orgData?.parentGroupNo ||
        parseThreadChannelId(thread.channel.channelID)?.groupNo;
      if (parentGroupNo) {
        const list = threadsByParent.get(parentGroupNo) || [];
        list.push(thread);
        threadsByParent.set(parentGroupNo, list);
      }
    }

    // 重新组织：群组后面跟着其子区（默认全收起）
    const result: Array<
      | ConversationWrap
      | {
          type: "thread-overflow";
          parentGroupId: string;
          count: number;
          unreadCount: number;
        }
    > = [];
    const usedThreads = new Set<string>();

    for (const conv of convs) {
      if (conv.channel.channelType === ChannelTypeCommunityTopic) {
        // 子区会在父群组后面添加，这里跳过
        continue;
      }
      result.push(conv);
      // 如果是群组，子区默认全部折叠进 overflow
      if (conv.channel.channelType === ChannelTypeGroup) {
        const groupThreads = threadsByParent.get(conv.channel.channelID) || [];
        const visibleThreads = groupThreads.slice(0, MAX_VISIBLE_THREADS);
        const overflowCount = groupThreads.length - MAX_VISIBLE_THREADS;

        // 标记所有已分组的子区（包括溢出的）为已使用
        for (const thread of groupThreads) {
          usedThreads.add(thread.channel.channelID);
        }

        for (const thread of visibleThreads) {
          result.push(thread);
        }

        // 如果有超出的子区，添加溢出提示
        if (overflowCount > 0) {
          // 计算溢出子区的总未读数
          const overflowThreads = groupThreads.slice(MAX_VISIBLE_THREADS);
          const overflowUnread = overflowThreads.reduce(
            (sum, t) => sum + t.unread,
            0
          );

          result.push({
            type: "thread-overflow",
            parentGroupId: conv.channel.channelID,
            count: overflowCount,
            unreadCount: overflowUnread,
          });
        }
      }
    }

    // 收集列表中存在的群组 ID
    const groupIdsInList = new Set(
      convs
        .filter((c) => c.channel.channelType === ChannelTypeGroup)
        .map((c) => c.channel.channelID)
    );

    // 孤儿子区：父群组在列表中但未被分组的先显示，父群组不在列表中的隐藏
    for (const thread of threads) {
      if (!usedThreads.has(thread.channel.channelID)) {
        const parentGroupNo =
          thread.channelInfo?.orgData?.parentGroupNo ||
          parseThreadChannelId(thread.channel.channelID)?.groupNo;
        if (parentGroupNo && groupIdsInList.has(parentGroupNo)) {
          // 父群组在列表中但子区未被分组（理论上不应该出现）
          result.push(thread);
        }
        // 父群组不在列表中（已退出等）：隐藏
      }
    }

    return { items: result, threadsByParent };
  }

  render() {
    const { conversations, select, compact } = this.props;
    const { selectConversationWrap } = this.state;

    const filtered =
      conversations?.filter((c) => this.filterConversation(c)) ?? [];

    // compact（关注 tab）：把子区按父群嵌套；
    // 非 compact（最近 tab，design v3.1）：扁平按时间序，子区作为独立条目自带父频道面包屑。
    type GroupedItem =
      | ConversationWrap
      | {
          type: "thread-overflow";
          parentGroupId: string;
          count: number;
          unreadCount: number;
        };
    let grouped: GroupedItem[];
    let threadsByParent: Map<string, ConversationWrap[]>;
    if (compact) {
      const r = this.groupThreadsWithParent(filtered);
      grouped = r.items;
      threadsByParent = r.threadsByParent;
    } else {
      grouped = filtered;
      threadsByParent = this.buildThreadsByParent(filtered);
    }
    const groupedPinned = grouped.filter((item) => {
      if ("type" in item) return false;
      return (item as ConversationWrap).channelInfo?.top;
    });

    // 子区和溢出提示跟随父群组：如果父群组被置顶，把它的子区也移到置顶区
    const pinnedGroupIds = new Set(
      groupedPinned
        .filter(
          (item) =>
            !("type" in item) &&
            (item as ConversationWrap).channel.channelType === ChannelTypeGroup
        )
        .map((item) => (item as ConversationWrap).channel.channelID)
    );
    const finalPinned: typeof grouped = [];
    const finalRecent: typeof grouped = [];
    if (this.props.disablePinSplit) {
      // 关注 tab 顺序由 sidebar 的 follow_sort 决定，不按 pin 状态拆段
      finalRecent.push(...grouped);
    } else {
    for (const item of grouped) {
      if ("type" in item) {
        // thread-overflow 跟随父群组
        if (pinnedGroupIds.has(item.parentGroupId)) {
          finalPinned.push(item);
        } else {
          finalRecent.push(item);
        }
      } else {
        const conv = item as ConversationWrap;
        if (conv.channelInfo?.top) {
          finalPinned.push(item);
        } else if (compact && conv.channel.channelType === ChannelTypeCommunityTopic) {
          // compact 嵌套语义：子区跟随父群组
          const parentGroupNo =
            conv.channelInfo?.orgData?.parentGroupNo ||
            parseThreadChannelId(conv.channel.channelID)?.groupNo;
          if (parentGroupNo && pinnedGroupIds.has(parentGroupNo)) {
            finalPinned.push(item);
          } else {
            finalRecent.push(item);
          }
        } else {
          finalRecent.push(item);
        }
      }
    }
    }

    const { onThreadOverflowClick } = this.props;
    const { expandedGroupIds } = this.state;

    const renderItem = (
      item:
        | ConversationWrap
        | {
            type: "thread-overflow";
            parentGroupId: string;
            count: number;
            unreadCount: number;
          }
    ) => {
      if ("type" in item && item.type === "thread-overflow") {
        // 展开/收起由双击群组行触发，不显示「+N 个子区」控件
        const isExpanded = expandedGroupIds.has(item.parentGroupId);
        if (!isExpanded) return null;
        const extraThreads = threadsByParent.get(item.parentGroupId) ?? [];
        return (
          <React.Fragment key={`overflow-${item.parentGroupId}`}>
            {extraThreads.map((conv) => {
              const selected = !!(
                this.props.select && this.props.select.isEqual(conv.channel)
              );
              return (
                <CompactGroupItem
                  key={conv.channel.getChannelKey()}
                  conversationWrap={conv}
                  selected={selected}
                  onClick={() => {
                    if (this.props.onClick) this.props.onClick(conv);
                  }}
                  onContextMenu={(e) => {
                    this._handleContextMenu(conv, e);
                  }}
                />
              );
            })}
          </React.Fragment>
        );
      }
      const conv = item as ConversationWrap;

      // compact 模式展开：已展开分组的子区（overflow 之后的部分）是否显示
      // 注意：groupThreadsWithParent 只给前 2 个，overflow 的在 threadsByParent 里
      // 这里 item 已经是 groupThreadsWithParent 处理后的，超出的不在 items 里
      // 所以只需要在 overflow click 后渲染额外的子区
      // → 用 expandedGroupIds 控制是否渲染 threadsByParent 里超出 MAX 的部分（见下方额外渲染）

      const hasThreads =
        conv.channel.channelType === ChannelTypeGroup &&
        threadsByParent.has(conv.channel.channelID);
      const threadUnread = (() => {
        if (!hasThreads) return 0;
        const isExpanded = expandedGroupIds.has(conv.channel.channelID);
        if (isExpanded) return 0;
        const threads = threadsByParent.get(conv.channel.channelID) ?? [];
        // 子区有独立免打扰设置，汇总时过滤掉已开启免打扰的子区未读
        return threads.reduce((sum, t) => {
          // 对齐 effectiveMute 逻辑：显式设置看自身，未设置继承父群
          const rawMute = (t.channelInfo?.orgData?.thread as any)?.mute as number | null | undefined
          const parentInfo = WKSDK.shared().channelManager.getChannelInfo(
            new Channel(conv.channel.channelID, ChannelTypeGroup)
          )
          const tMute = rawMute != null ? rawMute === 1 : !!(parentInfo?.mute)
          return sum + (tMute ? 0 : t.unread)
        }, 0);
      })();
      return this.conversationItem(conv, hasThreads, threadUnread);
    };

    return (
      <div
        id="wk-conversationlist"
        className="wk-conversationlist"
        onScroll={this._handleScroll}
      >
        {finalPinned.map(renderItem)}
        {finalRecent.map(renderItem)}

        <ContextMenus
          onContext={(ctx) => {
            this.contextMenusContext = ctx;
          }}
          menus={(() => {
            const conv = selectConversationWrap;
            const channelInfo = conv?.channelInfo;
            const channel = conv?.channel;
            const extraMenus = this.props.extraContextMenus
              ? this.props.extraContextMenus(conv)
              : [];

            const menus: any[] = [];

            // 1. 标为已读（有未读时显示）
            if (conv && conv.unread > 0) {
              menus.push({
                title: "标为已读",
                icon: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z",
                onClick: () => {
                  if (!channel) return;
                  WKApp.apiClient.put("conversation/clearUnread", {
                    channel_id: channel.channelID,
                    channel_type: channel.channelType,
                    unread: 0,
                  });
                },
              });
            }

            // 2. 关闭聊天窗口
            if (!this.props.hideCloseChat) {
              menus.push({
                title: "关闭聊天窗口",
                icon: "M18 6 6 18 M6 6l12 12",
                onClick: () => {
                  if (!channel) return;
                  Modal.confirm({
                    title: "确认关闭",
                    content: "确定要关闭此聊天窗口吗？",
                    okText: "确定",
                    cancelText: "取消",
                    onOk: () => {
                      this.onCloseChat(channel);
                    },
                  });
                },
              });
            }

            // 3. 额外菜单项（移出分组 / 移到分组等，由上层通过 extraContextMenus 传入）
            if (extraMenus.length > 0) {
              menus.push(...extraMenus);
            }

            // 4. 置顶 / 取消置顶（子区不显示；关注 tab 用 hidePin 关闭）
            if (channel?.channelType !== ChannelTypeCommunityTopic && !this.props.hidePin) {
              menus.push({
                title: channelInfo?.top ? "取消置顶" : "置顶聊天",
                icon: channelInfo?.top
                  ? "M12 17v5 M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89 M2 2l20 20 M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"
                  : "M12 17v5 M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z",
                onClick: () => {
                  if (channelInfo) this.onTop(channelInfo);
                },
              });
            }

            // 5. 免打扰 / 关闭免打扰
            // 菜单标题跟 effectiveMute（用户看到的静音状态）保持一致：
            // 子区：有显式设置看自身；未设置继承父群
            // 群组：只看自身
            const menuIsThread = channel?.channelType === ChannelTypeCommunityTopic
            const menuParentGroupNo = menuIsThread
              ? (channelInfo?.orgData?.parentGroupNo as string | undefined)
              : undefined
            const menuParentChannelInfo = menuParentGroupNo
              ? WKSDK.shared().channelManager.getChannelInfo(new Channel(menuParentGroupNo, ChannelTypeGroup))
              : undefined
            const menuRawMute = menuIsThread
              ? (channelInfo?.orgData?.thread as any)?.mute as number | null | undefined
              : undefined
            const menuEffectiveMute = menuIsThread
              ? menuRawMute != null ? menuRawMute === 1 : !!(menuParentChannelInfo?.mute)
              : !!(channelInfo?.mute)
            menus.push({
              title: menuEffectiveMute ? "关闭免打扰" : "开启免打扰",
              icon: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9 M13.73 21a2 2 0 0 1-3.46 0",
              onClick: () => {
                if (channelInfo) this.onMuteWithValue(!menuEffectiveMute, channelInfo);
              },
            });

            // 6. 展开/收起子区（compact 模式下、群组且有子区时显示）
            if (
              compact &&
              channel &&
              channel.channelType === ChannelTypeGroup &&
              threadsByParent.has(channel.channelID)
            ) {
              const isExpanded = expandedGroupIds.has(channel.channelID);
              menus.push({
                title: isExpanded ? "收起子区" : "展开子区",
                icon: isExpanded ? "M19 9l-7 7-7-7" : "M5 15l7-7 7 7",
                onClick: () => {
                  this._toggleGroupExpand(channel.channelID);
                },
              });
            }

            // 7. 分隔线
            menus.push({ separator: true } as ContextMenusData);

            // 8. 清空聊天记录 / 关闭并清空
            // 子区：直接展开到顶层
            // 群组：保留在「更多」子菜单里
            // hideCloseChat（关注 tab）下只保留「仅清空记录」的项，否则即便上面隐藏了
            // 显式「关闭聊天窗口」项，「更多 → 关闭窗口并清空」仍能让用户关掉关注的会话。
            const clearItems: ContextMenusData[] = [
              {
                title: "清空聊天记录",
                danger: true,
                onClick: () => {
                  if (!channel) return;
                  Modal.confirm({
                    title: "确认清空",
                    content: "确定要清空所有聊天记录吗？此操作不可撤销。",
                    okText: "确定",
                    cancelText: "取消",
                    onOk: () => {
                      this.onClearMessages(channel);
                    },
                  });
                },
              },
            ];
            if (!this.props.hideCloseChat) {
              clearItems.push({
                title: "关闭窗口并清空记录",
                danger: true,
                onClick: () => {
                  if (!channel) return;
                  Modal.confirm({
                    title: "确认关闭并清空",
                    content:
                      "确定要关闭窗口并清空所有聊天记录吗？此操作不可撤销。",
                    okText: "确定",
                    cancelText: "取消",
                    onOk: () => {
                      this.onCloseChat(channel);
                      this.onClearMessages(channel);
                    },
                  });
                },
              });
            }

            if (channel?.channelType === ChannelTypeCommunityTopic) {
              menus.push(...clearItems);
            } else {
              menus.push({
                title: "更多",
                icon: "M12 12m-1 0a1 1 0 1 0 2 0 1 1 0 1 0-2 0 M12 5m-1 0a1 1 0 1 0 2 0 1 1 0 1 0-2 0 M12 19m-1 0a1 1 0 1 0 2 0 1 1 0 1 0-2 0",
                children: clearItems,
              });
            }

            return menus;
          })()}
        />
      </div>
    );
  }
}

interface OnlineStatusBadgeProps {
  tip?: string;
}
export class OnlineStatusBadge extends Component<OnlineStatusBadgeProps> {
  render(): React.ReactNode {
    const { tip } = this.props;
    return (
      <div
        className={classNames(
          "wk-onlinestatusbadge",
          !tip ? "wk-onlinestatusbadge-empty" : undefined
        )}
      >
        <div className="wk-onlinestatusbadge-content">
          <div className="wk-onlinestatusbadge-content-tip">{tip}</div>
        </div>
      </div>
    );
  }
}
