const state = {
  user: 'admin',
  role: 'admin',
  token: null,
  authSource: 'local',
  fullName: null,
  features: {
    whatsapp: false,
  },
  logLines: [],
  interactions: [],
  oidc: {
    enabled: false,
    loginPath: '/auth/oidc/start?return_mode=popup',
    providerLabel: 'Keycloak',
  },
  liveCalls: {
    items: [],
    recentItems: [],
    selectedCallId: '',
    pollTimer: null,
    pendingAction: '',
    aiSummaries: {},
    aiSummaryPending: {},
    nameEditor: {
      open: false,
      mode: 'panel',
      callId: '',
      customerId: '',
      draft: '',
      saving: false,
      error: '',
    },
  },
  customers: {
    items: [],
    page: 1,
    pageSize: 7,
    selectedCustomerId: '',
    leadFormOpen: false,
    historyById: {},
    pendingHistoryById: {},
    historyErrorsById: {},
    nameEditor: {
      open: false,
      customerId: '',
      draft: '',
      saving: false,
      error: '',
      flash: '',
    },
  },
  telegram: {
    threads: [],
    selectedThreadId: '',
    messages: [],
    selectedThreadSummary: null,
    searchQuery: '',
    messageSearchQuery: '',
    messageSearchOpen: false,
    operatorTrayOpen: false,
    pendingAction: '',
    pollTimer: null,
  },
  messenger: {
    conversations: [],
    selectedConversationId: '',
    messages: [],
    selectedSummary: null,
    activeFilter: 'all',
    composerText: '',
    backendError: '',
    pendingAction: '',
    pollTimer: null,
  },
  whatsapp: {
    chats: [],
    selectedChatId: '',
    searchQuery: '',
    activeFilter: 'all',
    composerText: '',
    selectedThreadSummary: null,
    pendingAction: '',
    pollTimer: null,
    mode: 'idle',
    backendError: '',
    mockFallbackLogged: false,
  },
  browserPhone: {
    config: null,
    ua: null,
    registerer: null,
    session: null,
    status: 'Не настроен',
    connected: false,
    connecting: false,
    incoming: false,
    muted: false,
    micDeviceId: '',
    speakerDeviceId: '',
    autoClaimCallId: '',
    popupCallId: '',
    endingCallId: '',
    callPhase: '',
    autoClaimInFlight: false,
    warning: '',
    settingsOpen: false,
    sessionStartedAt: '',
    localStream: null,
    localStreamPromise: null,
    ringtoneContext: null,
    ringtoneTimer: null,
    ringtoneActive: false,
  },
};

const $ = (id) => document.getElementById(id);
const SESSION_STORAGE_KEY = 'cc_session';
const DEMO_ASSIGNEE = 'operator_a';
const DEMO_QUEUE = 'line2';
const ROLE_LABELS = {
  admin: 'Администратор',
  supervisor: 'Супервизор',
  operator: 'Оператор',
  analyst: 'Аналитик',
};

const STATUS_META = {
  new: { label: 'новое', className: 'status-new' },
  in_progress: { label: 'назначено', className: 'status-in-progress' },
  escalated: { label: 'эскалировано', className: 'status-escalated' },
  closed: { label: 'закрыто', className: 'status-closed' },
  abandoned: { label: 'потеряно', className: 'status-closed' },
};

const CHANNEL_LABELS = {
  voice: 'Голос',
  telegram: 'Telegram',
  whatsapp: 'WhatsApp',
  webchat: 'Webchat',
  email: 'Email',
};

const TELEGRAM_AVATAR_PALETTES = [
  ['#5bc7ff', '#288cff'],
  ['#7a8cff', '#5664f5'],
  ['#52d6b5', '#0f9f8a'],
  ['#ffa96e', '#f57b3d'],
  ['#ff88b6', '#f34b86'],
  ['#8a7dff', '#5d58ea'],
];

const WHATSAPP_AVATAR_PALETTES = [
  ['#2dd4bf', '#0f766e'],
  ['#60a5fa', '#1d4ed8'],
  ['#f59e0b', '#c2410c'],
  ['#a78bfa', '#6d28d9'],
  ['#f472b6', '#be185d'],
  ['#34d399', '#047857'],
  ['#fb7185', '#be123c'],
];

const WHATSAPP_MOCK_CHATS = [
  {
    id: 'wa-amy-chen',
    title: 'Amy Chen',
    handle: '@amyc',
    statusLine: 'в сети',
    isGroup: false,
    unreadCount: 0,
    pinned: true,
    muted: false,
    lastMessageAt: '2026-03-10T18:43:00+05:00',
    lastMessagePreview: 'Отлично, пришлите образцы и счёт.',
    messages: [
      {
        id: 'wa-amy-1',
        direction: 'in',
        text: 'Здравствуйте! В обновлённое предложение вошли матовые бутылки?',
        createdAt: '2026-03-10T18:15:00+05:00',
      },
      {
        id: 'wa-amy-2',
        direction: 'out',
        text: 'Да, я добавил и матовое покрытие, и вариант с индивидуальной этикеткой.',
        createdAt: '2026-03-10T18:18:00+05:00',
        deliveryStatus: 'read',
      },
      {
        id: 'wa-amy-3',
        direction: 'in',
        text: 'Отлично, пришлите образцы и счёт.',
        createdAt: '2026-03-10T18:43:00+05:00',
      },
    ],
  },
  {
    id: 'wa-design-ops',
    title: 'Design Ops',
    handle: '6 participants',
    statusLine: 'Анна, Тимур, Лейла и ещё 3 участника',
    isGroup: true,
    unreadCount: 12,
    pinned: true,
    muted: true,
    lastMessageAt: '2026-03-10T17:58:00+05:00',
    lastMessagePreview: 'Лейла: hero-карточки согласованы, остались только иконки.',
    messages: [
      {
        id: 'wa-design-1',
        direction: 'system',
        text: 'Today',
        createdAt: '2026-03-10T09:00:00+05:00',
      },
      {
        id: 'wa-design-2',
        direction: 'in',
        author: 'Timur',
        text: 'Залил обновлённые отступы сайдбара и тёмные токены.',
        createdAt: '2026-03-10T17:28:00+05:00',
      },
      {
        id: 'wa-design-3',
        direction: 'out',
        text: 'Отлично. Оставьте rail компактным, а действия с чатом перенесите в header.',
        createdAt: '2026-03-10T17:35:00+05:00',
        deliveryStatus: 'read',
      },
      {
        id: 'wa-design-4',
        direction: 'in',
        author: 'Leila',
        text: 'Hero-карточки согласованы, остались только иконки.',
        createdAt: '2026-03-10T17:58:00+05:00',
      },
    ],
  },
  {
    id: 'wa-marat-logistics',
    title: 'Marat Logistics',
    handle: '+7 707 555 2001',
    statusLine: 'был в сети сегодня в 16:42',
    isGroup: false,
    unreadCount: 0,
    pinned: false,
    muted: false,
    lastMessageAt: '2026-03-10T16:41:00+05:00',
    lastMessagePreview: 'Грузовик будет у ворот склада через 20 минут.',
    messages: [
      {
        id: 'wa-marat-1',
        direction: 'in',
        text: 'Грузовик будет у ворот склада через 20 минут.',
        createdAt: '2026-03-10T16:41:00+05:00',
      },
      {
        id: 'wa-marat-2',
        direction: 'out',
        text: 'Принято. На пост охраны уже передали обновлённую накладную.',
        createdAt: '2026-03-10T16:44:00+05:00',
        deliveryStatus: 'delivered',
      },
    ],
  },
  {
    id: 'wa-leila',
    title: 'Leila',
    handle: '@leila.studio',
    statusLine: 'печатает...',
    isGroup: false,
    unreadCount: 1,
    pinned: false,
    muted: false,
    lastMessageAt: '2026-03-10T15:26:00+05:00',
    lastMessagePreview: 'Можно перенести ревью на 18:30?',
    messages: [
      {
        id: 'wa-leila-1',
        direction: 'out',
        text: 'Могу быстро посмотреть в 18:00, если так удобнее.',
        createdAt: '2026-03-10T15:20:00+05:00',
        deliveryStatus: 'read',
      },
      {
        id: 'wa-leila-2',
        direction: 'in',
        text: 'Можно перенести ревью на 18:30?',
        createdAt: '2026-03-10T15:26:00+05:00',
      },
    ],
  },
  {
    id: 'wa-family-trip',
    title: 'Family Trip 2026',
    handle: '8 participants',
    statusLine: 'Закреплённая группа',
    isGroup: true,
    unreadCount: 0,
    pinned: false,
    muted: false,
    lastMessageAt: '2026-03-10T13:08:00+05:00',
    lastMessagePreview: 'Арман: я забронировал отель на пятницу.',
    messages: [
      {
        id: 'wa-family-1',
        direction: 'in',
        author: 'Arman',
        text: 'Я забронировал отель на пятницу.',
        createdAt: '2026-03-10T13:08:00+05:00',
      },
      {
        id: 'wa-family-2',
        direction: 'out',
        text: 'Отлично. Позже отправлю сюда маршрут и детали заселения.',
        createdAt: '2026-03-10T13:11:00+05:00',
        deliveryStatus: 'read',
      },
    ],
  },
  {
    id: 'wa-studio-intake',
    title: 'Studio Intake',
    handle: '+7 701 222 8811',
    statusLine: 'был в сети вчера в 22:10',
    isGroup: false,
    unreadCount: 4,
    pinned: false,
    muted: true,
    lastMessageAt: '2026-03-09T22:08:00+05:00',
    lastMessagePreview: 'Можете ещё раз прислать референсы по упаковке?',
    messages: [
      {
        id: 'wa-studio-1',
        direction: 'in',
        text: 'Можете ещё раз прислать референсы по упаковке?',
        createdAt: '2026-03-09T22:08:00+05:00',
      },
      {
        id: 'wa-studio-2',
        direction: 'out',
        text: 'Да, сейчас отправлю свежий PDF и подборку образцов.',
        createdAt: '2026-03-09T22:12:00+05:00',
        deliveryStatus: 'read',
      },
    ],
  },
  {
    id: 'wa-oleg-voice',
    title: 'Oleg Voice',
    handle: '+7 705 123 4490',
    statusLine: 'был в сети вчера в 20:31',
    isGroup: false,
    unreadCount: 0,
    pinned: false,
    muted: false,
    lastMessageAt: '2026-03-09T19:18:00+05:00',
    lastMessagePreview: 'Спасибо, заметки для handoff хватило, чтобы продолжить.',
    messages: [
      {
        id: 'wa-oleg-1',
        direction: 'out',
        text: 'Оставил handoff-сводку в карточке задачи и отправил заметки по звонку.',
        createdAt: '2026-03-09T19:08:00+05:00',
        deliveryStatus: 'read',
      },
      {
        id: 'wa-oleg-2',
        direction: 'in',
        text: 'Спасибо, заметки для handoff хватило, чтобы продолжить.',
        createdAt: '2026-03-09T19:18:00+05:00',
      },
    ],
  },
];

const BOARD_COLUMNS = [
  { key: 'new', title: 'Новые' },
  { key: 'in_progress', title: 'В работе' },
  { key: 'escalated', title: '2 линия' },
  { key: 'closed', title: 'Закрытые' },
];

const UNIFIED_INBOX_COLUMNS = [
  { key: 'new', title: 'Новые', emptyMessage: 'Новых задач сейчас нет.' },
  { key: 'mine', title: 'Мои', emptyMessage: 'За вами пока ничего не закреплено.' },
  { key: 'ai', title: 'AI handoff', emptyMessage: 'Передач от AI сейчас нет.' },
  { key: 'escalated', title: 'Эскалации', emptyMessage: 'Эскалаций сейчас нет.' },
  { key: 'calls', title: 'Активные звонки', emptyMessage: 'Активных звонков сейчас нет.' },
];

const MESSENGER_FILTERS = {
  all: 'Все',
  new: 'Новые',
  mine: 'Мои',
  ai: 'AI handoff',
};

const LIVE_TELEPHONY_LABELS = {
  ringing: 'Звонит',
  claimed: 'Взято',
  connected: 'Соединено',
  ended: 'Завершено',
  failed: 'Ошибка',
};

const VOICE_AI_STATE_META = {
  greeting: { label: 'AI активен', className: 'ai-active' },
  listening: { label: 'AI активен', className: 'ai-active' },
  thinking: { label: 'AI думает', className: 'ai-thinking' },
  speaking: { label: 'AI активен', className: 'ai-active' },
  active: { label: 'AI активен', className: 'ai-active' },
  handoff_requested: { label: 'Ждёт человека', className: 'ai-handoff' },
  handoff_required: { label: 'Ждёт человека', className: 'ai-handoff' },
  human_owned: { label: 'У оператора', className: 'ai-human' },
  error: { label: 'Ошибка AI', className: 'ai-error' },
  closed: { label: 'AI завершён', className: 'ai-muted' },
};

const OPERATOR_VIEW_IDS = {
  workspace: 'workspaceView',
  messages: 'messagesView',
  customers: 'customersView',
  'customer-profile': 'customerProfileView',
  telegram: 'telegramView',
  whatsapp: 'whatsappView',
  calls: 'callsView',
};

function featureEnabled(feature) {
  return Boolean(state.features?.[feature]);
}

function isOperatorViewEnabled(view) {
  if (view === 'whatsapp') {
    return featureEnabled('whatsapp');
  }
  return true;
}

function stopWhatsappPolling() {
  if (state.whatsapp.pollTimer) {
    window.clearInterval(state.whatsapp.pollTimer);
    state.whatsapp.pollTimer = null;
  }
}

function resetWhatsappState(mode = 'idle') {
  state.whatsapp.chats = [];
  state.whatsapp.selectedChatId = '';
  state.whatsapp.searchQuery = '';
  state.whatsapp.activeFilter = 'all';
  state.whatsapp.composerText = '';
  state.whatsapp.selectedThreadSummary = null;
  state.whatsapp.pendingAction = '';
  state.whatsapp.backendError = '';
  state.whatsapp.mockFallbackLogged = false;
  state.whatsapp.mode = mode;
}

function syncWhatsappFeatureVisibility() {
  const enabled = featureEnabled('whatsapp');
  document.querySelectorAll('[data-feature="whatsapp"]').forEach((element) => {
    element.hidden = !enabled;
  });
  document.querySelectorAll('[data-feature-option="whatsapp"]').forEach((element) => {
    element.hidden = !enabled;
    element.disabled = !enabled;
  });
  if (!enabled) {
    stopWhatsappPolling();
    resetWhatsappState('disabled');
    if ($('interactionChannel')?.value === 'whatsapp') {
      $('interactionChannel').value = 'voice';
    }
    return;
  }
  if (state.whatsapp.mode === 'disabled') {
    resetWhatsappState('idle');
  }
}

function syncSessionFromInputs() {
  state.user = $('sessionUser').value.trim() || 'admin';
  state.role = $('sessionRole').value || 'admin';
}

function destinationForRole(role) {
  if (role === 'admin') {
    return '/admin';
  }
  if (role === 'supervisor') {
    return '/supervisor';
  }
  if (role === 'analyst') {
    return '/analyst';
  }
  return '/operator';
}

function applyRoleNavigation() {
  document.querySelectorAll('[data-roles]').forEach((link) => {
    const allowed = (link.dataset.roles || '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean);
    link.style.display = !allowed.length || allowed.includes(state.role) ? '' : 'none';
  });
  document.querySelectorAll('.nav-group').forEach((group) => {
    const shellLinks = [...group.querySelectorAll('[data-shell]')];
    if (!shellLinks.length) {
      group.style.display = '';
      return;
    }
    const hasVisibleShellLink = shellLinks.some((link) => link.style.display !== 'none');
    group.style.display = hasVisibleShellLink ? '' : 'none';
  });
}

function persistSession() {
  window.localStorage.setItem(
    SESSION_STORAGE_KEY,
    JSON.stringify({
      access_token: state.token,
      user: state.user,
      role: state.role,
      auth_source: state.authSource,
      full_name: state.fullName,
    }),
  );
}

function clearStoredSession() {
  window.localStorage.removeItem(SESSION_STORAGE_KEY);
}

function restoreStoredSession() {
  const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);
  if (!raw) {
    return false;
  }
  try {
    const payload = JSON.parse(raw);
    if (!payload?.access_token) {
      clearStoredSession();
      return false;
    }
    state.token = payload.access_token;
    state.user = payload.user || state.user;
    state.role = payload.role || state.role;
    state.authSource = payload.auth_source || 'local';
    state.fullName = payload.full_name || null;
    $('sessionUser').value = state.user;
    $('sessionRole').value = state.role;
    if ($('authPanel')) {
      $('authPanel').style.display = 'none';
    }
    updateProfileMeta();
    applyRoleNavigation();
    return true;
  } catch {
    clearStoredSession();
    return false;
  }
}

function logout() {
  if (state.liveCalls.pollTimer) {
    window.clearInterval(state.liveCalls.pollTimer);
    state.liveCalls.pollTimer = null;
  }
  if (state.telegram.pollTimer) {
    window.clearInterval(state.telegram.pollTimer);
    state.telegram.pollTimer = null;
  }
  stopMessengerPolling();
  stopWhatsappPolling();
  clearStoredSession();
  window.location.href = '/';
}

function shortJson(payload) {
  if (!payload || typeof payload !== 'object') {
    return '';
  }
  const parts = Object.entries(payload)
    .filter(([, value]) => value !== null && value !== undefined && value !== '')
    .slice(0, 3)
    .map(([key, value]) => {
      const printable = typeof value === 'object' ? JSON.stringify(value) : value;
      return `${key}=${printable}`;
    });
  return parts.join(', ');
}

function log(message, payload) {
  const ts = new Date().toLocaleTimeString();
  const suffix = shortJson(payload);
  const line = suffix ? `[${ts}] ${message} (${suffix})` : `[${ts}] ${message}`;
  state.logLines.unshift(line);
  state.logLines = state.logLines.slice(0, 14);
  const target = $('eventLog');
  if (target) {
    target.textContent = state.logLines.join('\n');
  }
}

function setOutput(targetId, payload) {
  $(targetId).textContent = JSON.stringify(payload, null, 2);
}

function setEmptyList(targetId, message) {
  $(targetId).innerHTML = `<li class="empty-state">${message}</li>`;
}

function setEmptyBlock(targetId, message) {
  $(targetId).innerHTML = `<div class="empty-state">${message}</div>`;
}

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

function escapeRegExp(value) {
  return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function customerHashSeed(item, fallback = 0) {
  const raw = `${item?.customer_id || ''}|${item?.display_name || ''}|${(item?.phones || []).join(',') || ''}`;
  return [...raw].reduce((acc, char, index) => (acc + char.charCodeAt(0) * (index + 1)) % 997, fallback);
}

function normalizedCustomerTags(item) {
  return Array.isArray(item?.tags)
    ? item.tags.map((tag) => String(tag || '').trim().toLowerCase()).filter(Boolean)
    : [];
}

function isFacebookCustomer(item) {
  const tags = normalizedCustomerTags(item);
  return tags.includes('facebook') || tags.includes('facebook-lead');
}

function isFacebookLeadInteraction(item) {
  const subject = String(item?.subject || item?.title || '').trim().toLowerCase();
  if (subject.startsWith('[facebook]')) {
    return true;
  }

  const customerId = item?.customer_id || item?.customerId || '';
  if (!customerId) {
    return false;
  }
  const customer = state.customers.items.find((entry) => entry.customer_id === customerId) || null;
  return isFacebookCustomer(customer);
}

function customerInitials(name) {
  const parts = String(name || '')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2);
  if (!parts.length) {
    return 'LD';
  }
  return parts.map((part) => part[0].toUpperCase()).join('');
}

function customerLeadSourceMeta(item, index) {
  if (isFacebookCustomer(item)) {
    return { label: 'Facebook', className: 'lead-badge source-facebook' };
  }
  const variants = [
    { label: 'Instagram', className: 'lead-badge source-instagram' },
    { label: 'WhatsApp', className: 'lead-badge source-whatsapp' },
    { label: 'Site', className: 'lead-badge source-site' },
    { label: 'Referral', className: 'lead-badge source-referral' },
  ];
  return variants[customerHashSeed(item, index) % variants.length];
}

function customerLeadStatusMeta(item, index) {
  if (isFacebookCustomer(item)) {
    return { label: 'Новый', className: 'lead-badge status-new' };
  }
  const variants = [
    { label: 'Переговоры', className: 'lead-badge status-negotiation' },
    { label: 'Квалификация', className: 'lead-badge status-qualification' },
    { label: 'Предложение', className: 'lead-badge status-proposal' },
    { label: 'Первичный контакт', className: 'lead-badge status-initial' },
    { label: 'Успех', className: 'lead-badge status-success' },
    { label: 'Новый', className: 'lead-badge status-new' },
    { label: 'Отказ', className: 'lead-badge status-lost' },
  ];
  return variants[customerHashSeed(item, index + 17) % variants.length];
}

function customerLeadScore(item, index) {
  return 12 + (customerHashSeed(item, index + 29) % 81);
}

function customerLeadScoreClass(score) {
  if (score >= 80) {
    return 'lead-score score-strong';
  }
  if (score >= 50) {
    return 'lead-score score-medium';
  }
  return 'lead-score score-weak';
}

function customerLeadCompany(item) {
  const tags = (Array.isArray(item?.tags) ? item.tags : [])
    .map((tag) => String(tag || '').trim())
    .filter((tag) => tag && !['facebook', 'facebook-lead', 'hot-lead'].includes(tag.toLowerCase()));
  if (tags.length) {
    return tags[0];
  }
  const suffixes = ['Tech', 'Group', 'Retail', 'Logistics', 'Studio', 'Services'];
  const seed = customerHashSeed(item, 41);
  return `${String(item?.display_name || 'Lead').split(' ')[0]} ${suffixes[seed % suffixes.length]}`;
}

function customerLeadCreatedAtLabel(item, index) {
  if (item?.created_at) {
    return formatIsoShort(item.created_at);
  }
  const seed = customerHashSeed(item, index + 59);
  const shifted = new Date(Date.now() - ((seed % 9) * 86400000 + (seed % 18) * 3600000));
  return shifted.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric' });
}

function customerLeadRows() {
  const start = (state.customers.page - 1) * state.customers.pageSize;
  return state.customers.items.slice(start, start + state.customers.pageSize);
}

function selectedCustomer() {
  return state.customers.items.find((item) => item.customer_id === state.customers.selectedCustomerId) || state.customers.items[0] || null;
}

function customerProfileHash(customerId = state.customers.selectedCustomerId) {
  return customerId ? `#customer-profile/${encodeURIComponent(customerId)}` : '#customer-profile';
}

function openCustomerProfile(customerId) {
  selectCustomer(customerId || '');
  ensureCustomerHistoryLoaded(customerId || state.customers.selectedCustomerId, { force: true }).catch(() => {});
  window.location.hash = customerProfileHash(customerId || state.customers.selectedCustomerId);
}

function interactionById(interactionId) {
  return state.interactions.find((item) => item.interaction_id === interactionId) || null;
}

function customerIdForInteractionId(interactionId) {
  return interactionById(interactionId)?.customer_id || '';
}

function customerDisplayName(customerId) {
  if (!customerId) {
    return 'не привязан';
  }
  const customer = state.customers.items.find((item) => item.customer_id === customerId) || null;
  return customer?.display_name || customerId;
}

function customerIndex(customer) {
  if (!customer) {
    return -1;
  }
  return state.customers.items.findIndex((item) => item.customer_id === customer.customer_id);
}

function customerPhones(customer) {
  const phones = [];
  if (Array.isArray(customer?.phones)) {
    phones.push(...customer.phones);
  }
  if (customer?.phone) {
    phones.push(customer.phone);
  }
  if (customer?.preferred_phone) {
    phones.unshift(customer.preferred_phone);
  }
  return [...new Set(phones.map((value) => String(value || '').trim()).filter(Boolean))];
}

function customerInteractions(customer) {
  if (!customer) {
    return [];
  }
  return state.interactions.filter((item) => item.customer_id === customer.customer_id);
}

function customerInteractionIds(customer) {
  return new Set(customerInteractions(customer).map((item) => item.interaction_id).filter(Boolean));
}

function customerTelegramThreads(customer) {
  const interactionIds = customerInteractionIds(customer);
  return state.telegram.threads.filter((thread) => interactionIds.has(thread.interaction_id));
}

function customerLiveCalls(customer) {
  const interactionIds = customerInteractionIds(customer);
  const phones = new Set(customerPhones(customer));
  return uniqueLiveCalls([...state.liveCalls.items, ...state.liveCalls.recentItems]).filter((item) => {
    if (interactionIds.has(item.interaction_id)) {
      return true;
    }
    return phones.has(String(item.caller_number || '').trim());
  });
}

function customerHistoryPayload(customerId) {
  if (!customerId) {
    return null;
  }
  return state.customers.historyById[customerId] || null;
}

function customerHistoryPending(customerId) {
  return Boolean(customerId && state.customers.pendingHistoryById[customerId]);
}

async function ensureCustomerHistoryLoaded(customerId, options = {}) {
  const { force = false } = options;
  if (!customerId) {
    return null;
  }
  if (customerHistoryPending(customerId)) {
    return customerHistoryPayload(customerId);
  }
  if (!force && customerHistoryPayload(customerId)) {
    return customerHistoryPayload(customerId);
  }
  state.customers.pendingHistoryById[customerId] = true;
  delete state.customers.historyErrorsById[customerId];
  try {
    const data = await api('customer', `customers/${encodeURIComponent(customerId)}/history`);
    state.customers.historyById[customerId] = data || null;
    return state.customers.historyById[customerId];
  } catch (err) {
    state.customers.historyErrorsById[customerId] = err.message;
    return null;
  } finally {
    delete state.customers.pendingHistoryById[customerId];
    renderCustomerProfilePage();
  }
}

function customerPreferredChannel(customer) {
  const counters = new Map();
  customerInteractions(customer).forEach((item) => {
    const key = item.channel || 'voice';
    counters.set(key, (counters.get(key) || 0) + 1);
  });
  if (customerTelegramThreads(customer).length) {
    counters.set('telegram', (counters.get('telegram') || 0) + customerTelegramThreads(customer).length);
  }
  if (customerLiveCalls(customer).length) {
    counters.set('voice', (counters.get('voice') || 0) + customerLiveCalls(customer).length);
  }
  const [winner] = [...counters.entries()].sort((left, right) => right[1] - left[1]);
  return winner?.[0] || 'voice';
}

function customerHistoryTimeValue(value) {
  if (!value) {
    return 0;
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return 0;
  }
  return dt.getTime();
}

function customerHistoryMeta(kind) {
  if (kind === 'voice') {
    return { token: 'VC', className: 'voice' };
  }
  if (kind === 'telegram') {
    return { token: 'TG', className: 'telegram' };
  }
  if (kind === 'email') {
    return { token: 'EM', className: 'email' };
  }
  if (kind === 'webchat') {
    return { token: 'WC', className: 'webchat' };
  }
  if (kind === 'profile') {
    return { token: 'ID', className: 'profile' };
  }
  return { token: 'CS', className: 'case' };
}

function buildCustomerHistoryEvents(customer) {
  if (!customer) {
    return [];
  }

  const company = customerLeadCompany(customer);
  const phones = customerPhones(customer);
  const interactions = customerInteractions(customer);
  const interactionIds = new Set(interactions.map((item) => item.interaction_id).filter(Boolean));
  const threads = customerTelegramThreads(customer);
  const liveCalls = customerLiveCalls(customer);
  const events = [];

  events.push({
    timestamp: customer.created_at || '',
    kind: 'profile',
    title: 'Профиль клиента активен',
    body: phones.length
      ? `Контакт доступен для оператора по номеру ${phones[0]}.`
      : 'Контакт добавлен в клиентскую базу и готов к работе.',
    note: [customer.customer_id, company].filter(Boolean).join(' • '),
  });

  interactions.forEach((item) => {
    const meta = statusMeta(item.status);
    events.push({
      timestamp: item.updated_at || item.created_at || '',
      kind: item.channel || 'case',
      title: `${channelLabel(item.channel)} • ${meta.label}`,
      body: item.subject || 'Обращение без темы',
      note: [
        item.interaction_id,
        item.assigned_to ? `владелец ${item.assigned_to}` : '',
        item.queue_id ? `queue ${item.queue_id}` : '',
      ].filter(Boolean).join(' • '),
      interaction_id: item.interaction_id,
    });
  });

  threads.forEach((thread) => {
    events.push({
      timestamp: thread.last_message_at || '',
      kind: 'telegram',
      title: 'Telegram диалог',
      body: thread.last_message_preview || 'Последнее сообщение недоступно.',
      note: [
        thread.display_name || thread.username || `chat ${thread.chat_id}`,
        thread.claimed_by_user ? `владелец ${thread.claimed_by_user}` : '',
        thread.status ? statusMeta(thread.status).label : '',
      ].filter(Boolean).join(' • '),
      interaction_id: thread.interaction_id,
      thread_id: thread.thread_id,
    });
  });

  const activeThread = selectedTelegramThread();
  if (activeThread && interactionIds.has(activeThread.interaction_id) && state.telegram.messages.length) {
    state.telegram.messages.slice(-3).forEach((message) => {
      const direction = message.direction === 'outbound' ? 'Исходящее сообщение' : 'Входящее сообщение';
      events.push({
        timestamp: message.created_at || '',
        kind: 'telegram',
        title: direction,
        body: message.text || 'Сообщение без текста',
        note: [
          activeThread.display_name || activeThread.username || `chat ${activeThread.chat_id}`,
          message.operator_user ? `оператор ${message.operator_user}` : '',
          message.delivery_status || '',
        ].filter(Boolean).join(' • '),
        interaction_id: activeThread.interaction_id,
        thread_id: activeThread.thread_id,
      });
    });
  }

  liveCalls.forEach((item) => {
    const completed = item.telephony_status === 'ended' || item.ended_at;
    events.push({
      timestamp: item.last_transition_at || item.ended_at || item.connected_at || item.started_at || '',
      kind: 'voice',
      title: completed ? `Звонок • ${terminalActionLabel(item)}` : `Звонок • ${telephonyLabel(item.telephony_status)}`,
      body: `Номер клиента: ${item.caller_number || 'не определён'}`,
      note: [
        item.call_id,
        item.operator_extension ? `внутр. ${item.operator_extension}` : '',
        item.claimed_by_user ? `владелец ${item.claimed_by_user}` : '',
      ].filter(Boolean).join(' • '),
      interaction_id: item.interaction_id,
      call_id: item.call_id,
    });
    const aiMeta = voiceAiStatusMeta(item);
    if (aiMeta) {
      events.push({
        timestamp: item.ai_last_model_at || item.last_transition_at || item.updated_at || '',
        kind: 'voice',
        title: `Голосовой AI • ${aiMeta.label}`,
        body: item.ai_handoff_reason || 'AI участвовал в обработке звонка и обновил контекст для оператора.',
        note: [
          item.call_id,
          item.voice_session_id ? `голос ${item.voice_session_id}` : '',
          item.ai_session_id ? `ai ${item.ai_session_id}` : '',
        ].filter(Boolean).join(' • '),
        interaction_id: item.interaction_id,
        call_id: item.call_id,
      });
    }
  });

  return events
    .sort((left, right) => {
      const timeDelta = customerHistoryTimeValue(right.timestamp) - customerHistoryTimeValue(left.timestamp);
      if (timeDelta !== 0) {
        return timeDelta;
      }
      return String(right.note || '').localeCompare(String(left.note || ''));
    })
    .slice(0, 9);
}

function latestItemByTime(items, fields = []) {
  const source = Array.isArray(items) ? items.filter(Boolean) : [];
  if (!source.length) {
    return null;
  }
  return [...source].sort((left, right) => {
    const rightTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(right?.[field])), 0);
    const leftTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(left?.[field])), 0);
    if (rightTime !== leftTime) {
      return rightTime - leftTime;
    }
    return String(right?.interaction_id || right?.thread_id || right?.call_id || '').localeCompare(
      String(left?.interaction_id || left?.thread_id || left?.call_id || ''),
    );
  })[0] || null;
}

function customerHistoryEventActions(event, context = {}) {
  const threadIds = context.threadIds instanceof Set ? context.threadIds : new Set();
  const callIds = context.callIds instanceof Set ? context.callIds : new Set();
  const actions = [];
  if (event?.thread_id && threadIds.has(event.thread_id)) {
    actions.push({
      action: 'telegram',
      label: 'К Telegram',
      threadId: event.thread_id,
    });
  }
  if (event?.call_id && callIds.has(event.call_id)) {
    actions.push({
      action: 'call',
      label: 'К звонку',
      callId: event.call_id,
    });
  }
  if (event?.interaction_id) {
    actions.push({
      action: 'interaction',
      label: 'К кейсу',
      interactionId: event.interaction_id,
    });
  }
  return actions;
}

function renderCustomerHistoryActions(event, context = {}) {
  const actions = customerHistoryEventActions(event, context);
  if (!actions.length) {
    return '';
  }
  return `
    <div class="customer-history-actions">
      ${actions.map((action) => `
        <button
          class="btn ghost customer-history-link"
          type="button"
          data-customer-action="${escapeHtml(action.action)}"
          data-thread-id="${escapeHtml(action.threadId || '')}"
          data-call-id="${escapeHtml(action.callId || '')}"
          data-interaction-id="${escapeHtml(action.interactionId || '')}"
        >${escapeHtml(action.label)}</button>
      `).join('')}
    </div>
  `;
}

