import { renderHeader } from '../components/header.js';
import { authService, getMessageReactionState, setMessageReactionState, state } from '../state.js';
import { captureClientError } from '../services/client-error-reporter.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import {
  animatePress,
  createSkeletonCard,
  longPressFeel,
  shareOrCopyLink,
  showToast,
  softHaptic,
} from '../services/channels-ux.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import { navigateBack } from '../router.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { extractLoginFromBlockchainName, makeProfileRoute, makeShineChannelRoute, makeShineMessageRoute } from '../services/shine-routes.js';

export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };

const pendingReactionActions = new Set();
const pendingThreadScroll = new Map();
const threadAvatarSnapshotCache = new Map();
const threadAvatarPendingByLogin = new Map();

async function loadThreadAvatarSnapshot(login) {
  const cleanLogin = String(login || '').trim();
  if (!cleanLogin) return null;
  const key = cleanLogin.toLowerCase();
  if (threadAvatarSnapshotCache.has(key)) return threadAvatarSnapshotCache.get(key);
  if (threadAvatarPendingByLogin.has(key)) return threadAvatarPendingByLogin.get(key);
  const pending = loadProfileSnapshot(cleanLogin)
    .then((snapshot) => {
      threadAvatarSnapshotCache.set(key, snapshot || null);
      threadAvatarPendingByLogin.delete(key);
      return snapshot || null;
    })
    .catch(() => {
      threadAvatarSnapshotCache.set(key, null);
      threadAvatarPendingByLogin.delete(key);
      return null;
    });
  threadAvatarPendingByLogin.set(key, pending);
  return pending;
}

function createThreadAvatar(login) {
  const cleanLogin = String(login || '').trim();
  const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
  const avatarEl = renderUserAvatar({
    login: cleanLogin || 'unknown',
    size: 'small',
    className: 'channel-message-avatar',
    title,
  });
  if (!cleanLogin) return avatarEl;
  void loadThreadAvatarSnapshot(cleanLogin).then((snapshot) => {
    if (!avatarEl.isConnected) return;
    const upgraded = renderUserAvatar({
      login: cleanLogin,
      avatar: snapshot?.avatar?.txId
        ? {
          ar: String(snapshot.avatar.txId || '').trim(),
          sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
        }
        : null,
      size: 'small',
      className: 'channel-message-avatar',
      title,
    });
    avatarEl.replaceWith(upgraded);
  });
  return avatarEl;
}

function logThreadRuntimeError(stage, error, context = {}) {
  const message = String(error?.message || error || 'thread runtime error');
  console.error(`[channel-thread-view:${stage}]`, error, context);
  captureClientError({
    kind: 'channels_thread_runtime',
    message,
    stack: error?.stack || '',
    context: { stage, ...context },
  });
}

function encodeRoutePart(value = '') {
  return encodeURIComponent(String(value));
}

function normalizeRouteHash(hash) {
  const normalized = String(hash || '').trim().toLowerCase();
  return normalized || '0';
}

function normalizeMessageHash(hash) {
  const normalized = String(hash || '').trim().toLowerCase();
  if (!/^[0-9a-f]{64}$/.test(normalized)) return '';
  if (/^0+$/.test(normalized)) return '';
  return normalized;
}

