/**
 * Hermes Hub Web Client
 * Vanilla JavaScript (ES2022) — No npm, no build, no framework.
 * Single source of truth: docs/web-api/CONTRACT.md
 */

// ── CONFIGURATION & STATE ──
const USE_MOCK_FIXTURE = false;

let lastAppliedSeq = -1;
// Какой профиль сейчас открыт в окне аккаунта: нужно, чтобы перерисовывать
// его по свежему снапшоту, а не оставлять с заглушкой.
let _openAccountModalProfile = null;
let currentSnapshot = null;
let activeView = 'overview';
let pollTimer = null;
let pollIntervalMs = 5000;
let authToken = localStorage.getItem('hermes_hub_token') || '';
let cachedEvents = [];
let currentSettings = {};
let currentDragState = null;
let latestUpdateInfo = null;

const CANONICAL_ROLE_DESCRIPTIONS = {
  orchestrator: 'Главный оркестратор команды и маршрутизатор запросов',
  'coder-primary': 'Основная разработка кода и реализация задач',
  'coder-secondary': 'Вспомогательная разработка и параллельные задачи',
  reviewer: 'Ревью кода, аудит изменений и контроль качества',
  research: 'Read-only поиск в кодовой базе и сбор фактов',
  fast: 'Оперативные вызовы, быстрые проверки и тесты',
};

function getProviderIdFromName(name) {
  if (!name) return '';
  const n = name.toLowerCase();
  if (n.includes('antigravity') || n.includes('google')) return 'antigravity';
  if (n.includes('codex') || n.includes('openai')) return 'openai-codex';
  if (n.includes('opencode') || n.includes('go')) return 'opencode-go';
  if (n.includes('claude') || n.includes('anthropic')) return 'claude';
  if (n.includes('grok') || n.includes('xai')) return 'grok';
  return name;
}

// ── DOM ELEMENTS ──
const elements = {
  navItems: document.querySelectorAll('.nav-item'),
  viewPanes: document.querySelectorAll('.view-pane'),
  pageTitle: document.getElementById('page-title'),
  navAccountsCount: document.getElementById('nav-accounts-count'),
  headerReadinessBadge: document.getElementById('header-readiness-badge'),
  headerReadinessText: document.getElementById('header-readiness-text'),
  sourceText: document.getElementById('source-text'),
  sourceDot: document.querySelector('#source-indicator .status-dot'),
  accountsContainer: document.getElementById('accounts-container'),
  accountsSearch: document.getElementById('accounts-search'),
  filterProvider: document.getElementById('filter-provider'),
  filterHealth: document.getElementById('filter-health'),
  accountsStatsSummary: document.getElementById('accounts-stats-summary'),
  btnRefreshAll: document.getElementById('btn-refresh-all'),
  btnAddAccount: document.getElementById('btn-add-account'),
  modalBackdrop: document.getElementById('modal-backdrop'),
  modalTitle: document.getElementById('modal-title'),
  modalBody: document.getElementById('modal-body'),
  modalFooter: document.getElementById('modal-footer'),
  modalCloseBtn: document.getElementById('modal-close-btn'),
  toastContainer: document.getElementById('toast-container'),
};

// ── INITIALIZATION ──
document.addEventListener('DOMContentLoaded', () => {
  initNavigation();
  initEventListeners();
  initSettings();
  fetchSnapshot();
  startPolling();
  checkUpdates(true);
});

// ── NAVIGATION ──
function initNavigation() {
  document.querySelectorAll('.nav-item').forEach((btn) => {
    btn.addEventListener('click', () => {
      const view = btn.dataset.view;
      switchView(view);
    });
  });
}

function switchView(viewName) {
  activeView = viewName;
  document.querySelectorAll('.nav-item').forEach((btn) => {
    btn.classList.toggle('active', btn.dataset.view === viewName);
  });
  document.querySelectorAll('.view-pane').forEach((pane) => {
    pane.classList.toggle('active', pane.id === `view-${viewName}`);
  });

  const titles = {
    overview: 'Обзор системы',
    accounts: 'Аккаунты и квоты',
    routing: 'Главный экран управления маршрутизацией',
    analytics: 'Аналитика и телеметрия',
    health: 'Состояние и диагностика',
    logs: 'Журнал событий',
    settings: 'Настройки Hermes Hub',
  };
  if (elements.pageTitle) {
    elements.pageTitle.textContent = titles[viewName] || 'Hermes Hub';
  }

  if (currentSnapshot) {
    renderCurrentView();
  }
}

// ── EVENT LISTENERS ──
function initEventListeners() {
  if (elements.accountsSearch) elements.accountsSearch.addEventListener('input', () => renderAccountsView());
  if (elements.filterProvider) elements.filterProvider.addEventListener('change', () => renderAccountsView());
  if (elements.filterHealth) elements.filterHealth.addEventListener('change', () => renderAccountsView());

  if (elements.btnRefreshAll) {
    elements.btnRefreshAll.addEventListener('click', () => {
      executeAction('refresh_all', {});
    });
  }

  if (elements.btnAddAccount) {
    elements.btnAddAccount.addEventListener('click', () => {
      openAddAccountWizard();
    });
  }

  const btnAutoAssign = document.getElementById('btn-auto-assign');
  if (btnAutoAssign) {
    btnAutoAssign.addEventListener('click', () => {
      executeAction('auto_assign_all', {});
    });
  }

  if (elements.modalCloseBtn) elements.modalCloseBtn.addEventListener('click', closeModal);
  if (elements.modalBackdrop) {
    elements.modalBackdrop.addEventListener('click', (e) => {
      if (e.target === elements.modalBackdrop) closeModal();
    });
  }

  // Logs view filters
  const logsSearch = document.getElementById('logs-search');
  const logsFilterLevel = document.getElementById('logs-filter-level');
  const logsFilterCategory = document.getElementById('logs-filter-category');
  const btnRefreshLogs = document.getElementById('btn-refresh-logs');

  if (logsSearch) logsSearch.addEventListener('input', () => renderLogsList());
  if (logsFilterLevel) logsFilterLevel.addEventListener('change', () => renderLogsList());
  if (logsFilterCategory) logsFilterCategory.addEventListener('change', () => renderLogsList());
  if (btnRefreshLogs) btnRefreshLogs.addEventListener('click', () => fetchLogs());

  // Settings view buttons
  const btnSaveHub = document.getElementById('btn-save-hub-settings');
  if (btnSaveHub) btnSaveHub.addEventListener('click', () => saveHubServerSettings());

  const themeSel = document.getElementById('setting-theme');
  if (themeSel) {
    themeSel.addEventListener('change', () => {
      applyTheme(themeSel.value);
    });
  }

  // Updates event listeners
  const btnHeaderUpdate = document.getElementById('header-update-badge');
  if (btnHeaderUpdate) {
    btnHeaderUpdate.addEventListener('click', () => openUpdateModal());
  }

  const btnCheckUpdates = document.getElementById('btn-check-updates');
  if (btnCheckUpdates) {
    btnCheckUpdates.addEventListener('click', () => checkUpdates(false));
  }

  const btnApplyUpdate = document.getElementById('btn-apply-update');
  if (btnApplyUpdate) {
    btnApplyUpdate.addEventListener('click', () => applyUpdate());
  }

  // Preflight check listener
  const btnPreflight = document.getElementById('btn-run-preflight');
  if (btnPreflight) {
    btnPreflight.addEventListener('click', () => runPreflightChecks());
  }
}


// ── SNAPSHOT INGESTION & MONOTONIC SEQ ──
async function fetchSnapshot() {
  const urlParams = new URLSearchParams(window.location.search);
  const forceFixture = USE_MOCK_FIXTURE || urlParams.get('fixture') === '1' || window.location.protocol === 'file:';

  if (forceFixture) {
    try {
      const res = await fetch('snapshot.example.json');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      setSourceIndicator(true, 'Фикстура (snapshot.example.json)');
      applySnapshot(data);
      return;
    } catch (err) {
      console.error('Failed to load snapshot.example.json:', err);
      setSourceIndicator(false, 'Ошибка фикстуры');
      return;
    }
  }

  try {
    const headers = {};
    if (authToken) headers['X-Hub-Token'] = authToken;

    const res = await fetch('/api/snapshot', { headers });
    if (res.status === 401) {
      handleUnauthorized();
      return;
    }
    if (!res.ok) {
      if (res.status === 404 || res.status === 502 || res.status === 503) {
        throw new Error(`Server returned ${res.status}`);
      }
      const errBody = await res.json().catch(() => ({}));
      showToast(errBody.error || `Ошибка сервера: ${res.status}`, 'error');
      setSourceIndicator(false, `Ошибка /api/snapshot (${res.status})`);
      return;
    }

    const data = await res.json();
    setSourceIndicator(true, 'Live API (/api/snapshot)');
    applySnapshot(data);
  } catch (err) {
    // Раньше здесь при недоступном API молча подставлялась
    // snapshot.example.json — 63 чужих профиля с почтами user@example.test —
    // и индикатор ставился в зелёное. Экран выглядел здоровой панелью,
    // которая не имеет отношения к этому серверу: при работе через SSH-туннель
    // достаточно промахнуться портом или потерять туннель, чтобы принять
    // пример за свои данные. Осознанная работа с фикстурой осталась —
    // ?fixture=1 и открытие файлом, — но подставлять её вместо ответа сервера
    // нельзя: отсутствие данных должно читаться как отсутствие данных.
    console.warn('Live API unavailable:', err);
    setSourceIndicator(false, 'Сервер недоступен');
    showToast('Сервер не отвечает. Данные на экране могут быть устаревшими.', 'error');
  }
}

function applySnapshot(snapshot) {
  if (!snapshot || typeof snapshot !== 'object') return;

  // Monotonic seq check: reject out-of-order stale responses
  if (typeof snapshot.seq === 'number') {
    if (snapshot.seq < lastAppliedSeq) {
      console.warn(`[Hub] Stale snapshot rejected: seq ${snapshot.seq} < lastAppliedSeq ${lastAppliedSeq}`);
      return;
    }
    lastAppliedSeq = snapshot.seq;
  }

  const isFirstLoad = !currentSnapshot;
  currentSnapshot = snapshot;
  updateGlobalHeader();

  // Открытое окно аккаунта рисовалось один раз и на опрос не реагировало.
  // Если его открыть до того, как придут живые квоты, оно навсегда
  // оставалось с заглушкой («Grok 2h — Н/Д») и со статусом «Не проверялся»
  // даже после успешной проверки. Перерисовываем по свежему снапшоту.
  if (_openAccountModalProfile) {
    try {
      openAccountDetailsModal(_openAccountModalProfile, true);
    } catch (e) {
      console.warn('[Hub] Не удалось обновить окно аккаунта:', e);
    }
  }

  if (isFirstLoad) {
    const params = new URLSearchParams(window.location.search);
    const targetView = params.get('view');
    const targetModal = params.get('modal');
    const targetProfile = params.get('profile');

    if (targetView) {
      switchView(targetView);
    } else {
      renderCurrentView();
    }

    if (targetModal === 'grok_wizard') {
      openAddAccountWizard();
      showWizardStep2('grok');
    } else if (targetModal === 'antigravity_wizard') {
      openAddAccountWizard();
      showWizardStep2('antigravity');
    } else if (targetModal === 'account_details') {
      openAccountDetailsModal(targetProfile || 'ag-w1');
    } else if (targetModal === 'agent_model') {
      const targetRole = params.get('role') || 'coder-primary';
      const ag = (currentSnapshot.agents || []).find(a => a.role_id === targetRole) || (currentSnapshot.agents || [])[1];
      if (ag) openAgentModelModal(ag.role_id, ag.assigned_profile_id);
    }
  } else {
    renderCurrentView();
  }
}

function setSourceIndicator(healthy, text) {
  if (elements.sourceDot) {
    elements.sourceDot.className = `status-dot ${healthy ? 'healthy' : 'error'}`;
  }
  if (elements.sourceText) {
    elements.sourceText.textContent = text;
  }
}

function startPolling() {
  if (pollTimer) clearInterval(pollTimer);
  if (pollIntervalMs > 0) {
    pollTimer = setInterval(fetchSnapshot, pollIntervalMs);
  }
}

// Подключён ли профиль на самом деле.
//
// A26 определял это как «health_state не равен not_configured», а поле
// authenticated в модели вообще отсутствует, поэтому первая половина условия
// была мертва. Через фильтр проходили холодный резерв (health_state
// "disabled") и непроверенные пустые слоты: при нуле настоящих аккаунтов
// страница показывала три карточки «Холодный резерв», а «Обзор» предлагал
// назначать роли на пустые слоты — то самое мышление слотами, ради отмены
// которого задание и делалось.
//
// Authoritative признак — auth_state: у подключённого AUTHENTICATED, у
// пустого слота и у холодного резерва NOT_CONFIGURED. Состояния
// AUTH_REQUIRED и AUTH_EXPIRED означают подключённый аккаунт, которому нужен
// повторный вход, — их показываем.
function isConnectedProfile(p) {
  if (!p) return false;
  const st = String(p.auth_state || '').toUpperCase();
  if (st) return st !== 'NOT_CONFIGURED';
  // Запасной путь, если поле не пришло: судим по наличию опознанного аккаунта.
  return Boolean(p.email);
}

// ── КОПИРОВАНИЕ В БУФЕР ──
//
// navigator.clipboard существует только в защищённом контексте: HTTPS или
// localhost. Когда хаб открыт по сети (http://192.168.1.81:5800), его нет
// вовсе, и кнопки «Копировать» молча не работали — хуже того, показывали
// «Ссылка скопирована», потому что промис никто не проверял.
// Запасной путь — execCommand('copy'), он работает и по HTTP.
async function copyToClipboard(text, okMessage) {
  try {
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(text);
      showToast(okMessage || 'Скопировано', 'success');
      return true;
    }
  } catch (err) {
    console.warn('clipboard API недоступен:', err);
  }

  try {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.setAttribute('readonly', '');
    ta.style.position = 'fixed';
    ta.style.top = '-1000px';
    document.body.appendChild(ta);
    ta.select();
    const ok = document.execCommand('copy');
    document.body.removeChild(ta);
    if (ok) {
      showToast(okMessage || 'Скопировано', 'success');
      return true;
    }
  } catch (err) {
    console.warn('execCommand copy не сработал:', err);
  }

  // Молчать нельзя: владелец решит, что скопировалось, и вставит старое.
  showToast('Скопировать не удалось — выделите текст в поле и нажмите Ctrl+C', 'warning');
  return false;
}