function emptyCustomerSpotlightMarkup(title, description) {
  return `
    <div class="customer-spotlight-empty">
      <strong>${escapeHtml(title)}</strong>
      <p>${escapeHtml(description)}</p>
    </div>
  `;
}

function customerProfileVoiceNameContext(liveCalls = []) {
  const latestCall = latestItemByTime(liveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']);
  if (!latestCall) {
    return {
      statusMeta: null,
      sourceLabel: '',
    };
  }
  const voiceContext = voiceSummaryForItem(latestCall) || latestCall || {};
  return {
    statusMeta: voiceCustomerNameStatusMeta(voiceContext?.customer_name_status),
    sourceLabel: formatVoiceCustomerNameSource(voiceContext?.customer_name_source),
  };
}

function customerProfileNameEditorMarkup(customer, context = {}) {
  const editor = state.customers.nameEditor;
  const isOpen = editor.open && editor.customerId === customer.customer_id;
  const statusMeta = context.statusMeta || null;
  const sourceLabel = String(context.sourceLabel || '').trim();
  const flash = editor.flash && editor.customerId === customer.customer_id ? editor.flash : '';
  const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : 'Имя сохранится в профиле клиента и voice-контуре.');
  return `
    <div class="customer-profile-name-row">
      <div class="customer-profile-name-stack">
        <h3>${escapeHtml(customer.display_name || 'Без имени')}</h3>
        <p>${escapeHtml(customerLeadCompany(customer))}</p>
        ${(statusMeta || sourceLabel || flash) ? `
          <div class="customer-profile-name-meta">
            ${statusMeta ? `<span class="micro-badge ${escapeHtml(statusMeta.className)}">${escapeHtml(statusMeta.shortLabel)}</span>` : ''}
            ${sourceLabel ? `<span class="customer-profile-name-note">Источник: ${escapeHtml(sourceLabel)}</span>` : ''}
            ${flash ? `<span class="customer-profile-name-flash">${escapeHtml(flash)}</span>` : ''}
          </div>
        ` : ''}
      </div>
      <button class="btn ghost customer-profile-name-trigger" type="button" data-customer-action="edit-name">
        ${isOpen ? 'Редактирование имени' : 'Изменить имя'}
      </button>
    </div>
    ${isOpen ? `
      <form class="customer-profile-name-editor" data-customer-name-form>
        <div class="customer-profile-name-editor-label">Как сохранить имя клиента в профиле</div>
        <div class="customer-profile-name-editor-form">
          <input
            type="text"
            value="${escapeHtml(editor.draft)}"
            placeholder="Введите имя клиента"
            data-customer-name-input
            ${editor.saving ? 'disabled' : ''}
          />
          <button class="btn" type="submit" ${editor.saving ? 'disabled' : ''}>${editor.saving ? 'Сохраняем...' : 'Сохранить имя'}</button>
          <button class="btn ghost" type="button" data-customer-action="cancel-name-edit" ${editor.saving ? 'disabled' : ''}>Отмена</button>
        </div>
        <p class="hint${editor.error ? ' error' : ''}" data-customer-name-status>${escapeHtml(statusText)}</p>
      </form>
    ` : ''}
  `;
}

function customerSpotlightMarkup(customer, options = {}) {
  const { pageView = false } = options;
  const index = Math.max(customerIndex(customer), 0);
  const phones = customerPhones(customer);
  const source = customerLeadSourceMeta(customer, index);
  const leadStatus = customerLeadStatusMeta(customer, index);
  const score = customerLeadScore(customer, index);
  const historyPayload = customerHistoryPayload(customer.customer_id);
  const historySummary = historyPayload?.summary || null;
  const currentThreads = customerTelegramThreads(customer);
  const currentLiveCalls = customerLiveCalls(customer);
  const interactions = Array.isArray(historyPayload?.interactions)
    ? historyPayload.interactions
    : customerInteractions(customer);
  const threads = Array.isArray(historyPayload?.telegram_threads)
    ? historyPayload.telegram_threads
    : customerTelegramThreads(customer);
  const liveCalls = Array.isArray(historyPayload?.live_calls)
    ? historyPayload.live_calls
    : customerLiveCalls(customer);
  const profileNameContext = customerProfileVoiceNameContext(liveCalls);
  const historyEvents = Array.isArray(historyPayload?.history) && historyPayload.history.length
    ? historyPayload.history
    : buildCustomerHistoryEvents(customer);
  const latestEvent = historyEvents[0] || null;
  const openCases = Number.isFinite(Number(historySummary?.open_cases))
    ? Number(historySummary.open_cases)
    : interactions.filter((item) => item.status !== 'closed').length;
  const activeChannels = Array.isArray(historySummary?.active_channels) && historySummary.active_channels.length
    ? historySummary.active_channels
    : [...new Set([
      ...interactions.map((item) => item.channel).filter(Boolean),
      ...(threads.length ? ['telegram'] : []),
      ...(liveCalls.length ? ['voice'] : []),
    ])];
  const tags = Array.isArray(customer.tags) ? customer.tags.filter(Boolean) : [];
  const latestInteraction = latestItemByTime(interactions, ['updated_at', 'created_at']);
  const primaryThread = historySummary?.primary_telegram_thread_id
    ? currentThreads.find((item) => item.thread_id === historySummary.primary_telegram_thread_id) || null
    : latestItemByTime(currentThreads, ['last_message_at', 'updated_at', 'created_at']);
  const primaryCall = latestItemByTime(currentLiveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']);
  const historyActionContext = {
    threadIds: new Set(currentThreads.map((item) => item.thread_id).filter(Boolean)),
    callIds: new Set(currentLiveCalls.map((item) => item.call_id).filter(Boolean)),
  };
  const historyStatusText = customerHistoryPending(customer.customer_id)
    ? 'Обновляем ленту клиента из backend...'
    : state.customers.historyErrorsById[customer.customer_id]
      ? 'Показываем локальную историю, пока backend недоступен.'
      : (latestEvent ? `Последнее событие: ${latestEvent.title}` : 'События появятся после первого обращения');
  const summaryCards = [
    renderSummaryCard(
      'Контакты',
      String(Number.isFinite(Number(historySummary?.contact_points)) ? Number(historySummary.contact_points) : (interactions.length + threads.length + liveCalls.length)),
      'Все точки касания клиента',
    ),
    renderSummaryCard('Открытые кейсы', String(openCases), openCases ? 'Требуют внимания оператора' : 'Новых действий нет'),
    renderSummaryCard(
      'Каналы',
      String(activeChannels.length || (phones.length ? 1 : 0)),
      activeChannels.length ? activeChannels.map((item) => channelLabel(item)).join(' • ') : 'Пока только профиль',
    ),
    renderSummaryCard(
      'Последний контакт',
      historySummary?.latest_event_at ? formatIsoShort(historySummary.latest_event_at) : (latestEvent ? formatIsoShort(latestEvent.timestamp) : '—'),
      historySummary?.latest_event_title || latestEvent?.title || 'Активность ещё не зафиксирована',
    ),
  ].join('');

  const historyMarkup = historyEvents.length
    ? historyEvents.map((event) => {
      const meta = customerHistoryMeta(event.kind);
      return `
        <article class="customer-history-event ${meta.className}">
          <div class="customer-history-mark">${meta.token}</div>
          <div class="customer-history-body">
            <div class="customer-history-head">
              <div class="customer-history-title">${escapeHtml(event.title)}</div>
              <time class="customer-history-time">${escapeHtml(formatIsoShort(event.timestamp))}</time>
            </div>
            <div class="customer-history-copy">${escapeHtml(event.body)}</div>
            ${event.note ? `<div class="customer-history-note">${escapeHtml(event.note)}</div>` : ''}
            ${renderCustomerHistoryActions(event, historyActionContext)}
          </div>
        </article>
      `;
    }).join('')
    : '<div class="empty-state">Пока нет событий по этому клиенту.</div>';

  return `
    <div class="customer-spotlight-grid${pageView ? ' page' : ''}">
      <section class="customer-profile-card">
        <div class="customer-profile-hero">
          <div class="customer-profile-avatar">${escapeHtml(customerInitials(customer.display_name))}</div>
          <div class="customer-profile-copy">
            <div class="customer-profile-kicker">Клиент ${escapeHtml(customer.customer_id)}</div>
            ${customerProfileNameEditorMarkup(customer, profileNameContext)}
          </div>
          <div class="customer-profile-badges">
            <span class="${source.className}">${escapeHtml(source.label)}</span>
            <span class="${leadStatus.className}">${escapeHtml(leadStatus.label)}</span>
            <span class="customer-score-pill ${customerLeadScoreClass(score)}">AI ${score}</span>
          </div>
        </div>

        <div class="customer-profile-summary-grid">
          ${summaryCards}
        </div>

        <div class="customer-profile-meta-grid">
          <article class="customer-meta-card">
            <div class="customer-meta-label">Основной номер</div>
            <div class="customer-meta-value">${escapeHtml(historySummary?.primary_phone || phones[0] || 'Не указан')}</div>
            <div class="customer-meta-note">${phones.length > 1 ? `+${phones.length - 1} дополнительных номера` : 'Один контактный номер'}</div>
          </article>
          <article class="customer-meta-card">
            <div class="customer-meta-label">Предпочтительный канал</div>
            <div class="customer-meta-value">${escapeHtml(channelLabel(customerPreferredChannel(customer)))}</div>
            <div class="customer-meta-note">${escapeHtml(openCases ? 'Есть активные обращения' : 'Сейчас без открытых кейсов')}</div>
          </article>
          <article class="customer-meta-card">
            <div class="customer-meta-label">Теги профиля</div>
            <div class="customer-chip-row">
              ${(tags.length ? tags : [customerLeadCompany(customer)]).slice(0, 4).map((tag) => `<span class="customer-chip">${escapeHtml(tag)}</span>`).join('')}
            </div>
          </article>
          <article class="customer-meta-card">
            <div class="customer-meta-label">Активные каналы</div>
            <div class="customer-chip-row">
              ${(activeChannels.length ? activeChannels : ['profile']).map((channel) => `<span class="customer-channel-pill">${escapeHtml(channel === 'profile' ? 'Профиль' : channelLabel(channel))}</span>`).join('')}
            </div>
          </article>
        </div>

        <div class="customer-profile-actions">
          <button class="btn" type="button" data-customer-action="workspace">Открыть рабочий стол</button>
          <button class="btn ghost" type="button" data-customer-action="interaction" data-interaction-id="${escapeHtml(latestInteraction?.interaction_id || '')}" ${latestInteraction ? '' : 'disabled'}>Открыть кейс</button>
          <button class="btn ghost" type="button" data-customer-action="telegram" data-thread-id="${escapeHtml(primaryThread?.thread_id || '')}" ${primaryThread ? '' : 'disabled'}>Открыть Telegram</button>
          <button class="btn ghost" type="button" data-customer-action="call" data-call-id="${escapeHtml(primaryCall?.call_id || '')}" ${primaryCall ? '' : 'disabled'}>Открыть звонок</button>
        </div>
      </section>

      <section class="customer-history-card">
        <div class="customer-history-header">
          <div>
            <div class="customer-profile-kicker">Единая история клиента</div>
            <h3>Все контакты в одной ленте</h3>
          </div>
          <div class="customer-history-caption">${escapeHtml(historyStatusText)}</div>
        </div>
        <div class="customer-history-list">
          ${historyMarkup}
        </div>
      </section>
    </div>
  `;
}

function renderCustomerSpotlight() {
  const target = $('customerSpotlight');
  const customer = selectedCustomer();
  if (target && !customer) {
    target.innerHTML = emptyCustomerSpotlightMarkup(
      'Выберите клиента',
      'Здесь появятся профиль клиента и единая история контактов по всем каналам.',
    );
  }
  if (target && customer) {
    const listEl = target.querySelector('.customer-history-list');
    const scrollPos = listEl ? listEl.scrollTop : 0;
    const h = target.offsetHeight;
    if (h > 0) target.style.minHeight = h + 'px';

    target.innerHTML = customerSpotlightMarkup(customer);

    if (h > 0) target.style.minHeight = '';
    const newListEl = target.querySelector('.customer-history-list');
    if (newListEl && scrollPos > 0) {
      newListEl.scrollTop = scrollPos;
    }
  }
  renderCustomerProfilePage();
}

function renderCustomerProfilePage() {
  const target = $('customerProfileContent');
  if (!target) {
    return;
  }

  const customer = selectedCustomer();
  const title = $('customerProfileTitle');
  const hint = $('customerProfileHint');

  if (!customer) {
    if (title) {
      title.textContent = 'Профиль клиента';
    }
    if (hint) {
      hint.textContent = 'Откройте карточку из списка клиентов, чтобы увидеть профиль и историю.';
    }
    target.innerHTML = emptyCustomerSpotlightMarkup(
      'Выберите клиента',
      'Откройте карточку из списка клиентов, чтобы увидеть профиль и историю.',
    );
    return;
  }

  if (title) {
    title.textContent = customer.display_name || 'Профиль клиента';
  }
  const historyPayload = customerHistoryPayload(customer.customer_id);
  ensureCustomerHistoryLoaded(customer.customer_id).catch(() => {});
  if (hint) {
    const phones = customerPhones(customer);
    const primaryPhone = historyPayload?.summary?.primary_phone || phones[0] || '';
    hint.textContent = primaryPhone
      ? `Клиент ${customer.customer_id} • основной номер ${primaryPhone}`
      : `Клиент ${customer.customer_id} • омниканальный профиль`;
  }

  const listEl = target.querySelector('.customer-history-list');
  const scrollPos = listEl ? listEl.scrollTop : 0;
  const pageScroll = window.scrollY || document.documentElement.scrollTop;
  const h = target.offsetHeight;
  if (h > 0) target.style.minHeight = h + 'px';

  target.innerHTML = customerSpotlightMarkup(customer, { pageView: true });

  if (h > 0) target.style.minHeight = '';
  const newListEl = target.querySelector('.customer-history-list');
  if (newListEl && scrollPos > 0) {
    newListEl.scrollTop = scrollPos;
  }
  if (pageScroll > 0 && pageScroll > window.scrollY) {
    window.scrollTo(window.scrollX, pageScroll);
  }
}

function syncCustomerFormUi() {
  $('customerLeadForm').classList.toggle('hidden', !state.customers.leadFormOpen);
  $('toggleCustomerLeadFormBtn').textContent = state.customers.leadFormOpen ? 'Закрыть форму' : '＋ Добавить лид';
}

function updateCustomerPagerUi() {
  const total = state.customers.items.length;
  const pageCount = Math.max(1, Math.ceil(total / state.customers.pageSize));
  const start = total ? (state.customers.page - 1) * state.customers.pageSize + 1 : 0;
  const end = total ? Math.min(state.customers.page * state.customers.pageSize, total) : 0;
  $('customerPageSummary').textContent = total ? `Показано ${start}-${end} из ${total}` : 'Пока нет лидов.';
  $('customerPrevPageBtn').disabled = state.customers.page <= 1;
  $('customerNextPageBtn').disabled = state.customers.page >= pageCount;
}

function renderCustomerList() {
  const rows = customerLeadRows();
  if (!rows.length) {
    $('customerList').innerHTML = `
      <div class="lead-empty-state">
        <strong>Лиды пока не найдены.</strong>
        <p>Попробуйте другой запрос или добавьте новый лид вручную.</p>
      </div>
    `;
    renderCustomerSpotlight();
    updateCustomerPagerUi();
    return;
  }

  $('customerList').innerHTML = `
    <div class="lead-table">
      <div class="lead-table-head">
        <div class="lead-cell checkbox-cell"><input type="checkbox" aria-label="Выбрать все лиды" /></div>
        <div class="lead-cell contact-cell">ИМЯ КОНТАКТА</div>
        <div class="lead-cell">ИСТОЧНИК</div>
        <div class="lead-cell">ДАТА СОЗДАНИЯ</div>
        <div class="lead-cell">СТАТУС</div>
        <div class="lead-cell score-cell">AI SCORE</div>
      </div>
      ${rows.map((item, index) => {
        const source = customerLeadSourceMeta(item, index);
        const status = customerLeadStatusMeta(item, index);
        const score = customerLeadScore(item, index);
        const selected = item.customer_id === state.customers.selectedCustomerId ? ' selected' : '';
        return `
          <div class="lead-table-row${selected}" data-customer-id="${escapeHtml(item.customer_id)}" role="button" tabindex="0">
            <div class="lead-cell checkbox-cell"><input type="checkbox" tabindex="-1" aria-hidden="true" /></div>
            <div class="lead-cell contact-cell">
              <div class="lead-avatar">${escapeHtml(customerInitials(item.display_name))}</div>
              <div class="lead-contact-copy">
                <div class="lead-contact-name">${escapeHtml(item.display_name)}</div>
                <div class="lead-contact-company">${escapeHtml(customerLeadCompany(item))}</div>
              </div>
            </div>
            <div class="lead-cell"><span class="${source.className}">${escapeHtml(source.label)}</span></div>
            <div class="lead-cell">${escapeHtml(customerLeadCreatedAtLabel(item, index))}</div>
            <div class="lead-cell"><span class="${status.className}">${escapeHtml(status.label)}</span></div>
            <div class="lead-cell score-cell"><span class="${customerLeadScoreClass(score)}">${score}</span></div>
          </div>
        `;
      }).join('')}
    </div>
  `;
  renderCustomerSpotlight();
  updateCustomerPagerUi();
}

function selectCustomer(customerId) {
  if (state.customers.nameEditor.customerId && state.customers.nameEditor.customerId !== customerId) {
    state.customers.nameEditor.open = false;
    state.customers.nameEditor.customerId = '';
    state.customers.nameEditor.draft = '';
    state.customers.nameEditor.saving = false;
    state.customers.nameEditor.error = '';
    state.customers.nameEditor.flash = '';
  }
  state.customers.selectedCustomerId = customerId || '';
  if (customerId) {
    $('interactionCustomerId').value = customerId;
  }
  renderCustomerList();
}

function setCustomerPage(page) {
  const totalPages = Math.max(1, Math.ceil(state.customers.items.length / state.customers.pageSize));
  state.customers.page = Math.min(Math.max(page, 1), totalPages);
  renderCustomerList();
}

function selectedTelegramThread() {
  return state.telegram.threads.find((thread) => thread.thread_id === state.telegram.selectedThreadId) || null;
}

function telegramThreadCanClaim(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  return !thread.claimed_by_user;
}

function telegramThreadCanReply(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  if (state.role === 'admin' || state.role === 'supervisor') {
    return true;
  }
  return thread.claimed_by_user === state.user;
}

function telegramThreadCanManage(thread) {
  if (!thread) {
    return false;
  }
  if (state.role === 'admin' || state.role === 'supervisor') {
    return true;
  }
  return thread.claimed_by_user === state.user;
}

function telegramThreadCanReturnToAi(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  if (String(thread.ai_state || '') !== 'human_owned') {
    return false;
  }
  return telegramThreadCanManage(thread);
}

function telegramThreadDisplayName(thread) {
  return thread?.display_name || thread?.username || thread?.chat_id || 'Telegram';
}

function telegramThreadInitials(thread) {
  return customerInitials(telegramThreadDisplayName(thread));
}

function telegramThreadTimeLabel(value) {
  if (!value) {
    return 'сейчас';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return String(value);
  }
  const now = new Date();
  const sameDay = dt.toDateString() === now.toDateString();
  if (sameDay) {
    return dt.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
  }
  return dt.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' });
}

function telegramHashValue(value) {
  let hash = 0;
  const text = String(value || 'telegram');
  for (let index = 0; index < text.length; index += 1) {
    hash = ((hash << 5) - hash + text.charCodeAt(index)) >>> 0;
  }
  return hash;
}

function telegramAvatarPalette(seed) {
  return TELEGRAM_AVATAR_PALETTES[telegramHashValue(seed) % TELEGRAM_AVATAR_PALETTES.length];
}

function telegramAvatarStyle(seed) {
  const [start, end] = telegramAvatarPalette(seed);
  return `style="--telegram-avatar-start:${start};--telegram-avatar-end:${end};"`;
}

function whatsappAvatarPalette(seed) {
  return WHATSAPP_AVATAR_PALETTES[telegramHashValue(seed || 'whatsapp') % WHATSAPP_AVATAR_PALETTES.length];
}

function cloneWhatsappChats() {
  return WHATSAPP_MOCK_CHATS.map((chat) => ({
    ...chat,
    messages: (chat.messages || []).map((message) => ({ ...message })),
  }));
}

function sortWhatsappChats(chats) {
  return [...(chats || [])].sort((left, right) => {
    const pinnedDelta = Number(Boolean(right.pinned)) - Number(Boolean(left.pinned));
    if (pinnedDelta !== 0) {
      return pinnedDelta;
    }
    return String(right.lastMessageAt || '').localeCompare(String(left.lastMessageAt || ''));
  });
}

function seedWhatsappChats() {
  if (state.whatsapp.chats.length) {
    return;
  }
  state.whatsapp.mode = 'mock';
  state.whatsapp.chats = sortWhatsappChats(cloneWhatsappChats());
  state.whatsapp.selectedChatId = state.whatsapp.chats[0]?.id || '';
}

function whatsappChatThreadId(chat) {
  return chat?.threadId || chat?.id || '';
}

function whatsappThreadCanClaim(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  return !thread.claimedByUser;
}

function whatsappThreadCanReply(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  if (state.role === 'admin' || state.role === 'supervisor') {
    return true;
  }
  return !thread.claimedByUser || thread.claimedByUser === state.user;
}

function whatsappThreadCanManage(thread) {
  if (!thread) {
    return false;
  }
  if (state.role === 'admin' || state.role === 'supervisor') {
    return true;
  }
  return thread.claimedByUser === state.user;
}

function whatsappThreadCanReturnToAi(thread) {
  if (!thread || thread.status === 'closed') {
    return false;
  }
  if (String(thread.aiState || '').trim() !== 'human_owned') {
    return false;
  }
  return whatsappThreadCanManage(thread);
}

function whatsappThreadDisplayName(thread) {
  return thread?.title || thread?.display_name || thread?.phoneNumber || thread?.phone_number || thread?.chatId || thread?.chat_id || 'WhatsApp';
}

function whatsappThreadAiStateMeta(thread) {
  const stateKey = String(thread?.aiState || thread?.ai_state || '').trim();
  if (!stateKey) {
    return null;
  }
  const meta = {
    queued: { label: 'AI готов', tone: 'queued' },
    thinking: { label: 'AI думает', tone: 'thinking' },
    active: { label: 'AI активен', tone: 'active' },
    handoff_required: { label: 'Нужен оператор', tone: 'handoff' },
    human_owned: { label: 'Operator', tone: 'human' },
    closed: { label: 'AI завершён', tone: 'closed' },
    error: { label: 'Ошибка AI', tone: 'error' },
  };
  return meta[stateKey] || { label: stateKey, tone: 'queued' };
}

function whatsappThreadAiClaimable(thread) {
  return Boolean(thread && !thread.claimedByUser && ['queued', 'thinking', 'active', 'handoff_required'].includes(String(thread.aiState || '')));
}

function whatsappThreadUnreadCount(thread) {
  const raw = Number(thread?.unreadCount ?? thread?.unread_count ?? 0);
  return Number.isFinite(raw) && raw > 0 ? raw : 0;
}

function whatsappThreadHandle(thread) {
  if (!thread) {
    return 'Direct message';
  }
  if (thread.handle) {
    return thread.handle;
  }
  if (thread.phoneNumber || thread.phone_number) {
    return thread.phoneNumber || thread.phone_number;
  }
  if (thread.whatsappUserId || thread.whatsapp_user_id) {
    return thread.whatsappUserId || thread.whatsapp_user_id;
  }
  if (thread.isGroup || thread.is_group) {
    return 'Групповой чат';
  }
  const chatId = thread.chatId || thread.chat_id;
  return chatId ? `chat ${chatId}` : 'Direct message';
}

function whatsappThreadPresenceText(thread) {
  if (!thread) {
    return 'Выберите чат';
  }
  if (thread.status === 'closed') {
    return 'Закрыт';
  }
  const aiMeta = whatsappThreadAiStateMeta(thread);
  if (aiMeta && String(thread.aiState || '').trim() !== 'human_owned') {
    return aiMeta.label;
  }
  if (thread.claimedByUser) {
    return `в работе у ${thread.claimedByUser}`;
  }
  return thread.statusLine || whatsappThreadHandle(thread);
}

function whatsappThreadOwnerText(thread) {
  if (whatsappThreadAiClaimable(thread)) {
    return 'владелец AI';
  }
  return thread?.claimedByUser ? `владелец ${thread.claimedByUser}` : 'владелец -';
}

function whatsappThreadSummaryAccessible(thread) {
  if (!thread) {
    return false;
  }
  if (!['handoff_required', 'human_owned'].includes(String(thread.aiState || '').trim())) {
    return false;
  }
  if (String(state.role || '') !== 'operator') {
    return true;
  }
  const claimedByUser = String(thread.claimedByUser || '').trim();
  return !claimedByUser || claimedByUser === String(state.user || '').trim();
}

function whatsappSelectedSummaryVisible(thread) {
  if (!whatsappThreadSummaryAccessible(thread)) {
    return false;
  }
  const summary = state.whatsapp.selectedThreadSummary;
  return Boolean(summary && summary.thread_id === whatsappChatThreadId(thread));
}

function whatsappSummaryFieldMarkup(label, value) {
  return `
    <article class="whatsapp-summary-field">
      <div class="whatsapp-summary-label">${escapeHtml(label)}</div>
      <div class="whatsapp-summary-value">${escapeHtml(value || '-')}</div>
    </article>
  `;
}

function whatsappSummaryStatusMarkup(summary) {
  if (!summary?.status_label) {
    return '';
  }
  const tone = String(summary.status_tone || 'handoff').trim() || 'handoff';
  return `<span class="whatsapp-summary-status ${escapeHtml(tone)}">${escapeHtml(summary.status_label)}</span>`;
}

function normalizeWhatsappDeliveryStatus(status) {
  const raw = String(status || '').trim().toLowerCase();
  if (!raw) {
    return '';
  }
  if (raw.includes('read')) {
    return 'read';
  }
  if (raw.includes('deliver') || raw.includes('sent')) {
    return 'delivered';
  }
  return raw;
}

function whatsappAuthorLabel(message, chat = null) {
  if (message.author) {
    return message.author;
  }
  const payload = message.payload || {};
  if (message.author_type === 'customer') {
    return payload.display_name || payload.profile_name || chat?.title || '';
  }
  if (message.author_type === 'human') {
    return message.operator_user || payload.operator_user || 'Оператор';
  }
  if (message.author_type === 'ai') {
    return 'AI';
  }
  return '';
}

function normalizeWhatsappMessage(message, chat = null) {
  const direction = message.direction === 'outbound'
    ? 'out'
    : message.direction === 'inbound'
      ? 'in'
      : (message.direction || 'system');
  return {
    id: message.message_id || message.id,
    direction,
    text: message.text || '',
    createdAt: message.created_at || message.createdAt || '',
    deliveryStatus: normalizeWhatsappDeliveryStatus(message.delivery_status || message.deliveryStatus),
    author: whatsappAuthorLabel(message, chat),
    authorType: message.author_type || message.authorType || '',
    operatorUser: message.operator_user || '',
    payload: message.payload || {},
  };
}

function whatsappThreadStatusLine(thread) {
  const aiMeta = whatsappThreadAiStateMeta(thread);
  if (aiMeta && String(thread.ai_state || '').trim() !== 'human_owned') {
    return aiMeta.label;
  }
  if (thread.claimed_by_user) {
    return `owned by ${thread.claimed_by_user}`;
  }
  return thread.phone_number || thread.whatsapp_user_id || (thread.is_group ? 'Групповой чат' : 'Чат WhatsApp');
}

function normalizeWhatsappThread(thread, existingChat = null) {
  return {
    id: thread.thread_id,
    threadId: thread.thread_id,
    interactionId: thread.interaction_id,
    chatId: thread.chat_id,
    whatsappUserId: thread.whatsapp_user_id || '',
    phoneNumber: thread.phone_number || '',
    title: thread.display_name || thread.phone_number || thread.chat_id || 'WhatsApp',
    handle: thread.phone_number || thread.whatsapp_user_id || (thread.is_group ? 'Групповой чат' : `чат ${thread.chat_id}`),
    statusLine: whatsappThreadStatusLine(thread),
    isGroup: Boolean(thread.is_group),
    unreadCount: whatsappThreadUnreadCount(thread),
    pinned: Boolean(existingChat?.pinned),
    muted: Boolean(existingChat?.muted),
    lastMessageAt: thread.last_message_at || '',
    lastMessagePreview: thread.last_message_preview || '',
    claimedByUser: thread.claimed_by_user || '',
    claimedAt: thread.claimed_at || '',
    status: thread.status || '',
    queueId: thread.queue_id || '',
    aiSessionId: thread.ai_session_id || '',
    aiState: thread.ai_state || '',
    aiHandoffReason: thread.ai_handoff_reason || '',
    aiLastModelAt: thread.ai_last_model_at || '',
    createdAt: thread.created_at || '',
    updatedAt: thread.updated_at || '',
    source: 'live',
    messages: Array.isArray(existingChat?.messages) ? existingChat.messages.map((message) => ({ ...message })) : [],
  };
}

function applyWhatsappChats(chats, options = {}) {
  const { preserveSelection = true } = options;
  const sorted = sortWhatsappChats(chats);
  const previous = preserveSelection ? state.whatsapp.selectedChatId : '';
  const stillExists = sorted.some((chat) => chat.id === previous);
  const nextSelected = stillExists ? previous : (sorted[0]?.id || '');
  state.whatsapp.chats = sorted;
  state.whatsapp.selectedChatId = nextSelected;
  if (nextSelected !== previous) {
    state.whatsapp.composerText = '';
    state.whatsapp.selectedThreadSummary = null;
  }
  if (!nextSelected) {
    state.whatsapp.selectedThreadSummary = null;
  }
  renderWhatsappWorkspace();
  return nextSelected;
}

function upsertWhatsappThreadData(thread, options = {}) {
  const existing = state.whatsapp.chats.find((item) => item.id === thread.thread_id) || null;
  const normalized = normalizeWhatsappThread(thread, existing);
  const nextChats = existing
    ? state.whatsapp.chats.map((item) => (item.id === normalized.id ? normalized : item))
    : [...state.whatsapp.chats, normalized];
  applyWhatsappChats(nextChats, options);
  return normalized;
}

function whatsappEstimateUnreadCount(messages) {
  let lastOutboundIndex = -1;
  (messages || []).forEach((message, index) => {
    if (message.direction === 'out') {
      lastOutboundIndex = index;
    }
  });
  return (messages || [])
    .slice(lastOutboundIndex + 1)
    .filter((message) => message.direction === 'in')
    .length;
}

function updateWhatsappChatMessages(threadId, messages, options = {}) {
  const { preserveUnread = false } = options;
  const nextChats = state.whatsapp.chats.map((chat) => {
    if (chat.id !== threadId) {
      return chat;
    }
    return {
      ...chat,
      messages,
      unreadCount: preserveUnread ? chat.unreadCount : (threadId === state.whatsapp.selectedChatId ? 0 : whatsappEstimateUnreadCount(messages)),
    };
  });
  applyWhatsappChats(nextChats);
}

function renderWhatsappSummaryCard() {
  const card = $('whatsappAiSummaryCard');
  const content = $('whatsappAiSummaryContent');
  const meta = $('whatsappAiSummaryMeta');
  const status = $('whatsappAiSummaryStatus');
  if (!card || !content) {
    return;
  }
  const thread = selectedWhatsappChat();
  if (!whatsappSelectedSummaryVisible(thread)) {
    card.classList.add('hidden');
    content.innerHTML = '';
    if (meta) {
      meta.textContent = '';
    }
    if (status) {
      status.innerHTML = '';
    }
    return;
  }
  const summary = state.whatsapp.selectedThreadSummary;
  const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(whatsappChatTimeLabel(summary.generated_at))}` : '';
  content.innerHTML = [
    whatsappSummaryFieldMarkup('Запрос клиента', summary.customer_request_text),
    whatsappSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text),
    whatsappSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason),
    whatsappSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step),
  ].join('');
  if (meta) {
    meta.textContent = generatedAt;
  }
  if (status) {
    status.innerHTML = whatsappSummaryStatusMarkup(summary);
  }
  card.classList.remove('hidden');
}

function telegramThreadSummary(thread) {
  return thread.username ? `@${thread.username}` : `chat ${thread.chat_id}`;
}

function telegramThreadStatusText(thread) {
  return statusMeta(thread?.status || '').label;
}

function telegramThreadAiStateMeta(thread) {
  const stateKey = String(thread?.ai_state || '').trim();
  if (!stateKey) {
    return null;
  }
  const meta = {
    queued: { label: 'AI готов', tone: 'queued' },
    thinking: { label: 'AI думает', tone: 'thinking' },
    active: { label: 'AI активен', tone: 'active' },
    handoff_required: { label: 'Ждёт человека', tone: 'handoff' },
    human_owned: { label: 'Человек', tone: 'human' },
    closed: { label: 'AI завершён', tone: 'closed' },
    error: { label: 'Ошибка AI', tone: 'error' },
  };
  return meta[stateKey] || { label: stateKey, tone: 'queued' };
}

function telegramThreadAiClaimable(thread) {
  return Boolean(thread && !thread.claimed_by_user && ['queued', 'thinking', 'active', 'handoff_required'].includes(String(thread.ai_state || '')));
}

function telegramClaimButtonLabel(thread) {
  return telegramThreadAiClaimable(thread) ? 'Забрать у AI' : 'В работу';
}

function telegramThreadUnreadCount(thread) {
  const raw = Number(thread?.unread_count ?? 0);
  return Number.isFinite(raw) && raw > 0 ? raw : 0;
}

function telegramThreadPresenceText(thread) {
  if (!thread) {
    return 'нет диалога';
  }
  const aiMeta = telegramThreadAiStateMeta(thread);
  if (aiMeta) {
    return aiMeta.label;
  }
  return thread.claimed_by_user ? 'в работе' : 'свободен';
}

function telegramThreadOwnerText(thread) {
  if (telegramThreadAiClaimable(thread)) {
    return 'владелец AI';
  }
  return thread?.claimed_by_user ? `владелец ${thread.claimed_by_user}` : 'владелец —';
}

function telegramThreadHeaderPresence(thread) {
  if (!thread) {
    return 'Выберите диалог слева';
  }
  if (thread.status === 'closed') {
    return 'был(а) недавно';
  }
  const aiMeta = telegramThreadAiStateMeta(thread);
  if (aiMeta && thread.ai_state !== 'human_owned') {
    return aiMeta.label;
  }
  return thread.claimed_by_user ? 'в работе' : 'в сети';
}

function telegramThreadNeedsSummary(thread) {
  if (!thread) {
    return false;
  }
  return ['handoff_required', 'human_owned'].includes(String(thread.ai_state || ''));
}

function telegramThreadSummaryAccessible(thread) {
  if (!telegramThreadNeedsSummary(thread)) {
    return false;
  }
  if (String(state.role || '') !== 'operator') {
    return true;
  }
  const claimedByUser = String(thread?.claimed_by_user || '').trim();
  return !claimedByUser || claimedByUser === String(state.user || '').trim();
}

function telegramSelectedSummaryVisible(thread) {
  if (!telegramThreadSummaryAccessible(thread)) {
    return false;
  }
  const summary = state.telegram.selectedThreadSummary;
  return Boolean(summary && summary.thread_id === thread?.thread_id);
}

function telegramSummaryFieldMarkup(label, value) {
  return `
    <article class="telegram-summary-field">
      <div class="telegram-summary-label">${escapeHtml(label)}</div>
      <div class="telegram-summary-value">${escapeHtml(value || '—')}</div>
    </article>
  `;
}

function telegramSummaryStatusMarkup(summary) {
  if (!summary?.status_label) {
    return '';
  }
  const tone = String(summary.status_tone || 'handoff').trim() || 'handoff';
  return `<span class="telegram-summary-status ${escapeHtml(tone)}">${escapeHtml(summary.status_label)}</span>`;
}

function renderTelegramSummaryCard() {
  const card = $('telegramAiSummaryCard');
  const content = $('telegramAiSummaryContent');
  const meta = $('telegramAiSummaryMeta');
  const status = $('telegramAiSummaryStatus');
  if (!card || !content) {
    return;
  }
  const thread = selectedTelegramThread();
  if (!telegramSelectedSummaryVisible(thread)) {
    card.classList.add('hidden');
    content.innerHTML = '';
    if (meta) {
      meta.textContent = '';
    }
    if (status) {
      status.innerHTML = '';
    }
    return;
  }
  const summary = state.telegram.selectedThreadSummary;
  const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(telegramThreadTimeLabel(summary.generated_at))}` : '';
  content.innerHTML = [
    telegramSummaryFieldMarkup('Запрос клиента', summary.customer_request_text),
    telegramSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text),
    telegramSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason),
    telegramSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step),
  ].join('');
  if (meta) {
    meta.textContent = generatedAt;
  }
  if (status) {
    status.innerHTML = telegramSummaryStatusMarkup(summary);
  }
  card.classList.remove('hidden');
}

