const state = {
  user: 'analyst',
  role: 'analyst',
  token: null,
  authSource: 'local',
  fullName: null,
  analytics: {
    preset: '7d',
    fromTs: '',
    toTs: '',
    queueId: 'all',
    channel: 'all',
    compareMode: 'previous',
    trendMetric: 'volume',
    voiceNameTrendMetric: 'scenario_calls',
    aiTrendMetric: 'containment_rate',
    agentTrendMetric: 'interactions_per_agent',
    overview: null,
    compare: null,
    trend: [],
    voiceNameOverview: null,
    voiceNameCompare: null,
    voiceNameTrend: [],
    aiOverview: null,
    aiCompare: null,
    aiTrend: [],
    agentOverview: null,
    agentCompare: null,
    agentTrend: [],
    channelRows: [],
    queueRows: [],
    agentRows: [],
    coverage: null,
    queueOptions: [],
    savedViews: [],
    activeViewId: '',
    lastRangeMeta: null,
    trendInterval: 'day',
    agentTrendInterval: 'day',
    statusMessage: '',
    mockData: null,
    mockDataError: '',
    loading: false,
    error: '',
    voiceNameError: '',
    voiceNameTrendError: '',
    aiError: '',
    agentError: '',
    agentTrendError: '',
    queueAccessError: '',
    requestId: 0,
  },
  drilldown: {
    open: false,
    mode: 'interaction',
    sourceType: '',
    sourceLabel: '',
    sourceValue: '',
    metric: '',
    filters: null,
    coverage: null,
    metricNote: '',
    items: [],
    total: 0,
    limit: 12,
    offset: 0,
    selectedInteractionId: '',
    selectedInteraction: null,
    selectedLinkedInteraction: null,
    selectedTimeline: [],
    loading: false,
    detailLoading: false,
    exporting: false,
    notice: '',
    error: '',
  },
  oidc: {
    enabled: false,
    loginPath: '/auth/oidc/start?return_mode=popup',
    providerLabel: 'Keycloak',
  },
};

const $ = (id) => document.getElementById(id);
const SESSION_STORAGE_KEY = 'cc_session';
const DRILLDOWN_PAGE_SIZE = 12;
const DRILLDOWN_EXPORT_BATCH_SIZE = 100;
const DRILLDOWN_EXPORT_SOFT_CAP = 1000;
const ANALYTICS_TREND_OPTIONS = ['volume', 'SL', 'ASA', 'Abandon'];
const VOICE_NAME_TREND_OPTIONS = [
  'scenario_calls',
  'start_capture_rate',
  'downstream_rescue_rate',
  'handoff_unconfirmed_rate',
  'manual_correction_rate',
];
const AI_ANALYTICS_TREND_OPTIONS = [
  'containment_rate',
  'handoff_rate',
  'human_touched_rate',
  'ai_latency_avg_ms',
  'closed_without_operator_rate',
];
const AGENT_ANALYTICS_TREND_OPTIONS = [
  'interactions_per_agent',
  'agents_with_activity',
  'avg_handle_seconds',
  'fcr_rate',
];
const ENABLE_ANALYTICS_MOCK_DATA = true;
const ANALYTICS_MOCK_DATA_URL = '/analyst/assets/mock-analytics.json';
const ANALYTICS_DEEP_LINK_KEYS = [
  'preset',
  'from',
  'to',
  'queue',
  'channel',
  'compare',
  'trend',
  'voice_name_trend',
  'ai_trend',
  'agent_trend',
  'view',
  'dd',
  'dd_mode',
  'dd_source',
  'dd_value',
  'dd_metric',
  'dd_slice',
  'dd_reason',
  'dd_status',
  'dd_q',
  'dd_sort_by',
  'dd_sort_dir',
  'dd_offset',
  'dd_selected',
  'dd_agent',
];
let analyticsDrilldownSearchDebounce = 0;
let analyticsDrilldownReturnFocus = null;
let analyticsUrlSyncSuspended = false;
const ROLE_LABELS = {
  admin: 'Администратор',
  supervisor: 'Супервизор',
  operator: 'Оператор',
  analyst: 'Аналитик',
};

const ANALYTICS_CHANNEL_OPTIONS = [
  { value: 'all', label: 'Все каналы' },
  { value: 'voice', label: 'Голос' },
  { value: 'telegram', label: 'Telegram' },
  { value: 'whatsapp', label: 'WhatsApp' },
  { value: 'webchat', label: 'Веб-чат' },
  { value: 'email', label: 'Email' },
];

const ANALYTICS_PRESET_LABELS = {
  today: 'Сегодня',
  '7d': 'Последние 7 дней',
  '30d': 'Последние 30 дней',
};

const ANALYTICS_COMPARE_MODE_LABELS = {
  previous: 'С предыдущим окном',
  off: 'Только текущий срез',
};

const ANALYTICS_TREND_LABELS = {
  volume: 'Обращения',
  SL: 'SLA',
  ASA: 'Среднее ожидание',
  Abandon: 'Потери',
};

const VOICE_NAME_TREND_LABELS = {
  scenario_calls: 'Звонки в сценарии',
  start_capture_rate: 'Имя взято сразу',
  downstream_rescue_rate: 'Имя добрал AI после follow-up',
  handoff_unconfirmed_rate: 'Передача без подтверждённого имени',
  manual_correction_rate: 'Ручное исправление',
};

const VOICE_NAME_METRIC_META = {
  scenario_calls: { unit: 'count', better: 'up', note: 'Все voice-звонки, прошедшие через сценарий сбора имени.' },
  start_capture_rate: { unit: 'pct', better: 'up', note: 'Доля звонков, где имя удалось взять сразу на стартовом этапе.' },
  downstream_rescue_rate: { unit: 'pct', better: 'up', note: 'Доля кейсов, где AI успешно добрал имя после стартового follow-up.' },
  handoff_unconfirmed_rate: { unit: 'pct', better: 'down', note: 'Доля звонков, переданных оператору без подтверждённого имени.' },
  manual_correction_rate: { unit: 'pct', better: 'down', note: 'Как часто оператору пришлось исправлять имя вручную.' },
};

const VOICE_NAME_FUNNEL_LABELS = {
  scenario_calls: 'Звонки в сценарии',
  start_obtained: 'Имя взято сразу',
  needed_downstream: 'Потребовался AI после старта',
  downstream_ai_obtained: 'Имя добрал AI после follow-up',
  handoff_confirmed_name: 'Передача с подтверждённым именем',
  handoff_unconfirmed_name: 'Передача без подтверждённого имени',
};

const VOICE_NAME_HANDOFF_LABELS = {
  confirmed_name: 'Передача с подтверждённым именем',
  unconfirmed_name: 'Передача без подтверждённого имени',
};

const ANALYTICS_METRIC_META = {
  total: { unit: 'count', better: 'up', note: 'Все обращения за выбранный период' },
  answered: { unit: 'count', better: 'up', note: 'Обращения, обработанные без потери' },
  SL: { unit: 'pct', better: 'up', note: 'Доля обращений, уложившихся в SLA' },
  ASA: { unit: 'seconds', better: 'down', note: 'Среднее ожидание до ответа' },
  AHT: { unit: 'seconds', better: 'down', note: 'Среднее время обработки обращения' },
  Abandon: { unit: 'pct', better: 'down', note: 'Доля потерянных обращений' },
  FCR: { unit: 'pct', better: 'up', note: 'Решено с первого контакта' },
  DigitalShare: { unit: 'pct', better: 'neutral', note: 'Доля цифровых каналов в потоке' },
  volume: { unit: 'count', better: 'up', note: 'Объём обращений в интервале' },
};

const AI_ANALYTICS_TREND_LABELS = {
  containment_rate: 'Закрыто AI',
  handoff_rate: 'Передано оператору',
  ai_latency_avg_ms: 'Задержка AI',
  closed_without_operator_rate: 'Закрыто без оператора',
  human_touched_rate: 'С участием оператора',
};

const AI_ANALYTICS_METRIC_META = {
  containment_rate: {
    unit: 'pct',
    better: 'up',
    note: 'Доля AI-сессий, закрытых самим AI без передачи оператору и без назначения исполнителя.',
  },
  handoff_rate: {
    unit: 'pct',
    better: 'down',
    note: 'Доля AI-сессий, которые завершились передачей оператору или ушли в ручное ведение.',
  },
  ai_latency_avg_ms: {
    unit: 'ms',
    better: 'down',
    note: 'Средняя задержка AI-ответа по ответам модели.',
  },
  closed_without_operator_rate: {
    unit: 'pct',
    better: 'up',
    note: 'Среди закрытых AI-связанных обращений доля кейсов без назначения оператора.',
  },
  human_touched_rate: {
    unit: 'pct',
    better: 'down',
    note: 'Доля сессий, где участвовал оператор: через назначение или ручной takeover.',
  },
};

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-shell]').forEach((link) => {
    const allowed = (link.dataset.roles || '')
      .split(',')
      .map((item) => item.trim())
      .filter(Boolean);
    link.style.display = !allowed.length || allowed.includes(state.role) ? '' : 'none';
  });
}

function ensurePageAccess() {
  const allowed = ['admin', 'supervisor', 'analyst'];
  if (!allowed.includes(state.role)) {
    window.location.href = destinationForRole(state.role);
    return false;
  }
  return true;
}

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 syncSessionInputs() {
  const sessionUser = $('sessionUser');
  const sessionRole = $('sessionRole');
  if (sessionUser) {
    sessionUser.value = state.user;
  }
  if (sessionRole) {
    sessionRole.value = state.role;
  }
}

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;
    syncSessionInputs();
    updateProfileMeta();
    applyRoleNavigation();
    return true;
  } catch {
    clearStoredSession();
    return false;
  }
}

function logout() {
  clearStoredSession();
  window.location.href = '/';
}

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

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

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 updateSessionInfo(extra = '') {
  const box = $('sessionInfo');
  if (!box) {
    return;
  }
  if (!state.token) {
    box.textContent = 'Токен: отсутствует';
    return;
  }
  const source = state.authSource === 'oidc' ? 'корпоративный' : 'локальный';
  box.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;
  syncSessionInputs();
  updateSessionInfo(data.provider ? `провайдер: ${data.provider}` : '');
  updateProfileMeta();
  persistSession();
  applyRoleNavigation();
  ensurePageAccess();
}

function buildProxyHeaders(extra = {}) {
  syncSessionFromInputs();
  const headers = {
    'X-User': state.user,
    'X-Role': state.role,
    ...extra,
  };
  if (state.token) {
    headers.Authorization = `Bearer ${state.token}`;
  }
  return headers;
}

async function api(service, path, options = {}) {
  const headers = buildProxyHeaders({
    'Content-Type': 'application/json',
    ...(options.headers || {}),
  });

  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;
}

function contentDispositionFilename(value) {
  if (!value) {
    return '';
  }
  const match = /filename=\"?([^\";]+)\"?/i.exec(value);
  return match ? decodeURIComponent(match[1]) : '';
}

async function downloadProxyFile(service, path, fallbackFilename) {
  const response = await fetch(`/proxy/${service}/${path}`, {
    headers: buildProxyHeaders(),
  });
  if (!response.ok) {
    let detail = '';
    try {
      detail = JSON.stringify(await response.json());
    } catch {
      detail = await response.text();
    }
    throw new Error(`${response.status}: ${detail}`);
  }
  const blob = await response.blob();
  const filename = contentDispositionFilename(response.headers.get('content-disposition')) || fallbackFilename;
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.append(link);
  link.click();
  link.remove();
  URL.revokeObjectURL(url);
  return filename;
}

async function checkGateway() {
  const pill = $('gatewayStatus');
  if (!pill) {
    return;
  }
  try {
    const response = await fetch('/health');
    const data = await response.json();
    pill.textContent = `Данные: ${data.status === 'ok' ? 'готовы' : data.status}`;
  } catch {
    pill.textContent = 'Данные: недоступны';
  }
}

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';
  } catch {
    state.oidc.enabled = false;
  }
}

function handleOidcMessage(event) {
  if (!event?.data || typeof event.data !== 'object') {
    return;
  }
  if (event.data.type === 'oidc-login' && event.data.access_token) {
    applyAuthenticatedSession(event.data);
    return;
  }
  if (event.data.type === 'oidc-error') {
    updateSessionInfo(`ошибка SSO: ${event.data.message || 'неизвестно'}`);
  }
}

function emptyKpiEnvelope(fromTs = null, toTs = null) {
  return {
    window: { from: fromTs, to: toTs },
    volume: {
      total: 0,
      answered: 0,
      abandoned: 0,
    },
    kpi: {
      SL: 0,
      ASA: 0,
      AHT: 0,
      Abandon: 0,
      FCR: 0,
      AnswerRate: 0,
      WaitP95: 0,
      HandleP95: 0,
      Occupancy: 0,
      DigitalShare: 0,
    },
    breakdowns: {
      by_channel: {},
    },
    coverage: {
      metric_status: {},
      supported_channels: {},
      exact_rows: 0,
      total_rows: 0,
      note: null,
      metric_details: {},
    },
    filters: {},
  };
}

function emptyAiAnalyticsOverview(fromTs = null, toTs = null, channel = 'all', queueId = null) {
  return {
    window: {
      from_ts: fromTs,
      to_ts: toTs,
    },
    filters: {
      from_ts: fromTs,
      to_ts: toTs,
      queue_id: queueId,
      channel,
    },
    totals: {
      sessions_started: 0,
      sessions_contained: 0,
      sessions_handoff: 0,
      sessions_closed: 0,
      sessions_closed_without_operator: 0,
      assistant_turns: 0,
    },
    metrics: {
      containment_rate: 0,
      handoff_rate: 0,
      ai_latency_avg_ms: null,
      ai_latency_p95_ms: null,
      closed_without_operator_rate: 0,
      human_touched_rate: 0,
    },
    breakdowns: {
      by_channel: [],
      by_outcome: [],
      by_handoff_reason: [],
    },
    coverage: {
      sessions_with_interaction_id: 0,
      sessions_with_queue_id: 0,
      sessions_with_latency_turns: 0,
      sessions_with_terminal_state: 0,
      sessions_with_handoff_reason: 0,
    },
  };
}

function emptyAiAnalyticsTimeseries(metric = 'containment_rate', interval = 'day', fromTs = null, toTs = null, channel = 'all', queueId = null) {
  return {
    metric,
    interval,
    filters: {
      from_ts: fromTs,
      to_ts: toTs,
      queue_id: queueId,
      channel,
    },
    points: [],
  };
}

function emptyAgentAnalyticsOverview(fromTs = null, toTs = null, channel = 'all', queueId = null) {
  return {
    window: {
      from_ts: fromTs,
      to_ts: toTs,
    },
    filters: {
      from_ts: fromTs,
      to_ts: toTs,
      queue_id: queueId,
      channel,
      sort_by: 'interactions_total',
      sort_dir: 'desc',
      limit: 25,
    },
    totals: {
      agents_total: 0,
      agents_with_activity: 0,
      interactions_total: 0,
      answered_total: 0,
      closed_total: 0,
      ready_now: 0,
      busy_now: 0,
      break_now: 0,
      offline_now: 0,
      avg_interactions_per_agent: 0,
      avg_handle_seconds: null,
      avg_fcr_rate: null,
    },
    state_snapshot: {
      by_state: {},
      updated_at: null,
    },
    breakdowns: {
      by_team: [],
      by_shift: [],
    },
    items: [],
  };
}

function emptyAgentAnalyticsTimeseries(metric = 'interactions_per_agent', interval = 'day') {
  return {
    metric,
    interval,
    filters: {
      from_ts: null,
      to_ts: null,
      queue_id: null,
      channel: 'all',
      metric,
      interval,
    },
    points: [],
  };
}

function emptyVoiceNameAnalyticsOverview(fromTs = null, toTs = null, queueId = null, language = null) {
  return {
    window: {
      from_ts: fromTs,
      to_ts: toTs,
    },
    filters: {
      from_ts: fromTs,
      to_ts: toTs,
      queue_id: queueId,
      language,
    },
    totals: {
      scenario_calls: 0,
      start_obtained: 0,
      downstream_ai_obtained: 0,
      followup_required: 0,
      name_not_obtained: 0,
      manual_corrected: 0,
      handoff_confirmed_name: 0,
      handoff_unconfirmed_name: 0,
      needed_downstream: 0,
    },
    metrics: {
      start_capture_rate: 0,
      downstream_rescue_rate: 0,
      handoff_unconfirmed_rate: 0,
      manual_correction_rate: 0,
    },
    breakdowns: {
      funnel: [],
      by_language: [],
      by_queue: [],
      handoff: [],
    },
    coverage: {
      sessions_with_start_decision: 0,
      sessions_with_final_ai_state: 0,
      sessions_with_manual_overlay: 0,
      note: null,
    },
  };
}

function emptyVoiceNameAnalyticsTimeseries(metric = 'scenario_calls', interval = 'day', fromTs = null, toTs = null, queueId = null, language = null) {
  return {
    metric,
    interval,
    filters: {
      from_ts: fromTs,
      to_ts: toTs,
      queue_id: queueId,
      language,
    },
    points: [],
  };
}

function emptyAnalyticsMetricCoverage() {
  return {
    status: 'unavailable',
    supported_channels: [],
    exact_rows: 0,
    total_rows: 0,
    note: null,
  };
}

function emptyDrilldownState() {
  return {
    open: false,
    mode: 'interaction',
    sourceType: '',
    sourceLabel: '',
    sourceValue: '',
    metric: '',
    filters: null,
    coverage: null,
    metricNote: '',
    items: [],
    total: 0,
    limit: DRILLDOWN_PAGE_SIZE,
    offset: 0,
    selectedInteractionId: '',
    selectedInteraction: null,
    selectedLinkedInteraction: null,
    selectedTimeline: [],
    loading: false,
    detailLoading: false,
    exporting: false,
    notice: '',
    error: '',
  };
}

function pad2(value) {
  return String(value).padStart(2, '0');
}

function startOfDay(date) {
  const next = new Date(date);
  next.setHours(0, 0, 0, 0);
  return next;
}

function addDays(date, days) {
  const next = new Date(date);
  next.setDate(next.getDate() + days);
  return next;
}

function normalizeAnalyticsRange(from, to) {
  const safeFrom = new Date(from);
  const safeTo = new Date(to);
  if (Number.isNaN(safeFrom.getTime()) || Number.isNaN(safeTo.getTime())) {
    return { from: new Date(), to: new Date() };
  }
  if (safeTo <= safeFrom) {
    return { from: safeFrom, to: new Date(safeFrom.getTime() + 60 * 60 * 1000) };
  }
  return { from: safeFrom, to: safeTo };
}

function analyticsRangeFromControls() {
  const preset = $('analyticsPreset').value || state.analytics.preset || '7d';
  const customFrom = $('analyticsFrom').value.trim();
  const customTo = $('analyticsTo').value.trim();
  const now = new Date();
  let from;
  let to;
  let custom = false;

  if (customFrom && customTo) {
    custom = true;
    from = new Date(customFrom);
    to = new Date(customTo);
  } else if (preset === 'today') {
    from = startOfDay(now);
    to = now;
  } else if (preset === '30d') {
    from = startOfDay(addDays(now, -29));
    to = now;
  } else {
    from = startOfDay(addDays(now, -6));
    to = now;
  }

  const current = normalizeAnalyticsRange(from, to);
  const durationMs = Math.max(current.to.getTime() - current.from.getTime(), 60 * 60 * 1000);
  const previous = {
    from: new Date(current.from.getTime() - durationMs),
    to: new Date(current.from.getTime()),
  };

  return {
    preset,
    custom,
    current,
    previous,
  };
}

function syncAnalyticsStateFromControls() {
  state.analytics.preset = $('analyticsPreset').value || '7d';
  state.analytics.fromTs = $('analyticsFrom').value.trim();
  state.analytics.toTs = $('analyticsTo').value.trim();
  state.analytics.queueId = $('analyticsQueue').value || 'all';
  state.analytics.channel = $('analyticsChannel').value || 'all';
  state.analytics.compareMode = $('analyticsCompareMode').value || 'previous';
  state.analytics.trendMetric = $('analyticsTrendMetric').value || 'volume';
  state.analytics.voiceNameTrendMetric = $('voiceNameAnalyticsTrendMetric')?.value || 'scenario_calls';
  state.analytics.aiTrendMetric = $('aiAnalyticsTrendMetric')?.value || 'containment_rate';
  state.analytics.agentTrendMetric = $('agentAnalyticsTrendMetric')?.value || 'interactions_per_agent';
}

function syncAnalyticsControlsFromState() {
  $('analyticsPreset').value = state.analytics.preset || '7d';
  $('analyticsFrom').value = state.analytics.fromTs || '';
  $('analyticsTo').value = state.analytics.toTs || '';
  $('analyticsChannel').value = state.analytics.channel || 'all';
  $('analyticsCompareMode').value = state.analytics.compareMode || 'previous';
  $('analyticsTrendMetric').value = state.analytics.trendMetric || 'volume';
  if ($('voiceNameAnalyticsTrendMetric')) {
    $('voiceNameAnalyticsTrendMetric').value = state.analytics.voiceNameTrendMetric || 'scenario_calls';
  }
  if ($('aiAnalyticsTrendMetric')) {
    $('aiAnalyticsTrendMetric').value = state.analytics.aiTrendMetric || 'containment_rate';
  }
  if ($('agentAnalyticsTrendMetric')) {
    $('agentAnalyticsTrendMetric').value = state.analytics.agentTrendMetric || 'interactions_per_agent';
  }
  renderAnalyticsQueueOptions();
  renderSavedAnalyticsViews();
}

function resetAnalyticsFilters() {
  state.analytics.preset = '7d';
  state.analytics.fromTs = '';
  state.analytics.toTs = '';
  state.analytics.queueId = 'all';
  state.analytics.channel = 'all';
  state.analytics.compareMode = 'previous';
  state.analytics.trendMetric = 'volume';
  state.analytics.voiceNameTrendMetric = 'scenario_calls';
  state.analytics.aiTrendMetric = 'containment_rate';
  state.analytics.agentTrendMetric = 'interactions_per_agent';
  state.analytics.activeViewId = '';
  state.analytics.trendInterval = 'day';
  state.analytics.agentTrendInterval = 'day';
  state.analytics.statusMessage = '';
  resetAnalyticsDrilldown();
  syncAnalyticsControlsFromState();
  syncAnalyticsUrlState();
}

function analyticsCurrentSnapshot() {
  return {
    preset: state.analytics.preset || '7d',
    fromTs: state.analytics.fromTs || '',
    toTs: state.analytics.toTs || '',
    queueId: state.analytics.queueId || 'all',
    channel: state.analytics.channel || 'all',
    compareMode: state.analytics.compareMode || 'previous',
    trendMetric: state.analytics.trendMetric || 'volume',
    voiceNameTrendMetric: state.analytics.voiceNameTrendMetric || 'scenario_calls',
    aiTrendMetric: state.analytics.aiTrendMetric || 'containment_rate',
    agentTrendMetric: state.analytics.agentTrendMetric || 'interactions_per_agent',
  };
}

function analyticsMockEnabled() {
  return ENABLE_ANALYTICS_MOCK_DATA === true;
}

function analyticsMockData() {
  return state.analytics.mockData || {};
}

async function ensureAnalyticsMockDataLoaded() {
  if (!analyticsMockEnabled()) {
    return null;
  }
  if (state.analytics.mockData) {
    return state.analytics.mockData;
  }
  try {
    const response = await fetch(ANALYTICS_MOCK_DATA_URL, { cache: 'no-store' });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const payload = await response.json();
    state.analytics.mockData = payload && typeof payload === 'object' ? payload : {};
    state.analytics.mockDataError = '';
    return state.analytics.mockData;
  } catch (error) {
    state.analytics.mockData = {};
    state.analytics.mockDataError = error?.message || 'Не удалось загрузить mock-analytics.json';
    return state.analytics.mockData;
  }
}

function mockAnalyticsScale(range) {
  if (!range?.from || !range?.to) {
    return 1;
  }
  const dayMs = 24 * 60 * 60 * 1000;
  const duration = Math.max(range.to.getTime() - range.from.getTime(), dayMs);
  return Math.max(0.6, duration / (7 * dayMs));
}

function mockAnalyticsQueueOptions() {
  const queueOptions = Array.isArray(analyticsMockData().queue_options)
    ? analyticsMockData().queue_options
    : [];
  return queueOptions.map((item) => ({ ...item }));
}

function mockAnalyticsAgents() {
  return Array.isArray(analyticsMockData().agents)
    ? analyticsMockData().agents.slice()
    : [];
}

function mockAnalyticsQueueFactor(queueId = state.analytics.queueId) {
  const factors = analyticsMockData().queue_factors || {};
  if (!queueId || queueId === 'all') {
    return 1;
  }
  return factors[queueId] || 0.84;
}

function mockAnalyticsChannelBase() {
  return analyticsMockData().channel_base || {};
}

function mockAiReasonLabel(reasonKey) {
  const labels = analyticsMockData().ai_reason_labels || {};
  return labels[reasonKey] || reasonKey || 'Другая причина';
}

function mockInteractionStatus(index = 0) {
  const statuses = Array.isArray(analyticsMockData().interaction_statuses)
    ? analyticsMockData().interaction_statuses
    : [];
  if (!statuses.length) {
    return 'closed';
  }
  return statuses[index % statuses.length];
}

function mockAnalyticsCoverage(totalRows, note = analyticsMockData().coverage_note || 'Показаны демонстрационные данные для временного наполнения витрины.') {
  const metricNames = ['total', 'answered', 'SL', 'ASA', 'AHT', 'Abandon', 'FCR', 'DigitalShare'];
  const metricStatus = {};
  const supportedChannels = {};
  const metricDetails = {};
  metricNames.forEach((metric) => {
    metricStatus[metric] = 'exact';
    supportedChannels[metric] = ['voice', 'telegram', 'whatsapp', 'webchat', 'email'];
    metricDetails[metric] = {
      status: 'exact',
      supported_channels: supportedChannels[metric],
      exact_rows: totalRows,
      total_rows: totalRows,
      note,
    };
  });
  return {
    metric_status: metricStatus,
    supported_channels: supportedChannels,
    exact_rows: totalRows,
    total_rows: totalRows,
    note,
    metric_details: metricDetails,
  };
}

function buildMockAnalyticsOverview(rangeMeta, options = {}) {
  const previous = Boolean(options.previous);
  const queueId = options.queueId ?? state.analytics.queueId;
  const channel = options.channel ?? state.analytics.channel;
  const scale = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) * mockAnalyticsQueueFactor(queueId) * (previous ? 0.89 : 1);
  const baseByChannel = mockAnalyticsChannelBase();
  const channelKeys = channel && channel !== 'all' ? [channel] : Object.keys(baseByChannel);
  const byChannel = {};
  channelKeys.forEach((key) => {
    const base = baseByChannel[key] || { total: 0, answered: 0, abandoned: 0 };
    byChannel[key] = {
      total: Math.round(base.total * scale),
      answered: Math.round(base.answered * scale),
      abandoned: Math.round(base.abandoned * scale),
    };
  });
  const totals = Object.values(byChannel).reduce((acc, item) => ({
    total: acc.total + Number(item.total || 0),
    answered: acc.answered + Number(item.answered || 0),
    abandoned: acc.abandoned + Number(item.abandoned || 0),
  }), { total: 0, answered: 0, abandoned: 0 });
  const digitalTotal = Object.entries(byChannel)
    .filter(([key]) => key !== 'voice')
    .reduce((acc, [, item]) => acc + Number(item.total || 0), 0);
  const answerRate = totals.total ? (totals.answered / totals.total) * 100 : 0;
  const abandonRate = totals.total ? (totals.abandoned / totals.total) * 100 : 0;
  const sl = Math.max(58, Math.min(96, 84 - (queueId === 'vip_kz' ? 4 : 0) - (channel === 'email' ? 9 : 0) + (previous ? -3 : 0)));
  const asa = Math.max(10, 22 + (queueId === 'support_kz' ? 6 : 0) + (channel === 'email' ? 12 : channel === 'voice' ? 3 : 0) + (previous ? 4 : 0));
  const aht = Math.max(120, 332 + (queueId === 'vip_kz' ? 86 : 0) + (channel === 'email' ? 112 : channel === 'voice' ? 38 : 0) + (previous ? 24 : 0));
  const fcr = Math.max(48, Math.min(92, 73 - (queueId === 'support_kz' ? 5 : 0) + (channel === 'telegram' ? 4 : 0) + (previous ? -2 : 0)));
  return {
    window: {
      from: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
    },
    volume: totals,
    kpi: {
      SL: Number(sl.toFixed(2)),
      ASA: Number(asa.toFixed(2)),
      AHT: Number(aht.toFixed(2)),
      Abandon: Number(abandonRate.toFixed(2)),
      FCR: Number(fcr.toFixed(2)),
      AnswerRate: Number(answerRate.toFixed(2)),
      WaitP95: Number((asa * 2.6).toFixed(2)),
      HandleP95: Number((aht * 1.9).toFixed(2)),
      Occupancy: Number((66 + (queueId === 'support_kz' ? 8 : 0) + (previous ? -3 : 0)).toFixed(2)),
      DigitalShare: totals.total ? Number(((digitalTotal / totals.total) * 100).toFixed(2)) : 0,
    },
    breakdowns: {
      by_channel: byChannel,
    },
    coverage: mockAnalyticsCoverage(totals.total),
    filters: {
      queue_id: queueId === 'all' ? null : queueId,
      channel,
    },
  };
}

function buildMockTrendPayloads(rangeMeta, metric) {
  const overview = buildMockAnalyticsOverview(rangeMeta);
  const baseValue = metricValueFromPayload(metric, overview);
  const buckets = buildTrendBuckets(rangeMeta.current, rangeMeta.preset, rangeMeta.custom);
  const count = Math.max(buckets.length, 1);
  return buckets.map((bucket, index) => {
    const wave = 0.88 + (((index % 5) - 2) * 0.06) + (index / Math.max(count - 1, 1)) * 0.12;
    const nextValue = Math.max(0, baseValue * wave);
    return {
      label: bucket.label,
      range: bucket.range,
      payload: metric === 'volume' || metric === 'total'
        ? {
            ...emptyKpiEnvelope(bucket.range.from.toISOString(), bucket.range.to.toISOString()),
            volume: {
              total: Math.round(nextValue),
              answered: Math.round(nextValue * 0.92),
              abandoned: Math.round(nextValue * 0.08),
            },
            kpi: {
              ...emptyKpiEnvelope().kpi,
              SL: 82 + (index % 4),
              ASA: 16 + index,
              AHT: 320 + index * 8,
              Abandon: 6 + (index % 3),
              FCR: 72 + (index % 5),
              DigitalShare: 44 + (index % 4),
            },
          }
        : {
            ...emptyKpiEnvelope(bucket.range.from.toISOString(), bucket.range.to.toISOString()),
            volume: {
              total: Math.round(overview.volume.total / count),
              answered: Math.round((overview.volume.answered || 0) / count),
              abandoned: Math.round((overview.volume.abandoned || 0) / count),
            },
            kpi: {
              ...emptyKpiEnvelope().kpi,
              [metric]: Number(nextValue.toFixed(2)),
            },
          },
    };
  });
}

function buildMockQueueRows(queueOptions, rangeMeta) {
  const overview = buildMockAnalyticsOverview(rangeMeta);
  const channel = state.analytics.channel;
  return queueOptions.map((queue, index) => {
    const factor = mockAnalyticsQueueFactor(queue.queue_id) * (channel === 'all' ? 1 : 0.82);
    const total = Math.round(overview.volume.total * factor * (0.34 + index * 0.11));
    const answered = Math.round(total * (0.91 - index * 0.03));
    return {
      queue_id: queue.queue_id,
      name: queue.name,
      total,
      answered,
      SL: Number((84 - index * 4).toFixed(2)),
      Abandon: Number((6 + index * 1.4).toFixed(2)),
    };
  }).sort((a, b) => b.total - a.total);
}

function buildMockAgentOverview(rangeMeta, options = {}) {
  const previous = Boolean(options.previous);
  const queueId = options.queueId ?? state.analytics.queueId;
  const queueOptions = mockAnalyticsQueueOptions();
  const agents = mockAnalyticsAgents();
  const stateCycle = Array.isArray(analyticsMockData().agent_state_cycle)
    ? analyticsMockData().agent_state_cycle
    : [];
  const rows = agents.map((agentId, index) => {
    const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz', name: 'Продажи KZ' };
    const dominantQueue = queueId !== 'all' ? queueId : queue.queue_id;
    const interactionsTotal = Math.round((24 - index * 2) * (previous ? 0.88 : 1));
    const answeredTotal = Math.max(0, Math.round(interactionsTotal * (0.88 - index * 0.015)));
    return {
      agent_id: agentId,
      interactions_total: interactionsTotal,
      answered_total: answeredTotal,
      closed_total: Math.max(0, answeredTotal - (index % 3)),
      avg_handle_seconds: Number((290 + index * 24 + (previous ? 18 : 0)).toFixed(2)),
      fcr_rate: Number((78 - index * 3 + (previous ? -2 : 0)).toFixed(2)),
      current_state: stateCycle.length ? stateCycle[index % stateCycle.length] : 'READY',
      current_queue_id: dominantQueue,
      dominant_queue_id: dominantQueue,
      last_activity_at: new Date(rangeMeta.current.to.getTime() - index * 37 * 60 * 1000).toISOString(),
    };
  }).filter((item) => queueId === 'all' || item.current_queue_id === queueId);
  const agentsWithActivity = rows.filter((item) => item.interactions_total > 0).length;
  const interactionsTotal = rows.reduce((acc, item) => acc + item.interactions_total, 0);
  const answeredTotal = rows.reduce((acc, item) => acc + item.answered_total, 0);
  const readyNow = rows.filter((item) => item.current_state === 'READY').length;
  const busyNow = rows.filter((item) => item.current_state === 'BUSY').length;
  const breakNow = rows.filter((item) => item.current_state === 'BREAK').length;
  const offlineNow = rows.filter((item) => item.current_state === 'OFFLINE').length;
  const byTeam = mockAnalyticsQueueOptions()
    .filter((queue) => queueId === 'all' || queue.queue_id === queueId)
    .map((queue, index) => {
      const teamRows = rows.filter((item) => item.current_queue_id === queue.queue_id);
      const teamInteractions = teamRows.reduce((acc, item) => acc + item.interactions_total, 0);
      return {
        team_key: queue.queue_id,
        label: queue.name,
        agents_total: teamRows.length,
        agents_with_activity: teamRows.filter((item) => item.interactions_total > 0).length,
        interactions_total: teamInteractions,
        answered_total: teamRows.reduce((acc, item) => acc + item.answered_total, 0),
        avg_handle_seconds: teamRows.length ? Number((312 + index * 22).toFixed(2)) : null,
        fcr_rate: teamRows.length ? Number((76 - index * 4).toFixed(2)) : null,
        ready_now: teamRows.filter((item) => item.current_state === 'READY').length,
        busy_now: teamRows.filter((item) => item.current_state === 'BUSY').length,
        break_now: teamRows.filter((item) => item.current_state === 'BREAK').length,
        offline_now: teamRows.filter((item) => item.current_state === 'OFFLINE').length,
      };
    })
    .filter((item) => item.agents_total > 0);
  const shiftSeeds = Array.isArray(analyticsMockData().agent_shift_rows)
    ? analyticsMockData().agent_shift_rows
    : [];
  const byShift = shiftSeeds.map((seed) => ({
    shift_key: seed.shift_key,
    label: seed.label,
    agents_with_activity: Math.max(1, Math.round(Number(seed.agents_with_activity || 0) * (previous ? 0.92 : 1))),
    interactions_total: Math.max(0, Math.round(Number(seed.interactions_total || 0) * (previous ? 0.9 : 1))),
    answered_total: Math.max(0, Math.round(Number(seed.answered_total || 0) * (previous ? 0.9 : 1))),
    avg_handle_seconds: Number((Number(seed.avg_handle_seconds || 0) + (previous ? 18 : 0)).toFixed(2)),
    fcr_rate: Number((Number(seed.fcr_rate || 0) + (previous ? -2 : 0)).toFixed(2)),
  }));
  return {
    window: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
    },
    filters: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
      queue_id: queueId === 'all' ? null : queueId,
      channel: state.analytics.channel,
      sort_by: 'interactions_total',
      sort_dir: 'desc',
      limit: 25,
    },
    totals: {
      agents_total: rows.length,
      agents_with_activity: agentsWithActivity,
      interactions_total: interactionsTotal,
      answered_total: answeredTotal,
      closed_total: rows.reduce((acc, item) => acc + item.closed_total, 0),
      ready_now: readyNow,
      busy_now: busyNow,
      break_now: breakNow,
      offline_now: offlineNow,
      avg_interactions_per_agent: rows.length ? Number((interactionsTotal / rows.length).toFixed(2)) : 0,
      avg_handle_seconds: rows.length ? Number((rows.reduce((acc, item) => acc + Number(item.avg_handle_seconds || 0), 0) / rows.length).toFixed(2)) : null,
      avg_fcr_rate: rows.length ? Number((rows.reduce((acc, item) => acc + Number(item.fcr_rate || 0), 0) / rows.length).toFixed(2)) : null,
    },
    state_snapshot: {
      by_state: {
        READY: readyNow,
        BUSY: busyNow,
        BREAK: breakNow,
        OFFLINE: offlineNow,
      },
      updated_at: rangeMeta.current.to.toISOString(),
    },
    breakdowns: {
      by_team: byTeam,
      by_shift: byShift,
    },
    items: rows,
  };
}