// ── ЗАПРОС ТОКЕНА ПРИ 401 ──
//
// Сервер, привязанный не к localhost, требует X-Hub-Token. Раньше клиент этого
// случая не знал вовсе: 401 попадал в общую ветку ошибок и превращался в
// красный тост «Ошибка сервера: 401», который повторялся на каждом опросе.
// Владелец видел пустую панель и не имел ни одной подсказки, что нужен токен
// и где его взять. Теперь спрашиваем прямо, один раз.
let _tokenPromptOpen = false;

function handleUnauthorized() {
  if (_tokenPromptOpen) return;
  _tokenPromptOpen = true;
  // Опрос останавливаем, иначе окно ввода будет перекрываться новыми 401
  // каждые несколько секунд. Отдельной stopPolling в клиенте нет — таймер
  // гасится напрямую, как это делает startPolling перед перезапуском.
  if (pollTimer) {
    clearInterval(pollTimer);
    pollTimer = null;
  }
  setSourceIndicator(false, 'Требуется токен доступа');

  elements.modalTitle.textContent = 'Требуется токен доступа';
  elements.modalBody.innerHTML = `
    <div class="modal-feedback info" style="margin-bottom:14px;">
      Этот хаб открыт по сети, поэтому запросы к нему требуют токен.
      Он был показан при выполнении <code>enable_lan_access.py</code> и хранится
      на сервере в <code>~/.hermes/hub_settings.json</code>.
    </div>
    <label style="display:block; font-weight:600; margin-bottom:4px;">Токен доступа:</label>
    <input type="password" class="input-text" style="width:100%;" id="auth-token-input"
           placeholder="Вставьте токен" autocomplete="off">
    <div id="auth-token-feedback" style="font-size:12px; color:var(--text-muted); margin-top:8px;">
      Токен сохранится в этом браузере и больше спрашиваться не будет.
    </div>
  `;
  elements.modalFooter.innerHTML = `
    <button class="btn btn-primary" onclick="saveAuthTokenFromPrompt()">Сохранить и продолжить</button>
  `;
  showModal();
  setTimeout(() => {
    const el = document.getElementById('auth-token-input');
    if (el) el.focus();
  }, 50);
}

async function saveAuthTokenFromPrompt() {
  const input = document.getElementById('auth-token-input');
  const feedback = document.getElementById('auth-token-feedback');
  if (!input) return;
  const value = (input.value || '').trim();
  if (!value) {
    feedback.innerHTML = '<span style="color:var(--status-warning);">Поле пустое.</span>';
    return;
  }

  // Заголовки HTTP переносят только ASCII. Без этой проверки fetch бросает
  // TypeError и обработчик умирает целиком, не показав ничего: так бывает,
  // если вместе с токеном скопировали русский текст или лишний символ.
  if (!/^[!-~]+$/.test(value)) {
    feedback.innerHTML = '<span style="color:var(--status-error);">В токене посторонние символы. Скопируйте только сам токен, без кавычек, пробелов и текста вокруг.</span>';
    return;
  }

  feedback.textContent = 'Проверяем…';
  let res;
  try {
    res = await fetch('/api/snapshot', { headers: { 'X-Hub-Token': value } });
  } catch (err) {
    feedback.innerHTML = `<span style="color:var(--status-error);">Не удалось обратиться к серверу: ${escapeHtml(err.message)}</span>`;
    return;
  }
  if (res.status === 401) {
    feedback.innerHTML = '<span style="color:var(--status-error);">Токен не подошёл. Проверьте, что скопирован целиком.</span>';
    return;
  }
  if (!res.ok) {
    feedback.innerHTML = `<span style="color:var(--status-error);">Сервер ответил ${res.status}.</span>`;
    return;
  }

  authToken = value;
  localStorage.setItem('hermes_hub_token', authToken);
  const tokenInput = document.getElementById('setting-client-token-input');
  if (tokenInput) tokenInput.value = authToken;

  _tokenPromptOpen = false;
  closeModal();
  showToast('Токен принят', 'success');
  startPolling();
  fetchSnapshot();
}

// ── ACTIONS EXECUTION (POST /api/action) ──
async function executeAction(actionName, actionData = {}) {
  showToast(`Выполняется «${actionName}»...`, 'info');
  try {
    const headers = { 'Content-Type': 'application/json' };
    if (authToken) headers['X-Hub-Token'] = authToken;

    const res = await fetch('/api/action', {
      method: 'POST',
      headers,
      body: JSON.stringify({ action: actionName, data: actionData }),
    });

    if (res.status === 401) {
      handleUnauthorized();
      return { ok: false, message: 'Требуется токен доступа' };
    }

    const result = await res.json().catch(() => ({ ok: false, message: `Ошибка парсинга ответа (${res.status})` }));

    if (result.ok) {
      showToast(result.message || 'Действие выполнено успешно', 'success');
      fetchSnapshot();
      return result;
    } else {
      showToast(result.message || 'Отказ выполнения действия', 'warning');
      return result;
    }
  } catch (err) {
    console.error(`Action ${actionName} failed:`, err);
    showToast(`Ошибка сети: ${err.message}`, 'error');
    return { ok: false, message: `Ошибка сети: ${err.message}` };
  }
}

// ── GLOBAL HEADER ──
function updateGlobalHeader() {
  if (!currentSnapshot) return;

  const readiness = currentSnapshot.readiness || {};
  const allProfiles = Object.values(currentSnapshot.all_profiles || {});
  const connectedAccounts = readiness.accounts_connected_count ?? allProfiles.filter(
    (p) => isConnectedProfile(p)
  ).length;

  if (elements.navAccountsCount) elements.navAccountsCount.textContent = connectedAccounts;

  const isHealthy = readiness.state === 'healthy';
  const readyRoles = readiness.roles_ready_count || 0;
  const totalRoles = readiness.total_roles ?? 0;

  if (elements.headerReadinessBadge) {
    elements.headerReadinessBadge.className = `header-readiness-badge ${isHealthy ? 'text-healthy' : 'text-warning'}`;
  }
  if (elements.headerReadinessText) {
    elements.headerReadinessText.textContent = readiness.title_ru
      ? `${readiness.title_ru} (${readyRoles}/${totalRoles} ролей)`
      : 'Н/Д: состояние ещё не измерено';
  }

  const kpiReadiness = document.getElementById('kpi-system-readiness');
  const kpiSummary = document.getElementById('kpi-readiness-summary');
  const kpiTotalAccounts = document.getElementById('kpi-total-accounts');
  const kpiAccountsSub = document.getElementById('kpi-accounts-sub');
  const kpiReadyRoles = document.getElementById('kpi-ready-roles');
  const kpiRolesSub = document.getElementById('kpi-roles-sub');
  const kpiProvidersCount = document.getElementById('kpi-providers-count');

  if (kpiReadiness) kpiReadiness.textContent = readiness.title_ru || 'Работает';
  if (kpiSummary) kpiSummary.textContent = readiness.summary_ru || 'Все маршруты доступны';
  if (kpiTotalAccounts) kpiTotalAccounts.textContent = connectedAccounts;
  if (kpiAccountsSub) kpiAccountsSub.textContent = `Подключено: ${connectedAccounts}`;
  if (kpiReadyRoles) kpiReadyRoles.textContent = `${readyRoles}/${totalRoles}`;
  if (kpiRolesSub) kpiRolesSub.textContent = `${readyRoles} из ${totalRoles} ролей маршрутизации активны`;
  if (kpiProvidersCount) kpiProvidersCount.textContent = (currentSnapshot.providers || []).length || 5;
}

// ── VIEW ROUTER ──
function renderCurrentView() {
  if (!currentSnapshot) return;
  switch (activeView) {
    case 'overview':
      renderOverviewView();
      break;
    case 'accounts':
      renderAccountsView();
      break;
    case 'routing':
      renderRoutingView();
      break;
    case 'analytics':
      renderAnalyticsView();
      break;
    case 'health':
      renderHealthView();
      break;
    case 'logs':
      renderLogsView();
      break;
    case 'settings':
      renderSettingsView();
      break;
  }
}

// ═══════════════════════════════════════════════════════════════
//  1. ACCOUNTS VIEW (P0-2 Only Connected Accounts & Empty State)
// ═══════════════════════════════════════════════════════════════
function renderAccountsView() {
  const container = elements.accountsContainer;
  if (!container || !currentSnapshot) return;

  const allProfiles = Object.values(currentSnapshot.all_profiles || {});
  const totalConnectedInSystem = allProfiles.filter(
    (p) => isConnectedProfile(p)
  ).length;

  if (totalConnectedInSystem === 0) {
    container.innerHTML = `
      <div class="accounts-empty-state">
        <div class="empty-state-icon">👥</div>
        <h3>Нет подключённых аккаунтов</h3>
        <p>Подключите ваш первый аккаунт провайдера ИИ для распределения ролей и работы с Hermes Hub.</p>
        <button class="btn btn-primary" onclick="openAddAccountWizard()">+ Подключить аккаунт</button>
      </div>
    `;
    if (elements.accountsStatsSummary) {
      elements.accountsStatsSummary.innerHTML = 'Показано: <strong>0</strong> из <strong>0</strong> подключённых аккаунтов';
    }
    return;
  }

  const searchQuery = (elements.accountsSearch ? elements.accountsSearch.value : '').trim().toLowerCase();
  const providerFilter = elements.filterProvider ? elements.filterProvider.value : 'all';
  const healthFilter = elements.filterHealth ? elements.filterHealth.value : 'all';

  const providerNames = {
    antigravity: 'Google Antigravity',
    'openai-codex': 'OpenAI Codex',
    'opencode-go': 'OpenCode Go',
    claude: 'Claude (Anthropic)',
    grok: 'Grok (xAI)',
    local: 'Local LLM',
    'local-llm': 'Local LLM',
    'llama.cpp': 'Local LLM (llama.cpp)',
    ollama: 'Ollama',
    vllm: 'vLLM',
  };

  const profilesByProv = currentSnapshot.profiles_by_provider || {};
  let totalProfiles = 0;
  let visibleProfiles = 0;
  let html = '';

  for (const [providerId, profiles] of Object.entries(profilesByProv)) {
    if (providerFilter !== 'all' && providerFilter !== providerId) continue;

    const filtered = profiles.filter((p) => {
      const isConnected = isConnectedProfile(p);
      if (!isConnected) return false;

      totalProfiles++;
      const matchesSearch =
        !searchQuery ||
        (p.display_name && p.display_name.toLowerCase().includes(searchQuery)) ||
        (p.account_identity && p.account_identity.toLowerCase().includes(searchQuery)) ||
        (p.email && p.email.toLowerCase().includes(searchQuery)) ||
        (p.profile_id && p.profile_id.toLowerCase().includes(searchQuery)) ||
        (p.assigned_roles && p.assigned_roles.some((r) => r.toLowerCase().includes(searchQuery))) ||
        (p.preferred_models && p.preferred_models.some((m) => m.toLowerCase().includes(searchQuery)));

      const matchesHealth =
        healthFilter === 'all' ||
        p.health_state === healthFilter ||
        (healthFilter === 'disabled' && (p.is_cold_spare || !p.enabled || p.health_state === 'disabled'));

      return matchesSearch && matchesHealth;
    });

    filtered.sort((a, b) => {
      const rank = (p) => {
        if (p.health_state === 'not_configured') return 5;
        if (p.is_cold_spare || !p.enabled || p.health_state === 'disabled') return 4;
        if (p.health_state === 'not_tested') return 3;
        if (p.health_state === 'auth_required' || p.health_state === 'auth_expired') return 2;
        if (p.health_state === 'healthy') return 0;
        return 1;
      };
      const d = rank(a) - rank(b);
      if (d !== 0) return d;
      return (a.display_name || '').localeCompare(b.display_name || '', 'ru');
    });

    if (filtered.length === 0) continue;
    visibleProfiles += filtered.length;

    html += `
      <div class="provider-group">
        <div class="provider-group-header">
          <div class="provider-group-title">
            <span class="provider-dot" style="color: var(--prov-${providerId.replace('openai-', '').replace('-go', '')})">●</span>
            <span>${providerNames[providerId] || providerId}</span>
          </div>
          <div class="provider-group-count">${filtered.length} аккаунт(ов)</div>
        </div>
        <div class="accounts-grid">
          ${filtered.map((p) => renderAccountCard(p)).join('')}
        </div>
      </div>
    `;
  }

  container.innerHTML = html || '<div class="view-header-note">Аккаунты по заданным фильтрам не найдены.</div>';
  if (elements.accountsStatsSummary) {
    elements.accountsStatsSummary.innerHTML = `Показано: <strong>${visibleProfiles}</strong> из <strong>${totalProfiles}</strong> подключённых аккаунтов`;
  }

  container.querySelectorAll('.account-card').forEach((card) => {
    card.addEventListener('click', (e) => {
      // If click was on refresh button, skip modal
      if (e.target.closest('.btn-ghost')) return;
      const profileId = card.dataset.profileId;
      openAccountDetailsModal(profileId);
    });
  });
}

function renderAccountCard(profile) {
  const isMain = profile.is_main_account || profile.is_main_orchestrator;
  const roles = (profile.assigned_roles || []).join(', ') || 'Роль: Н/Д';
  const identity = profile.email || profile.account_identity || profile.display_name || profile.profile_id;
  const healthState = profile.health_state || 'unknown';
  const healthLabel = profile.health_label_ru || (profile.enabled ? 'Работает' : 'Отключён');
  const plan = profile.plan_code && profile.plan_code !== 'UNKNOWN' ? profile.plan_code : '';

  const quotaSnap = profile.quota_snapshot || (currentSnapshot.quotas || {})[profile.profile_id];
  const buckets = (quotaSnap && quotaSnap.buckets) ? quotaSnap.buckets : [];
  const unavailableReason = quotaSnap ? quotaSnap.unavailable_reason : null;

  let quotaGridHtml = '';

  if (buckets.length > 0) {
    const visibleBuckets = buckets.slice(0, 4);
    quotaGridHtml = `
      <div class="account-quota-grid ${visibleBuckets.length === 1 ? 'single-cell' : ''}">
        ${visibleBuckets.map((b) => renderQuotaCell(b, unavailableReason)).join('')}
      </div>
    `;
  } else {
    let reasonText = unavailableReason;
    if (!unavailableReason && quotaSnap && quotaSnap.is_loading) {
      reasonText = 'Загрузка квот…';
    } else if (!reasonText) {
      reasonText = (profile.health_state === 'not_configured' || profile.health_state === 'auth_required')
        ? 'Аккаунт не подключён'
        : 'Провайдер не отдаёт лимиты';
    }
    quotaGridHtml = `
      <div class="account-quota-grid single-cell">
        <div class="quota-cell">
          <div class="quota-cell-top">
            <span class="quota-cell-title">Квота</span>
            <span class="quota-cell-value text-muted">Н/Д</span>
          </div>
          <div class="quota-bar-track">
            <div class="quota-bar-fill" style="width: 0%; background-color: var(--status-disabled);"></div>
          </div>
          <div class="quota-cell-reset" title="${escapeHtml(reasonText)}">${escapeHtml(reasonText)}</div>
        </div>
      </div>
    `;
  }

  return `
    <div class="account-card ${isMain ? 'main-account' : ''}" data-profile-id="${escapeHtml(profile.profile_id)}">
      <div class="account-card-header">
        <div class="account-provider-tag">
          <span>${escapeHtml(profile.provider_display_name || profile.provider)}</span>
        </div>
        <div class="account-badges">
          ${plan ? `<span class="badge badge-plan">${escapeHtml(plan)}</span>` : ''}
          <span class="badge badge-status ${healthState}">● ${escapeHtml(healthLabel)}</span>
        </div>
      </div>

      <div class="account-identity-row">
        <div class="account-email" title="${escapeHtml(identity)}">${escapeHtml(identity)}</div>
        <div class="account-meta" title="${escapeHtml(profile.display_name)} • ${escapeHtml(roles)}">
          ${escapeHtml(profile.display_name)} • ${escapeHtml(roles)}
        </div>
      </div>

      ${quotaGridHtml}
    </div>
  `;
}