function telegramThreadCountLabel(count) {
  const mod10 = count % 10;
  const mod100 = count % 100;
  if (mod10 === 1 && mod100 !== 11) {
    return `${count} диалог`;
  }
  if ([2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100)) {
    return `${count} диалога`;
  }
  return `${count} диалогов`;
}

function telegramVisibleThreads() {
  const query = String(state.telegram.searchQuery || '').trim().toLowerCase();
  return state.telegram.threads.filter((thread) => {
    if (!query) {
      return true;
    }
    return [
      telegramThreadDisplayName(thread),
      thread.username ? `@${thread.username}` : '',
      thread.chat_id,
      thread.last_message_preview,
    ].some((value) => String(value || '').toLowerCase().includes(query));
  });
}

function resetTelegramMessageSearch() {
  state.telegram.messageSearchQuery = '';
  state.telegram.messageSearchOpen = false;
}

function telegramMessageSearchQuery() {
  return String(state.telegram.messageSearchQuery || '').trim();
}

function telegramMessageMatchesSearch(message) {
  const query = telegramMessageSearchQuery().toLowerCase();
  if (!query) {
    return true;
  }
  return String(message?.text || '').toLowerCase().includes(query);
}

function telegramVisibleMessages() {
  return state.telegram.messages.filter((message) => telegramMessageMatchesSearch(message));
}

function telegramMessageSearchMeta(thread) {
  if (!thread) {
    return 'Выберите чат для поиска';
  }
  const query = telegramMessageSearchQuery();
  if (!query) {
    return 'Поиск по текущему чату';
  }
  const matches = telegramVisibleMessages().length;
  if (matches === 0) {
    return 'Ничего не найдено';
  }
  if (matches === 1) {
    return '1 совпадение';
  }
  return `${matches} совпадений`;
}

function highlightTelegramMessageText(value) {
  const source = String(value || 'Сообщение без текста.');
  const query = telegramMessageSearchQuery();
  if (!query) {
    return escapeHtml(source);
  }
  const matcher = new RegExp(`(${escapeRegExp(query)})`, 'gi');
  return source
    .split(matcher)
    .map((part) => (part.toLowerCase() === query.toLowerCase()
      ? `<mark class="telegram-message-highlight">${escapeHtml(part)}</mark>`
      : escapeHtml(part)))
    .join('');
}

function toggleTelegramMessageSearch(force = null) {
  const hasThread = Boolean(selectedTelegramThread());
  const nextOpen = force === null ? !state.telegram.messageSearchOpen : Boolean(force);
  state.telegram.messageSearchOpen = hasThread && nextOpen;
  if (!state.telegram.messageSearchOpen) {
    state.telegram.messageSearchQuery = '';
  }
  renderTelegramWorkspace();
  if (state.telegram.messageSearchOpen) {
    window.requestAnimationFrame(() => {
      $('telegramMessageSearch')?.focus();
      $('telegramMessageSearch')?.select();
    });
  }
}

function telegramMessageDayKey(value) {
  if (!value) {
    return '';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return '';
  }
  return `${dt.getFullYear()}-${dt.getMonth()}-${dt.getDate()}`;
}

function telegramMessageDayLabel(value) {
  if (!value) {
    return 'Недавно';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return String(value);
  }
  const now = new Date();
  const sameDay = dt.toDateString() === now.toDateString();
  if (sameDay) {
    return 'Сегодня';
  }
  const yesterday = new Date(now);
  yesterday.setDate(now.getDate() - 1);
  if (dt.toDateString() === yesterday.toDateString()) {
    return 'Вчера';
  }
  return dt.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' });
}

function telegramMessageStatusMarkup(message) {
  if (message.direction !== 'outbound' || !message.delivery_status) {
    return '';
  }
  const status = String(message.delivery_status || '').toLowerCase();
  const checks = status.includes('deliver') || status.includes('read') || status.includes('sent') ? '✓✓' : '✓';
  return `<span class="telegram-message-checks" title="${escapeHtml(message.delivery_status)}">${checks}</span>`;
}

function syncTelegramComposerHeight() {
  const input = $('telegramReplyText');
  if (!input) {
    return;
  }
  input.style.height = '0px';
  const nextHeight = Math.min(Math.max(input.scrollHeight, 54), 164);
  input.style.height = `${nextHeight}px`;
}

function renderTelegramThreadsList() {
  const target = $('telegramThreadsList');
  const threads = telegramVisibleThreads();
  if (!threads.length) {
    target.innerHTML = '<div class="empty-state telegram-empty-state">Пока нет Telegram диалогов.</div>';
    return;
  }
  target.innerHTML = threads
    .map((thread) => {
      const selected = thread.thread_id === state.telegram.selectedThreadId ? ' selected' : '';
      const preview = thread.last_message_preview || 'Нет сообщений.';
      const unread = telegramThreadUnreadCount(thread);
      const aiMeta = telegramThreadAiStateMeta(thread);
      return `
        <button class="telegram-thread-card${selected}" data-telegram-thread-id="${escapeHtml(thread.thread_id)}" type="button">
          <div class="telegram-thread-avatar${selected ? ' selected' : ''}" ${telegramAvatarStyle(telegramThreadDisplayName(thread))}>${escapeHtml(telegramThreadInitials(thread))}</div>
          <div class="telegram-thread-body">
            <div class="telegram-thread-card-head">
              <div class="telegram-thread-name">${escapeHtml(telegramThreadDisplayName(thread))}</div>
              <div class="telegram-thread-time">${escapeHtml(telegramThreadTimeLabel(thread.last_message_at))}</div>
            </div>
            <div class="telegram-thread-preview-row">
              <div class="telegram-thread-preview">${escapeHtml(preview)}</div>
              ${unread > 0 && !selected ? `<span class="telegram-thread-unread">${unread}</span>` : ''}
            </div>
            <div class="telegram-thread-status-row">
              <div class="telegram-thread-summary">${escapeHtml(telegramThreadSummary(thread))}</div>
              ${aiMeta ? `<span class="telegram-thread-ai-chip ${aiMeta.tone}">${escapeHtml(aiMeta.label)}</span>` : ''}
            </div>
          </div>
        </button>
      `;
    })
    .join('');
}

function renderTelegramMessagesTimeline() {
  const target = $('telegramMessagesTimeline');
  const thread = selectedTelegramThread();
  if (!thread) {
    target.innerHTML = '<div class="empty-state">Выберите диалог слева, чтобы увидеть историю сообщений.</div>';
    return;
  }
  if (!state.telegram.messages.length) {
    target.innerHTML = '<div class="empty-state">Сообщений пока нет.</div>';
    return;
  }
  const messages = telegramVisibleMessages();
  if (!messages.length) {
    target.innerHTML = '<div class="empty-state telegram-empty-state">Ничего не найдено в этом чате.</div>';
    return;
  }
  const fragments = messages.map((message) => {
    const direction = message.direction || 'system';
    if (direction === 'system') {
      return `
        <article class="telegram-message system">
          <div class="telegram-message-system-pill">${highlightTelegramMessageText(message.text || 'Системное сообщение')}</div>
        </article>
      `;
    }
    const mine = direction === 'outbound';
    const aiBadge = message.author_type === 'ai'
      ? '<span class="telegram-message-badge ai">AI</span>'
      : '';
    return `
      <article class="telegram-message ${mine ? 'me' : 'other'}">
        <div class="telegram-message-bubble ${mine ? 'me' : 'other'}${message.author_type === 'ai' ? ' ai' : ''}">
          ${aiBadge}
          <div class="telegram-message-body">${highlightTelegramMessageText(message.text || 'Сообщение без текста.')}</div>
          <div class="telegram-message-meta">
            <span class="telegram-message-time">${escapeHtml(telegramThreadTimeLabel(message.created_at))}</span>
            ${mine ? telegramMessageStatusMarkup(message) : ''}
          </div>
        </div>
      </article>
    `;
  });
  target.innerHTML = fragments.join('');
  target.scrollTop = telegramMessageSearchQuery() ? 0 : target.scrollHeight;
}

function syncTelegramActionButtons() {
  const thread = selectedTelegramThread();
  const pending = state.telegram.pendingAction;
  const replyText = $('telegramReplyText').value.trim();
  $('telegramClaimThreadBtn').textContent = telegramClaimButtonLabel(thread);
  $('telegramClaimThreadBtn').disabled = pending !== '' || !telegramThreadCanClaim(thread);
  $('telegramReturnToAiBtn').disabled = pending !== '' || !telegramThreadCanReturnToAi(thread);
  $('telegramCloseThreadBtn').disabled = pending !== '' || !telegramThreadCanManage(thread) || thread?.status === 'closed';
  $('telegramEscalateThreadBtn').disabled = pending !== '' || !telegramThreadCanManage(thread);
  $('telegramSendReplyBtn').disabled = pending !== '' || !telegramThreadCanReply(thread) || !replyText;
  syncTelegramComposerHeight();
}

function syncTelegramOperatorTray() {
  $('telegramOperatorTray').classList.toggle('hidden', !state.telegram.operatorTrayOpen);
}

function toggleTelegramOperatorTray(force = null) {
  state.telegram.operatorTrayOpen = force === null ? !state.telegram.operatorTrayOpen : Boolean(force);
  syncTelegramOperatorTray();
}

function renderTelegramWorkspace() {
  renderTelegramThreadsList();
  renderTelegramSummaryCard();
  renderTelegramMessagesTimeline();
  const thread = selectedTelegramThread();
  const avatar = $('telegramThreadAvatar');
  const presence = $('telegramThreadPresenceChip');
  const owner = $('telegramThreadOwnerChip');
  const messageSearchButton = $('telegramHeaderSearchBtn');
  const messageSearchBar = $('telegramMessageSearchBar');
  const messageSearchInput = $('telegramMessageSearch');
  const messageSearchMeta = $('telegramMessageSearchMeta');
  const messageSearchClearBtn = $('telegramMessageSearchClearBtn');
  const aiChip = $('telegramThreadAiChip');
  const aiMeta = telegramThreadAiStateMeta(thread);
  $('telegramThreadDisplayName').textContent = thread
    ? telegramThreadDisplayName(thread)
    : 'Выберите диалог';
  $('telegramThreadMeta').textContent = thread
    ? telegramThreadHeaderPresence(thread)
    : 'Выберите диалог слева.';
  if (avatar) {
    avatar.textContent = thread ? telegramThreadInitials(thread) : 'TG';
    const [start, end] = telegramAvatarPalette(thread ? telegramThreadDisplayName(thread) : 'Telegram');
    avatar.style.setProperty('--telegram-avatar-start', start);
    avatar.style.setProperty('--telegram-avatar-end', end);
  }
  if (presence) {
    presence.textContent = thread ? telegramThreadPresenceText(thread) : 'нет диалога';
    const presenceTone = aiMeta
      ? aiMeta.tone
      : (thread?.claimed_by_user ? 'claimed' : 'free');
    presence.className = `telegram-operator-chip ${presenceTone}`;
  }
  if (owner) {
    owner.textContent = thread ? telegramThreadOwnerText(thread) : 'владелец —';
    owner.className = 'telegram-operator-chip subtle';
  }
  if (aiChip) {
    if (aiMeta) {
      aiChip.textContent = aiMeta.label;
      aiChip.className = `telegram-thread-ai-chip ${aiMeta.tone}`;
    } else {
      aiChip.textContent = '';
      aiChip.className = 'telegram-thread-ai-chip hidden';
    }
  }
  if (messageSearchButton) {
    messageSearchButton.disabled = !thread;
    messageSearchButton.classList.toggle('active', Boolean(thread) && state.telegram.messageSearchOpen);
  }
  if (messageSearchBar) {
    messageSearchBar.classList.toggle('hidden', !thread || !state.telegram.messageSearchOpen);
  }
  if (messageSearchInput) {
    messageSearchInput.value = state.telegram.messageSearchQuery || '';
    messageSearchInput.disabled = !thread;
  }
  if (messageSearchMeta) {
    messageSearchMeta.textContent = telegramMessageSearchMeta(thread);
  }
  if (messageSearchClearBtn) {
    messageSearchClearBtn.textContent = telegramMessageSearchQuery() ? 'Очистить' : 'Закрыть';
  }
  $('telegramComposerHint').textContent = thread
    ? telegramThreadCanReply(thread)
      ? 'Reply уйдёт через Telegram Bot API и сохранится в thread history.'
      : telegramThreadCanReturnToAi(thread)
        ? 'Диалог сейчас у человека. Нажмите «Вернуть AI», чтобы снова включить автоответ.'
      : telegramThreadAiClaimable(thread)
        ? 'Диалог сейчас у AI. Заберите его в работу, чтобы ответить вручную.'
        : 'Reply доступен после claim или для admin/supervisor.'
    : 'Выберите thread, чтобы отвечать в Telegram.';
  syncTelegramOperatorTray();
  syncTelegramComposerHeight();
  syncTelegramActionButtons();
}

function applyTelegramCollections(threads, options = {}) {
  const { preserveSelection = true } = options;
  const sorted = [...(threads || [])].sort((left, right) => {
    return String(right.last_message_at || '').localeCompare(String(left.last_message_at || ''));
  });
  state.telegram.threads = sorted;
  const previous = preserveSelection ? state.telegram.selectedThreadId : '';
  const stillExists = sorted.some((thread) => thread.thread_id === previous);
  state.telegram.selectedThreadId = stillExists ? previous : (sorted[0]?.thread_id || '');
  if (state.telegram.selectedThreadId !== previous) {
    resetTelegramMessageSearch();
    state.telegram.selectedThreadSummary = null;
  }
  if (!state.telegram.selectedThreadId) {
    state.telegram.selectedThreadSummary = null;
  }
  syncMessengerConversations({ preserveSelection: true });
  renderTelegramWorkspace();
  renderMessengerWorkspace();
  renderCustomerSpotlight();
  renderUnifiedInbox();
}

async function loadTelegramThreadMessages(threadId = state.telegram.selectedThreadId, logResult = false) {
  if (!threadId) {
    state.telegram.messages = [];
    state.telegram.selectedThreadSummary = null;
    resetTelegramMessageSearch();
    renderTelegramWorkspace();
    return [];
  }
  const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/messages`);
  if (threadId === state.telegram.selectedThreadId) {
    state.telegram.messages = Array.isArray(data) ? data : [];
    renderTelegramWorkspace();
    renderCustomerSpotlight();
  }
  if (logResult) {
    log('Сообщения Telegram-диалога обновлены', { thread_id: threadId, messages: Array.isArray(data) ? data.length : 0 });
  }
  return Array.isArray(data) ? data : [];
}

async function loadTelegramThreadSummary(threadId = state.telegram.selectedThreadId, options = {}) {
  const { logResult = false } = options;
  if (!threadId) {
    state.telegram.selectedThreadSummary = null;
    renderTelegramWorkspace();
    return null;
  }
  const thread = state.telegram.threads.find((item) => item.thread_id === threadId) || null;
  if (!telegramThreadSummaryAccessible(thread)) {
    if (threadId === state.telegram.selectedThreadId) {
      state.telegram.selectedThreadSummary = null;
      renderTelegramWorkspace();
    }
    return null;
  }
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/ai-summary`);
    if (threadId === state.telegram.selectedThreadId) {
      state.telegram.selectedThreadSummary = data || null;
      renderTelegramWorkspace();
    }
    if (logResult && data) {
      log('AI-сводка загружена', { thread_id: threadId, session_id: data.session_id });
    }
    return data || null;
  } catch (err) {
    if (threadId === state.telegram.selectedThreadId) {
      state.telegram.selectedThreadSummary = null;
      renderTelegramWorkspace();
    }
    if (logResult) {
      log('Не удалось загрузить AI-сводку', { thread_id: threadId, error: err.message });
    }
    return null;
  }
}

async function loadTelegramThreads(logResult = true, options = {}) {
  const { preserveSelection = true, preserveOnError = false } = options;
  try {
    const data = await api('telegram', 'integrations/telegram/threads');
    applyTelegramCollections(Array.isArray(data) ? data : [], { preserveSelection });
    await loadTelegramThreadMessages(state.telegram.selectedThreadId, false);
    await loadTelegramThreadSummary(state.telegram.selectedThreadId, { logResult: false });
    if (logResult) {
      log('Telegram-диалоги обновлены', { threads: state.telegram.threads.length });
    }
  } catch (err) {
    if (!preserveOnError) {
      state.telegram.threads = [];
      state.telegram.selectedThreadId = '';
      state.telegram.messages = [];
      state.telegram.selectedThreadSummary = null;
      renderTelegramWorkspace();
    }
    if (logResult) {
      log('Не удалось загрузить Telegram-диалоги', { error: err.message });
    }
  }
}

async function selectTelegramThread(threadId) {
  if ((threadId || '') !== state.telegram.selectedThreadId) {
    resetTelegramMessageSearch();
  }
  state.telegram.selectedThreadId = threadId || '';
  state.telegram.selectedThreadSummary = null;
  state.telegram.operatorTrayOpen = false;
  renderTelegramWorkspace();
  await loadTelegramThreadMessages(threadId, false);
  await loadTelegramThreadSummary(threadId, { logResult: false });
}

async function claimTelegramThread() {
  const thread = selectedTelegramThread();
  if (!thread) {
    return;
  }
  state.telegram.pendingAction = 'claim';
  renderTelegramWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/claim`, {
      method: 'POST',
    });
    const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item));
    applyTelegramCollections(nextThreads);
    await loadTelegramThreadSummary(data.thread_id, { logResult: false });
    refreshLiveCallsInBackground();
    loadInteractions().catch(() => {});
    log('Telegram-диалог взят в работу', { thread_id: data.thread_id, interaction_id: data.interaction_id });
  } catch (err) {
    log('Не удалось взять Telegram-диалог в работу', { error: err.message });
  } finally {
    state.telegram.pendingAction = '';
    renderTelegramWorkspace();
  }
}

async function returnTelegramThreadToAi() {
  const thread = selectedTelegramThread();
  if (!thread) {
    return;
  }
  state.telegram.pendingAction = 'return-ai';
  renderTelegramWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/return-to-ai`, {
      method: 'POST',
    });
    const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item));
    applyTelegramCollections(nextThreads);
    await loadTelegramThreadSummary(data.thread_id, { logResult: false });
    loadInteractions().catch(() => {});
    log('Telegram-диалог возвращён AI', { thread_id: data.thread_id, interaction_id: data.interaction_id });
  } catch (err) {
    log('Не удалось вернуть Telegram-диалог AI', { error: err.message });
  } finally {
    state.telegram.pendingAction = '';
    renderTelegramWorkspace();
  }
}

async function sendTelegramReply() {
  const thread = selectedTelegramThread();
  const text = $('telegramReplyText').value.trim();
  if (!thread || !text) {
    return;
  }
  state.telegram.pendingAction = 'reply';
  renderTelegramWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/messages`, {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    $('telegramReplyText').value = '';
    state.telegram.messages = [...state.telegram.messages, data];
    const nextThreads = state.telegram.threads.map((item) => {
      if (item.thread_id !== thread.thread_id) {
        return item;
      }
      return {
        ...item,
        last_message_at: data.created_at,
        last_message_preview: data.text,
      };
    });
    applyTelegramCollections(nextThreads);
    renderTelegramWorkspace();
    log('Ответ в Telegram поставлен в очередь', { thread_id: thread.thread_id, delivery_status: data.delivery_status || 'pending' });
    window.setTimeout(() => loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }), 0);
  } catch (err) {
    log('Не удалось отправить ответ в Telegram', { error: err.message });
  } finally {
    state.telegram.pendingAction = '';
    renderTelegramWorkspace();
  }
}

async function closeTelegramThread() {
  const thread = selectedTelegramThread();
  if (!thread) {
    return;
  }
  state.telegram.pendingAction = 'close';
  renderTelegramWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/close`, {
      method: 'POST',
    });
    const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item));
    applyTelegramCollections(nextThreads);
    await loadTelegramThreadSummary(data.thread_id, { logResult: false });
    loadInteractions().catch(() => {});
    log('Telegram-диалог закрыт', { thread_id: data.thread_id });
  } catch (err) {
    log('Не удалось закрыть Telegram-диалог', { error: err.message });
  } finally {
    state.telegram.pendingAction = '';
    renderTelegramWorkspace();
  }
}

async function escalateTelegramThread() {
  const thread = selectedTelegramThread();
  const targetQueueId = $('telegramEscalationQueue').value.trim() || DEMO_QUEUE;
  if (!thread) {
    return;
  }
  state.telegram.pendingAction = 'escalate';
  renderTelegramWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/escalate`, {
      method: 'POST',
      body: JSON.stringify({ target_queue_id: targetQueueId }),
    });
    const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item));
    applyTelegramCollections(nextThreads);
    await loadTelegramThreadSummary(data.thread_id, { logResult: false });
    loadInteractions().catch(() => {});
    log('Telegram-диалог эскалирован', { thread_id: data.thread_id, queue_id: data.queue_id });
  } catch (err) {
    log('Не удалось эскалировать Telegram-диалог', { error: err.message });
  } finally {
    state.telegram.pendingAction = '';
    renderTelegramWorkspace();
  }
}

function startTelegramPolling() {
  if (state.telegram.pollTimer) {
    window.clearInterval(state.telegram.pollTimer);
    state.telegram.pollTimer = null;
  }
  state.telegram.pollTimer = window.setInterval(() => {
    if (operatorHashState().view === 'messages') {
      return;
    }
    loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true });
  }, 5000);
}

function messengerConversationId(channel, threadId) {
  return `${channel}:${threadId || ''}`;
}

function messengerConversationFromTelegramThread(thread) {
  const customerId = thread?.customer_id || customerIdForInteractionId(thread?.interaction_id) || '';
  const unread = telegramThreadUnreadCount(thread);
  return {
    conversation_id: messengerConversationId('telegram', thread?.thread_id),
    channel: 'telegram',
    channel_label: 'Telegram',
    thread_id: thread?.thread_id || '',
    customer_id: customerId,
    interaction_id: thread?.interaction_id || '',
    display_name: telegramThreadDisplayName(thread),
    last_message_preview: thread?.last_message_preview || '',
    last_message_at: thread?.last_message_at || thread?.updated_at || thread?.created_at || '',
    status: thread?.status || '',
    claimed_by_user: thread?.claimed_by_user || '',
    ai_state: thread?.ai_state || '',
    unread,
    attention: unread > 0 || ['handoff_required', 'handoff_requested'].includes(String(thread?.ai_state || '')),
    source_thread: thread,
  };
}

function buildMessengerConversations() {
  return state.telegram.threads
    .map((thread) => messengerConversationFromTelegramThread(thread))
    .sort((left, right) => {
      return String(right.last_message_at || '').localeCompare(String(left.last_message_at || ''));
    });
}

function syncMessengerConversations(options = {}) {
  const { preserveSelection = true } = options;
  const previous = preserveSelection ? state.messenger.selectedConversationId : '';
  const conversations = buildMessengerConversations();
  const stillExists = conversations.some((item) => item.conversation_id === previous);
  state.messenger.conversations = conversations;
  if (!stillExists) {
    state.messenger.selectedConversationId = '';
    state.messenger.messages = [];
    state.messenger.selectedSummary = null;
  }
}

function selectedMessengerConversation() {
  return state.messenger.conversations.find((item) => item.conversation_id === state.messenger.selectedConversationId) || null;
}

function selectedMessengerThread() {
  const conversation = selectedMessengerConversation();
  if (!conversation || conversation.channel !== 'telegram') {
    return null;
  }
  return state.telegram.threads.find((thread) => thread.thread_id === conversation.thread_id) || conversation.source_thread || null;
}

function messengerConversationMatchesFilter(conversation) {
  const filter = state.messenger.activeFilter || 'all';
  if (filter === 'new') {
    return conversation.status === 'new' || conversation.unread > 0 || !conversation.claimed_by_user;
  }
  if (filter === 'mine') {
    return String(conversation.claimed_by_user || '') === String(state.user || '');
  }
  if (filter === 'ai') {
    return ['handoff_required', 'handoff_requested', 'human_owned'].includes(String(conversation.ai_state || ''));
  }
  return true;
}

function messengerVisibleConversations() {
  return state.messenger.conversations.filter((conversation) => messengerConversationMatchesFilter(conversation));
}

function messengerConversationCanClaim(conversation) {
  return Boolean(conversation && conversation.status !== 'closed' && !conversation.claimed_by_user);
}

function messengerConversationCanReply(conversation) {
  return Boolean(conversation && conversation.status !== 'closed' && String(conversation.claimed_by_user || '') === String(state.user || ''));
}

function messengerConversationCanManage(conversation) {
  if (!conversation || conversation.status === 'closed') {
    return false;
  }
  if (state.role === 'admin' || state.role === 'supervisor') {
    return true;
  }
  return String(conversation.claimed_by_user || '') === String(state.user || '');
}

function messengerAiStateMeta(conversation) {
  return telegramThreadAiStateMeta(conversation?.source_thread || selectedMessengerThread());
}

function messengerContextCustomerId(conversation = selectedMessengerConversation()) {
  return conversation?.customer_id || customerIdForInteractionId(conversation?.interaction_id) || '';
}

function messengerCustomerProfileAvailable(conversation = selectedMessengerConversation()) {
  const customerId = messengerContextCustomerId(conversation);
  return Boolean(customerId && state.customers.items.some((item) => item.customer_id === customerId));
}

function messengerSummaryVisible(conversation) {
  if (!conversation || conversation.channel !== 'telegram') {
    return false;
  }
  const summary = state.messenger.selectedSummary;
  return Boolean(summary && summary.thread_id === conversation.thread_id);
}

function renderMessengerConversations() {
  const target = $('messengerConversationList');
  if (!target) {
    return;
  }
  if (state.messenger.backendError && !state.messenger.conversations.length) {
    target.innerHTML = `
      <div class="empty-state messenger-empty-state">
        <strong>Backend недоступен</strong>
        <span>${escapeHtml(state.messenger.backendError)}</span>
      </div>
    `;
    return;
  }
  const conversations = messengerVisibleConversations();
  if (!state.messenger.conversations.length) {
    target.innerHTML = '<div class="empty-state messenger-empty-state">Новых сообщений пока нет</div>';
    return;
  }
  if (!conversations.length) {
    target.innerHTML = `<div class="empty-state messenger-empty-state">Нет диалогов в фильтре «${escapeHtml(MESSENGER_FILTERS[state.messenger.activeFilter] || 'Все')}»</div>`;
    return;
  }
  target.innerHTML = conversations
    .map((conversation) => {
      const selected = conversation.conversation_id === state.messenger.selectedConversationId ? ' selected' : '';
      const preview = conversation.last_message_preview || 'Нет сообщений.';
      const aiMeta = messengerAiStateMeta(conversation);
      return `
        <button class="messenger-conversation-card${selected}" data-messenger-conversation-id="${escapeHtml(conversation.conversation_id)}" type="button">
          <div class="telegram-thread-avatar${selected ? ' selected' : ''}" ${telegramAvatarStyle(conversation.display_name)}>${escapeHtml(customerInitials(conversation.display_name))}</div>
          <div class="messenger-conversation-body">
            <div class="messenger-conversation-head">
              <span class="messenger-conversation-name">${escapeHtml(conversation.display_name)}</span>
              <span class="messenger-conversation-time">${escapeHtml(telegramThreadTimeLabel(conversation.last_message_at))}</span>
            </div>
            <div class="messenger-conversation-preview-row">
              <span class="messenger-conversation-preview">${escapeHtml(preview)}</span>
              ${conversation.unread > 0 && !selected ? `<span class="telegram-thread-unread">${conversation.unread}</span>` : ''}
            </div>
            <div class="messenger-conversation-badges">
              <span class="messenger-channel-badge compact">Telegram</span>
              <span class="messenger-status-badge">${escapeHtml(statusMeta(conversation.status).label)}</span>
              ${conversation.claimed_by_user ? `<span class="messenger-status-badge owner">${escapeHtml(conversation.claimed_by_user)}</span>` : ''}
              ${aiMeta ? `<span class="telegram-thread-ai-chip ${aiMeta.tone}">${escapeHtml(aiMeta.label)}</span>` : ''}
            </div>
          </div>
        </button>
      `;
    })
    .join('');
}

function renderMessengerMessages() {
  const target = $('messengerMessagesTimeline');
  const conversation = selectedMessengerConversation();
  if (!target) {
    return;
  }
  if (!conversation) {
    target.innerHTML = '<div class="empty-state">Выберите диалог слева</div>';
    return;
  }
  if (!state.messenger.messages.length) {
    target.innerHTML = '<div class="empty-state">Сообщений пока нет.</div>';
    return;
  }
  target.innerHTML = state.messenger.messages
    .map((message) => {
      const direction = message.direction || 'system';
      if (direction === 'system') {
        return `
          <article class="telegram-message system">
            <div class="telegram-message-system-pill">${escapeHtml(message.text || 'Системное сообщение')}</div>
          </article>
        `;
      }
      const mine = direction === 'outbound';
      const aiBadge = message.author_type === 'ai'
        ? '<span class="telegram-message-badge ai">AI</span>'
        : '';
      return `
        <article class="telegram-message ${mine ? 'me' : 'other'}">
          <div class="telegram-message-bubble ${mine ? 'me' : 'other'}${message.author_type === 'ai' ? ' ai' : ''}">
            ${aiBadge}
            <div class="telegram-message-body">${escapeHtml(message.text || 'Сообщение без текста.')}</div>
            <div class="telegram-message-meta">
              <span class="telegram-message-time">${escapeHtml(telegramThreadTimeLabel(message.created_at))}</span>
              ${mine ? telegramMessageStatusMarkup(message) : ''}
            </div>
          </div>
        </article>
      `;
    })
    .join('');
  target.scrollTop = target.scrollHeight;
}

function renderMessengerSummaryCard() {
  const card = $('messengerAiSummaryCard');
  const content = $('messengerAiSummaryContent');
  const meta = $('messengerAiSummaryMeta');
  const status = $('messengerAiSummaryStatus');
  const conversation = selectedMessengerConversation();
  if (!card || !content) {
    return;
  }
  if (!messengerSummaryVisible(conversation)) {
    card.classList.add('hidden');
    content.innerHTML = '';
    if (meta) {
      meta.textContent = '';
    }
    if (status) {
      status.innerHTML = '';
    }
    return;
  }
  const summary = state.messenger.selectedSummary;
  const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(telegramThreadTimeLabel(summary.generated_at))}` : '';
  content.innerHTML = [
    telegramSummaryFieldMarkup('Запрос клиента', summary.customer_request_text),
    telegramSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text),
    telegramSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason),
    telegramSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step),
  ].join('');
  if (meta) {
    meta.textContent = generatedAt;
  }
  if (status) {
    status.innerHTML = telegramSummaryStatusMarkup(summary);
  }
  card.classList.remove('hidden');
}