function buildMockAgentTrend(rangeMeta, metric) {
  const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
  const points = [];
  let cursor = new Date(interval === 'hour'
    ? rangeMeta.current.from.getTime()
    : startOfDay(rangeMeta.current.from).getTime());
  while (cursor < rangeMeta.current.to) {
    const index = points.length;
    let value = 0;
    if (metric === 'agents_with_activity') {
      value = 5 + (index % 4);
    } else if (metric === 'avg_handle_seconds') {
      value = 296 + index * 8;
    } else if (metric === 'fcr_rate') {
      value = 69 + (index % 5) * 2.2;
    } else {
      value = 3.4 + (index % 4) * 0.42;
    }
    points.push({
      ts: cursor.toISOString(),
      value: Number(value.toFixed(2)),
      agents_with_activity: 5 + (index % 3),
      interactions_total: 18 + index * 2,
    });
    cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000));
  }
  return {
    metric,
    interval,
    filters: {
      from_ts: rangeMeta.current.from.toISOString(),
      to_ts: rangeMeta.current.to.toISOString(),
      queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      channel: state.analytics.channel,
      metric,
      interval,
    },
    points,
  };
}

function buildMockAiOverview(rangeMeta, options = {}) {
  const previous = Boolean(options.previous);
  const channel = options.channel ?? state.analytics.channel;
  if (!aiAnalyticsSupportedChannel(channel)) {
    return emptyAiAnalyticsOverview(
      (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
      channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
  }
  const scale = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) * (previous ? 0.91 : 1);
  const aiChannelRows = Array.isArray(analyticsMockData().ai_channel_rows)
    ? analyticsMockData().ai_channel_rows
    : [];
  const baseRows = aiChannelRows.map((item) => ({
    ...item,
    sessions_started: Math.round(Number(item.sessions_started || 0) * scale),
    ai_only_sessions: Math.round(Number(item.ai_only_sessions || 0) * scale),
    human_touched_sessions: Math.round(Number(item.human_touched_sessions || 0) * scale),
  })).filter((item) => channel === 'all' || item.channel === channel);
  const sessionsStarted = baseRows.reduce((acc, item) => acc + item.sessions_started, 0);
  const sessionsContained = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.containment_rate / 100), 0));
  const sessionsHandoff = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.handoff_rate / 100), 0));
  const sessionsClosedWithoutOperator = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.closed_without_operator_rate / 100), 0));
  const humanTouched = baseRows.reduce((acc, item) => acc + item.human_touched_sessions, 0);
  const byOutcome = [
    { outcome: 'contained', label: 'Закрыто AI', sessions: sessionsContained, share: sessionsStarted ? (sessionsContained / sessionsStarted) * 100 : 0 },
    { outcome: 'handoff', label: 'Передано оператору', sessions: sessionsHandoff, share: sessionsStarted ? (sessionsHandoff / sessionsStarted) * 100 : 0 },
    { outcome: 'human_touched', label: 'С участием оператора', sessions: humanTouched, share: sessionsStarted ? (humanTouched / sessionsStarted) * 100 : 0 },
    { outcome: 'closed_without_operator', label: 'Закрыто без оператора', sessions: sessionsClosedWithoutOperator, share: sessionsStarted ? (sessionsClosedWithoutOperator / sessionsStarted) * 100 : 0 },
    { outcome: 'active', label: 'Активные', sessions: Math.max(8, Math.round(sessionsStarted * 0.06)), share: 6.1 },
    { outcome: 'error', label: 'Ошибки', sessions: Math.max(3, Math.round(sessionsStarted * 0.02)), share: 1.9 },
  ];
  const byReason = [
    'requested_human',
    'knowledge_or_tool_gap',
    'policy_or_sensitive',
    'delivery_or_runtime_error',
    'manual_claim',
  ].map((reasonKey, index) => ({
    reason_key: reasonKey,
    label: mockAiReasonLabel(reasonKey),
    sessions: Math.max(4, Math.round(sessionsHandoff * (0.34 - index * 0.05))),
    share: Math.max(6, Number((32 - index * 5.2).toFixed(2))),
  }));
  return {
    window: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
    },
    filters: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
      queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      channel,
    },
    totals: {
      sessions_started: sessionsStarted,
      sessions_contained: sessionsContained,
      sessions_handoff: sessionsHandoff,
      sessions_closed: Math.round(sessionsStarted * 0.86),
      sessions_closed_without_operator: sessionsClosedWithoutOperator,
      assistant_turns: Math.round(sessionsStarted * 5.4),
    },
    metrics: {
      containment_rate: sessionsStarted ? Number(((sessionsContained / sessionsStarted) * 100).toFixed(2)) : 0,
      handoff_rate: sessionsStarted ? Number(((sessionsHandoff / sessionsStarted) * 100).toFixed(2)) : 0,
      ai_latency_avg_ms: 1452,
      ai_latency_p95_ms: 2486,
      closed_without_operator_rate: sessionsStarted ? Number(((sessionsClosedWithoutOperator / sessionsStarted) * 100).toFixed(2)) : 0,
      human_touched_rate: sessionsStarted ? Number(((humanTouched / sessionsStarted) * 100).toFixed(2)) : 0,
    },
    breakdowns: {
      by_channel: baseRows,
      by_outcome: byOutcome,
      by_handoff_reason: byReason,
    },
    coverage: {
      sessions_with_interaction_id: Math.round(sessionsStarted * 0.94),
      sessions_with_queue_id: Math.round(sessionsStarted * 0.91),
      sessions_with_latency_turns: Math.round(sessionsStarted * 0.97),
      sessions_with_terminal_state: Math.round(sessionsStarted * 0.88),
      sessions_with_handoff_reason: Math.round(sessionsHandoff * 0.82),
    },
  };
}

function buildMockAiTrend(rangeMeta, metric) {
  const interval = aiAnalyticsIntervalForRange(rangeMeta);
  const points = [];
  let cursor = new Date(interval === 'hour'
    ? rangeMeta.current.from.getTime()
    : startOfDay(rangeMeta.current.from).getTime());
  while (cursor < rangeMeta.current.to) {
    const index = points.length;
    let value = 0;
    if (metric === 'handoff_rate') {
      value = 21 + (index % 5) * 1.8;
    } else if (metric === 'human_touched_rate') {
      value = 32 + (index % 4) * 1.6;
    } else if (metric === 'ai_latency_avg_ms') {
      value = 1380 + index * 34;
    } else if (metric === 'closed_without_operator_rate') {
      value = 47 + (index % 4) * 1.9;
    } else {
      value = 54 + (index % 5) * 2.1;
    }
    points.push({
      ts: cursor.toISOString(),
      value: Number(value.toFixed(2)),
      sessions: 24 + index * 3,
      assistant_turns: 118 + index * 11,
    });
    cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000));
  }
  return {
    metric,
    interval,
    filters: {
      from_ts: rangeMeta.current.from.toISOString(),
      to_ts: rangeMeta.current.to.toISOString(),
      queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      channel: state.analytics.channel,
    },
    points,
  };
}

function mockVoiceNameSeed() {
  const seed = analyticsMockData().voice_name_flow;
  return seed && typeof seed === 'object' ? seed : {};
}

function mockVoiceNameLabels() {
  const labels = analyticsMockData().voice_name_labels;
  return labels && typeof labels === 'object' ? labels : {};
}

function mockVoiceNameLabel(key, fallback) {
  return mockVoiceNameLabels()[key] || fallback;
}

function mockVoiceNameProfile(metric, fallback) {
  const profile = mockVoiceNameSeed().trend_profiles?.[metric];
  if (!profile || typeof profile !== 'object') {
    return fallback;
  }
  return {
    base: Number.isFinite(Number(profile.base)) ? Number(profile.base) : fallback.base,
    step: Number.isFinite(Number(profile.step)) ? Number(profile.step) : fallback.step,
    cycle: Math.max(1, Number.isFinite(Number(profile.cycle)) ? Number(profile.cycle) : fallback.cycle),
  };
}

function voiceNameMetricRatesFromTotals(totals) {
  const scenarioCalls = Number(totals.scenario_calls || 0);
  const neededDownstream = Number(totals.needed_downstream || Math.max(0, scenarioCalls - Number(totals.start_obtained || 0)));
  const allHandoffs = Number(totals.handoff_confirmed_name || 0) + Number(totals.handoff_unconfirmed_name || 0);
  return {
    start_capture_rate: scenarioCalls ? Number(((Number(totals.start_obtained || 0) / scenarioCalls) * 100).toFixed(2)) : 0,
    downstream_rescue_rate: neededDownstream ? Number(((Number(totals.downstream_ai_obtained || 0) / neededDownstream) * 100).toFixed(2)) : 0,
    handoff_unconfirmed_rate: allHandoffs ? Number(((Number(totals.handoff_unconfirmed_name || 0) / allHandoffs) * 100).toFixed(2)) : 0,
    manual_correction_rate: allHandoffs ? Number(((Number(totals.manual_corrected || 0) / allHandoffs) * 100).toFixed(2)) : 0,
  };
}