function renderQuotaCell(bucket, unavailableReason) {
  const remaining = bucket.remaining_percent;
  let formattedValue = 'Н/Д';
  let barWidth = 0;
  let colorClass = 'var(--status-disabled)';

  if (typeof remaining === 'number') {
    formattedValue = `${remaining.toFixed(1)}%`;
    barWidth = Math.max(0, Math.min(100, remaining));
    if (remaining <= 0) colorClass = 'var(--status-error)';
    else if (remaining < 20) colorClass = 'var(--status-warning)';
    else colorClass = 'var(--status-healthy)';
  } else if (unavailableReason) {
    formattedValue = 'Н/Д';
  }

  let resetText = bucket.reset_at
    ? `Сброс: ${formatIsoDate(bucket.reset_at)}`
    : (bucket.period ? `Период: ${bucket.period}` : (unavailableReason || 'Период провайдера'));

  return `
    <div class="quota-cell">
      <div class="quota-cell-top">
        <span class="quota-cell-title" title="${escapeHtml(bucket.display_name)}">${escapeHtml(bucket.display_name)}</span>
        <span class="quota-cell-value" style="color: ${colorClass}">${formattedValue}</span>
      </div>
      <div class="quota-bar-track">
        <div class="quota-bar-fill" style="width: ${barWidth}%; background-color: ${colorClass}"></div>
      </div>
      <div class="quota-cell-reset" title="${escapeHtml(resetText)}">${escapeHtml(resetText)}</div>
    </div>
  `;
}

// ═══════════════════════════════════════════════════════════════
//  1. OVERVIEW VIEW (P0-3, P0-4 Diagram Model Select & Counters)
// ═══════════════════════════════════════════════════════════════
function renderOverviewView() {
  if (typeof renderWorkflowOverview === 'function') {
    renderWorkflowOverview(currentSnapshot);
    return;
  }
  if (!currentSnapshot) return;

  const providers = currentSnapshot.providers || [];
  const readiness = currentSnapshot.readiness || {};
  let totalSlots = 0;
  let connectedSlots = 0;
  let onlineSlots = 0;
  let authRequiredSlots = 0;

  providers.forEach((p) => {
    totalSlots += p.total_slots || 0;
    connectedSlots += p.connected_count || 0;
    onlineSlots += p.online_count || 0;
    authRequiredSlots += p.auth_required_count || 0;
  });

  const kpiSystem = document.getElementById('kpi-system-readiness');
  const kpiSystemSub = document.getElementById('kpi-readiness-summary');
  if (kpiSystem) {
    kpiSystem.textContent = readiness.title_ru || 'Система готова';
    kpiSystem.className = `kpi-value ${readiness.state === 'healthy' ? 'text-healthy' : (readiness.state === 'warning' ? 'text-warning' : 'text-error')}`;
  }
  if (kpiSystemSub) {
    kpiSystemSub.textContent = readiness.summary_ru || 'Все ключевые роли обеспечены';
  }

  const kpiAccounts = document.getElementById('kpi-total-accounts');
  const kpiAccountsSub = document.getElementById('kpi-accounts-sub');
  if (kpiAccounts) {
    kpiAccounts.textContent = `${connectedSlots}/${totalSlots} слотов`;
  }
  if (kpiAccountsSub) {
    kpiAccountsSub.textContent = `Онлайн: ${onlineSlots} • Требуют входа: ${authRequiredSlots}`;
  }

  const kpiRoles = document.getElementById('kpi-ready-roles');
  const kpiRolesSub = document.getElementById('kpi-roles-sub');
  if (kpiRoles) {
    kpiRoles.textContent = `${readiness.roles_ready_count || 0}/${readiness.total_roles || 6}`;
  }
  if (kpiRolesSub) {
    kpiRolesSub.textContent = (readiness.roles_ready_count >= readiness.total_roles)
      ? 'Все роли обеспечены'
      : 'Требуется подключение аккаунтов';
  }

  const kpiProviders = document.getElementById('kpi-providers-count');
  const kpiProvidersSub = document.getElementById('kpi-providers-sub');
  if (kpiProviders) {
    const connectedProviders = providers.filter((p) => (p.connected_count || 0) > 0).length;
    kpiProviders.textContent = `${connectedProviders}/${providers.length}`;
  }
  if (kpiProvidersSub) {
    kpiProvidersSub.textContent = 'Подключено провайдеров ИИ';
  }

  const diagramBox = document.getElementById('overview-route-diagram');
  if (diagramBox) {
    const roles = currentSnapshot.routing || {};
    const allConnectedProfiles = Object.values(currentSnapshot.all_profiles || {}).filter(
      (p) => isConnectedProfile(p)
    );
    let diagramHtml = '';

    for (const [roleId, pipeline] of Object.entries(roles)) {
      const nodes = pipeline.nodes || [];

      diagramHtml += `
        <div class="diagram-column">
          <div class="diagram-column-header">${escapeHtml(pipeline.role_name_ru || roleId)}</div>
          ${nodes.map((node, idx) => {
            const profile = (currentSnapshot.all_profiles || {})[node.profile_id];
            const provId = profile?.provider || getProviderIdFromName(node.provider);
            const provSummary = (currentSnapshot.providers || []).find((p) => p.provider_id === provId || p.provider_name === node.provider);
            const discoveredModels = (provSummary && provSummary.discovered_models && provSummary.discovered_models.length > 0) ? provSummary.discovered_models : [];
            const currentModel = node.model || (profile && profile.preferred_models && profile.preferred_models[0]) || '';

            const hasCurrentInConnected = allConnectedProfiles.some((p) => p.profile_id === node.profile_id);
            let accountControlHtml = '';
            if (allConnectedProfiles.length > 0) {
              accountControlHtml = `
                <select class="diagram-account-select" title="Сменить назначенный аккаунт" onchange="handleNodeAccountChange('${escapeHtml(roleId)}', this.value, ${idx === 0})">
                  ${allConnectedProfiles.map((p) => `<option value="${escapeHtml(p.profile_id)}" ${p.profile_id === node.profile_id ? 'selected' : ''}>${escapeHtml(p.email || p.account_identity || p.display_name || p.profile_id)} (${escapeHtml(p.provider)})</option>`).join('')}
                  ${!hasCurrentInConnected && node.profile_id ? `<option value="${escapeHtml(node.profile_id)}" selected>${escapeHtml(node.display_name || node.profile_id)}</option>` : ''}
                </select>
              `;
            }

            let modelControlHtml = '';
            if (discoveredModels.length > 0) {
              modelControlHtml = `
                <select class="diagram-model-select" title="Сменить рабочую модель" onchange="handleNodeModelChange('${escapeHtml(roleId)}', '${escapeHtml(node.profile_id)}', this.value)">
                  ${discoveredModels.map((m) => `<option value="${escapeHtml(m)}" ${m === currentModel ? 'selected' : ''}>${escapeHtml(m)}</option>`).join('')}
                </select>
              `;
            } else {
              modelControlHtml = `
                <div style="font-size:10px; color:var(--text-muted); display:flex; align-items:center; justify-content:space-between;">
                  <span>Список моделей ещё не получен</span>
                  <button class="btn btn-secondary btn-xs" onclick="handleRefreshProviderModels('${escapeHtml(provId)}')">↻</button>
                </div>
              `;
            }

            return `
              <div class="diagram-node ${node.is_active ? 'active' : ''}">
                <div style="display:flex; justify-content:space-between; font-weight:600; font-size:11px;">
                  <span>${idx === 0 ? '★ Основной' : `Резерв ${idx}`}</span>
                  <span class="${node.is_active ? 'text-healthy' : 'text-muted'}">${node.is_active ? '● Активен' : 'Ожидание'}</span>
                </div>
                <div style="font-size:12px; font-weight:700; margin-top:2px;">${escapeHtml(node.account_identity && node.account_identity !== 'Аккаунт не добавлен' ? node.account_identity : (node.display_name || node.profile_id))}</div>
                <div style="font-size:10px; color:var(--text-secondary); margin-bottom:4px;">${escapeHtml(node.display_name || node.profile_id)} (${escapeHtml(node.provider)})</div>
                ${accountControlHtml}
                ${modelControlHtml}
              </div>
            `;
          }).join('') || '<div class="empty-text">Цепочка не задана</div>'}
        </div>
      `;
    }
    diagramBox.innerHTML = diagramHtml || '<div class="empty-text">Нет данных маршрутизации.</div>';
  }

  const provSummaryBox = document.getElementById('overview-providers-summary');
  if (provSummaryBox) {
    provSummaryBox.innerHTML = providers.map((prov) => `
      <div class="provider-summary-card">
        <div class="provider-summary-title">${escapeHtml(prov.provider_name || prov.provider_id)}</div>
        <div class="provider-summary-stats">
          Всего слотов: <strong>${prov.total_slots}</strong> •
          Подключено: <strong>${prov.connected_count}</strong> •
          Онлайн: <strong>${prov.online_count}</strong> •
          Требуют входа: <strong>${prov.auth_required_count}</strong> •
          Холодный резерв: <strong>${prov.cold_spare_count}</strong>
        </div>
        <div class="provider-models-tag">
          Модели: ${prov.discovered_models && prov.discovered_models.length ? escapeHtml(prov.discovered_models.join(', ')) : 'Н/Д — список моделей ещё не получен'}
        </div>
      </div>
    `).join('') || '<div class="empty-text">Нет данных провайдеров.</div>';
  }
}

// ═══════════════════════════════════════════════════════════════
//  2. ROUTING VIEW (P0-1, P0-2 Main Routing Control Center)
// ═══════════════════════════════════════════════════════════════

function getProviderIcon(provider) {
  const map = {
    'openai-codex': 'codex.png',
    'google-antigravity': 'антигравити.png',
    'opencode-go': 'opencode.png',
    'anthropic-claude': 'claude.png',
    'deepseek': 'deepseek.png',
    'grok': 'grok.jfif'
  };
  return map[provider] || 'llama.png';
}

function renderRoutingView() {
  const leftCol = document.getElementById('routing-roles-container');
  const rightCol = document.getElementById('routing-available-container');
  if (!leftCol || !rightCol || !currentSnapshot) return;

  const routing = currentSnapshot.routing || {};
  const agents = currentSnapshot.agents || [];
  const profiles = currentSnapshot.profiles || {};

  // Render Left Column (Roles)
  let rolesHtml = '';
  for (const [roleId, pipeline] of Object.entries(routing)) {
    const chain = pipeline.preferred_chain || [];
    const agentInfo = agents.find((a) => a.role_id === roleId);
    
    let isImportant = ['manager', 'developer-1', 'developer-2'].includes(roleId);
    let badgeHtml = isImportant ? `<span class="role-badge"><i class="fa-solid fa-star"></i> Важная роль</span>` : '';
    let roleDesc = CANONICAL_ROLE_DESCRIPTIONS[roleId] || '';

    rolesHtml += `
      <div class="role-section" data-role="${escapeHtml(roleId)}">
        <div class="role-header">
          <div>
            <h4><i class="fa-solid fa-network-wired" style="color:var(--accent);"></i> ${escapeHtml(pipeline.role_name_ru || roleId)} ${badgeHtml}</h4>
            <div style="font-size:11px; color:var(--text-muted); margin-top:2px;">${escapeHtml(roleDesc)}</div>
          </div>
          <div style="display:flex; align-items:center; gap:12px;">
            <span style="font-size:12px; color:var(--text-secondary);">${chain.length} аккаунта</span>
            <button class="btn btn-secondary btn-sm" onclick="openAddNodeToChainModal('${escapeHtml(roleId)}')">
              + Добавить аккаунт
            </button>
            <i class="fa-solid fa-ellipsis-vertical" style="color:var(--text-muted); cursor:pointer;"></i>
          </div>
        </div>
        <div class="grid-header">
          <div><!-- drag handle --></div>
          <div>Приоритет</div>
          <div>Аккаунт</div>
          <div>Модель</div>
          <div>Провайдер</div>
          <div>Квоты</div>
          <div>Сброс</div>
          <div>Статус</div>
          <div><!-- remove --></div>
        </div>
        <div class="role-chain-list" id="chain-${escapeHtml(roleId)}" style="min-height: 10px;">
    `;

    chain.forEach((pid, index) => {
      const prof = profiles[pid] || {};
      const prov = prof.provider || 'unknown';
      const icon = getProviderIcon(prov);
      
      rolesHtml += `
        <div class="account-row draggable-item" draggable="true" data-pid="${escapeHtml(pid)}" data-role="${escapeHtml(roleId)}">
          <div class="drag-handle"><i class="fa-solid fa-grip-vertical"></i></div>
          <div style="font-weight:600; color:var(--text-primary); text-align:center;">${index + 1}</div>
          <div style="display:flex; align-items:center; gap:8px;">
            <img src="/static/${icon}" class="provider-logo-sm" onerror="this.src='/static/llama.png'">
            <div>
              <div style="font-weight:600;">${escapeHtml(pid)}</div>
              <div style="font-size:10px; color:var(--text-muted);">${escapeHtml(prof.account_id || '')}</div>
            </div>
          </div>
          <div>
            <select class="form-control" style="padding:2px 4px; font-size:11px;" disabled>
              <option>${escapeHtml(prof.default_model || 'Default')}</option>
            </select>
          </div>
          <div style="display:flex; align-items:center; gap:4px;">
            <img src="/static/${icon}" class="provider-logo-sm" onerror="this.src='/static/llama.png'">
            ${escapeHtml(prov)}
          </div>
          <div>
            <div class="cell-bar-track"><div class="cell-bar-fill" style="width:70%; background:var(--status-healthy);"></div></div>
            <div class="cell-bar-track" style="margin-top:4px;"><div class="cell-bar-fill" style="width:40%; background:var(--status-healthy);"></div></div>
          </div>
          <div style="font-size:10px; color:var(--text-muted);">
            26 авг.,<br>18:42
          </div>
          <div>
            <span style="color:var(--status-healthy);">● Активен</span>
          </div>
          <div style="text-align:center; cursor:pointer; color:var(--status-warning);" onclick="removeProfileFromChain('${escapeHtml(roleId)}', '${escapeHtml(pid)}')">
            <i class="fa-solid fa-xmark"></i>
          </div>
        </div>
      `;
    });

    rolesHtml += `
        </div>
        <div class="drop-zone" data-role="${escapeHtml(roleId)}">
          <i class="fa-solid fa-arrows-up-down"></i> Перетащите аккаунт сюда для добавления в конец списка
        </div>
      </div>
    `;
  }
  leftCol.innerHTML = rolesHtml;

  // Render Right Column (Available Accounts)
  let availHtml = '';
  const searchEl = document.getElementById('routing-account-search');
  const q = searchEl ? searchEl.value.toLowerCase() : '';
  
  let count = 0;
  for (const [pid, prof] of Object.entries(profiles)) {
    if (q && !pid.toLowerCase().includes(q) && !(prof.provider||'').toLowerCase().includes(q)) continue;
    count++;
    const icon = getProviderIcon(prof.provider);
    availHtml += `
      <div class="available-account-card draggable-item" draggable="true" data-pid="${escapeHtml(pid)}" data-source="available">
        <img src="/static/${icon}" style="width:24px; height:24px; border-radius:4px;" onerror="this.src='/static/llama.png'">
        <div style="flex:1;">
          <div style="font-weight:600; font-size:12px;">${escapeHtml(pid)}</div>
          <div style="font-size:10px; color:var(--text-muted);">${escapeHtml(prof.provider || '')}</div>
        </div>
        <div style="width:40px;">
           <div class="cell-bar-track"><div class="cell-bar-fill" style="width:80%; background:var(--status-healthy);"></div></div>
        </div>
        <button class="btn btn-secondary btn-sm" style="padding:2px 6px;" onclick="quickAddProfile('${escapeHtml(pid)}')"><i class="fa-solid fa-plus"></i></button>
      </div>
    `;
  }
  rightCol.innerHTML = availHtml;
  const countEl = document.getElementById('available-accounts-count');
  if (countEl) countEl.innerText = `${count} аккаунтов`;

  setupDragAndDrop();
}