function renderMessengerContext() {
  const conversation = selectedMessengerConversation();
  const customerId = messengerContextCustomerId(conversation);
  const interactionId = conversation?.interaction_id || '';
  const aiMeta = messengerAiStateMeta(conversation);
  const profileAvailable = messengerCustomerProfileAvailable(conversation);
  const preview = conversation?.last_message_preview || '';
  const summary = messengerSummaryVisible(conversation) ? state.messenger.selectedSummary : null;
  const summaryText = summary
    ? [summary.customer_request_text, summary.recommended_next_step].filter(Boolean).join(' · ')
    : '';
  if ($('messengerContextName')) {
    $('messengerContextName').textContent = conversation ? conversation.display_name : 'Диалог не выбран';
  }
  if ($('messengerContextChannel')) {
    $('messengerContextChannel').textContent = conversation?.channel_label || 'Telegram';
  }
  if ($('messengerContextStatus')) {
    $('messengerContextStatus').textContent = conversation
      ? [statusMeta(conversation.status).label, aiMeta?.label].filter(Boolean).join(' · ')
      : '—';
  }
  if ($('messengerContextCustomer')) {
    $('messengerContextCustomer').textContent = customerId ? customerDisplayName(customerId) : '—';
  }
  if ($('messengerContextInteraction')) {
    $('messengerContextInteraction').textContent = interactionId || '—';
  }
  if ($('messengerContextPreview')) {
    $('messengerContextPreview').textContent = preview || (conversation ? 'Нет сообщений.' : 'Выберите диалог слева.');
  }
  if ($('messengerContextAiSummary')) {
    $('messengerContextAiSummary').textContent = summaryText || (conversation ? 'AI summary пока недоступен.' : 'Сводка появится после AI handoff.');
  }
  ['messengerOpenProfileBtn', 'messengerContextOpenProfileBtn'].forEach((id) => {
    const button = $(id);
    if (button) {
      button.disabled = !profileAvailable;
    }
  });
}

function syncMessengerActionButtons() {
  const conversation = selectedMessengerConversation();
  const pending = state.messenger.pendingAction;
  const replyText = String($('messengerReplyText')?.value || '').trim();
  const canClaim = messengerConversationCanClaim(conversation);
  const canReply = messengerConversationCanReply(conversation);
  const canManage = messengerConversationCanManage(conversation);
  const claimButton = $('messengerClaimBtn');
  const closeButton = $('messengerCloseBtn');
  const sendButton = $('messengerSendReplyBtn');
  const composer = $('messengerReplyText');
  if (claimButton) {
    claimButton.disabled = pending !== '' || !canClaim;
    claimButton.textContent = telegramThreadAiClaimable(selectedMessengerThread()) ? 'Забрать у AI' : 'Взять в работу';
  }
  if (closeButton) {
    closeButton.disabled = pending !== '' || !canManage;
  }
  if (sendButton) {
    sendButton.disabled = pending !== '' || !canReply || !replyText;
  }
  if (composer) {
    composer.disabled = pending !== '' || !canReply;
  }
  if ($('messengerComposerHint')) {
    $('messengerComposerHint').textContent = conversation
      ? canReply
        ? 'Ответ уйдёт через Telegram endpoint выбранного диалога.'
        : canClaim
          ? 'Сначала нажмите «Взять в работу», после этого будет доступен ответ.'
          : 'Ответ недоступен: диалог закрыт или закреплён за другим оператором.'
      : 'Выберите диалог слева.';
  }
}

function syncMessengerComposerHeight() {
  const input = $('messengerReplyText');
  if (!input) {
    return;
  }
  input.style.height = '0px';
  const nextHeight = Math.min(Math.max(input.scrollHeight, 54), 148);
  input.style.height = `${nextHeight}px`;
}

function renderMessengerWorkspace() {
  if (!$('messengerWorkspace')) {
    return;
  }
  const conversation = selectedMessengerConversation();
  const aiMeta = messengerAiStateMeta(conversation);
  document.querySelectorAll('[data-messenger-filter]').forEach((button) => {
    button.classList.toggle('active', button.dataset.messengerFilter === state.messenger.activeFilter);
  });
  renderMessengerConversations();
  renderMessengerSummaryCard();
  renderMessengerMessages();
  renderMessengerContext();
  if ($('messengerConversationAvatar')) {
    $('messengerConversationAvatar').textContent = conversation ? customerInitials(conversation.display_name) : 'TG';
    const [start, end] = telegramAvatarPalette(conversation ? conversation.display_name : 'Telegram');
    $('messengerConversationAvatar').style.setProperty('--telegram-avatar-start', start);
    $('messengerConversationAvatar').style.setProperty('--telegram-avatar-end', end);
  }
  if ($('messengerConversationTitle')) {
    $('messengerConversationTitle').textContent = conversation ? conversation.display_name : 'Выберите диалог';
  }
  if ($('messengerConversationMeta')) {
    $('messengerConversationMeta').textContent = conversation
      ? [conversation.channel_label, statusMeta(conversation.status).label, aiMeta?.label, conversation.claimed_by_user ? `оператор ${conversation.claimed_by_user}` : 'не закреплён']
        .filter(Boolean)
        .join(' · ')
      : 'Выберите диалог слева.';
  }
  if ($('messengerChannelBadge')) {
    $('messengerChannelBadge').textContent = conversation?.channel_label || 'Telegram';
  }
  if ($('messengerBackendError')) {
    $('messengerBackendError').classList.toggle('hidden', !state.messenger.backendError);
    const errorText = $('messengerBackendError').querySelector('span');
    if (errorText) {
      errorText.textContent = state.messenger.backendError || 'Backend сообщений недоступен.';
    }
  }
  syncMessengerComposerHeight();
  syncMessengerActionButtons();
}

function setMessengerFilter(filter) {
  state.messenger.activeFilter = MESSENGER_FILTERS[filter] ? filter : 'all';
  renderMessengerWorkspace();
}

async function loadMessengerMessages(threadId) {
  if (!threadId) {
    state.messenger.messages = [];
    renderMessengerWorkspace();
    return [];
  }
  const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/messages`);
  const messages = Array.isArray(data) ? data : [];
  const conversation = selectedMessengerConversation();
  if (conversation?.thread_id === threadId) {
    state.messenger.messages = messages;
    if (state.telegram.selectedThreadId === threadId) {
      state.telegram.messages = messages;
    }
    renderMessengerWorkspace();
  }
  return messages;
}

async function loadMessengerSummary(threadId) {
  if (!threadId) {
    state.messenger.selectedSummary = null;
    renderMessengerWorkspace();
    return null;
  }
  const thread = state.telegram.threads.find((item) => item.thread_id === threadId) || null;
  if (!telegramThreadSummaryAccessible(thread)) {
    state.messenger.selectedSummary = null;
    renderMessengerWorkspace();
    return null;
  }
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/ai-summary`);
    const conversation = selectedMessengerConversation();
    if (conversation?.thread_id === threadId) {
      state.messenger.selectedSummary = data || null;
      renderMessengerWorkspace();
    }
    return data || null;
  } catch {
    const conversation = selectedMessengerConversation();
    if (conversation?.thread_id === threadId) {
      state.messenger.selectedSummary = null;
      renderMessengerWorkspace();
    }
    return null;
  }
}

async function loadMessengerThreads(logResult = false, options = {}) {
  const { preserveSelection = true, preserveOnError = false } = options;
  try {
    const data = await api('telegram', 'integrations/telegram/threads');
    applyTelegramCollections(Array.isArray(data) ? data : [], { preserveSelection: true });
    syncMessengerConversations({ preserveSelection });
    state.messenger.backendError = '';
    renderMessengerWorkspace();
    const conversation = selectedMessengerConversation();
    if (conversation?.thread_id) {
      await Promise.all([
        loadMessengerMessages(conversation.thread_id),
        loadMessengerSummary(conversation.thread_id),
      ]);
    }
    if (logResult) {
      log('Сообщения обновлены', { conversations: state.messenger.conversations.length });
    }
  } catch (err) {
    state.messenger.backendError = err.message || 'Backend сообщений недоступен.';
    if (!preserveOnError) {
      state.messenger.conversations = [];
      state.messenger.selectedConversationId = '';
      state.messenger.messages = [];
      state.messenger.selectedSummary = null;
    }
    renderMessengerWorkspace();
    if (logResult) {
      log('Не удалось загрузить сообщения', { error: err.message });
    }
    throw err;
  }
}

async function selectMessengerConversation(conversationId) {
  state.messenger.selectedConversationId = conversationId || '';
  state.messenger.messages = [];
  state.messenger.selectedSummary = null;
  state.messenger.backendError = '';
  const conversation = selectedMessengerConversation();
  if (conversation?.channel === 'telegram') {
    state.telegram.selectedThreadId = conversation.thread_id;
  }
  const customerId = messengerContextCustomerId(conversation);
  if (customerId && state.customers.items.some((item) => item.customer_id === customerId)) {
    selectCustomer(customerId);
    ensureCustomerHistoryLoaded(customerId, { force: false }).catch(() => {});
  }
  renderMessengerWorkspace();
  if (conversation?.thread_id) {
    await Promise.all([
      loadMessengerMessages(conversation.thread_id),
      loadMessengerSummary(conversation.thread_id),
    ]);
  }
}

function upsertMessengerTelegramThread(thread, options = {}) {
  const exists = state.telegram.threads.some((item) => item.thread_id === thread.thread_id);
  const nextThreads = exists
    ? state.telegram.threads.map((item) => (item.thread_id === thread.thread_id ? thread : item))
    : [thread, ...state.telegram.threads];
  applyTelegramCollections(nextThreads, { preserveSelection: true });
  syncMessengerConversations({ preserveSelection: true, ...options });
}

async function claimMessengerConversation() {
  const conversation = selectedMessengerConversation();
  if (!conversation?.thread_id || !messengerConversationCanClaim(conversation)) {
    return;
  }
  state.messenger.pendingAction = 'claim';
  renderMessengerWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/claim`, {
      method: 'POST',
    });
    upsertMessengerTelegramThread(data);
    state.messenger.selectedConversationId = messengerConversationId('telegram', data.thread_id);
    await loadMessengerSummary(data.thread_id);
    loadInteractions().catch(() => {});
    log('Диалог взят в работу из Сообщений', { thread_id: data.thread_id, interaction_id: data.interaction_id });
  } catch (err) {
    log('Не удалось взять диалог в работу', { error: err.message });
  } finally {
    state.messenger.pendingAction = '';
    renderMessengerWorkspace();
  }
}

async function sendMessengerReply() {
  const conversation = selectedMessengerConversation();
  const text = String($('messengerReplyText')?.value || '').trim();
  if (!conversation?.thread_id || !text || !messengerConversationCanReply(conversation)) {
    syncMessengerActionButtons();
    return;
  }
  state.messenger.pendingAction = 'reply';
  renderMessengerWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/messages`, {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    $('messengerReplyText').value = '';
    state.messenger.composerText = '';
    state.messenger.messages = [...state.messenger.messages, data];
    if (state.telegram.selectedThreadId === conversation.thread_id) {
      state.telegram.messages = [...state.telegram.messages, data];
    }
    const nextThreads = state.telegram.threads.map((item) => {
      if (item.thread_id !== conversation.thread_id) {
        return item;
      }
      return {
        ...item,
        last_message_at: data.created_at,
        last_message_preview: data.text,
      };
    });
    applyTelegramCollections(nextThreads, { preserveSelection: true });
    renderMessengerWorkspace();
    log('Ответ из Сообщений отправлен через Telegram', { thread_id: conversation.thread_id, delivery_status: data.delivery_status || 'pending' });
    window.setTimeout(() => loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch(() => {}), 0);
  } catch (err) {
    log('Не удалось отправить ответ из Сообщений', { error: err.message });
  } finally {
    state.messenger.pendingAction = '';
    renderMessengerWorkspace();
  }
}

async function closeMessengerConversation() {
  const conversation = selectedMessengerConversation();
  if (!conversation?.thread_id || !messengerConversationCanManage(conversation)) {
    return;
  }
  state.messenger.pendingAction = 'close';
  renderMessengerWorkspace();
  try {
    const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/close`, {
      method: 'POST',
    });
    upsertMessengerTelegramThread(data);
    await loadMessengerSummary(data.thread_id);
    loadInteractions().catch(() => {});
    log('Диалог закрыт из Сообщений', { thread_id: data.thread_id });
  } catch (err) {
    log('Не удалось закрыть диалог из Сообщений', { error: err.message });
  } finally {
    state.messenger.pendingAction = '';
    renderMessengerWorkspace();
  }
}

function openMessengerCustomerProfile() {
  const customerId = messengerContextCustomerId();
  if (!customerId || !messengerCustomerProfileAvailable()) {
    return;
  }
  openCustomerProfile(customerId);
}

function stopMessengerPolling() {
  if (state.messenger.pollTimer) {
    window.clearInterval(state.messenger.pollTimer);
    state.messenger.pollTimer = null;
  }
}

function startMessengerPolling() {
  stopMessengerPolling();
  state.messenger.pollTimer = window.setInterval(() => {
    if (operatorHashState().view !== 'messages') {
      return;
    }
    loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch(() => {});
  }, 5000);
}

function selectedWhatsappChat() {
  return state.whatsapp.chats.find((chat) => chat.id === state.whatsapp.selectedChatId) || null;
}

function ensureWhatsappSelection() {
  if (!state.whatsapp.chats.length) {
    state.whatsapp.selectedChatId = '';
    return;
  }
  const exists = state.whatsapp.chats.some((chat) => chat.id === state.whatsapp.selectedChatId);
  if (!exists) {
    state.whatsapp.selectedChatId = state.whatsapp.chats[0].id;
  }
}

function whatsappChatInitials(chat) {
  const parts = String(chat?.title || 'WA')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2);
  if (!parts.length) {
    return 'WA';
  }
  return parts.map((part) => part[0].toUpperCase()).join('');
}

function whatsappChatTimeLabel(value) {
  if (!value) {
    return '';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return '';
  }
  const now = new Date();
  const sameDay = dt.toDateString() === now.toDateString();
  if (sameDay) {
    return dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  }
  return dt.toLocaleDateString([], { day: '2-digit', month: 'short' });
}

function whatsappMessageTimeLabel(value) {
  if (!value) {
    return '';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return '';
  }
  return dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

function whatsappMessageDayKey(value) {
  if (!value) {
    return '';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return '';
  }
  return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
}

function whatsappMessageDayLabel(value) {
  if (!value) {
    return '';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return '';
  }
  const today = new Date();
  const yesterday = new Date();
  yesterday.setDate(today.getDate() - 1);
  if (dt.toDateString() === today.toDateString()) {
    return 'Today';
  }
  if (dt.toDateString() === yesterday.toDateString()) {
    return 'Yesterday';
  }
  return dt.toLocaleDateString([], { day: 'numeric', month: 'long' });
}

function whatsappVisibleChats() {
  const query = String(state.whatsapp.searchQuery || '').trim().toLowerCase();
  return state.whatsapp.chats.filter((chat) => {
    if (state.whatsapp.activeFilter === 'unread' && !(whatsappThreadUnreadCount(chat) > 0)) {
      return false;
    }
    if (state.whatsapp.activeFilter === 'groups' && !chat.isGroup) {
      return false;
    }
    if (!query) {
      return true;
    }
    return [
      chat.title,
      chat.handle,
      chat.statusLine,
      chat.lastMessagePreview,
      ...(chat.messages || []).map((message) => message.text || ''),
    ]
      .join(' ')
      .toLowerCase()
      .includes(query);
  });
}

function whatsappMessageStatusMarkup(message) {
  if (message.direction !== 'out') {
    return '';
  }
  if (message.deliveryStatus === 'read') {
    return '<span class="whatsapp-message-checks read" aria-hidden="true">&#10003;&#10003;</span>';
  }
  if (message.deliveryStatus === 'delivered') {
    return '<span class="whatsapp-message-checks delivered" aria-hidden="true">&#10003;&#10003;</span>';
  }
  return '<span class="whatsapp-message-checks" aria-hidden="true">&#10003;</span>';
}

function whatsappComposerBlockedReason(chat) {
  if (!chat) {
    return 'Выберите чат';
  }
  if (chat.status === 'closed') {
    return 'Thread is closed';
  }
  if (state.role === 'operator' && chat.claimedByUser && chat.claimedByUser !== state.user) {
    return `Claimed by ${chat.claimedByUser}`;
  }
  return '';
}

function syncWhatsappComposerHeight() {
  const input = $('whatsappComposerInput');
  if (!input) {
    return;
  }
  input.style.height = 'auto';
  input.style.height = `${Math.min(Math.max(input.scrollHeight, 24), 120)}px`;
}

function syncWhatsappComposerUi() {
  const input = $('whatsappComposerInput');
  const sendBtn = $('whatsappSendBtn');
  if (!input || !sendBtn) {
    return;
  }
  sendBtn.disabled = Boolean(whatsappComposerBlockedReason(selectedWhatsappChat()))
    || !String(state.whatsapp.composerText || '').trim()
    || Boolean(state.whatsapp.pendingAction);
  syncWhatsappComposerHeight();
}

function renderWhatsappChatList() {
  const target = $('whatsappChatList');
  if (!target) {
    return;
  }
  const chats = whatsappVisibleChats();
  if (!chats.length) {
    target.innerHTML = `
      <div class="whatsapp-chat-list-empty">
        <strong>Чаты не найдены</strong>
        <p>Попробуйте другой запрос или переключите активный фильтр.</p>
      </div>
    `;
    return;
  }
  target.innerHTML = chats.map((chat) => {
    const [start, end] = whatsappAvatarPalette(chat.title);
    const selected = chat.id === state.whatsapp.selectedChatId;
    const aiMeta = whatsappThreadAiStateMeta(chat);
    return `
      <button
        class="whatsapp-chat-card${selected ? ' selected' : ''}"
        type="button"
        data-whatsapp-chat-id="${escapeHtml(chat.id)}"
        style="--whatsapp-avatar-start:${start};--whatsapp-avatar-end:${end};"
      >
        <div class="whatsapp-chat-card-avatar">${escapeHtml(whatsappChatInitials(chat))}</div>
        <div class="whatsapp-chat-card-copy">
          <div class="whatsapp-chat-card-head">
            <span class="whatsapp-chat-card-title">${escapeHtml(chat.title)}</span>
            <span class="whatsapp-chat-card-time">${escapeHtml(whatsappChatTimeLabel(chat.lastMessageAt))}</span>
          </div>
          <div class="whatsapp-chat-card-preview-row">
            <span class="whatsapp-chat-card-preview">${escapeHtml(chat.lastMessagePreview || '')}</span>
            ${whatsappThreadUnreadCount(chat) > 0 ? `<span class="whatsapp-chat-unread">${whatsappThreadUnreadCount(chat)}</span>` : ''}
          </div>
          <div class="whatsapp-chat-card-meta">
            <span>${escapeHtml(whatsappThreadHandle(chat))}</span>
            ${aiMeta ? `<span class="whatsapp-chat-tag ai ${aiMeta.tone}">${escapeHtml(aiMeta.label)}</span>` : ''}
            ${chat.claimedByUser ? `<span class="whatsapp-chat-tag owner">${escapeHtml(chat.claimedByUser === state.user ? 'Мой чат' : `Владелец ${chat.claimedByUser}`)}</span>` : ''}
            ${chat.muted ? '<span class="whatsapp-chat-tag">Без звука</span>' : ''}
            ${chat.isGroup ? '<span class="whatsapp-chat-tag">Группа</span>' : ''}
          </div>
        </div>
      </button>
    `;
  }).join('');
}

function renderWhatsappContextPanel() {
  const panel = $('whatsappContextPanel');
  const aiChip = $('whatsappAiStateChip');
  const ownerChip = $('whatsappOwnerChip');
  const claimBtn = $('whatsappClaimBtn');
  const returnBtn = $('whatsappReturnToAiBtn');
  const chat = selectedWhatsappChat();
  if (!panel || !aiChip || !ownerChip || !claimBtn || !returnBtn) {
    return;
  }
  const aiMeta = whatsappThreadAiStateMeta(chat);
  const ownerText = whatsappThreadOwnerText(chat);
  const showPanel = Boolean(chat && (aiMeta || chat.claimedByUser || whatsappSelectedSummaryVisible(chat)));
  panel.classList.toggle('hidden', !showPanel);

  if (aiMeta) {
    aiChip.textContent = aiMeta.label;
    aiChip.className = `whatsapp-state-chip ${aiMeta.tone}`;
  } else {
    aiChip.textContent = '';
    aiChip.className = 'whatsapp-state-chip hidden';
  }

  if (chat) {
    ownerChip.textContent = ownerText;
    ownerChip.className = 'whatsapp-state-chip subtle';
  } else {
    ownerChip.textContent = '';
    ownerChip.className = 'whatsapp-state-chip subtle hidden';
  }

  claimBtn.disabled = Boolean(state.whatsapp.pendingAction) || !whatsappThreadCanClaim(chat);
  claimBtn.classList.toggle('hidden', !whatsappThreadCanClaim(chat));
  returnBtn.disabled = Boolean(state.whatsapp.pendingAction) || !whatsappThreadCanReturnToAi(chat);
  returnBtn.classList.toggle('hidden', !whatsappThreadCanReturnToAi(chat));

  renderWhatsappSummaryCard();
}

function renderWhatsappMessagesTimeline() {
  const target = $('whatsappMessageTimeline');
  if (!target) {
    return;
  }
  const chat = selectedWhatsappChat();
  if (!chat) {
    target.innerHTML = `
      <div class="whatsapp-chat-empty">
        <strong>Выберите чат</strong>
        <p>${state.whatsapp.mode === 'live' ? 'Здесь появятся живые WhatsApp-диалоги.' : 'Демо-режим остаётся доступным, пока backend недоступен.'}</p>
      </div>
    `;
    return;
  }
  if (!chat.messages?.length) {
    target.innerHTML = `
      <div class="whatsapp-chat-empty">
        <strong>Пока нет сообщений</strong>
        <p>${state.whatsapp.mode === 'live' ? 'Ждём входящую активность WhatsApp в этом чате.' : 'Демо-рабочее место готово к первому сообщению.'}</p>
      </div>
    `;
    return;
  }
  const fragments = [
    '<div class="whatsapp-encryption-pill">Сообщения и звонки защищены сквозным шифрованием. Никто вне этого чата не может их прочитать или прослушать.</div>',
  ];
  let lastDayKey = '';
  (chat.messages || []).forEach((message) => {
    const dayKey = whatsappMessageDayKey(message.createdAt);
    if (message.direction === 'system') {
      fragments.push(`<div class="whatsapp-day-pill">${escapeHtml(message.text || whatsappMessageDayLabel(message.createdAt))}</div>`);
      lastDayKey = dayKey || lastDayKey;
      return;
    }
    if (dayKey && dayKey !== lastDayKey) {
      lastDayKey = dayKey;
      fragments.push(`<div class="whatsapp-day-pill">${escapeHtml(whatsappMessageDayLabel(message.createdAt))}</div>`);
    }
    const mine = message.direction === 'out';
    const aiBadge = mine && message.authorType === 'ai'
      ? '<span class="whatsapp-message-badge ai">AI</span>'
      : '';
    const author = !mine && chat.isGroup && message.author
      ? `<div class="whatsapp-message-author">${escapeHtml(message.author)}</div>`
      : '';
    fragments.push(`
      <article class="whatsapp-message-row ${mine ? 'me' : 'other'}">
        <div class="whatsapp-message-bubble ${mine ? 'me' : 'other'}">
          ${aiBadge}
          ${author}
          <div class="whatsapp-message-body">${escapeHtml(message.text || '')}</div>
          <div class="whatsapp-message-meta">
            <span class="whatsapp-message-time">${escapeHtml(whatsappMessageTimeLabel(message.createdAt))}</span>
            ${whatsappMessageStatusMarkup(message)}
          </div>
        </div>
      </article>
    `);
  });
  target.innerHTML = fragments.join('');
  target.scrollTop = target.scrollHeight;
}

function renderWhatsappWorkspace() {
  if (!featureEnabled('whatsapp')) {
    return;
  }
  ensureWhatsappSelection();
  renderWhatsappChatList();
  renderWhatsappContextPanel();
  renderWhatsappMessagesTimeline();
  const chat = selectedWhatsappChat();
  const searchInput = $('whatsappSearchInput');
  const composerInput = $('whatsappComposerInput');
  const avatar = $('whatsappChatAvatar');
  const paneCaption = $('whatsappPaneCaption');
  const blockedReason = whatsappComposerBlockedReason(chat);
  const [start, end] = whatsappAvatarPalette(chat?.title || 'WhatsApp');
  if (searchInput && searchInput.value !== state.whatsapp.searchQuery) {
    searchInput.value = state.whatsapp.searchQuery;
  }
  if (composerInput && composerInput.value !== state.whatsapp.composerText) {
    composerInput.value = state.whatsapp.composerText;
  }
  if (composerInput) {
    composerInput.disabled = !chat || Boolean(blockedReason) || Boolean(state.whatsapp.pendingAction);
    composerInput.placeholder = !chat
      ? 'Выберите чат'
      : blockedReason || 'Написать сообщение';
  }
  if (avatar) {
    avatar.textContent = chat ? whatsappChatInitials(chat) : 'WA';
    avatar.style.setProperty('--whatsapp-avatar-start', start);
    avatar.style.setProperty('--whatsapp-avatar-end', end);
  }
  if (paneCaption) {
    paneCaption.textContent = state.whatsapp.mode === 'live'
      ? 'Живая очередь с поддержкой AI'
      : state.whatsapp.mode === 'mock'
        ? 'Демо-режим, пока backend недоступен'
        : 'Подключаемся к backend WhatsApp';
  }
  $('whatsappChatTitle').textContent = chat ? chat.title : 'WhatsApp';
  $('whatsappChatMeta').textContent = chat
    ? [whatsappThreadHandle(chat), whatsappThreadPresenceText(chat)].filter(Boolean).join(' • ')
    : 'Выберите чат для начала переписки.';
  document.querySelectorAll('[data-whatsapp-filter]').forEach((button) => {
    button.classList.toggle('active', button.dataset.whatsappFilter === state.whatsapp.activeFilter);
  });
  syncWhatsappComposerUi();
}

async function loadWhatsappThreadMessages(threadId = state.whatsapp.selectedChatId, logResult = false) {
  if (!featureEnabled('whatsapp')) {
    return [];
  }
  if (!threadId) {
    state.whatsapp.selectedThreadSummary = null;
    renderWhatsappWorkspace();
    return [];
  }
  if (state.whatsapp.mode !== 'live') {
    return selectedWhatsappChat()?.messages || [];
  }
  const chat = state.whatsapp.chats.find((item) => item.id === threadId) || null;
  const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(threadId)}/messages`);
  const messages = Array.isArray(data)
    ? data.map((message) => normalizeWhatsappMessage(message, chat))
    : [];
  updateWhatsappChatMessages(threadId, messages);
  if (logResult) {
    log('WhatsApp thread messages обновлены', { thread_id: threadId, messages: messages.length });
  }
  return messages;
}

async function loadWhatsappThreadSummary(threadId = state.whatsapp.selectedChatId, options = {}) {
  if (!featureEnabled('whatsapp')) {
    return null;
  }
  const { logResult = false } = options;
  if (!threadId || state.whatsapp.mode !== 'live') {
    state.whatsapp.selectedThreadSummary = null;
    renderWhatsappWorkspace();
    return null;
  }
  const thread = state.whatsapp.chats.find((item) => item.id === threadId) || null;
  if (!whatsappThreadSummaryAccessible(thread)) {
    if (threadId === state.whatsapp.selectedChatId) {
      state.whatsapp.selectedThreadSummary = null;
      renderWhatsappWorkspace();
    }
    return null;
  }
  try {
    const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(threadId)}/ai-summary`);
    if (threadId === state.whatsapp.selectedChatId) {
      state.whatsapp.selectedThreadSummary = data || null;
      renderWhatsappWorkspace();
    }
    if (logResult && data) {
      log('AI-сводка WhatsApp загружена', { thread_id: threadId, session_id: data.session_id });
    }
    return data || null;
  } catch (err) {
    if (threadId === state.whatsapp.selectedChatId) {
      state.whatsapp.selectedThreadSummary = null;
      renderWhatsappWorkspace();
    }
    if (logResult) {
      log('Не удалось загрузить AI-сводку WhatsApp', { thread_id: threadId, error: err.message });
    }
    return null;
  }
}

async function loadWhatsappThreads(logResult = true, options = {}) {
  if (!featureEnabled('whatsapp')) {
    return [];
  }
  const {
    preserveSelection = true,
    preserveOnError = false,
    allowMockFallback = true,
  } = options;
  try {
    const data = await api('whatsapp', 'integrations/whatsapp/threads');
    const existingById = new Map(state.whatsapp.chats.map((chat) => [chat.id, chat]));
    const chats = (Array.isArray(data) ? data : []).map((thread) => normalizeWhatsappThread(thread, existingById.get(thread.thread_id)));
    state.whatsapp.mode = 'live';
    state.whatsapp.backendError = '';
    applyWhatsappChats(chats, { preserveSelection });
    await loadWhatsappThreadMessages(state.whatsapp.selectedChatId, false);
    await loadWhatsappThreadSummary(state.whatsapp.selectedChatId, { logResult: false });
    if (logResult) {
      log('WhatsApp threads обновлены', {
        threads: state.whatsapp.chats.length,
        ai_threads: state.whatsapp.chats.filter((chat) => chat.aiState).length,
      });
    }
  } catch (err) {
    state.whatsapp.backendError = err.message;
    if (!state.whatsapp.chats.length && allowMockFallback) {
      seedWhatsappChats();
      renderWhatsappWorkspace();
      if (logResult || !state.whatsapp.mockFallbackLogged) {
        log('WhatsApp backend недоступен, включён demo fallback', { error: err.message });
        state.whatsapp.mockFallbackLogged = true;
      }
      return;
    }
    if (!preserveOnError) {
      state.whatsapp.chats = [];
      state.whatsapp.selectedChatId = '';
      state.whatsapp.selectedThreadSummary = null;
      renderWhatsappWorkspace();
    }
    if (logResult) {
      log('Не удалось загрузить WhatsApp threads', { error: err.message });
    }
  }
}

async function selectWhatsappChat(chatId) {
  if (!chatId) {
    return;
  }
  if (chatId !== state.whatsapp.selectedChatId) {
    state.whatsapp.selectedThreadSummary = null;
  }
  state.whatsapp.selectedChatId = chatId;
  state.whatsapp.composerText = '';
  state.whatsapp.chats = state.whatsapp.chats.map((chat) => ({
    ...chat,
    unreadCount: chat.id === chatId ? 0 : chat.unreadCount,
  }));
  renderWhatsappWorkspace();
  if (state.whatsapp.mode === 'live') {
    await loadWhatsappThreadMessages(chatId, false);
    await loadWhatsappThreadSummary(chatId, { logResult: false });
  }
  window.requestAnimationFrame(() => {
    $('whatsappComposerInput')?.focus();
  });
}

function setWhatsappFilter(filter) {
  state.whatsapp.activeFilter = filter || 'all';
  renderWhatsappWorkspace();
}

async function claimWhatsappThread(options = {}) {
  const { silent = false } = options;
  const chat = selectedWhatsappChat();
  if (!chat) {
    return null;
  }
  const shouldManagePending = !state.whatsapp.pendingAction;
  if (shouldManagePending) {
    state.whatsapp.pendingAction = 'claim';
    renderWhatsappWorkspace();
  }
  try {
    const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(chat))}/claim`, {
      method: 'POST',
    });
    upsertWhatsappThreadData(data);
    await loadWhatsappThreadSummary(data.thread_id, { logResult: false });
    loadInteractions().catch(() => {});
    if (!silent) {
      log('WhatsApp thread взят в работу', { thread_id: data.thread_id, interaction_id: data.interaction_id });
    }
    return selectedWhatsappChat();
  } catch (err) {
    if (!silent) {
      log('Не удалось взять WhatsApp thread в работу', { error: err.message });
    }
    throw err;
  } finally {
    if (shouldManagePending) {
      state.whatsapp.pendingAction = '';
      renderWhatsappWorkspace();
    }
  }
}