function buildMockVoiceNameOverview(rangeMeta, options = {}) {
  const previous = Boolean(options.previous);
  const channel = options.channel ?? state.analytics.channel;
  if (!(channel === 'all' || channel === 'voice')) {
    return emptyVoiceNameAnalyticsOverview(
      (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      null,
    );
  }
  const seed = mockVoiceNameSeed();
  const totalSeed = seed.totals || {};
  const queueId = options.queueId ?? state.analytics.queueId;
  const factor = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current)
    * (previous ? 0.91 : 1)
    * (queueId === 'all' ? 1 : Math.max(0.48, mockAnalyticsQueueFactor(queueId)));
  const baseScenarioCalls = Math.max(Number(totalSeed.scenario_calls || 180), 1);
  const scenarioCalls = Math.max(0, Math.round(baseScenarioCalls * factor));
  const startRatio = Number(totalSeed.start_obtained || 110) / baseScenarioCalls;
  const downstreamRatio = Number(totalSeed.downstream_ai_obtained || 36) / baseScenarioCalls;
  const followupRatio = Number(totalSeed.followup_required || 20) / baseScenarioCalls;
  const handoffConfirmedRatio = Number(totalSeed.handoff_confirmed_name || 12) / baseScenarioCalls;
  const handoffUnconfirmedRatio = Number(totalSeed.handoff_unconfirmed_name || 26) / baseScenarioCalls;
  const manualRateSeed = Number(totalSeed.manual_corrected || 10)
    / Math.max((Number(totalSeed.handoff_confirmed_name || 12) + Number(totalSeed.handoff_unconfirmed_name || 26)), 1);

  const startObtained = Math.min(scenarioCalls, Math.round(scenarioCalls * startRatio));
  const downstreamAiObtained = Math.max(0, Math.round(scenarioCalls * downstreamRatio));
  const followupRequired = Math.max(0, Math.round(scenarioCalls * followupRatio));
  const nameNotObtained = Math.max(0, scenarioCalls - startObtained - downstreamAiObtained - followupRequired);
  const neededDownstream = Math.max(0, scenarioCalls - startObtained);
  const handoffConfirmedName = Math.max(0, Math.round(scenarioCalls * handoffConfirmedRatio));
  const handoffUnconfirmedName = Math.max(0, Math.round(scenarioCalls * handoffUnconfirmedRatio));
  const allHandoffs = Math.max(0, handoffConfirmedName + handoffUnconfirmedName);
  const manualCorrected = Math.min(allHandoffs, Math.round(allHandoffs * manualRateSeed));
  const totals = {
    scenario_calls: scenarioCalls,
    start_obtained: startObtained,
    downstream_ai_obtained: downstreamAiObtained,
    followup_required: followupRequired,
    name_not_obtained: nameNotObtained,
    manual_corrected: manualCorrected,
    handoff_confirmed_name: handoffConfirmedName,
    handoff_unconfirmed_name: handoffUnconfirmedName,
    needed_downstream: neededDownstream,
  };
  const metrics = voiceNameMetricRatesFromTotals(totals);

  const languageSeeds = Array.isArray(seed.languages) ? seed.languages : [];
  const languageRows = languageSeeds.map((item) => {
    const share = Number(item.share || 0);
    const calls = Math.max(0, Math.round(scenarioCalls * share));
    const startCaptured = Math.min(calls, Math.round(calls * Number(item.start_capture_rate || 0) / 100));
    const downstreamNeeded = Math.max(0, calls - startCaptured);
    const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * Number(item.downstream_rescue_rate || 0) / 100));
    const unresolved = Math.max(0, calls - startCaptured - downstreamCaptured);
    const followup = Math.round(unresolved * 0.58);
    const missing = Math.max(0, unresolved - followup);
    const handoffs = Math.round(unresolved * 0.72);
    const unconfirmed = Math.min(handoffs, Math.round(handoffs * Number(item.handoff_unconfirmed_rate || 0) / 100));
    const confirmed = Math.max(0, handoffs - unconfirmed);
    const manual = Math.min(handoffs, Math.round(handoffs * Number(item.manual_correction_rate || 0) / 100));
    return {
      language: item.language || 'unknown',
      scenario_calls: calls,
      start_obtained: startCaptured,
      downstream_ai_obtained: downstreamCaptured,
      followup_required: followup,
      name_not_obtained: missing,
      manual_corrected: manual,
      handoff_confirmed_name: confirmed,
      handoff_unconfirmed_name: unconfirmed,
      ...voiceNameMetricRatesFromTotals({
        scenario_calls: calls,
        start_obtained: startCaptured,
        downstream_ai_obtained: downstreamCaptured,
        needed_downstream: downstreamNeeded,
        handoff_confirmed_name: confirmed,
        handoff_unconfirmed_name: unconfirmed,
        manual_corrected: manual,
      }),
    };
  }).filter((item) => item.scenario_calls > 0);

  const queueSeeds = Array.isArray(seed.queues) ? seed.queues : [];
  const queueRows = (queueSeeds.length ? queueSeeds : mockAnalyticsQueueOptions().map((item, index) => ({
    queue_id: item.queue_id,
    share: 0.2 + index * 0.12,
    start_capture_rate: 69 - index * 4,
    downstream_rescue_rate: 45 - index * 5,
    handoff_unconfirmed_rate: 38 + index * 5,
    manual_correction_rate: 17 + index * 3,
  })))
    .filter((item) => queueId === 'all' || item.queue_id === queueId)
    .map((item) => {
      const calls = Math.max(0, Math.round(scenarioCalls * Number(item.share || 0)));
      const startCaptured = Math.min(calls, Math.round(calls * Number(item.start_capture_rate || 0) / 100));
      const downstreamNeeded = Math.max(0, calls - startCaptured);
      const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * Number(item.downstream_rescue_rate || 0) / 100));
      const unresolved = Math.max(0, calls - startCaptured - downstreamCaptured);
      const followup = Math.round(unresolved * 0.58);
      const missing = Math.max(0, unresolved - followup);
      const handoffs = Math.round(unresolved * 0.72);
      const unconfirmed = Math.min(handoffs, Math.round(handoffs * Number(item.handoff_unconfirmed_rate || 0) / 100));
      const confirmed = Math.max(0, handoffs - unconfirmed);
      const manual = Math.min(handoffs, Math.round(handoffs * Number(item.manual_correction_rate || 0) / 100));
      return {
        queue_id: item.queue_id,
        scenario_calls: calls,
        start_obtained: startCaptured,
        downstream_ai_obtained: downstreamCaptured,
        followup_required: followup,
        name_not_obtained: missing,
        manual_corrected: manual,
        handoff_confirmed_name: confirmed,
        handoff_unconfirmed_name: unconfirmed,
        ...voiceNameMetricRatesFromTotals({
          scenario_calls: calls,
          start_obtained: startCaptured,
          downstream_ai_obtained: downstreamCaptured,
          needed_downstream: downstreamNeeded,
          handoff_confirmed_name: confirmed,
          handoff_unconfirmed_name: unconfirmed,
          manual_corrected: manual,
        }),
      };
    })
    .filter((item) => item.scenario_calls > 0);

  const coverageSeed = seed.coverage || {};
  const coverage = {
    sessions_with_start_decision: Math.min(
      scenarioCalls,
      Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_start_decision || baseScenarioCalls) / baseScenarioCalls)),
    ),
    sessions_with_final_ai_state: Math.min(
      scenarioCalls,
      Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_final_ai_state || baseScenarioCalls) / baseScenarioCalls)),
    ),
    sessions_with_manual_overlay: Math.min(
      scenarioCalls,
      Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_manual_overlay || manualCorrected) / baseScenarioCalls)),
    ),
    note: coverageSeed.note || analyticsMockData().coverage_note || 'Показаны демонстрационные данные по name-flow.',
  };

  return {
    window: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
    },
    filters: {
      from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(),
      to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(),
      queue_id: queueId === 'all' ? null : queueId,
      language: null,
    },
    totals,
    metrics,
    breakdowns: {
      funnel: [
        { stage: 'scenario_calls', label: mockVoiceNameLabel('scenario_calls', 'Звонки в сценарии'), sessions: scenarioCalls, share: 100 },
        { stage: 'start_obtained', label: mockVoiceNameLabel('start_obtained', 'Имя взято сразу'), sessions: startObtained, share: scenarioCalls ? Number(((startObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
        { stage: 'needed_downstream', label: mockVoiceNameLabel('needed_downstream', 'Потребовался AI после старта'), sessions: neededDownstream, share: scenarioCalls ? Number(((neededDownstream / scenarioCalls) * 100).toFixed(2)) : 0 },
        { stage: 'downstream_ai_obtained', label: mockVoiceNameLabel('downstream_ai_obtained', 'Имя добрал AI после follow-up'), sessions: downstreamAiObtained, share: scenarioCalls ? Number(((downstreamAiObtained / scenarioCalls) * 100).toFixed(2)) : 0 },
        { stage: 'handoff_confirmed_name', label: mockVoiceNameLabel('handoff_confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: scenarioCalls ? Number(((handoffConfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
        { stage: 'handoff_unconfirmed_name', label: mockVoiceNameLabel('handoff_unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: scenarioCalls ? Number(((handoffUnconfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 },
      ],
      by_language: languageRows,
      by_queue: queueRows,
      handoff: [
        { outcome: 'confirmed_name', label: mockVoiceNameLabel('confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: allHandoffs ? Number(((handoffConfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
        { outcome: 'unconfirmed_name', label: mockVoiceNameLabel('unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: allHandoffs ? Number(((handoffUnconfirmedName / allHandoffs) * 100).toFixed(2)) : 0 },
      ],
    },
    coverage,
  };
}

function buildMockVoiceNameTrend(rangeMeta, metric) {
  const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
  const scenarioProfile = mockVoiceNameProfile('scenario_calls', { base: 24, step: 2.4, cycle: 4 });
  const metricProfile = metric === 'scenario_calls'
    ? scenarioProfile
    : mockVoiceNameProfile(metric, { base: 60, step: 2.1, cycle: 4 });
  const queueFactor = Math.max(0.48, mockAnalyticsQueueFactor(state.analytics.queueId === 'all' ? 'sales_kz' : state.analytics.queueId));
  const points = [];
  let cursor = new Date(interval === 'hour'
    ? rangeMeta.current.from.getTime()
    : startOfDay(rangeMeta.current.from).getTime());
  while (cursor < rangeMeta.current.to) {
    const index = points.length;
    const scenarioCalls = Math.max(
      4,
      Math.round((scenarioProfile.base + (index % scenarioProfile.cycle) * scenarioProfile.step) * queueFactor),
    );
    const neededDownstream = Math.max(1, Math.round(scenarioCalls * 0.35));
    const handoffs = Math.max(1, Math.round(scenarioCalls * 0.18));
    let value = 0;
    let denominator = scenarioCalls;
    if (metric === 'scenario_calls') {
      value = scenarioCalls;
      denominator = scenarioCalls;
    } else if (metric === 'downstream_rescue_rate') {
      value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
      denominator = neededDownstream;
    } else if (metric === 'handoff_unconfirmed_rate' || metric === 'manual_correction_rate') {
      value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
      denominator = handoffs;
    } else {
      value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step;
      denominator = scenarioCalls;
    }
    points.push({
      ts: cursor.toISOString(),
      value: Number(value.toFixed(metric === 'scenario_calls' ? 0 : 2)),
      scenario_calls: scenarioCalls,
      denominator,
    });
    cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000));
  }
  return {
    metric,
    interval,
    filters: {
      from_ts: rangeMeta.current.from.toISOString(),
      to_ts: rangeMeta.current.to.toISOString(),
      queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      language: null,
    },
    points,
  };
}

function buildMockInteractionDrilldownData(filters, limit, offset, mode) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const queueOptions = mockAnalyticsQueueOptions();
  const agents = mockAnalyticsAgents();
  const total = 18;
  const rows = Array.from({ length: total }, (_, index) => {
    const queueId = normalized.queue_id || queueOptions[index % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz';
    let channel = normalized.channel || ['voice', 'telegram', 'whatsapp', 'webchat', 'email'][index % 5];
    if (mode === 'metric' && normalized.metric === 'DigitalShare') {
      channel = ['telegram', 'whatsapp', 'webchat', 'email'][index % 4];
    }
    const answered = normalized.metric === 'Abandon' ? false : index % 5 !== 0;
    const abandoned = normalized.metric === 'Abandon' ? true : index % 6 === 0;
    const withinSla = normalized.metric === 'SL' ? true : answered ? index % 4 !== 0 : null;
    const resolvedFirstContact = normalized.metric === 'FCR' ? true : answered ? index % 3 === 0 : null;
    const createdAt = new Date(Date.now() - (index + 1) * 3 * 60 * 60 * 1000);
    return {
      interaction_id: `mock-int-${index + 1}`,
      subject: `${mode === 'metric' ? 'KPI' : 'Обращение'} ${index + 1}: ${analyticsChannelLabel(channel)}`,
      channel,
      status: abandoned ? 'closed' : mockInteractionStatus(index),
      queue_id: queueId,
      assigned_to: normalized.agent_id || (abandoned ? null : (agents[index % Math.max(agents.length, 1)] || null)),
      created_at: createdAt.toISOString(),
      updated_at: new Date(createdAt.getTime() + 42 * 60 * 1000).toISOString(),
      answered,
      abandoned,
      wait_seconds: abandoned ? 44 + index : 12 + index * 2,
      handle_seconds: answered ? 210 + index * 14 : 0,
      within_sla: withinSla,
      resolved_first_contact: resolvedFirstContact,
    };
  }).filter((item) => {
    if (normalized.channel && item.channel !== normalized.channel) {
      return false;
    }
    if (normalized.queue_id && item.queue_id !== normalized.queue_id) {
      return false;
    }
    if (normalized.agent_id && item.assigned_to !== normalized.agent_id) {
      return false;
    }
    return true;
  });
  const paged = rows.slice(offset, offset + limit);
  return {
    items: paged,
    total: rows.length,
    limit,
    offset,
    filters: normalized,
    metric: normalized.metric || '',
    coverage: mockAnalyticsCoverage(rows.length),
  };
}

function buildMockInteractionDetail(interactionId) {
  const queueOptions = mockAnalyticsQueueOptions();
  const agents = mockAnalyticsAgents();
  const interaction = {
    interaction_id: interactionId,
    subject: `Карточка ${interactionId}`,
    channel: ['voice', 'telegram', 'whatsapp'][interactionId.length % 3],
    status: 'closed',
    queue_id: queueOptions[interactionId.length % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz',
    assigned_to: agents[interactionId.length % Math.max(agents.length, 1)] || null,
    created_at: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(),
    updated_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
    wait_seconds: 18,
    handle_seconds: 324,
    within_sla: true,
    resolved_first_contact: true,
    abandoned: false,
  };
  const timeline = {
    events: [
      { action: 'Создано обращение', timestamp: interaction.created_at, metadata: { channel: interaction.channel, queue_id: interaction.queue_id } },
      { action: 'Назначено оператору', timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(), metadata: { assigned_to: interaction.assigned_to } },
      { action: 'Закрыто', timestamp: interaction.updated_at, metadata: { resolved_first_contact: true, within_sla: true } },
    ],
  };
  return { detail: interaction, timeline };
}

function buildMockAiDrilldownData(filters, limit, offset) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const queueOptions = mockAnalyticsQueueOptions();
  const agents = mockAnalyticsAgents();
  const total = 14;
  const allItems = Array.from({ length: total }, (_, index) => {
    const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz' };
    const channel = normalized.channel && normalized.channel !== 'all'
      ? normalized.channel
      : ['telegram', 'whatsapp'][index % 2];
    const slice = normalized.slice || 'all';
    const reasonKey = normalized.reason_key || ['requested_human', 'knowledge_or_tool_gap', 'manual_claim'][index % 3];
    const handoff = slice === 'handoff' || slice === 'human_touched' || index % 4 === 0;
    const contained = slice === 'contained' || (!handoff && index % 2 === 0);
    const humanTouched = slice === 'human_touched' || handoff;
    const closedWithoutOperator = slice === 'closed_without_operator' || (contained && index % 3 !== 0);
    const createdAt = new Date(Date.now() - (index + 2) * 2 * 60 * 60 * 1000);
    return {
      session_id: `mock-ai-${index + 1}`,
      interaction_id: `mock-int-${index + 1}`,
      thread_id: `thread-${index + 1}`,
      channel,
      queue_id: queue.queue_id,
      status: handoff ? 'human_owned' : contained ? 'closed' : 'active',
      reason_key: handoff ? reasonKey : '',
      reason_label: handoff ? mockAiReasonLabel(reasonKey) : '',
      raw_handoff_reason: handoff ? `raw:${reasonKey}` : '',
      assigned_to: handoff ? (agents[index % Math.max(agents.length, 1)] || null) : null,
      claimed_by_user: handoff ? `sup-${(index % 3) + 1}` : null,
      assistant_turns: 6 + index,
      user_turns: 3 + (index % 4),
      tool_turns: 1 + (index % 2),
      ai_latency_avg_ms: 1280 + index * 44,
      ai_latency_p95_ms: 2010 + index * 61,
      created_at: createdAt.toISOString(),
      updated_at: new Date(createdAt.getTime() + 55 * 60 * 1000).toISOString(),
      closed_at: contained || handoff ? new Date(createdAt.getTime() + 94 * 60 * 1000).toISOString() : null,
      contained,
      handoff,
      human_touched: humanTouched,
      closed_without_operator: closedWithoutOperator,
    };
  }).filter((item) => {
    if (normalized.channel && normalized.channel !== 'all' && item.channel !== normalized.channel) {
      return false;
    }
    if (normalized.queue_id && item.queue_id !== normalized.queue_id) {
      return false;
    }
    if (normalized.reason_key && item.reason_key !== normalized.reason_key) {
      return false;
    }
    if (normalized.slice === 'contained' && !item.contained) {
      return false;
    }
    if (normalized.slice === 'handoff' && !item.handoff) {
      return false;
    }
    if (normalized.slice === 'human_touched' && !item.human_touched) {
      return false;
    }
    if (normalized.slice === 'closed_without_operator' && !item.closed_without_operator) {
      return false;
    }
    if (normalized.slice === 'active' && item.status !== 'active') {
      return false;
    }
    if (normalized.status && item.status !== normalized.status) {
      return false;
    }
    return true;
  });
  return {
    items: allItems.slice(offset, offset + limit),
    total: allItems.length,
    limit,
    offset,
    filters: normalized,
    coverage: {
      sessions_with_interaction_id: allItems.length,
      sessions_with_queue_id: allItems.length,
      sessions_with_latency_turns: allItems.length,
      sessions_with_terminal_state: Math.round(allItems.length * 0.86),
      sessions_with_handoff_reason: allItems.filter((item) => item.reason_key).length,
    },
  };
}

function buildMockAiDetail(sessionId) {
  const session = buildMockAiDrilldownData({ slice: 'all' }, 20, 0).items.find((item) => item.session_id === sessionId)
    || buildMockAiDrilldownData({ slice: 'all' }, 1, 0).items[0];
  return {
    session,
    interaction: {
      interaction_id: session.interaction_id,
      channel: session.channel,
      queue_id: session.queue_id,
      status: session.handoff ? 'in_progress' : 'closed',
      assigned_to: session.assigned_to,
      subject: `Связанное обращение ${session.interaction_id}`,
      updated_at: session.updated_at,
    },
    timeline: [
      { label: 'Создана AI-сессия', event_type: 'session.created', ts: session.created_at, metadata: { channel: session.channel, queue_id: session.queue_id } },
      { label: 'Ответ AI', event_type: 'assistant.turn', ts: new Date(new Date(session.created_at).getTime() + 12 * 60 * 1000).toISOString(), metadata: { assistant_turns: session.assistant_turns } },
      { label: session.handoff ? 'Передано оператору' : 'Сессия закрыта', event_type: session.handoff ? 'session.handoff' : 'session.closed', ts: session.updated_at, metadata: { reason_key: session.reason_key || null, assigned_to: session.assigned_to || null } },
    ],
  };
}

function buildMockAnalyticsDashboard(rangeMeta) {
  const queueOptions = mockAnalyticsQueueOptions();
  const overview = buildMockAnalyticsOverview(rangeMeta);
  return {
    queueOptions,
    overview,
    compare: buildMockAnalyticsOverview(rangeMeta, { previous: true }),
    coverage: {
      implemented_metrics: Array.isArray(analyticsMockData().dashboard_coverage?.implemented_metrics)
        ? analyticsMockData().dashboard_coverage.implemented_metrics
        : [],
      dimensions: Array.isArray(analyticsMockData().dashboard_coverage?.dimensions)
        ? analyticsMockData().dashboard_coverage.dimensions
        : [],
      supported_filters: Array.isArray(analyticsMockData().dashboard_coverage?.supported_filters)
        ? analyticsMockData().dashboard_coverage.supported_filters
        : [],
    },
    voiceNameOverview: buildMockVoiceNameOverview(rangeMeta),
    voiceNameCompare: buildMockVoiceNameOverview(rangeMeta, { previous: true }),
    aiOverview: buildMockAiOverview(rangeMeta),
    aiCompare: buildMockAiOverview(rangeMeta, { previous: true }),
    agentOverview: buildMockAgentOverview(rangeMeta),
    agentCompare: buildMockAgentOverview(rangeMeta, { previous: true }),
    trend: buildMockTrendPayloads(rangeMeta, state.analytics.trendMetric || 'volume'),
    voiceNameTrend: buildMockVoiceNameTrend(rangeMeta, state.analytics.voiceNameTrendMetric || 'scenario_calls'),
    aiTrend: buildMockAiTrend(rangeMeta, state.analytics.aiTrendMetric || 'containment_rate'),
    agentTrend: buildMockAgentTrend(rangeMeta, state.analytics.agentTrendMetric || 'interactions_per_agent'),
    queueRows: buildMockQueueRows(queueOptions, rangeMeta),
    channelRows: Object.entries(overview.breakdowns.by_channel).map(([channel, payload]) => ({
      channel,
      total: Number(payload?.total || 0),
      answered: Number(payload?.answered || 0),
      abandoned: Number(payload?.abandoned || 0),
    })).sort((a, b) => b.total - a.total),
  };
}

function applyAnalyticsSnapshot(snapshot = {}) {
  state.analytics.preset = snapshot.preset || '7d';
  state.analytics.fromTs = snapshot.fromTs || '';
  state.analytics.toTs = snapshot.toTs || '';
  state.analytics.queueId = snapshot.queueId || 'all';
  state.analytics.channel = snapshot.channel || 'all';
  state.analytics.compareMode = snapshot.compareMode || 'previous';
  state.analytics.trendMetric = snapshot.trendMetric || 'volume';
  state.analytics.voiceNameTrendMetric = snapshot.voiceNameTrendMetric || 'scenario_calls';
  state.analytics.aiTrendMetric = snapshot.aiTrendMetric || 'containment_rate';
  state.analytics.agentTrendMetric = snapshot.agentTrendMetric || 'interactions_per_agent';
}

function withAnalyticsUrlSyncSuspended(callback) {
  analyticsUrlSyncSuspended = true;
  try {
    return callback();
  } finally {
    analyticsUrlSyncSuspended = false;
  }
}

function sanitizeAnalyticsSnapshot(snapshot = {}) {
  const validChannel = ANALYTICS_CHANNEL_OPTIONS.some((item) => item.value === snapshot.channel);
  return {
    preset: Object.prototype.hasOwnProperty.call(ANALYTICS_PRESET_LABELS, snapshot.preset) ? snapshot.preset : '7d',
    fromTs: snapshot.fromTs || '',
    toTs: snapshot.toTs || '',
    queueId: snapshot.queueId || 'all',
    channel: validChannel ? snapshot.channel : 'all',
    compareMode: snapshot.compareMode === 'off' ? 'off' : 'previous',
    trendMetric: ANALYTICS_TREND_OPTIONS.includes(snapshot.trendMetric) ? snapshot.trendMetric : 'volume',
    voiceNameTrendMetric: VOICE_NAME_TREND_OPTIONS.includes(snapshot.voiceNameTrendMetric)
      ? snapshot.voiceNameTrendMetric
      : 'scenario_calls',
    aiTrendMetric: AI_ANALYTICS_TREND_OPTIONS.includes(snapshot.aiTrendMetric)
      ? snapshot.aiTrendMetric
      : 'containment_rate',
    agentTrendMetric: AGENT_ANALYTICS_TREND_OPTIONS.includes(snapshot.agentTrendMetric)
      ? snapshot.agentTrendMetric
      : 'interactions_per_agent',
  };
}

function analyticsDeepLinkDrilldownState() {
  if (!state.drilldown.open || !state.drilldown.filters) {
    return null;
  }
  return {
    mode: state.drilldown.mode || 'interaction',
    sourceType: state.drilldown.sourceType || '',
    sourceValue: state.drilldown.sourceValue || '',
    metric: state.drilldown.metric || '',
    filters: normalizeAnalyticsDrilldownFilters(state.drilldown.filters),
    offset: Number(state.drilldown.offset || 0),
    selectedId: state.drilldown.selectedInteractionId || '',
  };
}

function syncAnalyticsUrlState() {
  if (analyticsUrlSyncSuspended) {
    return;
  }
  const url = new URL(window.location.href);
  ANALYTICS_DEEP_LINK_KEYS.forEach((key) => {
    url.searchParams.delete(key);
  });

  const snapshot = sanitizeAnalyticsSnapshot(analyticsCurrentSnapshot());
  url.searchParams.set('preset', snapshot.preset);
  if (snapshot.fromTs) {
    url.searchParams.set('from', snapshot.fromTs);
  }
  if (snapshot.toTs) {
    url.searchParams.set('to', snapshot.toTs);
  }
  if (snapshot.queueId && snapshot.queueId !== 'all') {
    url.searchParams.set('queue', snapshot.queueId);
  }
  if (snapshot.channel && snapshot.channel !== 'all') {
    url.searchParams.set('channel', snapshot.channel);
  }
  if (snapshot.compareMode !== 'previous') {
    url.searchParams.set('compare', snapshot.compareMode);
  }
  if (snapshot.trendMetric !== 'volume') {
    url.searchParams.set('trend', snapshot.trendMetric);
  }
  if (snapshot.voiceNameTrendMetric !== 'scenario_calls') {
    url.searchParams.set('voice_name_trend', snapshot.voiceNameTrendMetric);
  }
  if (snapshot.aiTrendMetric !== 'containment_rate') {
    url.searchParams.set('ai_trend', snapshot.aiTrendMetric);
  }
  if (snapshot.agentTrendMetric !== 'interactions_per_agent') {
    url.searchParams.set('agent_trend', snapshot.agentTrendMetric);
  }
  if (state.analytics.activeViewId) {
    url.searchParams.set('view', state.analytics.activeViewId);
  }

  const drilldown = analyticsDeepLinkDrilldownState();
  if (drilldown) {
    url.searchParams.set('dd', '1');
    url.searchParams.set('dd_mode', drilldown.mode);
    url.searchParams.set('dd_source', drilldown.sourceType || 'overview');
    if (drilldown.sourceValue) {
      url.searchParams.set('dd_value', drilldown.sourceValue);
    }
    if (drilldown.metric) {
      url.searchParams.set('dd_metric', drilldown.metric);
    }
    if (drilldown.filters.slice && drilldown.filters.slice !== 'all') {
      url.searchParams.set('dd_slice', drilldown.filters.slice);
    }
    if (drilldown.filters.reason_key) {
      url.searchParams.set('dd_reason', drilldown.filters.reason_key);
    }
    if (drilldown.filters.status) {
      url.searchParams.set('dd_status', drilldown.filters.status);
    }
    if (drilldown.filters.q) {
      url.searchParams.set('dd_q', drilldown.filters.q);
    }
    if (drilldown.filters.sort_by && drilldown.filters.sort_by !== 'created_at') {
      url.searchParams.set('dd_sort_by', drilldown.filters.sort_by);
    }
    if (drilldown.filters.sort_dir && drilldown.filters.sort_dir !== 'desc') {
      url.searchParams.set('dd_sort_dir', drilldown.filters.sort_dir);
    }
    if (drilldown.filters.agent_id) {
      url.searchParams.set('dd_agent', drilldown.filters.agent_id);
    }
    if (drilldown.offset > 0) {
      url.searchParams.set('dd_offset', String(drilldown.offset));
    }
    if (drilldown.selectedId) {
      url.searchParams.set('dd_selected', drilldown.selectedId);
    }
  }

  const nextUrl = `${url.pathname}${url.search}${url.hash}`;
  const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
  if (nextUrl !== currentUrl) {
    window.history.replaceState(null, '', nextUrl);
  }
}

function parseAnalyticsDeepLinkState() {
  const params = new URLSearchParams(window.location.search);
  const hasSnapshot = [
    'preset',
    'from',
    'to',
    'queue',
    'channel',
    'compare',
    'trend',
    'voice_name_trend',
    'ai_trend',
    'agent_trend',
  ].some((key) => params.has(key));
  const snapshot = sanitizeAnalyticsSnapshot({
    preset: params.get('preset') || '7d',
    fromTs: params.get('from') || '',
    toTs: params.get('to') || '',
    queueId: params.get('queue') || 'all',
    channel: params.get('channel') || 'all',
    compareMode: params.get('compare') || 'previous',
    trendMetric: params.get('trend') || 'volume',
    voiceNameTrendMetric: params.get('voice_name_trend') || 'scenario_calls',
    aiTrendMetric: params.get('ai_trend') || 'containment_rate',
    agentTrendMetric: params.get('agent_trend') || 'interactions_per_agent',
  });
  let drilldown = null;
  if (params.get('dd') === '1') {
    const mode = ['interaction', 'metric', 'ai'].includes(params.get('dd_mode'))
      ? params.get('dd_mode')
      : 'interaction';
    drilldown = {
      mode,
      sourceType: params.get('dd_source') || (mode === 'metric' ? 'metric' : mode === 'ai' ? 'ai-overview' : 'overview'),
      sourceValue: params.get('dd_value') || '',
      metric: params.get('dd_metric') || '',
      offset: Math.max(0, Number(params.get('dd_offset') || 0)),
      selectedId: params.get('dd_selected') || '',
      filters: normalizeAnalyticsDrilldownFilters({
        metric: params.get('dd_metric') || '',
        slice: params.get('dd_slice') || 'all',
        reason_key: params.get('dd_reason') || '',
        agent_id: params.get('dd_agent') || '',
        status: params.get('dd_status') || '',
        q: params.get('dd_q') || '',
        sort_by: params.get('dd_sort_by') || 'created_at',
        sort_dir: params.get('dd_sort_dir') || 'desc',
      }),
    };
  }
  return {
    hasSnapshot,
    snapshot,
    activeViewId: params.get('view') || '',
    drilldown,
  };
}

function restoreAnalyticsDrilldownFromUrlState(drilldownState) {
  if (!drilldownState) {
    return;
  }
  const mode = ['interaction', 'metric', 'ai'].includes(drilldownState.mode) ? drilldownState.mode : 'interaction';
  const sourceType = drilldownState.sourceType || (mode === 'metric' ? 'metric' : mode === 'ai' ? 'ai-overview' : 'overview');
  const sourceValue = drilldownState.sourceValue || '';
  const filters = {
    ...analyticsDrilldownBaseFilters(state.analytics.lastRangeMeta || analyticsRangeFromControls()),
    ...normalizeAnalyticsDrilldownFilters(drilldownState.filters || {}),
  };
  if (mode === 'metric') {
    filters.metric = drilldownState.metric || filters.metric || sourceValue;
    filters.status = '';
    filters.q = '';
  }
  if (mode === 'ai') {
    filters.slice = filters.slice || 'all';
    if (sourceType === 'ai-overview' && sourceValue) {
      filters.slice = sourceValue;
    }
    if (sourceType === 'ai-channel' && sourceValue) {
      filters.channel = sourceValue;
    }
    if (sourceType === 'ai-reason' && sourceValue) {
      filters.slice = 'handoff';
      filters.reason_key = sourceValue;
    }
  }
  if (sourceType === 'channel' && sourceValue) {
    filters.channel = sourceValue;
  }
  if (sourceType === 'queue' && sourceValue) {
    filters.queue_id = sourceValue;
  }
  if (sourceType === 'agent' && sourceValue) {
    filters.agent_id = sourceValue;
  }
  state.drilldown = {
    ...emptyDrilldownState(),
    open: true,
    mode,
    sourceType,
    sourceLabel: analyticsDrilldownSourceLabel(sourceType, sourceValue),
    sourceValue,
    metric: mode === 'metric' ? (drilldownState.metric || filters.metric || sourceValue) : '',
    filters: normalizeAnalyticsDrilldownFilters(filters),
    offset: Math.max(0, Number(drilldownState.offset || 0)),
    selectedInteractionId: drilldownState.selectedId || '',
    coverage: mode === 'metric' ? analyticsMetricCoverage(drilldownState.metric || filters.metric || sourceValue) : null,
    metricNote: mode === 'metric'
      ? analyticsMetricDrilldownNote(drilldownState.metric || filters.metric || sourceValue)
      : mode === 'ai'
        ? aiAnalyticsDrilldownNote(filters)
        : '',
  };
  renderAnalyticsDrilldown();
  void loadAnalyticsDrilldownPage({ preserveSelection: Boolean(drilldownState.selectedId) });
}

function analyticsCompareModeLabel(mode) {
  return ANALYTICS_COMPARE_MODE_LABELS[mode] || mode || 'Сравнение';
}

function suggestAnalyticsViewName() {
  const parts = [analyticsPresetLabel(state.analytics.preset || '7d')];
  if (state.analytics.channel && state.analytics.channel !== 'all') {
    parts.push(analyticsChannelLabel(state.analytics.channel));
  }
  if (state.analytics.queueId && state.analytics.queueId !== 'all') {
    const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId);
    parts.push(queue?.name || state.analytics.queueId);
  }
  return parts.join(' · ');
}

function normalizeSavedAnalyticsView(item = {}) {
  return {
    id: String(item.id || `view-${Date.now()}`),
    name: String(item.name || 'Сохранённый вид'),
    snapshot: {
      preset: item.snapshot?.preset || '7d',
      fromTs: item.snapshot?.fromTs || '',
      toTs: item.snapshot?.toTs || '',
      queueId: item.snapshot?.queueId || 'all',
      channel: item.snapshot?.channel || 'all',
      compareMode: item.snapshot?.compareMode || 'previous',
      trendMetric: item.snapshot?.trendMetric || 'volume',
      voiceNameTrendMetric: item.snapshot?.voiceNameTrendMetric || 'scenario_calls',
      aiTrendMetric: item.snapshot?.aiTrendMetric || 'containment_rate',
      agentTrendMetric: item.snapshot?.agentTrendMetric || 'interactions_per_agent',
    },
    createdAt: item.created_at || item.createdAt || item.updated_at || item.updatedAt || new Date().toISOString(),
    updatedAt: item.updated_at || item.updatedAt || item.created_at || item.createdAt || new Date().toISOString(),
  };
}

function renderSavedAnalyticsViews() {
  const select = $('analyticsSavedViewSelect');
  const input = $('analyticsSavedViewName');
  const meta = $('analyticsSavedViewMeta');
  const deleteBtn = $('analyticsDeleteViewBtn');
  if (!select || !input || !meta || !deleteBtn) {
    return;
  }

  const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null;
  select.innerHTML = [
    '<option value="">Без сохранённого вида</option>',
    ...state.analytics.savedViews.map(
      (item) => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.name)}</option>`,
    ),
  ].join('');
  select.value = activeView ? activeView.id : '';

  if (activeView) {
    input.value = activeView.name;
  }
  input.placeholder = suggestAnalyticsViewName();
  deleteBtn.disabled = !activeView;

  const parts = [];
  parts.push(
    state.analytics.savedViews.length
      ? `${state.analytics.savedViews.length} сохранённых ${state.analytics.savedViews.length === 1 ? 'вид' : 'вида'}`
      : 'Сохранённых видов пока нет',
  );
  if (activeView) {
    parts.push(`активен: ${activeView.name}`);
  }
  if (state.analytics.statusMessage) {
    parts.push(state.analytics.statusMessage);
  }
  meta.textContent = parts.join(' · ');
}

function detachActiveAnalyticsView() {
  if (!state.analytics.activeViewId) {
    return;
  }
  state.analytics.activeViewId = '';
  state.analytics.statusMessage = 'Фильтры изменены. Можно сохранить новый вид.';
}

function analyticsQuery(range, overrides = {}) {
  const params = new URLSearchParams();
  params.set('from_ts', range.from.toISOString());
  params.set('to_ts', range.to.toISOString());

  const queueId = overrides.queueId ?? state.analytics.queueId;
  const channel = overrides.channel ?? state.analytics.channel;

  if (queueId && queueId !== 'all') {
    params.set('queue_id', queueId);
  }
  if (channel && channel !== 'all') {
    params.set('channel', channel);
  }

  return params.toString();
}

async function fetchAnalyticsKpi(range, overrides = {}) {
  return api('reporting', `reports/kpi?${analyticsQuery(range, overrides)}`);
}

async function fetchAgentAnalyticsOverview(range, overrides = {}) {
  return api('reporting', `reports/agents/overview?${analyticsQuery(range, overrides)}`);
}

async function fetchAgentAnalyticsTimeseries(range, metric, interval, overrides = {}) {
  const params = new URLSearchParams(analyticsQuery(range, overrides));
  params.set('metric', metric || state.analytics.agentTrendMetric || 'interactions_per_agent');
  params.set('interval', interval || 'day');
  return api('reporting', `reports/agents/timeseries?${params.toString()}`);
}

function voiceNameAnalyticsSupportedChannel(channel = state.analytics.channel) {
  return channel === 'all' || channel === 'voice';
}

function voiceNameAnalyticsQuery(range, overrides = {}) {
  const params = new URLSearchParams();
  params.set('from_ts', range.from.toISOString());
  params.set('to_ts', range.to.toISOString());
  const queueId = overrides.queueId ?? state.analytics.queueId;
  const language = overrides.language ?? null;
  if (queueId && queueId !== 'all') {
    params.set('queue_id', queueId);
  }
  if (language) {
    params.set('language', language);
  }
  return params.toString();
}

async function fetchVoiceNameAnalyticsOverview(range, overrides = {}) {
  return api('ai', `ai/analytics/voice-name-flow/overview?${voiceNameAnalyticsQuery(range, overrides)}`);
}

async function fetchVoiceNameAnalyticsTimeseries(range, metric, overrides = {}) {
  const params = new URLSearchParams(voiceNameAnalyticsQuery(range, overrides));
  params.set('metric', metric || state.analytics.voiceNameTrendMetric || 'scenario_calls');
  return api('ai', `ai/analytics/voice-name-flow/timeseries?${params.toString()}`);
}

async function fetchAnalyticsMetricDrilldown(filters, limit, offset) {
  return api('reporting', `reports/drilldown?${analyticsMetricDrilldownQuery(filters, limit, offset)}`);
}

function aiAnalyticsSupportedChannel(channel = state.analytics.channel) {
  return channel === 'all' || channel === 'telegram' || channel === 'whatsapp';
}

function aiAnalyticsQuery(range, overrides = {}) {
  const params = new URLSearchParams();
  params.set('from_ts', range.from.toISOString());
  params.set('to_ts', range.to.toISOString());

  const queueId = overrides.queueId ?? state.analytics.queueId;
  const channel = overrides.channel ?? state.analytics.channel;

  if (queueId && queueId !== 'all') {
    params.set('queue_id', queueId);
  }
  if (channel) {
    params.set('channel', channel);
  }
  return params.toString();
}

async function fetchAiAnalyticsOverview(range, overrides = {}) {
  return api('ai', `ai/analytics/overview?${aiAnalyticsQuery(range, overrides)}`);
}

async function fetchAiAnalyticsTimeseries(range, metric, interval, overrides = {}) {
  const params = new URLSearchParams(aiAnalyticsQuery(range, overrides));
  params.set('metric', metric || state.analytics.aiTrendMetric || 'containment_rate');
  params.set('interval', interval || 'day');
  return api('ai', `ai/analytics/timeseries?${params.toString()}`);
}

async function fetchAiAnalyticsDrilldown(filters, limit, offset) {
  return api('ai', `ai/analytics/drilldown?${aiAnalyticsDrilldownQuery(filters, limit, offset)}`);
}

async function fetchAiAnalyticsSessionDetail(sessionId) {
  return api('ai', `ai/analytics/sessions/${encodeURIComponent(sessionId)}`);
}

function renderAnalyticsQueueOptions() {
  const select = $('analyticsQueue');
  if (!select) {
    return;
  }
  const selected = state.analytics.queueId || 'all';
  const options = ['<option value="all">Все очереди</option>'];
  if (selected !== 'all' && !state.analytics.queueOptions.some((item) => item.queue_id === selected)) {
    options.push(`<option value="${escapeHtml(selected)}">${escapeHtml(selected)}</option>`);
  }
  options.push(
    ...state.analytics.queueOptions.map(
      (item) => `<option value="${escapeHtml(item.queue_id)}">${escapeHtml(item.name || item.queue_id)}</option>`,
    ),
  );
  select.innerHTML = options.join('');
  select.value = options.some((item) => item.includes(`value="${selected}"`)) ? selected : 'all';
}

async function loadAnalyticsQueueOptions() {
  try {
    const data = await api('routing', 'queues');
    state.analytics.queueOptions = Array.isArray(data) ? data : [];
    state.analytics.queueAccessError = '';
  } catch (err) {
    state.analytics.queueOptions = [];
    state.analytics.queueAccessError = err.message;
  }
  renderAnalyticsQueueOptions();
  return state.analytics.queueOptions;
}

function formatAnalyticsNumber(value, digits = 0) {
  return new Intl.NumberFormat('ru-RU', {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  }).format(Number(value || 0));
}

function analyticsChannelLabel(channel) {
  const meta = ANALYTICS_CHANNEL_OPTIONS.find((item) => item.value === channel);
  return meta ? meta.label : String(channel || 'неизвестно');
}

function metricValueFromPayload(metric, payload) {
  if (metric === 'volume' || metric === 'total') {
    return Number(payload?.volume?.total || 0);
  }
  if (metric === 'answered') {
    return Number(payload?.volume?.answered || 0);
  }
  return Number(payload?.kpi?.[metric] || 0);
}

function formatAnalyticsMetric(metric, value) {
  const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count' };
  if (meta.unit === 'pct') {
    return `${formatAnalyticsNumber(value, 2)}%`;
  }
  if (meta.unit === 'seconds') {
    return `${formatAnalyticsNumber(value, 2)} с`;
  }
  return formatAnalyticsNumber(value, 0);
}

function analyticsMetricCoverage(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const detail = payload?.coverage?.metric_details?.[metric];
  if (detail && typeof detail === 'object') {
    return {
      ...emptyAnalyticsMetricCoverage(),
      ...detail,
      supported_channels: Array.isArray(detail.supported_channels) ? detail.supported_channels : [],
      exact_rows: Number(detail.exact_rows || 0),
      total_rows: Number(detail.total_rows || 0),
    };
  }
  return {
    ...emptyAnalyticsMetricCoverage(),
    status: payload?.coverage?.metric_status?.[metric] || 'unavailable',
    supported_channels: Array.isArray(payload?.coverage?.supported_channels?.[metric])
      ? payload.coverage.supported_channels[metric]
      : [],
  };
}

function analyticsMetricCanDrilldown(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  return analyticsMetricCoverage(metric, payload).status === 'exact';
}

function analyticsMetricCoverageTone(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const coverage = analyticsMetricCoverage(metric, payload);
  if (coverage.status === 'exact') {
    return 'exact';
  }
  if (coverage.status === 'partial') {
    return 'partial';
  }
  return 'unavailable';
}

function analyticsMetricCoverageLabel(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const coverage = analyticsMetricCoverage(metric, payload);
  if (coverage.status === 'exact') {
    return 'Точно';
  }
  if (
    coverage.status === 'partial'
    && coverage.supported_channels.length === 1
    && coverage.supported_channels[0] === 'voice'
  ) {
    return 'Только голос';
  }
  if (coverage.status === 'partial') {
    return 'Частично';
  }
  return 'Нет точного покрытия';
}

function analyticsMetricCoverageHint(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const coverage = analyticsMetricCoverage(metric, payload);
  if (coverage.status === 'exact') {
    return 'Открыть точный KPI-срез по ID обращения.';
  }
  if (
    coverage.supported_channels.length === 1
    && coverage.supported_channels[0] === 'voice'
    && coverage.status === 'partial'
  ) {
    return 'Точная детализация сейчас доступна только для голосовой части выбранного окна.';
  }
  if (!coverage.total_rows) {
    return 'Для выбранного окна пока нет точных записей фактов для этой метрики.';
  }
  if (!coverage.supported_channels.length) {
    return 'Для этой метрики точная детализация в текущем срезе пока недоступна.';
  }
  return 'Точная детализация пока доступна только для части среза, покрытой слоем KPI-фактов.';
}

function analyticsMetricCoverageBadge(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const tone = analyticsMetricCoverageTone(metric, payload);
  const label = analyticsMetricCoverageLabel(metric, payload);
  return `<span class="analytics-coverage-badge analytics-coverage-badge-${tone}">${escapeHtml(label)}</span>`;
}

function aiAnalyticsMetricValue(metric, payload) {
  return payload?.metrics?.[metric] ?? null;
}

function aiAnalyticsMetricSlice(metric) {
  if (metric === 'containment_rate') {
    return 'contained';
  }
  if (metric === 'handoff_rate') {
    return 'handoff';
  }
  if (metric === 'human_touched_rate') {
    return 'human_touched';
  }
  if (metric === 'closed_without_operator_rate') {
    return 'closed_without_operator';
  }
  return 'all';
}

function aiAnalyticsStatusLabel(status) {
  const labels = {
    active: 'Активна',
    closed: 'Закрыта',
    handoff_required: 'Требуется передача',
    human_owned: 'У оператора',
    error: 'Ошибка',
  };
  return labels[status] || status || 'Неизвестно';
}

function aiAnalyticsSliceLabel(slice) {
  const labels = {
    all: 'Все AI-сессии',
    contained: 'Закрыто AI',
    handoff: 'Передано оператору',
    human_touched: 'С участием оператора',
    closed_without_operator: 'Закрыто без оператора',
    active: 'Активные',
    error: 'С ошибкой',
  };
  return labels[slice] || slice || 'AI-срез';
}

function aiAnalyticsSortLabel(filters = state.drilldown.filters) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const fieldLabels = {
    created_at: 'созданию',
    updated_at: 'обновлению',
    ai_latency_avg_ms: 'задержке',
    status: 'статусу',
  };
  const dirLabel = normalized.sort_dir === 'asc' ? 'по возрастанию' : 'по убыванию';
  return `Сортировка: по ${fieldLabels[normalized.sort_by] || normalized.sort_by}, ${dirLabel}`;
}

function aiAnalyticsDrilldownNote(filters = state.drilldown.filters) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const parts = [
    'Только метаданные по AI-сессиям: без текстов сообщений, AI-сводок, записей и транскриптов.',
  ];
  if (normalized.reason_key) {
    parts.push(`Причина: ${normalized.reason_key}`);
  }
  return parts.join(' ');
}

function formatAiAnalyticsMetric(metric, value) {
  const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count' };
  if (value === null || value === undefined || Number.isNaN(Number(value))) {
    return '—';
  }
  if (meta.unit === 'pct') {
    return `${formatAnalyticsNumber(value, 2)}%`;
  }
  if (meta.unit === 'ms') {
    return `${formatAnalyticsNumber(value, 0)} ms`;
  }
  return formatAnalyticsNumber(value, 0);
}

function signedDelta(value, digits = 1) {
  if (!value) {
    return '0';
  }
  const sign = value > 0 ? '+' : '-';
  return `${sign}${formatAnalyticsNumber(Math.abs(value), digits)}`;
}

function analyticsDelta(metric, currentPayload, previousPayload) {
  const current = metricValueFromPayload(metric, currentPayload);
  const previous = metricValueFromPayload(metric, previousPayload);
  const diff = current - previous;
  const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count', better: 'neutral' };

  let text = signedDelta(diff, meta.unit === 'count' ? 0 : 1);
  if (meta.unit === 'pct') {
    text = `${text} п.п.`;
  } else if (meta.unit === 'seconds') {
    text = `${text} с`;
  }

  let tone = 'neutral';
  if (meta.better === 'up') {
    tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral';
  } else if (meta.better === 'down') {
    tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral';
  }

  return { text, tone };
}

function voiceNameAnalyticsMetricLabel(metric) {
  return VOICE_NAME_TREND_LABELS[metric] || metric;
}

function voiceNameAnalyticsMetricValue(metric, payload = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview()) {
  if (metric === 'scenario_calls') {
    return Number(payload?.totals?.scenario_calls || 0);
  }
  return Number(payload?.metrics?.[metric] || 0);
}

function formatVoiceNameAnalyticsMetric(metric, value) {
  const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count' };
  if (value === null || value === undefined || Number.isNaN(Number(value))) {
    return '—';
  }
  if (meta.unit === 'pct') {
    return `${formatAnalyticsNumber(value, 2)}%`;
  }
  return formatAnalyticsNumber(value, 0);
}

function formatVoiceNameAnalyticsAxisValue(metric, value) {
  const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count' };
  return formatAnalyticsNumber(value, meta.unit === 'pct' ? 1 : 0);
}

function voiceNameAnalyticsDelta(metric, currentPayload, previousPayload) {
  const current = voiceNameAnalyticsMetricValue(metric, currentPayload);
  const previous = voiceNameAnalyticsMetricValue(metric, previousPayload);
  const diff = Number(current || 0) - Number(previous || 0);
  const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count', better: 'neutral' };
  let text = signedDelta(diff, meta.unit === 'pct' ? 1 : 0);
  if (meta.unit === 'pct') {
    text = `${text} п.п.`;
  }
  let tone = 'neutral';
  if (meta.better === 'up') {
    tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral';
  } else if (meta.better === 'down') {
    tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral';
  }
  return { text, tone };
}

function aiAnalyticsMetricLabel(metric) {
  return AI_ANALYTICS_TREND_LABELS[metric] || metric;
}

function aiAnalyticsDelta(metric, currentPayload, previousPayload) {
  const current = aiAnalyticsMetricValue(metric, currentPayload);
  const previous = aiAnalyticsMetricValue(metric, previousPayload);
  const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count', better: 'neutral' };
  if (
    current === null
    || current === undefined
    || previous === null
    || previous === undefined
    || Number.isNaN(Number(current))
    || Number.isNaN(Number(previous))
  ) {
    return { text: '-', tone: 'neutral' };
  }

  const diff = Number(current) - Number(previous);
  let text = signedDelta(diff, meta.unit === 'pct' ? 1 : 0);
  if (meta.unit === 'pct') {
    text = `${text} п.п.`;
  } else if (meta.unit === 'ms') {
    text = `${text} ms`;
  }

  let tone = 'neutral';
  if (meta.better === 'up') {
    tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral';
  } else if (meta.better === 'down') {
    tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral';
  }
  return { text, tone };
}

function renderAnalyticsCard(label, metric, payload, previousPayload, options = {}) {
  const value = metricValueFromPayload(metric, payload);
  const showCompare = state.analytics.compareMode === 'previous';
  const delta = showCompare ? analyticsDelta(metric, payload, previousPayload) : null;
  const note = ANALYTICS_METRIC_META[metric]?.note || '';
  const metricDrilldown = Boolean(options.metricDrilldown);
  const drilldownEnabled = metricDrilldown
    ? analyticsMetricCanDrilldown(metric, payload)
    : Boolean(options.drilldown);
  const coverageBadge = metricDrilldown ? analyticsMetricCoverageBadge(metric, payload) : '';
  const classes = [
    'summary-card',
    'analytics-card',
    drilldownEnabled ? 'analytics-card-button' : 'analytics-card-readonly',
    metricDrilldown ? `analytics-card-coverage-${analyticsMetricCoverageTone(metric, payload)}` : '',
  ].join(' ');
  const attributes = drilldownEnabled
    ? `type="button" data-analytics-drilldown-source="${escapeHtml(options.sourceType || 'overview')}" data-analytics-drilldown-label="${escapeHtml(options.sourceLabel || label)}"${metricDrilldown ? ` data-analytics-drilldown-metric="${escapeHtml(metric)}"` : ''}`
    : '';
  const hint = metricDrilldown
    ? analyticsMetricCoverageHint(metric, payload)
    : drilldownEnabled
      ? 'Открыть детализацию по обращениям'
      : 'Точная детализация появится после связи KPI с interaction_id.';
  const tag = drilldownEnabled ? 'button' : 'div';
  return `
    <${tag} class="${classes}" ${attributes}>
      <div class="analytics-card-top">
        <div class="summary-label">${label}</div>
        <div class="analytics-card-top-meta">
          ${coverageBadge}
          ${showCompare ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>` : ''}
        </div>
      </div>
      <div class="summary-value">${formatAnalyticsMetric(metric, value)}</div>
      ${note ? `<div class="summary-note">${note}</div>` : ''}
      <div class="analytics-card-hint">${hint}</div>
    </${tag}>
  `;
}

function formatTime(ts) {
  if (!ts) {
    return '-';
  }
  const date = new Date(ts);
  if (Number.isNaN(date.getTime())) {
    return ts;
  }
  return date.toLocaleString();
}

function analyticsPresetLabel(preset) {
  return ANALYTICS_PRESET_LABELS[preset] || preset;
}

function analyticsMetricLabel(metric) {
  return ANALYTICS_TREND_LABELS[metric] || metric;
}

function analyticsQueueName(queueId) {
  if (queueId === 'all') {
    return 'Все очереди';
  }
  if (!queueId) {
    return 'Без очереди';
  }
  const queue = state.analytics.queueOptions.find((item) => item.queue_id === queueId);
  return queue?.name || queueId;
}

function interactionStatusLabel(status) {
  const labels = {
    new: 'Новая',
    in_progress: 'В работе',
    escalated: 'Эскалация',
    closed: 'Закрыта',
    abandoned: 'Потеряна',
  };
  return labels[status] || status || 'Не указан';
}

function analyticsMetricDrilldownNote(metric, payload = state.analytics.overview || emptyKpiEnvelope()) {
  const metricNote = ANALYTICS_METRIC_META[metric]?.note || '';
  const coverageHint = analyticsMetricCoverageHint(metric, payload);
  return [metricNote, coverageHint]
    .filter(Boolean)
    .join(' ');
}

function analyticsDrilldownFactBadges(item) {
  if (!item || typeof item !== 'object') {
    return [];
  }
  const badges = [];
  if (item.wait_seconds !== null && item.wait_seconds !== undefined) {
    badges.push({ tone: 'wait', label: `Ожидание ${formatAnalyticsNumber(item.wait_seconds, 0)} с` });
  }
  if (item.handle_seconds !== null && item.handle_seconds !== undefined) {
    badges.push({ tone: 'handle', label: `Обработка ${formatAnalyticsNumber(item.handle_seconds, 0)} с` });
  }
  if (item.within_sla === true) {
    badges.push({ tone: 'sla', label: 'В SLA' });
  } else if (item.within_sla === false) {
    badges.push({ tone: 'sla-miss', label: 'Вне SLA' });
  }
  if (item.resolved_first_contact === true) {
    badges.push({ tone: 'fcr', label: 'FCR' });
  }
  if (item.abandoned === true) {
    badges.push({ tone: 'abandoned', label: 'Потеря' });
  }
  return badges;
}

function renderAnalyticsDrilldownFactBadges(item) {
  const badges = analyticsDrilldownFactBadges(item);
  if (!badges.length) {
    return '';
  }
  return `
    <div class="analytics-drilldown-facts">
      ${badges
        .map(
          (badge) => `<span class="analytics-fact-badge analytics-fact-badge-${escapeHtml(badge.tone)}">${escapeHtml(badge.label)}</span>`,
        )
        .join('')}
    </div>
  `;
}

function clearAnalyticsDrilldownSearchDebounce() {
  if (analyticsDrilldownSearchDebounce) {
    window.clearTimeout(analyticsDrilldownSearchDebounce);
    analyticsDrilldownSearchDebounce = 0;
  }
}

function normalizeAnalyticsDrilldownFilters(filters = {}) {
  return {
    from_ts: filters.from_ts || '',
    to_ts: filters.to_ts || '',
    metric: filters.metric || '',
    slice: filters.slice || 'all',
    reason_key: filters.reason_key || '',
    queue_id: filters.queue_id || '',
    channel: filters.channel || '',
    agent_id: filters.agent_id || '',
    sl_threshold_seconds: Number(filters.sl_threshold_seconds || 30),
    status: filters.status || '',
    q: filters.q || '',
    sort_by: filters.sort_by || 'created_at',
    sort_dir: filters.sort_dir || 'desc',
  };
}

function analyticsDrilldownSortValue(filters = state.drilldown.filters) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  return `${normalized.sort_by}:${normalized.sort_dir}`;
}

function parseAnalyticsDrilldownSort(value) {
  const [sortByRaw = 'created_at', sortDirRaw = 'desc'] = String(value || 'created_at:desc').split(':');
  const allowedFields = state.drilldown.mode === 'ai'
    ? ['created_at', 'updated_at', 'ai_latency_avg_ms', 'status']
    : ['created_at', 'updated_at', 'subject'];
  const sortBy = allowedFields.includes(sortByRaw) ? sortByRaw : 'created_at';
  const sortDir = sortDirRaw === 'asc' ? 'asc' : 'desc';
  return { sort_by: sortBy, sort_dir: sortDir };
}

function analyticsDrilldownSortLabel(filters = state.drilldown.filters) {
  if (state.drilldown.mode === 'ai') {
    return aiAnalyticsSortLabel(filters);
  }
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const fieldLabels = {
    created_at: 'созданию',
    updated_at: 'обновлению',
    subject: 'теме',
  };
  const dirLabel = normalized.sort_dir === 'asc' ? 'по возрастанию' : 'по убыванию';
  return `Сортировка: по ${fieldLabels[normalized.sort_by] || normalized.sort_by}, ${dirLabel}`;
}

function analyticsDrilldownStatusClass(status) {
  return String(status || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '-');
}

function analyticsDrilldownFilenameSource() {
  if (state.drilldown.mode === 'ai') {
    const slice = state.drilldown.filters?.slice || 'all';
    if (state.drilldown.filters?.reason_key) {
      return `ai-${slice}-${String(state.drilldown.filters.reason_key).toLowerCase()}`;
    }
    if (state.drilldown.filters?.channel) {
      return `ai-${String(state.drilldown.filters.channel).toLowerCase()}-${slice}`;
    }
    return `ai-${slice}`;
  }
  if (state.drilldown.sourceType === 'metric' && state.drilldown.metric) {
    return `metric-${String(state.drilldown.metric).toLowerCase()}`;
  }
  if (state.drilldown.sourceType === 'channel' && state.drilldown.filters?.channel) {
    return `channel-${String(state.drilldown.filters.channel).toLowerCase()}`;
  }
  if (state.drilldown.sourceType === 'queue' && state.drilldown.filters?.queue_id) {
    return `queue-${String(state.drilldown.filters.queue_id).toLowerCase().replace(/[^a-z0-9_-]+/g, '-')}`;
  }
  if (state.drilldown.sourceType === 'agent' && state.drilldown.filters?.agent_id) {
    return `agent-${String(state.drilldown.filters.agent_id).toLowerCase().replace(/[^a-z0-9._-]+/g, '-')}`;
  }
  return state.drilldown.sourceType === 'overview' ? 'all-interactions' : String(state.drilldown.sourceType || 'slice');
}

function sanitizeAnalyticsTimelineMetadata(metadata) {
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
    return {};
  }
  const blockedKeyParts = ['message', 'messages', 'text', 'body', 'summary', 'transcript', 'recording', 'audio', 'media', 'content'];
  const safe = {};
  Object.entries(metadata).forEach(([key, value]) => {
    const normalizedKey = String(key || '').toLowerCase();
    if (blockedKeyParts.some((part) => normalizedKey.includes(part))) {
      return;
    }
    if (typeof value === 'string') {
      safe[key] = value.length > 120 ? `[hidden string ${value.length} chars]` : value;
      return;
    }
    if (Array.isArray(value)) {
      safe[key] = `[items: ${value.length}]`;
      return;
    }
    if (value && typeof value === 'object') {
      safe[key] = '[object]';
      return;
    }
    safe[key] = value;
  });
  return safe;
}

function rememberAnalyticsDrilldownReturnFocus() {
  const active = document.activeElement;
  analyticsDrilldownReturnFocus = active instanceof HTMLElement ? active : null;
}

function restoreAnalyticsDrilldownReturnFocus() {
  if (analyticsDrilldownReturnFocus instanceof HTMLElement && document.contains(analyticsDrilldownReturnFocus)) {
    analyticsDrilldownReturnFocus.focus();
  }
  analyticsDrilldownReturnFocus = null;
}

function analyticsDrilldownFocusableElements() {
  const drawer = $('analyticsDrilldownDrawer');
  if (!drawer || !state.drilldown.open) {
    return [];
  }
  return Array.from(
    drawer.querySelectorAll(
      'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
    ),
  ).filter(
    (element) =>
      !element.hasAttribute('hidden')
      && element.getAttribute('aria-hidden') !== 'true'
      && element.offsetParent !== null,
  );
}

function focusAnalyticsDrilldownPrimaryControl(selectText = false) {
  const searchInput = $('analyticsDrilldownSearch');
  if (searchInput && !searchInput.disabled) {
    searchInput.focus();
    if (selectText && typeof searchInput.select === 'function') {
      searchInput.select();
    }
    return;
  }
  $('analyticsDrilldownDrawer')?.focus();
}

function handleAnalyticsDrilldownWindowKeydown(event) {
  if (!state.drilldown.open) {
    return;
  }
  if (event.key === 'Escape') {
    event.preventDefault();
    closeAnalyticsDrilldown();
    return;
  }
  if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
    const targetTag = document.activeElement?.tagName || '';
    if (!['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTag)) {
      event.preventDefault();
      focusAnalyticsDrilldownPrimaryControl(true);
    }
    return;
  }
  if (event.key !== 'Tab') {
    return;
  }
  const focusable = analyticsDrilldownFocusableElements();
  if (!focusable.length) {
    return;
  }
  const first = focusable[0];
  const last = focusable[focusable.length - 1];
  const active = document.activeElement;
  if (event.shiftKey && active === first) {
    event.preventDefault();
    last.focus();
    return;
  }
  if (!event.shiftKey && active === last) {
    event.preventDefault();
    first.focus();
  }
}

function maybeScrollAnalyticsDrilldownDetailIntoView() {
  if (!window.matchMedia('(max-width: 960px)').matches) {
    return;
  }
  window.requestAnimationFrame(() => {
    $('analyticsDrilldownDetail')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  });
}

function syncAnalyticsDrilldownControls() {
  const searchInput = $('analyticsDrilldownSearch');
  const statusSelect = $('analyticsDrilldownStatus');
  const sortSelect = $('analyticsDrilldownSort');
  const clearBtn = $('analyticsDrilldownClearBtn');
  const exportBtn = $('analyticsDrilldownExportBtn');
  const isOpen = state.drilldown.open;
  const localFiltersEnabled = isOpen && state.drilldown.mode === 'interaction';
  const filters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters || {});
  if (searchInput) {
    searchInput.value = isOpen ? filters.q : '';
    searchInput.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
    searchInput.placeholder = state.drilldown.mode === 'metric'
      ? 'Поиск доступен только для среза обращений'
      : 'ID, тема или назначенный';
  }
  if (statusSelect) {
    statusSelect.value = isOpen ? (filters.status || 'all') : 'all';
    statusSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (sortSelect) {
    sortSelect.value = analyticsDrilldownSortValue(filters);
    sortSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (clearBtn) {
    clearBtn.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (exportBtn) {
    exportBtn.disabled = !isOpen || state.drilldown.loading || state.drilldown.exporting || !state.drilldown.total;
    exportBtn.textContent = state.drilldown.exporting ? 'Готовим CSV...' : 'Экспорт CSV';
  }
}

function resetAnalyticsDrilldown() {
  clearAnalyticsDrilldownSearchDebounce();
  state.drilldown = emptyDrilldownState();
}

function closeAnalyticsDrilldown(options = {}) {
  if (!state.drilldown.open && !state.drilldown.loading && !state.drilldown.error) {
    return;
  }
  resetAnalyticsDrilldown();
  renderAnalyticsDrilldown();
  if (!options.skipUrlSync) {
    syncAnalyticsUrlState();
  }
  window.requestAnimationFrame(() => {
    restoreAnalyticsDrilldownReturnFocus();
  });
}

function analyticsDrilldownBaseFilters(rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls()) {
  return normalizeAnalyticsDrilldownFilters({
    from_ts: rangeMeta.current.from.toISOString(),
    to_ts: rangeMeta.current.to.toISOString(),
    metric: '',
    slice: 'all',
    reason_key: '',
    queue_id: state.analytics.queueId !== 'all' ? state.analytics.queueId : '',
    channel: state.analytics.channel !== 'all' ? state.analytics.channel : '',
    agent_id: '',
    sl_threshold_seconds: 30,
    status: '',
    q: '',
    sort_by: 'created_at',
    sort_dir: 'desc',
  });
}

function analyticsDrilldownSourceLabel(sourceType, sourceValue = '') {
  if (sourceType === 'agent') {
    return `Агент: ${sourceValue}`;
  }
  if (sourceType === 'ai-overview') {
    return aiAnalyticsSliceLabel(sourceValue || 'all');
  }
  if (sourceType === 'ai-channel') {
    return `AI-канал: ${analyticsChannelLabel(sourceValue)}`;
  }
  if (sourceType === 'ai-reason') {
    return `Причина передачи: ${sourceValue}`;
  }
  if (sourceType === 'metric') {
    return `KPI: ${sourceValue}`;
  }
  if (sourceType === 'channel') {
    return `Канал: ${analyticsChannelLabel(sourceValue)}`;
  }
  if (sourceType === 'queue') {
    return `Очередь: ${analyticsQueueName(sourceValue)}`;
  }
  return 'Все обращения';
}

function analyticsDrilldownQuery(filters, limit, offset) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const params = new URLSearchParams();
  params.set('from_ts', normalized.from_ts);
  params.set('to_ts', normalized.to_ts);
  params.set('limit', String(limit));
  params.set('offset', String(offset));
  params.set('sort_by', normalized.sort_by || 'created_at');
  params.set('sort_dir', normalized.sort_dir || 'desc');
  if (normalized.queue_id) {
    params.set('queue_id', normalized.queue_id);
  }
  if (normalized.channel) {
    params.set('channel', normalized.channel);
  }
  if (normalized.agent_id) {
    params.set('agent_id', normalized.agent_id);
  }
  if (normalized.status) {
    params.set('status', normalized.status);
  }
  if (normalized.q) {
    params.set('q', normalized.q);
  }
  return params.toString();
}

function aiAnalyticsDrilldownQuery(filters, limit, offset) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const params = new URLSearchParams();
  params.set('from_ts', normalized.from_ts);
  params.set('to_ts', normalized.to_ts);
  params.set('slice', normalized.slice || 'all');
  params.set('limit', String(limit));
  params.set('offset', String(offset));
  params.set('sort_by', normalized.sort_by || 'created_at');
  params.set('sort_dir', normalized.sort_dir || 'desc');
  if (normalized.queue_id) {
    params.set('queue_id', normalized.queue_id);
  }
  if (normalized.channel) {
    params.set('channel', normalized.channel);
  }
  if (normalized.reason_key) {
    params.set('reason_key', normalized.reason_key);
  }
  if (normalized.status) {
    params.set('status', normalized.status);
  }
  if (normalized.q) {
    params.set('q', normalized.q);
  }
  return params.toString();
}

function analyticsMetricDrilldownQuery(filters, limit, offset) {
  const normalized = normalizeAnalyticsDrilldownFilters(filters);
  const params = new URLSearchParams();
  params.set('from_ts', normalized.from_ts);
  params.set('to_ts', normalized.to_ts);
  params.set('metric', normalized.metric);
  params.set('limit', String(limit));
  params.set('offset', String(offset));
  params.set('sl_threshold_seconds', String(normalized.sl_threshold_seconds || 30));
  if (normalized.queue_id) {
    params.set('queue_id', normalized.queue_id);
  }
  if (normalized.channel) {
    params.set('channel', normalized.channel);
  }
  return params.toString();
}

function analyticsDrilldownChips() {
  if (!state.drilldown.filters) {
    return [];
  }
  const drilldownFilters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters);
  const chips = [];
  chips.push(state.drilldown.sourceLabel || 'Детализация');
  chips.push(
    `${formatTime(drilldownFilters.from_ts)} - ${formatTime(drilldownFilters.to_ts)}`,
  );
  if (drilldownFilters.channel) {
    chips.push(analyticsChannelLabel(drilldownFilters.channel));
  }
  if (drilldownFilters.queue_id) {
    chips.push(analyticsQueueName(drilldownFilters.queue_id));
  }
  if (drilldownFilters.agent_id) {
    chips.push(`Агент: ${drilldownFilters.agent_id}`);
  }
  if (drilldownFilters.status) {
    chips.push(`Статус: ${interactionStatusLabel(drilldownFilters.status)}`);
  }
  if (drilldownFilters.q) {
    chips.push(`Поиск: ${drilldownFilters.q}`);
  }
  if (state.drilldown.mode === 'metric' && state.drilldown.metric) {
    chips.push(analyticsMetricCoverageLabel(state.drilldown.metric));
  } else {
    chips.push(analyticsDrilldownSortLabel(drilldownFilters));
  }
  return chips;
}

function updateAnalyticsRangeHint(range) {
  const box = $('analyticsRangeHint');
  if (!box) {
    return;
  }
  const filters = [];
  if (state.analytics.queueId !== 'all') {
    const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId);
    filters.push(`очередь: ${queue?.name || state.analytics.queueId}`);
  }
  if (state.analytics.channel !== 'all') {
    filters.push(`канал: ${analyticsChannelLabel(state.analytics.channel)}`);
  }
  filters.push(
    state.analytics.compareMode === 'previous'
      ? 'сравнение с предыдущим окном'
      : 'только текущий срез',
  );
  const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId);
  if (activeView) {
    filters.push(`вид: ${activeView.name}`);
  }
  const suffix = filters.length ? ` | ${filters.join(' | ')}` : '';
  box.textContent = range.custom
    ? `Окно анализа: ${formatTime(range.current.from.toISOString())} - ${formatTime(range.current.to.toISOString())}${suffix}`
    : `Период: ${analyticsPresetLabel(range.preset)}${suffix}`;
}

function renderAnalyticsNarrative(rangeMeta) {
  const box = $('analyticsNarrative');
  if (!box) {
    return;
  }
  const current = state.analytics.overview || emptyKpiEnvelope();
  const previous = state.analytics.compare || emptyKpiEnvelope();
  const total = Number(current?.volume?.total || 0);
  const sl = Number(current?.kpi?.SL || 0);
  const abandon = Number(current?.kpi?.Abandon || 0);
  const digitalShare = Number(current?.kpi?.DigitalShare || 0);
  const delta = analyticsDelta('total', current, previous);
  const filters = [];
  if (state.analytics.channel !== 'all') {
    filters.push(analyticsChannelLabel(state.analytics.channel));
  }
  if (state.analytics.queueId !== 'all') {
    const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId);
    filters.push(queue?.name || state.analytics.queueId);
  }
  const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId);
  if (activeView) {
    filters.push(activeView.name);
  }

  const chips = [
    analyticsPresetLabel(rangeMeta.preset),
    analyticsCompareModeLabel(state.analytics.compareMode),
    ...filters,
  ];
  const deltaBadge = state.analytics.compareMode === 'previous'
    ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text} к предыдущему окну</span>`
    : '<span class="analytics-filter-chip analytics-filter-chip-quiet">Сравнение отключено</span>';

  box.innerHTML = `
    <div class="analytics-narrative-main">
      <strong>${formatAnalyticsNumber(total, 0)} обращений</strong>
      <p>За выбранный период ${formatAnalyticsNumber(sl, 2)}% потока уложилось в SLA, потери составили ${formatAnalyticsNumber(abandon, 2)}%, а цифровая доля достигла ${formatAnalyticsNumber(digitalShare, 2)}%.</p>
    </div>
    <div class="analytics-narrative-meta">
      ${deltaBadge}
      <div class="analytics-filter-chips">
        ${chips.map((chip) => `<span class="analytics-filter-chip">${escapeHtml(chip)}</span>`).join('')}
      </div>
    </div>
  `;
}

function renderAnalyticsCompareCard(label, metric, currentPayload, previousPayload) {
  const currentValue = metricValueFromPayload(metric, currentPayload);
  const previousValue = metricValueFromPayload(metric, previousPayload);
  const delta = analyticsDelta(metric, currentPayload, previousPayload);
  const note = ANALYTICS_METRIC_META[metric]?.note || '';
  const coverageBadge = analyticsMetricCoverageBadge(metric, currentPayload);
  return `
    <article class="analytics-compare-card">
      <div class="analytics-compare-card-head">
        <span class="analytics-compare-label">${label}</span>
        <div class="analytics-card-top-meta">
          ${coverageBadge}
          <span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>
        </div>
      </div>
      <div class="analytics-compare-values">
        <div class="analytics-compare-value">
          <small>Текущее окно</small>
          <strong>${formatAnalyticsMetric(metric, currentValue)}</strong>
        </div>
        <div class="analytics-compare-value">
          <small>Предыдущее окно</small>
          <strong>${formatAnalyticsMetric(metric, previousValue)}</strong>
        </div>
      </div>
      <p class="analytics-compare-note">${note}</p>
    </article>
  `;
}

function renderAnalyticsComparePanel() {
  const box = $('analyticsComparePanel');
  if (!box) {
    return;
  }
  if (state.analytics.compareMode !== 'previous') {
    box.hidden = true;
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.overview || emptyKpiEnvelope();
  const previous = state.analytics.compare || emptyKpiEnvelope();
  box.hidden = false;
  box.innerHTML = `
    <div class="analytics-compare-head">
      <div>
        <h3>Сравнение периодов</h3>
        <p class="hint">Быстрый разрез по главным изменениям между текущим и предыдущим окном без перехода в отдельный отчёт.</p>
      </div>
      <span class="analytics-filter-chip analytics-filter-chip-quiet">V2 compare mode</span>
    </div>
    <div class="analytics-compare-grid">
      ${[
        renderAnalyticsCompareCard('Обращения', 'total', current, previous),
        renderAnalyticsCompareCard('SL', 'SL', current, previous),
        renderAnalyticsCompareCard('Среднее ожидание', 'ASA', current, previous),
        renderAnalyticsCompareCard('Потери', 'Abandon', current, previous),
      ].join('')}
    </div>
  `;
}

function renderAnalyticsOverview() {
  const current = state.analytics.overview || emptyKpiEnvelope();
  const previous = state.analytics.compare || emptyKpiEnvelope();
  $('analyticsOverview').innerHTML = [
    renderAnalyticsCard('Обращения', 'total', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Обращения',
    }),
    renderAnalyticsCard('Обработано', 'answered', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Обработано',
    }),
    renderAnalyticsCard('SL', 'SL', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'SL',
    }),
    renderAnalyticsCard('Среднее ожидание', 'ASA', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Среднее ожидание',
    }),
    renderAnalyticsCard('Среднее время', 'AHT', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Среднее время',
    }),
    renderAnalyticsCard('Потери', 'Abandon', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Потери',
    }),
    renderAnalyticsCard('FCR', 'FCR', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'FCR',
    }),
    renderAnalyticsCard('Цифровая доля', 'DigitalShare', current, previous, {
      metricDrilldown: true,
      sourceType: 'metric',
      sourceLabel: 'Цифровая доля',
    }),
  ].join('');
}

function voiceNameLanguageLabel(language) {
  if (language === 'ru') {
    return 'Русский';
  }
  if (language === 'kz') {
    return 'Казахский';
  }
  if (language === 'unknown' || !language) {
    return 'Не определён';
  }
  return String(language);
}

function voiceNameAnalyticsMetricDetail(metric, payload) {
  const totals = payload?.totals || {};
  const allHandoffs = Number(totals.handoff_confirmed_name || 0) + Number(totals.handoff_unconfirmed_name || 0);
  if (metric === 'scenario_calls') {
    return `${formatAnalyticsNumber(totals.scenario_calls || 0, 0)} звонков прошли через сценарий`;
  }
  if (metric === 'start_capture_rate') {
    return `${formatAnalyticsNumber(totals.start_obtained || 0, 0)} из ${formatAnalyticsNumber(totals.scenario_calls || 0, 0)} звонков`;
  }
  if (metric === 'downstream_rescue_rate') {
    return `${formatAnalyticsNumber(totals.downstream_ai_obtained || 0, 0)} из ${formatAnalyticsNumber(totals.needed_downstream || 0, 0)} случаев`;
  }
  if (metric === 'handoff_unconfirmed_rate') {
    return `${formatAnalyticsNumber(totals.handoff_unconfirmed_name || 0, 0)} из ${formatAnalyticsNumber(allHandoffs || 0, 0)} передач`;
  }
  return `${formatAnalyticsNumber(totals.manual_corrected || 0, 0)} из ${formatAnalyticsNumber(allHandoffs || 0, 0)} передач`;
}

function renderVoiceNameAnalyticsCard(label, metric, currentPayload, previousPayload) {
  const value = voiceNameAnalyticsMetricValue(metric, currentPayload);
  const showCompare = state.analytics.compareMode === 'previous';
  const delta = showCompare ? voiceNameAnalyticsDelta(metric, currentPayload, previousPayload) : null;
  const note = VOICE_NAME_METRIC_META[metric]?.note || '';
  const detail = voiceNameAnalyticsMetricDetail(metric, currentPayload);
  return `
    <div class="summary-card analytics-card voice-name-card analytics-card-readonly">
      <div class="analytics-card-top">
        <div class="summary-label">${label}</div>
        ${showCompare ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>` : ''}
      </div>
      <div class="summary-value">${formatVoiceNameAnalyticsMetric(metric, value)}</div>
      ${detail ? `<div class="summary-note">${detail}</div>` : ''}
      ${note ? `<div class="analytics-card-hint">${note}</div>` : ''}
    </div>
  `;
}

function renderVoiceNameAnalyticsOverview() {
  const box = $('voiceNameAnalyticsOverview');
  if (!box) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) {
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview();
  if (!state.analytics.loading && !Number(current?.totals?.scenario_calls || 0)) {
    box.innerHTML = '';
    return;
  }
  const previous = state.analytics.voiceNameCompare || emptyVoiceNameAnalyticsOverview();
  box.innerHTML = [
    renderVoiceNameAnalyticsCard('Звонки в сценарии', 'scenario_calls', current, previous),
    renderVoiceNameAnalyticsCard('Имя взято сразу', 'start_capture_rate', current, previous),
    renderVoiceNameAnalyticsCard('Имя добрал AI после follow-up', 'downstream_rescue_rate', current, previous),
    renderVoiceNameAnalyticsCard('Передача без подтверждённого имени', 'handoff_unconfirmed_rate', current, previous),
    renderVoiceNameAnalyticsCard('Ручное исправление оператором', 'manual_correction_rate', current, previous),
  ].join('');
}

function renderVoiceNameAnalyticsTrendChart() {
  const container = $('voiceNameAnalyticsTrendChart');
  if (!container) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel()) {
    container.innerHTML = '';
    return;
  }
  if (state.analytics.voiceNameTrendError && !state.analytics.voiceNameTrend?.points?.length) {
    container.innerHTML = `<div class="empty-state">Не удалось обновить тренд по voice name-flow: ${escapeHtml(state.analytics.voiceNameTrendError)}</div>`;
    return;
  }
  const metric = state.analytics.voiceNameTrendMetric || 'scenario_calls';
  const trend = state.analytics.voiceNameTrend || emptyVoiceNameAnalyticsTimeseries(metric);
  const items = Array.isArray(trend.points) ? trend.points : [];
  const current = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview();
  const previous = state.analytics.voiceNameCompare || emptyVoiceNameAnalyticsOverview();
  const delta = state.analytics.compareMode === 'previous'
    ? voiceNameAnalyticsDelta(metric, current, previous)
    : null;
  const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null;
  renderTrendChart(container, {
    title: voiceNameAnalyticsMetricLabel(metric),
    items,
    valueAccessor: (item) => item.value,
    labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || 'day'),
    axisFormatter: (value) => formatVoiceNameAnalyticsAxisValue(metric, value),
    valueFormatter: (value) => formatVoiceNameAnalyticsMetric(metric, value),
    summaryText: lastPoint
      ? `${formatVoiceNameAnalyticsMetric(metric, lastPoint.value)} в последней точке`
      : '—',
    metaBadge: delta ? `<span class="analytics-delta analytics-delta-${delta.tone}">${escapeHtml(delta.text)}</span>` : '',
    ariaLabel: 'График voice name-flow аналитики',
    emptyText: 'За выбранный период недостаточно точек для анализа сценария сбора имени.',
  });
}

