import { renderHeader } from '../components/header.js';
import {
  authService,
  getMessageReactionState,
  setChannelsFeed,
  setMessageReactionState,
  state,
} from '../state.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import {
  animatePress,
  createSkeletonCard,
  formatRelativeTime,
  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,
  makeShineMessageRoute,
} from '../services/shine-routes.js';

export const pageMeta = { id: 'channel-view', title: 'Канал' };
const CHANNEL_TYPE_PERSONAL = 100;

const pendingReactionActions = new Set();
const pendingScrollByRoute = new Map();
const messageAvatarSnapshotCache = new Map();
const messageAvatarPendingByLogin = new Map();

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

function createMessageAvatar(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 loadMessageAvatarSnapshot(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 isChannelsDemoMode() {
  try {
    const qs = new URLSearchParams(window.location.search);
    if (qs.get('channelsDemo') === '1') return true;
    return localStorage.getItem('shine-channels-demo') === '1';
  } catch {
    return false;
  }
}

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 parseMessageRefKey(key) {
  const raw = String(key || '').trim();
  if (!raw) return null;
  const parts = raw.split(':');
  if (parts.length !== 3) return null;
  const blockNumber = Number(parts[1]);
  const blockHash = normalizeMessageHash(parts[2]);
  if (!parts[0] || !Number.isFinite(blockNumber) || blockNumber < 0 || !blockHash) return null;
  return {
    blockchainName: parts[0],
    blockNumber,
    blockHash,
  };
}

function blockRefToMessageKey(blockRef, fallbackBch = '') {
  const blockNumber = toSafeInt(blockRef?.blockNumber);
  const blockHash = normalizeMessageHash(blockRef?.blockHash);
  const blockchainName = String(fallbackBch || '').trim();
  if (!blockchainName || blockNumber == null || !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 buildSelectorFromRoute(route, channelId) {
  const params = route?.params || {};

  if (params.ownerBlockchainName && params.channelName) {
    return {
      ownerBlockchainName: String(params.ownerBlockchainName || '').trim(),
      channelName: String(params.channelName || '').trim(),
    };
  }

  if (params.ownerBlockchainName) {
    const rootBlockNumber = toSafeInt(params.channelRootBlockNumber);
    if (rootBlockNumber != null) {
      return {
        ownerBlockchainName: String(params.ownerBlockchainName),
        channelRootBlockNumber: rootBlockNumber,
        channelRootBlockHash: normalizeRouteHash(params.channelRootBlockHash),
      };
    }
  }

  const summary = channelId ? state.channelsIndex[channelId] : null;
  if (!summary) return null;
  return {
    ownerBlockchainName: summary.channel?.ownerBlockchainName,
    channelRootBlockNumber: summary.channel?.channelRoot?.blockNumber,
    channelRootBlockHash: normalizeRouteHash(summary.channel?.channelRoot?.blockHash),
  };
}

function buildThreadRoute(messageRef, selector) {
  if (!messageRef || !selector) return '';
  const ownerLogin = extractLoginFromBlockchainName(selector.ownerBlockchainName);
  return makeShineMessageRoute({
    ownerLogin,
    messageBlockchainName: messageRef.blockchainName,
    messageBlockNumber: messageRef.blockNumber,
  });
}

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 resolveMessageText(message) {
  return firstNonEmptyText(
    message?.text,
    message?.message,
    message?.body,
    latestVersionText(message?.versions),
  );
}

function toTimestampMs(...candidates) {
  for (const candidate of candidates) {
    if (candidate == null) continue;
    if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
      return candidate > 1e12 ? Math.round(candidate) : Math.round(candidate * 1000);
    }
    if (typeof candidate === 'string') {
      const trimmed = candidate.trim();
      if (!trimmed) continue;
      const asNum = Number(trimmed);
      if (Number.isFinite(asNum) && asNum > 0) {
        return asNum > 1e12 ? Math.round(asNum) : Math.round(asNum * 1000);
      }
      const parsed = Date.parse(trimmed);
      if (Number.isFinite(parsed) && parsed > 0) return parsed;
    }
  }
  return 0;
}

function resolveMessageTimestampMs(message) {
  return toTimestampMs(
    message?.messageTimeMs,
    message?.message_time_ms,
    message?.timeMs,
    message?.time_ms,
    message?.timestampMs,
    message?.timestamp_ms,
    message?.createdAtMs,
    message?.created_at_ms,
    message?.messageTime,
    message?.createdAt,
    message?.created_at,
    message?.timestamp,
  );
}

function openAboutChannelModal(channel) {
  const root = document.getElementById('modal-root');
  root.innerHTML = `
    <div class="modal" id="about-channel-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">О канале</h3>
        <p><strong>${channel.displayName || channel.name}</strong></p>
        <p class="meta-muted">${channel.description || 'Описание не задано.'}</p>
        <button class="secondary-btn" id="about-channel-close" type="button">Закрыть</button>
      </div>
    </div>
  `;

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

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

  const textEl = root.querySelector('#reply-text');
  const errorEl = root.querySelector('#reply-error');
  const submitEl = root.querySelector('#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('#reply-cancel')?.addEventListener('click', close);
  root.querySelector('#reply-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 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="repost-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">Репост</h3>
        <label class="meta-muted" for="repost-channel-select">Канал</label>
        <select id="repost-channel-select" class="input">${options}</select>
        <label class="meta-muted" for="repost-comment">Комментарий</label>
        <textarea id="repost-comment" class="input" rows="5" maxlength="2000" placeholder="Комментарий к репосту"></textarea>
        <div class="row wrap-row">
          <button class="ghost-btn" id="repost-voice" type="button">🎤 Голосом</button>
        </div>
        <div class="meta-muted inline-error" id="repost-error"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="repost-cancel" type="button">Отмена</button>
          <button class="primary-btn" id="repost-submit" type="button">Опубликовать репост</button>
        </div>
      </div>
    </div>
  `;

  const selectEl = root.querySelector('#repost-channel-select');
  const textEl = root.querySelector('#repost-comment');
  const errorEl = root.querySelector('#repost-error');
  const submitEl = root.querySelector('#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('#repost-cancel')?.addEventListener('click', close);
  root.querySelector('#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 openAddMessageModal({ channelName, onSubmit, navigate }) {
  const root = document.getElementById('modal-root');
  root.innerHTML = `
    <div class="modal" id="channel-message-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">Новое сообщение в канале</h3>
        <p class="meta-muted">${channelName}</p>
        <textarea id="channel-message-text" class="input" rows="6" maxlength="2000" placeholder="Текст сообщения"></textarea>
        <div class="row wrap-row">
          <button class="ghost-btn" id="channel-message-voice" type="button">🎤 Голосом</button>
        </div>
        <div class="meta-muted inline-error" id="channel-message-error"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="channel-message-cancel" type="button">Отмена</button>
          <button class="primary-btn" id="channel-message-submit" type="button">Отправить</button>
        </div>
      </div>
    </div>
  `;

  const textEl = root.querySelector('#channel-message-text');
  const errorEl = root.querySelector('#channel-message-error');
  const submitEl = root.querySelector('#channel-message-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('#channel-message-cancel')?.addEventListener('click', close);
  root.querySelector('#channel-message-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 body = String(textEl?.value || '').trim();
    if (!body) {
      errorEl.textContent = 'Введите текст сообщения.';
      return;
    }

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

    try {
      await onSubmit(body);
      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="message-history-modal">
      <div class="modal-card stack">
        <h3 class="modal-title">${title}</h3>
        <div class="stack" id="message-history-list"></div>
        <div class="form-actions-grid">
          <button class="secondary-btn" id="message-history-close" type="button">Закрыть</button>
        </div>
      </div>
    </div>
  `;

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

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

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

  const textEl = root.querySelector('#edit-message-text');
  const errorEl = root.querySelector('#edit-message-error');
  if (textEl) textEl.value = String(initialText || '');

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

  root.querySelector('#edit-message-cancel')?.addEventListener('click', close);
  root.querySelector('#edit-message-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('#edit-message-delete')?.addEventListener('click', async () => {
    try {
      await onDelete();
      close();
    } catch (error) {
      errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
    }
  });
  if (textEl) textEl.focus();
}

function mapApiMessageToPost(message, selector, localNumber) {
  const blockNumber = toSafeInt(message?.messageRef?.blockNumber);
  const blockHash = normalizeMessageHash(message?.messageRef?.blockHash);
  const messageBch = String(message?.authorBlockchainName || selector?.ownerBlockchainName || '').trim();
  const hasRef = !!(messageBch && blockNumber != null && blockHash);

  const resolvedText = resolveMessageText(message);
  const messageRef = hasRef
    ? {
      blockchainName: messageBch,
      blockNumber,
      blockHash,
    }
    : null;

  if (messageRef) {
    setMessageReactionState(messageRef, message?.likedByMe === true ? 'liked' : 'unliked');
  }

  return {
    localNumber,
    authorLogin: message?.authorLogin || 'автор',
    body: resolvedText || (Number(message?.versionsTotal || 1) > 1 ? 'удалено' : '(пусто)'),
    versionsTotal: Number(message?.versionsTotal || 1),
    versions: Array.isArray(message?.versions) ? message.versions : [],
    likesCount: Number(message?.likesCount || 0),
    repliesCount: Number(message?.repliesCount || 0),
    timestampMs: resolveMessageTimestampMs(message),
    messageRef,
    msgSubType: Number(message?.msgSubType || 0),
    targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
      ? {
        blockchainName: String(message.targetBlockchainName).trim(),
        blockNumber: Number(message.targetBlockNumber),
        blockHash: normalizeMessageHash(message?.targetBlockHash),
      }
      : null,
    reactionState: messageRef ? getMessageReactionState(messageRef) : '',
    isOwnMessage: String(message?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase(),
  };
}

async function loadFromApi(route, channelId) {
  const currentSessionLogin = String(state.session.login || '').trim();
  const isAuthorized = !!currentSessionLogin;
  let cachedFeed = null;
  const ensureFeed = async () => {
    if (cachedFeed) return cachedFeed;
    if (!isAuthorized) {
      cachedFeed = {};
      return cachedFeed;
    }
    cachedFeed = await authService.listSubscriptionsFeed(currentSessionLogin, 1000);
    return cachedFeed;
  };
  const getAllRows = async () => {
    const feed = await ensureFeed();
    return [
      ...(Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : []),
      ...(Array.isArray(feed?.followedUsersChannels) ? feed.followedUsersChannels : []),
      ...(Array.isArray(feed?.followedChannels) ? feed.followedChannels : []),
    ];
  };

  let selector = buildSelectorFromRoute(route, channelId);
  if (selector?.ownerBlockchainName && selector?.channelName) {
    const routeOwnerRaw = String(selector.ownerBlockchainName || '').trim();
    const routeOwnerNormalized = routeOwnerRaw.toLowerCase();
    const routeOwnerLoginFromBch = extractLoginFromBlockchainName(routeOwnerRaw);
    let channel = null;
    if (isAuthorized) {
      const allRows = await getAllRows();
      channel = allRows.find((item) => (
        String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === routeOwnerNormalized
        && String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
      ));
      if (!channel) {
        channel = allRows.find((item) => (
          String(item?.channel?.ownerLogin || '').trim().toLowerCase() === routeOwnerNormalized
          && String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
        ));
      }
      if (!channel && !looksLikeBlockchainName(routeOwnerRaw)) {
        try {
          const ownerUser = await authService.getUser(routeOwnerRaw);
          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() === selector.channelName.toLowerCase()
            ));
          }
        } catch {
          // ignore fallback lookup failures
        }
      }
    }
    if (!channel) {
      const ownerLoginForLookup = routeOwnerLoginFromBch || (!looksLikeBlockchainName(routeOwnerRaw) ? routeOwnerRaw : '');
      if (ownerLoginForLookup) {
      try {
        const ownerFeed = await authService.listSubscriptionsFeed(ownerLoginForLookup, 500);
        const ownerRows = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
        channel = ownerRows.find((item) => (
          String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === routeOwnerNormalized
          && String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
        ));
      } catch {
        // ignore owner feed lookup failures
      }
    }
    }
    if (!channel?.channel?.ownerBlockchainName || channel?.channel?.channelRoot?.blockNumber == null) {
      throw new Error('Канал не найден.');
    }
    selector = {
      ownerBlockchainName: String(channel.channel.ownerBlockchainName),
      channelRootBlockNumber: Number(channel.channel.channelRoot.blockNumber),
      channelRootBlockHash: normalizeRouteHash(channel.channel.channelRoot.blockHash),
      channelName: selector.channelName,
    };
  }

  if (!selector?.ownerBlockchainName || selector.channelRootBlockNumber == null) {
    throw new Error('Не удалось определить канал из адреса страницы.');
  }

  const payload = await authService.getChannelMessages(selector, 200, 'asc', currentSessionLogin);
  const messages = Array.isArray(payload.messages) ? payload.messages : [];
  let reverseChannelMissingWarning = '';
  let mergedMessages = [...messages];

  const currentLogin = currentSessionLogin;
  const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
  const channelName = String(payload.channel?.channelName || '').trim();
  const channelTypeCode = Number(payload.channel?.channelTypeCode ?? 1);
  const canResolveReverse = (
    channelTypeCode === CHANNEL_TYPE_PERSONAL
    && !!currentLogin
    && !!ownerLogin
    && !!channelName
    && ownerLogin.toLowerCase() === currentLogin.toLowerCase()
  );

  if (canResolveReverse) {
    const allRows = await getAllRows();
    const reverseSummary = allRows.find((item) => (
      Number(item?.channel?.channelTypeCode ?? 1) === CHANNEL_TYPE_PERSONAL
      && String(item?.channel?.ownerLogin || '').trim().toLowerCase() === channelName.toLowerCase()
      && String(item?.channel?.channelName || '').trim().toLowerCase() === currentLogin.toLowerCase()
    ));

    if (reverseSummary?.channel?.ownerBlockchainName && reverseSummary?.channel?.channelRoot?.blockNumber != null) {
      const reverseSelector = {
        ownerBlockchainName: String(reverseSummary.channel.ownerBlockchainName),
        channelRootBlockNumber: Number(reverseSummary.channel.channelRoot.blockNumber),
        channelRootBlockHash: normalizeRouteHash(reverseSummary.channel.channelRoot.blockHash),
      };
      const reversePayload = await authService.getChannelMessages(reverseSelector, 200, 'asc', currentSessionLogin);
      const reverseMessages = Array.isArray(reversePayload?.messages) ? reversePayload.messages : [];
      mergedMessages = mergedMessages.concat(reverseMessages);
    } else {
      reverseChannelMissingWarning = `У собеседника ${channelName} пока не создан ответный персональный чат.`;
    }
  }

  const posts = mergedMessages
    .map((message, index) => mapApiMessageToPost(message, selector, index + 1))
    .sort((a, b) => {
      const byTime = Number(a?.timestampMs || 0) - Number(b?.timestampMs || 0);
      if (byTime !== 0) return byTime;
      const aNum = Number(a?.messageRef?.blockNumber || 0);
      const bNum = Number(b?.messageRef?.blockNumber || 0);
      return aNum - bNum;
    })
    .map((post, index) => ({ ...post, localNumber: index + 1 }));
  const isOwnChannel = ownerLogin.toLowerCase() === currentSessionLogin.toLowerCase();
  const followedRows = Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : [];
  const isSubscribed = isAuthorized && followedRows.some((row) => (
    String(row?.channel?.ownerBlockchainName || '') === String(selector.ownerBlockchainName || '')
    && Number(row?.channel?.channelRoot?.blockNumber) === Number(selector.channelRootBlockNumber)
    && normalizeRouteHash(row?.channel?.channelRoot?.blockHash) === normalizeRouteHash(selector.channelRootBlockHash)
  ));

  return {
    channel: {
      name: payload.channel?.channelName || 'неизвестный канал',
      displayName: `${ownerLogin || 'неизвестно'}/${payload.channel?.channelName || 'неизвестный канал'}`,
      description: String(payload.channel?.channelDescription || '').trim(),
      ownerName: ownerLogin || 'неизвестно',
    },
    posts,
    reverseChannelMissingWarning,
    isOwnChannel,
    isSubscribed,
    selector,
  };
}

function renderLoadError(screen, navigate, message, onRetry) {
  const card = document.createElement('div');
  card.className = 'card stack channels-status';
  card.innerHTML = `
    <strong>Не удалось загрузить канал</strong>
    <p class="meta-muted">${message || 'Проверьте подключение к серверу и повторите попытку.'}</p>
  `;

  const retry = document.createElement('button');
  retry.type = 'button';
  retry.className = 'primary-btn';
  retry.textContent = 'Повторить';
  retry.addEventListener('click', onRetry);

  const back = document.createElement('button');
  back.type = 'button';
  back.className = 'secondary-btn';
  back.textContent = 'Назад к каналам';
  back.addEventListener('click', () => navigate('channels-list'));

  card.append(retry, back);
  screen.append(card);
}

function renderDemoFallback(screen, navigate, error) {
  const info = document.createElement('div');
  info.className = 'card stack';
  info.innerHTML = `
    <strong>Включен демо-режим</strong>
    <p class="meta-muted">Данные канала с сервера недоступны. Показан демо-контент.</p>
    <p class="meta-muted">${toUserMessage(error, 'Ошибка API/WS')}</p>
  `;
  screen.append(info);

  const back = document.createElement('button');
  back.className = 'secondary-btn';
  back.textContent = 'Назад к каналам';
  back.addEventListener('click', () => navigate('channels-list'));
  screen.append(back);
}

function scrollChannelToBottom(screen, smooth = true) {
  const feed = screen.querySelector('.channel-feed');
  if (feed) {
    feed.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'end' });
  }
  const appScreen = document.getElementById('app-screen');
  if (appScreen) {
    appScreen.scrollTo({ top: appScreen.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
    return;
  }
  window.scrollTo({ top: document.body.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
}

function applyPendingScroll(screen, routeKey, forceBottom = false) {
  const target = pendingScrollByRoute.get(routeKey);
  if (!target && !forceBottom) return;

  const doScroll = () => {
    if (!target && forceBottom) {
      scrollChannelToBottom(screen, false);
      return;
    }

    if (target === '__LAST__') {
      scrollChannelToBottom(screen, true);
      pendingScrollByRoute.delete(routeKey);
      return;
    }

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

  setTimeout(doScroll, 20);
}

function renderPostCard(post, {
  navigate,
  selector,
  onToggleLike,
  onReply,
  onRepost,
  onShare,
  onEdit,
}) {
  const versionsTotal = Number(post?.versionsTotal || 1);

  const card = document.createElement('article');
  card.className = 'card stack channel-message-card';

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

  const avatar = createMessageAvatar(post.authorLogin);

  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 = post.authorLogin;

  const numberEl = document.createElement('span');
  numberEl.className = 'author-line-num';
  numberEl.textContent = `· #${post.localNumber}`;

  const timestamp = document.createElement('div');
  timestamp.className = 'channel-message-time';
  timestamp.textContent = post.timestampMs ? formatRelativeTime(post.timestampMs) : '—';

  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: `История #${post.localNumber}`,
        versions: post.versions,
      });
    });
    title.append(editedMarker);
  }
  authorBlock.append(title, timestamp);
  authorTile.append(avatar, authorBlock);
  authorTile.addEventListener('click', (event) => {
    event.stopPropagation();
    const cleanLogin = String(post.authorLogin || '').trim();
    if (!cleanLogin) return;
    navigate(makeProfileRoute(cleanLogin));
  });

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

  card.append(authorTile, body);

  const refKey = messageRefKey(post.messageRef);
  if (refKey) {
    card.dataset.messageKey = refKey;
  }
  card.classList.add('is-counters-visible');

  if (!post.messageRef || !selector) return card;

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

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

  const likeButton = document.createElement('button');
  likeButton.type = 'button';
  likeButton.className = 'channel-action-item channel-action-like';
  const isLiked = post.reactionState === 'liked';
  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">${post.likesCount || 0}</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;
    const labelEl = likeButton.querySelector('.channel-action-label');
    if (labelEl) labelEl.textContent = 'Лайк...';
    await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
  });

  const replyButton = document.createElement('button');
  replyButton.type = 'button';
  replyButton.className = 'channel-action-item channel-action-reply';
  replyButton.innerHTML = `
    <span class="channel-action-icon" aria-hidden="true">💬</span>
    <span class="channel-action-label">Ответить</span>
    <span class="channel-action-counter">${post.repliesCount || 0}</span>
  `;
  replyButton.addEventListener('click', (event) => {
    event.stopPropagation();
    animatePress(event.currentTarget);
    openReplyModal({
      navigate,
      onSubmit: async (text) => onReply(post.messageRef, text),
    });
  });
  // Репосты временно отключены до будущей реализации.
  // Точка возврата: Dev_Docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md
  actions.append(likeButton, replyButton);

  const shareButton = document.createElement('button');
  shareButton.type = 'button';
  shareButton.className = 'channel-action-item channel-action-share';
  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);
    const route = buildThreadRoute(post.messageRef, selector);
    await onShare(route);
  });

  actions.append(shareButton);
  if (post.msgSubType === 30 && post.targetRef?.blockchainName && Number.isFinite(post.targetRef?.blockNumber) && post.targetRef?.blockHash) {
    const originalBtn = document.createElement('button');
    originalBtn.type = 'button';
    originalBtn.className = 'channel-action-item';
    originalBtn.innerHTML = `
      <span class="channel-action-icon" aria-hidden="true">↪</span>
      <span class="channel-action-label">Оригинал</span>
    `;
    originalBtn.addEventListener('click', (event) => {
      event.stopPropagation();
      const ownerLogin = extractLoginFromBlockchainName(post.targetRef.blockchainName);
      if (!ownerLogin) return;
      const ok = window.confirm('Перейти к оригинальному сообщению?');
      if (!ok) return;
      navigate(makeShineMessageRoute({
        ownerLogin,
        messageBlockchainName: post.targetRef.blockchainName,
        messageBlockNumber: post.targetRef.blockNumber,
      }));
    });
    actions.append(originalBtn);
  }
  if (post.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(post.body || '').trim() === 'удалено' ? '' : post.body,
        onSave: async (nextText) => onEdit(post.messageRef, nextText, { isDelete: false }),
        onDelete: async () => onEdit(post.messageRef, '', { isDelete: true }),
      });
    });
    actions.append(editButton);
  }
  card.append(actions);
  card.addEventListener('click', () => {
    const route = buildThreadRoute(post.messageRef, selector);
    if (route) navigate(route);
  });
  return card;
}