async function returnWhatsappThreadToAi() {
  const chat = selectedWhatsappChat();
  if (!chat) {
    return;
  }
  state.whatsapp.pendingAction = 'return-ai';
  renderWhatsappWorkspace();
  try {
    const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(chat))}/return-to-ai`, {
      method: 'POST',
    });
    upsertWhatsappThreadData(data);
    await loadWhatsappThreadSummary(data.thread_id, { logResult: false });
    loadInteractions().catch(() => {});
    log('WhatsApp thread возвращён AI', { thread_id: data.thread_id, interaction_id: data.interaction_id });
  } catch (err) {
    log('Не удалось вернуть WhatsApp thread AI', { error: err.message });
  } finally {
    state.whatsapp.pendingAction = '';
    renderWhatsappWorkspace();
  }
}

async function sendWhatsappMessage() {
  const chat = selectedWhatsappChat();
  const text = String(state.whatsapp.composerText || '').trim();
  if (!chat || !text) {
    return;
  }
  const blockedReason = whatsappComposerBlockedReason(chat);
  if (blockedReason) {
    log('Нельзя отправить WhatsApp reply', { reason: blockedReason, thread_id: whatsappChatThreadId(chat) });
    return;
  }
  if (state.whatsapp.mode !== 'live') {
    const now = new Date().toISOString();
    const outgoing = {
      id: `wa-local-${Date.now()}`,
      direction: 'out',
      text,
      createdAt: now,
      deliveryStatus: 'read',
      authorType: 'human',
    };
    state.whatsapp.chats = sortWhatsappChats(state.whatsapp.chats.map((item) => {
      if (item.id !== chat.id) {
        return item;
      }
      return {
        ...item,
        unreadCount: 0,
        lastMessageAt: now,
        lastMessagePreview: text,
        messages: [...(item.messages || []), outgoing],
      };
    }));
    state.whatsapp.selectedChatId = chat.id;
    state.whatsapp.composerText = '';
    renderWhatsappWorkspace();
    window.requestAnimationFrame(() => {
      $('whatsappComposerInput')?.focus();
    });
    log('WhatsApp local reply saved', { chat_id: chat.id });
    return;
  }
  state.whatsapp.pendingAction = 'reply';
  renderWhatsappWorkspace();
  try {
    let activeChat = selectedWhatsappChat() || chat;
    if (state.role === 'operator' && !activeChat.claimedByUser) {
      const claimedChat = await claimWhatsappThread({ silent: true });
      activeChat = claimedChat || selectedWhatsappChat() || activeChat;
    }
    const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(activeChat))}/messages`, {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    const outgoing = normalizeWhatsappMessage(data, activeChat);
    state.whatsapp.composerText = '';
    const nextChats = state.whatsapp.chats.map((item) => {
      if (item.id !== activeChat.id) {
        return item;
      }
      return {
        ...item,
        claimedByUser: item.claimedByUser || (state.role === 'operator' ? state.user : ''),
        aiState: 'human_owned',
        unreadCount: 0,
        lastMessageAt: outgoing.createdAt,
        lastMessagePreview: outgoing.text,
        messages: [...(item.messages || []), outgoing],
      };
    });
    applyWhatsappChats(nextChats);
    await loadWhatsappThreadSummary(whatsappChatThreadId(activeChat), { logResult: false });
    log('WhatsApp reply поставлен в очередь', {
      thread_id: whatsappChatThreadId(activeChat),
      delivery_status: data.delivery_status || 'pending',
    });
    window.setTimeout(() => {
      loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true, allowMockFallback: false });
    }, 0);
  } catch (err) {
    log('Не удалось отправить WhatsApp reply', { error: err.message });
  } finally {
    state.whatsapp.pendingAction = '';
    renderWhatsappWorkspace();
    window.requestAnimationFrame(() => {
      $('whatsappComposerInput')?.focus();
    });
  }
}

function startWhatsappPolling() {
  if (!featureEnabled('whatsapp')) {
    stopWhatsappPolling();
    return;
  }
  stopWhatsappPolling();
  state.whatsapp.pollTimer = window.setInterval(() => {
    loadWhatsappThreads(false, {
      preserveSelection: true,
      preserveOnError: true,
      allowMockFallback: state.whatsapp.mode !== 'live',
    });
  }, 5000);
}

function handleWhatsappUiAction(event) {
  if (!featureEnabled('whatsapp')) {
    return;
  }
  const action = event.currentTarget.dataset.whatsappUiAction || 'неизвестно';
  if (action === 'chats') {
    return;
  }
  if (action === 'search-chat') {
    $('whatsappSearchInput')?.focus();
    return;
  }
  if (action === 'new-chat') {
    $('whatsappSearchInput')?.focus();
    log('Создание нового WhatsApp чата пока не подключено');
    return;
  }
  log('WhatsApp UI action пока без backend-привязки', { action, mode: state.whatsapp.mode });
}

function toggleCustomerLeadForm(forceOpen = null) {
  state.customers.leadFormOpen = forceOpen === null ? !state.customers.leadFormOpen : Boolean(forceOpen);
  syncCustomerFormUi();
}

function exportCustomersCsv() {
  if (!state.customers.items.length) {
    log('Нечего экспортировать: список лидов пуст.');
    return;
  }
  const csv = [
    ['customer_id', 'display_name', 'phone', 'source', 'status', 'score'],
    ...state.customers.items.map((item, index) => [
      item.customer_id,
      item.display_name,
      (item.phones || []).join(', '),
      customerLeadSourceMeta(item, index).label,
      customerLeadStatusMeta(item, index).label,
      customerLeadScore(item, index),
    ]),
  ]
    .map((row) => row.map((value) => `"${String(value ?? '').replaceAll('"', '""')}"`).join(','))
    .join('\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'leads.csv';
  link.click();
  URL.revokeObjectURL(url);
  log('Экспорт лидов подготовлен', { rows: state.customers.items.length });
}

function statusMeta(status) {
  return STATUS_META[status] || { label: status || 'неизвестно', className: 'status-closed' };
}

function channelLabel(channel) {
  return CHANNEL_LABELS[channel] || channel || 'Канал не указан';
}

function formatIsoShort(value) {
  if (!value) {
    return 'н/д';
  }
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) {
    return value;
  }
  return dt.toLocaleString();
}

function renderSummaryCard(label, value, note = '') {
  return `
    <div class="summary-card">
      <div class="summary-label">${label}</div>
      <div class="summary-value">${value}</div>
      ${note ? `<div class="summary-note">${note}</div>` : ''}
    </div>
  `;
}

function updateProfileMeta() {
  const profileName = $('profileName');
  const profileRole = $('profileRole');
  if (!profileName || !profileRole) {
    return;
  }
  profileName.textContent = state.fullName || state.user || 'Пользователь KonturCC';
  profileRole.textContent = ROLE_LABELS[state.role] || state.role || 'Пользователь';
}

function operatorHashState(hash = window.location.hash) {
  const raw = String(hash || '').replace(/^#/, '');
  if (raw.startsWith('customer-profile/')) {
    return { view: 'customer-profile', anchor: '', customerId: decodeURIComponent(raw.slice('customer-profile/'.length)) };
  }
  if (raw === 'customer-profile') {
    return { view: 'customer-profile', anchor: '', customerId: '' };
  }
  if (!raw || raw === 'workspace') {
    return { view: 'workspace', anchor: '', customerId: '' };
  }
  if (raw === 'customers-page' || raw === 'customers') {
    return { view: 'customers', anchor: '', customerId: '' };
  }
  if (raw === 'messages-page' || raw === 'messages') {
    return { view: 'messages', anchor: '', customerId: '' };
  }
  if (raw === 'telegram-page' || raw === 'telegram') {
    return { view: 'telegram', anchor: '', customerId: '' };
  }
  if (raw === 'whatsapp-page' || raw === 'whatsapp') {
    return { view: 'whatsapp', anchor: '', customerId: '' };
  }
  if (raw === 'calls' || raw === 'calls-page' || raw === 'voice-debug' || raw === 'voice-debug-page') {
    return { view: 'calls', anchor: '', customerId: '' };
  }
  if (raw === 'interactions' || raw === 'integrations') {
    return { view: 'workspace', anchor: raw, customerId: '' };
  }
  return { view: 'workspace', anchor: '', customerId: '' };
}

function syncOperatorViewNav(view, anchor = '') {
  document.querySelectorAll('[data-operator-view-link]').forEach((link) => {
    const linkView = link.dataset.operatorViewLink;
    const isCustomers = view === 'customer-profile' && linkView === 'customers';
    link.classList.toggle('active', linkView === view || isCustomers);
  });
  document.querySelectorAll('[data-operator-anchor-link]').forEach((link) => {
    link.classList.toggle('active', view === 'workspace' && link.dataset.operatorAnchorLink === anchor);
  });
}

function applyOperatorViewFromHash(options = {}) {
  const { scroll = false } = options;
  let { view, anchor, customerId } = operatorHashState();
  const viewLink = document.querySelector(`[data-operator-view-link="${view}"]`);
  if (!isOperatorViewEnabled(view) || (viewLink && (viewLink.hidden || viewLink.style.display === 'none'))) {
    view = 'workspace';
    anchor = '';
    window.history.replaceState(null, '', '#workspace');
  } else if (view === 'calls' && (window.location.hash === '#voice-debug' || window.location.hash === '#voice-debug-page')) {
    window.history.replaceState(null, '', '#calls');
  }
  if (customerId) {
    state.customers.selectedCustomerId = customerId;
  }
  if (view === 'customer-profile' && !selectedCustomer()) {
    view = 'customers';
    window.history.replaceState(null, '', '#customers-page');
  }
  Object.entries(OPERATOR_VIEW_IDS).forEach(([key, id]) => {
    $(id)?.classList.toggle('hidden', key !== view);
  });
  syncOperatorViewNav(view, anchor);
  if (view === 'messages') {
    renderMessengerWorkspace();
    startMessengerPolling();
    loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch((err) => {
      state.messenger.backendError = err.message || 'Не удалось загрузить сообщения.';
      renderMessengerWorkspace();
    });
  } else {
    stopMessengerPolling();
  }

  const targetId = view === 'workspace' ? anchor : '';
  window.requestAnimationFrame(() => {
    if (targetId && $(targetId)) {
      $(targetId).scrollIntoView({ behavior: scroll ? 'smooth' : 'auto', block: 'start' });
      return;
    }
    window.scrollTo({ top: 0, behavior: scroll ? 'smooth' : 'auto' });
  });
}

function handleOperatorHashChange() {
  applyOperatorViewFromHash({ scroll: true });
}

function renderSupervisorSummary(data) {
  const agents = data?.agents || {};
  const byState = agents.by_state || {};
  const firstQueue = (data?.queues || [])[0] || {};
  const total = Number(agents.total || 0);
  const ready = Number(byState.READY || 0);
  const busy = Number(byState.BUSY || 0);
  const queueId = firstQueue.queue_id || 'нет очереди';
  const inQueue = Number(firstQueue.in_queue || 0);

  $('supervisorSummary').innerHTML = [
    renderSummaryCard('Агентов', total, 'Текущее состояние смены'),
    renderSummaryCard('Готовы', ready, 'Могут взять следующий контакт'),
    renderSummaryCard('Заняты', busy, 'Сейчас в обработке'),
    renderSummaryCard('Очередь', queueId, `В очереди: ${inQueue}`),
  ].join('');
}

function renderKpiSummary(data) {
  const kpi = data?.kpi || {};
  $('kpiSummary').innerHTML = [
    renderSummaryCard('SL', `${Number(kpi.SL || 0).toFixed(2)}%`, 'Доля контактов в SLA'),
    renderSummaryCard('ASA', `${Number(kpi.ASA || 0).toFixed(2)} с`, 'Среднее время ожидания'),
    renderSummaryCard('AHT', `${Number(kpi.AHT || 0).toFixed(2)} с`, 'Среднее время обработки'),
    renderSummaryCard('Abandon', `${Number(kpi.Abandon || 0).toFixed(2)}%`, 'Потерянные обращения'),
    renderSummaryCard('FCR', `${Number(kpi.FCR || 0).toFixed(2)}%`, 'Решено с первого контакта'),
  ].join('');
}

function getDefaultAssignee() {
  return $('defaultAssignee').value.trim() || DEMO_ASSIGNEE;
}

function getDefaultEscalationQueue() {
  return $('defaultEscalationQueue').value.trim() || DEMO_QUEUE;
}

function updateSessionInfo(extra = '') {
  if (!state.token) {
    $('sessionInfo').textContent = 'Токен: отсутствует';
    return;
  }
  const source = state.authSource === 'oidc' ? 'корпоративный' : 'локальный';
  $('sessionInfo').textContent = `Токен активен | роль: ${state.role} | вход: ${source}${extra ? ` | ${extra}` : ''}`;
}

function applyAuthenticatedSession(data) {
  state.token = data.access_token;
  state.role = data.role;
  state.user = data.username || state.user;
  state.authSource = data.auth_source || 'local';
  state.fullName = data.full_name || null;
  $('sessionUser').value = state.user;
  $('sessionRole').value = data.role;
  updateSessionInfo(data.provider ? `провайдер: ${data.provider}` : '');
  updateProfileMeta();
  persistSession();
  applyRoleNavigation();
}

async function api(service, path, options = {}) {
  syncSessionFromInputs();
  const headers = {
    'Content-Type': 'application/json',
    'X-User': state.user,
    'X-Role': state.role,
    ...(options.headers || {}),
  };
  if (state.token) {
    headers.Authorization = `Bearer ${state.token}`;
  }

  const response = await fetch(`/proxy/${service}/${path}`, {
    ...options,
    headers,
  });

  let data;
  try {
    data = await response.json();
  } catch {
    data = { error: 'Сервис вернул ответ не в JSON-формате' };
  }

  if (!response.ok) {
    if (response.status === 401 && state.token) {
      state.token = null;
      state.authSource = 'local';
      clearStoredSession();
      updateSessionInfo('требуется повторный вход');
      window.location.href = '/';
    }
    throw new Error(`${response.status}: ${JSON.stringify(data)}`);
  }
  return data;
}

async function checkGateway() {
  try {
    const res = await fetch('/health');
    const data = await res.json();
    $('gatewayStatus').textContent = `Шлюз: ${data.status === 'ok' ? 'готов' : data.status}`;
  } catch {
    $('gatewayStatus').textContent = 'Шлюз: недоступен';
  }
}

async function loadOperatorConfig() {
  try {
    const response = await fetch('/operator/config');
    const data = await response.json();
    state.features.whatsapp = Boolean(data?.features?.whatsapp);
  } catch {
    state.features.whatsapp = false;
  }
  syncWhatsappFeatureVisibility();
}

async function loadOidcConfig() {
  try {
    const response = await fetch('/proxy/auth/auth/oidc/config');
    const data = await response.json();
    state.oidc.enabled = Boolean(data.enabled);
    state.oidc.loginPath = data.login_path || '/auth/oidc/start?return_mode=popup';
    state.oidc.providerLabel = data.provider_label || 'Keycloak';
    $('corporateLoginBtn').disabled = !state.oidc.enabled;
    $('corporateLoginBtn').textContent = state.oidc.enabled
      ? `Корпоративный вход (${state.oidc.providerLabel})`
      : 'Корпоративный вход недоступен';
  } catch {
    $('corporateLoginBtn').disabled = true;
    $('corporateLoginBtn').textContent = 'Корпоративный вход недоступен';
  }
}

function startCorporateLogin() {
  if (!state.oidc.enabled) {
    log('Корпоративный вход недоступен');
    return;
  }
  const popup = window.open(`/proxy/auth${state.oidc.loginPath}`, 'oidc-login', 'width=620,height=760');
  if (!popup) {
    log('Не удалось открыть окно корпоративного входа');
    return;
  }
  log('Открыт корпоративный вход', { провайдер: state.oidc.providerLabel });
}

function handleOidcMessage(event) {
  if (!event?.data || typeof event.data !== 'object') {
    return;
  }
  if (event.data.type === 'oidc-login' && event.data.access_token) {
    applyAuthenticatedSession(event.data);
    log('Корпоративный вход выполнен', { user: state.user, role: state.role });
    return;
  }
  if (event.data.type === 'oidc-error') {
    log('Корпоративный вход не выполнен', { error: event.data.message || 'неизвестно' });
    updateSessionInfo(`ошибка SSO: ${event.data.message || 'неизвестно'}`);
  }
}

async function login() {
  syncSessionFromInputs();
  const password = $('sessionPassword').value;

  try {
    const data = await api('auth', 'auth/login', {
      method: 'POST',
      body: JSON.stringify({ username: state.user, password }),
    });
    applyAuthenticatedSession({
      ...data,
      username: state.user,
      auth_source: data.auth_source || 'local',
    });
    log('Вход выполнен', { user: state.user, role: data.role });
  } catch (err) {
    $('sessionInfo').textContent = `Ошибка входа: ${err.message}`;
    log('Вход не выполнен', { error: err.message });
  }
}

async function createCustomer() {
  const displayName = $('customerName').value.trim();
  const phone = $('customerPhone').value.trim();
  if (!displayName) {
    return;
  }

  const payload = {
    display_name: displayName,
    phones: phone ? [phone] : [],
    preferred_phone: phone || null,
    tags: [],
  };

  try {
    const data = await api('customer', 'customers', {
      method: 'POST',
      body: JSON.stringify(payload),
    });
    $('interactionCustomerId').value = data.customer_id;
    $('customerName').value = '';
    $('customerPhone').value = '';
    toggleCustomerLeadForm(false);
    log('Клиент создан', { customer_id: data.customer_id, name: data.display_name });
    await searchCustomers();
  } catch (err) {
    log('Не удалось создать клиента', { error: err.message });
  }
}

async function searchCustomers() {
  const rawQuery = $('customerQuery').value.trim();
  const q = encodeURIComponent(rawQuery);
  try {
    const data = await api('customer', `customers${q ? `?query=${q}` : ''}`);
    if (!data.length) {
      state.customers.items = [];
      state.customers.page = 1;
      state.customers.selectedCustomerId = '';
      renderCustomerList();
      renderUnifiedInbox();
      return;
    }
    if (!$('interactionCustomerId').value.trim()) {
      $('interactionCustomerId').value = data[0].customer_id;
    }
    state.customers.items = data;
    state.customers.page = 1;
    const selectedExists = data.some((item) => item.customer_id === state.customers.selectedCustomerId);
    state.customers.selectedCustomerId = selectedExists ? state.customers.selectedCustomerId : data[0].customer_id;
    renderCustomerList();
    renderUnifiedInbox();
  } catch (err) {
    state.customers.items = [];
    state.customers.page = 1;
    state.customers.selectedCustomerId = '';
    renderCustomerList();
    renderUnifiedInbox();
    log('Не удалось загрузить клиентов', { error: err.message });
  }
}

async function createInteraction() {
  const payload = {
    channel: $('interactionChannel').value,
    subject: $('interactionSubject').value.trim() || 'Операционный контакт',
    customer_id: $('interactionCustomerId').value.trim() || null,
    queue_id: 'q_main',
    priority: 3,
  };

  try {
    const data = await api('interaction', 'interactions', {
      method: 'POST',
      body: JSON.stringify(payload),
    });
    log('Обращение создано', { interaction_id: data.interaction_id, channel: data.channel });
    await loadInteractions();
  } catch (err) {
    log('Не удалось создать обращение', { error: err.message });
  }
}

function renderInteractionCard(item) {
  const meta = statusMeta(item.status);
  const facebookLead = isFacebookLeadInteraction(item);
  const cardClass = item.status === 'escalated'
    ? 'pipeline-card escalated'
    : item.status === 'closed'
      ? 'pipeline-card closed'
      : 'pipeline-card';
  const badges = [
    facebookLead ? '<span class="micro-badge facebook">Facebook</span>' : '',
    `<span class="micro-badge channel">${escapeHtml(channelLabel(item.channel))}</span>`,
    item.queue_id ? `<span class="micro-badge queue">${escapeHtml(item.queue_id)}</span>` : '',
    item.assigned_to ? `<span class="micro-badge assignee">${escapeHtml(item.assigned_to)}</span>` : '',
  ].filter(Boolean).join('');

  return `
    <article class="${cardClass}">
      <div class="card-badges">${badges}</div>
      <div class="row-item-head">
        <h3 class="card-title">${escapeHtml(item.subject || 'Без темы')}</h3>
        <span class="status-chip ${meta.className}">${escapeHtml(meta.label)}</span>
      </div>
      <p class="card-subtitle">${escapeHtml(item.interaction_id)}</p>
      <p class="card-meta-line">Клиент: ${escapeHtml(item.customer_id || 'не привязан')}</p>
      <p class="card-meta-line">Обновлено: ${escapeHtml(formatIsoShort(item.updated_at))}</p>
      <div class="card-divider"></div>
      <div class="row-actions">
        ${facebookLead ? `<button type="button" data-reply-action="facebook" data-interaction-id="${escapeHtml(item.interaction_id)}">Ответить</button>` : ''}
        <button onclick="assignInteraction('${item.interaction_id}')">Назначить</button>
        <button onclick="escalateInteraction('${item.interaction_id}')">Передать на 2 линию</button>
        <button onclick="closeInteraction('${item.interaction_id}')">Закрыть</button>
      </div>
    </article>
  `;
}

function renderInteractionBoard(items) {
  const groups = {
    new: [],
    in_progress: [],
    escalated: [],
    closed: [],
  };

  items.forEach((item) => {
    const bucket = groups[item.status] ? item.status : 'closed';
    groups[bucket].push(item);
  });

  return `
    <div class="pipeline-board">
      ${BOARD_COLUMNS.map((column) => {
        const cards = groups[column.key];
        const content = cards.length
          ? cards.map((item) => renderInteractionCard(item)).join('')
          : '<div class="empty-state">Пока пусто.</div>';
        return `
          <section class="pipeline-column">
            <div class="pipeline-head">
              <div class="pipeline-title">${column.title}</div>
              <div class="pipeline-count">${cards.length}</div>
            </div>
            <div class="pipeline-stack">
              ${content}
            </div>
          </section>
        `;
      }).join('')}
    </div>
  `;
}

function unifiedInboxSortValue(...values) {
  for (const value of values) {
    const parsed = new Date(value || '').getTime();
    if (Number.isFinite(parsed) && parsed > 0) {
      return parsed;
    }
  }
  return 0;
}

function unifiedInboxInteractionBucket(item) {
  if (!item || item.status === 'closed') {
    return '';
  }
  if (item.status === 'escalated') {
    return 'escalated';
  }
  if (String(item.assigned_to || '').trim() === String(state.user || '').trim()) {
    return 'mine';
  }
  if (item.status === 'new' || !String(item.assigned_to || '').trim()) {
    return 'new';
  }
  return '';
}

function unifiedInboxTelegramBucket(thread) {
  if (!thread || thread.status === 'closed') {
    return '';
  }
  if (['handoff_required', 'human_owned'].includes(String(thread.ai_state || '').trim())) {
    return 'ai';
  }
  if (String(thread.claimed_by_user || '').trim() === String(state.user || '').trim()) {
    return 'mine';
  }
  return 'new';
}

function renderUnifiedInboxBadge(label, className = '') {
  if (!label) {
    return '';
  }
  return `<span class="micro-badge ${className}">${escapeHtml(label)}</span>`;
}

function unifiedInboxActionLabel(item) {
  if (item.kind === 'telegram') {
    return 'Открыть чат';
  }
  if (item.kind === 'call') {
    return 'Открыть звонок';
  }
  return 'К обращению';
}

function buildUnifiedInboxInteractionItem(item) {
  const bucket = unifiedInboxInteractionBucket(item);
  if (!bucket) {
    return null;
  }
  const facebookLead = isFacebookLeadInteraction(item);
  const badges = [
    facebookLead ? renderUnifiedInboxBadge('Facebook', 'facebook') : '',
    renderUnifiedInboxBadge(channelLabel(item.channel), 'channel'),
    item.queue_id ? renderUnifiedInboxBadge(item.queue_id, 'queue') : '',
    item.assigned_to ? renderUnifiedInboxBadge(item.assigned_to, 'owner') : '',
  ].filter(Boolean);
  return {
    key: `interaction:${item.interaction_id}`,
    kind: 'interaction',
    bucket,
    sortValue: unifiedInboxSortValue(item.updated_at, item.created_at),
    title: item.subject || 'Обращение без темы',
    subtitle: item.interaction_id,
    badges,
    metaLines: [
      `Клиент: ${customerDisplayName(item.customer_id || '')}`,
      `Статус: ${statusMeta(item.status).label}`,
      `Обновлено: ${formatIsoShort(item.updated_at || item.created_at)}`,
    ],
    customerId: item.customer_id || '',
    interactionId: item.interaction_id,
    canReply: facebookLead,
  };
}

function buildUnifiedInboxTelegramItem(thread) {
  const bucket = unifiedInboxTelegramBucket(thread);
  if (!bucket) {
    return null;
  }
  const customerId = customerIdForInteractionId(thread.interaction_id);
  const aiMeta = telegramThreadAiStateMeta(thread);
  const unreadCount = telegramThreadUnreadCount(thread);
  const badges = [
    renderUnifiedInboxBadge('Telegram', 'channel'),
    unreadCount > 0 ? renderUnifiedInboxBadge(`${unreadCount} new`, 'queue') : '',
    aiMeta ? renderUnifiedInboxBadge(aiMeta.label, aiMeta.tone) : '',
    thread.claimed_by_user ? renderUnifiedInboxBadge(thread.claimed_by_user, 'owner') : '',
  ].filter(Boolean);
  return {
    key: `telegram:${thread.thread_id}`,
    kind: 'telegram',
    bucket,
    sortValue: unifiedInboxSortValue(thread.last_message_at, thread.updated_at, thread.created_at),
    title: telegramThreadDisplayName(thread),
    subtitle: telegramThreadSummary(thread),
    badges,
    metaLines: [
      `Клиент: ${customerDisplayName(customerId)}`,
      thread.last_message_preview ? `Последнее: ${thread.last_message_preview}` : 'Последнее сообщение пока не загружено',
      `Статус: ${telegramThreadHeaderPresence(thread)}`,
    ],
    customerId,
    interactionId: thread.interaction_id || '',
    threadId: thread.thread_id,
  };
}

function buildUnifiedInboxCallItem(item) {
  const summary = voiceSummaryForItem(item);
  const aiMeta = voiceAiStatusMeta(item);
  const interaction = interactionById(item.interaction_id || '');
  const customerId = interaction?.customer_id || '';
  const caller = voiceCustomerDisplayName(item, summary);
  const nameStatusMeta = voiceCustomerNameStatusMeta(summary?.customer_name_status);
  const badges = [
    renderUnifiedInboxBadge('Голос', 'channel'),
    renderUnifiedInboxBadge(telephonyLabel(item.telephony_status), 'assignee'),
    aiMeta ? renderUnifiedInboxBadge(aiMeta.label, aiMeta.className) : '',
    nameStatusMeta ? renderUnifiedInboxBadge(nameStatusMeta.shortLabel, nameStatusMeta.className) : '',
    item.claimed_by_user ? renderUnifiedInboxBadge(item.claimed_by_user, 'owner') : '',
  ].filter(Boolean);
  return {
    key: `call:${item.call_id}`,
    kind: 'call',
    bucket: 'calls',
    sortValue: unifiedInboxSortValue(item.started_at, item.updated_at),
    title: caller,
    subtitle: voiceCustomerCallSubtitle(item),
    badges,
    metaLines: [
      `Клиент: ${customerDisplayName(customerId)}`,
      `Контакт: ${voiceCustomerCallSubtitle(item)}`,
      summary ? `Имя: ${voiceCustomerNameStateLine(summary) || 'без подтверждения'}` : '',
      `Обращение: ${item.interaction_id || 'не найдено'}`,
      `Начат: ${formatIsoShort(item.started_at || item.connected_at)}`,
      item.ai_handoff_reason ? `AI: ${item.ai_handoff_reason}` : `Статус: ${telephonyLabel(item.telephony_status)}`,
    ].filter(Boolean),
    customerId,
    interactionId: item.interaction_id || '',
    callId: item.call_id,
  };
}

function buildUnifiedInboxCollections() {
  const collections = {
    new: [],
    mine: [],
    ai: [],
    escalated: [],
    calls: [],
  };
  const representedInteractionIds = new Set();

  state.telegram.threads.forEach((thread) => {
    if (thread?.interaction_id && thread.status !== 'closed') {
      representedInteractionIds.add(thread.interaction_id);
    }
    const item = buildUnifiedInboxTelegramItem(thread);
    if (item) {
      collections[item.bucket].push(item);
    }
  });

  state.liveCalls.items.forEach((call) => {
    if (call?.interaction_id) {
      representedInteractionIds.add(call.interaction_id);
    }
    const item = buildUnifiedInboxCallItem(call);
    if (item) {
      collections.calls.push(item);
    }
  });

  state.interactions.forEach((item) => {
    if (representedInteractionIds.has(item.interaction_id)) {
      return;
    }
    const inboxItem = buildUnifiedInboxInteractionItem(item);
    if (inboxItem) {
      collections[inboxItem.bucket].push(inboxItem);
    }
  });

  Object.values(collections).forEach((items) => {
    items.sort((left, right) => right.sortValue - left.sortValue);
  });
  return collections;
}

function renderUnifiedInboxCard(item) {
  const actions = [
    `<button type="button" data-inbox-action="open" data-inbox-kind="${escapeHtml(item.kind)}" data-thread-id="${escapeHtml(item.threadId || '')}" data-interaction-id="${escapeHtml(item.interactionId || '')}" data-call-id="${escapeHtml(item.callId || '')}" data-customer-id="${escapeHtml(item.customerId || '')}">${escapeHtml(unifiedInboxActionLabel(item))}</button>`,
    item.customerId ? `<button type="button" data-inbox-action="customer" data-customer-id="${escapeHtml(item.customerId)}">К клиенту</button>` : '',
    item.canReply ? `<button type="button" data-reply-action="facebook" data-interaction-id="${escapeHtml(item.interactionId || '')}">Ответить</button>` : '',
  ].filter(Boolean).join('');

  return `
    <article class="pipeline-card unified-inbox-card${item.bucket === 'escalated' ? ' escalated' : ''}">
      <div class="card-badges">${item.badges.join('')}</div>
      <div class="row-item-head">
        <h3 class="card-title">${escapeHtml(item.title)}</h3>
      </div>
      <p class="card-subtitle">${escapeHtml(item.subtitle)}</p>
      ${item.metaLines.map((line) => `<p class="card-meta-line">${escapeHtml(line)}</p>`).join('')}
      <div class="card-divider"></div>
      <div class="row-actions">
        ${actions}
      </div>
    </article>
  `;
}

function renderUnifiedInbox() {
  const board = $('unifiedInboxBoard');
  const summary = $('unifiedInboxSummary');
  if (!board || !summary) {
    return;
  }
  const collections = buildUnifiedInboxCollections();
  const total = Object.values(collections).reduce((count, items) => count + items.length, 0);
  summary.innerHTML = [
    renderSummaryCard('В очереди', String(total), 'Все задачи без дублей между каналами'),
    renderSummaryCard('Новые', String(collections.new.length), 'Свободные обращения и чаты'),
    renderSummaryCard('Мои', String(collections.mine.length), 'Закреплено за текущим пользователем'),
    renderSummaryCard('AI handoff', String(collections.ai.length), 'Диалоги, где AI позвал человека'),
    renderSummaryCard('Звонки', String(collections.calls.length), 'Активные голосовые разговоры'),
    renderSummaryCard('Эскалации', String(collections.escalated.length), 'Очередь второй линии и спорные кейсы'),
  ].join('');
  board.innerHTML = `
    <div class="pipeline-board unified-inbox-board">
      ${UNIFIED_INBOX_COLUMNS.map((column) => `
        <section class="pipeline-column">
          <div class="pipeline-head">
            <div class="pipeline-title">${escapeHtml(column.title)}</div>
            <div class="pipeline-count">${collections[column.key].length}</div>
          </div>
          <div class="pipeline-stack">
            ${collections[column.key].length
              ? collections[column.key].map((item) => renderUnifiedInboxCard(item)).join('')
              : `<div class="empty-state">${escapeHtml(column.emptyMessage)}</div>`}
          </div>
        </section>
      `).join('')}
    </div>
  `;
}

function focusLiveCall(callId) {
  if (!callId) {
    return;
  }
  const activeCall = state.liveCalls.items.find((item) => item.call_id === callId) || null;
  const relatedCall = activeCall || state.liveCalls.recentItems.find((item) => item.call_id === callId) || null;
  const select = $('liveCallIdSelect');
  if (select) {
    select.value = activeCall ? callId : '';
  }
  state.liveCalls.selectedCallId = callId;
  renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems);
  syncLiveCallActionButtons();
  ensureVoiceAiSummaryLoaded(relatedCall);
}

