/**
 * ZeqTextTab — Full Messenger UI for Chrome Extension Side Panel
 * Ported from apps/zeq-encrypted-text/ UI logic.
 * WhatsApp/Telegram-style: contact sidebar + chat area + input bar.
 *
 * @version 1.287.3
 * @license CC BY 4.0 — Zeq. H / Zeq 1.287 Hz Limited
 */
const ZeqTextTab = (() => {
  'use strict';

  let container = null;
  let state = {
    contacts: [],
    activeChat: null,
    messages: {},
    unread: {},
    typing: {},
    presence: {},
  };
  let typingTimeout = null;

  // ── DOM refs ──────────────────────────────────────────────────────────────
  let $sidebar, $chatArea, $contactList, $searchInput, $chatHeader,
      $messageList, $inputArea, $msgInput, $sendBtn, $emptyChat,
      $typingIndicator, $addContactModal, $sidebarOverlay;

  // ── Init ──────────────────────────────────────────────────────────────────
  function init(containerId) {
    container = document.getElementById(containerId);
    if (!container) return;
    container.innerHTML = '';
    _buildHTML();
    _bindEvents();
    _loadContacts();

    // Wire ZeqTextClient events
    ZeqTextClient.on('message', _onMessage);
    ZeqTextClient.on('messageSent', _onMessageSent);
    ZeqTextClient.on('typing', _onTyping);
    ZeqTextClient.on('readReceipt', _onReadReceipt);
    ZeqTextClient.on('presence', _onPresence);
    ZeqTextClient.on('contactsChanged', _loadContacts);
  }

  function destroy() {
    ZeqTextClient.off('message', _onMessage);
    ZeqTextClient.off('messageSent', _onMessageSent);
    ZeqTextClient.off('typing', _onTyping);
    ZeqTextClient.off('readReceipt', _onReadReceipt);
    ZeqTextClient.off('presence', _onPresence);
    ZeqTextClient.off('contactsChanged', _loadContacts);
    if (container) container.innerHTML = '';
  }

  // ── Build HTML ────────────────────────────────────────────────────────────
  function _buildHTML() {
    container.innerHTML = `
      <div class="zt-app">
        <div class="zt-sidebar" id="zt-sidebar">
          <div class="zt-sidebar-header">
            <div class="zt-sidebar-title">Messages</div>
            <button class="zt-btn-icon" id="zt-btn-add-contact" title="Add Contact" style="gap:4px;font-size:11px;color:var(--accent-cyan)">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
              <span>Add</span>
            </button>
          </div>
          <div class="zt-search-wrap">
            <input type="text" class="zt-search" id="zt-search" placeholder="Search contacts..." autocomplete="off" />
          </div>
          <div class="zt-contact-list" id="zt-contact-list"></div>
        </div>
        <div class="zt-sidebar-overlay" id="zt-sidebar-overlay"></div>
        <div class="zt-chat" id="zt-chat-area">
          <div class="zt-empty-chat" id="zt-empty-chat">
            <div class="zt-empty-icon">
              <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="rgba(34,211,238,0.3)" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
            </div>
            <div class="zt-empty-title">Zeq Encrypted Text</div>
            <div class="zt-empty-sub">Select a contact to start messaging</div>
          </div>
          <div class="zt-chat-header" id="zt-chat-header" style="display:none">
            <button class="zt-btn-icon zt-btn-back" id="zt-btn-back" title="Back">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
            </button>
            <div class="zt-chat-avatar" id="zt-chat-avatar"></div>
            <div class="zt-chat-info">
              <div class="zt-chat-name" id="zt-chat-name"></div>
              <div class="zt-chat-status" id="zt-chat-status"></div>
            </div>
          </div>
          <div class="zt-message-list" id="zt-message-list" style="display:none"></div>
          <div class="zt-typing-indicator" id="zt-typing-indicator" style="display:none">
            <span class="zt-typing-dots"><span></span><span></span><span></span></span>
            <span class="zt-typing-text"></span>
          </div>
          <div class="zt-input-area" id="zt-input-area" style="display:none">
            <textarea class="zt-msg-input" id="zt-msg-input" placeholder="Type a message..." rows="1"></textarea>
            <button class="zt-btn-send" id="zt-btn-send" disabled>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
            </button>
          </div>
        </div>
      </div>
      <div class="zt-modal" id="zt-add-contact-modal">
        <div class="zt-modal-content">
          <div class="zt-modal-header">
            <span>Add Contact</span>
            <button class="zt-btn-icon" id="zt-btn-close-add">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </div>
          <div class="zt-modal-body">
            <input type="text" class="zt-search" id="zt-add-search" placeholder="Search by ZID or name..." autocomplete="off" />
            <div class="zt-add-results" id="zt-add-results"></div>
            <div class="zt-add-direct">
              <div class="zt-add-direct-label">Or add by ZID directly:</div>
              <div class="zt-add-direct-row">
                <input type="text" class="zt-search" id="zt-add-zid" placeholder="ZEQ07XXXXXXXXX" />
                <button class="zt-btn-primary" id="zt-btn-add-zid">Add</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    `;

    $sidebar = container.querySelector('#zt-sidebar');
    $chatArea = container.querySelector('#zt-chat-area');
    $contactList = container.querySelector('#zt-contact-list');
    $searchInput = container.querySelector('#zt-search');
    $chatHeader = container.querySelector('#zt-chat-header');
    $messageList = container.querySelector('#zt-message-list');
    $inputArea = container.querySelector('#zt-input-area');
    $msgInput = container.querySelector('#zt-msg-input');
    $sendBtn = container.querySelector('#zt-btn-send');
    $emptyChat = container.querySelector('#zt-empty-chat');
    $typingIndicator = container.querySelector('#zt-typing-indicator');
    $addContactModal = container.querySelector('#zt-add-contact-modal');
    $sidebarOverlay = container.querySelector('#zt-sidebar-overlay');
  }

  // ── Bind Events ───────────────────────────────────────────────────────────
  function _bindEvents() {
    // Search contacts
    $searchInput.addEventListener('input', () => _renderContacts());

    // Send message
    $sendBtn.addEventListener('click', _sendMessage);
    $msgInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        _sendMessage();
      }
    });

    // Auto-resize textarea
    $msgInput.addEventListener('input', () => {
      $msgInput.style.height = 'auto';
      $msgInput.style.height = Math.min($msgInput.scrollHeight, 120) + 'px';
      $sendBtn.disabled = !$msgInput.value.trim();
      // Send typing indicator
      if (state.activeChat && $msgInput.value.trim()) {
        ZeqTextClient.sendTyping(state.activeChat, true);
        clearTimeout(typingTimeout);
        typingTimeout = setTimeout(() => {
          ZeqTextClient.sendTyping(state.activeChat, false);
        }, 2000);
      }
    });

    // Add contact button
    container.querySelector('#zt-btn-add-contact').addEventListener('click', () => {
      $addContactModal.classList.add('active');
      container.querySelector('#zt-add-search').focus();
    });

    // Close add contact modal
    container.querySelector('#zt-btn-close-add').addEventListener('click', () => {
      $addContactModal.classList.remove('active');
    });
    $addContactModal.addEventListener('click', (e) => {
      if (e.target === $addContactModal) $addContactModal.classList.remove('active');
    });

    // Search users
    let searchTimer;
    container.querySelector('#zt-add-search').addEventListener('input', (e) => {
      clearTimeout(searchTimer);
      searchTimer = setTimeout(async () => {
        const q = e.target.value.trim();
        if (q.length < 2) { container.querySelector('#zt-add-results').innerHTML = ''; return; }
        try {
          const users = await ZeqTextClient.searchUsers(q);
          _renderSearchResults(users);
        } catch (err) {
          container.querySelector('#zt-add-results').innerHTML = '<div class="zt-add-no-result">Search failed</div>';
        }
      }, 300);
    });

    // Add by ZID directly
    container.querySelector('#zt-btn-add-zid').addEventListener('click', async () => {
      const zid = container.querySelector('#zt-add-zid').value.trim();
      if (!zid) return;
      try {
        await ZeqTextClient.addContact(zid);
        container.querySelector('#zt-add-zid').value = '';
        $addContactModal.classList.remove('active');
        _loadContacts();
      } catch (err) {
        alert('Could not add contact: ' + err.message);
      }
    });

    // Back button — return to contact list view
    container.querySelector('#zt-btn-back').addEventListener('click', () => {
      state.activeChat = null;
      _showContactList();
    });
  }

  // ── Load Contacts ─────────────────────────────────────────────────────────
  async function _loadContacts() {
    if (!ZeqTextClient.isConnected()) return;
    try {
      const contacts = await ZeqTextClient.getContacts();
      state.contacts = contacts || [];
      _renderContacts();
    } catch (err) {
      console.warn('[ZeqTextTab] Failed to load contacts:', err);
    }
  }

  // ── Render Contacts ───────────────────────────────────────────────────────
  function _renderContacts() {
    const q = ($searchInput ? $searchInput.value : '').toLowerCase();
    let filtered = state.contacts.filter(c =>
      !q || (c.displayName || '').toLowerCase().includes(q) || (c.zid || '').toLowerCase().includes(q)
    );

    // Sort: AI bot first, then by last message time or alphabetical
    filtered.sort((a, b) => {
      if (a.zid === ZeqTextClient.AI_BOT_ZID) return -1;
      if (b.zid === ZeqTextClient.AI_BOT_ZID) return 1;
      const aLast = _lastMessageTime(a.zid);
      const bLast = _lastMessageTime(b.zid);
      if (aLast && bLast) return bLast - aLast;
      if (aLast) return -1;
      if (bLast) return 1;
      return (a.displayName || '').localeCompare(b.displayName || '');
    });

    $contactList.innerHTML = '';

    if (filtered.length === 0) {
      $contactList.innerHTML = '<div class="zt-no-contacts">' +
        (state.contacts.length === 0 ? 'No contacts yet. Tap + to add.' : 'No matching contacts.') + '</div>';
      return;
    }

    filtered.forEach(contact => {
      const isActive = contact.zid === state.activeChat;
      const isOnline = state.presence[contact.zid];
      const isBot = contact.zid === ZeqTextClient.AI_BOT_ZID;
      const unread = state.unread[contact.zid] || 0;
      const lastMsg = _lastMessage(contact.zid);
      const color = _avatarColor(contact.displayName || contact.zid);
      const initials = _initials(contact.displayName || contact.zid);

      const div = document.createElement('div');
      div.className = 'zt-contact' + (isActive ? ' active' : '');
      div.innerHTML =
        '<div class="zt-contact-avatar" style="background:' + color + '">' +
          (isBot ? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7v1H3v-1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"/><rect x="7" y="17" width="10" height="5" rx="1"/></svg>' : initials) +
          (isOnline || isBot ? '<span class="zt-online-dot"></span>' : '') +
        '</div>' +
        '<div class="zt-contact-info">' +
          '<div class="zt-contact-top">' +
            '<span class="zt-contact-name">' + _esc(contact.displayName || contact.zid) + '</span>' +
            (lastMsg ? '<span class="zt-contact-time">' + _timeShort(lastMsg.createdAt || lastMsg.timestamp) + '</span>' : '') +
          '</div>' +
          '<div class="zt-contact-bottom">' +
            '<span class="zt-contact-preview">' + (lastMsg ? _esc(_truncate(lastMsg.content, 40)) : '<span style="opacity:0.4">No messages yet</span>') + '</span>' +
            (unread > 0 ? '<span class="zt-unread-badge">' + unread + '</span>' : '') +
          '</div>' +
        '</div>';
      div.addEventListener('click', () => _openChat(contact));
      $contactList.appendChild(div);
    });
  }

  // ── Open Chat ─────────────────────────────────────────────────────────────
  async function _openChat(contact) {
    state.activeChat = contact.zid;
    state.unread[contact.zid] = 0;

    // Update header
    const isBot = contact.zid === ZeqTextClient.AI_BOT_ZID;
    const isOnline = state.presence[contact.zid] || isBot;
    const color = _avatarColor(contact.displayName || contact.zid);
    const initials = _initials(contact.displayName || contact.zid);

    container.querySelector('#zt-chat-avatar').style.background = color;
    container.querySelector('#zt-chat-avatar').innerHTML = isBot ?
      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7v1H3v-1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"/><rect x="7" y="17" width="10" height="5" rx="1"/></svg>' :
      initials;
    container.querySelector('#zt-chat-name').textContent = contact.displayName || contact.zid;
    container.querySelector('#zt-chat-status').textContent = isOnline ? 'Online' : 'Offline';
    container.querySelector('#zt-chat-status').className = 'zt-chat-status' + (isOnline ? ' online' : '');

    // Show chat UI — toggle from contact list to chat view
    $emptyChat.style.display = 'none';
    $chatHeader.style.display = 'flex';
    $messageList.style.display = 'flex';
    $inputArea.style.display = 'flex';
    $chatArea.classList.add('active');
    $sidebar.style.display = 'none';

    // Load messages
    try {
      const messages = await ZeqTextClient.getMessages(contact.zid);
      state.messages[contact.zid] = messages || [];
      _renderMessages();
    } catch (err) {
      console.warn('[ZeqTextTab] Failed to load messages:', err);
      state.messages[contact.zid] = [];
      _renderMessages();
    }

    // Send read receipts for unread
    const msgs = state.messages[contact.zid] || [];
    const myZid = ZeqTextClient.getMyZid();
    msgs.forEach(m => {
      if (m.senderZid !== myZid && m.status !== 'read') {
        ZeqTextClient.sendReadReceipt(m.id, m.senderZid);
      }
    });

    $msgInput.focus();
    _renderContacts();
  }

  function _showEmptyChat() {
    $emptyChat.style.display = 'flex';
    $chatHeader.style.display = 'none';
    $messageList.style.display = 'none';
    $inputArea.style.display = 'none';
    $typingIndicator.style.display = 'none';
    $chatArea.classList.remove('active');
  }

  /** Switch to contact list view (hide chat, show sidebar) */
  function _showContactList() {
    $sidebar.style.display = 'flex';
    $chatArea.classList.remove('active');
    $chatHeader.style.display = 'none';
    $messageList.style.display = 'none';
    $inputArea.style.display = 'none';
    $typingIndicator.style.display = 'none';
    _renderContacts();
  }

  // ── Render Messages ───────────────────────────────────────────────────────
  function _renderMessages() {
    if (!state.activeChat) return;
    const msgs = state.messages[state.activeChat] || [];
    const myZid = ZeqTextClient.getMyZid();

    $messageList.innerHTML = '';
    let lastDate = '';

    msgs.forEach(msg => {
      const ts = msg.createdAt || msg.timestamp;
      const dateStr = _dateLabel(ts);

      // Date divider
      if (dateStr !== lastDate) {
        lastDate = dateStr;
        const divider = document.createElement('div');
        divider.className = 'zt-date-divider';
        divider.innerHTML = '<span>' + dateStr + '</span>';
        $messageList.appendChild(divider);
      }

      const isSent = msg.senderZid === myZid;
      const bubble = document.createElement('div');
      bubble.className = 'zt-msg ' + (isSent ? 'zt-msg-sent' : 'zt-msg-recv');
      bubble.innerHTML =
        '<div class="zt-msg-content">' + _esc(msg.content) + '</div>' +
        '<div class="zt-msg-meta">' +
          '<span class="zt-msg-time">' + _timeOnly(ts) + '</span>' +
          (isSent ? '<span class="zt-msg-status ' + (msg.status || 'sent') + '">' + _statusIcon(msg.status) + '</span>' : '') +
        '</div>';
      $messageList.appendChild(bubble);
    });

    // Scroll to bottom
    $messageList.scrollTop = $messageList.scrollHeight;
  }

  // ── Send Message ──────────────────────────────────────────────────────────
  function _sendMessage() {
    const content = $msgInput.value.trim();
    if (!content || !state.activeChat) return;

    const messageId = ZeqTextClient.sendMessage(state.activeChat, content);
    const myZid = ZeqTextClient.getMyZid();

    // Optimistic add
    const msg = {
      id: messageId,
      senderZid: myZid,
      recipientZid: state.activeChat,
      content,
      createdAt: new Date().toISOString(),
      status: 'sent'
    };

    if (!state.messages[state.activeChat]) state.messages[state.activeChat] = [];
    state.messages[state.activeChat].push(msg);
    _renderMessages();

    // Clear input
    $msgInput.value = '';
    $msgInput.style.height = 'auto';
    $sendBtn.disabled = true;

    // Stop typing
    ZeqTextClient.sendTyping(state.activeChat, false);
    clearTimeout(typingTimeout);

    _renderContacts();
  }

  // ── WS Event Handlers ─────────────────────────────────────────────────────
  function _onMessage(msg) {
    const chatZid = msg.senderZid;
    if (!state.messages[chatZid]) state.messages[chatZid] = [];
    state.messages[chatZid].push(msg);

    if (state.activeChat === chatZid) {
      _renderMessages();
      ZeqTextClient.sendReadReceipt(msg.id, msg.senderZid);
    } else {
      state.unread[chatZid] = (state.unread[chatZid] || 0) + 1;
    }
    _renderContacts();
  }

  function _onMessageSent(msg) {
    const chatZid = msg.recipientZid;
    if (!state.messages[chatZid]) state.messages[chatZid] = [];
    // Update existing optimistic message or add
    const existing = state.messages[chatZid].find(m => m.id === msg.id);
    if (existing) {
      existing.status = msg.status || 'delivered';
    }
    if (state.activeChat === chatZid) _renderMessages();
    _renderContacts();
  }

  function _onTyping(data) {
    if (data.isTyping) {
      state.typing[data.senderZid] = true;
    } else {
      delete state.typing[data.senderZid];
    }
    if (state.activeChat === data.senderZid) {
      if (state.typing[data.senderZid]) {
        $typingIndicator.style.display = 'flex';
        $typingIndicator.querySelector('.zt-typing-text').textContent = 'typing...';
      } else {
        $typingIndicator.style.display = 'none';
      }
    }
  }

  function _onReadReceipt(data) {
    // Mark message as read
    for (const zid in state.messages) {
      const msgs = state.messages[zid];
      const msg = msgs.find(m => m.id === data.messageId);
      if (msg) {
        msg.status = 'read';
        if (state.activeChat === zid) _renderMessages();
        break;
      }
    }
  }

  function _onPresence(data) {
    state.presence[data.zid] = data.online;
    _renderContacts();
    if (state.activeChat === data.zid) {
      const statusEl = container.querySelector('#zt-chat-status');
      if (statusEl) {
        statusEl.textContent = data.online ? 'Online' : 'Offline';
        statusEl.className = 'zt-chat-status' + (data.online ? ' online' : '');
      }
    }
  }

  // ── Render Search Results (Add Contact) ───────────────────────────────────
  function _renderSearchResults(users) {
    const resultsEl = container.querySelector('#zt-add-results');
    if (!users || users.length === 0) {
      resultsEl.innerHTML = '<div class="zt-add-no-result">No users found</div>';
      return;
    }
    resultsEl.innerHTML = '';
    users.forEach(user => {
      const div = document.createElement('div');
      div.className = 'zt-add-result-item';
      const color = _avatarColor(user.displayName || user.zid);
      div.innerHTML =
        '<div class="zt-contact-avatar" style="background:' + color + ';width:32px;height:32px;font-size:12px">' +
          _initials(user.displayName || user.zid) +
        '</div>' +
        '<div class="zt-add-result-info">' +
          '<div class="zt-add-result-name">' + _esc(user.displayName || user.zid) + '</div>' +
          '<div class="zt-add-result-zid">' + _esc(user.zid) + '</div>' +
        '</div>' +
        '<button class="zt-btn-primary zt-btn-sm">Add</button>';
      div.querySelector('button').addEventListener('click', async () => {
        try {
          await ZeqTextClient.addContact(user.zid);
          $addContactModal.classList.remove('active');
          _loadContacts();
        } catch (err) {
          alert('Could not add: ' + err.message);
        }
      });
      resultsEl.appendChild(div);
    });
  }

  // ── Helpers ───────────────────────────────────────────────────────────────
  function _lastMessage(zid) {
    const msgs = state.messages[zid];
    return msgs && msgs.length > 0 ? msgs[msgs.length - 1] : null;
  }

  function _lastMessageTime(zid) {
    const msg = _lastMessage(zid);
    if (!msg) return 0;
    return new Date(msg.createdAt || msg.timestamp).getTime();
  }

  function _statusIcon(status) {
    switch (status) {
      case 'read': return '<svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke="#22d3ee" stroke-width="1.5"><path d="M1 5l3 3 7-7"/><path d="M5 5l3 3 7-7"/></svg>';
      case 'delivered': return '<svg width="16" height="10" viewBox="0 0 16 10" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 5l3 3 7-7"/><path d="M5 5l3 3 7-7"/></svg>';
      default: return '<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 5l3 3 5-6"/></svg>';
    }
  }

  function _timeShort(ts) {
    if (!ts) return '';
    const d = new Date(ts);
    const now = new Date();
    const diff = now - d;
    if (diff < 86400000 && d.getDate() === now.getDate()) {
      return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    }
    if (diff < 604800000) {
      return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()];
    }
    return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
  }

  function _timeOnly(ts) {
    if (!ts) return '';
    return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  }

  function _dateLabel(ts) {
    if (!ts) return '';
    const d = new Date(ts);
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
    const diff = today - msgDay;
    if (diff === 0) return 'Today';
    if (diff === 86400000) return 'Yesterday';
    return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
  }

  const AVATAR_COLORS = [
    '#8b5cf6', '#22d3ee', '#f43f5e', '#f97316', '#22c55e',
    '#06b6d4', '#a855f7', '#ec4899', '#14b8a6', '#3b82f6'
  ];

  function _avatarColor(name) {
    let hash = 0;
    for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
    return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
  }

  function _initials(name) {
    if (!name) return '?';
    const parts = name.split(/[\s-]+/);
    if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
    return name.substring(0, 2).toUpperCase();
  }

  function _truncate(str, len) {
    if (!str) return '';
    return str.length > len ? str.substring(0, len) + '...' : str;
  }

  function _esc(str) {
    const div = document.createElement('div');
    div.textContent = str || '';
    return div.innerHTML;
  }

  return { init, destroy };
})();