function quickAddProfile(pid) {
    const routing = currentSnapshot.routing || {};
    let role = 'manager';
    if (!routing[role]) role = Object.keys(routing)[0];
    if (!role) return;
    addProfileToChain(role, pid);
}

function setupDragAndDrop() {
  let draggedEl = null;
  let dragPid = null;
  let sourceRole = null;

  document.querySelectorAll('.draggable-item').forEach(el => {
    el.addEventListener('dragstart', (e) => {
      draggedEl = el;
      dragPid = el.dataset.pid;
      sourceRole = el.dataset.role || null;
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', dragPid);
      setTimeout(() => el.style.opacity = '0.5', 0);
    });
    el.addEventListener('dragend', (e) => {
      el.style.opacity = '1';
      document.querySelectorAll('.drag-over').forEach(d => d.classList.remove('drag-over'));
      draggedEl = null;
    });
  });

  // Drop zones (the "add to end" zones)
  document.querySelectorAll('.drop-zone').forEach(zone => {
    zone.addEventListener('dragover', (e) => {
      e.preventDefault();
      e.dataTransfer.dropEffect = 'move';
      zone.classList.add('drag-over');
    });
    zone.addEventListener('dragleave', (e) => {
      zone.classList.remove('drag-over');
    });
    zone.addEventListener('drop', (e) => {
      e.preventDefault();
      zone.classList.remove('drag-over');
      const targetRole = zone.dataset.role;
      if (!targetRole || !dragPid) return;
      
      handleDropMove(dragPid, sourceRole, targetRole, -1);
    });
  });

  // Reordering inside role-chain-list
  document.querySelectorAll('.account-row').forEach(row => {
    row.addEventListener('dragover', (e) => {
      e.preventDefault();
      row.classList.add('drag-over');
    });
    row.addEventListener('dragleave', (e) => {
      row.classList.remove('drag-over');
    });
    row.addEventListener('drop', (e) => {
      e.preventDefault();
      row.classList.remove('drag-over');
      const targetRole = row.dataset.role;
      if (!targetRole || !dragPid) return;
      
      const list = row.parentNode;
      const children = Array.from(list.children);
      const insertIndex = children.indexOf(row);
      
      handleDropMove(dragPid, sourceRole, targetRole, insertIndex);
    });
  });
}

function handleDropMove(pid, sourceRole, targetRole, insertIndex) {
  const routing = currentSnapshot.routing;
  if (!routing || !routing[targetRole]) return;
  
  const targetChain = [...(routing[targetRole].preferred_chain || [])];
  
  if (sourceRole && sourceRole === targetRole) {
    const oldIndex = targetChain.indexOf(pid);
    if (oldIndex > -1) {
      targetChain.splice(oldIndex, 1);
    }
    if (insertIndex === -1) {
      targetChain.push(pid);
    } else {
      let idx = insertIndex;
      if (oldIndex > -1 && oldIndex < insertIndex) idx--;
      targetChain.splice(idx, 0, pid);
    }
    updateRoleChain(targetRole, targetChain);
  } else {
    if (targetChain.includes(pid)) {
        showToast(`Аккаунт ${pid} уже есть в роли ${targetRole}`, 'warning');
        return;
    }
    if (insertIndex === -1) {
      targetChain.push(pid);
    } else {
      targetChain.splice(insertIndex, 0, pid);
    }
    updateRoleChain(targetRole, targetChain);
  }
}

async function updateRoleChain(roleId, newChain) {
  try {
    const resp = await fetch(`/api/v1/router/roles/${encodeURIComponent(roleId)}/chain`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newChain)
    });
    if (!resp.ok) throw new Error(await resp.text());
    showToast(`Цепочка для ${roleId} обновлена`, 'success');
    await fetchSnapshot();
  } catch (e) {
    showToast(`Ошибка сохранения: ${e.message}`, 'error');
  }
}

async function removeProfileFromChain(roleId, pid) {
    const routing = currentSnapshot.routing;
    if (!routing || !routing[roleId]) return;
    const chain = [...(routing[roleId].preferred_chain || [])];
    const idx = chain.indexOf(pid);
    if (idx > -1) {
        chain.splice(idx, 1);
        await updateRoleChain(roleId, chain);
    }
}

function openAddNodeToChainModal(roleId) {
  if (!currentSnapshot) return;
  const pipeline = (currentSnapshot.routing || {})[roleId];
  if (!pipeline) return;

  const currentChain = (pipeline.nodes || []).map((n) => n.profile_id);
  const allProfiles = currentSnapshot.all_profiles || {};
  const available = Object.values(allProfiles).filter((p) => !currentChain.includes(p.profile_id));

  elements.modalTitle.textContent = `Добавить профиль в цепочку: ${pipeline.role_name_ru || roleId}`;
  if (available.length === 0) {
    elements.modalBody.innerHTML = `
      <div class="view-header-note">Все зарегистрированные профили уже включены в эту цепочку.</div>
    `;
    elements.modalFooter.innerHTML = `<button class="btn btn-ghost" onclick="closeModal()">Закрыть</button>`;
  } else {
    elements.modalBody.innerHTML = `
      <div id="modal-feedback-area"></div>
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Выберите доступный профиль для включения в цепочку отказоустойчивости:
      </div>
      <div style="margin-bottom:16px;">
        <select id="add-node-profile-select" class="select-filter" style="width:100%;">
          ${available.map((p) => `
            <option value="${escapeHtml(p.profile_id)}">
              ${escapeHtml(p.display_name || p.profile_id)} (${escapeHtml(p.provider_display_name || p.provider)}) — ${escapeHtml(p.email || p.account_identity || 'без email')}
            </option>
          `).join('')}
        </select>
      </div>
    `;
    elements.modalFooter.innerHTML = `
      <button class="btn btn-ghost" onclick="closeModal()">Отмена</button>
      <button class="btn btn-primary" onclick="handleAddNodeToChain('${escapeHtml(roleId)}')">+ Добавить в цепочку</button>
    `;
  }
  showModal();
}

async function handleAddNodeToChain(roleId) {
  const sel = document.getElementById('add-node-profile-select');
  if (!sel) return;
  const newProfileId = sel.value;
  if (!newProfileId) return;

  const pipeline = (currentSnapshot.routing || {})[roleId];
  const currentChain = (pipeline?.nodes || []).map((n) => n.profile_id);
  const newChain = [...currentChain, newProfileId];

  const feedbackArea = document.getElementById('modal-feedback-area');
  if (feedbackArea) {
    feedbackArea.innerHTML = '<div class="modal-feedback info">⏳ Добавление профиля в цепочку...</div>';
  }

  const res = await executeAction('save_chain', { role_id: roleId, chain: newChain });
  if (res.ok) {
    showToast(`Профиль добавлен в цепочку '${pipeline?.role_name_ru || roleId}'`, 'success');
    closeModal();
    fetchSnapshot();
  } else {
    if (feedbackArea) {
      feedbackArea.innerHTML = `<div class="modal-feedback error">❌ ${escapeHtml(res.message || 'Ошибка сохранения')}</div>`;
    }
  }
}

// ── SETTINGS MANAGEMENT ──
function renderSettingsView() {
  if (!currentSnapshot) return;
  const paths = currentSnapshot.system_paths || {};
  const s = currentSnapshot.settings || {};

  const elHome = document.getElementById('path-hermes-home');
  const elConfig = document.getElementById('path-config-dir');
  const elLog = document.getElementById('path-log-file');

  if (elHome) elHome.textContent = paths.hermes_home || '—';
  if (elConfig) elConfig.textContent = paths.config_dir || '—';
  if (elLog) elLog.textContent = paths.log_file || '—';

  const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent');
  const quotaActionSel = document.getElementById('setting-quota-threshold-action');
  const emailMaskingSel = document.getElementById('setting-email-masking-mode');
  const monitorIntervalInput = document.getElementById('setting-monitoring-interval');

  if (quotaThresholdSel && s.quota_threshold_percent !== undefined) {
    quotaThresholdSel.value = String(Math.round(s.quota_threshold_percent));
  }
  if (quotaActionSel && s.quota_threshold_action) {
    quotaActionSel.value = s.quota_threshold_action;
  }
  if (emailMaskingSel && s.email_masking_mode) {
    emailMaskingSel.value = s.email_masking_mode;
  }
  if (monitorIntervalInput && s.monitoring_interval_seconds !== undefined) {
    monitorIntervalInput.value = s.monitoring_interval_seconds;
  }
}

async function saveHubServerSettings() {
  const quotaThresholdSel = document.getElementById('setting-quota-threshold-percent');
  const quotaActionSel = document.getElementById('setting-quota-threshold-action');
  const emailMaskingSel = document.getElementById('setting-email-masking-mode');
  const monitorIntervalInput = document.getElementById('setting-monitoring-interval');

  const newSettings = {
    quota_threshold_percent: quotaThresholdSel ? parseFloat(quotaThresholdSel.value) || 10.0 : 10.0,
    quota_threshold_action: quotaActionSel ? quotaActionSel.value : 'notify',
    email_masking_mode: emailMaskingSel ? emailMaskingSel.value : 'none',
    monitoring_interval_seconds: monitorIntervalInput ? parseInt(monitorIntervalInput.value, 10) || 30 : 30,
  };

  showToast('Сохранение настроек сервера...', 'info');
  const res = await executeAction('save_settings', newSettings);
  if (res.ok) {
    showToast('Настройки сервера успешно сохранены', 'success');
    fetchSnapshot();
  } else {
    showToast(res.message || 'Ошибка сохранения настроек сервера', 'error');
  }
}

// ── PREFLIGHT READINESS CHECKS ──
async function runPreflightChecks() {
  const container = document.getElementById('preflight-results-container');
  const btn = document.getElementById('btn-run-preflight');
  if (btn) btn.disabled = true;
  if (container) {
    container.innerHTML = '<div class="loading-state" style="padding:12px; font-size:13px; color:var(--text-secondary);">⏳ Запуск zero-quota проверки зависимостей и окружения...</div>';
  }
  try {
    const res = await executeAction('run_preflight', {});
    if (!res) throw new Error('Сервер не вернул ответ');
    const report = res.data || {};
    renderPreflightReport(report, container);
    showToast(res.message || 'Проверка готовности завершена', res.ok ? 'success' : 'warning');
  } catch (err) {
    if (container) {
      container.innerHTML = `<div class="modal-feedback error">❌ Ошибка выполнения проверки: ${escapeHtml(err.message || String(err))}</div>`;
    }
    showToast('Ошибка при запуске проверки готовности', 'error');
  } finally {
    if (btn) btn.disabled = false;
  }
}

function renderPreflightReport(report, container) {
  if (!container) return;
  const checks = report.checks || [];
  const passed = report.passed_count || 0;
  const failed = report.failed_count || 0;
  const warn = report.warn_count || 0;

  const statusBadge = `<span class="badge ${failed === 0 ? 'healthy' : 'error'}">${failed === 0 ? 'Все проверки пройдены' : `Обнаружено ошибок: ${failed}`}</span>`;

  let html = `
    <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; padding:10px 14px; background:var(--surface-muted); border-radius:var(--radius-sm);">
      <div style="font-weight:600; font-size:13px;">Результат: ${statusBadge}</div>
      <div style="font-size:12px; color:var(--text-muted);">
        Пройдено: <strong style="color:var(--status-healthy);">${passed}</strong> • 
        Ошибок: <strong style="color:var(--status-error);">${failed}</strong> • 
        Предупреждений: <strong style="color:var(--status-warning);">${warn}</strong>
      </div>
    </div>
    <div class="preflight-list" style="display:flex; flex-direction:column; gap:8px;">
  `;

  checks.forEach((item) => {
    const badgeClass = item.status === 'PASS' ? 'healthy' : (item.status === 'WARN' ? 'warning' : 'error');
    const icon = item.status === 'PASS' ? '✓' : (item.status === 'WARN' ? '⚠' : '✕');
    html += `
      <div class="preflight-item" style="padding:10px 14px; background:var(--surface-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm);">
        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
          <div style="font-weight:600; font-size:13px; display:flex; align-items:center; gap:8px;">
            <span class="badge ${badgeClass}" style="padding:2px 8px; font-size:11px;">${icon} ${escapeHtml(item.status)}</span>
            <span>${escapeHtml(item.name || item.check_id)}</span>
          </div>
        </div>
        <div style="font-size:12px; color:var(--text-secondary); margin-left:4px;">${escapeHtml(item.message || '')}</div>
        ${item.remediation ? `<div style="font-size:12px; color:var(--status-warning); margin-top:4px; margin-left:4px; font-style:italic;">💡 Рекомендация: ${escapeHtml(item.remediation)}</div>` : ''}
      </div>
    `;
  });

  html += '</div>';
  container.innerHTML = html;
}