function handleLiveCallTableClick(event) {
  const button = event.target.closest('[data-live-call-action]');
  if (!button) {
    return;
  }
  const callId = button.dataset.callId || '';
  if (!callId) {
    return;
  }
  if (button.dataset.liveCallAction === 'edit-name') {
    openLiveCallNameEditor(callId, 'panel');
    return;
  }
  if (button.dataset.liveCallAction === 'customer') {
    const customerId = button.dataset.customerId || '';
    if (customerId) {
      openCustomerProfile(customerId);
    }
  }
}

async function openUnifiedInboxItem(button) {
  const kind = button.dataset.inboxKind || '';
  const interactionId = button.dataset.interactionId || '';
  const threadId = button.dataset.threadId || '';
  const callId = button.dataset.callId || '';
  const customerId = button.dataset.customerId || '';

  if (kind === 'telegram') {
    window.location.hash = '#messages';
    await selectMessengerConversation(messengerConversationId('telegram', threadId));
    return;
  }
  if (kind === 'call') {
    focusLiveCall(callId);
    window.location.hash = '#calls';
    return;
  }
  if (customerId) {
    $('interactionCustomerId').value = customerId;
  }
  window.location.hash = '#interactions';
  log('Открыто обращение из единой очереди', { interaction_id: interactionId, customer_id: customerId || '-' });
}

function handleUnifiedInboxClick(event) {
  const button = event.target.closest('[data-inbox-action]');
  if (!button) {
    return;
  }
  if (button.dataset.inboxAction === 'customer') {
    openCustomerProfile(button.dataset.customerId || '');
    return;
  }
  openUnifiedInboxItem(button).catch((err) => {
    log('Не удалось открыть элемент единой очереди', { error: err.message });
  });
}

async function refreshUnifiedInbox() {
  await Promise.all([
    searchCustomers(),
    loadInteractions(),
    loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }),
    loadLiveCalls(false, { preserveStateOnError: true }),
    selectedCustomer() ? ensureCustomerHistoryLoaded(selectedCustomer().customer_id, { force: true }) : Promise.resolve(null),
  ]);
  log('Единая очередь обновлена', { total: Object.values(buildUnifiedInboxCollections()).reduce((count, items) => count + items.length, 0) });
}

function telephonyLabel(value) {
  return LIVE_TELEPHONY_LABELS[value] || value || 'неизвестно';
}

function selectedLiveCallId() {
  const value = $('liveCallIdSelect')?.value || '';
  return value.trim();
}

function selectedLiveCallItem() {
  const callId = selectedLiveCallId();
  return state.liveCalls.items.find((item) => item.call_id === callId) || null;
}

function liveCallById(callId) {
  if (!callId) {
    return null;
  }
  return state.liveCalls.items.find((item) => item.call_id === callId)
    || state.liveCalls.recentItems.find((item) => item.call_id === callId)
    || null;
}

function liveCallCustomerId(item) {
  return interactionById(item?.interaction_id || '')?.customer_id || '';
}

function closeCustomerProfileNameEditor() {
  state.customers.nameEditor.open = false;
  state.customers.nameEditor.customerId = '';
  state.customers.nameEditor.draft = '';
  state.customers.nameEditor.saving = false;
  state.customers.nameEditor.error = '';
  renderCustomerSpotlight();
}

function openCustomerProfileNameEditor(customerId = state.customers.selectedCustomerId) {
  const customer = state.customers.items.find((item) => item.customer_id === customerId) || null;
  if (!customer) {
    return;
  }
  state.customers.nameEditor.open = true;
  state.customers.nameEditor.customerId = customer.customer_id;
  state.customers.nameEditor.draft = customer.display_name || '';
  state.customers.nameEditor.saving = false;
  state.customers.nameEditor.error = '';
  state.customers.nameEditor.flash = '';
  renderCustomerSpotlight();
}

function closeLiveCallNameEditor() {
  state.liveCalls.nameEditor.open = false;
  state.liveCalls.nameEditor.callId = '';
  state.liveCalls.nameEditor.customerId = '';
  state.liveCalls.nameEditor.draft = '';
  state.liveCalls.nameEditor.saving = false;
  state.liveCalls.nameEditor.error = '';
  updateLiveCallNameEditorsUi();
}

function openLiveCallNameEditor(callId, mode = 'panel') {
  const item = liveCallById(callId);
  const customerId = liveCallCustomerId(item);
  if (!item || !customerId) {
    log('Нельзя исправить имя: звонок не привязан к клиенту', { call_id: callId || '-' });
    return;
  }
  state.liveCalls.nameEditor.open = true;
  state.liveCalls.nameEditor.mode = mode;
  state.liveCalls.nameEditor.callId = item.call_id;
  state.liveCalls.nameEditor.customerId = customerId;
  state.liveCalls.nameEditor.draft = voiceCustomerDisplayName(item);
  state.liveCalls.nameEditor.saving = false;
  state.liveCalls.nameEditor.error = '';
  if (mode === 'panel') {
    focusLiveCall(item.call_id);
  }
  updateLiveCallNameEditorsUi();
}

function applyCustomerNamePatchLocally({ customerId, callId, displayName }) {
  const normalizedName = String(displayName || '').trim();
  if (!customerId || !normalizedName) {
    return;
  }
  state.customers.items = state.customers.items.map((item) => (
    item.customer_id === customerId ? { ...item, display_name: normalizedName } : item
  ));
  const history = state.customers.historyById[customerId];
  if (history?.customer) {
    history.customer.display_name = normalizedName;
  }
  const interactionIds = new Set(
    state.interactions
      .filter((item) => item.customer_id === customerId)
      .map((item) => item.interaction_id)
      .filter(Boolean),
  );
  const patchCall = (item) => {
    if (!item) {
      return item;
    }
    const matchesCall = callId && item.call_id === callId;
    const matchesCustomer = interactionIds.has(item.interaction_id);
    if (!matchesCall && !matchesCustomer) {
      return item;
    }
    return {
      ...item,
      caller_name: normalizedName,
    };
  };
  state.liveCalls.items = state.liveCalls.items.map(patchCall);
  state.liveCalls.recentItems = state.liveCalls.recentItems.map(patchCall);
  if (history?.live_calls) {
    history.live_calls = history.live_calls.map((item) => ({
      ...item,
      caller_name: interactionIds.has(item.interaction_id) || item.call_id === callId ? normalizedName : item.caller_name,
    }));
  }
  const affectedCallIds = new Set(
    [...state.liveCalls.items, ...state.liveCalls.recentItems]
      .filter((item) => interactionIds.has(item.interaction_id) || item.call_id === callId)
      .map((item) => item.call_id)
      .filter(Boolean),
  );
  affectedCallIds.forEach((id) => {
    if (!state.liveCalls.aiSummaries[id]) {
      return;
    }
    state.liveCalls.aiSummaries[id] = {
      ...state.liveCalls.aiSummaries[id],
      customer_name_status: 'name_obtained',
      customer_name_value: normalizedName,
      customer_name_source: 'manual',
    };
  });
  if (state.liveCalls.nameEditor.customerId === customerId && !state.liveCalls.nameEditor.saving) {
    state.liveCalls.nameEditor.draft = normalizedName;
  }
  if (state.customers.nameEditor.customerId === customerId && !state.customers.nameEditor.saving) {
    state.customers.nameEditor.draft = normalizedName;
  }
  renderCustomerList();
  refreshVoiceSummaryDependentViews();
}

async function persistCustomerDisplayName({ customerId, displayName, callId = '' }) {
  const data = await api('customer', `customers/${encodeURIComponent(customerId)}`, {
    method: 'PATCH',
    body: JSON.stringify({ display_name: displayName, source: 'manual' }),
  });
  const savedName = data?.display_name || displayName;
  applyCustomerNamePatchLocally({
    customerId,
    callId,
    displayName: savedName,
  });
  ensureCustomerHistoryLoaded(customerId, { force: true }).catch(() => {});
  if (callId) {
    loadVoiceAiSummary(callId, { force: true, silent: true }).catch(() => {});
  }
  refreshLiveCallsInBackground();
  return savedName;
}

async function saveLiveCallCustomerName() {
  const { customerId, callId } = state.liveCalls.nameEditor;
  const displayName = String(state.liveCalls.nameEditor.draft || '').trim().replace(/\s+/g, ' ');
  if (!customerId || !callId) {
    return;
  }
  if (displayName.length < 2) {
    state.liveCalls.nameEditor.error = 'Введите имя клиента минимум из 2 символов.';
    updateLiveCallNameEditorsUi();
    return;
  }
  state.liveCalls.nameEditor.saving = true;
  state.liveCalls.nameEditor.error = '';
  updateLiveCallNameEditorsUi();
  try {
    const savedName = await persistCustomerDisplayName({ customerId, callId, displayName });
    const data = { display_name: savedName };
    closeLiveCallNameEditor();
    log('Имя клиента обновлено оператором', { customer_id: customerId, call_id: callId, name: data?.display_name || displayName });
  } catch (err) {
    state.liveCalls.nameEditor.saving = false;
    state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
    updateLiveCallNameEditorsUi();
  }
}

async function saveCustomerProfileName() {
  const customerId = state.customers.nameEditor.customerId || state.customers.selectedCustomerId;
  const displayName = String(state.customers.nameEditor.draft || '').trim().replace(/\s+/g, ' ');
  if (!customerId) {
    return;
  }
  if (displayName.length < 2) {
    state.customers.nameEditor.error = 'Введите имя клиента минимум из 2 символов.';
    renderCustomerSpotlight();
    return;
  }
  const currentName = String(selectedCustomer()?.display_name || '').trim();
  if (displayName === currentName) {
    closeCustomerProfileNameEditor();
    return;
  }
  state.customers.nameEditor.saving = true;
  state.customers.nameEditor.error = '';
  renderCustomerSpotlight();
  try {
    const savedName = await persistCustomerDisplayName({ customerId, displayName });
    state.customers.nameEditor.flash = 'Сохранено';
    closeCustomerProfileNameEditor();
    log('Имя клиента обновлено из профиля', { customer_id: customerId, name: savedName });
  } catch (err) {
    state.customers.nameEditor.saving = false;
    state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
    renderCustomerSpotlight();
  }
}

function liveCallNameEditorMeta(item, customerId) {
  if (!item || !customerId) {
    return 'Выберите звонок, связанный с клиентом, чтобы исправить имя.';
  }
  return `${voiceCustomerCallSubtitle(item)} • клиент ${customerId}`;
}

function updateLiveCallNameEditorsUi() {
  const panel = $('liveCallNameEditor');
  const panelInput = $('liveCallNameInput');
  const panelMeta = $('liveCallNameEditorMeta');
  const panelStatus = $('liveCallNameStatus');
  const panelSave = $('liveCallNameSaveBtn');
  const panelCancel = $('liveCallNameCancelBtn');
  const popup = $('browserPhoneNameEditor');
  const popupInput = $('browserPhoneNameInput');
  const popupMeta = $('browserPhoneNameHint');
  const popupStatus = $('browserPhoneNameStatus');
  const popupSave = $('browserPhoneNameSaveBtn');
  const popupCancel = $('browserPhoneNameCancelBtn');
  const editor = state.liveCalls.nameEditor;
  const item = liveCallById(editor.callId);
  const meta = liveCallNameEditorMeta(item, editor.customerId);
  const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : '');

  if (panel) {
    const panelVisible = editor.open && editor.mode === 'panel';
    panel.classList.toggle('hidden', !panelVisible);
    if (panelInput) {
      if (panelInput.value !== editor.draft) {
        panelInput.value = editor.draft;
      }
      panelInput.disabled = editor.saving;
    }
    if (panelMeta) {
      panelMeta.textContent = meta;
    }
    if (panelStatus) {
      panelStatus.textContent = statusText;
      panelStatus.classList.toggle('error', Boolean(editor.error));
    }
    if (panelSave) {
      panelSave.disabled = editor.saving || !editor.customerId;
      panelSave.textContent = editor.saving ? 'Сохраняем...' : 'Сохранить имя';
    }
    if (panelCancel) {
      panelCancel.disabled = editor.saving;
    }
  }

  if (popup) {
    const popupVisible = editor.open && editor.mode === 'popup';
    popup.classList.toggle('hidden', !popupVisible);
    if (popupInput) {
      if (popupInput.value !== editor.draft) {
        popupInput.value = editor.draft;
      }
      popupInput.disabled = editor.saving;
    }
    if (popupMeta) {
      popupMeta.textContent = meta;
    }
    if (popupStatus) {
      popupStatus.textContent = statusText;
      popupStatus.classList.toggle('hidden', !statusText);
      popupStatus.classList.toggle('error', Boolean(editor.error));
    }
    if (popupSave) {
      popupSave.disabled = editor.saving || !editor.customerId;
      popupSave.textContent = editor.saving ? 'Сохраняем...' : 'Сохранить имя';
    }
    if (popupCancel) {
      popupCancel.disabled = editor.saving;
    }
  }
}

function isClaimableLiveCall(item) {
  if (!item) {
    return false;
  }
  return !item.claimed_by_user && ['ringing', 'claimed', 'connected'].includes(item.telephony_status);
}

function canControlLiveCall(item) {
  if (!item) {
    return false;
  }
  return ['claimed', 'connected'].includes(item.telephony_status);
}

function terminalActionLabel(item) {
  const value = item?.terminal_action || '';
  if (value === 'blind-transfer') {
    return 'Передан';
  }
  if (value === 'hangup') {
    return 'Завершён оператором';
  }
  return 'Завершён';
}

function voiceAiStatusMeta(item) {
  const stateKey = String(item?.ai_state || '').trim();
  if (!stateKey) {
    return null;
  }
  return VOICE_AI_STATE_META[stateKey] || { label: stateKey, className: 'ai-muted' };
}

function liveCallHasAi(item) {
  return Boolean(
    item?.voice_session_id
    || item?.ai_session_id
    || item?.ai_state
    || item?.ai_handoff_reason
    || item?.ai_last_model_at,
  );
}

function shouldLoadVoiceAiSummary(item) {
  if (!item || !liveCallHasAi(item)) {
    return false;
  }
  return ['handoff_requested', 'handoff_required', 'human_owned', 'error', 'closed'].includes(item.ai_state || '')
    || Boolean(item.ai_handoff_reason);
}

function voiceAiSummaryForCall(callId) {
  return state.liveCalls.aiSummaries[callId] || null;
}

function voiceAiSummaryPending(callId) {
  return Boolean(state.liveCalls.aiSummaryPending[callId]);
}