function renderVoiceNameAnalyticsFunnel() {
  const container = $('voiceNameAnalyticsFunnel');
  if (!container) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.funnel)
    ? state.analytics.voiceNameOverview.breakdowns.funnel
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Нет данных по этапам сценария за выбранный период.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head voice-name-funnel-grid">
      <span>Этап</span>
      <span>Сессии</span>
      <span>Доля</span>
    </div>
    ${rows.map((item) => `
      <div class="analytics-table-row voice-name-funnel-grid">
        <span><strong>${escapeHtml(VOICE_NAME_FUNNEL_LABELS[item.stage] || item.label || item.stage)}</strong></span>
        <span>${formatAnalyticsNumber(item.sessions || 0, 0)}</span>
        <span>${formatAnalyticsNumber(item.share || 0, 2)}%</span>
      </div>
    `).join('')}
  `;
}

function renderVoiceNameAnalyticsLanguageTable() {
  const container = $('voiceNameAnalyticsLanguageTable');
  if (!container) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.by_language)
    ? state.analytics.voiceNameOverview.breakdowns.by_language
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">По языкам пока нет достаточных данных.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head voice-name-language-grid">
      <span>Язык</span>
      <span>Звонки</span>
      <span>Старт</span>
      <span>AI после follow-up</span>
      <span>Передача без имени</span>
      <span>Ручное исправление</span>
    </div>
    ${rows.map((item) => `
      <div class="analytics-table-row voice-name-language-grid">
        <span><strong>${escapeHtml(voiceNameLanguageLabel(item.language))}</strong></span>
        <span>${formatAnalyticsNumber(item.scenario_calls || 0, 0)}</span>
        <span>${formatAnalyticsNumber(item.start_capture_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.downstream_rescue_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.handoff_unconfirmed_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.manual_correction_rate || 0, 2)}%</span>
      </div>
    `).join('')}
  `;
}

function renderVoiceNameAnalyticsQueueTable() {
  const container = $('voiceNameAnalyticsQueueTable');
  if (!container) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.by_queue)
    ? state.analytics.voiceNameOverview.breakdowns.by_queue
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">По очередям пока нет данных по voice name-flow.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head voice-name-queue-grid">
      <span>Очередь</span>
      <span>Звонки</span>
      <span>Старт</span>
      <span>AI после follow-up</span>
      <span>Передача без имени</span>
      <span>Ручное исправление</span>
    </div>
    ${rows.map((item) => `
      <div class="analytics-table-row voice-name-queue-grid">
        <span><strong>${escapeHtml(analyticsQueueName(item.queue_id))}</strong><small>${escapeHtml(item.queue_id)}</small></span>
        <span>${formatAnalyticsNumber(item.scenario_calls || 0, 0)}</span>
        <span>${formatAnalyticsNumber(item.start_capture_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.downstream_rescue_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.handoff_unconfirmed_rate || 0, 2)}%</span>
        <span>${formatAnalyticsNumber(item.manual_correction_rate || 0, 2)}%</span>
      </div>
    `).join('')}
  `;
}

function renderVoiceNameAnalyticsHandoffTable() {
  const container = $('voiceNameAnalyticsHandoffTable');
  if (!container) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.handoff)
    ? state.analytics.voiceNameOverview.breakdowns.handoff
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Разрез по передачам пока пуст для выбранного окна.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head voice-name-handoff-grid">
      <span>Итог передачи</span>
      <span>Звонки</span>
      <span>Доля</span>
    </div>
    ${rows.map((item) => `
      <div class="analytics-table-row voice-name-handoff-grid">
        <span><strong>${escapeHtml(VOICE_NAME_HANDOFF_LABELS[item.outcome] || item.label || item.outcome)}</strong></span>
        <span>${formatAnalyticsNumber(item.sessions || 0, 0)}</span>
        <span>${formatAnalyticsNumber(item.share || 0, 2)}%</span>
      </div>
    `).join('')}
  `;
}

function renderVoiceNameAnalyticsEmptyState() {
  const box = $('voiceNameAnalyticsEmptyState');
  if (!box) {
    return;
  }
  if (!voiceNameAnalyticsSupportedChannel()) {
    box.hidden = false;
    box.textContent = 'Аналитика voice name-flow доступна только для срезов “Все каналы” и “Голос”.';
    return;
  }
  if (state.analytics.voiceNameError) {
    box.hidden = false;
    box.textContent = `Не удалось обновить voice name-flow аналитику: ${state.analytics.voiceNameError}`;
    return;
  }
  const total = Number(state.analytics.voiceNameOverview?.totals?.scenario_calls || 0);
  if (!state.analytics.loading && total === 0) {
    box.hidden = false;
    box.textContent = mockVoiceNameSeed().empty_note || 'За выбранный период не найдено звонков, прошедших через сценарий сбора имени.';
    return;
  }
  box.hidden = true;
  box.textContent = '';
}

function aiAnalyticsMetricDetail(metric, payload) {
  const totals = payload?.totals || {};
  if (metric === 'containment_rate') {
    return `${formatAnalyticsNumber(totals.sessions_contained || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`;
  }
  if (metric === 'handoff_rate') {
    return `${formatAnalyticsNumber(totals.sessions_handoff || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`;
  }
  if (metric === 'ai_latency_avg_ms') {
    return `${formatAnalyticsNumber(totals.assistant_turns || 0, 0)} ответов AI`;
  }
  if (metric === 'closed_without_operator_rate') {
    return `${formatAnalyticsNumber(totals.sessions_closed_without_operator || 0, 0)} закрытий без оператора`;
  }
  return '';
}

function renderAiAnalyticsCard(label, metric, currentPayload, previousPayload) {
  const value = aiAnalyticsMetricValue(metric, currentPayload);
  const showCompare = state.analytics.compareMode === 'previous';
  const delta = showCompare ? aiAnalyticsDelta(metric, currentPayload, previousPayload) : null;
  const note = AI_ANALYTICS_METRIC_META[metric]?.note || '';
  const detail = aiAnalyticsMetricDetail(metric, currentPayload);
  return `
    <div class="summary-card analytics-card ai-analytics-card analytics-card-readonly">
      <div class="analytics-card-top">
        <div class="summary-label">${label}</div>
        ${showCompare ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>` : ''}
      </div>
      <div class="summary-value">${formatAiAnalyticsMetric(metric, value)}</div>
      ${detail ? `<div class="summary-note">${detail}</div>` : ''}
      ${note ? `<div class="analytics-card-hint ai-analytics-card-hint">${note}</div>` : ''}
    </div>
  `;
}

function renderAiAnalyticsOverview() {
  const box = $('aiAnalyticsOverview');
  if (!box) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.aiOverview || emptyAiAnalyticsOverview();
  if (!state.analytics.loading && Number(current?.totals?.sessions_started || 0) === 0) {
    box.innerHTML = '';
    return;
  }
  const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview();
  box.innerHTML = [
    renderAiAnalyticsCard('Закрыто AI', 'containment_rate', current, previous),
    renderAiAnalyticsCard('Передано оператору', 'handoff_rate', current, previous),
    renderAiAnalyticsCard('Задержка AI', 'ai_latency_avg_ms', current, previous),
    renderAiAnalyticsCard('Закрыто без оператора', 'closed_without_operator_rate', current, previous),
  ].join('');
}

function renderAiAnalyticsChannelComparison() {
  const container = $('aiAnalyticsChannelComparison');
  const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_channel)
    ? state.analytics.aiOverview.breakdowns.by_channel
    : [];
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Нет AI-сессий по Telegram и WhatsApp за выбранный период.</div>';
    return;
  }

  container.innerHTML = `
    <div class="analytics-table-head ai-analytics-channel-grid">
      <span>Канал</span>
      <span>Сессии</span>
      <span>Только AI</span>
      <span>С оператором</span>
      <span>Передача</span>
      <span>Средняя задержка</span>
    </div>
    ${rows
      .map((item) => {
        const aiOnlyShare = item.sessions_started ? (item.ai_only_sessions / item.sessions_started) * 100 : 0;
        const humanTouchedShare = item.sessions_started ? (item.human_touched_sessions / item.sessions_started) * 100 : 0;
        return `
          <div class="analytics-table-row ai-analytics-channel-grid">
            <span>${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span>${formatAnalyticsNumber(item.sessions_started, 0)}</span>
            <span>${formatAnalyticsNumber(aiOnlyShare, 2)}%</span>
            <span>${formatAnalyticsNumber(humanTouchedShare, 2)}%</span>
            <span>${formatAnalyticsNumber(item.handoff_rate || 0, 2)}%</span>
            <span>${formatAiAnalyticsMetric('ai_latency_avg_ms', item.ai_latency_avg_ms)}</span>
          </div>
        `;
      })
      .join('')}
  `;
}

function renderAiAnalyticsCoverage() {
  const container = $('aiAnalyticsCoverage');
  const coverage = state.analytics.aiOverview?.coverage || emptyAiAnalyticsOverview().coverage;
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!state.analytics.loading && Number(state.analytics.aiOverview?.totals?.sessions_started || 0) === 0) {
    container.innerHTML = '';
    return;
  }
  container.innerHTML = `
    <div class="ai-analytics-coverage-grid">
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_interaction_id || 0, 0)}</strong>
        <p>AI-сессий связаны с ID обращения</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_queue_id || 0, 0)}</strong>
        <p>AI-сессий имеют ID очереди через обращение или диалог</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_latency_turns || 0, 0)}</strong>
        <p>AI-сессий содержат данные по задержке ответов модели</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_terminal_state || 0, 0)}</strong>
        <p>AI-сессий дошли до финального состояния</p>
      </div>
    </div>
  `;
}

function agentAnalyticsMetricValue(metric, payload = state.analytics.agentOverview || emptyAgentAnalyticsOverview()) {
  const totals = payload?.totals || {};
  if (metric === 'agents_with_activity') {
    return Number(totals.agents_with_activity || 0);
  }
  if (metric === 'avg_interactions_per_agent') {
    return Number(totals.avg_interactions_per_agent || 0);
  }
  if (metric === 'avg_handle_seconds') {
    return totals.avg_handle_seconds === null || totals.avg_handle_seconds === undefined
      ? null
      : Number(totals.avg_handle_seconds);
  }
  if (metric === 'avg_fcr_rate') {
    return totals.avg_fcr_rate === null || totals.avg_fcr_rate === undefined
      ? null
      : Number(totals.avg_fcr_rate);
  }
  return Number(totals.agents_total || 0);
}

function formatAgentAnalyticsMetric(metric, value) {
  if (value === null || value === undefined) {
    return '—';
  }
  if (metric === 'avg_handle_seconds') {
    return `${formatAnalyticsNumber(value, 1)} с`;
  }
  if (metric === 'avg_fcr_rate') {
    return `${formatAnalyticsNumber(value, 1)}%`;
  }
  if (metric === 'avg_interactions_per_agent') {
    return formatAnalyticsNumber(value, 1);
  }
  return formatAnalyticsNumber(value, 0);
}

function agentAnalyticsDelta(metric, currentPayload, previousPayload) {
  const current = agentAnalyticsMetricValue(metric, currentPayload);
  const previous = agentAnalyticsMetricValue(metric, previousPayload);
  const normalizedCurrent = current === null || current === undefined ? 0 : Number(current);
  const normalizedPrevious = previous === null || previous === undefined ? 0 : Number(previous);
  const diff = normalizedCurrent - normalizedPrevious;
  const digits = metric === 'agents_total' || metric === 'agents_with_activity' ? 0 : 1;
  let text = signedDelta(diff, digits);
  if (metric === 'avg_handle_seconds') {
    text = `${text} с`;
  } else if (metric === 'avg_fcr_rate') {
    text = `${text} п.п.`;
  }
  let tone = 'neutral';
  if (diff !== 0) {
    const higherIsBetter = !['avg_handle_seconds'].includes(metric);
    tone = diff > 0 ? (higherIsBetter ? 'positive' : 'negative') : (higherIsBetter ? 'negative' : 'positive');
  }
  return { text, tone };
}

function agentAnalyticsStateLabel(stateValue) {
  const stateLabels = {
    READY: 'Готов',
    BUSY: 'Занят',
    BREAK: 'Перерыв',
    OFFLINE: 'Оффлайн',
  };
  return stateLabels[stateValue] || stateValue || '—';
}

function renderAgentAnalyticsCard(label, metric, currentPayload, previousPayload) {
  const value = agentAnalyticsMetricValue(metric, currentPayload);
  const delta = state.analytics.compareMode === 'previous'
    ? agentAnalyticsDelta(metric, currentPayload, previousPayload)
    : null;
  return `
    <div class="summary-card analytics-card agent-analytics-card analytics-card-readonly">
      <div class="analytics-card-top">
        <div class="summary-label">${label}</div>
        ${delta ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>` : ''}
      </div>
      <div class="summary-value">${formatAgentAnalyticsMetric(metric, value)}</div>
    </div>
  `;
}

function renderAgentAnalyticsOverview() {
  const box = $('agentAnalyticsOverview');
  if (!box) {
    return;
  }
  if (state.analytics.agentError) {
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.agentOverview || emptyAgentAnalyticsOverview();
  const previous = state.analytics.agentCompare || emptyAgentAnalyticsOverview();
  const totals = current?.totals || {};
  if (!state.analytics.loading && !Number(totals.agents_total || 0) && !Number(totals.agents_with_activity || 0)) {
    box.innerHTML = '';
    return;
  }
  box.innerHTML = [
    renderAgentAnalyticsCard('Агенты с активностью', 'agents_with_activity', current, previous),
    renderAgentAnalyticsCard('Средняя нагрузка', 'avg_interactions_per_agent', current, previous),
    renderAgentAnalyticsCard('Средний AHT', 'avg_handle_seconds', current, previous),
    renderAgentAnalyticsCard('FCR по агентам', 'avg_fcr_rate', current, previous),
  ].join('');
}

function renderAgentAnalyticsStateStrip() {
  const box = $('agentAnalyticsStateStrip');
  if (!box) {
    return;
  }
  if (state.analytics.agentError) {
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.agentOverview || emptyAgentAnalyticsOverview();
  const byState = current?.state_snapshot?.by_state || {};
  const chips = [
    ['READY', Number(byState.READY || 0)],
    ['BUSY', Number(byState.BUSY || 0)],
    ['BREAK', Number(byState.BREAK || 0)],
    ['OFFLINE', Number(byState.OFFLINE || 0)],
  ];
  if (!state.analytics.loading && chips.every(([, value]) => !value)) {
    box.innerHTML = '';
    return;
  }
  box.innerHTML = chips
    .map(([code, value]) => `<span class="agent-state-chip agent-state-chip-${String(code).toLowerCase()}">${agentAnalyticsStateLabel(code)}: ${formatAnalyticsNumber(value, 0)}</span>`)
    .join('');
}

function renderAgentAnalyticsTable() {
  const container = $('agentAnalyticsTable');
  if (!container) {
    return;
  }
  if (state.analytics.agentError) {
    container.innerHTML = '';
    return;
  }
  const payload = state.analytics.agentOverview || emptyAgentAnalyticsOverview();
  const rows = Array.isArray(payload.items) ? payload.items : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">За выбранный период пока нет обращений с назначенным агентом.</div>';
    return;
  }
  const totalAgents = Number(payload?.totals?.agents_total || rows.length);
  container.innerHTML = `
    <div class="analytics-table-head agent-analytics-grid">
      <span>Агент</span>
      <span>Статус</span>
      <span>Очередь</span>
      <span>Обращения</span>
      <span>Обработано</span>
      <span>AHT</span>
      <span>FCR</span>
    </div>
    ${rows
      .map((item) => `
        <button class="analytics-table-row analytics-table-button agent-analytics-grid" type="button" data-analytics-agent-id="${escapeHtml(item.agent_id)}" data-analytics-source-label="${escapeHtml(`Агент: ${item.agent_id}`)}">
          <span>
            <strong>${escapeHtml(item.agent_id)}</strong>
            <small>${escapeHtml(item.last_activity_at ? `Последняя активность: ${formatTime(item.last_activity_at)}` : 'Без активности в окне')}</small>
          </span>
          <span><span class="analytics-drilldown-status analytics-drilldown-status-${escapeHtml(String(item.current_state || 'offline').toLowerCase())}">${escapeHtml(agentAnalyticsStateLabel(item.current_state || 'OFFLINE'))}</span></span>
          <span>${escapeHtml(analyticsQueueName(item.current_queue_id || item.dominant_queue_id || ''))}</span>
          <span>${formatAnalyticsNumber(item.interactions_total || 0, 0)}</span>
          <span>${formatAnalyticsNumber(item.answered_total || 0, 0)}</span>
          <span>${formatAgentAnalyticsMetric('avg_handle_seconds', item.avg_handle_seconds)}</span>
          <span>${formatAgentAnalyticsMetric('avg_fcr_rate', item.fcr_rate)}</span>
        </button>
      `)
      .join('')}
    ${totalAgents > rows.length ? `<div class="agent-analytics-table-foot">Показаны ${formatAnalyticsNumber(rows.length, 0)} из ${formatAnalyticsNumber(totalAgents, 0)} агентов по текущему окну.</div>` : ''}
  `;
}

function agentAnalyticsTrendLabel(metric) {
  const labels = {
    interactions_per_agent: 'Нагрузка на агента',
    agents_with_activity: 'Агенты с активностью',
    avg_handle_seconds: 'Средний AHT',
    fcr_rate: 'FCR по агентам',
  };
  return labels[metric] || metric || 'Тренд';
}

function formatAgentAnalyticsTrendValue(metric, value) {
  if (value === null || value === undefined) {
    return '—';
  }
  if (metric === 'avg_handle_seconds') {
    return `${formatAnalyticsNumber(value, 1)} с`;
  }
  if (metric === 'fcr_rate') {
    return `${formatAnalyticsNumber(value, 1)}%`;
  }
  if (metric === 'interactions_per_agent') {
    return formatAnalyticsNumber(value, 2);
  }
  return formatAnalyticsNumber(value, 0);
}

async function loadVoiceNameAnalyticsTrend(rangeMeta, requestId) {
  const metric = state.analytics.voiceNameTrendMetric || 'scenario_calls';
  const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
  if (!voiceNameAnalyticsSupportedChannel()) {
    state.analytics.voiceNameTrendError = '';
    return emptyVoiceNameAnalyticsTimeseries(
      metric,
      interval,
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      null,
    );
  }
  try {
    const payload = await fetchVoiceNameAnalyticsTimeseries(rangeMeta.current, metric);
    if (requestId !== state.analytics.requestId) {
      return emptyVoiceNameAnalyticsTimeseries(metric, interval);
    }
    return payload || emptyVoiceNameAnalyticsTimeseries(metric, interval);
  } catch (err) {
    state.analytics.voiceNameTrendError = err.message;
    return emptyVoiceNameAnalyticsTimeseries(
      metric,
      interval,
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      null,
    );
  }
}

async function loadAgentAnalyticsTrend(rangeMeta, requestId) {
  const metric = state.analytics.agentTrendMetric || 'interactions_per_agent';
  const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
  try {
    const payload = await fetchAgentAnalyticsTimeseries(rangeMeta.current, metric, interval);
    if (requestId !== state.analytics.requestId) {
      return emptyAgentAnalyticsTimeseries(metric, interval);
    }
    state.analytics.agentTrendInterval = payload?.interval || interval;
    return payload || emptyAgentAnalyticsTimeseries(metric, interval);
  } catch (err) {
    state.analytics.agentTrendError = err.message;
    state.analytics.agentTrendInterval = interval;
    return emptyAgentAnalyticsTimeseries(metric, interval);
  }
}

function renderAgentAnalyticsTrendChart() {
  const container = $('agentAnalyticsTrendChart');
  if (!container) {
    return;
  }
  if (state.analytics.agentError) {
    container.innerHTML = '';
    return;
  }
  if (state.analytics.agentTrendError && !state.analytics.agentTrend?.points?.length) {
    container.innerHTML = `<div class="empty-state">Не удалось обновить тренд по агентам: ${escapeHtml(state.analytics.agentTrendError)}</div>`;
    return;
  }
  const metric = state.analytics.agentTrendMetric || 'interactions_per_agent';
  const trend = state.analytics.agentTrend || emptyAgentAnalyticsTimeseries(metric);
  const items = Array.isArray(trend.points) ? trend.points : [];
  const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null;
  renderTrendChart(container, {
    title: agentAnalyticsTrendLabel(metric),
    items,
    valueAccessor: (item) => item.value,
    labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || state.analytics.agentTrendInterval || 'day'),
    axisFormatter: (value) => formatAgentAnalyticsTrendValue(metric, value),
    valueFormatter: (value) => formatAgentAnalyticsTrendValue(metric, value),
    summaryText: lastPoint
      ? `${formatAgentAnalyticsTrendValue(metric, lastPoint.value)} в последней точке`
      : '—',
    metaBadge: '',
    ariaLabel: 'График тренда по агентской аналитике',
    emptyText: 'За выбранный период недостаточно точек для агентского тренда.',
  });
}