function toSafeInt(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function looksLikeBlockchainName(value) {
  const raw = String(value || '').trim();
  return /^[^-]+-\d+$/.test(raw);
}

function makeReactionActionKey(messageRef) {
  const login = String(state.session.login || '').trim().toLowerCase();
  const blockchainName = String(messageRef?.blockchainName || '').trim();
  const blockNumber = Number(messageRef?.blockNumber);
  const blockHash = normalizeMessageHash(messageRef?.blockHash);
  if (!login || !blockchainName || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return '';
  return `${login}|${blockchainName}|${blockNumber}|${blockHash}`;
}

function messageRefKey(messageRef) {
  const blockchainName = String(messageRef?.blockchainName || '').trim();
  const blockNumber = Number(messageRef?.blockNumber);
  const blockHash = normalizeMessageHash(messageRef?.blockHash);
  if (!blockchainName || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return '';
  return `${blockchainName}:${blockNumber}:${blockHash}`;
}

function buildAbsoluteRouteUrl(routePath = '') {
  const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
  const url = new URL(window.location.href);
  url.pathname = `/${cleanRoute}`;
  url.hash = '';
  return url.toString();
}

function parseThreadSelector(route) {
  const params = route?.params || {};
  if (params.ownerBlockchainName && params.channelName && params.messageBlockNumber) {
    return {
      short: {
        ownerBlockchainName: String(params.ownerBlockchainName || '').trim(),
        channelName: String(params.channelName || '').trim(),
      },
      message: {
        blockchainName: '',
        blockNumber: toSafeInt(params.messageBlockNumber),
        blockHash: normalizeRouteHash(params.messageBlockHash),
      },
      channel: {
        ownerBlockchainName: '',
        channelRootBlockNumber: null,
        channelRootBlockHash: '0',
      },
    };
  }
  const blockNumber = toSafeInt(params.messageBlockNumber);
  if (!params.messageBlockchainName || blockNumber == null) return null;

  return {
    message: {
      blockchainName: String(params.messageBlockchainName),
      blockNumber,
      blockHash: normalizeRouteHash(params.messageBlockHash),
    },
    channel: {
      ownerBlockchainName: String(params.channelOwnerBlockchainName || ''),
      channelRootBlockNumber: toSafeInt(params.channelRootBlockNumber),
      channelRootBlockHash: normalizeRouteHash(params.channelRootBlockHash),
    },
  };
}

function allFeedSummaries() {
  const feed = state.channelsFeed || {};
  return [
    ...(feed.ownedChannels || []),
    ...(feed.followedUsersChannels || []),
    ...(feed.followedChannels || []),
  ];
}

function resolveChannelDisplayName(channelSelector) {
  const rootNumber = channelSelector?.channelRootBlockNumber ?? channelSelector?.rootBlockNumber;
  const rootHashRaw = channelSelector?.channelRootBlockHash ?? channelSelector?.rootBlockHash;
  if (!channelSelector?.ownerBlockchainName || rootNumber == null) return '';
  const ownerBch = String(channelSelector.ownerBlockchainName);
  const rootNo = Number(rootNumber);
  const rootHash = normalizeRouteHash(rootHashRaw);

  const found = allFeedSummaries().find((summary) => (
    String(summary?.channel?.ownerBlockchainName || '') === ownerBch
    && Number(summary?.channel?.channelRoot?.blockNumber) === rootNo
    && normalizeRouteHash(summary?.channel?.channelRoot?.blockHash) === rootHash
  ));
  if (!found) return '';
  return `${found.channel?.ownerLogin || 'неизвестно'}/${found.channel?.channelName || 'канал'}`;
}

function extractChannelContextFromThreadPayload(payload) {
  const focusInfo = payload?.focus?.channelInfo;
  if (focusInfo?.ownerBlockchainName && focusInfo?.channelRoot?.blockNumber != null) {
    return {
      ownerBlockchainName: String(focusInfo.ownerBlockchainName || '').trim(),
      channelRootBlockNumber: Number(focusInfo.channelRoot.blockNumber),
      channelRootBlockHash: '0',
    };
  }

  const ancestors = Array.isArray(payload?.ancestors) ? payload.ancestors : [];
  for (let i = ancestors.length - 1; i >= 0; i -= 1) {
    const info = ancestors[i]?.channelInfo;
    if (info?.ownerBlockchainName && info?.channelRoot?.blockNumber != null) {
      return {
        ownerBlockchainName: String(info.ownerBlockchainName || '').trim(),
        channelRootBlockNumber: Number(info.channelRoot.blockNumber),
        channelRootBlockHash: '0',
      };
    }
  }
  return null;
}

async function resolveChannelDisplayNameFromServer(channelSelector) {
  const ownerBch = String(channelSelector?.ownerBlockchainName || '').trim();
  const rootNo = Number(channelSelector?.channelRootBlockNumber);
  if (!ownerBch || !Number.isFinite(rootNo) || rootNo < 0) return '';

  const ownerLogin = extractLoginFromBlockchainName(ownerBch);
  if (!ownerLogin) return '';

  try {
    const feed = await authService.listSubscriptionsFeed(ownerLogin, 1000);
    const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
    const row = rows.find((item) => (
      String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch.toLowerCase()
      && Number(item?.channel?.channelRoot?.blockNumber) === rootNo
    ));
    if (!row?.channel?.channelName) return '';

    channelSelector.channelRootBlockHash = normalizeRouteHash(row?.channel?.channelRoot?.blockHash);
    return `${row.channel.ownerLogin || ownerLogin}/${row.channel.channelName}`;
  } catch {
    return '';
  }
}

function buildThreadRouteFromTarget(target, selector) {
  if (!target) return '';
  const ownerBch = String(selector?.short?.ownerBlockchainName || selector?.channel?.ownerBlockchainName || '').trim();
  return makeShineMessageRoute({
    ownerLogin: extractLoginFromBlockchainName(ownerBch) || extractLoginFromBlockchainName(target.blockchainName),
    messageBlockchainName: target.blockchainName,
    messageBlockNumber: target.blockNumber,
  });
}

function buildChannelRouteFromThread(selector, resolvedChannelLabel = '') {
  const ownerBch = String(selector?.short?.ownerBlockchainName || selector?.channel?.ownerBlockchainName || '').trim();
  if (selector?.short?.ownerBlockchainName && selector?.short?.channelName) {
    return makeShineChannelRoute({
      ownerLogin: extractLoginFromBlockchainName(ownerBch),
      ownerBlockchainName: ownerBch,
      channelName: selector.short.channelName,
    });
  }
  const label = String(resolvedChannelLabel || '').trim();
  const slashIndex = label.indexOf('/');
  const channelName = slashIndex >= 0 ? label.slice(slashIndex + 1).trim() : '';
  return makeShineChannelRoute({
    ownerLogin: extractLoginFromBlockchainName(ownerBch),
    ownerBlockchainName: ownerBch,
    channelName,
  });
}

function buildTargetFromNode(node) {
  const blockchainName = String(node?.authorBlockchainName || '').trim();
  const blockNumber = Number(node?.messageRef?.blockNumber);
  const blockHash = normalizeMessageHash(node?.messageRef?.blockHash);
  if (!blockchainName || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return null;
  return { blockchainName, blockNumber, blockHash };
}

function buildRepostTargetFromNode(node) {
  const blockchainName = String(node?.targetBlockchainName || '').trim();
  const blockNumber = Number(node?.targetBlockNumber);
  const blockHash = normalizeMessageHash(node?.targetBlockHash);
  if (!blockchainName || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return null;
  return { blockchainName, blockNumber, blockHash };
}

function firstNonEmptyText(...candidates) {
  for (const candidate of candidates) {
    if (typeof candidate !== 'string') continue;
    const trimmed = candidate.trim();
    if (trimmed.length > 0) return candidate;
  }
  return '';
}

function latestVersionText(versions) {
  if (!Array.isArray(versions) || !versions.length) return '';
  const version = versions[versions.length - 1];
  if (typeof version?.text === 'string') return version.text;
  if (typeof version?.message === 'string') return version.message;
  if (typeof version?.body === 'string') return version.body;
  return '';
}

function resolveNodeText(node) {
  return firstNonEmptyText(
    node?.text,
    node?.message,
    node?.body,
    latestVersionText(node?.versions),
  );
}

function openReplyModal({ onSubmit, navigate }) {
  const root = document.getElementById('modal-root');
  root.innerHTML = `
    <div class="modal" id="thread-reply-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">Ответ</h3>
        <textarea id="thread-reply-text" class="input" rows="5" maxlength="2000" placeholder="Текст ответа"></textarea>
        <div class="row wrap-row">
          <button class="ghost-btn" id="thread-reply-voice" type="button">🎤 Голосом</button>
        </div>
        <div class="meta-muted inline-error" id="thread-reply-error"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="thread-reply-cancel" type="button">Отмена</button>
          <button class="primary-btn" id="thread-reply-submit" type="button">Отправить</button>
        </div>
      </div>
    </div>
  `;

  const textEl = root.querySelector('#thread-reply-text');
  const errorEl = root.querySelector('#thread-reply-error');
  const submitEl = root.querySelector('#thread-reply-submit');
  let inFlight = false;

  const setBusy = (busy) => {
    inFlight = !!busy;
    submitEl.disabled = inFlight;
    if (textEl) textEl.disabled = inFlight;
    submitEl.textContent = inFlight ? 'Отправляем...' : 'Отправить';
  };

  const close = () => {
    root.innerHTML = '';
  };

  root.querySelector('#thread-reply-cancel')?.addEventListener('click', close);
  root.querySelector('#thread-reply-voice')?.addEventListener('click', async () => {
    await openSpeechInputModal({
      navigate,
      onTextReady: (text) => {
        const prev = String(textEl?.value || '').trim();
        if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
      },
    });
  });
  root.querySelector('#thread-reply-submit')?.addEventListener('click', async () => {
    if (inFlight) return;

    const text = String(textEl?.value || '').trim();
    if (!text) {
      errorEl.textContent = 'Введите текст ответа.';
      return;
    }

    setBusy(true);
    errorEl.textContent = '';

    try {
      await onSubmit(text);
      close();
    } catch (error) {
      setBusy(false);
      errorEl.textContent = toUserMessage(error, 'Не удалось отправить ответ.');
    }
  });

  if (textEl) textEl.focus();
}

function openRepostModal({ navigate, channels = [], onSubmit }) {
  const root = document.getElementById('modal-root');
  const options = (Array.isArray(channels) ? channels : [])
    .filter((item) => item?.selector?.ownerBlockchainName && Number.isFinite(Number(item?.selector?.channelRootBlockNumber)))
    .map((item, index) => {
      const owner = String(item?.ownerLogin || '').trim();
      const name = String(item?.channelName || '').trim();
      const label = `${owner || 'my'} / ${name || 'stories'}`;
      return `<option value="${index}">${label}</option>`;
    })
    .join('');

  root.innerHTML = `
    <div class="modal" id="thread-repost-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">Репост</h3>
        <label class="meta-muted" for="thread-repost-channel-select">Канал</label>
        <select id="thread-repost-channel-select" class="input">${options}</select>
        <label class="meta-muted" for="thread-repost-comment">Комментарий</label>
        <textarea id="thread-repost-comment" class="input" rows="5" maxlength="2000" placeholder="Комментарий к репосту"></textarea>
        <div class="row wrap-row">
          <button class="ghost-btn" id="thread-repost-voice" type="button">🎤 Голосом</button>
        </div>
        <div class="meta-muted inline-error" id="thread-repost-error"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="thread-repost-cancel" type="button">Отмена</button>
          <button class="primary-btn" id="thread-repost-submit" type="button">Опубликовать репост</button>
        </div>
      </div>
    </div>
  `;

  const selectEl = root.querySelector('#thread-repost-channel-select');
  const textEl = root.querySelector('#thread-repost-comment');
  const errorEl = root.querySelector('#thread-repost-error');
  const submitEl = root.querySelector('#thread-repost-submit');
  let inFlight = false;

  const setBusy = (busy) => {
    inFlight = !!busy;
    if (selectEl) selectEl.disabled = inFlight;
    if (textEl) textEl.disabled = inFlight;
    if (submitEl) {
      submitEl.disabled = inFlight;
      submitEl.textContent = inFlight ? 'Публикуем...' : 'Опубликовать репост';
    }
  };

  const close = () => {
    root.innerHTML = '';
  };

  root.querySelector('#thread-repost-cancel')?.addEventListener('click', close);
  root.querySelector('#thread-repost-voice')?.addEventListener('click', async () => {
    await openSpeechInputModal({
      navigate,
      onTextReady: (text) => {
        const prev = String(textEl?.value || '').trim();
        if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
      },
    });
  });

  submitEl?.addEventListener('click', async () => {
    if (inFlight) return;
    const idx = Number(selectEl?.value ?? -1);
    if (!Number.isFinite(idx) || idx < 0 || idx >= channels.length) {
      errorEl.textContent = 'Выберите канал для репоста.';
      return;
    }
    const text = String(textEl?.value || '').trim();
    if (!text) {
      errorEl.textContent = 'Введите комментарий к репосту.';
      return;
    }
    setBusy(true);
    errorEl.textContent = '';
    try {
      await onSubmit({ channel: channels[idx].selector, text });
      close();
    } catch (error) {
      setBusy(false);
      errorEl.textContent = toUserMessage(error, 'Не удалось сделать репост.');
    }
  });

  if (textEl) textEl.focus();
}

function openMessageHistoryModal({ versions = [], title = 'История изменений' }) {
  const root = document.getElementById('modal-root');
  const rows = Array.isArray(versions) ? versions : [];
  root.innerHTML = `
    <div class="modal" id="thread-history-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">${title}</h3>
        <div class="stack" id="thread-history-list"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="thread-history-close" type="button">Закрыть</button>
        </div>
      </div>
    </div>
  `;

  const list = root.querySelector('#thread-history-list');
  if (list) {
    rows.forEach((item, index) => {
      const row = document.createElement('div');
      row.className = 'card stack';
      const ts = Number(item?.createdAtMs || 0);
      const text = String(item?.text || '').trim() || 'удалено';
      row.innerHTML = `
        <strong>Версия ${index + 1}</strong>
        <div class="meta-muted">${ts > 0 ? new Date(ts).toLocaleString('ru-RU') : '—'}</div>
        <p class="channel-message-body">${text}</p>
      `;
      list.append(row);
    });
  }

  root.querySelector('#thread-history-close')?.addEventListener('click', () => {
    root.innerHTML = '';
  });
}

function openEditMessageModal({ initialText = '', onSave, onDelete }) {
  const root = document.getElementById('modal-root');
  root.innerHTML = `
    <div class="modal" id="thread-edit-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">Редактировать сообщение</h3>
        <textarea id="thread-edit-text" class="input" rows="6" maxlength="2000"></textarea>
        <div class="meta-muted inline-error" id="thread-edit-error"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="thread-edit-cancel" type="button">Отмена</button>
          <button class="primary-btn" id="thread-edit-save" type="button">ОК</button>
        </div>
        <button class="destructive-btn modal-danger-action" id="thread-edit-delete" type="button">Удалить</button>
      </div>
    </div>
  `;
  const textEl = root.querySelector('#thread-edit-text');
  const errorEl = root.querySelector('#thread-edit-error');
  if (textEl) textEl.value = String(initialText || '');

  const close = () => {
    root.innerHTML = '';
  };

  root.querySelector('#thread-edit-cancel')?.addEventListener('click', close);
  root.querySelector('#thread-edit-save')?.addEventListener('click', async () => {
    const value = String(textEl?.value || '').trim();
    if (!value) {
      errorEl.textContent = 'Введите текст сообщения.';
      return;
    }
    try {
      await onSave(value);
      close();
    } catch (error) {
      errorEl.textContent = toUserMessage(error, 'Не удалось изменить сообщение.');
    }
  });
  root.querySelector('#thread-edit-delete')?.addEventListener('click', async () => {
    try {
      await onDelete();
      close();
    } catch (error) {
      errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
    }
  });
  if (textEl) textEl.focus();
}

function renderNodeCard(node, heading, handlers, localNumber) {
  const card = document.createElement('article');
  card.className = 'card stack thread-node-card channel-message-card';
  card.classList.add('is-counters-visible');

  const author = node?.authorLogin || 'автор';
  const versions = Array.isArray(node?.versions) ? node.versions : [];
  const versionsTotal = Number(node?.versionsTotal || versions.length || 1);
  const text = resolveNodeText(node) || (versionsTotal > 1 ? 'удалено' : '(пусто)');
  const likes = Number(node?.likesCount || 0);
  const replies = Number(node?.repliesCount || 0);
  const isOwnMessage = String(node?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase();
  const isChannelPost = Number(node?.channelInfo?.channelRoot?.blockNumber) >= 0;
  const msgSubType = Number(node?.msgSubType || 0);
  const repostTarget = msgSubType === 30 ? buildRepostTargetFromNode(node) : null;

  const headingText = String(heading || '').trim();
  if (headingText) {
    const headingEl = document.createElement('strong');
    headingEl.className = 'thread-node-heading';
    headingEl.textContent = headingText;
    card.append(headingEl);
  }

  const authorTile = document.createElement('button');
  authorTile.type = 'button';
  authorTile.className = 'channel-message-author-tile';

  const avatar = createThreadAvatar(author);

  const authorBlock = document.createElement('div');
  authorBlock.className = 'channel-message-author';
  const title = document.createElement('div');
  title.className = 'channel-message-title author-line';
  const loginEl = document.createElement('span');
  loginEl.className = 'author-line-login';
  loginEl.textContent = author;
  const numberEl = document.createElement('span');
  numberEl.className = 'author-line-num';
  numberEl.textContent = `· #${localNumber}`;
  title.append(loginEl, numberEl);
  if (versionsTotal > 1) {
    const editedMarker = document.createElement('button');
    editedMarker.type = 'button';
    editedMarker.className = 'message-edited-marker';
    editedMarker.textContent = `изменено ${Math.max(1, versionsTotal - 1)}`;
    editedMarker.title = 'Открыть историю редактирования';
    editedMarker.addEventListener('click', (event) => {
      event.stopPropagation();
      animatePress(event.currentTarget);
      openMessageHistoryModal({
        title: `История #${localNumber}`,
        versions,
      });
    });
    title.append(editedMarker);
  }
  const timestamp = document.createElement('div');
  timestamp.className = 'channel-message-time';
  timestamp.textContent = node?.createdAtMs ? new Date(node.createdAtMs).toLocaleString() : '—';
  authorBlock.append(title, timestamp);
  authorTile.append(avatar, authorBlock);

  const isDeletedMessage = String(text || '').trim().toLowerCase() === 'удалено';
  const body = document.createElement('p');
  body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
  body.textContent = isDeletedMessage ? 'Сообщение удалено' : text;

  card.append(authorTile, body);

  const target = buildTargetFromNode(node);
  const refKey = messageRefKey(target);
  if (!target || !handlers) return card;

  if (refKey) card.dataset.messageKey = refKey;

  setMessageReactionState(target, node?.likedByMe === true ? 'liked' : 'unliked');

  const actionKey = makeReactionActionKey(target);
  const isPending = actionKey ? pendingReactionActions.has(actionKey) : false;

  const isLiked = getMessageReactionState(target) === 'liked';

  const actions = document.createElement('div');
  actions.className = 'thread-node-actions channel-message-actions';

  const likeButton = document.createElement('button');
  likeButton.type = 'button';
  likeButton.className = 'channel-action-item thread-like-btn';
  if (isLiked) likeButton.classList.add('is-liked');
  likeButton.innerHTML = `
    <span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
    <span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
    <span class="channel-action-counter">${likes}</span>
  `;
  likeButton.disabled = isPending;
  likeButton.addEventListener('click', async (event) => {
    event.stopPropagation();
    animatePress(event.currentTarget);
    if (isPending) return;
    if (!isLiked) {
      const ok = window.confirm('Поставить лайк?');
      if (!ok) return;
    }
    await longPressFeel(event.currentTarget, 130);
    likeButton.disabled = true;
    try {
      await handlers.onToggleLike(target, isLiked ? 'unlike' : 'like');
    } catch (error) {
      logThreadRuntimeError('like_click', error, {
        action: isLiked ? 'unlike' : 'like',
        targetBlockchainName: target?.blockchainName || '',
        targetBlockNumber: target?.blockNumber,
      });
      handlers?.onActionError?.(error, isLiked ? 'unlike' : 'like');
    }
  });

  const replyButton = document.createElement('button');
  replyButton.type = 'button';
  replyButton.className = 'channel-action-item thread-reply-btn';
  replyButton.innerHTML = `
    <span class="channel-action-icon" aria-hidden="true">💬</span>
    <span class="channel-action-label">Ответить</span>
    <span class="channel-action-counter">${replies}</span>
  `;
  replyButton.addEventListener('click', (event) => {
    event.stopPropagation();
    animatePress(event.currentTarget);
    openReplyModal({
      navigate: handlers.navigate,
      onSubmit: async (textValue) => handlers.onReply(target, textValue),
    });
  });

  const shareButton = document.createElement('button');
  shareButton.type = 'button';
  shareButton.className = 'channel-action-item thread-share-btn';
  shareButton.innerHTML = `
    <span class="channel-action-icon" aria-hidden="true">↗</span>
    <span class="channel-action-label">Отправить</span>
  `;
  shareButton.addEventListener('click', async (event) => {
    event.stopPropagation();
    animatePress(event.currentTarget);
    await handlers.onShare(target);
  });

  // Репосты временно отключены до будущей реализации.
  // Точка возврата: Dev_Docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md
  actions.append(likeButton, replyButton, shareButton);
  if (repostTarget) {
    const originalButton = document.createElement('button');
    originalButton.type = 'button';
    originalButton.className = 'channel-action-item';
    originalButton.innerHTML = `
      <span class="channel-action-icon" aria-hidden="true">↪</span>
      <span class="channel-action-label">Оригинал</span>
    `;
    originalButton.addEventListener('click', (event) => {
      event.stopPropagation();
      const ok = window.confirm('Перейти к оригинальному сообщению?');
      if (!ok) return;
      const ownerLogin = extractLoginFromBlockchainName(repostTarget.blockchainName);
      if (!ownerLogin) return;
      handlers.navigate(makeShineMessageRoute({
        ownerLogin,
        messageBlockchainName: repostTarget.blockchainName,
        messageBlockNumber: repostTarget.blockNumber,
      }));
    });
    actions.append(originalButton);
  }
  if (isOwnMessage) {
    const editButton = document.createElement('button');
    editButton.type = 'button';
    editButton.className = 'channel-action-item';
    editButton.setAttribute('aria-label', 'Редактировать');
    editButton.title = 'Редактировать';
    editButton.innerHTML = `
      <span class="channel-action-icon" aria-hidden="true">✏️</span>
    `;
    editButton.addEventListener('click', (event) => {
      event.stopPropagation();
      animatePress(event.currentTarget);
      openEditMessageModal({
        initialText: String(text || '').trim() === 'удалено' ? '' : text,
        onSave: async (nextText) => handlers.onEdit(target, nextText, { isChannelPost }),
        onDelete: async () => handlers.onEdit(target, '', { isChannelPost, isDelete: true }),
      });
    });
    actions.append(editButton);
  }
  card.append(actions);
  authorTile.addEventListener('click', (event) => {
    event.stopPropagation();
    const login = String(node?.authorLogin || '').trim();
    if (!login) return;
    handlers.navigate(makeProfileRoute(login));
  });
  card.addEventListener('click', () => {
    handlers.onOpenThread(target);
  });
  return card;
}

function renderDescendants(items, handlers, nextNumber, depth = 0) {
  const wrap = document.createElement('div');
  wrap.className = 'stack';

  const normalized = Array.isArray(items) ? items : [];
  normalized.forEach((branch, index) => {
    try {
      const nodeNumber = nextNumber();
      const row = renderNodeCard(branch?.node, `Ответ ${index + 1}`, handlers, nodeNumber);
      row.classList.add('thread-node-level');
      row.style.setProperty('--depth', String(Math.min(depth, 4)));
      wrap.append(row);

      if (Array.isArray(branch?.children) && branch.children.length) {
        wrap.append(renderDescendants(branch.children, handlers, nextNumber, depth + 1));
      }
    } catch (error) {
      logThreadRuntimeError('render_descendants_branch', error, { depth, index });
    }
  });

  return wrap;
}

function applyPendingScroll(screen, routeKey) {
  const target = pendingThreadScroll.get(routeKey);
  if (!target) return;

  const doScroll = () => {
    if (target === '__LAST_REPLY__') {
      const cards = screen.querySelectorAll('.thread-block--replies [data-message-key]');
      const last = cards[cards.length - 1];
      if (last) {
        last.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
      pendingThreadScroll.delete(routeKey);
      return;
    }

    const node = screen.querySelector(`[data-message-key="${target}"]`);
    if (node) {
      node.scrollIntoView({ behavior: 'smooth', block: 'center' });
      pendingThreadScroll.delete(routeKey);
    }
  };

  setTimeout(doScroll, 20);
}

function renderSkeleton(screen) {
  const wrap = document.createElement('div');
  wrap.className = 'stack';
  wrap.append(createSkeletonCard(), createSkeletonCard(), createSkeletonCard());
  screen.append(wrap);
  return wrap;
}

export function render({ navigate, route }) {
  const selector = parseThreadSelector(route);
  const channelDisplayName = resolveChannelDisplayName(selector?.channel);
  const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;

  const screen = document.createElement('section');
  screen.className = 'stack channels-screen channels-screen--thread';
  const appScreen = document.getElementById('app-screen');
  appScreen?.classList.add('channels-scroll-clean');

  const header = renderHeader({
    title: '',
    leftAction: { label: '<', onClick: () => navigateBack() },
    rightActions: [{ label: 'Тред в канале: ...', onClick: () => {} }],
  });
  const threadHeaderButton = header.querySelector('.header-actions .icon-btn');
  if (threadHeaderButton) {
    threadHeaderButton.classList.add('channel-header-route-btn');
    threadHeaderButton.disabled = true;
  }

  const statusBox = document.createElement('div');
  statusBox.className = 'card status-line is-unavailable channels-status';
  statusBox.style.display = 'none';

  const rerender = () => {
    try {
      const current = document.querySelector('section.channels-screen--thread');
      if (!current) return;
      const next = render({ navigate, route });
      current.replaceWith(next);
    } catch (error) {
      logThreadRuntimeError('rerender', error, { routePath: window.location.pathname });
    }
  };

  const showStatus = (message) => {
    if (!message) {
      statusBox.style.display = 'none';
      statusBox.textContent = '';
      return;
    }
    statusBox.textContent = message;
    statusBox.style.display = '';
  };

  const requireSigningSession = () => {
    const login = state.session.login;
    const storagePwd = state.session.storagePwdInMemory;
    if (!login || !storagePwd) {
      state.authReturnHash = window.location.pathname || '/channels-list';
      navigate('login-view');
      throw new Error('Для этого действия нужно войти');
    }
    return { login, storagePwd };
  };

  const handlers = {
    navigate,
    onToggleLike: async (target, action) => {
      const actionKey = makeReactionActionKey(target);
      if (!actionKey) throw new Error('Некорректная ссылка на сообщение для реакции.');
      if (pendingReactionActions.has(actionKey)) return;

      const previousReaction = getMessageReactionState(target);
      const nextReaction = action === 'unlike' ? 'unliked' : 'liked';

      pendingReactionActions.add(actionKey);
      try {
        const { login, storagePwd } = requireSigningSession();
        if (action === 'unlike') {
          await authService.addBlockUnlike({ login, storagePwd, message: target });
        } else {
          await authService.addBlockLike({ login, storagePwd, message: target });
        }

        setMessageReactionState(target, nextReaction);
        softHaptic(10);
        rerender();
      } catch (error) {
        setMessageReactionState(target, previousReaction || 'unliked');
        rerender();
        throw error;
      } finally {
        pendingReactionActions.delete(actionKey);
      }
    },
    onReply: async (target, textValue) => {
      const { login, storagePwd } = requireSigningSession();
      await authService.addBlockReply({ login, storagePwd, message: target, text: textValue });
      pendingThreadScroll.set(routeKey, '__LAST_REPLY__');
      softHaptic(15);
      showToast('Ответ отправлен');
      showStatus('');
      rerender();
    },
    onRepost: async (target) => {
      const { login, storagePwd } = requireSigningSession();
      const feed = await authService.listSubscriptionsFeed(login, 1000);
      const channels = (Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [])
        .map((row) => {
          const selectorRow = {
            ownerBlockchainName: String(row?.channel?.ownerBlockchainName || '').trim(),
            channelRootBlockNumber: Number(row?.channel?.channelRoot?.blockNumber),
            channelRootBlockHash: normalizeRouteHash(row?.channel?.channelRoot?.blockHash),
          };
          if (!selectorRow.ownerBlockchainName || !Number.isFinite(selectorRow.channelRootBlockNumber) || selectorRow.channelRootBlockNumber < 0) {
            return null;
          }
          return {
            ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
            channelName: String(row?.channel?.channelName || '').trim(),
            selector: selectorRow,
          };
        })
        .filter(Boolean);
      if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');

      openRepostModal({
        navigate,
        channels,
        onSubmit: async ({ channel, text }) => {
          await authService.addBlockRepost({
            login,
            storagePwd,
            channel,
            message: target,
            text,
          });
          softHaptic(12);
          showToast('Репост опубликован');
          showStatus('');
        },
      });
    },
    onShare: async (target) => {
      try {
        const routePath = buildThreadRouteFromTarget(target, selector);
        if (!routePath) throw new Error('Не удалось подготовить ссылку на тред.');
        const result = await shareOrCopyLink({
          title: 'SHiNE · Тред',
          text: 'Сообщение из треда SHiNE',
          url: buildAbsoluteRouteUrl(routePath),
        });
        if (result === 'copied') showToast('Ссылка скопирована');
        if (result === 'shared') showToast('Ссылка передана');
        if (result === 'copied' || result === 'shared') softHaptic(10);
      } catch (error) {
        showStatus(toUserMessage(error, 'Не удалось транслировать ссылку.'));
      }
    },
    onOpenThread: (target) => {
      const routePath = buildThreadRouteFromTarget(target, selector);
      if (!routePath) {
        showStatus('Не удалось определить путь до треда.');
        return;
      }
      navigate(routePath);
    },
    onActionError: (error, action) => {
      const fallback = action === 'unlike'
        ? 'Не удалось убрать лайк.'
        : action === 'repost'
          ? 'Не удалось сделать репост.'
          : 'Не удалось поставить лайк.';
      showStatus(toUserMessage(error, fallback));
    },
    onEdit: async (target, textValue, meta = {}) => {
      const { login, storagePwd } = requireSigningSession();
      await authService.addBlockEditMessage({
        login,
        storagePwd,
        message: target,
        text: textValue,
        isChannelPost: meta?.isChannelPost === true,
        channel: selector?.channel || null,
      });
      softHaptic(12);
      showToast('Сообщение обновлено');
      showStatus('');
      rerender();
    },
  };

  screen.append(header, statusBox);

  if (!selector) {
    const invalid = document.createElement('div');
    invalid.className = 'card meta-muted';
    invalid.textContent = 'Некорректный идентификатор треда в адресе страницы.';
    screen.append(invalid);
    return screen;
  }

  const skeleton = renderSkeleton(screen);

  (async () => {
    try {
      let resolvedMessage = selector.message;
      if (selector.short?.ownerBlockchainName && selector.short?.channelName) {
        const ownFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
        const allRows = [
          ...(Array.isArray(ownFeed?.ownedChannels) ? ownFeed.ownedChannels : []),
          ...(Array.isArray(ownFeed?.followedUsersChannels) ? ownFeed.followedUsersChannels : []),
          ...(Array.isArray(ownFeed?.followedChannels) ? ownFeed.followedChannels : []),
        ];
        const ownerRaw = String(selector.short.ownerBlockchainName || '').trim();
        const ownerNormalized = ownerRaw.toLowerCase();
        const ownerLoginFromBch = extractLoginFromBlockchainName(ownerRaw);
        const channelNameNormalized = String(selector.short.channelName || '').trim().toLowerCase();
        let channel = allRows.find((item) => (
          String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerNormalized
          && String(item?.channel?.channelName || '').trim().toLowerCase() === channelNameNormalized
        ));
        if (!channel) {
          channel = allRows.find((item) => (
            String(item?.channel?.ownerLogin || '').trim().toLowerCase() === ownerNormalized
            && String(item?.channel?.channelName || '').trim().toLowerCase() === channelNameNormalized
          ));
        }
        if (!channel && !looksLikeBlockchainName(ownerRaw)) {
          try {
            const ownerUser = await authService.getUser(ownerRaw);
            const ownerBch = String(ownerUser?.blockchainName || '').trim().toLowerCase();
            if (ownerBch) {
              channel = allRows.find((item) => (
                String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch
                && String(item?.channel?.channelName || '').trim().toLowerCase() === channelNameNormalized
              ));
            }
          } catch {
            // ignore fallback lookup errors
          }
        }
        if (!channel && ownerLoginFromBch) {
          try {
            const ownerFeed = await authService.listSubscriptionsFeed(ownerLoginFromBch, 500);
            const ownerRows = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
            channel = ownerRows.find((item) => (
              String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerNormalized
              && String(item?.channel?.channelName || '').trim().toLowerCase() === channelNameNormalized
            ));
          } catch {
            // ignore owner feed lookup errors
          }
        }
        const ownerBch = String(channel?.channel?.ownerBlockchainName || '').trim();
        const rootNo = Number(channel?.channel?.channelRoot?.blockNumber);
        const rootHash = normalizeRouteHash(channel?.channel?.channelRoot?.blockHash);
        if (!ownerBch || !Number.isFinite(rootNo) || !Number.isFinite(resolvedMessage?.blockNumber)) {
          throw new Error('Канал или сообщение не найдено.');
        }
        selector.channel = {
          ownerBlockchainName: ownerBch,
          channelRootBlockNumber: rootNo,
          channelRootBlockHash: rootHash,
        };

        resolvedMessage = {
          blockchainName: ownerBch,
          blockNumber: resolvedMessage.blockNumber,
          blockHash: normalizeMessageHash(resolvedMessage?.blockHash),
        };
      }

      const payload = await authService.getMessageThread(resolvedMessage, 20, 2, 50, state.session.login);
      skeleton.remove();

      const ancestors = Array.isArray(payload?.ancestors) ? payload.ancestors : [];
      const focus = payload?.focus || null;
      const descendants = Array.isArray(payload?.descendants) ? payload.descendants : [];

      const focusHash = normalizeMessageHash(focus?.messageRef?.blockHash);
      if (focusHash && selector?.message) {
        selector.message.blockHash = focusHash;
      }

      if ((!selector?.channel?.ownerBlockchainName || selector?.channel?.channelRootBlockNumber == null) && payload) {
        const context = extractChannelContextFromThreadPayload(payload);
        if (context) {
          selector.channel = {
            ownerBlockchainName: context.ownerBlockchainName,
            channelRootBlockNumber: context.channelRootBlockNumber,
            channelRootBlockHash: normalizeRouteHash(context.channelRootBlockHash),
          };
        }
      }

      let resolvedChannelLabel = resolveChannelDisplayName(selector?.channel);
      if (!resolvedChannelLabel && selector?.channel?.ownerBlockchainName && selector?.channel?.channelRootBlockNumber != null) {
        resolvedChannelLabel = await resolveChannelDisplayNameFromServer(selector.channel);
      }
      const fallbackChannel = String(selector?.channel?.ownerBlockchainName || '').trim() || 'неизвестно';
      const resolvedChannelTitle = resolvedChannelLabel || fallbackChannel;
      if (threadHeaderButton) {
        threadHeaderButton.textContent = `Тред в канале: ${resolvedChannelTitle}`;
        threadHeaderButton.disabled = false;
        threadHeaderButton.onclick = (event) => {
          event.preventDefault();
          animatePress(event.currentTarget);
          const routeToChannel = buildChannelRouteFromThread(selector, resolvedChannelLabel);
          if (routeToChannel) navigate(routeToChannel);
          else navigate('channels-list');
        };
      }

      let seq = 0;
      const nextNumber = () => {
        seq += 1;
        return seq;
      };

      let ancestorsWrap = null;
      if (ancestors.length) {
        ancestorsWrap = document.createElement('div');
        ancestorsWrap.className = 'stack thread-block thread-block--ancestors';
        const title = document.createElement('h3');
        title.className = 'section-title';
        title.textContent = 'История выше (на что это ответ)';
        ancestorsWrap.append(title);
        ancestors.forEach((node, index) => {
          ancestorsWrap.append(renderNodeCard(node, `Предок ${index + 1}`, handlers, nextNumber()));
        });
      }

      let focusWrap = null;
      if (focus) {
        focusWrap = document.createElement('div');
        focusWrap.className = 'stack thread-block thread-block--focus';
        const focusTitle = document.createElement('h3');
        focusTitle.className = 'section-title';
        focusTitle.textContent = 'Текущее сообщение';
        focusWrap.append(focusTitle);
        focusWrap.append(renderNodeCard(focus, '', handlers, nextNumber()));
      }

      const descendantsWrap = document.createElement('div');
      descendantsWrap.className = 'stack thread-block thread-block--replies';
      const descendantsTitle = document.createElement('h3');
      descendantsTitle.className = 'section-title';
      descendantsTitle.textContent = 'Ответы';
      descendantsWrap.append(descendantsTitle);

      if (descendants.length) {
        descendantsWrap.append(renderDescendants(descendants, handlers, nextNumber));
      } else {
        const empty = document.createElement('div');
        empty.className = 'card meta-muted';
        empty.textContent = 'Ответов пока нет.';
        descendantsWrap.append(empty);
      }

      if (ancestorsWrap) {
        screen.append(ancestorsWrap);
        const divider = document.createElement('div');
        divider.className = 'thread-history-divider';
        screen.append(divider);
      }

      if (focusWrap) screen.append(focusWrap);
      screen.append(descendantsWrap);

      applyPendingScroll(screen, routeKey);
      const hasPendingScroll = pendingThreadScroll.has(routeKey);
      if (!hasPendingScroll && focusWrap) {
        setTimeout(() => {
          focusWrap.scrollIntoView({ behavior: 'auto', block: 'start' });
        }, 20);
      }
    } catch (error) {
      skeleton.remove();
      const failed = document.createElement('div');
      failed.className = 'card meta-muted';
      failed.textContent = `Не удалось загрузить тред: ${toUserMessage(error, 'неизвестная ошибка')}`;
      screen.append(failed);
    }
  })();

  screen.cleanup = () => {
    appScreen?.classList.remove('channels-scroll-clean');
  };

  return screen;
}