async function loadVoiceAiSummary(callId, options = {}) {
  if (!callId || voiceAiSummaryPending(callId)) {
    return voiceAiSummaryForCall(callId);
  }
  const { force = false, silent = false } = options;
  if (!force && Object.prototype.hasOwnProperty.call(state.liveCalls.aiSummaries, callId)) {
    return state.liveCalls.aiSummaries[callId];
  }
  state.liveCalls.aiSummaryPending[callId] = true;
  try {
    const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/ai-summary`);
    state.liveCalls.aiSummaries[callId] = data || null;
    return state.liveCalls.aiSummaries[callId];
  } catch (err) {
    if (!silent) {
      log('Не удалось загрузить сводку AI по звонку', { call_id: callId, error: err.message });
    }
    state.liveCalls.aiSummaries[callId] = null;
    return null;
  } finally {
    delete state.liveCalls.aiSummaryPending[callId];
    refreshVoiceSummaryDependentViews();
  }
}

function ensureVoiceAiSummaryLoaded(item) {
  if (!shouldLoadVoiceAiSummary(item)) {
    return;
  }
  const callId = item.call_id || '';
  if (!callId || voiceAiSummaryPending(callId) || Object.prototype.hasOwnProperty.call(state.liveCalls.aiSummaries, callId)) {
    return;
  }
  window.setTimeout(() => {
    loadVoiceAiSummary(callId, { silent: true });
  }, 0);
}

function renderVoiceAiSummaryField(label, value) {
  if (!value) {
    return '';
  }
  return `
    <div class="voice-summary-field">
      <div class="voice-summary-label">${escapeHtml(label)}</div>
      <div class="voice-summary-value">${escapeHtml(value)}</div>
    </div>
  `;
}

function formatVoiceCustomerNameStatus(status) {
  switch (String(status || '').trim()) {
    case 'name_obtained':
      return 'Подтверждено';
    case 'name_followup_required':
      return 'Нужно уточнить';
    case 'name_not_obtained':
      return 'Не подтверждено';
    default:
      return String(status || '').trim();
  }
}

function voiceCustomerNameStatusMeta(status) {
  switch (String(status || '').trim()) {
    case 'name_obtained':
      return { label: 'Имя подтверждено', shortLabel: 'Имя подтверждено', className: 'name-confirmed' };
    case 'name_followup_required':
      return { label: 'Имя нужно уточнить', shortLabel: 'Уточнить имя', className: 'name-followup' };
    case 'name_not_obtained':
      return { label: 'Имя не подтверждено', shortLabel: 'Без подтверждения', className: 'name-missing' };
    default: {
      const fallback = String(status || '').trim();
      return fallback ? { label: fallback, shortLabel: fallback, className: 'name-missing' } : null;
    }
  }
}

function formatVoiceCustomerNameSource(source) {
  switch (String(source || '').trim()) {
    case 'known_customer':
      return 'Из карточки клиента';
    case 'voice_start':
      return 'Стартовый этап';
    case 'voice_followup':
      return 'Уточнил AI';
    case 'manual':
      return 'Оператор';
    case 'external_identity':
      return 'Из voice identity';
    default:
      return String(source || '').trim();
  }
}

function formatVoiceStartLanguage(language) {
  switch (String(language || '').trim()) {
    case 'ru':
      return 'Русский';
    case 'kz':
      return 'Қазақша';
    default:
      return String(language || '').trim();
  }
}

function voiceSummaryForItem(item) {
  const callId = String(item?.call_id || '').trim();
  return callId ? voiceAiSummaryForCall(callId) : null;
}

function voiceCustomerDisplayName(item, summary = voiceSummaryForItem(item)) {
  const preferredName = String(summary?.customer_name_value || '').trim();
  if (preferredName) {
    return preferredName;
  }
  const callerName = String(item?.caller_name || '').trim();
  if (callerName) {
    return callerName;
  }
  const callerNumber = String(item?.caller_number || '').trim();
  if (callerNumber) {
    return callerNumber;
  }
  const callId = String(item?.call_id || '').trim();
  if (callId) {
    return callId;
  }
  return 'Неизвестный абонент';
}

function voiceCustomerCallSubtitle(item) {
  const parts = [
    String(item?.caller_number || '').trim(),
    String(item?.call_id || '').trim(),
  ].filter(Boolean);
  return parts.join(' • ') || 'Звонок без номера';
}

function voiceCustomerNameStateLine(summary) {
  if (!summary) {
    return '';
  }
  const parts = [];
  const statusMeta = voiceCustomerNameStatusMeta(summary.customer_name_status);
  if (statusMeta?.label) {
    parts.push(statusMeta.label);
  }
  const sourceLabel = formatVoiceCustomerNameSource(summary.customer_name_source);
  if (sourceLabel) {
    parts.push(sourceLabel);
  }
  const languageLabel = formatVoiceStartLanguage(summary.voice_start_language);
  if (languageLabel) {
    parts.push(languageLabel);
  }
  return parts.join(' • ');
}

function voiceCustomerIncomingMeta(item, summary = voiceSummaryForItem(item)) {
  const parts = [];
  const callerNumber = String(item?.caller_number || '').trim();
  if (callerNumber) {
    parts.push(`Номер: ${callerNumber}`);
  }
  const nameState = voiceCustomerNameStateLine(summary);
  if (nameState) {
    parts.push(nameState);
  }
  return parts.join(' • ') || `call_id: ${String(item?.call_id || '—').trim() || '—'}`;
}

function refreshVoiceSummaryDependentViews() {
  updateLiveCallSelector(state.liveCalls.items);
  renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems);
  renderUnifiedInbox();
  updateBrowserPhoneUi();
}

function renderVoiceAiTranscript(summary) {
  const segments = Array.isArray(summary?.transcript_segments)
    ? summary.transcript_segments.filter((segment) => segment && String(segment.text || '').trim())
    : [];
  if (!segments.length) {
    return '';
  }
  return `
    <div class="voice-summary-transcript">
      <div class="voice-summary-transcript-head">
        <div class="voice-summary-label">Транскрипт разговора</div>
        <div class="voice-summary-transcript-meta">${escapeHtml(`${segments.length} реплик`)}</div>
      </div>
      <div class="voice-summary-transcript-list">
        ${segments
          .map((segment) => {
            const speaker = segment?.speaker === 'assistant' ? 'AI' : 'Клиент';
            const lineClass = segment?.speaker === 'assistant' ? 'assistant' : 'caller';
            const timeLabel = formatIsoShort(segment?.created_at || '');
            const interrupted = segment?.interrupted && segment?.speaker === 'assistant' ? ' • прерван' : '';
            return `
              <div class="voice-transcript-line ${lineClass}">
                <div class="voice-transcript-speaker-row">
                  <div class="voice-transcript-speaker">${escapeHtml(`${speaker}${interrupted}`)}</div>
                  <div class="voice-transcript-time">${escapeHtml(timeLabel)}</div>
                </div>
                <div class="voice-transcript-text">${escapeHtml(segment?.text || '')}</div>
              </div>
            `;
          })
          .join('')}
      </div>
    </div>
  `;
}

function renderVoiceAiSummary(summary, pending = false) {
  if (!summary && !pending) {
    return '';
  }
  if (pending && !summary) {
    return `
      <section class="voice-ai-summary voice-ai-summary-loading">
        <div class="voice-ai-summary-head">
          <div>
            <div class="voice-ai-summary-title">Сводка AI</div>
            <div class="voice-ai-summary-caption">Подтягиваем контекст AI по текущему звонку...</div>
          </div>
        </div>
      </section>
    `;
  }
  const statusClass = summary?.status_tone === 'handoff' ? 'handoff' : 'answered';
  return `
    <section class="voice-ai-summary">
      <div class="voice-ai-summary-head">
        <div>
          <div class="voice-ai-summary-title">Сводка AI</div>
          <div class="voice-ai-summary-caption">Краткий контекст передачи до ручной обработки звонка.</div>
        </div>
        <div class="voice-ai-summary-side">
          <div class="voice-summary-status ${statusClass}">${escapeHtml(summary?.status_label || 'AI-сводка')}</div>
          <div class="voice-ai-summary-meta">${escapeHtml(formatIsoShort(summary?.generated_at || ''))}</div>
        </div>
      </div>
      <div class="voice-ai-summary-grid">
        ${renderVoiceAiSummaryField('Имя клиента', summary?.customer_name_value)}
        ${renderVoiceAiSummaryField('Статус имени', formatVoiceCustomerNameStatus(summary?.customer_name_status))}
        ${renderVoiceAiSummaryField('Источник имени', formatVoiceCustomerNameSource(summary?.customer_name_source))}
        ${renderVoiceAiSummaryField('Язык старта', formatVoiceStartLanguage(summary?.voice_start_language))}
        ${renderVoiceAiSummaryField('Запрос клиента', summary?.customer_request_text)}
        ${renderVoiceAiSummaryField('Что сделал AI', summary?.ai_outcome_text)}
        ${renderVoiceAiSummaryField('Причина передачи', summary?.handoff_reason)}
        ${renderVoiceAiSummaryField('Следующий шаг', summary?.recommended_next_step)}
      </div>
      ${renderVoiceAiTranscript(summary)}
    </section>
  `;
}

function describeLiveCallError(message) {
  const text = String(message || '').trim();
  const lower = text.toLowerCase();
  if (!text) {
    return 'Операция по звонку не выполнена.';
  }
  if (lower.includes('already ended') || lower.includes('channel_already_closed')) {
    return 'Звонок уже завершён.';
  }
  if (lower.includes('already claimed by another operator')) {
    return 'Звонок уже закреплён другим оператором.';
  }
  if (lower.includes('unable to resolve active channel')) {
    return 'Asterisk не смог найти активный канал звонка.';
  }
  if (lower.includes('operator_extension is required')) {
    return 'Укажите внутренний номер оператора для этого действия.';
  }
  if (lower.includes('target extension')) {
    return 'Указанный target недоступен для передачи.';
  }
  if (lower.includes('blind transfer failed')) {
    return 'Передача звонка не выполнена.';
  }
  if (lower.includes('hangup failed')) {
    return 'Не удалось завершить звонок.';
  }
  if (lower.includes('claim failed')) {
    return 'Не удалось принять звонок в работу.';
  }
  return text;
}

function browserPhoneConfig() {
  return state.browserPhone.config || null;
}

function browserPhoneLibraryAvailable() {
  return Boolean(window.SIP && window.SIP.UserAgent);
}

function browserPhoneAudioElement() {
  return $('browserPhoneRemoteAudio');
}

function browserPhoneAudioConstraints() {
  if (state.browserPhone.micDeviceId) {
    return { audio: { deviceId: { exact: state.browserPhone.micDeviceId } }, video: false };
  }
  return { audio: true, video: false };
}

function browserPhoneIncomingLabel(session) {
  const remote = session?.remoteIdentity;
  const name = remote?.displayName || remote?.uri?.user || remote?.uri?.toString?.() || 'неизвестно';
  return `Входящий звонок: ${name}`;
}

function browserPhoneAudioContext() {
  if (state.browserPhone.ringtoneContext) {
    return state.browserPhone.ringtoneContext;
  }
  const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
  if (!AudioContextCtor) {
    return null;
  }
  state.browserPhone.ringtoneContext = new AudioContextCtor();
  return state.browserPhone.ringtoneContext;
}

async function ensureBrowserPhoneAudioContextResumed() {
  const context = browserPhoneAudioContext();
  if (!context) {
    return null;
  }
  if (context.state === 'suspended') {
    await context.resume().catch(() => {});
  }
  return context;
}

function playBrowserPhoneRingtoneBurst(context) {
  if (!context) {
    return;
  }
  const tones = [
    { offset: 0, duration: 0.18, frequency: 880 },
    { offset: 0.24, duration: 0.18, frequency: 660 },
  ];
  tones.forEach((tone) => {
    const oscillator = context.createOscillator();
    const gain = context.createGain();
    const startAt = context.currentTime + tone.offset;
    const stopAt = startAt + tone.duration;
    oscillator.type = 'sine';
    oscillator.frequency.setValueAtTime(tone.frequency, startAt);
    gain.gain.setValueAtTime(0.0001, startAt);
    gain.gain.exponentialRampToValueAtTime(0.14, startAt + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
    oscillator.connect(gain);
    gain.connect(context.destination);
    oscillator.start(startAt);
    oscillator.stop(stopAt + 0.02);
  });
}

async function startBrowserPhoneRingtone() {
  if (state.browserPhone.ringtoneTimer) {
    return;
  }
  const context = await ensureBrowserPhoneAudioContextResumed();
  if (!context) {
    return;
  }
  state.browserPhone.ringtoneActive = true;
  playBrowserPhoneRingtoneBurst(context);
  state.browserPhone.ringtoneTimer = window.setInterval(() => {
    playBrowserPhoneRingtoneBurst(context);
  }, 2200);
  updateBrowserPhoneUi();
}

function stopBrowserPhoneRingtone() {
  if (state.browserPhone.ringtoneTimer) {
    window.clearInterval(state.browserPhone.ringtoneTimer);
    state.browserPhone.ringtoneTimer = null;
  }
  if (state.browserPhone.ringtoneActive) {
    state.browserPhone.ringtoneActive = false;
    updateBrowserPhoneUi();
  }
}

function syncBrowserPhoneSpeakerSink() {
  const element = browserPhoneAudioElement();
  if (!element || !state.browserPhone.speakerDeviceId) {
    return;
  }
  if (typeof element.setSinkId !== 'function') {
    return;
  }
  element.setSinkId(state.browserPhone.speakerDeviceId).catch(() => {});
}

function setBrowserPhoneStatus(text) {
  state.browserPhone.status = text;
  updateBrowserPhoneUi();
}

function setBrowserPhoneWarning(text = '') {
  state.browserPhone.warning = text;
  updateBrowserPhoneUi();
}

function toggleBrowserPhoneSettings() {
  state.browserPhone.settingsOpen = !state.browserPhone.settingsOpen;
  updateBrowserPhoneUi();
}

function browserPhoneStatusState() {
  const config = browserPhoneConfig();
  const hasSession = Boolean(state.browserPhone.session);
  const popupPhase = browserPhonePopupPhase();
  if (!config) {
    return 'unavailable';
  }
  if (!config.enabled) {
    return 'disabled';
  }
  if (state.browserPhone.connecting && popupPhase === 'idle') {
    return 'connecting';
  }
  if (popupPhase === 'error') {
    return 'error';
  }
  if (popupPhase === 'ending') {
    return 'ending';
  }
  if (popupPhase === 'incoming') {
    return 'incoming';
  }
  if (popupPhase === 'connecting') {
    return 'connecting';
  }
  if (popupPhase === 'in-call' || hasSession) {
    return 'active';
  }
  if (state.browserPhone.connected) {
    return 'registered';
  }
  return 'offline';
}

function browserPhoneStatusLabel() {
  const labels = {
    unavailable: 'Недоступен',
    disabled: 'Отключен',
    connecting: 'Подключение',
    ending: 'Завершаем',
    error: 'Нужно завершить',
    incoming: 'Входящий звонок',
    active: 'В разговоре',
    registered: 'Зарегистрирован',
    offline: 'Не подключен',
  };
  return labels[browserPhoneStatusState()] || state.browserPhone.status || 'Не подключен';
}

function browserPhonePopupPhase() {
  if (state.browserPhone.callPhase === 'error') {
    return 'error';
  }
  if (state.browserPhone.endingCallId || state.browserPhone.callPhase === 'ending') {
    return 'ending';
  }
  if (state.browserPhone.callPhase === 'connecting') {
    return 'connecting';
  }
  if (state.browserPhone.incoming || state.browserPhone.callPhase === 'incoming') {
    return 'incoming';
  }
  if (state.browserPhone.callPhase === 'in-call' || state.browserPhone.session) {
    return 'in-call';
  }
  return 'idle';
}

function browserPhoneCallById(callId, options = {}) {
  if (!callId) {
    return null;
  }
  const { includeRecent = true } = options;
  const live = state.liveCalls.items.find((item) => item.call_id === callId);
  if (live) {
    return live;
  }
  if (!includeRecent) {
    return null;
  }
  return state.liveCalls.recentItems.find((item) => item.call_id === callId) || null;
}

function browserPhoneActivePopupCall() {
  const trackedIds = [
    state.browserPhone.endingCallId,
    state.browserPhone.popupCallId,
    state.browserPhone.autoClaimCallId,
  ].filter(Boolean);
  for (const callId of trackedIds) {
    const found = browserPhoneCallById(callId);
    if (found) {
      return found;
    }
  }
  const owned = state.liveCalls.items.find((item) => item.claimed_by_user === state.user);
  if (owned) {
    return owned;
  }
  const candidates = browserPhoneClaimCandidates();
  if (candidates.length === 1) {
    return candidates[0];
  }
  const selected = selectedLiveCallItem();
  if (selected) {
    return selected;
  }
  return null;
}

function browserPhoneCallTimerSource(item) {
  return item?.connected_at || item?.started_at || state.browserPhone.sessionStartedAt || '';
}

function formatCallTimer(value) {
  if (!value) {
    return '00:00';
  }
  const startedAt = new Date(value);
  if (Number.isNaN(startedAt.getTime())) {
    return '00:00';
  }
  const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt.getTime()) / 1000));
  const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
  const seconds = String(totalSeconds % 60).padStart(2, '0');
  return `${minutes}:${seconds}`;
}

function browserPhonePopupVisible() {
  return browserPhonePopupPhase() !== 'idle';
}

function browserPhoneCallSummary(item) {
  if (!item) {
    return 'Ожидаем карточку звонка из bridge...';
  }
  const summary = voiceSummaryForItem(item);
  const aiMeta = voiceAiStatusMeta(item);
  const nameState = voiceCustomerNameStateLine(summary);
  const parts = [
    item.queue_code || item.queue_id || 'queue?',
    item.interaction_id || 'interaction?',
    item.operator_extension ? `ext ${item.operator_extension}` : '',
    nameState,
    aiMeta ? aiMeta.label : '',
  ].filter(Boolean);
  return parts.join(' • ');
}

function browserPhoneHintText(config) {
  const popupPhase = browserPhonePopupPhase();
  if (!config) {
    return 'Browser softphone недоступен: bridge не вернул конфиг для текущего пользователя.';
  }
  if (!config.enabled) {
    return 'Browser softphone отключён в окружении. Внешний SIP-клиент остаётся fallback-путём.';
  }
  if (!browserPhoneLibraryAvailable()) {
    return 'Browser softphone не инициализирован: SIP.js asset не загрузился.';
  }
  if (popupPhase === 'incoming' && state.browserPhone.ringtoneActive) {
  return 'Входящий звонок в браузере. Используйте Ответить или Отклонить. Рингтон воспроизводится локально.';
  }
  if (popupPhase === 'connecting') {
    return 'Подключаем медиа и быстро сопоставляем звонок с bridge. При неоднозначности можно нажать «Принять в работу» вручную.';
  }
  if (popupPhase === 'ending') {
    return 'Ждём финальное подтверждение завершения звонка от bridge.';
  }
  if (popupPhase === 'error') {
    return 'Локальная media session уже завершена, но bridge ещё не подтвердил финальный terminal state. Проверьте предупреждение в popup.';
  }
  if (state.browserPhone.connected) {
    return 'Телефон зарегистрирован. Входящий браузерный звонок откроет отдельное окно и попытается выполнить авто-claim.';
  }
  return 'Подключите browser phone и разрешите доступ к микрофону.';
}

function syncBrowserPhonePopupLifecycle() {
  const activeCallId = state.browserPhone.endingCallId || state.browserPhone.popupCallId || state.browserPhone.autoClaimCallId;
  if (!activeCallId) {
    updateBrowserPhoneUi();
    return;
  }
  const liveCall = state.liveCalls.items.find((item) => item.call_id === activeCallId);
  const recentCall = state.liveCalls.recentItems.find((item) => item.call_id === activeCallId);
  if (!liveCall && recentCall && !state.browserPhone.session && !state.browserPhone.incoming) {
    state.browserPhone.endingCallId = '';
    state.browserPhone.popupCallId = '';
    state.browserPhone.autoClaimCallId = '';
    state.browserPhone.callPhase = '';
    state.browserPhone.warning = '';
    if (state.browserPhone.connected) {
      state.browserPhone.status = 'Зарегистрирован';
    }
  }
  updateBrowserPhoneUi();
}

function updateBrowserPhoneUi() {
  const config = browserPhoneConfig();
  const hasSession = Boolean(state.browserPhone.session);
  const pendingAction = state.liveCalls.pendingAction || '';
  const popupCall = browserPhoneActivePopupCall();
  const popupPhase = browserPhonePopupPhase();
  const phoneState = browserPhoneStatusState();
  const connectBtn = $('browserPhoneConnectBtn');
  const disconnectBtn = $('browserPhoneDisconnectBtn');
  const micSelect = $('browserPhoneMicSelect');
  const speakerSelect = $('browserPhoneSpeakerSelect');
  const settingsPanel = $('browserPhoneSettingsPanel');
  const statusBtn = $('browserPhoneStatusBtn');
  const statusLabel = $('browserPhoneStatusLabel');
  const hint = $('browserPhoneHint');
  const muteBtn = $('browserPhoneMuteBtn');
  const answerBtn = $('browserPhoneAnswerBtn');
  const rejectBtn = $('browserPhoneRejectBtn');
  const claimBtn = $('browserPhoneCallClaimBtn');
  const editNameBtn = $('browserPhoneEditNameBtn');
  const transferBtn = $('browserPhoneCallTransferBtn');
  const hangupBtn = $('browserPhoneCallHangupBtn');
  const transferType = $('browserPhoneTransferTargetType');
  const transferValue = $('browserPhoneTransferTargetValue');
  const overlay = $('browserPhoneCallOverlay');
  const popupState = $('browserPhoneCallState');
  const popupTitle = $('browserPhoneCallTitle');
  const popupIncomingText = $('browserPhoneIncomingText');
  const popupMeta = $('browserPhoneCallMeta');
  const popupAiSummary = $('browserPhoneAiSummary');
  const popupTimer = $('browserPhoneCallTimer');
  const popupWarning = $('browserPhoneCallWarning');
  const popupAiSummaryData = popupCall ? voiceAiSummaryForCall(popupCall.call_id) : null;
  const popupAiSummaryPending = popupCall ? voiceAiSummaryPending(popupCall.call_id) : false;

  $('browserPhoneRegistrationState').textContent = state.browserPhone.status;
  $('browserPhoneOperatorExtension').textContent = config?.operator_extension || '—';
  $('browserPhoneWsUrl').textContent = config?.ws_url || '—';
  statusLabel.textContent = browserPhoneStatusLabel();
  statusBtn.dataset.state = phoneState;
  statusBtn.setAttribute('aria-expanded', state.browserPhone.settingsOpen ? 'true' : 'false');
  settingsPanel.classList.toggle('hidden', !state.browserPhone.settingsOpen);

  connectBtn.disabled = !config?.enabled || state.browserPhone.connected || state.browserPhone.connecting;
  disconnectBtn.disabled = !state.browserPhone.connected && !hasSession && !state.browserPhone.connecting;
  micSelect.disabled = state.browserPhone.connecting;
  speakerSelect.disabled = state.browserPhone.connecting;
  hint.textContent = browserPhoneHintText(config);

  const incoming = popupPhase === 'incoming';
  const connecting = popupPhase === 'connecting';
  const ending = popupPhase === 'ending';
  const errorState = popupPhase === 'error';
  const active = popupPhase === 'in-call';
  const canClaim = Boolean(popupCall && isClaimableLiveCall(popupCall) && !incoming && !connecting && !ending && !state.browserPhone.autoClaimInFlight);
  const canControl = Boolean(popupCall && canControlLiveCall(popupCall) && !connecting && !ending);
  const popupCustomerId = liveCallCustomerId(popupCall);

  overlay.classList.toggle('hidden', !browserPhonePopupVisible());
  popupState.textContent = errorState
    ? 'Нужна синхронизация завершения'
    : ending
      ? 'Завершаем звонок'
      : connecting
        ? 'Подключаем media'
        : incoming
          ? 'Входящий звонок'
          : active
            ? 'Разговор в браузере'
            : 'Browser call';
  popupTitle.textContent = popupCall
    ? voiceCustomerDisplayName(popupCall, popupAiSummaryData)
    : browserPhoneIncomingLabel(state.browserPhone.session).replace('Входящий звонок: ', '');
  popupIncomingText.textContent = popupCall
    ? voiceCustomerIncomingMeta(popupCall, popupAiSummaryData)
    : state.browserPhone.session
      ? browserPhoneIncomingLabel(state.browserPhone.session)
      : 'SIP invite ещё не поступал.';
  popupMeta.textContent = browserPhoneCallSummary(popupCall);
  if (popupCall) {
    ensureVoiceAiSummaryLoaded(popupCall);
  }
  popupAiSummary.innerHTML = renderVoiceAiSummary(popupAiSummaryData, popupAiSummaryPending);
  popupAiSummary.classList.toggle('hidden', !popupAiSummary.innerHTML.trim());
  popupTimer.textContent = incoming
    ? 'ringing'
    : connecting
      ? 'connecting…'
      : ending
        ? 'ending…'
        : formatCallTimer(browserPhoneCallTimerSource(popupCall));
  popupWarning.textContent = state.browserPhone.warning;
  popupWarning.classList.toggle('hidden', !state.browserPhone.warning);

  answerBtn.classList.toggle('hidden', !incoming);
  rejectBtn.classList.toggle('hidden', !incoming);
  claimBtn.classList.toggle('hidden', !canClaim);
  editNameBtn.classList.toggle('hidden', !popupCall || !popupCustomerId);
  muteBtn.classList.toggle('hidden', !(active || connecting));
  transferBtn.classList.toggle('hidden', !(canControl || ending || errorState));
  hangupBtn.classList.toggle('hidden', !(popupCall || hasSession || ending || errorState || connecting));
  transferType.classList.toggle('hidden', !(canControl || ending || errorState));
  transferValue.classList.toggle('hidden', !(canControl || ending || errorState));

  answerBtn.disabled = !incoming || Boolean(pendingAction);
  rejectBtn.disabled = !incoming || Boolean(pendingAction);
  claimBtn.disabled = !canClaim || Boolean(pendingAction) || state.browserPhone.autoClaimInFlight;
  editNameBtn.disabled = !popupCall || !popupCustomerId || state.liveCalls.nameEditor.saving;
  muteBtn.disabled = !hasSession;
  muteBtn.textContent = state.browserPhone.muted ? 'Включить микрофон' : 'Выключить микрофон';
  transferBtn.disabled = !canControl || Boolean(pendingAction) || ending || connecting || !transferValue.value.trim();
  hangupBtn.disabled = (!popupCall && !hasSession) || Boolean(pendingAction) || ending;
  transferType.disabled = !canControl || Boolean(pendingAction) || connecting;
  transferValue.disabled = !canControl || Boolean(pendingAction) || connecting;
  if (state.liveCalls.nameEditor.mode === 'popup' && (!popupCall || state.liveCalls.nameEditor.callId !== popupCall.call_id)) {
    closeLiveCallNameEditor();
  } else {
    updateLiveCallNameEditorsUi();
  }
}

function renderBrowserDeviceOptions(selectId, devices, preferredId, placeholder) {
  const select = $(selectId);
  if (!select) {
    return;
  }
  const items = devices.length ? devices : [{ deviceId: '', label: placeholder }];
  select.innerHTML = items
    .map((item, index) => `<option value="${escapeHtml(item.deviceId || '')}">${escapeHtml(item.label || `${placeholder} ${index + 1}`)}</option>`)
    .join('');
  if (preferredId && items.some((item) => item.deviceId === preferredId)) {
    select.value = preferredId;
  } else if (!devices.length) {
    select.value = '';
  }
}

async function loadBrowserPhoneDevices() {
  if (!navigator.mediaDevices?.enumerateDevices) {
    renderBrowserDeviceOptions('browserPhoneMicSelect', [], '', 'Media devices not available');
    renderBrowserDeviceOptions('browserPhoneSpeakerSelect', [], '', 'Output selection not supported');
    return;
  }
  const devices = await navigator.mediaDevices.enumerateDevices();
  const inputs = devices.filter((item) => item.kind === 'audioinput');
  const outputs = devices.filter((item) => item.kind === 'audiooutput');
  renderBrowserDeviceOptions('browserPhoneMicSelect', inputs, state.browserPhone.micDeviceId, 'Микрофон');
  renderBrowserDeviceOptions('browserPhoneSpeakerSelect', outputs, state.browserPhone.speakerDeviceId, 'Динамик');
  if (!state.browserPhone.micDeviceId) {
    state.browserPhone.micDeviceId = $('browserPhoneMicSelect').value;
  }
  if (!state.browserPhone.speakerDeviceId) {
    state.browserPhone.speakerDeviceId = $('browserPhoneSpeakerSelect').value;
  }
  syncBrowserPhoneSpeakerSink();
}

async function ensureBrowserPhoneMediaAccess() {
  if (!navigator.mediaDevices?.getUserMedia) {
    throw new Error('Browser getUserMedia is not available');
  }
  const stream = await navigator.mediaDevices.getUserMedia(browserPhoneAudioConstraints());
  stream.getTracks().forEach((track) => track.stop());
}

function stopBrowserPhoneLocalStream() {
  if (state.browserPhone.localStream) {
    state.browserPhone.localStream.getTracks().forEach((track) => track.stop());
  }
  state.browserPhone.localStream = null;
}

function browserPhoneLocalStreamHealthy() {
  const stream = state.browserPhone.localStream;
  return Boolean(stream && stream.getTracks().some((track) => track.readyState === 'live'));
}

async function ensureBrowserPhoneLocalStream(options = {}) {
  const { forceFresh = false } = options;
  if (!navigator.mediaDevices?.getUserMedia) {
    throw new Error('Browser getUserMedia is not available');
  }
  if (forceFresh) {
    stopBrowserPhoneLocalStream();
  }
  if (browserPhoneLocalStreamHealthy()) {
    return state.browserPhone.localStream;
  }
  if (state.browserPhone.localStreamPromise) {
    return state.browserPhone.localStreamPromise;
  }
  state.browserPhone.localStreamPromise = navigator.mediaDevices
    .getUserMedia(browserPhoneAudioConstraints())
    .then((stream) => {
      stopBrowserPhoneLocalStream();
      state.browserPhone.localStream = stream;
      return stream;
    })
    .finally(() => {
      state.browserPhone.localStreamPromise = null;
    });
  return state.browserPhone.localStreamPromise;
}

function prepareBrowserPhoneAnswerMedia() {
  return ensureBrowserPhoneLocalStream().catch((err) => {
    log('Browser softphone не смог заранее подготовить media', { error: err.message });
    return null;
  });
}

function browserPhoneClaimCandidates() {
  return browserPhoneClaimCandidatesFrom(state.liveCalls.items);
}

function browserPhoneCallTimestampMs(item) {
  const raw = item?.last_transition_at || item?.connected_at || item?.started_at || item?.updated_at || '';
  const ts = raw ? new Date(raw).getTime() : Number.NaN;
  return Number.isFinite(ts) ? ts : Number.NaN;
}

function browserPhoneSessionTimestampMs() {
  const raw = state.browserPhone.sessionStartedAt || '';
  const ts = raw ? new Date(raw).getTime() : Number.NaN;
  return Number.isFinite(ts) ? ts : Number.NaN;
}

function browserPhoneClaimCandidatesFrom(items) {
  const config = browserPhoneConfig();
  const extension = config?.operator_extension || '';
  return (items || []).filter((item) => {
    if (!['ringing', 'claimed', 'connected'].includes(item.telephony_status)) {
      return false;
    }
    if (extension && item.operator_extension && item.operator_extension !== extension) {
      return false;
    }
    return true;
  });
}

function browserPhoneBestCandidateMatch(candidates) {
  if (!Array.isArray(candidates) || !candidates.length) {
    return null;
  }
  const sessionTs = browserPhoneSessionTimestampMs();
  const ranked = [...candidates].sort((left, right) => {
    const leftTs = browserPhoneCallTimestampMs(left);
    const rightTs = browserPhoneCallTimestampMs(right);
    if (Number.isFinite(sessionTs)) {
      const leftDelta = Number.isFinite(leftTs) ? Math.abs(leftTs - sessionTs) : Number.POSITIVE_INFINITY;
      const rightDelta = Number.isFinite(rightTs) ? Math.abs(rightTs - sessionTs) : Number.POSITIVE_INFINITY;
      if (leftDelta !== rightDelta) {
        return leftDelta - rightDelta;
      }
    }
    if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
      return rightTs - leftTs;
    }
    return String(right?.call_id || '').localeCompare(String(left?.call_id || ''));
  });
  return ranked[0] || null;
}

function attachBrowserRemoteAudio(session) {
  const element = browserPhoneAudioElement();
  const sdh = session?.sessionDescriptionHandler;
  const peerConnection = sdh?.peerConnection;
  if (!element || !peerConnection) {
    return;
  }

  const syncRemoteStream = () => {
    const stream = new MediaStream();
    peerConnection.getReceivers().forEach((receiver) => {
      if (receiver.track) {
        stream.addTrack(receiver.track);
      }
    });
    element.srcObject = stream;
    syncBrowserPhoneSpeakerSink();
    element.play().catch(() => {});
  };

  syncRemoteStream();
  peerConnection.addEventListener('track', syncRemoteStream);
}

function applyBrowserMuteState() {
  const sdh = state.browserPhone.session?.sessionDescriptionHandler;
  const peerConnection = sdh?.peerConnection;
  if (!peerConnection) {
    return;
  }
  peerConnection.getSenders().forEach((sender) => {
    if (sender.track && sender.track.kind === 'audio') {
      sender.track.enabled = !state.browserPhone.muted;
    }
  });
}

function resetBrowserPhoneSessionState() {
  stopBrowserPhoneRingtone();
  state.browserPhone.session = null;
  state.browserPhone.incoming = false;
  state.browserPhone.muted = false;
  state.browserPhone.autoClaimCallId = '';
  state.browserPhone.autoClaimInFlight = false;
  state.browserPhone.sessionStartedAt = '';
  state.browserPhone.callPhase = state.browserPhone.endingCallId ? 'ending' : '';
  stopBrowserPhoneLocalStream();
  const element = browserPhoneAudioElement();
  if (element) {
    element.srcObject = null;
  }
  updateBrowserPhoneUi();
}

function browserSessionStateValue(session = state.browserPhone.session) {
  const SessionState = window.SIP?.SessionState;
  if (!session || !SessionState) {
    return null;
  }
  return session.state;
}

function browserSessionIsActive(session = state.browserPhone.session) {
  const SessionState = window.SIP?.SessionState;
  if (!session || !SessionState) {
    return false;
  }
  return ![SessionState.Terminated].includes(session.state);
}

function ensureBrowserSessionFreshForInvite() {
  if (!state.browserPhone.session) {
    return true;
  }
  if (browserSessionIsActive(state.browserPhone.session)) {
    return false;
  }
  resetBrowserPhoneSessionState();
  return true;
}

function bindBrowserPhoneSession(session) {
  const SessionState = window.SIP?.SessionState;
  state.browserPhone.session = session;
  state.browserPhone.incoming = true;
  state.browserPhone.muted = false;
  state.browserPhone.popupCallId = '';
  state.browserPhone.callPhase = 'incoming';
  state.browserPhone.autoClaimInFlight = false;
  state.browserPhone.warning = '';
  setBrowserPhoneStatus('Входящий звонок');
  startBrowserPhoneRingtone().catch(() => {});
  prepareBrowserPhoneAnswerMedia();
  updateBrowserPhoneUi();
  session.stateChange.addListener((newState) => {
    if (newState === SessionState.Established) {
      stopBrowserPhoneRingtone();
      state.browserPhone.incoming = false;
      state.browserPhone.callPhase = 'in-call';
      state.browserPhone.sessionStartedAt = new Date().toISOString();
      setBrowserPhoneStatus('Соединено в браузере');
      attachBrowserRemoteAudio(session);
      applyBrowserMuteState();
      if (!state.browserPhone.autoClaimInFlight && !state.browserPhone.popupCallId && !state.browserPhone.autoClaimCallId) {
        void bestEffortBrowserAutoClaim({ refreshAttempts: 1, retryDelayMs: 150 });
      }
      return;
    }
    if (newState === SessionState.Terminated) {
      resetBrowserPhoneSessionState();
      if (state.browserPhone.connected && !state.browserPhone.endingCallId) {
        setBrowserPhoneStatus('Зарегистрирован');
      }
      syncBrowserPhonePopupLifecycle();
    }
  });
}

async function terminateBrowserPhoneSessionLocally() {
  const session = state.browserPhone.session;
  if (!session) {
    return;
  }
  try {
    const SessionState = window.SIP?.SessionState;
    if (state.browserPhone.incoming && typeof session.reject === 'function') {
      await session.reject();
      return;
    }
    if (session.state === SessionState.Established && typeof session.bye === 'function') {
      await session.bye();
      return;
    }
    if (typeof session.cancel === 'function') {
      await session.cancel();
      return;
    }
    if (typeof session.dispose === 'function') {
      session.dispose();
    }
  } catch {
    if (typeof session.dispose === 'function') {
      session.dispose();
    }
  }
}

async function claimLiveCallById(callId, options = {}) {
  const operatorExtension = (options.operatorExtension || browserPhoneConfig()?.operator_extension || '').trim();
  const payload = {};
  if (operatorExtension) {
    payload.operator_extension = operatorExtension;
  }
  const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/claim`, {
    method: 'POST',
    body: JSON.stringify(payload),
  });
  state.browserPhone.popupCallId = data.call_id;
  state.browserPhone.endingCallId = '';
  state.browserPhone.warning = '';
  applyClaimedLiveCall(data);
  refreshLiveCallsInBackground();
  return data;
}

async function hangupLiveCallById(callId, options = {}) {
  const { optimistic = true } = options;
  const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/hangup`, {
    method: 'POST',
    body: JSON.stringify({}),
  });
  if (optimistic) {
    moveLiveCallToRecent(data);
    refreshLiveCallsInBackground();
  } else {
    state.browserPhone.endingCallId = callId;
    updateBrowserPhoneUi();
    refreshLiveCallsInBackground();
  }
  return data;
}

async function bestEffortBrowserAutoClaim(options = {}) {
  return bestEffortBrowserAutoClaimWithOptions(options);
}

function browserPhoneAutoClaimSnapshot(items = state.liveCalls.items) {
  const byId = (callId) => {
    if (!callId) {
      return null;
    }
    return (items || []).find((item) => item.call_id === callId) || null;
  };

  const trackedIds = [
    state.browserPhone.popupCallId,
    state.browserPhone.autoClaimCallId,
    selectedLiveCallId(),
  ].filter(Boolean);

  for (const callId of trackedIds) {
    const found = byId(callId);
    if (!found) {
      continue;
    }
    if (found.claimed_by_user === state.user) {
      return { mode: 'owned', call: found };
    }
    if (isClaimableLiveCall(found)) {
      return { mode: 'claim', call: found };
    }
  }

  const candidates = browserPhoneClaimCandidatesFrom(items);
  const ownedCandidates = candidates.filter((item) => item.claimed_by_user === state.user);
  if (ownedCandidates.length === 1) {
    return { mode: 'owned', call: ownedCandidates[0] };
  }
  if (ownedCandidates.length > 1) {
    const bestOwned = browserPhoneBestCandidateMatch(ownedCandidates);
    if (bestOwned) {
      return { mode: 'owned', call: bestOwned };
    }
  }
  const unclaimedCandidates = candidates.filter((item) => !item.claimed_by_user);
  if (unclaimedCandidates.length === 1) {
    return { mode: 'claim', call: unclaimedCandidates[0] };
  }
  if (unclaimedCandidates.length > 1) {
    const bestUnclaimed = browserPhoneBestCandidateMatch(unclaimedCandidates);
    if (bestUnclaimed) {
      return { mode: 'claim', call: bestUnclaimed };
    }
  }
  return null;
}

async function bestEffortBrowserAutoClaimWithOptions(options = {}) {
  const { refreshAttempts = 2, retryDelayMs = 150 } = options;
  if (state.browserPhone.autoClaimInFlight) {
    return false;
  }
  state.browserPhone.autoClaimInFlight = true;
  updateBrowserPhoneUi();
  const config = browserPhoneConfig();
  const operatorExtension = config?.operator_extension || '';
  const handleMatch = async (match) => {
    if (!match?.call) {
      return false;
    }
    if (match.mode === 'owned') {
      state.browserPhone.autoClaimCallId = match.call.call_id;
      state.browserPhone.popupCallId = match.call.call_id;
      state.browserPhone.warning = '';
      return true;
    }
    if (match.mode === 'claim') {
      try {
        const claimed = await claimLiveCallById(match.call.call_id, { operatorExtension });
        state.browserPhone.autoClaimCallId = claimed.call_id;
        state.browserPhone.popupCallId = claimed.call_id;
        state.browserPhone.warning = '';
        log('Browser softphone выполнил auto-claim', { call_id: claimed.call_id, extension: operatorExtension || claimed.operator_extension });
        return true;
      } catch (err) {
        setBrowserPhoneWarning('Звонок соединён, но авто-claim не выполнился. Нажмите «Принять в работу».');
        log('Browser softphone не смог выполнить auto-claim', { error: describeLiveCallError(err.message) });
        return false;
      }
    }
    return false;
  };

  try {
    const localMatch = browserPhoneAutoClaimSnapshot();
    if (await handleMatch(localMatch)) {
      return true;
    }
    for (let attempt = 0; attempt < refreshAttempts; attempt += 1) {
      if (attempt > 0 && retryDelayMs > 0) {
        await new Promise((resolve) => window.setTimeout(resolve, retryDelayMs));
      }
      await loadLiveCalls(false, { preserveStateOnError: true });
      const refreshedMatch = browserPhoneAutoClaimSnapshot();
      if (await handleMatch(refreshedMatch)) {
        return true;
      }
    }
    setBrowserPhoneWarning('Не удалось однозначно сопоставить звонок. Нажмите «Принять в работу» вручную.');
    log('Browser softphone требует ручного claim', { candidates: browserPhoneClaimCandidates().length });
    return false;
  } finally {
    state.browserPhone.autoClaimInFlight = false;
    updateBrowserPhoneUi();
  }
}

async function fetchBrowserSoftphoneConfig() {
  try {
    const data = await api('asterisk-bridge', 'asterisk/browser-softphone/config');
    state.browserPhone.config = data;
    state.browserPhone.status = data.enabled ? 'Готов к подключению' : 'Отключён в окружении';
  } catch (err) {
    state.browserPhone.config = null;
    state.browserPhone.status = 'Не настроен';
    log('Browser softphone config недоступен', { error: describeLiveCallError(err.message) });
  } finally {
    updateBrowserPhoneUi();
  }
}

async function connectBrowserSoftphone() {
  if (state.browserPhone.connecting || state.browserPhone.connected) {
    return;
  }
  if (!browserPhoneLibraryAvailable()) {
    setBrowserPhoneStatus('SIP.js asset не загружен');
    return;
  }
  if (!browserPhoneConfig()) {
    await fetchBrowserSoftphoneConfig();
  }
  const config = browserPhoneConfig();
  if (!config?.enabled) {
    updateBrowserPhoneUi();
    return;
  }

  try {
    state.browserPhone.connecting = true;
    state.browserPhone.settingsOpen = false;
    state.browserPhone.warning = '';
    setBrowserPhoneStatus('Подключаем browser softphone...');
    await ensureBrowserPhoneMediaAccess();
    await ensureBrowserPhoneAudioContextResumed();
    await loadBrowserPhoneDevices();

    const uri = window.SIP.UserAgent.makeURI(config.sip_uri);
    if (!uri) {
      throw new Error('Invalid browser SIP URI');
    }

    const userAgent = new window.SIP.UserAgent({
      uri,
      authorizationUsername: config.authorization_username,
      authorizationPassword: config.password,
      displayName: config.display_name,
      transportOptions: { server: config.ws_url },
      sessionDescriptionHandlerFactoryOptions: {
        constraints: browserPhoneAudioConstraints(),
        peerConnectionConfiguration: {
          iceServers: Array.isArray(config.ice_servers) ? config.ice_servers : [],
          iceCandidatePoolSize: 4,
          bundlePolicy: 'max-bundle',
          rtcpMuxPolicy: 'require',
        },
      },
    });
    userAgent.delegate = {
      onInvite: (invitation) => {
        if (!ensureBrowserSessionFreshForInvite()) {
          invitation.reject().catch(() => {});
          log('Browser softphone отклонил входящий INVITE: предыдущая session ещё активна');
          return;
        }
        bindBrowserPhoneSession(invitation);
        log('Browser softphone получил входящий звонок', { remote: browserPhoneIncomingLabel(invitation) });
      },
    };
    const registerer = new window.SIP.Registerer(userAgent);
    await userAgent.start();
    await registerer.register();

    state.browserPhone.ua = userAgent;
    state.browserPhone.registerer = registerer;
    state.browserPhone.connected = true;
    setBrowserPhoneStatus('Зарегистрирован');
  } catch (err) {
    state.browserPhone.connected = false;
    state.browserPhone.ua = null;
    state.browserPhone.registerer = null;
    setBrowserPhoneStatus('Ошибка регистрации');
    log('Browser softphone не подключился', { error: err.message });
  } finally {
    state.browserPhone.connecting = false;
    updateBrowserPhoneUi();
  }
}

async function disconnectBrowserSoftphone() {
  try {
    stopBrowserPhoneRingtone();
    if (state.browserPhone.session) {
      await terminateBrowserPhoneSessionLocally();
    }
    if (state.browserPhone.registerer) {
      await state.browserPhone.registerer.unregister().catch(() => {});
    }
    if (state.browserPhone.ua) {
      await state.browserPhone.ua.stop().catch(() => {});
    }
  } finally {
    state.browserPhone.connected = false;
    state.browserPhone.connecting = false;
    state.browserPhone.settingsOpen = false;
    state.browserPhone.ua = null;
    state.browserPhone.registerer = null;
    state.browserPhone.popupCallId = '';
    state.browserPhone.endingCallId = '';
    state.browserPhone.callPhase = '';
    state.browserPhone.warning = '';
    stopBrowserPhoneLocalStream();
    resetBrowserPhoneSessionState();
    setBrowserPhoneStatus(browserPhoneConfig()?.enabled ? 'Отключён' : 'Не настроен');
  }
}

async function answerBrowserSoftphoneCall() {
  const session = state.browserPhone.session;
  if (!session || !state.browserPhone.incoming) {
    return;
  }
  try {
    stopBrowserPhoneRingtone();
    state.browserPhone.warning = '';
    state.browserPhone.incoming = false;
    state.browserPhone.callPhase = 'connecting';
    setBrowserPhoneStatus('Соединяем browser media...');
    updateBrowserPhoneUi();
    const localMediaStream = await ensureBrowserPhoneLocalStream();
    await session.accept({
      sessionDescriptionHandlerOptions: {
        constraints: browserPhoneAudioConstraints(),
        localMediaStream,
      },
    });
    const hasImmediateMatch = Boolean(browserPhoneAutoClaimSnapshot());
    void bestEffortBrowserAutoClaimWithOptions({
      refreshAttempts: hasImmediateMatch ? 0 : 2,
      retryDelayMs: 150,
    });
  } catch (err) {
    state.browserPhone.incoming = true;
    state.browserPhone.callPhase = 'incoming';
    startBrowserPhoneRingtone().catch(() => {});
    setBrowserPhoneWarning('Не удалось ответить на звонок в браузере.');
    setBrowserPhoneStatus('Входящий звонок');
    log('Ответ в браузерном телефоне не выполнен', { error: err.message });
  }
}

async function rejectBrowserSoftphoneCall() {
  if (!state.browserPhone.session || !state.browserPhone.incoming) {
    return;
  }
  stopBrowserPhoneRingtone();
  state.browserPhone.warning = '';
  await terminateBrowserPhoneSessionLocally();
  resetBrowserPhoneSessionState();
  state.browserPhone.callPhase = '';
  if (state.browserPhone.connected) {
    setBrowserPhoneStatus('Зарегистрирован');
  }
}

function toggleBrowserSoftphoneMute() {
  if (!state.browserPhone.session) {
    return;
  }
  state.browserPhone.muted = !state.browserPhone.muted;
  applyBrowserMuteState();
  updateBrowserPhoneUi();
}

function syncLiveCallActionButtons() {
  const loadActionsBtn = $('loadLiveCallActionsBtn');
  const refreshBtn = $('loadLiveCallsBtn');
  const buttons = [loadActionsBtn, refreshBtn].filter(Boolean);
  const hasCall = Boolean(selectedLiveCallId());
  const pendingAction = state.liveCalls.pendingAction || '';

  buttons.forEach((button) => {
    if (!button.dataset.defaultLabel) {
      button.dataset.defaultLabel = button.textContent;
    }
    button.textContent = button.dataset.defaultLabel;
  });

  loadActionsBtn.disabled = !hasCall || Boolean(pendingAction);
  refreshBtn.disabled = Boolean(pendingAction);
  updateBrowserPhoneUi();
}

async function runLiveCallAction(actionName, handler) {
  if (state.liveCalls.pendingAction) {
    log('Дождитесь завершения предыдущего действия по звонку');
    return null;
  }
  state.liveCalls.pendingAction = actionName;
  syncLiveCallActionButtons();
  try {
    return await handler();
  } finally {
    state.liveCalls.pendingAction = '';
    syncLiveCallActionButtons();
  }
}

async function claimBrowserPopupCall() {
  const popupCall = browserPhoneActivePopupCall();
  if (!popupCall) {
    setBrowserPhoneWarning('Не удалось найти активный звонок для claim.');
    return;
  }
  await runLiveCallAction('claim', async () => {
    try {
      const data = await claimLiveCallById(popupCall.call_id);
      state.browserPhone.popupCallId = data.call_id;
      state.browserPhone.warning = '';
      log('Звонок принят в работу', {
        call_id: data.call_id,
        claimed_by: data.claimed_by_user,
        extension: data.operator_extension,
      });
    } catch (err) {
      setBrowserPhoneWarning(describeLiveCallError(err.message));
      log('Не удалось принять звонок в работу', { call_id: popupCall.call_id, error: describeLiveCallError(err.message) });
    }
  });
}

async function hangupBrowserPopupCall() {
  const popupCall = browserPhoneActivePopupCall();
  if (!popupCall && !state.browserPhone.session) {
    setBrowserPhoneWarning('Не удалось найти активный звонок для завершения.');
    return;
  }
  await runLiveCallAction('hangup', async () => {
    const callId = popupCall?.call_id || state.browserPhone.popupCallId || state.browserPhone.autoClaimCallId || selectedLiveCallId();
    try {
      state.browserPhone.endingCallId = callId;
      state.browserPhone.callPhase = 'ending';
      state.browserPhone.warning = '';
      updateBrowserPhoneUi();
      if (state.browserPhone.session) {
        await terminateBrowserPhoneSessionLocally();
        resetBrowserPhoneSessionState();
        state.browserPhone.endingCallId = callId;
        state.browserPhone.popupCallId = callId;
        state.browserPhone.callPhase = 'ending';
        updateBrowserPhoneUi();
      }
      if (!callId) {
        throw new Error('hangup failed: call_id is missing');
      }
      await hangupLiveCallById(callId, { optimistic: false });
      log('Отправлена команда завершения звонка', { call_id: callId });
    } catch (err) {
      state.browserPhone.callPhase = 'error';
      state.browserPhone.endingCallId = callId || state.browserPhone.endingCallId;
      setBrowserPhoneWarning(describeLiveCallError(err.message));
      refreshLiveCallsInBackground();
      log('Не удалось завершить звонок', { call_id: callId || popupCall?.call_id || '', error: describeLiveCallError(err.message) });
    }
  });
}

async function transferBrowserPopupCall() {
  const popupCall = browserPhoneActivePopupCall();
  const targetType = $('browserPhoneTransferTargetType').value;
  const targetValue = $('browserPhoneTransferTargetValue').value.trim();
  if (!popupCall) {
    setBrowserPhoneWarning('Не удалось найти активный звонок для передачи.');
    return;
  }
  if (!targetValue) {
    setBrowserPhoneWarning('Укажите target для передачи звонка.');
    return;
  }
  await runLiveCallAction('transfer', async () => {
    try {
      const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(popupCall.call_id)}/blind-transfer`, {
        method: 'POST',
        body: JSON.stringify({ target_type: targetType, target_value: targetValue }),
      });
      if (state.browserPhone.session) {
        await terminateBrowserPhoneSessionLocally();
        resetBrowserPhoneSessionState();
      }
      state.browserPhone.popupCallId = '';
      state.browserPhone.endingCallId = '';
      state.browserPhone.warning = '';
      moveLiveCallToRecent(data);
      refreshLiveCallsInBackground();
      log('Blind transfer выполнен', {
        call_id: data.call_id,
        target_type: targetType,
        target_value: targetValue,
      });
    } catch (err) {
      setBrowserPhoneWarning(describeLiveCallError(err.message));
      log('Не удалось передать звонок', { call_id: popupCall.call_id, error: describeLiveCallError(err.message) });
    }
  });
}