function initSettings() {
  const btnSave = document.getElementById('btn-save-client-settings');
  const tokenInput = document.getElementById('setting-client-token-input');
  const pollSelect = document.getElementById('setting-poll-interval');

  if (tokenInput && authToken) {
    tokenInput.value = authToken;
  }

  if (btnSave) {
    btnSave.addEventListener('click', () => {
      if (tokenInput) {
        authToken = tokenInput.value.trim();
        localStorage.setItem('hermes_hub_token', authToken);
      }
      if (pollSelect) {
        pollIntervalMs = parseInt(pollSelect.value, 10);
        startPolling();
      }
      showToast('Параметры веб-клиента сохранены', 'success');
      fetchSnapshot();
    });
  }
}

// ── MODAL HELPERS ──
function showModal() {
  if (elements.modalBackdrop) elements.modalBackdrop.classList.remove('hidden');
}

function closeModal() {
  _openAccountModalProfile = null;
  stopDeviceAuthPolling();
  // Опрос входа по ссылке иначе продолжал бы стучать в закрытое окно.
  stopRedirectAuthPolling();
  if (elements.modalBackdrop) elements.modalBackdrop.classList.add('hidden');
}

// ── TOAST NOTIFICATIONS ──
function showToast(message, type = 'info') {
  if (!elements.toastContainer) return;
  const toast = document.createElement('div');
  toast.className = `toast ${type}`;
  toast.textContent = message;
  elements.toastContainer.appendChild(toast);

  setTimeout(() => {
    toast.style.opacity = '0';
    toast.style.transition = 'opacity 0.3s ease';
    setTimeout(() => toast.remove(), 300);
  }, 4000);
}