function renderAgentAnalyticsTeamTable() {
  const container = $('agentAnalyticsTeamTable');
  if (!container) {
    return;
  }
  if (state.analytics.agentError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.agentOverview?.breakdowns?.by_team)
    ? state.analytics.agentOverview.breakdowns.by_team
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">По командам пока нет достаточно данных.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head agent-team-grid">
      <span>Команда</span>
      <span>Агенты</span>
      <span>Активны</span>
      <span>Обращения</span>
      <span>AHT</span>
      <span>FCR</span>
    </div>
    ${rows
      .map((item) => `
        <div class="analytics-table-row agent-team-grid">
          <span><strong>${escapeHtml(item.label || item.team_key || 'Без очереди')}</strong></span>
          <span>${formatAnalyticsNumber(item.agents_total || 0, 0)}</span>
          <span>${formatAnalyticsNumber(item.agents_with_activity || 0, 0)}</span>
          <span>${formatAnalyticsNumber(item.interactions_total || 0, 0)}</span>
          <span>${formatAgentAnalyticsMetric('avg_handle_seconds', item.avg_handle_seconds)}</span>
          <span>${formatAgentAnalyticsMetric('avg_fcr_rate', item.fcr_rate)}</span>
        </div>
      `)
      .join('')}
  `;
}

function renderAgentAnalyticsShiftTable() {
  const container = $('agentAnalyticsShiftTable');
  if (!container) {
    return;
  }
  if (state.analytics.agentError) {
    container.innerHTML = '';
    return;
  }
  const rows = Array.isArray(state.analytics.agentOverview?.breakdowns?.by_shift)
    ? state.analytics.agentOverview.breakdowns.by_shift
    : [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">По сменам пока нет достаточно данных.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head agent-shift-grid">
      <span>Смена</span>
      <span>Агенты</span>
      <span>Обращения</span>
      <span>Обработано</span>
      <span>AHT</span>
      <span>FCR</span>
    </div>
    ${rows
      .map((item) => `
        <div class="analytics-table-row agent-shift-grid">
          <span><strong>${escapeHtml(item.label || item.shift_key)}</strong></span>
          <span>${formatAnalyticsNumber(item.agents_with_activity || 0, 0)}</span>
          <span>${formatAnalyticsNumber(item.interactions_total || 0, 0)}</span>
          <span>${formatAnalyticsNumber(item.answered_total || 0, 0)}</span>
          <span>${formatAgentAnalyticsMetric('avg_handle_seconds', item.avg_handle_seconds)}</span>
          <span>${formatAgentAnalyticsMetric('avg_fcr_rate', item.fcr_rate)}</span>
        </div>
      `)
      .join('')}
  `;
}

function renderAgentAnalyticsEmptyState() {
  const box = $('agentAnalyticsEmptyState');
  if (!box) {
    return;
  }
  if (state.analytics.agentError) {
    box.hidden = false;
    box.textContent = `Не удалось обновить аналитику по агентам: ${state.analytics.agentError}`;
    return;
  }
  const totals = state.analytics.agentOverview?.totals || emptyAgentAnalyticsOverview().totals;
  if (!state.analytics.loading && !Number(totals.agents_total || 0) && !Number(totals.agents_with_activity || 0)) {
    box.hidden = false;
    box.textContent = 'За выбранный период нет агентской активности по текущим фильтрам. Попробуйте расширить окно анализа или снять часть ограничений.';
    return;
  }
  box.hidden = true;
  box.textContent = '';
}

function renderAiAnalyticsEmptyState() {
  const box = $('aiAnalyticsEmptyState');
  if (!box) {
    return;
  }
  if (!aiAnalyticsSupportedChannel()) {
    box.hidden = false;
    box.textContent = 'AI-аналитика доступна только для Telegram и WhatsApp. Снимите фильтр по голосу, веб-чату или электронной почте, чтобы увидеть AI-срез.';
    return;
  }
  if (state.analytics.aiError) {
    box.hidden = false;
    box.textContent = `Не удалось обновить AI-аналитику: ${state.analytics.aiError}`;
    return;
  }
  const total = Number(state.analytics.aiOverview?.totals?.sessions_started || 0);
  if (!state.analytics.loading && total === 0) {
    box.hidden = false;
    box.textContent = 'За выбранный период AI-сессий по Telegram и WhatsApp не найдено. Измените период или снимите часть фильтров.';
    return;
  }
  box.hidden = true;
  box.textContent = '';
}

function formatAnalyticsAxisValue(metric, value) {
  const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count' };
  if (meta.unit === 'count') {
    return formatAnalyticsNumber(value, 0);
  }
  return formatAnalyticsNumber(value, 1);
}

function formatAiAnalyticsAxisValue(metric, value) {
  const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count' };
  if (meta.unit === 'pct') {
    return formatAnalyticsNumber(value, 1);
  }
  if (meta.unit === 'ms') {
    return formatAnalyticsNumber(value, 0);
  }
  return formatAnalyticsNumber(value, 0);
}

function formatAnalyticsBucketLabel(ts, interval = 'day') {
  if (!ts) {
    return '';
  }
  const date = new Date(ts);
  if (Number.isNaN(date.getTime())) {
    return String(ts);
  }
  if (interval === 'hour') {
    return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
  }
  return date.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' });
}

function resetTrendChartHover(container) {
  if (!container) {
    return;
  }
  const tooltip = container.querySelector('[data-chart-tooltip]');
  const summary = container.querySelector('[data-chart-summary]');
  if (summary) {
    summary.textContent = summary.dataset.default || '';
    summary.classList.remove('is-hovered');
  }
  if (tooltip) {
    tooltip.hidden = true;
    tooltip.innerHTML = '';
  }
  container.querySelectorAll('.analytics-point.is-active').forEach((node) => node.classList.remove('is-active'));
}

function showTrendChartHover(container, target) {
  if (!container || !target) {
    return;
  }
  const tooltip = container.querySelector('[data-chart-tooltip]');
  const summary = container.querySelector('[data-chart-summary]');
  const plot = container.querySelector('.analytics-chart-plot');
  if (!tooltip || !summary || !plot) {
    return;
  }
  const label = target.dataset.label || '';
  const value = target.dataset.value || '';
  const index = target.dataset.index || '';
  const x = Number(target.dataset.x || 0);
  const width = Number(target.dataset.width || 720);
  const plotWidth = plot.clientWidth || width || 1;
  const leftPx = Math.max(54, Math.min(plotWidth - 54, (x / width) * plotWidth));

  summary.textContent = label && value ? `${label} · ${value}` : value || label || summary.dataset.default || '';
  summary.classList.add('is-hovered');
  tooltip.hidden = false;
  tooltip.style.left = `${leftPx}px`;
  tooltip.innerHTML = `
    <strong>${escapeHtml(label)}</strong>
    <span>${escapeHtml(value)}</span>
  `;

  container.querySelectorAll('.analytics-point.is-active').forEach((node) => node.classList.remove('is-active'));
  const activePoint = container.querySelector(`.analytics-point[data-chart-point="${escapeHtml(index)}"]`);
  if (activePoint) {
    activePoint.classList.add('is-active');
  }
}

function bindTrendChartHover(container) {
  if (!container) {
    return;
  }
  const plot = container.querySelector('.analytics-chart-plot');
  if (!plot) {
    return;
  }
  plot.querySelectorAll('.analytics-point-hit').forEach((node) => {
    node.addEventListener('mouseenter', () => showTrendChartHover(container, node));
    node.addEventListener('focus', () => showTrendChartHover(container, node));
  });
  plot.addEventListener('mouseleave', () => resetTrendChartHover(container));
  plot.addEventListener('focusout', (event) => {
    if (!plot.contains(event.relatedTarget)) {
      resetTrendChartHover(container);
    }
  });
}

function renderTrendChart(container, options) {
  if (!container) {
    return;
  }
  const items = Array.isArray(options.items) ? options.items : [];
  const points = items.map((item, index) => {
    const raw = options.valueAccessor(item, index);
    const value = raw === null || raw === undefined || Number.isNaN(Number(raw)) ? null : Number(raw);
    return {
      label: options.labelAccessor(item, index),
      value,
    };
  });
  const numericValues = points
    .filter((point) => point.value !== null)
    .map((point) => Number(point.value));

  if (!points.length || !numericValues.length) {
    container.innerHTML = `<div class="empty-state">${options.emptyText}</div>`;
    return;
  }

  const maxValue = Math.max(...numericValues, 1);
  const width = 720;
  const height = 220;
  const paddingLeft = 38;
  const paddingRight = 16;
  const paddingTop = 16;
  const paddingBottom = 36;
  const chartWidth = width - paddingLeft - paddingRight;
  const chartHeight = height - paddingTop - paddingBottom;
  const step = points.length > 1 ? chartWidth / (points.length - 1) : chartWidth;

  const pointFor = (value, index) => {
    const x = paddingLeft + index * step;
    const y = paddingTop + chartHeight - (value / maxValue) * chartHeight;
    return { x, y };
  };

  const grid = [0, 0.25, 0.5, 0.75, 1]
    .map((ratio) => {
      const y = paddingTop + chartHeight - ratio * chartHeight;
      const labelValue = maxValue * ratio;
      return `
        <line x1="${paddingLeft}" y1="${y}" x2="${width - paddingRight}" y2="${y}" class="analytics-grid-line"></line>
        <text x="${paddingLeft - 8}" y="${y + 4}" class="analytics-axis-label" text-anchor="end">${escapeHtml(options.axisFormatter(labelValue))}</text>
      `;
    })
    .join('');

  const lineSegments = [];
  let currentSegment = [];
  let markers = '';
  let hitTargets = '';
  points.forEach((point, index) => {
    if (point.value === null) {
      if (currentSegment.length > 1) {
        lineSegments.push(`<polyline points="${currentSegment.join(' ')}" class="analytics-line"></polyline>`);
      }
      currentSegment = [];
      return;
    }
    const marker = pointFor(point.value, index);
    currentSegment.push(`${marker.x},${marker.y}`);
    const formattedValue = options.valueFormatter(point.value);
    markers += `<circle cx="${marker.x}" cy="${marker.y}" r="4" class="analytics-point" data-chart-point="${index}"></circle>`;
    hitTargets += `<circle cx="${marker.x}" cy="${marker.y}" r="14" class="analytics-point-hit" tabindex="0" role="img" aria-label="${escapeHtml(`${point.label}: ${formattedValue}`)}" data-index="${index}" data-label="${escapeHtml(point.label)}" data-value="${escapeHtml(formattedValue)}" data-x="${marker.x}" data-width="${width}"></circle>`;
  });
  if (currentSegment.length > 1) {
    lineSegments.push(`<polyline points="${currentSegment.join(' ')}" class="analytics-line"></polyline>`);
  }

  const labelStep = points.length > 12 ? Math.ceil(points.length / 6) : 1;
  const labels = points
    .map((point, index) => {
      if (index % labelStep !== 0 && index !== points.length - 1) {
        return '';
      }
      const x = paddingLeft + index * step;
      return `<text x="${x}" y="${height - 10}" class="analytics-axis-label" text-anchor="middle">${escapeHtml(point.label)}</text>`;
    })
    .join('');

  const summaryText = options.summaryText || '';
  const metaBadge = options.metaBadge || '';
  container.innerHTML = `
    <div class="analytics-chart-meta">
      <strong>${escapeHtml(options.title)}</strong>
      <div class="analytics-chart-meta-values">
        ${metaBadge}
        <span class="analytics-chart-summary" data-chart-summary data-default="${escapeHtml(summaryText)}">${escapeHtml(summaryText)}</span>
      </div>
    </div>
    <div class="analytics-chart-plot">
      <div class="analytics-chart-tooltip" data-chart-tooltip hidden></div>
      <svg viewBox="0 0 ${width} ${height}" class="analytics-chart-svg" role="img" aria-label="${escapeHtml(options.ariaLabel)}">
        ${grid}
        ${lineSegments.join('')}
        ${markers}
        ${hitTargets}
        ${labels}
      </svg>
    </div>
  `;
  resetTrendChartHover(container);
  bindTrendChartHover(container);
}

function renderAnalyticsChannelTable() {
  const container = $('analyticsChannelTable');
  const rows = state.analytics.channelRows || [];
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Нет данных по каналам за выбранный период.</div>';
    return;
  }

  container.innerHTML = `
    <div class="analytics-table-head analytics-channel-grid">
      <span>Канал</span>
      <span>Всего</span>
      <span>Обработано</span>
      <span>Потери</span>
      <span>Доля ответа</span>
    </div>
    ${rows
      .map((item) => {
        const answerRate = item.total ? (item.answered / item.total) * 100 : 0;
        return `
          <button class="analytics-table-row analytics-table-button analytics-channel-grid" type="button" data-analytics-channel="${escapeHtml(item.channel)}" data-analytics-source-label="${escapeHtml(analyticsChannelLabel(item.channel))}">
            <span>${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span>${formatAnalyticsNumber(item.total, 0)}</span>
            <span>${formatAnalyticsNumber(item.answered, 0)}</span>
            <span>${formatAnalyticsNumber(item.abandoned, 0)}</span>
            <span>${formatAnalyticsNumber(answerRate, 2)}%</span>
          </button>
        `;
      })
      .join('')}
  `;
}

function renderAnalyticsQueueTable() {
  const container = $('analyticsQueueTable');
  const rows = state.analytics.queueRows || [];
  if (!rows.length) {
    const reason = state.analytics.queueAccessError
      ? 'Каталог очередей пока недоступен для этой роли. Можно продолжить анализ без разреза по очередям.'
      : 'За выбранное окно пока нет данных по очередям. Попробуйте расширить период или снять часть фильтров.';
    container.innerHTML = `<div class="empty-state">${escapeHtml(reason)}</div>`;
    return;
  }

  container.innerHTML = `
    <div class="analytics-table-head analytics-queue-grid">
      <span>Очередь</span>
      <span>Всего</span>
      <span>Обработано</span>
      <span>SL</span>
      <span>Потери</span>
    </div>
    ${rows
      .map((item) => `
        <button class="analytics-table-row analytics-table-button analytics-queue-grid" type="button" data-analytics-queue-id="${escapeHtml(item.queue_id)}" data-analytics-source-label="${escapeHtml(item.name || item.queue_id)}">
          <span><strong>${escapeHtml(item.name || item.queue_id)}</strong><small>${escapeHtml(item.queue_id)}</small></span>
          <span>${formatAnalyticsNumber(item.total, 0)}</span>
          <span>${formatAnalyticsNumber(item.answered, 0)}</span>
          <span>${formatAnalyticsNumber(item.SL, 2)}%</span>
          <span>${formatAnalyticsNumber(item.Abandon, 2)}%</span>
        </button>
      `)
      .join('')}
  `;
}

function renderAnalyticsCoverage() {
  const container = $('analyticsCoverage');
  if (!container) {
    return;
  }
  const implemented = Array.isArray(state.analytics.coverage?.implemented_metrics)
    ? state.analytics.coverage.implemented_metrics
    : [];
  const dimensions = Array.isArray(state.analytics.coverage?.dimensions)
    ? state.analytics.coverage.dimensions
    : [];
  const filters = Array.isArray(state.analytics.coverage?.supported_filters)
    ? state.analytics.coverage.supported_filters
    : [];

  container.innerHTML = `
    <div class="analytics-coverage-list">
      <div class="analytics-coverage-block">
        <h4>Уже измеряем</h4>
        <div class="analytics-coverage-chips">
          ${implemented.length
            ? implemented
                .map((item) => `<span class="analytics-coverage-chip">${escapeHtml(item.code)} · ${escapeHtml(item.label)}</span>`)
                .join('')
            : '<span class="empty-state">Каталог метрик пока не загружен.</span>'}
        </div>
      </div>
      <div class="analytics-coverage-block">
        <h4>Разрезы</h4>
        <div class="analytics-coverage-chips">
          ${dimensions.length
            ? dimensions.map((item) => `<span class="analytics-coverage-chip soft">${escapeHtml(item)}</span>`).join('')
            : '<span class="empty-state">Разрезы появятся после загрузки coverage.</span>'}
        </div>
      </div>
      <div class="analytics-coverage-block">
        <h4>Фильтры</h4>
        <div class="analytics-coverage-chips">
          ${filters.length
            ? filters.map((item) => `<span class="analytics-coverage-chip soft">${escapeHtml(item)}</span>`).join('')
            : '<span class="empty-state">Фильтры появятся после загрузки coverage.</span>'}
        </div>
      </div>
    </div>
    <div class="analytics-placeholder-grid">
      <article class="analytics-placeholder-card">
        <strong>Аналитика по агентам</strong>
        <p>Следующий шаг: персональные метрики по агентам, сменам и командам с детализацией по эффективности.</p>
      </article>
      <article class="analytics-placeholder-card">
        <strong>AI-аналитика</strong>
        <p>Добавим долю закрытий AI, передачу оператору, задержку ответа и устойчивость AI-сценариев по каналам.</p>
      </article>
      <article class="analytics-placeholder-card">
        <strong>Качество диалогов</strong>
        <p>Здесь появятся оценки качества речи и диалогов на основе скоринга транскриптов и QA-правил.</p>
      </article>
      ${
        state.analytics.queueAccessError
          ? `<article class="analytics-placeholder-card">
              <strong>Доступ к очередям</strong>
              <p>${escapeHtml(state.analytics.queueAccessError)}</p>
            </article>`
          : ''
      }
    </div>
  `;
}

function renderAnalyticsCoverage() {
  const container = $('analyticsCoverage');
  if (!container) {
    return;
  }
  const implemented = Array.isArray(state.analytics.coverage?.implemented_metrics)
    ? state.analytics.coverage.implemented_metrics
    : [];
  const dimensions = Array.isArray(state.analytics.coverage?.dimensions)
    ? state.analytics.coverage.dimensions
    : [];
  const filters = Array.isArray(state.analytics.coverage?.supported_filters)
    ? state.analytics.coverage.supported_filters
    : [];

  container.innerHTML = `
    <div class="analytics-coverage-list">
      <div class="analytics-coverage-block">
        <h4>Уже измеряем</h4>
        <div class="analytics-coverage-chips">
          ${implemented.length
            ? implemented
                .map((item) => `<span class="analytics-coverage-chip">${escapeHtml(item.code)} · ${escapeHtml(item.label)}</span>`)
                .join('')
            : '<span class="empty-state">Каталог метрик пока не загружен.</span>'}
        </div>
      </div>
      <div class="analytics-coverage-block">
        <h4>Разрезы</h4>
        <div class="analytics-coverage-chips">
          ${dimensions.length
            ? dimensions.map((item) => `<span class="analytics-coverage-chip soft">${escapeHtml(item)}</span>`).join('')
            : '<span class="empty-state">Разрезы появятся после загрузки coverage.</span>'}
        </div>
      </div>
      <div class="analytics-coverage-block">
        <h4>Фильтры</h4>
        <div class="analytics-coverage-chips">
          ${filters.length
            ? filters.map((item) => `<span class="analytics-coverage-chip soft">${escapeHtml(item)}</span>`).join('')
            : '<span class="empty-state">Фильтры появятся после загрузки coverage.</span>'}
        </div>
      </div>
    </div>
    <div class="analytics-placeholder-grid">
      <article class="analytics-placeholder-card">
        <strong>Аналитика по агентам</strong>
        <p>Следующий шаг: персональные метрики по агентам, сменам и командам с детализацией по эффективности.</p>
      </article>
      <article class="analytics-placeholder-card">
        <strong>Прогнозы и алерты</strong>
        <p>Дальше сюда добавим прогноз нагрузки, сигналы по SLA и аномалии по очередям и каналам.</p>
      </article>
      <article class="analytics-placeholder-card">
        <strong>Качество диалогов</strong>
        <p>Здесь появятся оценки качества речи и диалогов на основе скоринга транскриптов и QA-правил.</p>
      </article>
      ${
        state.analytics.queueAccessError
          ? `<article class="analytics-placeholder-card">
              <strong>Доступ к очередям</strong>
              <p>${escapeHtml(state.analytics.queueAccessError)}</p>
            </article>`
          : ''
      }
    </div>
  `;
}

function csvEscape(value) {
  const text = String(value ?? '');
  if (/[",\n]/.test(text)) {
    return `"${text.replaceAll('"', '""')}"`;
  }
  return text;
}

function exportRowsToCsv(rows) {
  const columns = ['section', 'key', 'label', 'current', 'previous', 'delta', 'value', 'note'];
  return [
    columns.join(','),
    ...rows.map((row) => columns.map((column) => csvEscape(row[column] ?? '')).join(',')),
  ].join('\n');
}

function downloadCsvFile(csv, filename) {
  const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.append(link);
  link.click();
  link.remove();
  URL.revokeObjectURL(url);
}

function buildAnalyticsExportRows() {
  const rows = [];
  const rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls();
  const current = state.analytics.overview || emptyKpiEnvelope();
  const previous = state.analytics.compare || emptyKpiEnvelope();
  const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null;

  rows.push({
    section: 'filters',
    key: 'period',
    label: 'Период',
    value: rangeMeta.custom
      ? `${formatTime(rangeMeta.current.from.toISOString())} - ${formatTime(rangeMeta.current.to.toISOString())}`
      : analyticsPresetLabel(rangeMeta.preset),
  });
  rows.push({
    section: 'filters',
    key: 'channel',
    label: 'Канал',
    value: state.analytics.channel === 'all' ? 'Все каналы' : analyticsChannelLabel(state.analytics.channel),
  });
  rows.push({
    section: 'filters',
    key: 'queue',
    label: 'Очередь',
    value: state.analytics.queueId === 'all'
      ? 'Все очереди'
      : state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId)?.name || state.analytics.queueId,
  });
  rows.push({
    section: 'filters',
    key: 'compare_mode',
    label: 'Сравнение',
    value: analyticsCompareModeLabel(state.analytics.compareMode),
  });
  if (activeView) {
    rows.push({
      section: 'filters',
      key: 'saved_view',
      label: 'Сохранённый вид',
      value: activeView.name,
    });
  }

  [
    ['total', 'Обращения'],
    ['answered', 'Обработано'],
    ['SL', 'SL'],
    ['ASA', 'Среднее ожидание'],
    ['AHT', 'Среднее время'],
    ['Abandon', 'Потери'],
    ['FCR', 'FCR'],
    ['DigitalShare', 'Цифровая доля'],
  ].forEach(([metric, label]) => {
    const delta = state.analytics.compareMode === 'previous'
      ? analyticsDelta(metric, current, previous).text
      : '';
    rows.push({
      section: 'overview',
      key: metric,
      label,
      current: formatAnalyticsMetric(metric, metricValueFromPayload(metric, current)),
      previous: state.analytics.compareMode === 'previous'
        ? formatAnalyticsMetric(metric, metricValueFromPayload(metric, previous))
        : '',
      delta,
      note: ANALYTICS_METRIC_META[metric]?.note || '',
    });
  });

  state.analytics.channelRows.forEach((item) => {
    const answerRate = item.total ? (item.answered / item.total) * 100 : 0;
    rows.push({
      section: 'channels',
      key: item.channel,
      label: analyticsChannelLabel(item.channel),
      current: formatAnalyticsNumber(item.total, 0),
      previous: formatAnalyticsNumber(item.answered, 0),
      delta: formatAnalyticsNumber(item.abandoned, 0),
      value: `${formatAnalyticsNumber(answerRate, 2)}%`,
      note: 'current=всего, previous=обработано, delta=потери, value=доля ответа',
    });
  });

  state.analytics.queueRows.forEach((item) => {
    rows.push({
      section: 'queues',
      key: item.queue_id,
      label: item.name || item.queue_id,
      current: formatAnalyticsNumber(item.total, 0),
      previous: formatAnalyticsNumber(item.answered, 0),
      delta: `${formatAnalyticsNumber(item.SL, 2)}%`,
      value: `${formatAnalyticsNumber(item.Abandon, 2)}%`,
      note: 'current=всего, previous=обработано, delta=SL, value=потери',
    });
  });

  state.analytics.trend.forEach((item) => {
    rows.push({
      section: 'trend',
      key: item.label,
      label: analyticsMetricLabel(state.analytics.trendMetric),
      value: formatAnalyticsMetric(state.analytics.trendMetric, metricValueFromPayload(state.analytics.trendMetric, item.payload)),
      note: item.label,
    });
  });

  (state.analytics.coverage?.implemented_metrics || []).forEach((item) => {
    rows.push({
      section: 'coverage',
      key: item.code,
      label: item.label,
      value: 'implemented',
    });
  });

  return rows;
}

function exportAnalyticsCsv() {
  const rows = buildAnalyticsExportRows();
  const dateStamp = new Date().toISOString().slice(0, 10);
  downloadCsvFile(exportRowsToCsv(rows), `konturcc-analytics-${dateStamp}.csv`);
  state.analytics.statusMessage = `CSV выгружен в ${new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}.`;
  renderSavedAnalyticsViews();
}

function exportDrilldownRowsToCsv(rows) {
  const columns = [
    'interaction_id',
    'subject',
    'channel',
    'status',
    'queue_id',
    'assigned_to',
    'created_at',
    'updated_at',
    'answered',
    'abandoned',
    'wait_seconds',
    'handle_seconds',
    'within_sla',
    'resolved_first_contact',
  ];
  return [
    columns.join(','),
    ...rows.map((row) => columns.map((column) => csvEscape(row[column] ?? '')).join(',')),
  ].join('\n');
}

const exportAnalyticsDrilldownCsvBase = exportAnalyticsDrilldownCsv;
const renderAnalyticsDrilldownBase = renderAnalyticsDrilldown;
const loadAnalyticsDrilldownDetailsBase = loadAnalyticsDrilldownDetails;
const loadAnalyticsDrilldownPageBase = loadAnalyticsDrilldownPage;
const openAnalyticsDrilldownBase = openAnalyticsDrilldown;
const handleAnalyticsDrilldownListClickBase = handleAnalyticsDrilldownListClick;

exportAnalyticsDrilldownCsv = async function exportAnalyticsDrilldownCsv() {
  if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.exporting) {
    return;
  }
  if (!state.drilldown.total) {
    state.drilldown.notice = 'Для текущего среза нет строк для экспорта.';
    renderAnalyticsDrilldown();
    return;
  }

  state.drilldown.exporting = true;
  state.drilldown.notice = '';
  state.drilldown.error = '';
  renderAnalyticsDrilldown();

  try {
    const exportLimit = Math.min(state.drilldown.total, DRILLDOWN_EXPORT_SOFT_CAP);
    const rows = [];
    for (let currentOffset = 0; currentOffset < exportLimit; currentOffset += DRILLDOWN_EXPORT_BATCH_SIZE) {
      const batchLimit = Math.min(DRILLDOWN_EXPORT_BATCH_SIZE, exportLimit - currentOffset);
      const payload = state.drilldown.mode === 'metric'
        ? await fetchAnalyticsMetricDrilldown(state.drilldown.filters, batchLimit, currentOffset)
        : await api(
          'interaction',
          `interactions/drilldown?${analyticsDrilldownQuery(state.drilldown.filters, batchLimit, currentOffset)}`,
        );
      const batchItems = Array.isArray(payload?.items) ? payload.items : [];
      rows.push(...batchItems);
      if (!batchItems.length || batchItems.length < batchLimit) {
        break;
      }
    }
    const dateStamp = new Date().toISOString().slice(0, 10);
    downloadCsvFile(
      exportDrilldownRowsToCsv(rows.slice(0, DRILLDOWN_EXPORT_SOFT_CAP)),
      `konturcc-drilldown-${analyticsDrilldownFilenameSource()}-${dateStamp}.csv`,
    );
    state.drilldown.notice = state.drilldown.total > DRILLDOWN_EXPORT_SOFT_CAP
      ? `Экспортирован только первый ${DRILLDOWN_EXPORT_SOFT_CAP} строк из ${formatAnalyticsNumber(state.drilldown.total, 0)}.`
      : `Экспорт готов: ${formatAnalyticsNumber(rows.length, 0)} строк.`;
  } catch (err) {
    state.drilldown.error = err.message;
  } finally {
    state.drilldown.exporting = false;
    renderAnalyticsDrilldown();
  }
}

function buildTrendBuckets(range, preset, custom) {
  const buckets = [];
  const hourMs = 60 * 60 * 1000;
  const dayMs = 24 * hourMs;

  if (custom) {
    const fromDay = startOfDay(range.from);
    const endDay = startOfDay(addDays(range.to, 1));
    const totalDays = Math.max(1, Math.ceil((endDay.getTime() - fromDay.getTime()) / dayMs));
    const bucketDays = Math.max(1, Math.ceil(totalDays / 31));
    for (let cursor = new Date(fromDay); cursor < endDay; cursor = new Date(cursor.getTime() + bucketDays * dayMs)) {
      const bucketFrom = new Date(cursor);
      const bucketTo = new Date(Math.min(bucketFrom.getTime() + bucketDays * dayMs, endDay.getTime()));
      const lastDay = new Date(bucketTo.getTime() - dayMs);
      const label = bucketDays === 1
        ? bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })
        : `${bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })} - ${lastDay.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })}`;
      buckets.push({ label, range: { from: bucketFrom, to: bucketTo } });
    }
    return buckets.slice(0, 31);
  }

  if (preset === 'today') {
    const dayStart = startOfDay(range.from);
    for (let hour = 0; hour < 24; hour += 1) {
      const bucketFrom = new Date(dayStart.getTime() + hour * hourMs);
      const bucketTo = new Date(bucketFrom.getTime() + hourMs);
      buckets.push({
        label: `${pad2(hour)}:00`,
        range: { from: bucketFrom, to: bucketTo },
      });
    }
    return buckets;
  }

  const days = preset === '30d' ? 30 : 7;
  const firstDay = startOfDay(range.from);
  for (let day = 0; day < days; day += 1) {
    const bucketFrom = new Date(firstDay.getTime() + day * dayMs);
    const bucketTo = new Date(bucketFrom.getTime() + dayMs);
    buckets.push({
      label: bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }),
      range: { from: bucketFrom, to: bucketTo },
    });
  }
  return buckets;
}

function aiAnalyticsIntervalForRange(rangeMeta) {
  const durationMs = Math.max(0, rangeMeta.current.to.getTime() - rangeMeta.current.from.getTime());
  return durationMs <= 36 * 60 * 60 * 1000 ? 'hour' : 'day';
}