function updateLiveCallSelector(items) {
  const select = $('liveCallIdSelect');
  const previous = state.liveCalls.selectedCallId || select.value || '';
  const sorted = [...items];
  select.innerHTML = sorted.length
    ? sorted
      .map((item) => {
        const summary = voiceSummaryForItem(item);
        const label = `${voiceCustomerDisplayName(item, summary)} | ${item.call_id}${item.interaction_id ? ` | ${item.interaction_id}` : ''}`;
        return `<option value="${escapeHtml(item.call_id)}">${escapeHtml(label)}</option>`;
      })
      .join('')
    : '<option value="">Нет активных звонков</option>';
  if (sorted.length) {
    const hasPrevious = sorted.some((item) => item.call_id === previous);
    const latestRinging = sorted.find((item) => item.telephony_status === 'ringing' && !item.claimed_by_user);
    if (latestRinging && latestRinging.call_id !== previous) {
      select.value = latestRinging.call_id;
    } else {
      select.value = hasPrevious ? previous : sorted[0].call_id;
    }
  } else {
    select.value = '';
  }
  state.liveCalls.selectedCallId = select.value;
  syncLiveCallActionButtons();
}

function renderLiveCallCard(item, { recent = false } = {}) {
  const isSelected = item.call_id === state.liveCalls.selectedCallId;
  const summary = voiceSummaryForItem(item);
  const caller = voiceCustomerDisplayName(item, summary);
  const aiMeta = voiceAiStatusMeta(item);
  const customerId = liveCallCustomerId(item);
  const nameStatusMeta = voiceCustomerNameStatusMeta(summary?.customer_name_status);
  const nameStatusLine = nameStatusMeta
    ? `<p class="card-meta-line">имя: ${escapeHtml(nameStatusMeta.label)}</p>`
    : '';
  const nameSourceLabel = formatVoiceCustomerNameSource(summary?.customer_name_source);
  const nameSourceLine = nameSourceLabel
    ? `<p class="card-meta-line">источник имени: ${escapeHtml(nameSourceLabel)}</p>`
    : '';
  const languageLabel = formatVoiceStartLanguage(summary?.voice_start_language);
  const languageLine = languageLabel
    ? `<p class="card-meta-line">язык старта: ${escapeHtml(languageLabel)}</p>`
    : '';
  const badges = [
    `<span class="micro-badge queue">${escapeHtml(item.queue_code || item.queue_id)}</span>`,
    `<span class="micro-badge assignee">${escapeHtml(telephonyLabel(item.telephony_status))}</span>`,
  ];
  if (aiMeta) {
    badges.push(`<span class="micro-badge ${escapeHtml(aiMeta.className)}">${escapeHtml(aiMeta.label)}</span>`);
  }
  if (nameStatusMeta) {
    badges.push(`<span class="micro-badge ${escapeHtml(nameStatusMeta.className)}">${escapeHtml(nameStatusMeta.shortLabel)}</span>`);
  }
  if (recent && item.terminal_action) {
    badges.push(`<span class="micro-badge terminal">${escapeHtml(terminalActionLabel(item))}</span>`);
  }
  if (!recent && item.claimed_by_user) {
    badges.push(`<span class="micro-badge owner">${escapeHtml(item.claimed_by_user)}</span>`);
  }
  const targetLine = recent && item.terminal_action === 'blind-transfer' && item.terminal_target
    ? `<p class="card-meta-line">цель: ${escapeHtml(item.terminal_target)}</p>`
    : '';
  const hangupLine = recent && item.hangup_cause
    ? `<p class="card-meta-line">завершение: ${escapeHtml(item.hangup_cause)}</p>`
    : '';
  const aiReasonLine = item.ai_handoff_reason
    ? `<p class="card-meta-line">AI: ${escapeHtml(item.ai_handoff_reason)}</p>`
    : '';
  const timingLine = recent
    ? `<p class="card-meta-line">завершён: ${escapeHtml(formatIsoShort(item.last_transition_at || item.ended_at || item.updated_at))}</p>`
    : `<p class="card-meta-line">соединён: ${escapeHtml(formatIsoShort(item.connected_at || item.started_at))}</p>`;
  const actions = [
    customerId ? `<button type="button" class="btn ghost" data-live-call-action="edit-name" data-call-id="${escapeHtml(item.call_id)}">Исправить имя</button>` : '',
    customerId ? `<button type="button" class="btn ghost" data-live-call-action="customer" data-call-id="${escapeHtml(item.call_id)}" data-customer-id="${escapeHtml(customerId)}">К клиенту</button>` : '',
  ].filter(Boolean);
  return `
    <article class="pipeline-card live-call-card ${recent ? 'closed' : ''} ${isSelected ? 'selected' : ''}">
      <div class="card-badges">${badges.join('')}</div>
      <h3 class="card-title">${escapeHtml(caller)}</h3>
      <p class="card-subtitle">${escapeHtml(voiceCustomerCallSubtitle(item))}</p>
      <p class="card-meta-line">обращение: ${escapeHtml(item.interaction_id || 'не найдено')}</p>
      <p class="card-meta-line">взял в работу: ${escapeHtml(item.claimed_by_user || '-')}</p>
      <p class="card-meta-line">внутренний номер: ${escapeHtml(item.operator_extension || '-')}</p>
      <p class="card-meta-line">начат: ${escapeHtml(formatIsoShort(item.started_at))}</p>
      ${timingLine}
      ${nameStatusLine}
      ${nameSourceLine}
      ${languageLine}
      ${aiReasonLine}
      ${targetLine}
      ${hangupLine}
      <p class="card-meta-line">запись: ${item.has_recording ? 'да' : 'нет'}</p>
      ${actions.length ? `<div class="live-call-card-actions">${actions.join('')}</div>` : ''}
    </article>
  `;
}

function renderLiveCallColumn(title, items, emptyMessage, options = {}) {
  const { recent = false } = options;
  return `
    <section class="pipeline-column ${recent ? 'pipeline-column-recent' : ''}">
      <div class="pipeline-head">
        <div class="pipeline-title">${escapeHtml(title)}</div>
        <div class="pipeline-count">${items.length}</div>
      </div>
      <div class="pipeline-stack">
        ${items.length ? items.map((item) => renderLiveCallCard(item, { recent })).join('') : `<div class="empty-state">${escapeHtml(emptyMessage)}</div>`}
      </div>
    </section>
  `;
}

function renderLiveCallsTable(activeItems, recentItems) {
  activeItems.forEach((item) => ensureVoiceAiSummaryLoaded(item));
  recentItems.forEach((item) => ensureVoiceAiSummaryLoaded(item));
  $('liveCallsTable').innerHTML = `
    <div class="pipeline-board live-calls-board">
      ${renderLiveCallColumn('Активные звонки', activeItems, 'Активных звонков нет.')}
      ${renderLiveCallColumn('Только что завершённые', recentItems, 'Недавних завершённых звонков нет.', { recent: true })}
    </div>
  `;

  if (!activeItems.length && !recentItems.length) {
    $('liveCallStatusHint').textContent = 'Активных звонков нет.';
    syncLiveCallActionButtons();
    return;
  }

  if (activeItems.length) {
    const first = activeItems[0];
    $('liveCallStatusHint').textContent = `Активных: ${activeItems.length}. Недавно завершённых: ${recentItems.length}. Последний активный: ${first.call_id} (${telephonyLabel(first.telephony_status)}).`;
  } else {
    const firstRecent = recentItems[0];
    $('liveCallStatusHint').textContent = `Активных нет. Недавно завершённых: ${recentItems.length}. Последний завершённый: ${firstRecent.call_id} (${terminalActionLabel(firstRecent)}).`;
  }
  syncLiveCallActionButtons();
}

function uniqueLiveCalls(items) {
  const seen = new Set();
  return (items || []).filter((item) => {
    const callId = item?.call_id;
    if (!callId || seen.has(callId)) {
      return false;
    }
    seen.add(callId);
    return true;
  });
}

function applyLiveCallCollections(activeItems, recentItems, options = {}) {
  const { selectedCallId = undefined } = options;
  state.liveCalls.items = uniqueLiveCalls(activeItems);
  state.liveCalls.recentItems = uniqueLiveCalls(recentItems);
  if (selectedCallId !== undefined) {
    state.liveCalls.selectedCallId = selectedCallId;
  }
  updateLiveCallSelector(state.liveCalls.items);
  renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems);
  updateLiveCallNameEditorsUi();
  syncBrowserPhonePopupLifecycle();
  renderCustomerSpotlight();
  renderUnifiedInbox();
}

function applyClaimedLiveCall(item) {
  const activeItems = [...state.liveCalls.items];
  const existingIndex = activeItems.findIndex((entry) => entry.call_id === item.call_id);
  if (existingIndex >= 0) {
    activeItems[existingIndex] = item;
  } else {
    activeItems.unshift(item);
  }
  const recentItems = state.liveCalls.recentItems.filter((entry) => entry.call_id !== item.call_id);
  applyLiveCallCollections(activeItems, recentItems, { selectedCallId: item.call_id });
}

function moveLiveCallToRecent(item) {
  const activeItems = state.liveCalls.items.filter((entry) => entry.call_id !== item.call_id);
  const recentItems = [item, ...state.liveCalls.recentItems.filter((entry) => entry.call_id !== item.call_id)];
  const nextSelectedCallId = state.liveCalls.selectedCallId === item.call_id
    ? (activeItems[0]?.call_id || '')
    : state.liveCalls.selectedCallId;
  applyLiveCallCollections(activeItems, recentItems, { selectedCallId: nextSelectedCallId });
}

function refreshLiveCallsInBackground() {
  window.setTimeout(() => {
    loadLiveCalls(false, { preserveStateOnError: true });
  }, 0);
}

async function loadLiveCalls(logResult = true, options = {}) {
  const { preserveStateOnError = false } = options;
  try {
    const [activeData, recentData] = await Promise.all([
      api('asterisk-bridge', 'asterisk/live-calls'),
      api('asterisk-bridge', 'asterisk/recent-calls'),
    ]);
    const items = Array.isArray(activeData) ? activeData : [];
    const recentItems = Array.isArray(recentData) ? recentData : [];
    applyLiveCallCollections(items, recentItems);
    if (logResult) {
      log('Живые звонки обновлены', { active: items.length, recent: recentItems.length });
    }
  } catch (err) {
    if (!preserveStateOnError) {
      applyLiveCallCollections([], [], { selectedCallId: '' });
      $('liveCallStatusHint').textContent = 'Сервис live-calls недоступен.';
    }
    if (logResult) {
      log('Не удалось загрузить звонки', { error: describeLiveCallError(err.message) });
    }
  } finally {
    syncLiveCallActionButtons();
  }
}

async function claimLiveCall() {
  await claimBrowserPopupCall();
}

async function hangupLiveCall() {
  await hangupBrowserPopupCall();
}

async function blindTransferLiveCall() {
  await transferBrowserPopupCall();
}

async function loadLiveCallActions() {
  const callId = selectedLiveCallId();
  if (!callId) {
    $('liveCallActionsOutput').textContent = 'Выберите звонок.';
    return;
  }
  try {
    const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/actions`);
    setOutput('liveCallActionsOutput', data);
    log('Загружен action log по звонку', { call_id: callId, actions: data.length });
  } catch (err) {
    $('liveCallActionsOutput').textContent = err.message;
    log('Не удалось загрузить action log', { call_id: callId, error: err.message });
  }
}

function startLiveCallsPolling() {
  if (state.liveCalls.pollTimer) {
    window.clearInterval(state.liveCalls.pollTimer);
    state.liveCalls.pollTimer = null;
  }
  state.liveCalls.pollTimer = window.setInterval(() => {
    loadLiveCalls(false);
  }, 3000);
}

async function loadInteractions() {
  try {
    const data = await api('interaction', 'interactions');
    state.interactions = Array.isArray(data) ? data : [];
    if (!data.length) {
      setEmptyBlock('interactionTable', 'Обращений пока нет. После подготовки демо они появятся автоматически.');
      renderCustomerSpotlight();
      renderUnifiedInbox();
      return;
    }
    $('interactionTable').innerHTML = renderInteractionBoard(data);
    renderCustomerSpotlight();
    renderUnifiedInbox();
  } catch (err) {
    state.interactions = [];
    setEmptyBlock('interactionTable', 'Не удалось загрузить обращения.');
    renderCustomerSpotlight();
    renderUnifiedInbox();
    log('Не удалось загрузить обращения', { error: err.message });
  }
}

window.assignInteraction = async function assignInteraction(id) {
  const assignee = getDefaultAssignee();
  try {
    await api('interaction', `interactions/${id}/assign`, {
      method: 'PATCH',
      body: JSON.stringify({ assignee }),
    });
    await loadInteractions();
    log('Обращение назначено', { id, assignee });
  } catch (err) {
    log('Не удалось назначить обращение', { error: err.message });
  }
};

window.escalateInteraction = async function escalateInteraction(id) {
  const targetQueueId = getDefaultEscalationQueue();
  try {
    await api('interaction', `interactions/${id}/escalate`, {
      method: 'POST',
      body: JSON.stringify({ target_queue_id: targetQueueId }),
    });
    await loadInteractions();
    log('Обращение передано на 2 линию', { id, target_queue_id: targetQueueId });
  } catch (err) {
    log('Не удалось передать обращение', { error: err.message });
  }
};

window.closeInteraction = async function closeInteraction(id) {
  try {
    await api('interaction', `interactions/${id}/status`, {
      method: 'PATCH',
      body: JSON.stringify({ status: 'closed' }),
    });
    await loadInteractions();
    log('Обращение закрыто', { id });
  } catch (err) {
    log('Не удалось закрыть обращение', { error: err.message });
  }
};

async function loadSupervisor() {
  try {
    const data = await api('supervisor', 'supervisor/realtime');
    renderSupervisorSummary(data);
    setOutput('supervisorOutput', data);
    log('Данные супервизора обновлены', {
      agents: data?.agents?.total || 0,
      queue: data?.queues?.[0]?.queue_id || 'нет',
    });
  } catch (err) {
    $('supervisorSummary').innerHTML = renderSummaryCard('Супервизор', 'нет данных', 'Сервис не ответил');
    $('supervisorOutput').textContent = err.message;
    log('Не удалось обновить супервизора', { error: err.message });
  }
}

async function loadKpi() {
  try {
    const data = await api('reporting', 'reports/kpi');
    renderKpiSummary(data);
    setOutput('kpiOutput', data);
    log('KPI обновлены', data?.kpi || {});
  } catch (err) {
    $('kpiSummary').innerHTML = renderSummaryCard('KPI', 'нет данных', 'Сервис не ответил');
    $('kpiOutput').textContent = err.message;
    log('Не удалось обновить KPI', { error: err.message });
  }
}

async function handleCustomerActionClick(event) {
  const button = event.target.closest('[data-customer-action]');
  if (!button) {
    return;
  }
  const customer = selectedCustomer();
  if (!customer) {
    return;
  }
  if (button.dataset.customerAction === 'edit-name') {
    openCustomerProfileNameEditor(customer.customer_id);
    return;
  }
  if (button.dataset.customerAction === 'cancel-name-edit') {
    closeCustomerProfileNameEditor();
    return;
  }
  if (button.dataset.customerAction === 'workspace') {
    $('interactionCustomerId').value = customer.customer_id;
    window.location.hash = '#workspace';
    log('Клиент открыт в рабочем столе', { customer_id: customer.customer_id });
    return;
  }
  if (button.dataset.customerAction === 'telegram') {
    const threadId = button.dataset.threadId || '';
    if (!threadId) {
      return;
    }
    window.location.hash = '#messages';
    await selectMessengerConversation(messengerConversationId('telegram', threadId));
    log('Открыт Telegram клиента в Сообщениях', { customer_id: customer.customer_id, thread_id: threadId });
    return;
  }
  if (button.dataset.customerAction === 'interaction') {
    const interactionId = button.dataset.interactionId || '';
    $('interactionCustomerId').value = customer.customer_id;
    window.location.hash = '#workspace';
    log('Открыт кейс клиента', { customer_id: customer.customer_id, interaction_id: interactionId || '-' });
    return;
  }
  if (button.dataset.customerAction === 'call') {
    const callId = button.dataset.callId || '';
    if (!callId) {
      return;
    }
    focusLiveCall(callId);
    window.location.hash = '#calls';
    log('Открыт звонок клиента', { customer_id: customer.customer_id, call_id: callId });
  }
}

function handleCustomerProfileNameInput(event) {
  const input = event.target?.closest?.('[data-customer-name-input]');
  if (!input) {
    return;
  }
  state.customers.nameEditor.draft = input.value || '';
  state.customers.nameEditor.flash = '';
  if (!state.customers.nameEditor.error) {
    return;
  }
  state.customers.nameEditor.error = '';
  const form = input.closest('[data-customer-name-form]');
  const status = form?.querySelector?.('[data-customer-name-status]');
  if (status) {
    status.textContent = 'Имя сохранится в профиле клиента и voice-контуре.';
    status.classList.remove('error');
  }
}

function handleCustomerProfileNameSubmit(event) {
  const form = event.target?.closest?.('[data-customer-name-form]');
  if (!form) {
    return;
  }
  event.preventDefault();
  saveCustomerProfileName().catch((err) => {
    state.customers.nameEditor.saving = false;
    state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
    renderCustomerSpotlight();
  });
}

function wire() {
  $('loginBtn').addEventListener('click', login);
  $('corporateLoginBtn').addEventListener('click', startCorporateLogin);
  $('refreshBtn').addEventListener('click', async () => {
    await refreshUnifiedInbox();
    if (operatorHashState().view === 'messages') {
      await loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true });
    }
    if (featureEnabled('whatsapp')) {
      await loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true });
    }
    log('Данные на экране обновлены');
  });
  $('refreshUnifiedInboxBtn').addEventListener('click', () => {
    refreshUnifiedInbox().catch((err) => {
      log('Не удалось обновить единую очередь', { error: err.message });
    });
  });
  $('unifiedInboxBoard').addEventListener('click', handleUnifiedInboxClick);
  $('toggleCustomerLeadFormBtn').addEventListener('click', () => toggleCustomerLeadForm());
  $('customerCancelBtn').addEventListener('click', () => toggleCustomerLeadForm(false));
  $('createCustomerBtn').addEventListener('click', createCustomer);
  $('exportCustomersBtn').addEventListener('click', exportCustomersCsv);
  $('customerPrevPageBtn').addEventListener('click', () => setCustomerPage(state.customers.page - 1));
  $('customerNextPageBtn').addEventListener('click', () => setCustomerPage(state.customers.page + 1));
  $('customerQuery').addEventListener('keydown', (event) => {
    if (event.key === 'Enter') {
      event.preventDefault();
      searchCustomers();
    }
  });
  $('customerList').addEventListener('click', (event) => {
    const row = event.target.closest('[data-customer-id]');
    if (!row) {
      return;
    }
    openCustomerProfile(row.dataset.customerId || '');
  });
  $('customerList').addEventListener('keydown', (event) => {
    if (event.key !== 'Enter' && event.key !== ' ') {
      return;
    }
    const row = event.target.closest('[data-customer-id]');
    if (!row) {
      return;
    }
    event.preventDefault();
    openCustomerProfile(row.dataset.customerId || '');
  });
  $('customerSpotlight')?.addEventListener('click', handleCustomerActionClick);
  $('customerSpotlight')?.addEventListener('input', handleCustomerProfileNameInput);
  $('customerSpotlight')?.addEventListener('submit', handleCustomerProfileNameSubmit);
  $('customerProfileContent').addEventListener('click', handleCustomerActionClick);
  $('customerProfileContent').addEventListener('input', handleCustomerProfileNameInput);
  $('customerProfileContent').addEventListener('submit', handleCustomerProfileNameSubmit);
  $('customerProfileBackBtn').addEventListener('click', () => {
    window.location.hash = '#customers-page';
  });
  $('messengerRefreshBtn').addEventListener('click', () => {
    loadMessengerThreads(true, { preserveSelection: true, preserveOnError: true }).catch(() => {});
  });
  $('messengerRetryBtn').addEventListener('click', () => {
    loadMessengerThreads(true, { preserveSelection: true, preserveOnError: true }).catch(() => {});
  });
  document.querySelectorAll('[data-messenger-filter]').forEach((button) => {
    button.addEventListener('click', () => setMessengerFilter(button.dataset.messengerFilter || 'all'));
  });
  $('messengerConversationList').addEventListener('click', (event) => {
    const button = event.target.closest('[data-messenger-conversation-id]');
    if (!button) {
      return;
    }
    selectMessengerConversation(button.dataset.messengerConversationId || '').catch(() => {});
  });
  $('messengerConversationList').addEventListener('keydown', (event) => {
    if (event.key !== 'Enter' && event.key !== ' ') {
      return;
    }
    const button = event.target.closest('[data-messenger-conversation-id]');
    if (!button) {
      return;
    }
    event.preventDefault();
    selectMessengerConversation(button.dataset.messengerConversationId || '').catch(() => {});
  });
  $('messengerClaimBtn').addEventListener('click', () => {
    claimMessengerConversation().catch(() => {});
  });
  $('messengerCloseBtn').addEventListener('click', () => {
    closeMessengerConversation().catch(() => {});
  });
  $('messengerOpenProfileBtn').addEventListener('click', openMessengerCustomerProfile);
  $('messengerContextOpenProfileBtn').addEventListener('click', openMessengerCustomerProfile);
  $('messengerSendReplyBtn').addEventListener('click', () => {
    sendMessengerReply().catch(() => {});
  });
  $('messengerReplyText').addEventListener('input', (event) => {
    state.messenger.composerText = event.target.value || '';
    syncMessengerComposerHeight();
    syncMessengerActionButtons();
  });
  $('messengerReplyText').addEventListener('keydown', (event) => {
    if (event.key !== 'Enter' || event.shiftKey) {
      return;
    }
    event.preventDefault();
    sendMessengerReply().catch(() => {});
  });
  $('loadTelegramThreadsBtn').addEventListener('click', () => loadTelegramThreads(true, { preserveSelection: true, preserveOnError: true }));
  $('telegramThreadSearch').addEventListener('input', (event) => {
    state.telegram.searchQuery = event.target.value || '';
    renderTelegramWorkspace();
  });
  $('telegramHeaderSearchBtn').addEventListener('click', () => {
    toggleTelegramMessageSearch();
  });
  $('telegramMessageSearch').addEventListener('input', (event) => {
    state.telegram.messageSearchQuery = event.target.value || '';
    renderTelegramWorkspace();
  });
  $('telegramMessageSearch').addEventListener('keydown', (event) => {
    if (event.key !== 'Escape') {
      return;
    }
    event.preventDefault();
    if (telegramMessageSearchQuery()) {
      state.telegram.messageSearchQuery = '';
      renderTelegramWorkspace();
      return;
    }
    toggleTelegramMessageSearch(false);
  });
  $('telegramMessageSearchClearBtn').addEventListener('click', () => {
    if (telegramMessageSearchQuery()) {
      state.telegram.messageSearchQuery = '';
      renderTelegramWorkspace();
      $('telegramMessageSearch').focus();
      return;
    }
    toggleTelegramMessageSearch(false);
  });
  $('telegramHeaderCallBtn').addEventListener('click', () => {
    log('Voice call action для Telegram пока не назначен');
  });
  $('telegramHeaderMenuBtn').addEventListener('click', () => {
    toggleTelegramOperatorTray();
  });
  $('telegramThreadsList').addEventListener('click', (event) => {
    const button = event.target.closest('[data-telegram-thread-id]');
    if (!button) {
      return;
    }
    selectTelegramThread(button.dataset.telegramThreadId || '');
  });
  $('telegramThreadsList').addEventListener('keydown', (event) => {
    if (event.key !== 'Enter' && event.key !== ' ') {
      return;
    }
    const button = event.target.closest('[data-telegram-thread-id]');
    if (!button) {
      return;
    }
    event.preventDefault();
    selectTelegramThread(button.dataset.telegramThreadId || '');
  });
  $('telegramClaimThreadBtn').addEventListener('click', claimTelegramThread);
  $('telegramReturnToAiBtn').addEventListener('click', returnTelegramThreadToAi);
  $('telegramCloseThreadBtn').addEventListener('click', closeTelegramThread);
  $('telegramEscalateThreadBtn').addEventListener('click', escalateTelegramThread);
  $('telegramSendReplyBtn').addEventListener('click', sendTelegramReply);
  $('telegramReplyText').addEventListener('input', () => {
    syncTelegramComposerHeight();
    syncTelegramActionButtons();
  });
  $('telegramReplyText').addEventListener('keydown', (event) => {
    if (event.key !== 'Enter') {
      return;
    }
    if (event.shiftKey) {
      return;
    }
    event.preventDefault();
    sendTelegramReply();
  });
  if (featureEnabled('whatsapp')) {
    $('whatsappSearchInput').addEventListener('input', (event) => {
      state.whatsapp.searchQuery = event.target.value || '';
      renderWhatsappWorkspace();
    });
    $('whatsappFilterBar').addEventListener('click', (event) => {
      const button = event.target.closest('[data-whatsapp-filter]');
      if (!button) {
        return;
      }
      setWhatsappFilter(button.dataset.whatsappFilter || 'all');
    });
    $('whatsappClaimBtn').addEventListener('click', () => {
      claimWhatsappThread().catch(() => {});
    });
    $('whatsappReturnToAiBtn').addEventListener('click', returnWhatsappThreadToAi);
    $('whatsappChatList').addEventListener('click', (event) => {
      const button = event.target.closest('[data-whatsapp-chat-id]');
      if (!button) {
        return;
      }
      selectWhatsappChat(button.dataset.whatsappChatId || '').catch(() => {});
    });
    $('whatsappChatList').addEventListener('keydown', (event) => {
      if (event.key !== 'Enter' && event.key !== ' ') {
        return;
      }
      const button = event.target.closest('[data-whatsapp-chat-id]');
      if (!button) {
        return;
      }
      event.preventDefault();
      selectWhatsappChat(button.dataset.whatsappChatId || '').catch(() => {});
    });
    document.querySelectorAll('[data-whatsapp-ui-action]').forEach((button) => {
      button.addEventListener('click', handleWhatsappUiAction);
    });
    $('whatsappSendBtn').addEventListener('click', sendWhatsappMessage);
    $('whatsappComposerInput').addEventListener('input', (event) => {
      state.whatsapp.composerText = event.target.value || '';
      syncWhatsappComposerUi();
    });
    $('whatsappComposerInput').addEventListener('keydown', (event) => {
      if (event.key !== 'Enter' || event.shiftKey) {
        return;
      }
      event.preventDefault();
      sendWhatsappMessage();
    });
  }
  $('createInteractionBtn').addEventListener('click', createInteraction);
  $('loadInteractionsBtn').addEventListener('click', loadInteractions);
  $('loadLiveCallsBtn').addEventListener('click', () => loadLiveCalls(true));
  $('loadLiveCallActionsBtn').addEventListener('click', loadLiveCallActions);
  $('liveCallsTable').addEventListener('click', handleLiveCallTableClick);
  $('liveCallNameInput').addEventListener('input', (event) => {
    state.liveCalls.nameEditor.draft = event.target.value || '';
    if (state.liveCalls.nameEditor.error) {
      state.liveCalls.nameEditor.error = '';
    }
    updateLiveCallNameEditorsUi();
  });
  $('liveCallNameSaveBtn').addEventListener('click', () => {
    saveLiveCallCustomerName().catch((err) => {
      state.liveCalls.nameEditor.saving = false;
      state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
      updateLiveCallNameEditorsUi();
    });
  });
  $('liveCallNameCancelBtn').addEventListener('click', closeLiveCallNameEditor);
  $('browserPhoneStatusBtn').addEventListener('click', toggleBrowserPhoneSettings);
  $('browserPhoneCallSettingsBtn').addEventListener('click', toggleBrowserPhoneSettings);
  $('browserPhoneConnectBtn').addEventListener('click', connectBrowserSoftphone);
  $('browserPhoneDisconnectBtn').addEventListener('click', disconnectBrowserSoftphone);
  $('browserPhoneMuteBtn').addEventListener('click', toggleBrowserSoftphoneMute);
  $('browserPhoneAnswerBtn').addEventListener('click', answerBrowserSoftphoneCall);
  $('browserPhoneRejectBtn').addEventListener('click', rejectBrowserSoftphoneCall);
  $('browserPhoneCallClaimBtn').addEventListener('click', claimBrowserPopupCall);
  $('browserPhoneEditNameBtn').addEventListener('click', () => {
    const popupCall = browserPhoneActivePopupCall();
    if (popupCall?.call_id) {
      openLiveCallNameEditor(popupCall.call_id, 'popup');
    }
  });
  $('browserPhoneCallTransferBtn').addEventListener('click', transferBrowserPopupCall);
  $('browserPhoneCallHangupBtn').addEventListener('click', hangupBrowserPopupCall);
  $('browserPhoneNameInput').addEventListener('input', (event) => {
    state.liveCalls.nameEditor.draft = event.target.value || '';
    if (state.liveCalls.nameEditor.error) {
      state.liveCalls.nameEditor.error = '';
    }
    updateLiveCallNameEditorsUi();
  });
  $('browserPhoneNameSaveBtn').addEventListener('click', () => {
    saveLiveCallCustomerName().catch((err) => {
      state.liveCalls.nameEditor.saving = false;
      state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.';
      updateLiveCallNameEditorsUi();
    });
  });
  $('browserPhoneNameCancelBtn').addEventListener('click', closeLiveCallNameEditor);
  $('browserPhoneMicSelect').addEventListener('change', () => {
    state.browserPhone.micDeviceId = $('browserPhoneMicSelect').value;
    stopBrowserPhoneLocalStream();
    if (state.browserPhone.incoming) {
      prepareBrowserPhoneAnswerMedia();
    }
  });
  $('browserPhoneSpeakerSelect').addEventListener('change', () => {
    state.browserPhone.speakerDeviceId = $('browserPhoneSpeakerSelect').value;
    syncBrowserPhoneSpeakerSink();
  });
  $('browserPhoneTransferTargetValue').addEventListener('input', updateBrowserPhoneUi);
  $('browserPhoneTransferTargetType').addEventListener('change', updateBrowserPhoneUi);
  $('liveCallIdSelect').addEventListener('change', () => {
    state.liveCalls.selectedCallId = $('liveCallIdSelect').value;
    syncLiveCallActionButtons();
  });
  $('logoutBtn').addEventListener('click', logout);
  window.addEventListener('message', handleOidcMessage);
  window.addEventListener('hashchange', handleOperatorHashChange);
  if (navigator.mediaDevices?.addEventListener) {
    navigator.mediaDevices.addEventListener('devicechange', () => {
      loadBrowserPhoneDevices().catch(() => {});
    });
  }
}

async function init() {
  if (!restoreStoredSession()) {
    window.location.href = '/';
    return;
  }
  await loadOperatorConfig();
  wire();
  applyOperatorViewFromHash();
  $('defaultAssignee').value = DEMO_ASSIGNEE;
  $('defaultEscalationQueue').value = DEMO_QUEUE;
  $('telegramEscalationQueue').value = DEMO_QUEUE;
  updateSessionInfo();
  syncLiveCallActionButtons();
  renderUnifiedInbox();
  renderMessengerWorkspace();
  if (featureEnabled('whatsapp')) {
    renderWhatsappWorkspace();
  }
  renderTelegramWorkspace();
  await Promise.all([checkGateway(), loadOidcConfig()]);
  await fetchBrowserSoftphoneConfig();
  await loadBrowserPhoneDevices().catch(() => {});
  syncCustomerFormUi();
  await searchCustomers();
  await loadInteractions();
  await loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true });
  if (featureEnabled('whatsapp')) {
    await loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true });
  }
  await loadLiveCalls(false);
  startLiveCallsPolling();
  startTelegramPolling();
  if (featureEnabled('whatsapp')) {
    startWhatsappPolling();
  }
  log('Экран готов к показу', { assignee: DEMO_ASSIGNEE, queue: DEMO_QUEUE });
}

init();