// ── UTILITIES ──
function escapeHtml(str) {
  if (!str) return '';
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

function formatIsoDate(isoStr) {
  if (!isoStr) return '';
  try {
    const d = new Date(isoStr);
    if (isNaN(d.getTime())) return isoStr;
    return d.toLocaleString('ru-RU', {
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  } catch (e) {
    return isoStr;
  }
}

// ── THEME SWITCHER ──
function applyTheme(theme) {
  if (theme === 'light') {
    document.body.setAttribute('data-theme', 'light');
    document.body.classList.add('theme-light');
  } else if (theme === 'dark') {
    document.body.setAttribute('data-theme', 'dark');
    document.body.classList.remove('theme-light');
  } else {
    document.body.removeAttribute('data-theme');
    document.body.classList.remove('theme-light');
  }
}

// ── UPDATE MANAGEMENT (P0-1 / In-App Updates) ──
async function checkUpdates(silent = false) {
  if (!silent) {
    showToast('Проверка обновлений...', 'info');
  }
  try {
    const res = await executeAction('check_updates', {});
    if (res && res.ok && res.data) {
      latestUpdateInfo = res.data;
      renderUpdateUI();
      if (!silent) {
        if (res.data.update_available) {
          const c = res.data.latest_commit ? res.data.latest_commit.slice(0, 7) : (res.data.release_tag || 'new');
          showToast(`Доступно обновление (сборка ${c})`, 'info');
        } else {
          showToast(res.data.message || 'Установлена последняя сборка', 'success');
        }
      }
    } else {
      if (res && res.data) {
        latestUpdateInfo = res.data;
        renderUpdateUI();
      }
      if (!silent) {
        showToast((res && res.message) || 'Ошибка проверки обновлений', 'error');
      }
    }
  } catch (err) {
    if (!silent) {
      showToast(`Ошибка проверки обновлений: ${err.message}`, 'error');
    }
  }
}

function renderUpdateUI() {
  const badge = document.getElementById('header-update-badge');
  const badgeText = document.getElementById('header-update-text');
  const commitTag = document.getElementById('commit-tag');

  const installedCommit = (latestUpdateInfo && latestUpdateInfo.installed_commit && latestUpdateInfo.installed_commit !== 'unknown')
    ? latestUpdateInfo.installed_commit
    : (currentSettings && currentSettings.installed_commit ? currentSettings.installed_commit : '');

  if (commitTag) {
    commitTag.textContent = installedCommit ? `Сборка: ${installedCommit.slice(0, 7)}` : 'Сборка: —';
  }

  if (badge && badgeText) {
    if (latestUpdateInfo && latestUpdateInfo.update_available) {
      badge.classList.remove('hidden');
      const c = latestUpdateInfo.latest_commit ? latestUpdateInfo.latest_commit.slice(0, 7) : (latestUpdateInfo.release_tag || 'new');
      badgeText.textContent = `Доступно обновление (${c})`;
    } else {
      badge.classList.add('hidden');
    }
  }

  const updateInfoDesc = document.getElementById('update-installed-info');
  const statusBadge = document.getElementById('update-status-badge');
  const lastCheckedDesc = document.getElementById('update-last-checked-desc');
  const btnApply = document.getElementById('btn-apply-update');
  const detailsBlock = document.getElementById('update-details-block');
  const releaseTitle = document.getElementById('update-release-title');
  const releaseMeta = document.getElementById('update-release-meta');
  const releaseNotes = document.getElementById('update-release-notes');

  const curVer = (latestUpdateInfo && latestUpdateInfo.current_version) || (currentSettings && currentSettings.version) || '0.1.1';
  const cDisplay = installedCommit ? installedCommit.slice(0, 7) : 'неизвестно';
  if (updateInfoDesc) {
    updateInfoDesc.textContent = `Hermes Hub v${curVer} (сборка: ${cDisplay})`;
  }

  if (statusBadge) {
    if (latestUpdateInfo && latestUpdateInfo.error) {
      statusBadge.textContent = 'Ошибка проверки';
      statusBadge.className = 'badge badge-status warning';
      statusBadge.title = latestUpdateInfo.error;
    } else if (latestUpdateInfo && latestUpdateInfo.update_available) {
      statusBadge.textContent = 'Доступно обновление';
      statusBadge.className = 'badge badge-status warning';
      statusBadge.title = '';
    } else if (latestUpdateInfo && latestUpdateInfo.checked_at > 0) {
      statusBadge.textContent = 'Актуально';
      statusBadge.className = 'badge healthy';
      statusBadge.title = '';
    } else {
      statusBadge.textContent = 'Не проверялось';
      statusBadge.className = 'badge';
      statusBadge.title = '';
    }
  }

  if (lastCheckedDesc) {
    if (latestUpdateInfo && latestUpdateInfo.checked_at > 0) {
      const tStr = new Date(latestUpdateInfo.checked_at * 1000).toLocaleTimeString('ru-RU');
      const errNote = latestUpdateInfo.error ? ` — Ошибка: ${latestUpdateInfo.error}` : '';
      lastCheckedDesc.textContent = `Последняя проверка: сегодня в ${tStr}${errNote}`;
    } else {
      lastCheckedDesc.textContent = 'Последняя проверка: еще не выполнялась';
    }
  }

  if (btnApply) {
    btnApply.disabled = !(latestUpdateInfo && latestUpdateInfo.update_available);
  }

  if (detailsBlock && releaseTitle && releaseMeta && releaseNotes) {
    if (latestUpdateInfo && latestUpdateInfo.update_available) {
      detailsBlock.classList.remove('hidden');
      const latC = latestUpdateInfo.latest_commit ? latestUpdateInfo.latest_commit.slice(0, 7) : '—';
      releaseTitle.textContent = `Релиз: ${latestUpdateInfo.release_tag || latestUpdateInfo.latest_version || 'Новая сборка'} (коммит: ${latC})`;
      releaseMeta.textContent = latestUpdateInfo.published_at ? `Опубликован: ${latestUpdateInfo.published_at}` : '';
      releaseNotes.textContent = latestUpdateInfo.changelog || latestUpdateInfo.release_notes || 'Описание изменений отсутствует.';
    } else {
      detailsBlock.classList.add('hidden');
    }
  }
}

function openUpdateModal() {
  if (!latestUpdateInfo) {
    checkUpdates(false);
    return;
  }

  const instC = (latestUpdateInfo.installed_commit && latestUpdateInfo.installed_commit !== 'unknown')
    ? latestUpdateInfo.installed_commit.slice(0, 7)
    : 'неизвестно';
  const latC = latestUpdateInfo.latest_commit ? latestUpdateInfo.latest_commit.slice(0, 7) : (latestUpdateInfo.release_tag || '—');

  if (elements.modalTitle) elements.modalTitle.textContent = 'Обновление Hermes Hub';
  if (elements.modalBody) {
    elements.modalBody.innerHTML = `
      <div class="update-modal-body">
        <div style="display:flex; justify-content:space-between; margin-bottom:12px; padding:10px; background:var(--surface-muted); border-radius:var(--radius-sm);">
          <div>
            <div style="font-size:11px; color:var(--text-muted);">Текущая сборка:</div>
            <div style="font-weight:600; font-family:var(--font-mono); font-size:13px;">${escapeHtml(instC)}</div>
          </div>
          <div style="text-align:right;">
            <div style="font-size:11px; color:var(--text-muted);">Новая сборка:</div>
            <div style="font-weight:600; font-family:var(--font-mono); font-size:13px; color:var(--status-warning);">${escapeHtml(latC)}</div>
          </div>
        </div>
        <div style="margin-bottom:8px; font-size:12px; color:var(--text-muted);">
          Тег: <strong>${escapeHtml(latestUpdateInfo.release_tag || latestUpdateInfo.latest_version || '—')}</strong>
          ${latestUpdateInfo.published_at ? ` &bull; Дата: ${escapeHtml(latestUpdateInfo.published_at)}` : ''}
        </div>
        <div style="font-weight:600; font-size:12px; margin-bottom:4px;">Список изменений (Release Notes):</div>
        <div style="max-height:200px; overflow-y:auto; font-size:12px; line-height:1.4; white-space:pre-wrap; background:var(--surface-muted); padding:10px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); font-family:var(--font-mono);">
          ${escapeHtml(latestUpdateInfo.changelog || latestUpdateInfo.release_notes || 'Описание изменений отсутствует.')}
        </div>
      </div>
    `;
  }
  if (elements.modalFooter) {
    elements.modalFooter.innerHTML = `
      <button class="btn btn-secondary" onclick="closeModal()">Закрыть</button>
      <button class="btn btn-primary" id="btn-modal-install-update" onclick="handleInstallUpdateFromModal()">Установить обновление</button>
    `;
  }
  showModal();
}

async function handleInstallUpdateFromModal() {
  const btn = document.getElementById('btn-modal-install-update');
  if (btn) {
    btn.disabled = true;
    btn.textContent = 'Установка...';
  }
  await applyUpdate();
  closeModal();
}

async function applyUpdate() {
  showToast('Загрузка и запуск обновления...', 'info');
  try {
    const res = await executeAction('apply_update', {});
    if (res && res.ok) {
      showToast(res.message || 'Обновление запущено успешно!', 'success');
    } else {
      showToast((res && res.message) || 'Ошибка установки обновления', 'error');
    }
  } catch (err) {
    showToast(`Ошибка установки: ${err.message}`, 'error');
  }
}

// ── OVERVIEW & ROUTING NODE HANDLERS ──
async function handleNodeAccountChange(roleId, profileId, isPrimary = true) {
  if (!roleId || !profileId) return;
  showToast(`Назначение аккаунта '${profileId}' на роль '${roleId}'...`, 'info');
  const res = await executeAction('assign_role', {
    role_id: roleId,
    profile_id: profileId,
    is_primary: isPrimary,
  });
  if (res && res.ok) {
    showToast(`Аккаунт '${profileId}' успешно назначен`, 'success');
    fetchSnapshot();
  } else {
    showToast((res && res.message) || 'Ошибка назначения аккаунта', 'error');
  }
}

async function handleNodeModelChange(roleId, profileId, newModel) {
  if (!newModel) return;
  showToast(`Сохранение модели '${newModel}' для ${profileId}...`, 'info');
  const res = await executeAction('set_model', { profile_id: profileId, model: newModel, role_id: roleId });
  if (res && res.ok) {
    showToast(`Модель '${newModel}' успешно сохранена`, 'success');
    if (currentSnapshot) {
      if (currentSnapshot.all_profiles && currentSnapshot.all_profiles[profileId]) {
        currentSnapshot.all_profiles[profileId].preferred_models = [newModel];
      }
      if (currentSnapshot.routing && currentSnapshot.routing[roleId]) {
        currentSnapshot.routing[roleId].default_model = newModel;
        const node = (currentSnapshot.routing[roleId].nodes || []).find((n) => n.profile_id === profileId);
        if (node) node.model = newModel;
      }
    }
    renderCurrentView();
    fetchSnapshot();
  } else {
    showToast((res && res.message) || 'Ошибка сохранения модели', 'error');
  }
}

async function handleRefreshProviderModels(providerId, profileId = null) {
  showToast(`Запрос списка моделей для ${providerId}...`, 'info');
  const res = await executeAction('refresh_models', { provider: providerId });
  if (res && res.ok) {
    showToast('Запрос обновления моделей отправлен', 'success');
    if (profileId) {
      setTimeout(() => openAccountDetailsModal(profileId, true), 500);
    } else {
      fetchSnapshot();
    }
  } else {
    showToast((res && res.message) || 'Ошибка обновления моделей', 'error');
  }
}

// ── ACCOUNT & AGENT MODALS ──
function openAccountDetailsModal(profileId, isRefresh = false) {
  _openAccountModalProfile = profileId;
  if (!currentSnapshot) return;
  const profile = (currentSnapshot.all_profiles || {})[profileId];
  if (!profile) return;

  const provSummary = (currentSnapshot.providers || []).find((p) => p.provider_id === profile.provider);
  const discoveredModels = (provSummary && provSummary.discovered_models) ? provSummary.discovered_models : [];
  const currentModel = (profile.preferred_models && profile.preferred_models.length) ? profile.preferred_models[0] : '';
  const qs = profile.quota_snapshot;
  const buckets = (qs && qs.buckets) ? qs.buckets : [];

  let modelBlockHtml = '';
  if (discoveredModels.length > 0) {
    modelBlockHtml = `
      <div style="background:var(--surface-muted); padding:10px 12px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); margin-bottom:14px;">
        <label style="display:block; font-weight:600; font-size:12px; margin-bottom:6px;">Предпочитаемая модель профиля:</label>
        <div style="display:flex; gap:8px;">
          <select id="modal-model-select" class="select-filter" style="flex:1;">
            ${discoveredModels.map((m) => `<option value="${escapeHtml(m)}" ${m === currentModel ? 'selected' : ''}>${escapeHtml(m)}</option>`).join('')}
          </select>
          <button class="btn btn-secondary btn-sm" onclick="handleSaveProfileModel('${escapeHtml(profileId)}')">Сохранить</button>
        </div>
      </div>
    `;
  } else {
    modelBlockHtml = `
      <div style="background:var(--surface-muted); padding:10px 12px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); margin-bottom:14px;">
        <div style="font-size:12px; color:var(--status-warning); margin-bottom:6px;">
          ⚠ Список моделей ещё не получен от провайдера ${escapeHtml(profile.provider_display_name || profile.provider)}.
        </div>
        <button class="btn btn-secondary btn-sm" onclick="handleRefreshProviderModels('${escapeHtml(profile.provider)}', '${escapeHtml(profileId)}')">↻ Запросить список моделей</button>
      </div>
    `;
  }

  elements.modalTitle.textContent = `Учетная запись: ${profile.display_name || profileId}`;
  elements.modalBody.innerHTML = `
    <div id="modal-feedback-area"></div>
    <div style="margin-bottom:14px;">
      <div style="font-size:14px; font-weight:700;">${escapeHtml(profile.account_identity || profile.email || profileId)}</div>
      <div style="font-size:12px; color:var(--text-muted);">
        Провайдер: <strong>${escapeHtml(profile.provider_display_name || profile.provider)}</strong> •
        Тариф: <strong>${escapeHtml(profile.plan || 'Неизвестен')}</strong> •
        Статус: <strong class="text-healthy">${escapeHtml(profile.health_label_ru || 'Работает')}</strong>
      </div>
      <div style="font-size:12px; color:var(--text-secondary); margin-top:4px;">
        Назначенные роли: <strong>${escapeHtml((profile.assigned_roles || []).join(', ') || 'Нет')}</strong>
      </div>
    </div>

    ${modelBlockHtml}

    <div style="margin-bottom:8px; font-weight:600; font-size:12px;">Лимиты и квоты провайдера:</div>
    <div style="display:flex; flex-direction:column; gap:8px; margin-bottom:14px;">
      ${buckets.map((b) => `
        <div style="background:var(--surface-card); border:1px solid var(--border-subtle); padding:8px 10px; border-radius:var(--radius-sm);">
          <div style="display:flex; justify-content:space-between; font-size:12px; font-weight:600; margin-bottom:4px;">
            <span>${escapeHtml(b.bucket_name || b.name || 'Квота')}</span>
            <span>${b.remaining_percent !== null && b.remaining_percent !== undefined ? Math.round(b.remaining_percent) + '%' : 'Н/Д'}</span>
          </div>
          <div class="cell-bar-track" style="margin-bottom:4px;">
            <div class="cell-bar-fill" style="width:${b.remaining_percent !== null && b.remaining_percent !== undefined ? Math.max(0, Math.min(100, b.remaining_percent)) : 0}%; background:${(b.remaining_percent !== null && b.remaining_percent < 20) ? 'var(--status-warning)' : 'var(--status-healthy)'};"></div>
          </div>
          <div style="font-size:10px; color:var(--text-muted);">
            ${b.reset_at ? `Сброс: ${formatIsoDate(b.reset_at)}` : (b.period ? `Период: ${b.period}` : 'Без отметки сброса')}
          </div>
        </div>
      `).join('') || '<div class="empty-text">Данные о квотах отсутствуют (провайдер не отдал лимиты).</div>'}
    </div>
  `;

  elements.modalFooter.innerHTML = `
    <button class="btn btn-secondary" onclick="handleTestProfile('${escapeHtml(profileId)}')">⚡ Проверить подключение</button>
    <button class="btn btn-secondary" onclick="executeAction('set_main', { profile_id: '${escapeHtml(profileId)}' })">★ Сделать основным</button>
    <button class="btn btn-secondary" onclick="handleDeleteCredentials('${escapeHtml(profileId)}')">Удалить ключ</button>
    <button class="btn btn-primary" onclick="closeModal()">Закрыть</button>
  `;

  if (!isRefresh) {
    showModal();
  }
}

async function handleSaveProfileModel(profileId) {
  const sel = document.getElementById('modal-model-select');
  if (!sel) return;
  const model = sel.value;
  const feedbackArea = document.getElementById('modal-feedback-area');
  if (feedbackArea) {
    feedbackArea.innerHTML = '<div class="modal-feedback info">⏳ Сохранение модели...</div>';
  }
  const res = await executeAction('set_model', { profile_id: profileId, model: model });
  if (feedbackArea) {
    if (res && res.ok) {
      feedbackArea.innerHTML = `<div class="modal-feedback success">✓ ${escapeHtml(res.message || 'Модель сохранена')}</div>`;
      if (currentSnapshot && currentSnapshot.all_profiles && currentSnapshot.all_profiles[profileId]) {
        currentSnapshot.all_profiles[profileId].preferred_models = [model];
      }
      fetchSnapshot();
    } else {
      feedbackArea.innerHTML = `<div class="modal-feedback error">❌ ${escapeHtml((res && res.message) || 'Ошибка сохранения модели')}</div>`;
    }
  }
}

function openAgentModelModal(roleId, profileId) {
  if (!currentSnapshot) return;
  const profile = (currentSnapshot.all_profiles || {})[profileId];
  if (!profile) return;

  const provSummary = (currentSnapshot.providers || []).find((p) => p.provider_id === profile.provider);
  const discoveredModels = (provSummary && provSummary.discovered_models) ? provSummary.discovered_models : [];
  const currentModel = (profile.preferred_models && profile.preferred_models.length) ? profile.preferred_models[0] : '';
  const roleName = ((currentSnapshot.routing || {})[roleId]?.role_name_ru) || roleId;

  elements.modalTitle.textContent = `Выбор модели для роли: ${roleName}`;
  elements.modalBody.innerHTML = `
    <div id="modal-feedback-area"></div>
    <div style="margin-bottom:12px; font-size:12px; color:var(--text-muted);">
      Профиль агента: <strong>${escapeHtml(profile.display_name)} (${profileId})</strong> • Провайдер: <strong>${escapeHtml(profile.provider_display_name || profile.provider)}</strong>
    </div>
    ${discoveredModels.length > 0 ? `
      <div style="margin-bottom:16px;">
        <label style="display:block; font-weight:600; margin-bottom:6px;">Выберите модель из обнаруженного списка:</label>
        <select id="role-model-select" class="select-filter" style="width:100%;">
          ${discoveredModels.map((m) => `<option value="${escapeHtml(m)}" ${m === currentModel ? 'selected' : ''}>${escapeHtml(m)}</option>`).join('')}
        </select>
      </div>
    ` : `
      <div style="background:var(--surface-muted); padding:10px 12px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); margin-bottom:16px;">
        <div style="font-size:12px; color:var(--status-warning); margin-bottom:6px;">
          ⚠ Список моделей ещё не получен от провайдера ${escapeHtml(profile.provider_display_name || profile.provider)}.
        </div>
        <button class="btn btn-secondary btn-sm" onclick="handleRefreshProviderModels('${escapeHtml(profile.provider)}')">↻ Запросить список моделей</button>
      </div>
    `}
  `;

  elements.modalFooter.innerHTML = `
    <button class="btn btn-ghost" onclick="closeModal()">Отмена</button>
    ${discoveredModels.length > 0 ? `<button class="btn btn-primary" onclick="handleSaveRoleModel('${escapeHtml(roleId)}', '${escapeHtml(profileId)}')">Сохранить модель</button>` : ''}
  `;

  showModal();
}

async function handleSaveRoleModel(roleId, profileId) {
  const sel = document.getElementById('role-model-select');
  if (!sel) return;
  const model = sel.value;
  const feedbackArea = document.getElementById('modal-feedback-area');
  if (feedbackArea) {
    feedbackArea.innerHTML = '<div class="modal-feedback info">⏳ Сохранение модели...</div>';
  }
  const res = await executeAction('set_model', { profile_id: profileId, model: model, role_id: roleId });
  if (feedbackArea) {
    if (res && res.ok) {
      feedbackArea.innerHTML = `<div class="modal-feedback success">✓ ${escapeHtml(res.message || 'Модель сохранена')}</div>`;
      if (currentSnapshot) {
        if (currentSnapshot.all_profiles && currentSnapshot.all_profiles[profileId]) {
          currentSnapshot.all_profiles[profileId].preferred_models = [model];
        }
        if (currentSnapshot.routing && currentSnapshot.routing[roleId]) {
          currentSnapshot.routing[roleId].default_model = model;
        }
        if (currentSnapshot.agents) {
          const ag = currentSnapshot.agents.find((a) => a.role_id === roleId);
          if (ag) ag.model = model;
        }
      }
      setTimeout(() => {
        closeModal();
        renderCurrentView();
        fetchSnapshot();
      }, 700);
    } else {
      feedbackArea.innerHTML = `<div class="modal-feedback error">❌ ${escapeHtml((res && res.message) || 'Ошибка сохранения модели')}</div>`;
    }
  }
}

async function handleTestProfile(profileId) {
  const feedbackArea = document.getElementById('modal-feedback-area');
  if (feedbackArea) {
    feedbackArea.innerHTML = '<div class="modal-feedback info">⏳ Запуск тестового запроса к провайдеру...</div>';
  }
  const res = await executeAction('test', { profile_id: profileId });
  if (feedbackArea) {
    if (res && res.ok) {
      feedbackArea.innerHTML = `<div class="modal-feedback success">✓ ${escapeHtml(res.message || 'Тест успешно пройден')}</div>`;
    } else {
      feedbackArea.innerHTML = `<div class="modal-feedback error">❌ ${escapeHtml((res && res.message) || 'Тест завершился с ошибкой')}</div>`;
    }
  }
}

// ── ADD ACCOUNT WIZARD (P0-1) ──
function openAddAccountWizard() {
  window._wiz_device_profile = undefined;
  window._wiz_device_session = undefined;
  window._wiz_redirect_session = undefined;
  window._wiz_redirect_provider = undefined;
  window._wiz_redirect_slot_id = undefined;
  window._wiz_base_url = undefined;
  window._wiz_token = undefined;
  if (elements.modalTitle) elements.modalTitle.textContent = 'Мастер подключения учетной записи';
  showWizardStep1();
  showModal();
}

function showWizardStep1() {
  if (elements.modalTitle) elements.modalTitle.textContent = 'Мастер подключения учетной записи';
  elements.modalBody.innerHTML = `
    <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
      Шаг 1 из 3: Выберите провайдера ИИ
    </div>
    <div style="display:grid; grid-template-columns:1fr; gap:8px;">
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('grok')">
        <span style="font-size:18px; color:var(--prov-grok);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">Grok (xAI)</div>
          <div style="font-size:11px; color:var(--text-muted);">Device Code OAuth (работает на сервере) или API Key</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('openai-codex')">
        <span style="font-size:18px; color:var(--prov-codex);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">OpenAI Codex</div>
          <div style="font-size:11px; color:var(--text-muted);">Device Code OAuth (работает на сервере) или API Key</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('opencode-go')">
        <span style="font-size:18px; color:var(--prov-opencode);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">OpenCode Go</div>
          <div style="font-size:11px; color:var(--text-muted);">API Key / Токен подписки</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('claude')">
        <span style="font-size:18px; color:var(--prov-claude);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">Claude (Anthropic)</div>
          <div style="font-size:11px; color:var(--text-muted);">OAuth редирект (с поддержкой SSH port-forward) или API Key</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('antigravity')">
        <span style="font-size:18px; color:var(--prov-antigravity);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">Google Antigravity</div>
          <div style="font-size:11px; color:var(--text-muted);">OAuth редирект (с поддержкой SSH port-forward)</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('local')">
        <span style="font-size:18px; color:var(--status-healthy, #22c55e);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">Локальная модель (Local LLM)</div>
          <div style="font-size:11px; color:var(--text-muted);">llama.cpp / Ollama / vLLM (OpenAI-совместимый сервер)</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('openrouter')">
        <span style="font-size:18px; color:var(--text-muted);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">OpenRouter</div>
          <div style="font-size:11px; color:var(--text-muted);">API Key + optional custom base URL</div>
        </div>
      </button>
      <button class="btn btn-secondary" style="justify-content:flex-start; padding:12px;" onclick="showWizardStep2('nvidia')">
        <span style="font-size:18px; color:var(--text-muted);">●</span>
        <div style="text-align:left; margin-left:8px;">
          <div style="font-weight:700;">NVIDIA NIM</div>
          <div style="font-size:11px; color:var(--text-muted);">API Key + optional custom base URL</div>
        </div>
      </button>
    </div>
  `;
  elements.modalFooter.innerHTML = `
    <button class="btn btn-ghost" onclick="closeModal()">Отмена</button>
  `;
}

function showWizardStep2(providerId) {
  let bodyHtml = '';
  let footerHtml = '';

  if (providerId === 'grok' || providerId === 'openai-codex') {
    const providerName = providerId === 'grok' ? 'Grok (xAI)' : 'OpenAI Codex';
    bodyHtml = `
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Шаг 2 из 3: Авторизация ${providerName} по коду устройства
      </div>
      <div style="margin-bottom:10px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">Слот, в который войти:</label>
        <select class="input-text" style="width:100%;" id="wiz-device-slot">${buildSlotOptions(providerId)}</select>
        <div style="font-size:12px; color:var(--text-muted); margin-top:4px;">
          Вход в занятый слот заменит учётные данные, которые в нём сейчас.
        </div>
      </div>
      <div style="margin-bottom:10px;">
        <button class="btn btn-primary btn-sm" onclick="startDeviceAuth('${escapeHtml(providerId)}')">Начать авторизацию</button>
      </div>
      <div id="device-auth-box" style="background:var(--surface-muted); padding:14px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle);">
        <div style="color:var(--text-secondary);">Выберите слот и нажмите «Начать авторизацию».</div>
      </div>
    `;
    footerHtml = `
      <button class="btn btn-ghost" onclick="showWizardStep1()">← Назад</button>
      <button class="btn btn-primary" onclick="proceedToWizardStep3('${escapeHtml(providerId)}')">Продолжить →</button>
    `;
  } else if (providerId === 'antigravity' || providerId === 'claude') {
    const providerName = providerId === 'antigravity' ? 'Google Antigravity' : 'Claude';
    bodyHtml = `
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Шаг 2 из 3: Авторизация ${providerName}
      </div>
      <div style="margin-bottom:10px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">Слот, в который войти:</label>
        <select class="input-text" style="width:100%;" id="wiz-redirect-slot">${buildSlotOptions(providerId)}</select>
        <div style="font-size:12px; color:var(--text-muted); margin-top:4px;">
          Вход в занятый слот заменит учётные данные, которые в нём сейчас.
        </div>
      </div>
      <div style="margin-bottom:10px;">
        <button class="btn btn-primary btn-sm" onclick="startRedirectAuth('${escapeHtml(providerId)}')">Получить ссылку</button>
      </div>
      <div id="redirect-auth-box" style="background:var(--surface-muted); padding:14px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle);">
        <div style="color:var(--text-secondary);">Выберите слот и нажмите «Получить ссылку».</div>
      </div>
    `;
    footerHtml = `
      <button class="btn btn-ghost" onclick="showWizardStep1()">← Назад</button>
      <button class="btn btn-primary" onclick="proceedToWizardStep3('${escapeHtml(providerId)}')">Продолжить →</button>
    `;
  } else if (providerId === 'local' || providerId === 'local-llm' || providerId === 'llama.cpp' || providerId === 'ollama' || providerId === 'vllm') {
    // P0-1: reset stale wizard slot so local add_account does not reuse grok/antigravity slot
    window._wiz_device_profile = undefined;
    bodyHtml = `
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Шаг 2 из 3: Настройка локального сервера (Local LLM)
      </div>
      <div style="margin-bottom:12px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">URL сервера (Base URL):</label>
        <input type="text" class="input-text" style="width:100%;" id="wiz-base-url-input" placeholder="http://127.0.0.1:8081/v1" value="http://127.0.0.1:8081/v1">
      </div>
      <div style="margin-bottom:10px; font-size:12px; color:var(--text-muted);">
        Поиск серверов выполняется на машине, где запущен Hub (не в браузере).
      </div>
      <div style="margin-bottom:12px;">
        <button class="btn btn-secondary" style="width:100%;" id="wiz-discover-btn" onclick="discoverLocalServers('discover_local_models')" data-action="discover_local_models">🔍 Найти на этом компьютере</button>
        <div id="wiz-discover-status" style="font-size:12px; color:var(--text-secondary); margin-top:4px;"></div>
      </div>
      <!-- discover_local_models: async search runs on Hub machine, not browser -->
      <div id="wiz-discover-results" style="margin-bottom:12px;"></div>
      <div style="margin-bottom:12px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">API Key (опционально):</label>
        <input type="password" class="input-text" style="width:100%;" id="wiz-token-input" placeholder="Оставьте пустым, если ключ не требуется">
      </div>
    `;
    footerHtml = `
      <button class="btn btn-ghost" onclick="showWizardStep1()">← Назад</button>
      <button class="btn btn-primary" onclick="proceedToWizardStep3('${escapeHtml(providerId)}')">Продолжить →</button>
    `;
  } else if (providerId === 'openrouter' || providerId === 'nvidia') {
    const providerName = providerId === 'openrouter' ? 'OpenRouter' : 'NVIDIA NIM';
    bodyHtml = `
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Шаг 2 из 3: Подключение ${providerName}
      </div>
      <div style="margin-bottom:10px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">Слот, в который сохранить:</label>
        <select class="input-text" style="width:100%;" id="wiz-redirect-slot">${buildSlotOptions(providerId)}</select>
        <div style="font-size:12px; color:var(--text-muted); margin-top:4px;">
          Вход в занятый слот заменит учётные данные, которые в нём сейчас.
        </div>
      </div>
      <div style="margin-bottom:12px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">API Key:</label>
        <input type="password" class="input-text" style="width:100%;" id="wiz-token-input" placeholder="sk-...">
      </div>
      <div style="margin-bottom:12px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">Base URL (опционально):</label>
        <input type="text" class="input-text" style="width:100%;" id="wiz-base-url-input" placeholder="${providerId === 'openrouter' ? 'https://openrouter.ai/api/v1' : 'https://integrate.api.nvidia.com/v1'}">
        <div style="font-size:12px; color:var(--text-muted); margin-top:4px;">
          Оставьте пустым для значения по умолчанию.
        </div>
      </div>
    `;
    footerHtml = `
      <button class="btn btn-ghost" onclick="showWizardStep1()">← Назад</button>
      <button class="btn btn-primary" onclick="proceedToWizardStep3('${escapeHtml(providerId)}')">Продолжить →</button>
    `;
  } else {
    bodyHtml = `
      <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
        Шаг 2 из 3: Ввод API ключа ${escapeHtml(providerId)}
      </div>
      <div style="margin-bottom:12px;">
        <label style="display:block; font-weight:600; margin-bottom:4px;">API Key / Subscription Token:</label>
        <input type="password" class="input-text" style="width:100%;" id="wiz-token-input" placeholder="sk-...">
      </div>
    `;
    footerHtml = `
      <button class="btn btn-ghost" onclick="showWizardStep1()">← Назад</button>
      <button class="btn btn-primary" onclick="proceedToWizardStep3('${escapeHtml(providerId)}')">Продолжить →</button>
    `;
  }

  elements.modalBody.innerHTML = `
    <div id="modal-feedback-area"></div>
    ${bodyHtml}
  `;
  elements.modalFooter.innerHTML = footerHtml;
}

function proceedToWizardStep3(providerId) {
  const baseInput = document.getElementById('wiz-base-url-input');
  if (baseInput) {
    window._wiz_base_url = baseInput.value.trim();
  }
  const tokenInput = document.getElementById('wiz-token-input');
  if (tokenInput) {
    window._wiz_token = tokenInput.value.trim();
  }
  // P0-1 BUG-1: persist owner-selected slot BEFORE showWizardStep3 destroys the select elements
  // Only read slot elements for providers that have them (grok/openai-codex have wiz-device-slot,
  // antigravity/claude/openrouter/nvidia have wiz-redirect-slot). Local providers have no slot elements.
  const isDeviceAuthFlow = providerId === 'grok' || providerId === 'openai-codex';
  const isRedirectAuthFlow = providerId === 'antigravity' || providerId === 'claude' || providerId === 'openrouter' || providerId === 'nvidia';
  if (isDeviceAuthFlow) {
    const deviceSlot = document.getElementById('wiz-device-slot');
    if (deviceSlot && deviceSlot.value) {
      window._wiz_device_profile = deviceSlot.value;
    }
  } else if (isRedirectAuthFlow) {
    const redirectSlot = document.getElementById('wiz-redirect-slot');
    if (redirectSlot && redirectSlot.value) {
      window._wiz_device_profile = redirectSlot.value;
    }
  }
  // For local providers (local, local-llm, llama.cpp, ollama, vllm), do not read any slot elements
  showWizardStep3(providerId);
}

function showWizardStep3(providerId) {
  // GAP-3: динамически строим options ролей из currentSnapshot.routing
  const routing = currentSnapshot && currentSnapshot.routing ? currentSnapshot.routing : {};
  const roleIds = Object.keys(routing);

  let roleOptionsHtml = '';
  if (roleIds.length === 0) {
    // Fallback: минимальный набор если snapshot ещё не загружен
    roleOptionsHtml = `
      <option value="coder-primary">Кодер 1 (Primary Coder)</option>
      <option value="coder-secondary">Кодер 2 (Secondary Coder)</option>
      <option value="orchestrator">Оркестратор (Fallback Router)</option>
      <option value="reviewer">Ревьюер кода (Reviewer)</option>
      <option value="research">Исследователь (Researcher)</option>
      <option value="fast">Быстрый агент (Fast / Flash)</option>
      <option value="spare">Резервный пул (Spare Pool)</option>
    `;
  } else {
    roleOptionsHtml = roleIds.map((roleId) => {
      const pipeline = routing[roleId] || {};
      const label = pipeline.role_name_ru || roleId;
      const desc = CANONICAL_ROLE_DESCRIPTIONS[roleId] || pipeline.role_description_ru || '';
      return `<option value="${escapeHtml(roleId)}">${escapeHtml(label)}${desc ? ' — ' + escapeHtml(desc) : ''}</option>`;
    }).join('');
  }

  elements.modalBody.innerHTML = `
    <div id="modal-feedback-area"></div>
    <div style="margin-bottom:12px; font-size:13px; color:var(--text-secondary);">
      Шаг 3 из 3: Назначение роли для нового аккаунта
    </div>
    <div style="margin-bottom:14px;">
      <label style="display:block; font-weight:600; margin-bottom:4px;">Целевая роль в роутере:</label>
      <select class="select-filter" style="width:100%;" id="wiz-target-role">
        ${roleOptionsHtml}
      </select>
    </div>
  `;

  elements.modalFooter.innerHTML = `
    <button class="btn btn-ghost" onclick="showWizardStep2('${escapeHtml(providerId)}')">← Назад</button>
    <button class="btn btn-primary" onclick="finishAddAccount('${escapeHtml(providerId)}')">✓ Завершить подключение</button>
  `;
}

async function finishAddAccount(providerId) {
  const roleSelect = document.getElementById('wiz-target-role');
  const targetRole = roleSelect ? roleSelect.value : 'coder-primary';

  // GAP-2: owner-selected slot — читаем выбранный профиль из UI
  // Local providers (local, local-llm, llama.cpp, ollama, vllm) have no slot selector;
  // do NOT read stale DOM slot elements from previous flows in the same test session.
  const isLocalProvider = providerId === 'local' || providerId === 'local-llm' || providerId === 'llama.cpp' || providerId === 'ollama' || providerId === 'vllm';
  let selectedProfileId;
  if (isLocalProvider) {
    selectedProfileId = '';
  } else {
    const deviceSlot = document.getElementById('wiz-device-slot');
    const redirectSlot = document.getElementById('wiz-redirect-slot');
    // Use nullish coalescing: if _wiz_device_profile was explicitly set (not null/undefined),
    // it wins over any stale DOM element value (e.g. from a previous grok flow in the same test)
    selectedProfileId = window._wiz_device_profile ?? (deviceSlot?.value || redirectSlot?.value || '');
  }

  const feedbackArea = document.getElementById('modal-feedback-area');
  if (feedbackArea) {
    feedbackArea.innerHTML = '<div class="modal-feedback info">⏳ Сохранение учетной записи в роутере...</div>';
  }

  const payload = {
    provider: providerId,
    target_role: targetRole,
    // GAP-2: передаём выбранный слот, чтобы бэкенд НЕ делал find_free_slot для owner
    profile_id: selectedProfileId,
  };
  if (window._wiz_base_url) {
    payload.base_url = window._wiz_base_url;
  }
  if (window._wiz_token) {
    payload.token = window._wiz_token;
  }

  const res = await executeAction('add_account', payload);

  if (res && res.ok) {
    showToast('Аккаунт успешно добавлен в маршрутизацию', 'success');
    closeModal();
    fetchSnapshot();
  } else {
    if (feedbackArea) {
      feedbackArea.innerHTML = `<div class="modal-feedback error">❌ ${escapeHtml((res && res.message) || 'Не удалось завершить подключение')}</div>`;
    }
  }
}

// ─────────────────────────────────────────────────────────────
//  Авторизация по коду устройства (Grok, OpenAI Codex)
// ─────────────────────────────────────────────────────────────

let _deviceAuthTimer = null;

function stopDeviceAuthPolling() {
  if (_deviceAuthTimer) {
    clearInterval(_deviceAuthTimer);
    _deviceAuthTimer = null;
  }
}

async function startDeviceAuth(providerId) {
  stopDeviceAuthPolling();
  const box = document.getElementById('device-auth-box');
  if (!box) return;

  // GAP-1: owner-selected slot — показать выбранное в UI до начала auth
  const slotSelect = document.getElementById('wiz-device-slot');
  const selectedSlot = slotSelect ? slotSelect.value : '';
  if (selectedSlot) {
    window._wiz_device_profile = selectedSlot;
  }

  box.innerHTML = `<div style="color:var(--text-secondary);">Запрашиваем код у провайдера…</div>`;
  // P0-1 BUG-2: send profile_id so server knows which slot the owner chose
  const res = await executeAction('start_device_auth', { provider: providerId, profile_id: selectedSlot });
  if (!res || !res.ok) {
    box.innerHTML = `<div class="modal-feedback error">${escapeHtml((res && res.message) || 'Не удалось начать авторизацию')}</div>`;
    return;
  }

  const d = res.data || {};
  window._wiz_device_session = d.session_id;
  // P0-1 BUG-3: owner-selected slot wins over server-assigned profile_id
  window._wiz_device_profile = selectedSlot || d.profile_id;

  box.innerHTML = `
    <div style="font-weight:700; margin-bottom:6px;">1. Откройте ссылку:</div>
    <div style="display:flex; gap:8px; margin-bottom:12px;">
      <input type="text" class="input-text" style="flex:1;" id="wiz-auth-url" value="${escapeHtml(d.url || '')}" readonly>
      <button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-auth-url').value, '_blank')">Открыть</button>
      <button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-auth-url').value, 'Ссылка скопирована')">Копировать</button>
    </div>
    <div style="font-weight:700; margin-bottom:6px;">2. Введите код:</div>
    <div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
      <div style="font-family:var(--font-mono); font-size:22px; font-weight:700; color:var(--text-accent); letter-spacing:2px;" id="wiz-auth-code">${escapeHtml(d.code || '')}</div>
      <button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-auth-code').innerText, 'Код скопирован')">Копировать код</button>
    </div>
    <div id="device-auth-status" style="font-size:12px; color:var(--text-muted);">
      3. Подтвердите доступ — окно обновится само. Код живёт недолго, не откладывайте.
    </div>
  `;

  _deviceAuthTimer = setInterval(() => pollDeviceAuth(providerId), 3000);
}

async function pollDeviceAuth(providerId) {
  const status = document.getElementById('device-auth-status');
  if (!status) {
    stopDeviceAuthPolling();
    return;
  }

  const res = await executeAction('poll_device_auth', {
    provider: providerId,
    session_id: window._wiz_device_session,
  });

  if (!res) return;

  const state = (res.data || {}).status;
  if (res.ok && state === 'completed') {
    stopDeviceAuthPolling();
    status.innerHTML = '<span style="color:var(--status-healthy); font-weight:600;">Аккаунт подключён</span>';
    showToast('Аккаунт подключён', 'success');
    fetchSnapshot();
    return;
  }
  if (!res.ok) {
    // Отказ и просроченный код — конечные исходы, а не ожидание.
    stopDeviceAuthPolling();
    status.innerHTML = `<span style="color:var(--status-error); font-weight:600;">${escapeHtml(res.message || 'Авторизация не завершена')}</span>`;
  }
}

// Профили, реально участвующие в цепочках маршрутизации.
//
// Судить по assigned_roles нельзя: холодный резерв и ag-spare-2 значатся с
// ролью "spare", которой среди шести маршрутизируемых ролей нет. Поэтому
// берём состав самих цепочек — это точно и не зависит от названий ролей.
function profilesInRouting() {
  const routing = (currentSnapshot || {}).routing || {};
  const ids = new Set();
  Object.values(routing).forEach((pipeline) => {
    ((pipeline && pipeline.nodes) || []).forEach((n) => {
      if (n && n.profile_id) ids.add(n.profile_id);
    });
  });
  return ids;
}

// Список слотов провайдера из снимка: занятые помечены, свободные идут первыми.
function buildSlotOptions(providerId) {
  const profiles = ((currentSnapshot || {}).profiles_by_provider || {})[providerId] || [];
  if (!profiles.length) {
    return '<option value="">Список слотов ещё не получен</option>';
  }
  // Роль слота показываем прямо в списке. Без этого выбор вслепую: слоты
  // ag-spare-* и ag-cold-* не входят ни в одну цепочку, поэтому подключённый
  // в них аккаунт честно не появляется ни в «Обзоре», ни в «Маршрутизации» —
  // и выглядит это как пропажа.
  const routed = profilesInRouting();
  const free = [];
  const used = [];
  const idle = [];
  profiles.forEach((p) => {
    const isFree = p.health_state === 'not_configured';
    const inRouting = routed.has(p.profile_id);
    const roles = (p.assigned_roles || []).join(', ');
    const who = p.email || p.account_identity || '';
    const state = isFree ? 'свободен' : `занят${who ? ': ' + who : ''}`;
    const label = inRouting
      ? `${p.profile_id} — ${state} · ${roles || 'в маршрутизации'}`
      : `${p.profile_id} — ${state} · не участвует в маршрутизации`;
    const opt = `<option value="${escapeHtml(p.profile_id)}">${escapeHtml(label)}</option>`;
    if (!inRouting) idle.push(opt);
    else if (isFree) free.push(opt);
    else used.push(opt);
  });
  // Свободные слоты с ролью — первыми: именно они дают работающий маршрут.
  return free.concat(used, idle).join('');
}

let _redirectAuthTimer = null;

function stopRedirectAuthPolling() {
  if (_redirectAuthTimer) {
    clearInterval(_redirectAuthTimer);
    _redirectAuthTimer = null;
  }
}

// Вход по ссылке для Antigravity и Claude.
//
// Смысл: браузер не обязан быть на той машине, где работает Hub. Владелец
// открывает ссылку у себя, подтверждает доступ и возвращает результат сюда.
// Antigravity кладёт код в адресную строку (браузер при этом покажет ошибку
// соединения, если слушатель на другой машине — это нормально, адрес всё
// равно годен). Claude показывает код прямо на странице.
async function startRedirectAuth(providerId) {
  stopRedirectAuthPolling();
  const box = document.getElementById('redirect-auth-box');
  if (!box) return;

  // Слот выбирает владелец, а не догадка сервера.
  //
  // find_free_slot определяет занятость по файлу учётных данных, но agy на
  // Windows хранит их в keyring — файла нет ни у одного слота, поэтому все
  // десять считаются свободными и всегда возвращается первый, ag-orch-fallback.
  // Вход в него затёр бы работающий аккаунт. Список ниже строится из снимка,
  // который знает настоящее состояние, и показывает, что занято.
  const slot = document.getElementById('wiz-redirect-slot');
  const chosen = slot ? slot.value : '';

  const res = await executeAction('start_redirect_auth', {
    provider: providerId,
    profile_id: chosen || undefined,
  });
  if (!res || !res.ok) {
    box.innerHTML = `<div class="modal-feedback error">${escapeHtml((res && res.message) || 'Не удалось начать авторизацию')}</div>`;
    return;
  }

  const d = res.data || {};
  window._wiz_redirect_session = d.session_id;
  window._wiz_redirect_provider = providerId;
  window._wiz_redirect_slot_id = d.profile_id;

  const pastesUrl = d.paste_kind !== 'code';
  const label = pastesUrl
    ? 'Вставьте адрес из адресной строки браузера целиком:'
    : 'Вставьте код, показанный на странице:';
  const placeholder = pastesUrl ? 'http://127.0.0.1:…/oauth-callback?code=…' : 'Код со страницы провайдера';

  // Если хаб открыт не с этой же машины, браузер после подтверждения уйдёт на
  // 127.0.0.1:<порт> СВОЕЙ машины, где никто не слушает: получается тупик,
  // из которого адрес с кодом ещё надо как-то выковырять. Проброс этого порта
  // убирает проблему целиком — возврат попадает прямо в слушатель хаба и вход
  // завершается сам. Показываем готовую команду, а вставку оставляем запасным
  // путём.
  const host = window.location.hostname;
  const isRemote = host !== '127.0.0.1' && host !== 'localhost' && host !== '';
  const cbPort = d.port || 0;

  const tunnelNote = pastesUrl && isRemote && cbPort
    ? `<div class="modal-feedback info" style="margin-top:10px; font-size:12px;">
         <strong>Проще всего — пробросить порт возврата.</strong> Выполните у себя
         в терминале, до подтверждения доступа:
         <div style="display:flex; gap:6px; margin:6px 0;">
           <input type="text" class="input-text" style="flex:1; font-family:var(--font-mono); font-size:11px;"
                  id="wiz-redirect-tunnel" readonly
                  value="ssh -L ${cbPort}:127.0.0.1:${cbPort} ${escapeHtml(host)}">
           <button class="btn btn-secondary btn-sm"
                   onclick="copyToClipboard(document.getElementById('wiz-redirect-tunnel').value, 'Команда скопирована')">Копировать</button>
         </div>
         Тогда вход завершится сам и вставлять ничего не придётся.
       </div>`
    : '';

  const localNote = pastesUrl && d.redirect_uri
    ? `<div style="font-size:12px; color:var(--text-muted); margin-top:8px;">
         Без проброса браузер после подтверждения уйдёт на <code>${escapeHtml(d.redirect_uri)}</code>
         и покажет «страница недоступна» — это ожидаемо, хаб на другой машине.
         Скопируйте из адресной строки весь адрес целиком либо только значение
         <code>code=</code> — принимается и то, и другое.
       </div>`
    : '';

  box.innerHTML = `
    <div style="font-weight:700; margin-bottom:6px;">1. Откройте ссылку — на любой машине, где есть браузер:</div>
    <div style="display:flex; gap:8px; margin-bottom:12px;">
      <input type="text" class="input-text" style="flex:1;" id="wiz-redirect-url" value="${escapeHtml(d.url || '')}" readonly>
      <button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-redirect-url').value, '_blank')">Открыть</button>
      <button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-redirect-url').value, 'Ссылка скопирована')">Копировать</button>
    </div>
    <div style="font-weight:700; margin-bottom:6px;">2. ${escapeHtml(label)}</div>
    <div style="display:flex; gap:8px; margin-bottom:6px;">
      <input type="text" class="input-text" style="flex:1;" id="wiz-redirect-paste" placeholder="${escapeHtml(placeholder)}">
      <button class="btn btn-primary btn-sm" onclick="submitRedirectCallback()">Завершить вход</button>
    </div>
    ${tunnelNote}
    ${localNote}
    <div id="redirect-auth-status" style="font-size:12px; color:var(--text-muted); margin-top:10px;">
      Слот: ${escapeHtml(d.profile_id || '—')}. Ссылка действует 20 минут.
    </div>
  `;

  // Если браузер открыт на этой же машине, слушатель поймает возврат сам —
  // тогда вставлять ничего не придётся.
  _redirectAuthTimer = setInterval(pollRedirectAuth, 3000);
}

async function submitRedirectCallback() {
  const input = document.getElementById('wiz-redirect-paste');
  const status = document.getElementById('redirect-auth-status');
  if (!input || !status) return;

  const value = (input.value || '').trim();
  if (!value) {
    status.innerHTML = '<span style="color:var(--status-warning);">Поле пустое — вставьте значение из браузера.</span>';
    return;
  }

  status.innerHTML = 'Проверяем…';
  const res = await executeAction('submit_redirect_callback', {
    session_id: window._wiz_redirect_session,
    provider: window._wiz_redirect_provider,
    callback_url: value,
  });

  if (res && res.ok) {
    stopRedirectAuthPolling();
    status.innerHTML = '<span style="color:var(--status-healthy); font-weight:600;">Аккаунт подключён</span>' + redirectSlotRoleNote();
    showToast('Аккаунт подключён', 'success');
    fetchSnapshot();
    return;
  }
  // Промах при вставке не заканчивает сессию: ссылка ещё годна, можно
  // вставить снова. Поэтому опрос не останавливаем.
  status.innerHTML = `<span style="color:var(--status-error);">${escapeHtml((res && res.message) || 'Не удалось завершить вход')}</span>`;
}

// Если слот не входит ни в одну цепочку, аккаунт не появится ни в «Обзоре»,
// ни в «Маршрутизации» — и это выглядит как пропажа. Говорим об этом сразу.
function redirectSlotRoleNote() {
  const pid = window._wiz_redirect_slot_id;
  if (!pid) return '';
  const all = (currentSnapshot || {}).all_profiles || [];
  const prof = all.find((p) => p.profile_id === pid);
  const roles = prof ? (prof.assigned_roles || []) : [];
  if (profilesInRouting().has(pid)) {
    return `<div style="margin-top:6px; font-size:12px; color:var(--text-muted);">Роль: ${escapeHtml(roles.join(', '))}</div>`;
  }
  return `<div class="modal-feedback info" style="margin-top:8px; font-size:12px;">
      Слот <code>${escapeHtml(pid)}</code> не входит ни в одну цепочку, поэтому в
      «Обзоре» и «Маршрутизации» аккаунт не появится. Добавьте его нужной роли
      в разделе «Маршрутизация» кнопкой «+ Добавить».
    </div>`;
}

async function pollRedirectAuth() {
  const status = document.getElementById('redirect-auth-status');
  if (!status) {
    stopRedirectAuthPolling();
    return;
  }
  const res = await executeAction('poll_redirect_auth', {
    session_id: window._wiz_redirect_session,
    provider: window._wiz_redirect_provider,
  });
  if (!res) return;

  if (res.ok && (res.data || {}).status === 'completed') {
    stopRedirectAuthPolling();
    status.innerHTML = '<span style="color:var(--status-healthy); font-weight:600;">Аккаунт подключён</span>';
    showToast('Аккаунт подключён', 'success');
    fetchSnapshot();
    return;
  }
  if (!res.ok) {
    // Конечный отказ провайдера — например, доступ отклонён.
    stopRedirectAuthPolling();
    status.innerHTML = `<span style="color:var(--status-error); font-weight:600;">${escapeHtml(res.message || 'Авторизация не завершена')}</span>`;
  }
}

async function handleDeleteCredentials(profileId) {
  // Подтверждение обязательно: действие необратимо, аккаунт придётся
  // подключать заново.
  if (!confirm(`Удалить учётные данные профиля ${profileId}? Аккаунт придётся подключить заново.`)) {
    return;
  }
  const res = await executeAction('delete_credentials', { profile_id: profileId });
  if (res && res.ok) {
    closeModal();
    fetchSnapshot();
  }
}

// ── P0-3: Local LLM server discovery ──────────────────────────────────────

async function discoverLocalServers() {
  const btn = document.getElementById('wiz-discover-btn');
  const statusEl = document.getElementById('wiz-discover-status');
  const resultsEl = document.getElementById('wiz-discover-results');
  if (!btn || !statusEl || !resultsEl) return;

  btn.disabled = true;
  btn.textContent = '⏳ Поиск...';
  statusEl.textContent = '';
  resultsEl.innerHTML = '';

  // wiz-base-url-input: filled by selectDiscoveredServer when user picks a server
  const res = await executeAction('discover_local_models', {});

  btn.disabled = false;
  btn.textContent = '🔍 Найти на этом компьютере';

  const servers = (res && res.data && res.data.servers) ? res.data.servers : [];

  if (!servers.length) {
    statusEl.textContent = res && res.message ? res.message
      : 'Ничего не найдено. Запустите Ollama, LM Studio или llama.cpp, либо введите адрес вручную.';
    statusEl.style.color = 'var(--text-muted)';
    return;
  }

  // Show message if any servers have errors (occupied by other service)
  const errorServers = servers.filter(s => s.error);
  if (errorServers.length && errorServers.length === servers.length) {
    statusEl.textContent = 'Найдены серверы, но подключиться к ним не удалось:';
    statusEl.style.color = 'var(--text-secondary)';
  } else if (errorServers.length) {
    statusEl.textContent = `Найдено ${servers.length - errorServers.length} рабочих серверов (+${errorServers.length} с ошибкой). Выберите рабочий сервер:`;
    statusEl.style.color = 'var(--text-secondary)';
  } else {
    statusEl.textContent = `Найдено серверов: ${servers.length}. Выберите:`;
    statusEl.style.color = 'var(--text-secondary)';
  }

  // Render server list
  let html = '<div style="border:1px solid var(--border); border-radius:6px; overflow:hidden; margin-top:4px;">';
  servers.forEach((srv, idx) => {
    const hasError = !!srv.error;
    const modelCount = srv.models && srv.models.length ? srv.models.length : 0;
    const modelLabel = modelCount ? `, ${modelCount} модель${modelCount === 1 ? '' : modelCount < 5 ? 'ели' : 'елей'}` : '';
    const errorTitle = hasError ? ` title="${escapeHtml(srv.error)}"` : '';
    const rowStyle = hasError
      ? 'padding:8px 10px; background:#2a1a1a; cursor:not-allowed; opacity:0.7;'
      : 'padding:8px 10px; cursor:pointer;';
    const clickAttr = hasError ? '' : ` onclick="selectDiscoveredServer('${escapeHtml(srv.base_url)}')"`;
    const icon = hasError ? '⚠️' : '🖥️';
    html += `<div style="${rowStyle}"${errorTitle}${clickAttr}>
      <div style="font-size:13px; font-weight:600;">${icon} ${escapeHtml(srv.name)}</div>
      <div style="font-size:11px; color:var(--text-muted); margin-top:2px;">${escapeHtml(srv.base_url)}${modelLabel}${hasError ? ` — <span style="color:var(--status-warning);">⚠ ${escapeHtml(srv.error)}</span>` : ''}</div>
    </div>`;
  });
  html += '</div>';
  resultsEl.innerHTML = html;
}

function selectDiscoveredServer(baseUrl) {
  const input = document.getElementById('wiz-base-url-input');
  if (input) {
    input.value = baseUrl;
    // Scroll input into view
    input.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }
  // Clear results to keep UI clean
  const resultsEl = document.getElementById('wiz-discover-results');
  if (resultsEl) resultsEl.innerHTML = '';
  const statusEl = document.getElementById('wiz-discover-status');
  if (statusEl) {
    statusEl.textContent = `Выбран: ${baseUrl}`;
    statusEl.style.color = 'var(--status-healthy)';
  }
}