async function loadAiAnalyticsTrend(rangeMeta, requestId) {
  const metric = state.analytics.aiTrendMetric || 'containment_rate';
  const interval = aiAnalyticsIntervalForRange(rangeMeta);
  if (!aiAnalyticsSupportedChannel()) {
    return emptyAiAnalyticsTimeseries(
      metric,
      interval,
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
  }

  try {
    const payload = await fetchAiAnalyticsTimeseries(rangeMeta.current, metric, interval);
    if (requestId !== state.analytics.requestId) {
      return emptyAiAnalyticsTimeseries(
        metric,
        interval,
        rangeMeta.current.from.toISOString(),
        rangeMeta.current.to.toISOString(),
        state.analytics.channel,
        state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      );
    }
    return payload || emptyAiAnalyticsTimeseries(
      metric,
      interval,
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
  } catch (err) {
    if (requestId === state.analytics.requestId) {
      state.analytics.aiError = err.message;
    }
    return emptyAiAnalyticsTimeseries(
      metric,
      interval,
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
  }
}

async function loadAnalyticsQueueRows(rangeMeta, queues, requestId) {
  if (!queues.length) {
    return [];
  }
  const rows = await Promise.all(
    queues.map(async (queue) => {
      try {
        const payload = await fetchAnalyticsKpi(rangeMeta.current, {
          queueId: queue.queue_id,
          channel: state.analytics.channel,
        });
        return {
          queue_id: queue.queue_id,
          name: queue.name || queue.queue_id,
          total: Number(payload?.volume?.total || 0),
          answered: Number(payload?.volume?.answered || 0),
          SL: Number(payload?.kpi?.SL || 0),
          Abandon: Number(payload?.kpi?.Abandon || 0),
        };
      } catch {
        return {
          queue_id: queue.queue_id,
          name: queue.name || queue.queue_id,
          total: 0,
          answered: 0,
          SL: 0,
          Abandon: 0,
        };
      }
    }),
  );
  if (requestId !== state.analytics.requestId) {
    return [];
  }
  return rows.sort((a, b) => b.total - a.total || a.name.localeCompare(b.name));
}

function renderAnalyticsTrendChart() {
  const metric = state.analytics.trendMetric || 'volume';
  const items = state.analytics.trend || [];
  const container = $('analyticsTrendChart');
  if (!items.length) {
    container.innerHTML = '<div class="empty-state">За выбранный период недостаточно точек для построения тренда. Попробуйте расширить окно анализа.</div>';
    return;
  }

  const values = items.map((item) => metricValueFromPayload(metric, item.payload));
  const maxValue = Math.max(...values, 1);
  const width = 720;
  const height = 220;
  const paddingLeft = 38;
  const paddingRight = 16;
  const paddingTop = 16;
  const paddingBottom = 36;
  const chartWidth = width - paddingLeft - paddingRight;
  const chartHeight = height - paddingTop - paddingBottom;
  const step = items.length > 1 ? chartWidth / (items.length - 1) : chartWidth;

  const pointFor = (value, index) => {
    const x = paddingLeft + index * step;
    const y = paddingTop + chartHeight - (value / maxValue) * chartHeight;
    return { x, y };
  };

  const polyline = values
    .map((value, index) => {
      const point = pointFor(value, index);
      return `${point.x},${point.y}`;
    })
    .join(' ');

  const grid = [0, 0.25, 0.5, 0.75, 1]
    .map((ratio) => {
      const y = paddingTop + chartHeight - ratio * chartHeight;
      const labelValue = maxValue * ratio;
      return `
        <line x1="${paddingLeft}" y1="${y}" x2="${width - paddingRight}" y2="${y}" class="analytics-grid-line"></line>
        <text x="${paddingLeft - 8}" y="${y + 4}" class="analytics-axis-label" text-anchor="end">${escapeHtml(formatAnalyticsNumber(labelValue, metric === 'volume' ? 0 : 1))}</text>
      `;
    })
    .join('');

  const markers = values
    .map((value, index) => {
      const point = pointFor(value, index);
      return `<circle cx="${point.x}" cy="${point.y}" r="4" class="analytics-point"></circle>`;
    })
    .join('');

  const labelStep = items.length > 12 ? Math.ceil(items.length / 6) : 1;
  const labels = items
    .map((item, index) => {
      if (index % labelStep !== 0 && index !== items.length - 1) {
        return '';
      }
      const point = pointFor(values[index], index);
      return `<text x="${point.x}" y="${height - 10}" class="analytics-axis-label" text-anchor="middle">${escapeHtml(item.label)}</text>`;
    })
    .join('');

  container.innerHTML = `
    <div class="analytics-chart-meta">
      <strong>${escapeHtml(analyticsMetricLabel(metric))}</strong>
      <span>${escapeHtml(formatAnalyticsMetric(metric, values[values.length - 1] || 0))} в последней точке</span>
    </div>
    <svg viewBox="0 0 ${width} ${height}" class="analytics-chart-svg" role="img" aria-label="Analytics trend chart">
      ${grid}
      <polyline points="${polyline}" class="analytics-line"></polyline>
      ${markers}
      ${labels}
    </svg>
  `;
}

function renderAnalyticsTrendChart() {
  const metric = state.analytics.trendMetric || 'volume';
  const items = state.analytics.trend || [];
  const container = $('analyticsTrendChart');
  if (!container) {
    return;
  }
  const lastValue = items.length
    ? metricValueFromPayload(metric, items[items.length - 1].payload)
    : null;
  renderTrendChart(container, {
    title: analyticsMetricLabel(metric),
    items,
    valueAccessor: (item) => metricValueFromPayload(metric, item.payload),
    labelAccessor: (item) => item.label,
    axisFormatter: (value) => formatAnalyticsAxisValue(metric, value),
    valueFormatter: (value) => formatAnalyticsMetric(metric, value),
    summaryText: lastValue === null || lastValue === undefined
      ? '—'
      : `${formatAnalyticsMetric(metric, lastValue)} в последней точке`,
    ariaLabel: 'Analytics trend chart',
    emptyText: 'За выбранный период недостаточно точек для построения тренда. Попробуйте расширить окно анализа.',
  });
}

function renderAiAnalyticsTrendChart() {
  const container = $('aiAnalyticsTrendChart');
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel()) {
    container.innerHTML = '';
    return;
  }
  if (state.analytics.aiError && !state.analytics.aiTrend?.points?.length) {
    container.innerHTML = `<div class="empty-state">Не удалось обновить AI-тренд: ${escapeHtml(state.analytics.aiError)}</div>`;
    return;
  }

  const metric = state.analytics.aiTrendMetric || 'containment_rate';
  const trend = state.analytics.aiTrend || emptyAiAnalyticsTimeseries(metric);
  const items = Array.isArray(trend.points) ? trend.points : [];
  const current = state.analytics.aiOverview || emptyAiAnalyticsOverview();
  const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview();
  const delta = state.analytics.compareMode === 'previous'
    ? aiAnalyticsDelta(metric, current, previous)
    : null;
  const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null;
  const emptyText = metric === 'ai_latency_avg_ms'
    ? 'Для выбранного окна пока нет данных по задержке ответов модели. Попробуйте изменить период или канал.'
    : 'За выбранный период недостаточно AI-точек для построения тренда.';
  renderTrendChart(container, {
    title: aiAnalyticsMetricLabel(metric),
    items,
    valueAccessor: (item) => item.value,
    labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || 'day'),
    axisFormatter: (value) => formatAiAnalyticsAxisValue(metric, value),
    valueFormatter: (value) => formatAiAnalyticsMetric(metric, value),
    summaryText: lastPoint
      ? `${formatAiAnalyticsMetric(metric, lastPoint.value)} в последней точке`
      : '—',
    metaBadge: delta ? `<span class="analytics-delta analytics-delta-${delta.tone}">${escapeHtml(delta.text)}</span>` : '',
    ariaLabel: 'График тренда AI-аналитики',
    emptyText,
  });
}

function analyticsDrilldownFiltersForCurrentSource() {
  const filters = analyticsDrilldownBaseFilters();
  if (state.drilldown.mode === 'metric' && state.drilldown.metric) {
    filters.metric = state.drilldown.metric;
    filters.q = '';
    filters.status = '';
  }
  if (state.drilldown.sourceType === 'channel' && state.drilldown.sourceValue) {
    filters.channel = state.drilldown.sourceValue;
  }
  if (state.drilldown.sourceType === 'queue' && state.drilldown.sourceValue) {
    filters.queue_id = state.drilldown.sourceValue;
  }
  if (state.drilldown.sourceType === 'agent' && state.drilldown.sourceValue) {
    filters.agent_id = state.drilldown.sourceValue;
  }
  return filters;
}

function applyAnalyticsDrilldownLocalFilters(nextFilters, options = {}) {
  if (!state.drilldown.open || state.drilldown.mode !== 'interaction') {
    return;
  }
  state.drilldown.filters = normalizeAnalyticsDrilldownFilters(nextFilters);
  state.drilldown.offset = 0;
  state.drilldown.notice = '';
  void loadAnalyticsDrilldownPage({ preserveSelection: options.preserveSelection !== false });
}

function resetAnalyticsDrilldownLocalFilters() {
  clearAnalyticsDrilldownSearchDebounce();
  if (!state.drilldown.open || state.drilldown.mode !== 'interaction') {
    return;
  }
  state.drilldown.filters = analyticsDrilldownFiltersForCurrentSource();
  state.drilldown.offset = 0;
  state.drilldown.notice = '';
  renderAnalyticsDrilldown();
  void loadAnalyticsDrilldownPage({ preserveSelection: true });
}

function handleAnalyticsDrilldownSearchInput(event) {
  if (!state.drilldown.open || state.drilldown.mode !== 'interaction') {
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  const query = event.target?.value || '';
  analyticsDrilldownSearchDebounce = window.setTimeout(() => {
    if (!state.drilldown.open || !state.drilldown.filters) {
      return;
    }
    const normalizedQuery = query.trim();
    if ((state.drilldown.filters.q || '') === normalizedQuery) {
      return;
    }
    applyAnalyticsDrilldownLocalFilters({
      ...state.drilldown.filters,
      q: normalizedQuery,
    });
  }, 300);
}

function handleAnalyticsDrilldownSearchKeydown(event) {
  if (event.key !== 'Enter' || !state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') {
    return;
  }
  event.preventDefault();
  clearAnalyticsDrilldownSearchDebounce();
  const normalizedQuery = (event.target?.value || '').trim();
  if ((state.drilldown.filters.q || '') === normalizedQuery) {
    return;
  }
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    q: normalizedQuery,
  });
}

function handleAnalyticsDrilldownStatusChange(event) {
  if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') {
    return;
  }
  const nextStatus = event.target?.value === 'all' ? '' : String(event.target?.value || '');
  if ((state.drilldown.filters.status || '') === nextStatus) {
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    status: nextStatus,
  });
}

function handleAnalyticsDrilldownSortChange(event) {
  if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') {
    return;
  }
  const nextSort = parseAnalyticsDrilldownSort(event.target?.value || 'created_at:desc');
  if (
    (state.drilldown.filters.sort_by || 'created_at') === nextSort.sort_by
    && (state.drilldown.filters.sort_dir || 'desc') === nextSort.sort_dir
  ) {
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    ...nextSort,
  });
}

function openAnalyticsDrilldown(sourceType, sourceValue = '', options = {}) {
  const mode = options.mode || (sourceType === 'metric' ? 'metric' : 'interaction');
  if (mode !== 'ai') {
    return openAnalyticsDrilldownBase(sourceType, sourceValue, options);
  }
  const filters = analyticsDrilldownBaseFilters();
  filters.slice = options.slice || sourceValue || 'all';
  filters.reason_key = options.reasonKey || '';
  filters.status = '';
  filters.q = '';
  filters.sort_by = 'created_at';
  filters.sort_dir = 'desc';
  if (sourceType === 'ai-channel' && sourceValue) {
    filters.channel = sourceValue;
  }
  if (sourceType === 'ai-reason' && sourceValue) {
    filters.slice = 'handoff';
    filters.reason_key = sourceValue;
  }
  rememberAnalyticsDrilldownReturnFocus();
  state.drilldown = {
    ...emptyDrilldownState(),
    open: true,
    mode: 'ai',
    sourceType,
    sourceLabel: options.sourceLabel || analyticsDrilldownSourceLabel(sourceType, sourceValue),
    sourceValue,
    filters: normalizeAnalyticsDrilldownFilters(filters),
    coverage: options.coverage || null,
    metricNote: options.metricNote || aiAnalyticsDrilldownNote(filters),
  };
  renderAnalyticsDrilldown();
  window.requestAnimationFrame(() => {
    focusAnalyticsDrilldownPrimaryControl(true);
  });
  void loadAnalyticsDrilldownPage();
}

async function loadAnalyticsDrilldownPage(options = {}) {
  if (state.drilldown.mode !== 'ai') {
    return loadAnalyticsDrilldownPageBase(options);
  }
  if (!state.drilldown.open || !state.drilldown.filters) {
    return;
  }
  const preserveSelection = Boolean(options.preserveSelection);
  state.drilldown.loading = true;
  state.drilldown.error = '';
  renderAnalyticsDrilldown();
  try {
    const data = await fetchAiAnalyticsDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset);
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
    state.drilldown.total = Number(data?.total || 0);
    state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE);
    state.drilldown.offset = Number(data?.offset || 0);
    state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters);
    state.drilldown.coverage = data?.coverage || state.drilldown.coverage;
    state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters);

    const selectedId = state.drilldown.selectedInteractionId || '';
    const selectedStillVisible = preserveSelection
      && selectedId
      && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId);
    if (selectedStillVisible) {
      state.drilldown.selectedInteraction = state.drilldown.items.find(
        (item) => analyticsDrilldownCurrentItemId(item) === selectedId,
      ) || state.drilldown.selectedInteraction;
      void loadAnalyticsDrilldownDetails(selectedId);
    } else if (state.drilldown.items.length) {
      state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]);
      state.drilldown.selectedInteraction = state.drilldown.items[0];
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else {
      state.drilldown.selectedInteractionId = '';
      state.drilldown.selectedInteraction = null;
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      state.drilldown.detailLoading = false;
    }
  } catch (err) {
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = [];
    state.drilldown.total = 0;
    state.drilldown.selectedInteractionId = '';
    state.drilldown.selectedInteraction = null;
    state.drilldown.selectedLinkedInteraction = null;
    state.drilldown.selectedTimeline = [];
    state.drilldown.detailLoading = false;
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open) {
      state.drilldown.loading = false;
      renderAnalyticsDrilldown();
    }
  }
}

async function loadAnalyticsDrilldownDetails(interactionId) {
  if (state.drilldown.mode !== 'ai') {
    return loadAnalyticsDrilldownDetailsBase(interactionId);
  }
  if (!state.drilldown.open || !interactionId) {
    return;
  }
  state.drilldown.selectedInteractionId = interactionId;
  state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.session_id === interactionId) || null;
  state.drilldown.selectedLinkedInteraction = null;
  state.drilldown.selectedTimeline = [];
  state.drilldown.error = '';
  state.drilldown.detailLoading = true;
  renderAnalyticsDrilldown();

  try {
    const detail = await fetchAiAnalyticsSessionDetail(interactionId);
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) {
      return;
    }
    state.drilldown.selectedInteraction = {
      ...(state.drilldown.selectedInteraction || {}),
      ...(detail?.session || {}),
    };
    state.drilldown.selectedLinkedInteraction = detail?.interaction || null;
    state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : [];
  } catch (err) {
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) {
      return;
    }
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open && state.drilldown.selectedInteractionId === interactionId) {
      state.drilldown.detailLoading = false;
      renderAnalyticsDrilldown();
    }
  }
}

function handleAnalyticsDrilldownListClick(event) {
  if (state.drilldown.mode !== 'ai') {
    return handleAnalyticsDrilldownListClickBase(event);
  }
  const button = event.target.closest('[data-analytics-row-id]');
  if (!button) {
    return;
  }
  maybeScrollAnalyticsDrilldownDetailIntoView();
  void loadAnalyticsDrilldownDetails(button.dataset.analyticsRowId || '');
}

function handleAiAnalyticsOverviewClick(event) {
  const button = event.target.closest('[data-ai-drilldown-slice]');
  if (!button) {
    return;
  }
  const slice = button.dataset.aiDrilldownSlice || 'all';
  openAnalyticsDrilldown('ai-overview', slice, {
    mode: 'ai',
    slice,
    sourceLabel: button.dataset.aiDrilldownLabel || aiAnalyticsSliceLabel(slice),
  });
}

function handleAiAnalyticsChannelClick(event) {
  const button = event.target.closest('[data-ai-channel]');
  if (!button) {
    return;
  }
  const channel = button.dataset.aiChannel || '';
  openAnalyticsDrilldown('ai-channel', channel, {
    mode: 'ai',
    slice: 'all',
    sourceLabel: `AI-канал: ${analyticsChannelLabel(channel)}`,
  });
}

function handleAiAnalyticsOutcomeClick(event) {
  const button = event.target.closest('[data-ai-outcome]');
  if (!button) {
    return;
  }
  const slice = button.dataset.aiOutcome || 'all';
  openAnalyticsDrilldown('ai-overview', slice, {
    mode: 'ai',
    slice,
    sourceLabel: button.dataset.aiOutcomeLabel || aiAnalyticsSliceLabel(slice),
  });
}

function handleAiAnalyticsReasonClick(event) {
  const button = event.target.closest('[data-ai-reason-key]');
  if (!button) {
    return;
  }
  const reasonKey = button.dataset.aiReasonKey || '';
  openAnalyticsDrilldown('ai-reason', reasonKey, {
    mode: 'ai',
    slice: 'handoff',
    reasonKey,
    sourceLabel: button.dataset.aiReasonLabel || `Причина передачи: ${reasonKey}`,
  });
}

function renderAiAnalyticsDrilldownFactBadges(item) {
  if (!item || typeof item !== 'object') {
    return '';
  }
  const badges = [];
  if (item.contained) {
    badges.push({ tone: 'sla', label: 'Закрыто AI' });
  }
  if (item.handoff) {
    badges.push({ tone: 'abandoned', label: 'Передано оператору' });
  }
  if (item.human_touched) {
    badges.push({ tone: 'handle', label: 'С участием оператора' });
  }
  if (item.closed_without_operator) {
    badges.push({ tone: 'fcr', label: 'Закрыто без оператора' });
  }
  if (item.ai_latency_avg_ms !== null && item.ai_latency_avg_ms !== undefined) {
    badges.push({ tone: 'wait', label: `Задержка ${formatAnalyticsNumber(item.ai_latency_avg_ms, 0)} мс` });
  }
  if (item.reason_label) {
    badges.push({ tone: 'handle', label: item.reason_label });
  }
  if (!badges.length) {
    return '';
  }
  return `
    <div class="analytics-drilldown-facts">
      ${badges
        .map(
          (badge) => `<span class="analytics-fact-badge analytics-fact-badge-${escapeHtml(badge.tone)}">${escapeHtml(badge.label)}</span>`,
        )
        .join('')}
    </div>
  `;
}

renderAnalyticsDrilldown = function renderAnalyticsDrilldown() {
  if (state.drilldown.mode !== 'ai') {
    return renderAnalyticsDrilldownBase();
  }
  const drawer = $('analyticsDrilldownDrawer');
  const backdrop = $('analyticsDrilldownBackdrop');
  const closeBtn = $('analyticsDrilldownCloseBtn');
  const title = $('analyticsDrilldownTitle');
  const meta = $('analyticsDrilldownMeta');
  const filters = $('analyticsDrilldownFilters');
  const list = $('analyticsDrilldownList');
  const detail = $('analyticsDrilldownDetail');
  const prevBtn = $('analyticsDrilldownPrevBtn');
  const nextBtn = $('analyticsDrilldownNextBtn');
  const pageMeta = $('analyticsDrilldownPageMeta');
  const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong');
  const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong');
  const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint');
  if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) {
    return;
  }

  const isOpen = state.drilldown.open;
  document.body.classList.toggle('analytics-drilldown-open', isOpen);
  drawer.classList.toggle('is-open', isOpen);
  backdrop.classList.toggle('is-open', isOpen);
  drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  closeBtn.classList.add('analytics-drawer-close');
  closeBtn.setAttribute('aria-label', 'Закрыть детализацию');
  closeBtn.textContent = 'Закрыть · Esc';
  drawer.hidden = !isOpen;
  backdrop.hidden = !isOpen;
  if (!isOpen) {
    title.textContent = 'Детализация AI-сессий';
    meta.textContent = 'Выберите карточку, исход, причину передачи или канал, чтобы открыть срез по AI-сессиям.';
    filters.innerHTML = '';
    list.innerHTML = '';
    detail.innerHTML = '';
    pageMeta.textContent = '0 из 0';
    prevBtn.disabled = true;
    nextBtn.disabled = true;
    if (listHeading) {
      listHeading.textContent = 'Список AI-сессий';
    }
    if (detailHeading) {
      detailHeading.textContent = 'Детали AI-сессии';
    }
    if (detailHint) {
      detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.';
    }
    syncAnalyticsDrilldownControls();
    return;
  }

  const chips = analyticsDrilldownChips();
  const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0;
  const currentPageEnd = state.drilldown.total
    ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total)
    : 0;
  title.textContent = state.drilldown.sourceLabel || 'Детализация AI-сессий';
  if (listHeading) {
    listHeading.textContent = 'Список AI-сессий';
  }
  if (detailHeading) {
    detailHeading.textContent = 'Детали AI-сессии';
  }
  if (detailHint) {
    detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.';
  }
  meta.textContent = state.drilldown.error
    ? `Не удалось обновить детализацию AI: ${state.drilldown.error}`
    : state.drilldown.exporting
      ? 'Готовим CSV по текущему AI-срезу.'
      : state.drilldown.loading
        ? 'Загружаем AI-сессии и ленту метаданных...'
        : `${formatAnalyticsNumber(state.drilldown.total, 0)} AI-сессий в выбранном срезе.`;
  filters.innerHTML = [
    ...chips.map((chip) => `<span class="analytics-filter-chip">${escapeHtml(chip)}</span>`),
    state.drilldown.notice
      ? `<span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(state.drilldown.notice)}</span>`
      : '',
  ].join('');
  pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`;
  prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0;
  nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total;
  syncAnalyticsDrilldownControls();

  if (state.drilldown.loading && !state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">Загружаем AI-сессии...</div>';
  } else if (!state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">По этому AI-срезу сессий пока нет.</div>';
  } else {
    list.innerHTML = state.drilldown.items
      .map((item) => `
        <button
          class="analytics-drilldown-row${item.session_id === state.drilldown.selectedInteractionId ? ' selected' : ''}"
          type="button"
          data-analytics-row-id="${escapeHtml(item.session_id)}"
        >
          <div class="analytics-drilldown-row-top">
            <strong>${escapeHtml(item.reason_label || item.session_id)}</strong>
            <span class="analytics-drilldown-status analytics-drilldown-status-${escapeHtml(analyticsDrilldownStatusClass(item.status))}">${escapeHtml(aiAnalyticsStatusLabel(item.status))}</span>
          </div>
          <div class="analytics-drilldown-row-meta">
            <span>${escapeHtml(item.session_id)}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsQueueName(item.queue_id))}</span>
          </div>
          ${renderAiAnalyticsDrilldownFactBadges(item)}
          <div class="analytics-drilldown-row-meta">
            <span>Обращение: ${escapeHtml(item.interaction_id || '—')}</span>
            <span>${escapeHtml(formatTime(item.created_at))}</span>
          </div>
        </button>
      `)
      .join('');
  }

  const selectedSession = state.drilldown.selectedInteraction
    || state.drilldown.items.find((item) => item.session_id === state.drilldown.selectedInteractionId)
    || null;
  if (!selectedSession) {
    detail.innerHTML = '<div class="empty-state">Выберите AI-сессию в списке, чтобы увидеть детали на уровне метаданных.</div>';
    return;
  }

  const timelineMarkup = state.drilldown.detailLoading
    ? '<div class="empty-state">Загружаем ленту метаданных...</div>'
    : state.drilldown.selectedTimeline.length
      ? state.drilldown.selectedTimeline
          .map((event) => {
            const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata);
            const metadataMarkup = Object.keys(safeMetadata).length
              ? `<pre>${escapeHtml(JSON.stringify(safeMetadata, null, 2))}</pre>`
               : '<p class="hint">Только событие уровня метаданных без текста сообщений.</p>';
            return `
              <div class="analytics-timeline-item">
                <div class="analytics-timeline-meta">
                  <strong>${escapeHtml(event.label || event.event_type || 'событие')}</strong>
                  <span>${escapeHtml(formatTime(event.ts))}</span>
                </div>
                ${metadataMarkup}
              </div>
            `;
          })
          .join('')
      : '<div class="empty-state">Metadata timeline по этой AI session пока пуст.</div>';

  const linkedInteractionMarkup = state.drilldown.selectedLinkedInteraction
    ? `
      <div class="analytics-drilldown-detail-card">
        <div class="analytics-drilldown-detail-head">
          <div>
            <strong>Связанное обращение</strong>
            <p class="hint">${escapeHtml(state.drilldown.selectedLinkedInteraction.interaction_id || '—')}</p>
          </div>
        </div>
        <div class="analytics-drilldown-detail-grid">
          <div><small>Канал</small><strong>${escapeHtml(analyticsChannelLabel(state.drilldown.selectedLinkedInteraction.channel))}</strong></div>
          <div><small>Очередь</small><strong>${escapeHtml(analyticsQueueName(state.drilldown.selectedLinkedInteraction.queue_id))}</strong></div>
          <div><small>Статус</small><strong>${escapeHtml(interactionStatusLabel(state.drilldown.selectedLinkedInteraction.status))}</strong></div>
          <div><small>Назначен</small><strong>${escapeHtml(state.drilldown.selectedLinkedInteraction.assigned_to || 'Не назначен')}</strong></div>
          <div><small>Тема</small><strong>${escapeHtml(state.drilldown.selectedLinkedInteraction.subject || '—')}</strong></div>
          <div><small>Обновлено</small><strong>${escapeHtml(formatTime(state.drilldown.selectedLinkedInteraction.updated_at))}</strong></div>
        </div>
      </div>
    `
    : '';

  detail.innerHTML = `
    <div class="analytics-drilldown-detail-card">
      <div class="analytics-drilldown-detail-head">
        <div>
          <strong>${escapeHtml(selectedSession.session_id)}</strong>
          <p class="hint">${escapeHtml(aiAnalyticsSliceLabel(state.drilldown.filters?.slice || 'all'))}</p>
        </div>
        <div class="analytics-card-top-meta">
          <span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(aiAnalyticsStatusLabel(selectedSession.status))}</span>
        </div>
      </div>
      <div class="analytics-drilldown-metric-note">
        <p class="hint">${escapeHtml(aiAnalyticsDrilldownNote(state.drilldown.filters))}</p>
      </div>
      ${renderAiAnalyticsDrilldownFactBadges(selectedSession)}
      <div class="analytics-drilldown-detail-grid">
        <div><small>Канал</small><strong>${escapeHtml(analyticsChannelLabel(selectedSession.channel))}</strong></div>
        <div><small>Очередь</small><strong>${escapeHtml(analyticsQueueName(selectedSession.queue_id))}</strong></div>
        <div><small>ID обращения</small><strong>${escapeHtml(selectedSession.interaction_id || '—')}</strong></div>
        <div><small>ID диалога</small><strong>${escapeHtml(selectedSession.thread_id || '—')}</strong></div>
        <div><small>Назначено</small><strong>${escapeHtml(selectedSession.assigned_to || 'Не назначен')}</strong></div>
        <div><small>Взял оператор</small><strong>${escapeHtml(selectedSession.claimed_by_user || '—')}</strong></div>
        <div><small>Ответов AI</small><strong>${escapeHtml(String(selectedSession.assistant_turns || 0))}</strong></div>
        <div><small>Сообщений клиента</small><strong>${escapeHtml(String(selectedSession.user_turns || 0))}</strong></div>
        <div><small>Вызовов инструментов</small><strong>${escapeHtml(String(selectedSession.tool_turns || 0))}</strong></div>
        <div><small>Средняя задержка</small><strong>${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_avg_ms))}</strong></div>
        <div><small>P95 задержки</small><strong>${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_p95_ms))}</strong></div>
        <div><small>Причина</small><strong>${escapeHtml(selectedSession.reason_label || selectedSession.raw_handoff_reason || '—')}</strong></div>
        <div><small>Исходная причина</small><strong>${escapeHtml(selectedSession.raw_handoff_reason || '—')}</strong></div>
        <div><small>Создано</small><strong>${escapeHtml(formatTime(selectedSession.created_at))}</strong></div>
        <div><small>Обновлено</small><strong>${escapeHtml(formatTime(selectedSession.updated_at))}</strong></div>
        <div><small>Закрыто</small><strong>${escapeHtml(formatTime(selectedSession.closed_at))}</strong></div>
      </div>
    </div>
    ${linkedInteractionMarkup}
    <div class="analytics-drilldown-timeline">
      ${timelineMarkup}
    </div>
  `;
}

async function exportAnalyticsDrilldownCsv() {
  if (state.drilldown.mode !== 'ai') {
    return exportAnalyticsDrilldownCsvBase();
  }
  if (!state.drilldown.open || !state.drilldown.filters || !state.drilldown.total || state.drilldown.exporting) {
    return;
  }
  state.drilldown.exporting = true;
  state.drilldown.notice = '';
  renderAnalyticsDrilldown();

  try {
    const lines = [[
      'session_id',
      'interaction_id',
      'thread_id',
      'channel',
      'queue_id',
      'status',
      'reason_key',
      'reason_label',
      'raw_handoff_reason',
      'assigned_to',
      'claimed_by_user',
      'assistant_turns',
      'user_turns',
      'tool_turns',
      'ai_latency_avg_ms',
      'ai_latency_p95_ms',
      'created_at',
      'updated_at',
      'closed_at',
    ].join(',')];
    const maxRows = Math.min(state.drilldown.total, DRILLDOWN_EXPORT_SOFT_CAP);
    let currentOffset = 0;
    while (currentOffset < maxRows) {
      const batchLimit = Math.min(DRILLDOWN_EXPORT_BATCH_SIZE, maxRows - currentOffset);
      const payload = await fetchAiAnalyticsDrilldown(state.drilldown.filters, batchLimit, currentOffset);
      const items = Array.isArray(payload?.items) ? payload.items : [];
      items.forEach((item) => {
        lines.push([
          item.session_id,
          item.interaction_id,
          item.thread_id,
          item.channel,
          item.queue_id,
          item.status,
          item.reason_key,
          item.reason_label,
          item.raw_handoff_reason,
          item.assigned_to,
          item.claimed_by_user,
          item.assistant_turns,
          item.user_turns,
          item.tool_turns,
          item.ai_latency_avg_ms,
          item.ai_latency_p95_ms,
          item.created_at,
          item.updated_at,
          item.closed_at,
        ].map(csvCell).join(','));
      });
      if (!items.length) {
        break;
      }
      currentOffset += batchLimit;
    }

    const dateStamp = new Date().toISOString().slice(0, 10);
    downloadCsv(
      lines.join('\n'),
      `konturcc-drilldown-${analyticsDrilldownFilenameSource()}-${dateStamp}.csv`,
    );
    state.drilldown.notice = state.drilldown.total > DRILLDOWN_EXPORT_SOFT_CAP
      ? `Экспортированы первые ${formatAnalyticsNumber(DRILLDOWN_EXPORT_SOFT_CAP, 0)} строк.`
      : 'CSV готов.';
  } catch (err) {
    state.drilldown.error = err.message;
  } finally {
    state.drilldown.exporting = false;
    renderAnalyticsDrilldown();
  }
}

function renderAiAnalyticsDrilldownActive() {
  const drawer = $('analyticsDrilldownDrawer');
  const backdrop = $('analyticsDrilldownBackdrop');
  const closeBtn = $('analyticsDrilldownCloseBtn');
  const title = $('analyticsDrilldownTitle');
  const meta = $('analyticsDrilldownMeta');
  const filters = $('analyticsDrilldownFilters');
  const list = $('analyticsDrilldownList');
  const detail = $('analyticsDrilldownDetail');
  const prevBtn = $('analyticsDrilldownPrevBtn');
  const nextBtn = $('analyticsDrilldownNextBtn');
  const pageMeta = $('analyticsDrilldownPageMeta');
  const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong');
  const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong');
  const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint');
  if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) {
    return;
  }

  const isOpen = state.drilldown.open;
  document.body.classList.toggle('analytics-drilldown-open', isOpen);
  drawer.classList.toggle('is-open', isOpen);
  backdrop.classList.toggle('is-open', isOpen);
  drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  closeBtn.classList.add('analytics-drawer-close');
  closeBtn.setAttribute('aria-label', 'Закрыть детализацию');
  closeBtn.textContent = 'Закрыть · Esc';
  drawer.hidden = !isOpen;
  backdrop.hidden = !isOpen;
  if (!isOpen) {
    title.textContent = 'Детализация AI-сессий';
    meta.textContent = 'Выберите карточку, исход, причину передачи или канал, чтобы открыть срез по AI-сессиям.';
    filters.innerHTML = '';
    list.innerHTML = '';
    detail.innerHTML = '';
    pageMeta.textContent = '0 из 0';
    prevBtn.disabled = true;
    nextBtn.disabled = true;
    if (listHeading) {
      listHeading.textContent = 'Список AI-сессий';
    }
    if (detailHeading) {
      detailHeading.textContent = 'Детали AI-сессии';
    }
    if (detailHint) {
      detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.';
    }
    syncAnalyticsDrilldownControls();
    return;
  }

  const chips = analyticsDrilldownChips();
  const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0;
  const currentPageEnd = state.drilldown.total
    ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total)
    : 0;
  title.textContent = state.drilldown.sourceLabel || 'Детализация AI-сессий';
  if (listHeading) {
    listHeading.textContent = 'Список AI-сессий';
  }
  if (detailHeading) {
    detailHeading.textContent = 'Детали AI-сессии';
  }
  if (detailHint) {
    detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.';
  }
  meta.textContent = state.drilldown.error
    ? `Не удалось обновить детализацию AI: ${state.drilldown.error}`
    : state.drilldown.exporting
      ? 'Готовим CSV по текущему AI-срезу.'
      : state.drilldown.loading
        ? 'Загружаем AI-сессии и ленту метаданных...'
        : `${formatAnalyticsNumber(state.drilldown.total, 0)} AI-сессий в выбранном срезе.`;
  filters.innerHTML = [
    ...chips.map((chip) => `<span class="analytics-filter-chip">${escapeHtml(chip)}</span>`),
    state.drilldown.notice
      ? `<span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(state.drilldown.notice)}</span>`
      : '',
  ].join('');
  pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`;
  prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0;
  nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total;
  syncAnalyticsDrilldownControls();

  if (state.drilldown.loading && !state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">Загружаем AI-сессии...</div>';
  } else if (!state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">По этому AI-срезу сессий пока нет.</div>';
  } else {
    list.innerHTML = state.drilldown.items
      .map((item) => `
        <button
          class="analytics-drilldown-row${item.session_id === state.drilldown.selectedInteractionId ? ' selected' : ''}"
          type="button"
          data-analytics-row-id="${escapeHtml(item.session_id)}"
        >
          <div class="analytics-drilldown-row-top">
            <strong>${escapeHtml(item.reason_label || item.session_id)}</strong>
            <span class="analytics-drilldown-status analytics-drilldown-status-${escapeHtml(analyticsDrilldownStatusClass(item.status))}">${escapeHtml(aiAnalyticsStatusLabel(item.status))}</span>
          </div>
          <div class="analytics-drilldown-row-meta">
            <span>${escapeHtml(item.session_id)}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsQueueName(item.queue_id))}</span>
          </div>
          ${renderAiAnalyticsDrilldownFactBadges(item)}
          <div class="analytics-drilldown-row-meta">
            <span>Обращение: ${escapeHtml(item.interaction_id || '—')}</span>
            <span>${escapeHtml(formatTime(item.created_at))}</span>
          </div>
        </button>
      `)
      .join('');
  }

  const selectedSession = state.drilldown.selectedInteraction
    || state.drilldown.items.find((item) => item.session_id === state.drilldown.selectedInteractionId)
    || null;
  if (!selectedSession) {
    detail.innerHTML = '<div class="empty-state">Выберите AI-сессию в списке, чтобы увидеть детали на уровне метаданных.</div>';
    return;
  }

  const timelineMarkup = state.drilldown.detailLoading
    ? '<div class="empty-state">Загружаем ленту метаданных...</div>'
    : state.drilldown.selectedTimeline.length
      ? state.drilldown.selectedTimeline
          .map((event) => {
            const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata);
            const metadataMarkup = Object.keys(safeMetadata).length
              ? `<pre>${escapeHtml(JSON.stringify(safeMetadata, null, 2))}</pre>`
              : '<p class="hint">Только событие уровня метаданных без текста сообщений.</p>';
            return `
              <div class="analytics-timeline-item">
                <div class="analytics-timeline-meta">
                  <strong>${escapeHtml(event.label || event.event_type || 'событие')}</strong>
                  <span>${escapeHtml(formatTime(event.ts))}</span>
                </div>
                ${metadataMarkup}
              </div>
            `;
          })
          .join('')
      : '<div class="empty-state">Лента метаданных по этой AI-сессии пока пуста.</div>';

  const linkedInteractionMarkup = state.drilldown.selectedLinkedInteraction
    ? `
      <div class="analytics-drilldown-detail-card">
        <div class="analytics-drilldown-detail-head">
          <div>
            <strong>Связанное обращение</strong>
            <p class="hint">${escapeHtml(state.drilldown.selectedLinkedInteraction.interaction_id || '—')}</p>
          </div>
        </div>
        <div class="analytics-drilldown-detail-grid">
          <div><small>Канал</small><strong>${escapeHtml(analyticsChannelLabel(state.drilldown.selectedLinkedInteraction.channel))}</strong></div>
          <div><small>Очередь</small><strong>${escapeHtml(analyticsQueueName(state.drilldown.selectedLinkedInteraction.queue_id))}</strong></div>
          <div><small>Статус</small><strong>${escapeHtml(interactionStatusLabel(state.drilldown.selectedLinkedInteraction.status))}</strong></div>
          <div><small>Назначен</small><strong>${escapeHtml(state.drilldown.selectedLinkedInteraction.assigned_to || 'Не назначен')}</strong></div>
          <div><small>Тема</small><strong>${escapeHtml(state.drilldown.selectedLinkedInteraction.subject || '—')}</strong></div>
          <div><small>Обновлено</small><strong>${escapeHtml(formatTime(state.drilldown.selectedLinkedInteraction.updated_at))}</strong></div>
        </div>
      </div>
    `
    : '';

  detail.innerHTML = `
    <div class="analytics-drilldown-detail-card">
      <div class="analytics-drilldown-detail-head">
        <div>
          <strong>${escapeHtml(selectedSession.session_id)}</strong>
          <p class="hint">${escapeHtml(aiAnalyticsSliceLabel(state.drilldown.filters?.slice || 'all'))}</p>
        </div>
        <div class="analytics-card-top-meta">
          <span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(aiAnalyticsStatusLabel(selectedSession.status))}</span>
        </div>
      </div>
      <div class="analytics-drilldown-metric-note">
        <p class="hint">${escapeHtml(aiAnalyticsDrilldownNote(state.drilldown.filters))}</p>
      </div>
      ${renderAiAnalyticsDrilldownFactBadges(selectedSession)}
      <div class="analytics-drilldown-detail-grid">
        <div><small>Канал</small><strong>${escapeHtml(analyticsChannelLabel(selectedSession.channel))}</strong></div>
        <div><small>Очередь</small><strong>${escapeHtml(analyticsQueueName(selectedSession.queue_id))}</strong></div>
        <div><small>ID обращения</small><strong>${escapeHtml(selectedSession.interaction_id || '—')}</strong></div>
        <div><small>ID диалога</small><strong>${escapeHtml(selectedSession.thread_id || '—')}</strong></div>
        <div><small>Назначено</small><strong>${escapeHtml(selectedSession.assigned_to || 'Не назначен')}</strong></div>
        <div><small>Взял оператор</small><strong>${escapeHtml(selectedSession.claimed_by_user || '—')}</strong></div>
        <div><small>Ответов AI</small><strong>${escapeHtml(String(selectedSession.assistant_turns || 0))}</strong></div>
        <div><small>Сообщений клиента</small><strong>${escapeHtml(String(selectedSession.user_turns || 0))}</strong></div>
        <div><small>Вызовов инструментов</small><strong>${escapeHtml(String(selectedSession.tool_turns || 0))}</strong></div>
        <div><small>Средняя задержка</small><strong>${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_avg_ms))}</strong></div>
        <div><small>P95 задержки</small><strong>${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_p95_ms))}</strong></div>
        <div><small>Причина</small><strong>${escapeHtml(selectedSession.reason_label || selectedSession.raw_handoff_reason || '—')}</strong></div>
        <div><small>Исходная причина</small><strong>${escapeHtml(selectedSession.raw_handoff_reason || '—')}</strong></div>
        <div><small>Создано</small><strong>${escapeHtml(formatTime(selectedSession.created_at))}</strong></div>
        <div><small>Обновлено</small><strong>${escapeHtml(formatTime(selectedSession.updated_at))}</strong></div>
        <div><small>Закрыто</small><strong>${escapeHtml(formatTime(selectedSession.closed_at))}</strong></div>
      </div>
    </div>
    ${linkedInteractionMarkup}
    <div class="analytics-drilldown-timeline">
      ${timelineMarkup}
    </div>
  `;
}