function renderBody(screen, navigate, routeKey, channelData, handlers) {
  if (channelData.reverseChannelMissingWarning) {
    const reverseWarning = document.createElement('p');
    reverseWarning.className = 'channel-head-meta';
    reverseWarning.textContent = channelData.reverseChannelMissingWarning;
    screen.append(reverseWarning);
  }

  const actionButton = document.createElement('button');
  actionButton.className = 'destructive-btn channel-main-action';
  actionButton.textContent = 'Подписаться на канал';

  const feed = document.createElement('div');
  feed.className = 'stack channel-feed';
  const postsByKey = new Map();

  if (channelData.posts.length) {
    channelData.posts.forEach((post) => {
      const row = renderPostCard(post, {
        navigate,
        selector: channelData.selector,
        onToggleLike: handlers.onToggleLike,
        onReply: handlers.onReply,
        onRepost: handlers.onRepost,
        onShare: handlers.onShare,
        onEdit: handlers.onEdit,
      });
      const key = messageRefKey(post.messageRef);
      if (key) {
        postsByKey.set(key, post);
      }
      feed.append(row);
    });
  } else {
    const empty = document.createElement('div');
    empty.className = 'card meta-muted';
    empty.textContent = 'Ждем ваших начинаний';
    feed.append(empty);
  }


  if (!channelData.isSubscribed) {
    actionButton.addEventListener('click', handlers.onSubscribeChannel);
  }

  const backButton = document.createElement('button');
  backButton.className = 'secondary-btn channel-back-btn';
  backButton.textContent = 'Назад к каналам';
  backButton.addEventListener('click', () => navigate('channels-list'));

  if (channelData.isOwnChannel) {
    screen.append(feed);
  } else if (!channelData.isSubscribed) {
    screen.append(actionButton, feed, backButton);
  } else {
    screen.append(feed, backButton);
  }

  applyPendingScroll(screen, routeKey, channelData.isOwnChannel);
  return () => {
    // noop
  };
}

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 channelId = route.params.channelId || '';
  const routeSelector = buildSelectorFromRoute(route, channelId);
  const routeKey = `${routeSelector?.ownerBlockchainName || ''}:${routeSelector?.channelRootBlockNumber || ''}:${routeSelector?.channelRootBlockHash || ''}`;

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

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

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

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

  const rerender = () => {
    const current = document.querySelector('section.channels-screen--channel');
    if (!current) return;
    const next = render({ navigate, route });
    current.replaceWith(next);
  };
  let activeSelector = null;

  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 onToggleLike = async (messageRef, action) => {
    const actionKey = makeReactionActionKey(messageRef);
    if (!actionKey) {
      throw new Error('Некорректная ссылка на сообщение для реакции.');
    }
    if (pendingReactionActions.has(actionKey)) return;

    const previousReaction = getMessageReactionState(messageRef);
    const nextReaction = action === 'unlike' ? 'unliked' : 'liked';
    pendingReactionActions.add(actionKey);

    try {
      const { login, storagePwd } = requireSigningSession();
      if (action === 'unlike') {
        await authService.addBlockUnlike({ login, storagePwd, message: messageRef });
      } else {
        await authService.addBlockLike({ login, storagePwd, message: messageRef });
      }
      setMessageReactionState(messageRef, nextReaction);
      softHaptic(10);
      rerender();
    } catch (error) {
      setMessageReactionState(messageRef, previousReaction || 'unliked');
      rerender();
      throw error;
    } finally {
      pendingReactionActions.delete(actionKey);
    }
  };

  const onReply = async (messageRef, text) => {
    const { login, storagePwd } = requireSigningSession();
    await authService.addBlockReply({ login, storagePwd, message: messageRef, text });

    const scrollTarget = messageRefKey(messageRef);
    if (scrollTarget) pendingScrollByRoute.set(routeKey, scrollTarget);

    softHaptic(15);
    showToast('Ответ отправлен');
    rerender();
  };

  const loadOwnedChannelsForRepost = async (login) => {
    const feed = await authService.listSubscriptionsFeed(login, 1000);
    const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
    return rows
      .map((row) => {
        const selector = {
          ownerBlockchainName: String(row?.channel?.ownerBlockchainName || '').trim(),
          channelRootBlockNumber: Number(row?.channel?.channelRoot?.blockNumber),
          channelRootBlockHash: normalizeRouteHash(row?.channel?.channelRoot?.blockHash),
        };
        if (!selector.ownerBlockchainName || !Number.isFinite(selector.channelRootBlockNumber) || selector.channelRootBlockNumber < 0) {
          return null;
        }
        return {
          ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
          channelName: String(row?.channel?.channelName || '').trim(),
          selector,
        };
      })
      .filter(Boolean);
  };

  const isSameChannelSelector = (a, b) => (
    String(a?.ownerBlockchainName || '').trim() === String(b?.ownerBlockchainName || '').trim()
    && Number(a?.channelRootBlockNumber) === Number(b?.channelRootBlockNumber)
    && normalizeRouteHash(a?.channelRootBlockHash) === normalizeRouteHash(b?.channelRootBlockHash)
  );

  const onRepost = async (messageRef) => {
    const { login, storagePwd } = requireSigningSession();
    const channels = await loadOwnedChannelsForRepost(login);
    if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');
    openRepostModal({
      navigate,
      channels,
      onSubmit: async ({ channel, text }) => {
        await authService.addBlockRepost({
          login,
          storagePwd,
          channel,
          message: messageRef,
          text,
        });
        if (isSameChannelSelector(channel, activeSelector)) {
          pendingScrollByRoute.set(routeKey, '__LAST__');
          rerender();
        }
        softHaptic(12);
        showToast('Репост опубликован');
      },
    });
  };

  const onShare = async (routePath) => {
    try {
      const routeToShare = String(routePath || '').trim();
      if (!routeToShare) throw new Error('Не удалось подготовить ссылку на сообщение.');
      const result = await shareOrCopyLink({
        title: 'SHiNE · Каналы',
        text: 'Тред из канала SHiNE',
        url: buildAbsoluteRouteUrl(routeToShare),
      });
      if (result === 'copied') showToast('Ссылка скопирована');
      if (result === 'shared') showToast('Ссылка передана');
      if (result === 'shared' || result === 'copied') softHaptic(10);
    } catch (error) {
      showStatus(toUserMessage(error, 'Не удалось отправить ссылку.'));
    }
  };

  const onAddPost = async (bodyText) => {
    const { login, storagePwd } = requireSigningSession();
    if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
      throw new Error('Идентификатор канала не готов.');
    }

    await authService.addBlockTextPost({
      login,
      storagePwd,
      channel: activeSelector,
      text: bodyText,
    });

    pendingScrollByRoute.set(routeKey, '__LAST__');
    softHaptic(15);
    showToast('Сообщение отправлено');
    rerender();
  };

  const onEditPost = async (messageRef, text) => {
    const { login, storagePwd } = requireSigningSession();
    if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
      throw new Error('Идентификатор канала не готов.');
    }
    await authService.addBlockEditMessage({
      login,
      storagePwd,
      message: messageRef,
      text,
      isChannelPost: true,
      channel: activeSelector,
    });
    softHaptic(12);
    showToast('Сообщение обновлено');
    rerender();
  };

  screen.append(header);
  screen.append(statusBox);

  const skeleton = renderSkeleton(screen);

  let cleanupSeenTracking = null;

  (async () => {
    try {
      const apiData = await loadFromApi(route, channelId);
      activeSelector = apiData?.selector || null;
      const channelRouteLabel = `Канал: ${apiData?.channel?.ownerName || 'owner'}/${apiData?.channel?.name || 'channel'}`;
      const ownChannelLabel = `Ваш канал: ${apiData?.channel?.name || 'channel'}`;
      if (channelHeaderButton) {
        channelHeaderButton.textContent = apiData?.isOwnChannel ? ownChannelLabel : channelRouteLabel;
        channelHeaderButton.disabled = false;
        channelHeaderButton.onclick = (event) => {
          animatePress(event.currentTarget);
          openAboutChannelModal(apiData.channel);
        };
      }
      if (apiData?.isOwnChannel) {
        const headerActions = header.querySelector('.header-actions');
        if (headerActions) {
          const addBtn = document.createElement('button');
          addBtn.type = 'button';
          addBtn.className = 'icon-btn channel-header-add-btn';
          addBtn.textContent = 'Добавить сообщение';
          addBtn.addEventListener('click', (event) => {
            animatePress(event.currentTarget);
            openAddMessageModal({
              channelName: apiData?.channel?.name || '',
              navigate,
              onSubmit: async (bodyText) => {
                try {
                  await onAddPost(bodyText);
                  showStatus('');
                } catch (error) {
                  throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
                }
              },
            });
          });
          headerActions.append(addBtn);
        }
      }
      skeleton.remove();
      cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
        onToggleLike: async (messageRef, action) => {
          try {
            await onToggleLike(messageRef, action);
            showStatus('');
          } catch (error) {
            showStatus(toUserMessage(error, action === 'unlike' ? 'Не удалось убрать лайк.' : 'Не удалось поставить лайк.'));
          }
        },
        onReply: async (messageRef, text) => {
          try {
            await onReply(messageRef, text);
            showStatus('');
          } catch (error) {
            throw new Error(toUserMessage(error, 'Не удалось отправить ответ.'));
          }
        },
        onRepost: async (messageRef) => {
          try {
            await onRepost(messageRef);
            showStatus('');
          } catch (error) {
            showStatus(toUserMessage(error, 'Не удалось сделать репост.'));
          }
        },
        onAddPost: async (bodyText) => {
          try {
            await onAddPost(bodyText);
            showStatus('');
          } catch (error) {
            throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
          }
        },
        onShare: onShare,
        onEdit: async (messageRef, text) => {
          try {
            await onEditPost(messageRef, text);
            showStatus('');
          } catch (error) {
            throw new Error(toUserMessage(error, 'Не удалось изменить сообщение.'));
          }
        },
        onSubscribeChannel: async (event) => {
          animatePress(event?.currentTarget);
          try {
            const { login, storagePwd } = requireSigningSession();
            if (!apiData.selector) throw new Error('Не удалось определить канал для подписки.');
            const targetName = `${apiData.channel?.ownerName || 'user'}/${apiData.channel?.name || 'channel'}`;
            const ok = window.confirm(`Подписаться на канал ${targetName}?`);
            if (!ok) return;

            await authService.addBlockFollowChannel({
              login,
              storagePwd,
              targetBlockchainName: apiData.selector.ownerBlockchainName,
              targetBlockNumber: apiData.selector.channelRootBlockNumber,
              targetBlockHashHex: apiData.selector.channelRootBlockHash,
              unfollow: false,
            });

            const feed = await authService.listSubscriptionsFeed(login, 200);
            setChannelsFeed(feed, state.channelsIndex);
            softHaptic(15);
            showToast('Подписка на канал выполнена');
            rerender();
          } catch (error) {
            showStatus(toUserMessage(error, 'Не удалось подписаться на канал.'));
          }
        },
      });
    } catch (error) {
      skeleton.remove();
      if (isChannelsDemoMode()) {
        renderDemoFallback(screen, navigate, error);
        return;
      }
      renderLoadError(screen, navigate, toUserMessage(error, 'Не удалось загрузить канал.'), rerender);
    }
  })();

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

  return screen;
}
