import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadUserProfileCard } from '../services/user-connections.js';

export const pageMeta = { id: 'network-view', title: 'Связи' };

const GENDER_MALE = 'male';
const GENDER_FEMALE = 'female';
const GENDER_UNKNOWN = 'unknown';
const CENTER_NODE_ID = '__center__';

function normalizeLogin(value) {
  return String(value || '').trim();
}

function normKey(value) {
  return normalizeLogin(value).toLowerCase();
}

function uniqueLogins(list) {
  const out = [];
  const seen = new Set();
  (Array.isArray(list) ? list : []).forEach((item) => {
    const login = normalizeLogin(item);
    if (!login) return;
    const key = normKey(login);
    if (seen.has(key)) return;
    seen.add(key);
    out.push(login);
  });
  return out;
}

function escapeHtml(text) {
  return String(text || '')
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

function normalizeGender(value) {
  const clean = String(value || '').trim().toLowerCase();
  if (clean === GENDER_MALE) return GENDER_MALE;
  if (clean === GENDER_FEMALE) return GENDER_FEMALE;
  return GENDER_UNKNOWN;
}

function toSet(list) {
  return new Set(uniqueLogins(list).map((value) => normKey(value)));
}

function hasLogin(setObj, login) {
  return setObj.has(normKey(login));
}

function getMarkByLogin(allUsers) {
  const map = new Map();
  (Array.isArray(allUsers) ? allUsers : []).forEach((row) => {
    const login = normalizeLogin(row?.login);
    if (!login) return;
    map.set(normKey(login), {
      login,
      official: Boolean(row?.official),
      shine: Boolean(row?.shine),
      officialLabel: String(row?.officialLabel || (row?.official ? 'официальный' : 'неофициальный')),
      shineLabel: String(row?.shineLabel || (row?.shine ? 'сияющий' : 'несияющий')),
      avatar: normalizeAvatar(row),
    });
  });
  return map;
}

function normalizeAvatar(row) {
  const txFromAvatar = String(row?.avatar?.ar || '').trim();
  if (txFromAvatar) return { ar: txFromAvatar };
  const txFallback = String(row?.avatarTxId || '').trim();
  if (txFallback) return { ar: txFallback };
  return null;
}

function applyRelativeGender(map, rows) {
  (Array.isArray(rows) ? rows : []).forEach((row) => {
    const login = normalizeLogin(row?.login);
    if (!login) return;
    const key = normKey(login);
    const gender = normalizeGender(row?.gender);
    const prev = map.get(key) || GENDER_UNKNOWN;
    if (prev === GENDER_UNKNOWN || gender !== GENDER_UNKNOWN) map.set(key, gender);
  });
}

function getRelativeGenderMap(graph) {
  const map = new Map();
  applyRelativeGender(map, graph?.parents);
  applyRelativeGender(map, graph?.children);
  applyRelativeGender(map, graph?.siblings);
  applyRelativeGender(map, graph?.spouses);
  return map;
}

function relativeRoleLabel(role, gender) {
  const cleanGender = normalizeGender(gender);
  if (role === 'parent') {
    if (cleanGender === GENDER_MALE) return 'отец';
    if (cleanGender === GENDER_FEMALE) return 'мать';
    return 'родитель';
  }
  if (role === 'child') {
    if (cleanGender === GENDER_MALE) return 'сын';
    if (cleanGender === GENDER_FEMALE) return 'дочь';
    return 'потомок';
  }
  if (role === 'sibling') {
    if (cleanGender === GENDER_MALE) return 'брат';
    if (cleanGender === GENDER_FEMALE) return 'сестра';
    return 'брат/сестра';
  }
  if (role === 'spouse') {
    if (cleanGender === GENDER_MALE) return 'муж';
    if (cleanGender === GENDER_FEMALE) return 'жена';
    return 'жена/муж';
  }
  return '';
}

function buildNameLines(firstName, lastName) {
  const first = String(firstName || '').trim();
  const last = String(lastName || '').trim();
  if (first && last) {
    const full = `${first} ${last}`.trim();
    if (full.length <= 20) return [full];
    return [first, last];
  }
  if (first) return [first];
  if (last) return [last];
  return [];
}

function applyNodeText(node, {
  login,
  firstName = '',
  lastName = '',
  role = 'friend',
  gender = GENDER_UNKNOWN,
  mark = null,
} = {}) {
  const loginText = normalizeLogin(login);
  const labelsWrap = node.querySelector('.node-label');
  const nameEl = node.querySelector('.node-name');
  const loginEl = node.querySelector('.node-login');
  const relationEl = node.querySelector('.node-relation');
  if (!(labelsWrap instanceof HTMLElement) || !(nameEl instanceof HTMLElement) || !(loginEl instanceof HTMLElement)) {
    return;
  }

  const nameLines = buildNameLines(firstName, lastName);
  nameEl.innerHTML = '';
  if (nameLines.length) {
    nameLines.forEach((line) => {
      const lineEl = document.createElement('span');
      lineEl.className = 'node-name-line';
      lineEl.textContent = line;
      nameEl.append(lineEl);
    });
    labelsWrap.classList.remove('is-login-only');
  } else {
    labelsWrap.classList.add('is-login-only');
  }
  loginEl.textContent = loginText;

  const relLabel = relativeRoleLabel(role, gender);
  if (relationEl instanceof HTMLElement) {
    relationEl.textContent = relLabel;
    relationEl.hidden = !relLabel;
  }

  const metaParts = [];
  if (mark?.officialLabel) metaParts.push(mark.officialLabel);
  if (mark?.shineLabel) metaParts.push(mark.shineLabel);
  if (relLabel) metaParts.push(`роль: ${relLabel}`);
  const titleMain = nameLines.length ? `${nameLines.join(' ')} (${loginText})` : loginText;
  node.title = metaParts.length ? `${titleMain}\n${metaParts.join(', ')}` : titleMain;
}

function spread(count, start, end) {
  if (count <= 0) return [];
  if (count === 1) return [(start + end) / 2];
  const out = [];
  const step = (end - start) / (count - 1);
  for (let i = 0; i < count; i += 1) out.push(start + step * i);
  return out;
}

function buildNodeElement({
  login,
  kind = 'friend',
  isCenter = false,
  mark = null,
  role = 'friend',
  gender = GENDER_UNKNOWN,
  firstName = '',
  lastName = '',
}) {
  const node = document.createElement('button');
  node.type = 'button';
  const classes = [
    'node',
    isCenter ? 'center' : '',
    kind === 'relative' ? 'is-relative' : 'is-friend',
    mark?.shine ? 'is-shine' : '',
    mark?.official ? 'is-official' : '',
  ].filter(Boolean);
  node.className = classes.join(' ');
  node.dataset.nodeLogin = login;

  if (mark?.official) {
    const officialBadge = document.createElement('span');
    officialBadge.className = 'node-badge-official';
    officialBadge.setAttribute('aria-hidden', 'true');
    officialBadge.textContent = 'ОФ';
    node.append(officialBadge);
  }
  node.append(renderUserAvatar({
    login,
    avatar: mark?.avatar || null,
    size: 'node',
    title: login,
  }));

  const label = document.createElement('span');
  label.className = 'node-label';
  label.innerHTML = `
    <span class="node-name"></span>
    <span class="node-login"></span>
    <span class="node-relation" hidden></span>
  `;
  node.append(label);
  applyNodeText(node, { login, firstName, lastName, role, gender, mark });
  return node;
}

function buildGraphModel(graph, centerLogin) {
  const login = normalizeLogin(graph?.login || centerLogin || state.session.login);
  const outFriends = toSet(graph?.outFriends);
  const inFriends = toSet(graph?.inFriends);
  const outParents = toSet(graph?.outParents);
  const inParents = toSet(graph?.inParents);
  const outChildren = toSet(graph?.outChildren);
  const inChildren = toSet(graph?.inChildren);
  const outSiblings = toSet(graph?.outSiblings);
  const inSiblings = toSet(graph?.inSiblings);
  const outSpouses = toSet(graph?.outSpouses);
  const inSpouses = toSet(graph?.inSpouses);

  const relativesGender = getRelativeGenderMap(graph);
  const allMarks = getMarkByLogin(graph?.allUsers);

  const allLogins = uniqueLogins([
    ...(graph?.outFriends || []),
    ...(graph?.inFriends || []),
    ...(graph?.outParents || []),
    ...(graph?.inParents || []),
    ...(graph?.outChildren || []),
    ...(graph?.inChildren || []),
    ...(graph?.outSiblings || []),
    ...(graph?.inSiblings || []),
    ...(graph?.outSpouses || []),
    ...(graph?.inSpouses || []),
  ]).filter((entry) => normKey(entry) !== normKey(login));

  const relations = allLogins.map((targetLogin) => {
    const parentOut = hasLogin(outParents, targetLogin);
    const parentIn = hasLogin(inChildren, targetLogin);
    const childOut = hasLogin(outChildren, targetLogin);
    const childIn = hasLogin(inParents, targetLogin);
    const siblingOut = hasLogin(outSiblings, targetLogin);
    const siblingIn = hasLogin(inSiblings, targetLogin);
    const spouseOut = hasLogin(outSpouses, targetLogin);
    const spouseIn = hasLogin(inSpouses, targetLogin);
    const friendOut = hasLogin(outFriends, targetLogin);
    const friendIn = hasLogin(inFriends, targetLogin);

    let role = 'friend';
    if (parentOut || parentIn) role = 'parent';
    else if (childOut || childIn) role = 'child';
    else if (spouseOut || spouseIn) role = 'spouse';
    else if (siblingOut || siblingIn) role = 'sibling';

    let forward = friendOut;
    let backward = friendIn;
    if (role === 'parent') {
      forward = parentOut;
      backward = parentIn;
    } else if (role === 'child') {
      forward = childOut;
      backward = childIn;
    } else if (role === 'spouse') {
      forward = spouseOut;
      backward = spouseIn;
    } else if (role === 'sibling') {
      forward = siblingOut;
      backward = siblingIn;
    }

    return {
      login: targetLogin,
      key: normKey(targetLogin),
      role,
      isRelative: role !== 'friend',
      gender: normalizeGender(relativesGender.get(normKey(targetLogin))),
      forward: Boolean(forward),
      backward: Boolean(backward),
      mark: allMarks.get(normKey(targetLogin)) || null,
    };
  });

  return {
    centerLogin: login,
    centerMark: allMarks.get(normKey(login)) || null,
    relations,
  };
}

function splitByGender(list) {
  const left = [];
  const right = [];
  const center = [];
  list.forEach((item) => {
    if (item.gender === GENDER_FEMALE) left.push(item);
    else if (item.gender === GENDER_MALE) right.push(item);
    else center.push(item);
  });
  return { left, right, center };
}

function sortByLogin(items) {
  return [...items].sort((a, b) => a.login.localeCompare(b.login, 'ru', { sensitivity: 'base' }));
}

function positionRows(nodes, x, yStart, yEnd) {
  const ys = spread(nodes.length, yStart, yEnd);
  return nodes.map((node, index) => ({ ...node, x, y: ys[index] }));
}

function layoutNodes(model) {
  const centerNode = {
    id: CENTER_NODE_ID,
    login: model.centerLogin,
    x: 50,
    y: 50,
    isCenter: true,
    kind: 'center',
    relation: null,
    gender: GENDER_UNKNOWN,
    mark: model.centerMark,
  };

  const parents = sortByLogin(model.relations.filter((item) => item.role === 'parent'));
  const children = sortByLogin(model.relations.filter((item) => item.role === 'child'));
  const spouses = sortByLogin(model.relations.filter((item) => item.role === 'spouse'));
  const siblings = sortByLogin(model.relations.filter((item) => item.role === 'sibling'));
  const friends = sortByLogin(model.relations.filter((item) => item.role === 'friend'));

  const parentSplit = splitByGender(parents);
  const childSplit = splitByGender(children);
  const spouseSplit = splitByGender(spouses);
  const siblingSplit = splitByGender(siblings);

  const friendLeft = [];
  const friendRight = [];
  friends.forEach((item, index) => {
    if (index % 2 === 0) friendLeft.push(item);
    else friendRight.push(item);
  });

  const positioned = [
    ...positionRows(parentSplit.left, 28, 16, 28),
    ...positionRows(parentSplit.right, 72, 16, 28),
    ...positionRows(parentSplit.center, 50, 10, 22),
    ...positionRows(friendLeft, 12, 30, 70),
    ...positionRows(friendRight, 88, 30, 70),
    ...positionRows(spouseSplit.left, 36, 38, 58),
    ...positionRows(spouseSplit.right, 64, 38, 58),
    ...positionRows(spouseSplit.center, 50, 40, 56),
    ...positionRows(siblingSplit.left, 30, 46, 66),
    ...positionRows(siblingSplit.right, 70, 46, 66),
    ...positionRows(siblingSplit.center, 50, 54, 70),
    ...positionRows(childSplit.left, 28, 68, 84),
    ...positionRows(childSplit.right, 72, 68, 84),
    ...positionRows(childSplit.center, 50, 78, 88),
  ];

  const nodes = [centerNode];
  const edges = [];

  positioned.forEach((item) => {
    const nodeId = item.key;
    nodes.push({
      id: nodeId,
      login: item.login,
      x: item.x,
      y: item.y,
      isCenter: false,
      kind: item.isRelative ? 'relative' : 'friend',
      relation: item.role,
      gender: item.gender,
      mark: item.mark,
    });
    edges.push({
      from: item.forward ? CENTER_NODE_ID : nodeId,
      to: item.forward ? nodeId : CENTER_NODE_ID,
      mutual: item.forward && item.backward,
      isRelative: item.isRelative,
      exists: item.forward || item.backward,
    });
  });

  return { nodes, edges };
}

function marker(svg, id, color) {
  const el = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
  el.setAttribute('id', id);
  el.setAttribute('viewBox', '0 0 10 10');
  el.setAttribute('refX', '9');
  el.setAttribute('refY', '5');
  el.setAttribute('markerUnits', 'strokeWidth');
  el.setAttribute('markerWidth', '6');
  el.setAttribute('markerHeight', '6');
  el.setAttribute('orient', 'auto');

  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
  path.setAttribute('fill', color);
  el.append(path);
  svg.append(el);
}

function getNodeCenter(boardRect, node) {
  const dot = node.querySelector('.node-dot');
  if (!(dot instanceof HTMLElement)) return null;
  const rect = dot.getBoundingClientRect();
  return {
    x: rect.left - boardRect.left + (rect.width / 2),
    y: rect.top - boardRect.top + (rect.height / 2),
    radius: rect.width / 2,
  };
}

function shortenLine(fromPoint, toPoint, fromOffset, toOffset) {
  const dx = toPoint.x - fromPoint.x;
  const dy = toPoint.y - fromPoint.y;
  const len = Math.hypot(dx, dy);
  if (len < 1) return null;
  const ux = dx / len;
  const uy = dy / len;
  return {
    x1: fromPoint.x + (ux * fromOffset),
    y1: fromPoint.y + (uy * fromOffset),
    x2: toPoint.x - (ux * toOffset),
    y2: toPoint.y - (uy * toOffset),
  };
}

function renderEdges(svg, board, nodeElements, edges) {
  svg.innerHTML = '';
  const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  marker(defs, 'network-arrow-friend', 'rgba(120, 179, 255, 0.95)');
  marker(defs, 'network-arrow-relative', 'rgba(255, 159, 94, 0.95)');
  svg.append(defs);

  const boardRect = board.getBoundingClientRect();
  const centers = new Map();
  nodeElements.forEach((value, key) => {
    const pt = getNodeCenter(boardRect, value);
    if (pt) centers.set(key, pt);
  });

  edges.forEach((edge) => {
    if (!edge.exists) return;
    const from = centers.get(edge.from);
    const to = centers.get(edge.to);
    if (!from || !to) return;

    const cut = shortenLine(from, to, from.radius + 3, to.radius + 3);
    if (!cut) return;

    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', String(cut.x1));
    line.setAttribute('y1', String(cut.y1));
    line.setAttribute('x2', String(cut.x2));
    line.setAttribute('y2', String(cut.y2));
    line.setAttribute('class', `network-link ${edge.isRelative ? 'is-relative' : 'is-friend'}`);

    if (!edge.mutual) {
      line.setAttribute('marker-end', `url(#${edge.isRelative ? 'network-arrow-relative' : 'network-arrow-friend'})`);
    }
    svg.append(line);
  });
}

let persistedCenterLogin = '';
let persistedCenterHistory = [];

export function render({ navigate, route }) {
  const keepHistory = String(route?.params?.mode || '').trim().toLowerCase() === 'keep-history';
  if (!keepHistory) {
    persistedCenterLogin = '';
    persistedCenterHistory = [];
  }

  const screen = document.createElement('section');
  screen.className = 'network-screen';
  const appScreenEl = document.getElementById('app-screen');
  appScreenEl?.classList.add('network-scroll-lock');

  const stage = document.createElement('div');
  stage.className = 'network-stage';

  const board = document.createElement('div');
  board.className = 'network-board network-board--full';

  const profileCardCache = new Map();
  let centerLogin = normalizeLogin(persistedCenterLogin || state.session.login || '');
  let centerHistory = Array.isArray(persistedCenterHistory) ? [...persistedCenterHistory] : [];
  let redrawEdges = () => {};
  let loadSeq = 0;

  function profileInfoRoute(login) {
    const cleanLogin = normalizeLogin(login);
    if (!cleanLogin) return '';
    if (normKey(cleanLogin) === normKey(state.session.login)) return 'profile-view';
    return `user/${encodeURIComponent(cleanLogin)}/${encodeURIComponent('network-view/keep-history')}`;
  }

  function helpText() {
    return [
      'Обозначения на экране связей:',
      '• Синие линии — близкие друзья.',
      '• Оранжевые линии — родственники.',
      '• Стрелка на линии — односторонняя связь.',
      '• Если стрелки нет, связь взаимная.',
    ].join('\n');
  }

  function persistHistory() {
    persistedCenterLogin = centerLogin;
    persistedCenterHistory = [...centerHistory];
  }

  function setBackButtonState(backBtn) {
    if (!(backBtn instanceof HTMLButtonElement)) return;
    backBtn.disabled = centerHistory.length === 0;
  }

  function openSearchModal() {
    const root = document.getElementById('modal-root');
    if (!(root instanceof HTMLElement)) return;
    root.innerHTML = `
      <div class="modal" id="network-search-modal">
        <div class="modal-card stack">
          <button class="icon-btn" type="button" id="network-search-close" style="justify-self:end;">✕</button>
          <h3 class="modal-title">Найти человека</h3>
          <div class="row" style="gap:8px;">
            <input class="input" id="network-search-input" type="text" maxlength="30" placeholder="Введите логин" />
            <button class="primary-btn" type="button" id="network-search-run">Искать</button>
          </div>
          <div class="meta-muted" id="network-search-meta">Введите логин и нажмите «Искать».</div>
          <div class="stack" id="network-search-results"></div>
          <div class="form-actions-grid">
            <button class="ghost-btn" type="button" id="network-search-profile" disabled>Показать профиль</button>
            <button class="primary-btn" type="button" id="network-search-graph" disabled>Показать связи</button>
          </div>
          <button class="secondary-btn" type="button" id="network-search-ok" disabled>OK</button>
        </div>
      </div>
    `;

    const modal = root.querySelector('#network-search-modal');
    const closeBtn = root.querySelector('#network-search-close');
    const inputEl = root.querySelector('#network-search-input');
    const runBtn = root.querySelector('#network-search-run');
    const metaEl = root.querySelector('#network-search-meta');
    const resultsEl = root.querySelector('#network-search-results');
    const profileBtn = root.querySelector('#network-search-profile');
    const graphBtn = root.querySelector('#network-search-graph');
    const okBtn = root.querySelector('#network-search-ok');
    if (!(modal instanceof HTMLElement) || !(inputEl instanceof HTMLInputElement) || !(resultsEl instanceof HTMLElement)) {
      root.innerHTML = '';
      return;
    }

    let selectedLogin = '';
    let searchSeq = 0;

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

    const applySelection = (login) => {
      selectedLogin = normalizeLogin(login);
      const rows = resultsEl.querySelectorAll('[data-candidate]');
      rows.forEach((row) => {
        if (!(row instanceof HTMLElement)) return;
        row.classList.toggle('is-selected', String(row.dataset.candidate || '') === selectedLogin);
      });
      const hasSelected = Boolean(selectedLogin);
      if (profileBtn instanceof HTMLButtonElement) profileBtn.disabled = !hasSelected;
      if (graphBtn instanceof HTMLButtonElement) graphBtn.disabled = !hasSelected;
      if (okBtn instanceof HTMLButtonElement) okBtn.disabled = !hasSelected;
    };

    const renderCandidates = (logins) => {
      const items = (Array.isArray(logins) ? logins : [])
        .map((item) => normalizeLogin(item))
        .filter(Boolean)
        .slice(0, 5);
      if (!items.length) {
        resultsEl.innerHTML = '<div class="meta-muted">Кандидаты не найдены.</div>';
        applySelection('');
        return;
      }
      resultsEl.innerHTML = items.map((login) => (
        `<button type="button" class="ghost-btn network-search-candidate" data-candidate="${escapeHtml(login)}">${escapeHtml(login)}</button>`
      )).join('');
      applySelection('');
    };

    const runSearch = async () => {
      const query = normalizeLogin(inputEl.value);
      if (!query) {
        metaEl.textContent = 'Введите логин.';
        renderCandidates([]);
        return;
      }
      const reqId = ++searchSeq;
      metaEl.textContent = `Поиск по «${query}»...`;
      if (runBtn instanceof HTMLButtonElement) runBtn.disabled = true;
      try {
        const found = await authService.searchUsers(query);
        if (reqId !== searchSeq) return;
        renderCandidates(found);
        const foundCount = Math.min(5, Array.isArray(found) ? found.length : 0);
        metaEl.textContent = foundCount > 0
          ? `Найдено кандидатов: ${foundCount}. Выберите одного.`
          : 'Кандидаты не найдены.';
      } catch (error) {
        if (reqId !== searchSeq) return;
        renderCandidates([]);
        metaEl.textContent = `Ошибка поиска: ${error?.message || 'unknown'}`;
      } finally {
        if (runBtn instanceof HTMLButtonElement) runBtn.disabled = false;
      }
    };

    modal.addEventListener('click', (event) => {
      if (event.target === modal) close();
    });
    closeBtn?.addEventListener('click', close);
    runBtn?.addEventListener('click', () => { void runSearch(); });
    inputEl.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        void runSearch();
      }
    });
    resultsEl.addEventListener('click', (event) => {
      const target = event.target;
      if (!(target instanceof HTMLElement)) return;
      const button = target.closest('[data-candidate]');
      if (!(button instanceof HTMLElement)) return;
      applySelection(String(button.dataset.candidate || ''));
    });
    profileBtn?.addEventListener('click', () => {
      if (!selectedLogin) return;
      const routeTo = profileInfoRoute(selectedLogin);
      if (!routeTo) return;
      close();
      navigate(routeTo);
    });
    graphBtn?.addEventListener('click', () => {
      if (!selectedLogin) return;
      close();
      void load(selectedLogin, { pushHistory: true });
    });
    okBtn?.addEventListener('click', () => {
      if (!selectedLogin) return;
      metaEl.textContent = `Выбран пользователь «${selectedLogin}». Используйте кнопки ниже.`;
    });

    window.setTimeout(() => inputEl.focus(), 0);
  }

  function getProfileCardCached(login) {
    const cleanLogin = normalizeLogin(login);
    const key = normKey(cleanLogin);
    if (!cleanLogin) return Promise.resolve(null);
    if (!profileCardCache.has(key)) {
      profileCardCache.set(key, loadUserProfileCard(cleanLogin).catch(() => null));
    }
    return profileCardCache.get(key);
  }

  async function hydrateNodeProfiles(layout, nodeElements, requestId) {
    const uniqueNodes = [];
    const seen = new Set();
    layout.nodes.forEach((nodeModel) => {
      const key = normKey(nodeModel.login);
      if (!key || seen.has(key)) return;
      seen.add(key);
      uniqueNodes.push(nodeModel);
    });

    const cards = await Promise.all(uniqueNodes.map((nodeModel) => getProfileCardCached(nodeModel.login)));
    if (requestId !== loadSeq) return;

    const cardByKey = new Map();
    cards.forEach((card) => {
      const login = normalizeLogin(card?.login);
      if (!login) return;
      cardByKey.set(normKey(login), card);
    });

    layout.nodes.forEach((nodeModel) => {
      const node = nodeElements.get(nodeModel.id);
      if (!(node instanceof HTMLElement)) return;
      const card = cardByKey.get(normKey(nodeModel.login));
      const cardGender = normalizeGender(card?.gender);
      applyNodeText(node, {
        login: nodeModel.login,
        firstName: card?.firstName || '',
        lastName: card?.lastName || '',
        role: nodeModel.relation || 'friend',
        gender: nodeModel.gender === GENDER_UNKNOWN ? cardGender : nodeModel.gender,
        mark: nodeModel.mark || null,
      });
    });

    requestAnimationFrame(() => redrawEdges());
  }

  function bindNodeInteraction(node, nodeModel) {
    node.addEventListener('click', () => {
      if (nodeModel.isCenter) {
        const routeTo = profileInfoRoute(nodeModel.login);
        if (routeTo) navigate(routeTo);
        return;
      }
      void load(nodeModel.login, { pushHistory: true });
    });
  }

  async function load(nextCenterLogin = '', { pushHistory = false } = {}) {
    const requestId = ++loadSeq;
    const prevCenter = centerLogin;
    const targetCenter = normalizeLogin(nextCenterLogin || prevCenter || state.session.login);

    try {
      const graph = await authService.getUserConnectionsGraph(targetCenter);
      if (requestId !== loadSeq) return;
      centerLogin = targetCenter;
      if (pushHistory && prevCenter && normKey(prevCenter) !== normKey(targetCenter)) {
        centerHistory.push(prevCenter);
      }

      const model = buildGraphModel(graph, targetCenter);
      const layout = layoutNodes(model);

      board.innerHTML = '';
      const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('class', 'network-svg');
      board.append(svg);

      const nodeElements = new Map();
      layout.nodes.forEach((nodeModel) => {
        const node = buildNodeElement({
          login: nodeModel.login,
          kind: nodeModel.kind,
          isCenter: nodeModel.isCenter,
          role: nodeModel.relation || 'friend',
          gender: nodeModel.gender,
          mark: nodeModel.mark,
        });
        node.style.left = `${nodeModel.x}%`;
        node.style.top = `${nodeModel.y}%`;
        board.append(node);
        nodeElements.set(nodeModel.id, node);
        bindNodeInteraction(node, nodeModel);
      });

      redrawEdges = () => renderEdges(svg, board, nodeElements, layout.edges);
      requestAnimationFrame(() => redrawEdges());
      void hydrateNodeProfiles(layout, nodeElements, requestId);
      persistHistory();
      setBackButtonState(backBtnEl);
    } catch (error) {
      if (requestId !== loadSeq) return;
      window.alert(`Ошибка загрузки связей: ${error?.message || 'unknown'}`);
    }
  }

  const header = renderHeader({
    title: 'Связи',
    leftAction: {
      label: '←',
      onClick: () => {
        if (!centerHistory.length) return;
        const prev = centerHistory.pop();
        if (!prev) {
          setBackButtonState(backBtnEl);
          return;
        }
        void load(prev, { pushHistory: false });
      },
    },
    rightActions: [
      { label: 'Найти', onClick: openSearchModal },
      { label: '?', onClick: () => window.alert(helpText()) },
    ],
  });
  const backBtnEl = header.querySelector('.header-left .icon-btn');
  setBackButtonState(backBtnEl);

  const onResize = () => redrawEdges();
  window.addEventListener('resize', onResize);

  let observer = null;
  if (typeof ResizeObserver !== 'undefined') {
    observer = new ResizeObserver(() => redrawEdges());
    observer.observe(board);
  }

  screen.cleanup = () => {
    window.removeEventListener('resize', onResize);
    if (observer) observer.disconnect();
    appScreenEl?.classList.remove('network-scroll-lock');
  };

  if (keepHistory && centerLogin) {
    void load(centerLogin, { pushHistory: false });
  } else {
    centerLogin = normalizeLogin(state.session.login || '');
    centerHistory = [];
    persistHistory();
    void load(centerLogin, { pushHistory: false });
  }
  setBackButtonState(backBtnEl);

  header.classList.add('network-header-overlay');
  stage.append(board, header);
  screen.append(stage);
  return screen;
}