function renderAnalyticsDrilldown() {
  if (state.drilldown.mode === 'ai') {
    renderAiAnalyticsDrilldownActive();
    return;
  }
  const drawer = $('analyticsDrilldownDrawer');
  const backdrop = $('analyticsDrilldownBackdrop');
  const closeBtn = $('analyticsDrilldownCloseBtn');
  const title = $('analyticsDrilldownTitle');
  const meta = $('analyticsDrilldownMeta');
  const filters = $('analyticsDrilldownFilters');
  const list = $('analyticsDrilldownList');
  const detail = $('analyticsDrilldownDetail');
  const prevBtn = $('analyticsDrilldownPrevBtn');
  const nextBtn = $('analyticsDrilldownNextBtn');
  const pageMeta = $('analyticsDrilldownPageMeta');
  const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong');
  const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong');
  const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint');
  if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) {
    return;
  }

  const isOpen = state.drilldown.open;
  document.body.classList.toggle('analytics-drilldown-open', isOpen);
  drawer.classList.toggle('is-open', isOpen);
  backdrop.classList.toggle('is-open', isOpen);
  drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
  closeBtn.classList.add('analytics-drawer-close');
  closeBtn.setAttribute('aria-label', 'Закрыть детализацию');
  closeBtn.textContent = 'Закрыть · Esc';
  drawer.hidden = !isOpen;
  backdrop.hidden = !isOpen;
  if (!isOpen) {
    title.textContent = 'Детализация обращений';
    meta.textContent = 'Выберите карточку обращений, канал или очередь на витрине, чтобы открыть список обращений.';
    filters.innerHTML = '';
    list.innerHTML = '';
    detail.innerHTML = '';
    pageMeta.textContent = '0 из 0';
    prevBtn.disabled = true;
    nextBtn.disabled = true;
    title.textContent = 'Точная детализация';
    meta.textContent = 'Выберите KPI-карточку, канал или очередь на витрине, чтобы открыть точный срез по обращениям.';
    pageMeta.textContent = '0 из 0';
    if (listHeading) {
      listHeading.textContent = 'Список обращений';
    }
    if (detailHeading) {
      detailHeading.textContent = 'Карточка обращения';
    }
    if (detailHint) {
      detailHint.textContent = 'Лента событий и базовые поля без записей, транскриптов и канального контента.';
    }
    syncAnalyticsDrilldownControls();
    return;
  }

  const chips = analyticsDrilldownChips();
  const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0;
  const currentPageEnd = state.drilldown.total
    ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total)
    : 0;
    title.textContent = state.drilldown.sourceLabel || 'Точная детализация';
  if (listHeading) {
    listHeading.textContent = state.drilldown.mode === 'metric' ? 'Точный KPI-срез' : 'Список обращений';
  }
  if (detailHeading) {
    detailHeading.textContent = state.drilldown.mode === 'metric' ? 'Карточка и KPI-факты' : 'Карточка обращения';
  }
  if (detailHint) {
    detailHint.textContent = state.drilldown.mode === 'metric'
      ? 'KPI-факты, лента событий и базовые поля без исходных сообщений, записей, транскриптов и AI-сводок.'
      : 'Лента событий и базовые поля без записей, транскриптов и канального контента.';
  }
  title.textContent = state.drilldown.sourceLabel || 'Детализация обращений';
  meta.textContent = state.drilldown.error
    ? `Не удалось полностью обновить детализацию: ${state.drilldown.error}`
    : state.drilldown.exporting
      ? 'Готовим CSV по текущему срезу. Детализация и фильтры остаются доступны только на уровне метаданных.'
      : state.drilldown.loading
        ? 'Загружаем список обращений и ленту событий...'
        : `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в выбранном срезе.`;
  meta.textContent = state.drilldown.error
    ? `Не удалось обновить детализацию: ${state.drilldown.error}`
    : state.drilldown.exporting
      ? 'Готовим CSV по текущему срезу. Детализация остаётся только на уровне метаданных.'
      : state.drilldown.loading
        ? state.drilldown.mode === 'metric'
          ? 'Загружаем точный KPI-срез и ленту событий...'
          : 'Загружаем список обращений и ленту событий...'
        : state.drilldown.mode === 'metric'
          ? `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в точном срезе по метрике. ${state.drilldown.metricNote || ''}`.trim()
          : `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в выбранном срезе.`;
  title.textContent = state.drilldown.sourceLabel || 'Точная детализация';
  filters.innerHTML = [
    ...chips.map((chip) => `<span class="analytics-filter-chip">${escapeHtml(chip)}</span>`),
    state.drilldown.notice
      ? `<span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(state.drilldown.notice)}</span>`
      : '',
  ].join('');
  pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`;
  prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0;
  nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total;
  syncAnalyticsDrilldownControls();

  if (state.drilldown.loading && !state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">Загружаем список обращений...</div>';
  } else if (!state.drilldown.items.length) {
    list.innerHTML = '<div class="empty-state">По этому срезу обращений пока нет.</div>';
  } else {
    list.innerHTML = state.drilldown.items
      .map((item) => `
        <button
          class="analytics-drilldown-row${item.interaction_id === state.drilldown.selectedInteractionId ? ' selected' : ''}"
          type="button"
          data-analytics-interaction-id="${escapeHtml(item.interaction_id)}"
        >
          <div class="analytics-drilldown-row-top">
            <strong>${escapeHtml(item.subject || item.interaction_id)}</strong>
            <span class="analytics-drilldown-status analytics-drilldown-status-${escapeHtml(analyticsDrilldownStatusClass(item.status))}">${escapeHtml(interactionStatusLabel(item.status))}</span>
          </div>
          <div class="analytics-drilldown-row-meta">
            <span>${escapeHtml(item.interaction_id)}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span class="analytics-drilldown-pill">${escapeHtml(analyticsQueueName(item.queue_id))}</span>
          </div>
          ${renderAnalyticsDrilldownFactBadges(item)}
          <div class="analytics-drilldown-row-meta">
            <span>Назначен: ${escapeHtml(item.assigned_to || 'Не назначен')}</span>
            <span>${escapeHtml(formatTime(item.created_at))}</span>
          </div>
        </button>
      `)
      .join('');
  }

  const selectedInteraction = state.drilldown.selectedInteraction
    || state.drilldown.items.find((item) => item.interaction_id === state.drilldown.selectedInteractionId)
    || null;
  if (!selectedInteraction) {
    detail.innerHTML = '<div class="empty-state">Выберите обращение в списке, чтобы увидеть карточку и timeline.</div>';
    return;
  }

  const timelineMarkup = state.drilldown.detailLoading
    ? '<div class="empty-state">Загружаем ленту событий...</div>'
    : state.drilldown.selectedTimeline.length
      ? state.drilldown.selectedTimeline
          .map((event) => `
            ${(() => {
              const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata);
              const metadataMarkup = Object.keys(safeMetadata).length
                ? `<pre>${escapeHtml(JSON.stringify(safeMetadata, null, 2))}</pre>`
                 : '<p class="hint">Только событие уровня метаданных: исходный текст, записи и транскрипты скрыты в analyst-экране.</p>';
              return `
            <div class="analytics-timeline-item">
              <div class="analytics-timeline-meta">
                <strong>${escapeHtml(event.action || 'событие')}</strong>
                <span>${escapeHtml(formatTime(event.timestamp))}</span>
              </div>
              ${metadataMarkup}
            </div>
          `;
            })()}
          `)
          .join('')
      : '<div class="empty-state">Лента событий по этому обращению пока пуста.</div>';

  const metricContextMarkup = state.drilldown.mode === 'metric' && state.drilldown.metric
    ? `
      <div class="analytics-drilldown-metric-note">
        <p class="hint">${escapeHtml(state.drilldown.metricNote || '')}</p>
      </div>
    `
    : '';
  const factBadgesMarkup = renderAnalyticsDrilldownFactBadges(selectedInteraction);
  const factFields = [
    selectedInteraction.wait_seconds !== null && selectedInteraction.wait_seconds !== undefined
      ? `<div><small>Ожидание</small><strong>${escapeHtml(`${formatAnalyticsNumber(selectedInteraction.wait_seconds, 0)} с`)}</strong></div>`
      : '',
    selectedInteraction.handle_seconds !== null && selectedInteraction.handle_seconds !== undefined
      ? `<div><small>Обработка</small><strong>${escapeHtml(`${formatAnalyticsNumber(selectedInteraction.handle_seconds, 0)} с`)}</strong></div>`
      : '',
    selectedInteraction.within_sla !== null && selectedInteraction.within_sla !== undefined
      ? `<div><small>SLA</small><strong>${escapeHtml(selectedInteraction.within_sla ? 'В SLA' : 'Вне SLA')}</strong></div>`
      : '',
    selectedInteraction.resolved_first_contact !== null && selectedInteraction.resolved_first_contact !== undefined
      ? `<div><small>FCR</small><strong>${escapeHtml(selectedInteraction.resolved_first_contact ? 'Да' : 'Нет')}</strong></div>`
      : '',
    selectedInteraction.abandoned !== null && selectedInteraction.abandoned !== undefined
      ? `<div><small>Потеря</small><strong>${escapeHtml(selectedInteraction.abandoned ? 'Да' : 'Нет')}</strong></div>`
      : '',
  ].filter(Boolean).join('');

  detail.innerHTML = `
    <div class="analytics-drilldown-detail-card">
      <div class="analytics-drilldown-detail-head">
        <div>
          <strong>${escapeHtml(selectedInteraction.subject || selectedInteraction.interaction_id)}</strong>
          <p class="hint">${escapeHtml(selectedInteraction.interaction_id)}</p>
        </div>
        <div class="analytics-card-top-meta">
          ${state.drilldown.mode === 'metric' && state.drilldown.metric ? analyticsMetricCoverageBadge(state.drilldown.metric) : ''}
          <span class="analytics-filter-chip analytics-filter-chip-quiet">${escapeHtml(interactionStatusLabel(selectedInteraction.status))}</span>
        </div>
      </div>
      ${metricContextMarkup}
      ${factBadgesMarkup}
      <div class="analytics-drilldown-detail-grid">
        <div><small>Канал</small><strong>${escapeHtml(analyticsChannelLabel(selectedInteraction.channel))}</strong></div>
        <div><small>Очередь</small><strong>${escapeHtml(analyticsQueueName(selectedInteraction.queue_id))}</strong></div>
        <div><small>Назначен</small><strong>${escapeHtml(selectedInteraction.assigned_to || 'Не назначен')}</strong></div>
        <div><small>Создано</small><strong>${escapeHtml(formatTime(selectedInteraction.created_at))}</strong></div>
        <div><small>Обновлено</small><strong>${escapeHtml(formatTime(selectedInteraction.updated_at))}</strong></div>
        <div><small>Срез</small><strong>${escapeHtml(state.drilldown.sourceLabel || 'Все обращения')}</strong></div>
        ${factFields}
      </div>
    </div>
    <div class="analytics-drilldown-timeline">
      ${timelineMarkup}
    </div>
  `;
}

async function loadAiAnalyticsDrilldownDetailsState(sessionId) {
  if (!state.drilldown.open || !sessionId) {
    return;
  }
  state.drilldown.selectedInteractionId = sessionId;
  state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.session_id === sessionId) || null;
  state.drilldown.selectedLinkedInteraction = null;
  state.drilldown.selectedTimeline = [];
  state.drilldown.error = '';
  state.drilldown.detailLoading = true;
  renderAnalyticsDrilldown();
  syncAnalyticsUrlState();

  if (analyticsMockEnabled()) {
    await ensureAnalyticsMockDataLoaded();
    const detail = buildMockAiDetail(sessionId);
    state.drilldown.selectedInteraction = {
      ...(state.drilldown.selectedInteraction || {}),
      ...(detail?.session || {}),
    };
    state.drilldown.selectedLinkedInteraction = detail?.interaction || null;
    state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : [];
    state.drilldown.detailLoading = false;
    renderAnalyticsDrilldown();
    syncAnalyticsUrlState();
    return;
  }

  try {
    const detail = await fetchAiAnalyticsSessionDetail(sessionId);
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== sessionId) {
      return;
    }
    state.drilldown.selectedInteraction = {
      ...(state.drilldown.selectedInteraction || {}),
      ...(detail?.session || {}),
    };
    state.drilldown.selectedLinkedInteraction = detail?.interaction || null;
    state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : [];
  } catch (err) {
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== sessionId) {
      return;
    }
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open && state.drilldown.selectedInteractionId === sessionId) {
      state.drilldown.detailLoading = false;
      renderAnalyticsDrilldown();
      syncAnalyticsUrlState();
    }
  }
}

loadAnalyticsDrilldownDetails = async function loadAnalyticsDrilldownDetails(interactionId) {
  if (state.drilldown.mode === 'ai') {
    return loadAiAnalyticsDrilldownDetailsState(interactionId);
  }
  if (!state.drilldown.open || !interactionId) {
    return;
  }
  state.drilldown.selectedInteractionId = interactionId;
  state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.interaction_id === interactionId) || null;
  state.drilldown.selectedTimeline = [];
  state.drilldown.error = '';
  state.drilldown.detailLoading = true;
  renderAnalyticsDrilldown();
  syncAnalyticsUrlState();

  if (analyticsMockEnabled()) {
    await ensureAnalyticsMockDataLoaded();
    const detail = buildMockInteractionDetail(interactionId);
    state.drilldown.selectedInteraction = {
      ...(state.drilldown.selectedInteraction || {}),
      ...(detail?.detail || {}),
    };
    state.drilldown.selectedTimeline = Array.isArray(detail?.timeline?.events) ? detail.timeline.events : [];
    state.drilldown.detailLoading = false;
    renderAnalyticsDrilldown();
    syncAnalyticsUrlState();
    return;
  }

  try {
    const [detail, timeline] = await Promise.all([
      api('interaction', `interactions/${encodeURIComponent(interactionId)}`),
      api('interaction', `interactions/${encodeURIComponent(interactionId)}/timeline`),
    ]);
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) {
      return;
    }
    state.drilldown.selectedInteraction = {
      ...(state.drilldown.selectedInteraction || {}),
      ...(detail || {}),
    };
    state.drilldown.selectedTimeline = Array.isArray(timeline?.events) ? timeline.events : [];
  } catch (err) {
    if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) {
      return;
    }
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open && state.drilldown.selectedInteractionId === interactionId) {
      state.drilldown.detailLoading = false;
      renderAnalyticsDrilldown();
      syncAnalyticsUrlState();
    }
  }
}

async function loadAiAnalyticsDrilldownPageState(options = {}) {
  if (!state.drilldown.open || !state.drilldown.filters) {
    return;
  }
  const preserveSelection = Boolean(options.preserveSelection);
  state.drilldown.loading = true;
  state.drilldown.error = '';
  renderAnalyticsDrilldown();

  if (analyticsMockEnabled()) {
    await ensureAnalyticsMockDataLoaded();
    const data = buildMockAiDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset);
    state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
    state.drilldown.total = Number(data?.total || 0);
    state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE);
    state.drilldown.offset = Number(data?.offset || 0);
    state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters);
    state.drilldown.coverage = data?.coverage || state.drilldown.coverage;
    state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters);
    const selectedId = state.drilldown.selectedInteractionId || '';
    const selectedStillVisible = preserveSelection
      && selectedId
      && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId);
    if (selectedStillVisible) {
      state.drilldown.selectedInteraction = state.drilldown.items.find((item) => analyticsDrilldownCurrentItemId(item) === selectedId) || state.drilldown.selectedInteraction;
      void loadAnalyticsDrilldownDetails(selectedId);
    } else if (state.drilldown.items.length) {
      state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]);
      state.drilldown.selectedInteraction = state.drilldown.items[0];
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else {
      state.drilldown.selectedInteractionId = '';
      state.drilldown.selectedInteraction = null;
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      state.drilldown.detailLoading = false;
    }
    state.drilldown.loading = false;
    renderAnalyticsDrilldown();
    syncAnalyticsUrlState();
    return;
  }

  try {
    const data = await fetchAiAnalyticsDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset);
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
    state.drilldown.total = Number(data?.total || 0);
    state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE);
    state.drilldown.offset = Number(data?.offset || 0);
    state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters);
    state.drilldown.coverage = data?.coverage || state.drilldown.coverage;
    state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters);

    const selectedId = state.drilldown.selectedInteractionId || '';
    const selectedStillVisible = preserveSelection
      && selectedId
      && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId);
    if (selectedStillVisible) {
      state.drilldown.selectedInteraction = state.drilldown.items.find(
        (item) => analyticsDrilldownCurrentItemId(item) === selectedId,
      ) || state.drilldown.selectedInteraction;
      void loadAnalyticsDrilldownDetails(selectedId);
    } else if (state.drilldown.items.length) {
      state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]);
      state.drilldown.selectedInteraction = state.drilldown.items[0];
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else {
      state.drilldown.selectedInteractionId = '';
      state.drilldown.selectedInteraction = null;
      state.drilldown.selectedLinkedInteraction = null;
      state.drilldown.selectedTimeline = [];
      state.drilldown.detailLoading = false;
    }
  } catch (err) {
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = [];
    state.drilldown.total = 0;
    state.drilldown.selectedInteractionId = '';
    state.drilldown.selectedInteraction = null;
    state.drilldown.selectedLinkedInteraction = null;
    state.drilldown.selectedTimeline = [];
    state.drilldown.detailLoading = false;
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open) {
      state.drilldown.loading = false;
      renderAnalyticsDrilldown();
      syncAnalyticsUrlState();
    }
  }
}

loadAnalyticsDrilldownPage = async function loadAnalyticsDrilldownPage(options = {}) {
  if (state.drilldown.mode === 'ai') {
    return loadAiAnalyticsDrilldownPageState(options);
  }
  if (!state.drilldown.open || !state.drilldown.filters) {
    return;
  }
  const preserveSelection = Boolean(options.preserveSelection);
  state.drilldown.loading = true;
  state.drilldown.error = '';
  renderAnalyticsDrilldown();

  if (analyticsMockEnabled()) {
    await ensureAnalyticsMockDataLoaded();
    const data = buildMockInteractionDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset, state.drilldown.mode);
    state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
    state.drilldown.total = Number(data?.total || 0);
    state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE);
    state.drilldown.offset = Number(data?.offset || 0);
    state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters);
    state.drilldown.metric = data?.metric || state.drilldown.metric;
    state.drilldown.coverage = data?.coverage || state.drilldown.coverage;
    if (state.drilldown.mode === 'metric' && state.drilldown.metric) {
      state.drilldown.metricNote = analyticsMetricDrilldownNote(state.drilldown.metric);
    }
    const selectedStillVisible = preserveSelection
      && state.drilldown.selectedInteractionId
      && state.drilldown.items.some((item) => item.interaction_id === state.drilldown.selectedInteractionId);
    if (selectedStillVisible) {
      state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.interaction_id === state.drilldown.selectedInteractionId) || state.drilldown.selectedInteraction;
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else if (state.drilldown.items.length) {
      state.drilldown.selectedInteractionId = state.drilldown.items[0].interaction_id;
      state.drilldown.selectedInteraction = state.drilldown.items[0];
      state.drilldown.selectedTimeline = [];
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else {
      state.drilldown.selectedInteractionId = '';
      state.drilldown.selectedInteraction = null;
      state.drilldown.selectedTimeline = [];
      state.drilldown.detailLoading = false;
    }
    state.drilldown.loading = false;
    renderAnalyticsDrilldown();
    syncAnalyticsUrlState();
    return;
  }

  try {
    const data = state.drilldown.mode === 'metric'
      ? await fetchAnalyticsMetricDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset)
      : await api(
        'interaction',
        `interactions/drilldown?${analyticsDrilldownQuery(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset)}`,
      );
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = Array.isArray(data?.items) ? data.items : [];
    state.drilldown.total = Number(data?.total || 0);
    state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE);
    state.drilldown.offset = Number(data?.offset || 0);
    state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters);
    state.drilldown.metric = data?.metric || state.drilldown.metric;
    state.drilldown.coverage = data?.coverage || state.drilldown.coverage;
    if (state.drilldown.mode === 'metric' && state.drilldown.metric) {
      state.drilldown.metricNote = analyticsMetricDrilldownNote(state.drilldown.metric);
    }

    const selectedStillVisible = preserveSelection
      && state.drilldown.selectedInteractionId
      && state.drilldown.items.some((item) => item.interaction_id === state.drilldown.selectedInteractionId);
    if (selectedStillVisible) {
      state.drilldown.selectedInteraction = state.drilldown.items.find(
        (item) => item.interaction_id === state.drilldown.selectedInteractionId,
      ) || state.drilldown.selectedInteraction;
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else if (state.drilldown.items.length) {
      state.drilldown.selectedInteractionId = state.drilldown.items[0].interaction_id;
      state.drilldown.selectedInteraction = state.drilldown.items[0];
      state.drilldown.selectedTimeline = [];
      void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId);
    } else {
      state.drilldown.selectedInteractionId = '';
      state.drilldown.selectedInteraction = null;
      state.drilldown.selectedTimeline = [];
      state.drilldown.detailLoading = false;
    }
  } catch (err) {
    if (!state.drilldown.open) {
      return;
    }
    state.drilldown.items = [];
    state.drilldown.total = 0;
    state.drilldown.selectedInteractionId = '';
    state.drilldown.selectedInteraction = null;
    state.drilldown.selectedTimeline = [];
    state.drilldown.detailLoading = false;
    state.drilldown.error = err.message;
  } finally {
    if (state.drilldown.open) {
      state.drilldown.loading = false;
      renderAnalyticsDrilldown();
      syncAnalyticsUrlState();
    }
  }
}

openAnalyticsDrilldown = function openAnalyticsDrilldown(sourceType, sourceValue = '', options = {}) {
  const mode = options.mode || (sourceType === 'metric' ? 'metric' : 'interaction');
  const filters = analyticsDrilldownBaseFilters();
  if (mode === 'metric') {
    filters.metric = options.metric || sourceValue;
    filters.status = '';
    filters.q = '';
  }
  if (mode === 'ai') {
    filters.slice = options.slice || 'all';
    filters.reason_key = options.reasonKey || '';
    filters.status = '';
    filters.q = '';
    filters.sort_by = 'created_at';
    filters.sort_dir = 'desc';
    if (sourceType === 'ai-overview' && sourceValue) {
      filters.slice = sourceValue;
    }
    if (sourceType === 'ai-channel' && sourceValue) {
      filters.channel = sourceValue;
    }
    if (sourceType === 'ai-reason' && sourceValue) {
      filters.slice = 'handoff';
      filters.reason_key = options.reasonKey || sourceValue;
    }
  }
  if (sourceType === 'channel' && sourceValue) {
    filters.channel = sourceValue;
  }
  if (sourceType === 'queue' && sourceValue) {
    filters.queue_id = sourceValue;
  }
  if (sourceType === 'agent' && sourceValue) {
    filters.agent_id = sourceValue;
  }
  rememberAnalyticsDrilldownReturnFocus();
  state.drilldown = {
    ...emptyDrilldownState(),
    open: true,
    mode,
    sourceType,
    sourceLabel: options.sourceLabel || analyticsDrilldownSourceLabel(sourceType, sourceValue),
    sourceValue,
    metric: mode === 'metric' ? (options.metric || sourceValue) : '',
    filters: normalizeAnalyticsDrilldownFilters(filters),
    coverage: options.coverage || null,
    metricNote: mode === 'metric'
      ? (options.metricNote || analyticsMetricDrilldownNote(options.metric || sourceValue))
      : mode === 'ai'
        ? aiAnalyticsDrilldownNote(filters)
        : '',
  };
  renderAnalyticsDrilldown();
  syncAnalyticsUrlState();
  window.requestAnimationFrame(() => {
    focusAnalyticsDrilldownPrimaryControl(true);
  });
  void loadAnalyticsDrilldownPage();
}

function renderAnalyticsEmptyState() {
  const box = $('analyticsEmptyState');
  if (state.analytics.error) {
    box.hidden = false;
    box.textContent = `Не удалось обновить витрину: ${state.analytics.error}`;
    return;
  }
  const total = Number(state.analytics.overview?.volume?.total || 0);
  if (!state.analytics.loading && total === 0) {
    box.hidden = false;
    box.textContent = 'За выбранный период обращений пока нет. Измените период, очередь или канал, чтобы увидеть данные.';
    return;
  }
  box.hidden = true;
  box.textContent = '';
}

function renderAnalyticsDashboard(rangeMeta = analyticsRangeFromControls()) {
  const refreshBtn = $('refreshBtn');
  const analyticsRefreshBtn = $('analyticsRefreshBtn');
  const analyticsExportBtn = $('analyticsExportBtn');
  const analyticsSaveViewBtn = $('analyticsSaveViewBtn');
  if (refreshBtn) {
    refreshBtn.disabled = state.analytics.loading;
  }
  if (analyticsRefreshBtn) {
    analyticsRefreshBtn.disabled = state.analytics.loading;
  }
  if (analyticsExportBtn) {
    analyticsExportBtn.disabled = state.analytics.loading;
  }
  if (analyticsSaveViewBtn) {
    analyticsSaveViewBtn.disabled = state.analytics.loading;
  }
  renderAnalyticsQueueOptions();
  renderSavedAnalyticsViews();
  updateAnalyticsRangeHint(rangeMeta);
  renderAnalyticsNarrative(rangeMeta);
  renderAnalyticsComparePanel();
  renderAnalyticsOverview();
  renderVoiceNameAnalyticsEmptyState();
  renderVoiceNameAnalyticsOverview();
  renderVoiceNameAnalyticsTrendChart();
  renderVoiceNameAnalyticsFunnel();
  renderVoiceNameAnalyticsLanguageTable();
  renderVoiceNameAnalyticsQueueTable();
  renderVoiceNameAnalyticsHandoffTable();
  renderAiAnalyticsEmptyState();
  renderAiAnalyticsOverview();
  renderAiAnalyticsTrendChart();
  renderAiAnalyticsChannelComparison();
  renderAiAnalyticsCoverage();
  renderAgentAnalyticsEmptyState();
  renderAgentAnalyticsOverview();
  renderAgentAnalyticsStateStrip();
  renderAgentAnalyticsTrendChart();
  renderAgentAnalyticsTeamTable();
  renderAgentAnalyticsShiftTable();
  renderAgentAnalyticsTable();
  renderAnalyticsTrendChart();
  renderAnalyticsChannelTable();
  renderAnalyticsQueueTable();
  renderAnalyticsCoverage();
  renderAnalyticsEmptyState();
  renderAnalyticsDrilldown();
}

async function loadAnalyticsDashboard(_silent = false) {
  syncAnalyticsStateFromControls();
  const rangeMeta = analyticsRangeFromControls();
  const requestId = state.analytics.requestId + 1;
  state.analytics.requestId = requestId;
  state.analytics.lastRangeMeta = rangeMeta;
  state.analytics.loading = true;
  state.analytics.error = '';
  state.analytics.voiceNameError = '';
  state.analytics.voiceNameTrendError = '';
  state.analytics.aiError = '';
  state.analytics.agentError = '';
  state.analytics.agentTrendError = '';
  renderAnalyticsDashboard(rangeMeta);

  if (analyticsMockEnabled()) {
    await ensureAnalyticsMockDataLoaded();
    const payload = buildMockAnalyticsDashboard(rangeMeta);
    if (requestId !== state.analytics.requestId) {
      return;
    }
    state.analytics.queueOptions = payload.queueOptions;
    state.analytics.queueAccessError = '';
    state.analytics.overview = payload.overview;
    state.analytics.compare = state.analytics.compareMode === 'previous' ? payload.compare : emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString());
    state.analytics.coverage = payload.coverage;
    state.analytics.voiceNameOverview = payload.voiceNameOverview;
    state.analytics.voiceNameCompare = state.analytics.compareMode === 'previous'
      ? payload.voiceNameCompare
      : emptyVoiceNameAnalyticsOverview(
          rangeMeta.previous.from.toISOString(),
          rangeMeta.previous.to.toISOString(),
          state.analytics.queueId === 'all' ? null : state.analytics.queueId,
          null,
        );
    state.analytics.aiOverview = payload.aiOverview;
    state.analytics.aiCompare = state.analytics.compareMode === 'previous' ? payload.aiCompare : emptyAiAnalyticsOverview(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString(), state.analytics.channel, state.analytics.queueId === 'all' ? null : state.analytics.queueId);
    state.analytics.agentOverview = payload.agentOverview;
    state.analytics.agentCompare = state.analytics.compareMode === 'previous' ? payload.agentCompare : emptyAgentAnalyticsOverview(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString(), state.analytics.channel, state.analytics.queueId === 'all' ? null : state.analytics.queueId);
    state.analytics.agentRows = Array.isArray(payload.agentOverview?.items) ? payload.agentOverview.items : [];
    state.analytics.channelRows = payload.channelRows;
    state.analytics.queueRows = payload.queueRows;
    state.analytics.trend = payload.trend;
    state.analytics.voiceNameTrend = payload.voiceNameTrend;
    state.analytics.aiTrend = payload.aiTrend;
    state.analytics.agentTrend = payload.agentTrend;
    state.analytics.loading = false;
    state.analytics.statusMessage = state.analytics.mockDataError
      ? `Показаны демонстрационные данные. Не удалось загрузить внешний mock-файл: ${state.analytics.mockDataError}`
      : 'Показаны демонстрационные данные из внешнего mock-файла.';
    renderAnalyticsDashboard(rangeMeta);
    syncAnalyticsUrlState();
    if (state.drilldown.open) {
      void loadAnalyticsDrilldownPage({ preserveSelection: true });
    }
    return;
  }

  try {
    const queuePromise = loadAnalyticsQueueOptions();
    const previousWindowPromise = state.analytics.compareMode === 'previous'
      ? fetchAnalyticsKpi(rangeMeta.previous).catch(() => emptyKpiEnvelope())
      : Promise.resolve(emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString()));
    const emptyAiCurrent = emptyAiAnalyticsOverview(
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      aiAnalyticsSupportedChannel() ? state.analytics.channel : state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
    const emptyAiPrevious = emptyAiAnalyticsOverview(
      rangeMeta.previous.from.toISOString(),
      rangeMeta.previous.to.toISOString(),
      aiAnalyticsSupportedChannel() ? state.analytics.channel : state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
    const emptyVoiceCurrent = emptyVoiceNameAnalyticsOverview(
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      null,
    );
    const emptyVoicePrevious = emptyVoiceNameAnalyticsOverview(
      rangeMeta.previous.from.toISOString(),
      rangeMeta.previous.to.toISOString(),
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
      null,
    );
    const emptyAgentCurrent = emptyAgentAnalyticsOverview(
      rangeMeta.current.from.toISOString(),
      rangeMeta.current.to.toISOString(),
      state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
    const emptyAgentPrevious = emptyAgentAnalyticsOverview(
      rangeMeta.previous.from.toISOString(),
      rangeMeta.previous.to.toISOString(),
      state.analytics.channel,
      state.analytics.queueId === 'all' ? null : state.analytics.queueId,
    );
    const aiOverviewPromise = aiAnalyticsSupportedChannel()
      ? fetchAiAnalyticsOverview(rangeMeta.current).catch((err) => {
          state.analytics.aiError = err.message;
          return emptyAiCurrent;
        })
      : Promise.resolve(emptyAiCurrent);
    const aiComparePromise = aiAnalyticsSupportedChannel() && state.analytics.compareMode === 'previous'
      ? fetchAiAnalyticsOverview(rangeMeta.previous).catch(() => emptyAiPrevious)
      : Promise.resolve(emptyAiPrevious);
    const voiceNameOverviewPromise = voiceNameAnalyticsSupportedChannel()
      ? fetchVoiceNameAnalyticsOverview(rangeMeta.current).catch((err) => {
          state.analytics.voiceNameError = err.message;
          return emptyVoiceCurrent;
        })
      : Promise.resolve(emptyVoiceCurrent);
    const voiceNameComparePromise = voiceNameAnalyticsSupportedChannel() && state.analytics.compareMode === 'previous'
      ? fetchVoiceNameAnalyticsOverview(rangeMeta.previous).catch(() => emptyVoicePrevious)
      : Promise.resolve(emptyVoicePrevious);
    const agentOverviewPromise = fetchAgentAnalyticsOverview(rangeMeta.current).catch((err) => {
      state.analytics.agentError = err.message;
      return emptyAgentCurrent;
    });
    const agentComparePromise = state.analytics.compareMode === 'previous'
      ? fetchAgentAnalyticsOverview(rangeMeta.previous).catch(() => emptyAgentPrevious)
      : Promise.resolve(emptyAgentPrevious);
    const [current, previous, coverage, queues, voiceNameOverview, voiceNameCompare, aiOverview, aiCompare, agentOverview, agentCompare] = await Promise.all([
      fetchAnalyticsKpi(rangeMeta.current),
      previousWindowPromise,
      api('reporting', 'reports/coverage').catch(() => ({ implemented_metrics: [], dimensions: [], supported_filters: [] })),
      queuePromise,
      voiceNameOverviewPromise,
      voiceNameComparePromise,
      aiOverviewPromise,
      aiComparePromise,
      agentOverviewPromise,
      agentComparePromise,
    ]);

    if (requestId !== state.analytics.requestId) {
      return;
    }

    state.analytics.overview = current || emptyKpiEnvelope(rangeMeta.current.from.toISOString(), rangeMeta.current.to.toISOString());
    state.analytics.compare = previous || emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString());
    state.analytics.coverage = coverage || { implemented_metrics: [], dimensions: [], supported_filters: [] };
    state.analytics.voiceNameOverview = voiceNameOverview || emptyVoiceCurrent;
    state.analytics.voiceNameCompare = voiceNameCompare || emptyVoicePrevious;
    state.analytics.aiOverview = aiOverview || emptyAiCurrent;
    state.analytics.aiCompare = aiCompare || emptyAiPrevious;
    state.analytics.agentOverview = agentOverview || emptyAgentCurrent;
    state.analytics.agentCompare = agentCompare || emptyAgentPrevious;
    state.analytics.agentRows = Array.isArray(agentOverview?.items) ? agentOverview.items : [];

    const byChannel = current?.breakdowns?.by_channel || {};
    state.analytics.channelRows = Object.entries(byChannel)
      .map(([channel, payload]) => ({
        channel,
        total: Number(payload?.total || 0),
        answered: Number(payload?.answered || 0),
        abandoned: Number(payload?.abandoned || 0),
      }))
      .sort((a, b) => b.total - a.total || a.channel.localeCompare(b.channel));

    const [trend, queueRows, voiceNameTrend, aiTrend, agentTrend] = await Promise.all([
      loadAnalyticsTrend(rangeMeta, requestId),
      loadAnalyticsQueueRows(rangeMeta, queues, requestId),
      loadVoiceNameAnalyticsTrend(rangeMeta, requestId),
      loadAiAnalyticsTrend(rangeMeta, requestId),
      loadAgentAnalyticsTrend(rangeMeta, requestId),
    ]);

    if (requestId !== state.analytics.requestId) {
      return;
    }

    state.analytics.trend = trend;
    state.analytics.queueRows = queueRows;
    state.analytics.voiceNameTrend = voiceNameTrend;
    state.analytics.aiTrend = aiTrend;
    state.analytics.agentTrend = agentTrend;
    state.analytics.loading = false;
    renderAnalyticsDashboard(rangeMeta);
    syncAnalyticsUrlState();
    if (state.drilldown.open) {
      void loadAnalyticsDrilldownPage({ preserveSelection: true });
    }
  } catch (err) {
    if (requestId !== state.analytics.requestId) {
      return;
    }
    state.analytics.loading = false;
    state.analytics.error = err.message;
    state.analytics.overview = emptyKpiEnvelope();
    state.analytics.compare = emptyKpiEnvelope();
    state.analytics.voiceNameOverview = emptyVoiceNameAnalyticsOverview();
    state.analytics.voiceNameCompare = emptyVoiceNameAnalyticsOverview();
    state.analytics.aiOverview = emptyAiAnalyticsOverview();
    state.analytics.aiCompare = emptyAiAnalyticsOverview();
    state.analytics.agentOverview = emptyAgentAnalyticsOverview();
    state.analytics.agentCompare = emptyAgentAnalyticsOverview();
    state.analytics.coverage = { implemented_metrics: [], dimensions: [], supported_filters: [] };
    state.analytics.trend = [];
    state.analytics.voiceNameTrend = emptyVoiceNameAnalyticsTimeseries(state.analytics.voiceNameTrendMetric || 'scenario_calls');
    state.analytics.aiTrend = emptyAiAnalyticsTimeseries(state.analytics.aiTrendMetric || 'containment_rate');
    state.analytics.agentTrend = emptyAgentAnalyticsTimeseries(state.analytics.agentTrendMetric || 'interactions_per_agent');
    state.analytics.channelRows = [];
    state.analytics.queueRows = [];
    state.analytics.agentRows = [];
    renderAnalyticsDashboard(rangeMeta);
    syncAnalyticsUrlState();
  }
}

function handleAnalyticsOverviewClick(event) {
  const button = event.target.closest('[data-analytics-drilldown-source]');
  if (!button) {
    return;
  }
  const metric = button.dataset.analyticsDrilldownMetric || '';
  const sourceType = button.dataset.analyticsDrilldownSource || 'overview';
  if (metric) {
    openAnalyticsDrilldown('metric', metric, {
      mode: 'metric',
      metric,
      sourceLabel: button.dataset.analyticsDrilldownLabel || analyticsDrilldownSourceLabel('metric', metric),
      coverage: analyticsMetricCoverage(metric),
      metricNote: analyticsMetricDrilldownNote(metric),
    });
    return;
  }
  openAnalyticsDrilldown(sourceType);
}

function handleAnalyticsChannelClick(event) {
  const button = event.target.closest('[data-analytics-channel]');
  if (!button) {
    return;
  }
  openAnalyticsDrilldown('channel', button.dataset.analyticsChannel || '');
}

function handleAnalyticsQueueClick(event) {
  const button = event.target.closest('[data-analytics-queue-id]');
  if (!button) {
    return;
  }
  openAnalyticsDrilldown('queue', button.dataset.analyticsQueueId || '');
}

function handleAgentAnalyticsClick(event) {
  const button = event.target.closest('[data-analytics-agent-id]');
  if (!button) {
    return;
  }
  openAnalyticsDrilldown('agent', button.dataset.analyticsAgentId || '', {
    sourceLabel: button.dataset.analyticsSourceLabel || `Агент: ${button.dataset.analyticsAgentId || ''}`,
  });
}

handleAnalyticsDrilldownListClick = function handleAnalyticsDrilldownListClick(event) {
  if (state.drilldown.mode === 'ai') {
    const button = event.target.closest('[data-analytics-row-id]');
    if (!button) {
      return;
    }
    maybeScrollAnalyticsDrilldownDetailIntoView();
    void loadAnalyticsDrilldownDetails(button.dataset.analyticsRowId || '');
    return;
  }
  const button = event.target.closest('[data-analytics-interaction-id]');
  if (!button) {
    return;
  }
  maybeScrollAnalyticsDrilldownDetailIntoView();
  void loadAnalyticsDrilldownDetails(button.dataset.analyticsInteractionId || '');
}

function handleAnalyticsDrilldownPrevPage() {
  if (!state.drilldown.open || state.drilldown.offset <= 0 || state.drilldown.loading) {
    return;
  }
  state.drilldown.offset = Math.max(0, state.drilldown.offset - state.drilldown.limit);
  void loadAnalyticsDrilldownPage();
}

function handleAnalyticsDrilldownNextPage() {
  if (!state.drilldown.open || state.drilldown.loading) {
    return;
  }
  if (state.drilldown.offset + state.drilldown.limit >= state.drilldown.total) {
    return;
  }
  state.drilldown.offset += state.drilldown.limit;
  void loadAnalyticsDrilldownPage();
}

function handleAnalyticsFilterChange() {
  closeAnalyticsDrilldown({ skipUrlSync: true });
  syncAnalyticsStateFromControls();
  detachActiveAnalyticsView();
  renderSavedAnalyticsViews();
  syncAnalyticsUrlState();
  void loadAnalyticsDashboard();
}

function handleAnalyticsPresetChange() {
  state.analytics.fromTs = '';
  state.analytics.toTs = '';
  $('analyticsFrom').value = '';
  $('analyticsTo').value = '';
  handleAnalyticsFilterChange();
}

function applySelectedAnalyticsView() {
  const viewId = $('analyticsSavedViewSelect').value || '';
  if (!viewId) {
    state.analytics.activeViewId = '';
    state.analytics.statusMessage = '';
    renderSavedAnalyticsViews();
    syncAnalyticsUrlState();
    return;
  }
  const view = state.analytics.savedViews.find((item) => item.id === viewId);
  if (!view) {
    state.analytics.activeViewId = '';
    renderSavedAnalyticsViews();
    syncAnalyticsUrlState();
    return;
  }
  state.analytics.activeViewId = view.id;
  state.analytics.statusMessage = `Загружен вид «${view.name}».`;
  applyAnalyticsSnapshot(view.snapshot);
  closeAnalyticsDrilldown({ skipUrlSync: true });
  syncAnalyticsControlsFromState();
  syncAnalyticsUrlState();
  void loadAnalyticsDashboard();
}

function aiAnalyticsOutcomeSessions(payload, outcome) {
  const rows = Array.isArray(payload?.breakdowns?.by_outcome) ? payload.breakdowns.by_outcome : [];
  return Number(rows.find((item) => item.outcome === outcome)?.sessions || 0);
}

function aiAnalyticsCanDrilldown(metric, payload = state.analytics.aiOverview || emptyAiAnalyticsOverview()) {
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    return false;
  }
  if (!['containment_rate', 'handoff_rate', 'human_touched_rate', 'closed_without_operator_rate'].includes(metric)) {
    return false;
  }
  const total = Number(payload?.totals?.sessions_started || 0);
  if (!total) {
    return false;
  }
  return aiAnalyticsOutcomeSessions(payload, aiAnalyticsMetricSlice(metric)) > 0;
}

function aiAnalyticsMetricDetail(metric, payload) {
  const totals = payload?.totals || {};
  if (metric === 'containment_rate') {
    return `${formatAnalyticsNumber(totals.sessions_contained || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`;
  }
  if (metric === 'handoff_rate') {
    return `${formatAnalyticsNumber(totals.sessions_handoff || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`;
  }
  if (metric === 'human_touched_rate') {
    return `${formatAnalyticsNumber(aiAnalyticsOutcomeSessions(payload, 'human_touched'), 0)} сессий с участием оператора`;
  }
  if (metric === 'ai_latency_avg_ms') {
    return `${formatAnalyticsNumber(totals.assistant_turns || 0, 0)} ответов AI`;
  }
  if (metric === 'closed_without_operator_rate') {
    return `${formatAnalyticsNumber(totals.sessions_closed_without_operator || 0, 0)} закрытий без оператора`;
  }
  return '';
}

function renderAiAnalyticsCard(label, metric, currentPayload, previousPayload) {
  const value = aiAnalyticsMetricValue(metric, currentPayload);
  const showCompare = state.analytics.compareMode === 'previous';
  const delta = showCompare ? aiAnalyticsDelta(metric, currentPayload, previousPayload) : null;
  const note = AI_ANALYTICS_METRIC_META[metric]?.note || '';
  const detail = aiAnalyticsMetricDetail(metric, currentPayload);
  const drilldownEnabled = aiAnalyticsCanDrilldown(metric, currentPayload);
  const slice = aiAnalyticsMetricSlice(metric);
  const tag = drilldownEnabled ? 'button' : 'div';
  const classes = [
    'summary-card',
    'analytics-card',
    'ai-analytics-card',
    drilldownEnabled ? 'analytics-card-button' : 'analytics-card-readonly',
  ].join(' ');
  const attributes = drilldownEnabled
    ? `type="button" data-ai-drilldown-slice="${escapeHtml(slice)}" data-ai-drilldown-label="${escapeHtml(label)}"`
    : '';
  return `
    <${tag} class="${classes}" ${attributes}>
      <div class="analytics-card-top">
        <div class="summary-label">${label}</div>
        ${showCompare ? `<span class="analytics-delta analytics-delta-${delta.tone}">${delta.text}</span>` : ''}
      </div>
      <div class="summary-value">${formatAiAnalyticsMetric(metric, value)}</div>
      ${detail ? `<div class="summary-note">${detail}</div>` : ''}
      ${note ? `<div class="analytics-card-hint ai-analytics-card-hint">${note}</div>` : ''}
    </${tag}>
  `;
}

function renderAiAnalyticsOverview() {
  const box = $('aiAnalyticsOverview');
  if (!box) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    box.innerHTML = '';
    return;
  }
  const current = state.analytics.aiOverview || emptyAiAnalyticsOverview();
  if (!state.analytics.loading && Number(current?.totals?.sessions_started || 0) === 0) {
    box.innerHTML = '';
    return;
  }
  const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview();
  box.innerHTML = [
    renderAiAnalyticsCard('Закрыто AI', 'containment_rate', current, previous),
    renderAiAnalyticsCard('Передано оператору', 'handoff_rate', current, previous),
    renderAiAnalyticsCard('С участием оператора', 'human_touched_rate', current, previous),
    renderAiAnalyticsCard('Закрыто без оператора', 'closed_without_operator_rate', current, previous),
  ].join('');
}

function renderAiAnalyticsChannelComparison() {
  const container = $('aiAnalyticsChannelComparison');
  const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_channel)
    ? state.analytics.aiOverview.breakdowns.by_channel
    : [];
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Нет AI-сессий по Telegram и WhatsApp за выбранный период.</div>';
    return;
  }

  container.innerHTML = `
    <div class="analytics-table-head ai-analytics-channel-grid">
      <span>Канал</span>
      <span>Сессии</span>
      <span>Только AI</span>
      <span>С оператором</span>
      <span>Передача</span>
      <span>Средняя задержка</span>
    </div>
    ${rows
      .map((item) => {
        const aiOnlyShare = item.sessions_started ? (item.ai_only_sessions / item.sessions_started) * 100 : 0;
        const humanTouchedShare = item.sessions_started ? (item.human_touched_sessions / item.sessions_started) * 100 : 0;
        const tag = item.sessions_started ? 'button' : 'div';
        const attributes = item.sessions_started
          ? `type="button" class="analytics-table-row ai-analytics-channel-grid ai-analytics-row-button" data-ai-channel="${escapeHtml(item.channel)}"`
          : 'class="analytics-table-row ai-analytics-channel-grid"';
        return `
          <${tag} ${attributes}>
            <span>${escapeHtml(analyticsChannelLabel(item.channel))}</span>
            <span>${formatAnalyticsNumber(item.sessions_started, 0)}</span>
            <span>${formatAnalyticsNumber(aiOnlyShare, 2)}%</span>
            <span>${formatAnalyticsNumber(humanTouchedShare, 2)}%</span>
            <span>${formatAnalyticsNumber(item.handoff_rate || 0, 2)}%</span>
            <span>${formatAiAnalyticsMetric('ai_latency_avg_ms', item.ai_latency_avg_ms)}</span>
          </${tag}>
        `;
      })
      .join('')}
  `;
}

function renderAiAnalyticsCoverage() {
  const container = $('aiAnalyticsCoverage');
  const coverage = state.analytics.aiOverview?.coverage || emptyAiAnalyticsOverview().coverage;
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!state.analytics.loading && Number(state.analytics.aiOverview?.totals?.sessions_started || 0) === 0) {
    container.innerHTML = '';
    return;
  }
  container.innerHTML = `
    <div class="ai-analytics-coverage-grid">
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_interaction_id || 0, 0)}</strong>
        <p>AI-сессий связаны с ID обращения</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_queue_id || 0, 0)}</strong>
        <p>AI-сессий имеют ID очереди через обращение или диалог</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_latency_turns || 0, 0)}</strong>
        <p>AI-сессий содержат данные по задержке ответов модели</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_terminal_state || 0, 0)}</strong>
        <p>AI-сессий дошли до финального состояния</p>
      </div>
      <div class="analytics-placeholder-card">
        <strong>${formatAnalyticsNumber(coverage.sessions_with_handoff_reason || 0, 0)}</strong>
        <p>AI-сессий содержат причину передачи</p>
      </div>
    </div>
  `;
}

function renderAiAnalyticsOutcomeTable() {
  const container = $('aiAnalyticsOutcomeTable');
  const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_outcome)
    ? state.analytics.aiOverview.breakdowns.by_outcome
    : [];
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">Срезы по исходам появятся, когда в окне будут AI-сессии.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head ai-analytics-outcome-grid">
      <span>Исход</span>
      <span>Сессии</span>
      <span>Доля</span>
    </div>
    ${rows
      .map((item) => {
        const tag = item.sessions ? 'button' : 'div';
        const attributes = item.sessions
          ? `type="button" class="analytics-table-row ai-analytics-outcome-grid ai-analytics-row-button" data-ai-outcome="${escapeHtml(item.outcome)}" data-ai-outcome-label="${escapeHtml(item.label || aiAnalyticsSliceLabel(item.outcome))}"`
          : 'class="analytics-table-row ai-analytics-outcome-grid"';
        return `
          <${tag} ${attributes}>
            <span>${escapeHtml(item.label || aiAnalyticsSliceLabel(item.outcome))}</span>
            <span>${formatAnalyticsNumber(item.sessions, 0)}</span>
            <span>${formatAnalyticsNumber(item.share || 0, 2)}%</span>
          </${tag}>
        `;
      })
      .join('')}
  `;
}

function renderAiAnalyticsReasonTable() {
  const container = $('aiAnalyticsReasonTable');
  const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_handoff_reason)
    ? state.analytics.aiOverview.breakdowns.by_handoff_reason
    : [];
  if (!container) {
    return;
  }
  if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) {
    container.innerHTML = '';
    return;
  }
  if (!rows.length) {
    container.innerHTML = '<div class="empty-state">За выбранный период handoff reasons не найдены.</div>';
    return;
  }
  container.innerHTML = `
    <div class="analytics-table-head ai-analytics-reason-grid">
      <span>Причина</span>
      <span>Сессии</span>
      <span>Доля</span>
    </div>
    ${rows
      .map((item) => `
        <button
          type="button"
          class="analytics-table-row ai-analytics-reason-grid ai-analytics-row-button"
          data-ai-reason-key="${escapeHtml(item.reason_key)}"
          data-ai-reason-label="${escapeHtml(item.label || item.reason_key)}"
        >
          <span>${escapeHtml(item.label || item.reason_key)}</span>
          <span>${formatAnalyticsNumber(item.sessions, 0)}</span>
          <span>${formatAnalyticsNumber(item.share || 0, 2)}%</span>
        </button>
      `)
      .join('')}
  `;
}

const renderAnalyticsDashboardBase = renderAnalyticsDashboard;
const syncAnalyticsDrilldownControlsBase = syncAnalyticsDrilldownControls;
const analyticsDrilldownFiltersForCurrentSourceBase = analyticsDrilldownFiltersForCurrentSource;
const applyAnalyticsDrilldownLocalFiltersBase = applyAnalyticsDrilldownLocalFilters;
const resetAnalyticsDrilldownLocalFiltersBase = resetAnalyticsDrilldownLocalFilters;
const handleAnalyticsDrilldownSearchInputBase = handleAnalyticsDrilldownSearchInput;
const handleAnalyticsDrilldownSearchKeydownBase = handleAnalyticsDrilldownSearchKeydown;
const handleAnalyticsDrilldownStatusChangeBase = handleAnalyticsDrilldownStatusChange;
const handleAnalyticsDrilldownSortChangeBase = handleAnalyticsDrilldownSortChange;

renderAnalyticsDashboard = function renderAnalyticsDashboard(rangeMeta = analyticsRangeFromControls()) {
  renderAnalyticsDashboardBase(rangeMeta);
  renderAiAnalyticsOutcomeTable();
  renderAiAnalyticsReasonTable();
}

analyticsDrilldownChips = function analyticsDrilldownChips() {
  if (!state.drilldown.filters) {
    return [];
  }
  const drilldownFilters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters);
  const chips = [];
  chips.push(state.drilldown.sourceLabel || 'Детализация');
  chips.push(`${formatTime(drilldownFilters.from_ts)} - ${formatTime(drilldownFilters.to_ts)}`);
  if (drilldownFilters.channel) {
    chips.push(analyticsChannelLabel(drilldownFilters.channel));
  }
  if (drilldownFilters.queue_id) {
    chips.push(analyticsQueueName(drilldownFilters.queue_id));
  }
  if (drilldownFilters.agent_id) {
    chips.push(`Агент: ${drilldownFilters.agent_id}`);
  }
  if (state.drilldown.mode === 'ai' && drilldownFilters.slice && drilldownFilters.slice !== 'all') {
    chips.push(aiAnalyticsSliceLabel(drilldownFilters.slice));
  }
  if (state.drilldown.mode === 'ai' && drilldownFilters.reason_key) {
    chips.push(`Причина: ${drilldownFilters.reason_key}`);
  }
  if (drilldownFilters.status) {
    chips.push(`Статус: ${state.drilldown.mode === 'ai' ? aiAnalyticsStatusLabel(drilldownFilters.status) : interactionStatusLabel(drilldownFilters.status)}`);
  }
  if (drilldownFilters.q) {
    chips.push(`Поиск: ${drilldownFilters.q}`);
  }
  if (state.drilldown.mode === 'metric' && state.drilldown.metric) {
    chips.push(analyticsMetricCoverageLabel(state.drilldown.metric));
  } else if (state.drilldown.mode === 'ai') {
    chips.push(aiAnalyticsSortLabel(drilldownFilters));
  } else {
    chips.push(analyticsDrilldownSortLabel(drilldownFilters));
  }
  return chips;
}

function analyticsDrilldownCurrentItemId(item) {
  if (!item) {
    return '';
  }
  if (state.drilldown.mode === 'ai') {
    return String(item.session_id || '');
  }
  return String(item.interaction_id || '');
}

syncAnalyticsDrilldownControls = function syncAnalyticsDrilldownControls() {
  if (state.drilldown.mode !== 'ai') {
    syncAnalyticsDrilldownControlsBase();
    return;
  }
  const searchInput = $('analyticsDrilldownSearch');
  const statusSelect = $('analyticsDrilldownStatus');
  const sortSelect = $('analyticsDrilldownSort');
  const clearBtn = $('analyticsDrilldownClearBtn');
  const exportBtn = $('analyticsDrilldownExportBtn');
  const isOpen = state.drilldown.open;
  const localFiltersEnabled = isOpen;
  const filters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters || {});

  if (statusSelect) {
    statusSelect.innerHTML = [
      '<option value="all">Все статусы</option>',
      '<option value="active">Активна</option>',
      '<option value="closed">Закрыта</option>',
      '<option value="handoff_required">Требуется передача</option>',
      '<option value="human_owned">У оператора</option>',
      '<option value="error">Ошибка</option>',
    ].join('');
    statusSelect.value = isOpen ? (filters.status || 'all') : 'all';
    statusSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (sortSelect) {
    sortSelect.innerHTML = [
      '<option value="created_at:desc">Новые сверху</option>',
      '<option value="created_at:asc">Старые сверху</option>',
      '<option value="updated_at:desc">Обновлённые сверху</option>',
      '<option value="ai_latency_avg_ms:desc">Задержка сверху</option>',
      '<option value="status:asc">Статус A-Z</option>',
    ].join('');
    sortSelect.value = analyticsDrilldownSortValue(filters);
    sortSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (searchInput) {
    searchInput.value = isOpen ? filters.q : '';
    searchInput.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
    searchInput.placeholder = 'ID сессии, диалога, обращения или причина';
  }
  if (clearBtn) {
    clearBtn.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting;
  }
  if (exportBtn) {
    exportBtn.disabled = !isOpen || state.drilldown.loading || state.drilldown.exporting || !state.drilldown.total;
    exportBtn.textContent = state.drilldown.exporting ? 'Готовим CSV...' : 'Экспорт CSV';
  }
}

analyticsDrilldownFiltersForCurrentSource = function analyticsDrilldownFiltersForCurrentSource() {
  if (state.drilldown.mode !== 'ai') {
    return analyticsDrilldownFiltersForCurrentSourceBase();
  }
  const filters = analyticsDrilldownBaseFilters();
  filters.slice = 'all';
  filters.reason_key = '';
  filters.status = '';
  filters.q = '';
  filters.sort_by = 'created_at';
  filters.sort_dir = 'desc';
  if (state.drilldown.sourceType === 'ai-overview' && state.drilldown.sourceValue) {
    filters.slice = state.drilldown.sourceValue;
  }
  if (state.drilldown.sourceType === 'ai-channel' && state.drilldown.sourceValue) {
    filters.channel = state.drilldown.sourceValue;
  }
  if (state.drilldown.sourceType === 'ai-reason' && state.drilldown.sourceValue) {
    filters.slice = 'handoff';
    filters.reason_key = state.drilldown.sourceValue;
  }
  return normalizeAnalyticsDrilldownFilters(filters);
}

applyAnalyticsDrilldownLocalFilters = function applyAnalyticsDrilldownLocalFilters(nextFilters, options = {}) {
  if (!state.drilldown.open || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return applyAnalyticsDrilldownLocalFiltersBase(nextFilters, options);
    }
    return;
  }
  state.drilldown.filters = normalizeAnalyticsDrilldownFilters(nextFilters);
  state.drilldown.offset = 0;
  state.drilldown.notice = '';
  void loadAnalyticsDrilldownPage({ preserveSelection: options.preserveSelection !== false });
}

resetAnalyticsDrilldownLocalFilters = function resetAnalyticsDrilldownLocalFilters() {
  clearAnalyticsDrilldownSearchDebounce();
  if (!state.drilldown.open || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return resetAnalyticsDrilldownLocalFiltersBase();
    }
    return;
  }
  state.drilldown.filters = analyticsDrilldownFiltersForCurrentSource();
  state.drilldown.offset = 0;
  state.drilldown.notice = '';
  renderAnalyticsDrilldown();
  void loadAnalyticsDrilldownPage({ preserveSelection: true });
}

handleAnalyticsDrilldownSearchInput = function handleAnalyticsDrilldownSearchInput(event) {
  if (!state.drilldown.open || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return handleAnalyticsDrilldownSearchInputBase(event);
    }
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  const query = event.target?.value || '';
  analyticsDrilldownSearchDebounce = window.setTimeout(() => {
    if (!state.drilldown.open || !state.drilldown.filters) {
      return;
    }
    const normalizedQuery = query.trim();
    if ((state.drilldown.filters.q || '') === normalizedQuery) {
      return;
    }
    applyAnalyticsDrilldownLocalFilters({
      ...state.drilldown.filters,
      q: normalizedQuery,
    });
  }, 300);
}

handleAnalyticsDrilldownSearchKeydown = function handleAnalyticsDrilldownSearchKeydown(event) {
  if (event.key !== 'Enter' || !state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return handleAnalyticsDrilldownSearchKeydownBase(event);
    }
    return;
  }
  event.preventDefault();
  clearAnalyticsDrilldownSearchDebounce();
  const normalizedQuery = (event.target?.value || '').trim();
  if ((state.drilldown.filters.q || '') === normalizedQuery) {
    return;
  }
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    q: normalizedQuery,
  });
}

handleAnalyticsDrilldownStatusChange = function handleAnalyticsDrilldownStatusChange(event) {
  if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return handleAnalyticsDrilldownStatusChangeBase(event);
    }
    return;
  }
  const nextStatus = event.target?.value === 'all' ? '' : String(event.target?.value || '');
  if ((state.drilldown.filters.status || '') === nextStatus) {
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    status: nextStatus,
  });
}

handleAnalyticsDrilldownSortChange = function handleAnalyticsDrilldownSortChange(event) {
  if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') {
    if (state.drilldown.mode !== 'ai') {
      return handleAnalyticsDrilldownSortChangeBase(event);
    }
    return;
  }
  const nextSort = parseAnalyticsDrilldownSort(event.target?.value || 'created_at:desc');
  if (
    (state.drilldown.filters.sort_by || 'created_at') === nextSort.sort_by
    && (state.drilldown.filters.sort_dir || 'desc') === nextSort.sort_dir
  ) {
    return;
  }
  clearAnalyticsDrilldownSearchDebounce();
  applyAnalyticsDrilldownLocalFilters({
    ...state.drilldown.filters,
    ...nextSort,
  });
}

function analyticsTimeseriesIntervalForRange(rangeMeta) {
  const durationMs = Math.max(0, rangeMeta.current.to.getTime() - rangeMeta.current.from.getTime());
  return durationMs <= 36 * 60 * 60 * 1000 ? 'hour' : 'day';
}

async function fetchAnalyticsTimeseries(range, metric, interval, overrides = {}) {
  const params = new URLSearchParams(analyticsQuery(range, overrides));
  params.set('metric', metric || state.analytics.trendMetric || 'volume');
  params.set('interval', interval || 'day');
  return api('reporting', `reports/timeseries?${params.toString()}`);
}

function trendPointEnvelope(metric, value) {
  const envelope = emptyKpiEnvelope();
  if (metric === 'volume' || metric === 'total') {
    envelope.volume.total = Number(value || 0);
    return envelope;
  }
  if (metric === 'answered') {
    envelope.volume.answered = Number(value || 0);
    return envelope;
  }
  envelope.kpi[metric] = Number(value || 0);
  return envelope;
}

async function loadAnalyticsTrend(rangeMeta, requestId) {
  const metric = state.analytics.trendMetric || 'volume';
  const interval = analyticsTimeseriesIntervalForRange(rangeMeta);
  try {
    const payload = await fetchAnalyticsTimeseries(rangeMeta.current, metric, interval);
    if (requestId !== state.analytics.requestId) {
      return [];
    }
    const resolvedInterval = payload?.interval || interval;
    state.analytics.trendInterval = resolvedInterval;
    const points = Array.isArray(payload?.points) ? payload.points : [];
    return points.map((point) => ({
      ts: point.ts,
      label: formatAnalyticsBucketLabel(point.ts, resolvedInterval),
      payload: trendPointEnvelope(metric, point.value),
      sampleSize: Number(point.sample_size || 0),
    }));
  } catch {
    state.analytics.trendInterval = interval;
    return [];
  }
}

function reportingExportQuery(range, extra = {}) {
  const params = new URLSearchParams(analyticsQuery(range, extra));
  if (extra.metric) {
    params.set('metric', extra.metric);
  }
  return params.toString();
}

async function exportAnalyticsCsv() {
  const rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls();
  const dateStamp = new Date().toISOString().slice(0, 10);
  try {
    await downloadProxyFile(
      'reporting',
      `reports/export?${reportingExportQuery(rangeMeta.current)}`,
      `konturcc-analytics-${dateStamp}.csv`,
    );
    state.analytics.statusMessage = `CSV выгружен в ${new Date().toLocaleTimeString('ru-RU', {
      hour: '2-digit',
      minute: '2-digit',
    })}.`;
  } catch (err) {
    state.analytics.statusMessage = `Не удалось выгрузить CSV: ${err.message}`;
  }
  renderSavedAnalyticsViews();
}

async function loadSavedAnalyticsViews() {
  try {
    const items = await api('reporting', 'reports/views');
    state.analytics.savedViews = Array.isArray(items)
      ? items
          .filter((item) => item && typeof item === 'object')
          .map((item) => normalizeSavedAnalyticsView(item))
          .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
      : [];
  } catch (err) {
    state.analytics.savedViews = [];
    state.analytics.statusMessage = `Не удалось загрузить сохранённые виды: ${err.message}`;
  }
  if (
    state.analytics.activeViewId
    && !state.analytics.savedViews.some((item) => item.id === state.analytics.activeViewId)
  ) {
    state.analytics.activeViewId = '';
  }
  renderSavedAnalyticsViews();
}

async function saveAnalyticsView() {
  syncAnalyticsStateFromControls();
  const input = $('analyticsSavedViewName');
  const name = input?.value.trim() || suggestAnalyticsViewName();
  try {
    const view = await api('reporting', 'reports/views', {
      method: 'POST',
      body: JSON.stringify({
        id: state.analytics.activeViewId || null,
        name,
        snapshot: analyticsCurrentSnapshot(),
      }),
    });
    const normalized = normalizeSavedAnalyticsView(view);
    const existingIndex = state.analytics.savedViews.findIndex((item) => item.id === normalized.id);
    if (existingIndex >= 0) {
      state.analytics.savedViews.splice(existingIndex, 1, normalized);
      state.analytics.statusMessage = `Вид «${name}» обновлён.`;
    } else {
      state.analytics.savedViews.unshift(normalized);
      state.analytics.statusMessage = `Вид «${name}» сохранён.`;
    }
    state.analytics.savedViews = state.analytics.savedViews
      .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
      .slice(0, 12);
    state.analytics.activeViewId = normalized.id;
  } catch (err) {
    state.analytics.statusMessage = `Не удалось сохранить вид: ${err.message}`;
  }
  renderSavedAnalyticsViews();
}

async function deleteAnalyticsView() {
  const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null;
  if (!activeView) {
    state.analytics.statusMessage = 'Выберите сохранённый вид, чтобы удалить его.';
    renderSavedAnalyticsViews();
    return;
  }
  try {
    await api('reporting', `reports/views/${encodeURIComponent(activeView.id)}`, { method: 'DELETE' });
    state.analytics.savedViews = state.analytics.savedViews.filter((item) => item.id !== activeView.id);
    state.analytics.activeViewId = '';
    state.analytics.statusMessage = `Вид «${activeView.name}» удалён.`;
  } catch (err) {
    state.analytics.statusMessage = `Не удалось удалить вид: ${err.message}`;
  }
  renderSavedAnalyticsViews();
}

function wire() {
  $('logoutBtn').addEventListener('click', logout);
  $('refreshBtn').addEventListener('click', () => {
    void loadAnalyticsDashboard();
  });
  $('analyticsRefreshBtn').addEventListener('click', () => {
    void loadAnalyticsDashboard();
  });
  $('analyticsResetBtn').addEventListener('click', () => {
    resetAnalyticsFilters();
    void loadAnalyticsDashboard();
  });
  $('analyticsPreset').addEventListener('change', handleAnalyticsPresetChange);
  $('analyticsFrom').addEventListener('change', handleAnalyticsFilterChange);
  $('analyticsTo').addEventListener('change', handleAnalyticsFilterChange);
  $('analyticsQueue').addEventListener('change', handleAnalyticsFilterChange);
  $('analyticsChannel').addEventListener('change', handleAnalyticsFilterChange);
  $('analyticsCompareMode').addEventListener('change', handleAnalyticsFilterChange);
  $('analyticsSavedViewSelect').addEventListener('change', applySelectedAnalyticsView);
  $('analyticsSaveViewBtn').addEventListener('click', async () => {
    await saveAnalyticsView();
    syncAnalyticsUrlState();
  });
  $('analyticsDeleteViewBtn').addEventListener('click', async () => {
    await deleteAnalyticsView();
    syncAnalyticsUrlState();
  });
  $('analyticsExportBtn').addEventListener('click', exportAnalyticsCsv);
  $('analyticsTrendMetric').addEventListener('change', () => {
    syncAnalyticsStateFromControls();
    detachActiveAnalyticsView();
    renderSavedAnalyticsViews();
    renderAnalyticsTrendChart();
    syncAnalyticsUrlState();
  });
  $('voiceNameAnalyticsTrendMetric')?.addEventListener('change', () => {
    syncAnalyticsStateFromControls();
    detachActiveAnalyticsView();
    renderSavedAnalyticsViews();
    syncAnalyticsUrlState();
    void loadAnalyticsDashboard();
  });
  $('aiAnalyticsTrendMetric').addEventListener('change', () => {
    syncAnalyticsStateFromControls();
    detachActiveAnalyticsView();
    renderSavedAnalyticsViews();
    syncAnalyticsUrlState();
    void loadAnalyticsDashboard();
  });
  $('agentAnalyticsTrendMetric')?.addEventListener('change', () => {
    syncAnalyticsStateFromControls();
    detachActiveAnalyticsView();
    renderSavedAnalyticsViews();
    syncAnalyticsUrlState();
    void loadAnalyticsDashboard();
  });
  $('analyticsOverview').addEventListener('click', handleAnalyticsOverviewClick);
  $('analyticsChannelTable').addEventListener('click', handleAnalyticsChannelClick);
  $('analyticsQueueTable').addEventListener('click', handleAnalyticsQueueClick);
  $('agentAnalyticsTable')?.addEventListener('click', handleAgentAnalyticsClick);
  $('analyticsDrilldownCloseBtn').addEventListener('click', closeAnalyticsDrilldown);
  $('analyticsDrilldownBackdrop').addEventListener('click', closeAnalyticsDrilldown);
  $('analyticsDrilldownList').addEventListener('click', handleAnalyticsDrilldownListClick);
  $('analyticsDrilldownPrevBtn').addEventListener('click', handleAnalyticsDrilldownPrevPage);
  $('analyticsDrilldownNextBtn').addEventListener('click', handleAnalyticsDrilldownNextPage);
  $('analyticsDrilldownSearch').addEventListener('input', handleAnalyticsDrilldownSearchInput);
  $('analyticsDrilldownSearch').addEventListener('keydown', handleAnalyticsDrilldownSearchKeydown);
  $('analyticsDrilldownStatus').addEventListener('change', handleAnalyticsDrilldownStatusChange);
  $('analyticsDrilldownSort').addEventListener('change', handleAnalyticsDrilldownSortChange);
  $('analyticsDrilldownClearBtn').addEventListener('click', resetAnalyticsDrilldownLocalFilters);
  $('analyticsDrilldownExportBtn').addEventListener('click', () => {
    void exportAnalyticsDrilldownCsv();
  });
  window.addEventListener('keydown', handleAnalyticsDrilldownWindowKeydown);
  window.addEventListener('message', handleOidcMessage);
  $('aiAnalyticsOverview')?.addEventListener('click', handleAiAnalyticsOverviewClick);
  $('aiAnalyticsChannelComparison')?.addEventListener('click', handleAiAnalyticsChannelClick);
  $('aiAnalyticsOutcomeTable')?.addEventListener('click', handleAiAnalyticsOutcomeClick);
  $('aiAnalyticsReasonTable')?.addEventListener('click', handleAiAnalyticsReasonClick);
}

async function init() {
  if (!restoreStoredSession()) {
    window.location.href = '/';
    return;
  }
  if (!ensurePageAccess()) {
    return;
  }
  wire();
  updateSessionInfo();
  const deepLinkState = parseAnalyticsDeepLinkState();
  analyticsUrlSyncSuspended = true;
  try {
    resetAnalyticsFilters();
    if (deepLinkState.hasSnapshot) {
      applyAnalyticsSnapshot(deepLinkState.snapshot);
    }
    state.analytics.activeViewId = deepLinkState.activeViewId || '';
    syncAnalyticsControlsFromState();
    await Promise.all([
      checkGateway(),
      loadOidcConfig(),
      loadSavedAnalyticsViews(),
    ]);
    if (!deepLinkState.hasSnapshot && state.analytics.activeViewId) {
      const savedView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null;
      if (savedView) {
        applyAnalyticsSnapshot(savedView.snapshot);
        syncAnalyticsControlsFromState();
      }
    } else {
      syncAnalyticsControlsFromState();
    }
    await loadAnalyticsDashboard(true);
    if (deepLinkState.drilldown) {
      restoreAnalyticsDrilldownFromUrlState(deepLinkState.drilldown);
    }
  } finally {
    analyticsUrlSyncSuspended = false;
  }
  syncAnalyticsUrlState();
}

init();
