import { addSystemChatMessage, authService, authorizeSession, state } from '../state.js';

const TYPES = {
  INVITE: 100,
  RINGING: 110,
  ACCEPT: 120,
  DECLINE_BUSY: 130,
  TIMEOUT: 140,
  HANGUP: 150,
  OFFER: 200,
  ANSWER: 210,
  ICE: 220,
};

const calls = new Map();
const callStateListeners = new Set();

let activeCallId = '';
let debugReporter = null;

let audioContext = null;
let toneTimerId = null;
let toneName = '';
let toneFlip = false;

const DEFAULT_ICE_SERVERS = Object.freeze([
  { urls: 'stun:stun.l.google.com:19302' },
]);

function nowMs() {
  return Date.now();
}

function resolveCallPreflightTimeoutMs() {
  const configured = Number(state?.entrySettings?.callPreflightTimeoutMs || 6000);
  return Math.max(1000, Math.min(20000, Number.isFinite(configured) ? configured : 6000));
}

function isSessionReadyForCall() {
  const wsOpen = Boolean(authService?.ws?.ws && authService.ws.ws.readyState === WebSocket.OPEN);
  const hasSession = Boolean(state?.session?.isAuthorized && state?.session?.login && state?.session?.sessionId);
  return wsOpen && hasSession;
}

async function withTimeout(promise, timeoutMs, timeoutMessage = 'timeout') {
  let timerId = 0;
  try {
    return await Promise.race([
      promise,
      new Promise((_, reject) => {
        timerId = window.setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
      }),
    ]);
  } finally {
    if (timerId) window.clearTimeout(timerId);
  }
}

async function ensureSessionForCall({ timeoutMs, force = false } = {}) {
  if (!force && isSessionReadyForCall()) return true;
  const login = String(state?.session?.login || '').trim();
  const sessionId = String(state?.session?.sessionId || '').trim();
  if (!login || !sessionId) return false;

  try {
    await withTimeout(authService.ws.open(), timeoutMs, 'call_preflight_ws_timeout');
    const resumed = await withTimeout(authService.resumeSession(login, sessionId), timeoutMs, 'call_preflight_resume_timeout');
    authorizeSession(resumed);
    return true;
  } catch {
    return false;
  }
}

function makeCallId() {
  return `${Date.now()}${Math.floor(Math.random() * 1_000_000_000)}`;
}

function getCall(callId) {
  return calls.get(callId) || null;
}

function getActiveCall() {
  if (!activeCallId) return null;
  return getCall(activeCallId);
}

function toErrorText(error) {
  return error?.message || String(error || 'unknown');
}

function formatDuration(ms) {
  const totalSec = Math.max(0, Math.round(Number(ms || 0) / 1000));
  const sec = totalSec % 60;
  const totalMin = Math.floor(totalSec / 60);
  const min = totalMin % 60;
  const hours = Math.floor(totalMin / 60);
  if (hours > 0) return `${hours}ч ${min}м ${sec}с`;
  if (min > 0) return `${min}м ${sec}с`;
  return `${sec}с`;
}

function cloneDefaultIceServers() {
  return DEFAULT_ICE_SERVERS.map((row) => ({ ...row }));
}

function parseIceUrls(raw) {
  if (Array.isArray(raw)) {
    return raw
      .map((item) => String(item || '').trim())
      .filter((item) => item.length > 0);
  }
  const single = String(raw || '').trim();
  if (!single) return [];
  return [single];
}

function uniqueUrls(urls = []) {
  const out = [];
  const seen = new Set();
  urls.forEach((url) => {
    const clean = String(url || '').trim();
    if (!clean || seen.has(clean)) return;
    seen.add(clean);
    out.push(clean);
  });
  return out;
}

function parseTurnHostFromUrl(rawUrl) {
  const value = String(rawUrl || '').trim();
  if (!value) return '';
  const noProto = value.replace(/^turns?:/i, '');
  const noQuery = noProto.split('?')[0];
  const noPath = noQuery.split('/')[0];
  const hostPort = noPath.replace(/^\/\//, '').trim();
  if (!hostPort) return '';
  if (hostPort.startsWith('[')) {
    const end = hostPort.indexOf(']');
    if (end > 1) return hostPort.slice(1, end);
    return '';
  }
  const idx = hostPort.indexOf(':');
  return (idx >= 0 ? hostPort.slice(0, idx) : hostPort).trim();
}

async function resolveIceServers(call) {
  try {
    const payload = await authService.getCallIceConfig();
    const stunUrls = uniqueUrls(parseIceUrls(payload?.stunUrls));
    const turnUrls = uniqueUrls(parseIceUrls(payload?.turnUrls));
    const turnUsername = String(payload?.turnUsername || '').trim();
    const turnPassword = String(payload?.turnPassword || '').trim();
    const turnServers = Array.isArray(payload?.turnServers) ? payload.turnServers : [];

    const turnHostSet = new Set();
    const iceServers = [];
    if (stunUrls.length > 0) {
      iceServers.push({ urls: stunUrls.length === 1 ? stunUrls[0] : stunUrls });
    }
    if (turnServers.length > 0) {
      turnServers.forEach((item) => {
        const urls = uniqueUrls(parseIceUrls(item?.urls));
        const username = String(item?.username || '').trim();
        const password = String(item?.password || '').trim();
        if (urls.length === 0 || !username || !password) return;
        urls.forEach((url) => {
          const host = parseTurnHostFromUrl(url);
          if (host) turnHostSet.add(host);
        });
        iceServers.push({
          urls: urls.length === 1 ? urls[0] : urls,
          username,
          credential: password,
        });
      });
    } else if (turnUrls.length > 0 && turnUsername && turnPassword) {
      turnUrls.forEach((url) => {
        const host = parseTurnHostFromUrl(url);
        if (host) turnHostSet.add(host);
      });
      iceServers.push({
        urls: turnUrls.length === 1 ? turnUrls[0] : turnUrls,
        username: turnUsername,
        credential: turnPassword,
      });
    }

    if (iceServers.length === 0) {
      await emitDebug(call, 'warn', 'call_ice_empty_from_server', 'using_default_stun');
      return cloneDefaultIceServers();
    }
    if (call) {
      call.turnHostsConfigured = Array.from(turnHostSet);
    }
    await emitDebug(call, 'info', 'call_ice_loaded_from_server', `stun=${stunUrls.length}; turnEntries=${Math.max(0, iceServers.length - (stunUrls.length > 0 ? 1 : 0))}`);
    return iceServers;
  } catch (error) {
    await emitDebug(call, 'warn', 'call_ice_load_failed', toErrorText(error));
    return cloneDefaultIceServers();
  }
}

async function collectIceCandidateAnalytics(call) {
  const pc = call?.pc;
  if (!pc || typeof pc.getStats !== 'function') return {};
  try {
    const stats = await pc.getStats();
    const localCounts = { host: 0, srflx: 0, relay: 0, prflx: 0, other: 0 };
    const remoteCounts = { host: 0, srflx: 0, relay: 0, prflx: 0, other: 0 };
    const relayLocalAddresses = new Set();
    const relayRemoteAddresses = new Set();
    const configuredHosts = new Set((call?.turnHostsConfigured || []).map((v) => String(v || '').trim()).filter(Boolean));
    const matchedConfiguredHosts = new Set();
    let succeededPairsCount = 0;
    let succeededPairsWithRelayCount = 0;

    const bump = (bucket, rawType) => {
      const type = String(rawType || '').trim().toLowerCase();
      if (!type) bucket.other += 1;
      else if (Object.prototype.hasOwnProperty.call(bucket, type)) bucket[type] += 1;
      else bucket.other += 1;
    };
    const hostOf = (candidate) => {
      const raw = String(candidate?.ip || candidate?.address || '').trim();
      if (!raw) return '';
      if (raw.startsWith('[')) {
        const end = raw.indexOf(']');
        if (end > 1) return raw.slice(1, end);
      }
      return raw;
    };

    stats.forEach((report) => {
      if (!report || report.type !== 'local-candidate') return;
      bump(localCounts, report.candidateType);
      if (String(report.candidateType || '').toLowerCase() === 'relay') {
        const addr = String(report.ip || report.address || '');
        const port = String(report.port || '');
        relayLocalAddresses.add(port ? `${addr}:${port}` : addr);
        const host = hostOf(report);
        if (host && configuredHosts.has(host)) matchedConfiguredHosts.add(host);
      }
    });
    stats.forEach((report) => {
      if (!report || report.type !== 'remote-candidate') return;
      bump(remoteCounts, report.candidateType);
      if (String(report.candidateType || '').toLowerCase() === 'relay') {
        const addr = String(report.ip || report.address || '');
        const port = String(report.port || '');
        relayRemoteAddresses.add(port ? `${addr}:${port}` : addr);
      }
    });
    stats.forEach((report) => {
      if (!report || report.type !== 'candidate-pair') return;
      const state = String(report.state || '').toLowerCase();
      const ok = report.selected === true || (report.nominated === true && state === 'succeeded') || state === 'succeeded';
      if (!ok) return;
      succeededPairsCount += 1;
      const local = report.localCandidateId && typeof stats.get === 'function' ? stats.get(report.localCandidateId) : null;
      const remote = report.remoteCandidateId && typeof stats.get === 'function' ? stats.get(report.remoteCandidateId) : null;
      const lt = String(local?.candidateType || '').toLowerCase();
      const rt = String(remote?.candidateType || '').toLowerCase();
      if (lt === 'relay' || rt === 'relay') succeededPairsWithRelayCount += 1;
    });

    return {
      localCandidatesHost: localCounts.host,
      localCandidatesSrflx: localCounts.srflx,
      localCandidatesRelay: localCounts.relay,
      localCandidatesPrflx: localCounts.prflx,
      localCandidatesOther: localCounts.other,
      remoteCandidatesHost: remoteCounts.host,
      remoteCandidatesSrflx: remoteCounts.srflx,
      remoteCandidatesRelay: remoteCounts.relay,
      remoteCandidatesPrflx: remoteCounts.prflx,
      remoteCandidatesOther: remoteCounts.other,
      relayLocalCandidatesFound: relayLocalAddresses.size,
      relayRemoteCandidatesFound: relayRemoteAddresses.size,
      relayLocalCandidatesAddresses: Array.from(relayLocalAddresses).join('|'),
      relayRemoteCandidatesAddresses: Array.from(relayRemoteAddresses).join('|'),
      configuredTurnHosts: Array.from(configuredHosts).join('|'),
      configuredTurnHostsCount: configuredHosts.size,
      reachableTurnHostsCount: matchedConfiguredHosts.size,
      reachableTurnHosts: Array.from(matchedConfiguredHosts).join('|'),
      turnConfiguredButNotReachedHosts: Array.from(configuredHosts).filter((host) => !matchedConfiguredHosts.has(host)).join('|'),
      succeededCandidatePairsCount: succeededPairsCount,
      succeededCandidatePairsWithRelayCount: succeededPairsWithRelayCount,
    };
  } catch {
    return {};
  }
}

function ensureAudioContext() {
  if (audioContext) return audioContext;
  const Ctx = window.AudioContext || window.webkitAudioContext;
  if (!Ctx) return null;
  audioContext = new Ctx();
  return audioContext;
}

function playBeep(freq = 440, durationMs = 120, gainValue = 0.08) {
  const ctx = ensureAudioContext();
  if (!ctx) return;
  if (ctx.state === 'suspended') {
    void ctx.resume().catch(() => {});
  }
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = 'sine';
  osc.frequency.value = Number(freq) || 440;
  gain.gain.value = gainValue;
  osc.connect(gain);
  gain.connect(ctx.destination);
  const now = ctx.currentTime;
  osc.start(now);
  osc.stop(now + Math.max(0.03, durationMs / 1000));
}

function stopTone() {
  if (toneTimerId) {
    clearInterval(toneTimerId);
    toneTimerId = null;
  }
  toneName = '';
  toneFlip = false;
}

function startTone(nextToneName) {
  if (!nextToneName) {
    stopTone();
    return;
  }
  if (toneName === nextToneName) return;
  stopTone();
  toneName = nextToneName;
  if (nextToneName === 'searching') {
    toneTimerId = window.setInterval(() => {
      toneFlip = !toneFlip;
      playBeep(toneFlip ? 920 : 760, 120, 0.07);
    }, 420);
    return;
  }
  if (nextToneName === 'ringback') {
    playBeep(425, 900, 0.08);
    toneTimerId = window.setInterval(() => {
      playBeep(425, 900, 0.08);
    }, 4000);
    return;
  }
  if (nextToneName === 'incoming') {
    const hit = () => {
      playBeep(830, 180, 0.09);
      window.setTimeout(() => playBeep(680, 180, 0.09), 240);
    };
    hit();
    toneTimerId = window.setInterval(hit, 2000);
  }
}

function getCallStateSnapshot() {
  const call = getActiveCall();
  if (!call) return null;
  const callPhase = String(call.phase || '').trim();
  return {
    callId: call.callId,
    peerLogin: call.peerLogin || '',
    direction: call.direction || 'out',
    phase: callPhase,
    statusText: call.statusText || '',
    muted: Boolean(call.muted),
    canAnswer: callPhase === 'incoming',
    canDecline: callPhase === 'incoming',
    canHangup: callPhase !== 'ended' && callPhase !== 'incoming',
    canMute: callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing' || callPhase === 'reconnecting',
  };
}

function notifyCallState() {
  const snapshot = getCallStateSnapshot();
  callStateListeners.forEach((listener) => {
    try {
      listener(snapshot);
    } catch {}
  });
}

function setStatus(call, statusText, phase = '') {
  if (!call) return;
  call.statusText = String(statusText || '').trim();
  if (phase) {
    call.phase = String(phase || '').trim();
  }
  if (call.phase === 'searching') startTone('searching');
  else if (call.phase === 'ringing') startTone('ringback');
  else if (call.phase === 'incoming') startTone('incoming');
  else stopTone();

  void emitDebug(call, 'info', `call_status: ${call.statusText}`, `callId=${call.callId}`);
  notifyCallState();
}

function buildActiveStatusText(call) {
  const route = String(call?.connectionRouteLabel || '').trim();
  return route ? `Разговор идёт (${route})` : 'Разговор идёт';
}

function setActiveStatus(call) {
  setStatus(call, buildActiveStatusText(call), 'active');
}

function toIsoTs(ts) {
  const n = Number(ts || 0);
  if (!Number.isFinite(n) || n <= 0) return '';
  try { return new Date(n).toISOString(); } catch { return ''; }
}

function buildCallFactsJson(call, extra = {}) {
  const pc = call?.pc || null;
  const facts = {
    callId: call?.callId || '',
    peerLogin: call?.peerLogin || '',
    remoteSessionId: call?.remoteSessionId || '',
    direction: call?.direction || '',
    phase: call?.phase || '',
    statusText: call?.statusText || '',
    startedAtMs: Number(call?.startedAtMs || 0),
    startedAtIso: toIsoTs(call?.startedAtMs),
    connectedAtMs: Number(call?.connectedAtMs || 0),
    connectedAtIso: toIsoTs(call?.connectedAtMs),
    routeLabel: call?.connectionRouteLabel || '',
    routeDetails: call?.connectionRouteDetails || '',
    pcConnectionState: pc?.connectionState || '',
    pcIceConnectionState: pc?.iceConnectionState || '',
    pcSignalingState: pc?.signalingState || '',
    hasLocalStream: Boolean(call?.localStream),
    localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0,
    ...extra,
  };
  try {
    return JSON.stringify(facts);
  } catch {
    return JSON.stringify({ callId: call?.callId || '', serializeError: true });
  }
}

function buildCallFactsLine(call, extra = {}) {
  const pc = call?.pc || null;
  const facts = {
    callId: call?.callId || '',
    peerLogin: call?.peerLogin || '',
    remoteSessionId: call?.remoteSessionId || '',
    direction: call?.direction || '',
    phase: call?.phase || '',
    statusText: call?.statusText || '',
    startedAtMs: Number(call?.startedAtMs || 0),
    startedAtIso: toIsoTs(call?.startedAtMs),
    connectedAtMs: Number(call?.connectedAtMs || 0),
    connectedAtIso: toIsoTs(call?.connectedAtMs),
    routeLabel: call?.connectionRouteLabel || '',
    routeDetails: call?.connectionRouteDetails || '',
    pcConnectionState: pc?.connectionState || '',
    pcIceConnectionState: pc?.iceConnectionState || '',
    pcSignalingState: pc?.signalingState || '',
    hasLocalStream: Boolean(call?.localStream),
    localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0,
    ...extra,
  };
  return Object.entries(facts)
    .map(([k, v]) => `${k}=${String(v ?? '').replace(/,/g, ';')}`)
    .join(', ');
}

function getCallDiagnosticsContext(call) {
  const pc = call?.pc || null;
  const nav = typeof navigator !== 'undefined' ? navigator : null;
  const conn = nav?.connection || nav?.mozConnection || nav?.webkitConnection || null;
  const permissionsApiAvailable = typeof nav?.permissions?.query === 'function';
  const mediaDevicesAvailable = Boolean(nav?.mediaDevices?.getUserMedia);
  const online = typeof nav?.onLine === 'boolean' ? nav.onLine : null;
  const visibilityState = typeof document !== 'undefined' ? String(document.visibilityState || '') : '';
  const pageFocused = typeof document !== 'undefined' && typeof document.hasFocus === 'function'
    ? Boolean(document.hasFocus())
    : false;
  const localTracks = call?.localStream?.getTracks?.() || [];
  const localAudioTracks = call?.localStream?.getAudioTracks?.() || [];
  const enabledLocalAudioTracks = localAudioTracks.filter((track) => track?.enabled).length;
  const transceiversCount = pc?.getTransceivers?.()?.length || 0;
  const sendersCount = pc?.getSenders?.()?.length || 0;
  const receiversCount = pc?.getReceivers?.()?.length || 0;
  const iceGatheringState = pc?.iceGatheringState || '';
  const currentLocalDescType = pc?.localDescription?.type || '';
  const currentRemoteDescType = pc?.remoteDescription?.type || '';

  return {
    remoteSessionIdPresent: Boolean(call?.remoteSessionId),
    callExistsInStore: calls.has(String(call?.callId || '')),
    browserOnline: online === null ? '' : String(online),
    documentVisibilityState: visibilityState,
    pageFocused,
    userAgent: typeof nav?.userAgent === 'string' ? nav.userAgent : '',
    platform: typeof nav?.platform === 'string' ? nav.platform : '',
    language: typeof nav?.language === 'string' ? nav.language : '',
    permissionsApiAvailable,
    mediaDevicesApiAvailable: mediaDevicesAvailable,
    connectionType: String(conn?.type || ''),
    effectiveConnectionType: String(conn?.effectiveType || ''),
    networkRttMs: Number(conn?.rtt || 0),
    networkDownlinkMbps: Number(conn?.downlink || 0),
    saveData: conn?.saveData === true,
    localTrackCount: localTracks.length,
    localAudioTracksCount: localAudioTracks.length,
    localAudioTracksEnabledCount: enabledLocalAudioTracks,
    localAudioTrackLabels: localAudioTracks.map((t) => String(t?.label || '')).join('|'),
    hasPeerConnection: Boolean(pc),
    pcConnectionState: pc?.connectionState || '',
    pcIceConnectionState: pc?.iceConnectionState || '',
    pcIceGatheringState: iceGatheringState,
    pcSignalingState: pc?.signalingState || '',
    pcCanTrickleIceCandidates: pc?.canTrickleIceCandidates === null || pc?.canTrickleIceCandidates === undefined
      ? ''
      : String(pc?.canTrickleIceCandidates),
    localDescriptionType: currentLocalDescType,
    remoteDescriptionType: currentRemoteDescType,
    pcTransceiversCount: transceiversCount,
    pcSendersCount: sendersCount,
    pcReceiversCount: receiversCount,
  };
}

async function sendCallDeliveryReport(call, eventType, eventCode, reason = '', extraFacts = {}) {
  if (!call || !authService || typeof authService.sendCallDeliveryReport !== 'function') return;
  try {
    const diagnostics = getCallDiagnosticsContext(call);
    const valueLine = buildCallFactsLine(call, {
      eventType: String(eventType || '').trim(),
      eventCode: String(eventCode || '').trim(),
      reason: String(reason || '').trim(),
      reportedAtMs: nowMs(),
      reportedAtIso: toIsoTs(nowMs()),
      ...diagnostics,
      ...extraFacts,
    });
    await authService.sendCallDeliveryReport({
      type: String(eventType || '').trim(),
      value: valueLine,
    });
  } catch {}
}

function cleanupTimers(call) {
  if (call.timers?.ack10s) clearTimeout(call.timers.ack10s);
  if (call.timers?.total35s) clearTimeout(call.timers.total35s);
  if (call.timers?.incoming20s) clearTimeout(call.timers.incoming20s);
  if (call.timers?.transportProbe) clearInterval(call.timers.transportProbe);
  call.timers.transportProbe = null;
}

async function flushPendingIceCandidates(call) {
  if (!call?.pc) return;
  const pending = Array.isArray(call.pendingRemoteIceCandidates) ? call.pendingRemoteIceCandidates : [];
  if (!pending.length) return;
  call.pendingRemoteIceCandidates = [];
  for (const candidate of pending) {
    try {
      await call.pc.addIceCandidate(new RTCIceCandidate(candidate));
      await emitDebug(call, 'info', 'ice_processed_from_queue', 'candidate added');
    } catch (error) {
      await emitDebug(call, 'warn', 'ice_process_failed_from_queue', toErrorText(error));
    }
  }
}

async function closeMedia(call) {
  const pc = call?.pc || null;
  try {
    const senders = pc?.getSenders?.() || [];
    senders.forEach((sender) => {
      try {
        if (typeof sender?.replaceTrack === 'function') sender.replaceTrack(null);
      } catch {}
    });
  } catch {}
  try {
    const transceivers = pc?.getTransceivers?.() || [];
    transceivers.forEach((tr) => {
      try { tr?.stop?.(); } catch {}
    });
  } catch {}
  try {
    call.localStream?.getTracks?.()?.forEach((track) => {
      try { track.enabled = false; } catch {}
      try { track.stop(); } catch {}
    });
  } catch {}
  try {
    if (call.remoteAudio) {
      try { call.remoteAudio.pause?.(); } catch {}
      try { call.remoteAudio.srcObject = null; } catch {}
      call.remoteAudio = null;
    }
  } catch {}
  try { pc?.close?.(); } catch {}
  call.pc = null;
  call.localStream = null;
  call.audioSenders = [];
  call.connectionRouteLabel = '';
  call.connectionRouteDetails = '';
  call.pendingRemoteIceCandidates = [];
}

function stopReconnectFlow(call) {
  if (!call?.timers) return;
  if (call.timers.reconnectStep) {
    clearTimeout(call.timers.reconnectStep);
    call.timers.reconnectStep = null;
  }
  if (call.timers.reconnectDeadline) {
    clearTimeout(call.timers.reconnectDeadline);
    call.timers.reconnectDeadline = null;
  }
  call.reconnectInProgress = false;
  call.reconnectAttempts = 0;
}

async function detectConnectionRoute(call) {
  const pc = call?.pc;
  if (!pc || typeof pc.getStats !== 'function') {
    return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
  }
  try {
    const stats = await pc.getStats();
    let selectedPair = null;

    stats.forEach((report) => {
      if (selectedPair) return;
      if (report.type !== 'transport') return;
      if (!report.selectedCandidatePairId) return;
      const pair = typeof stats.get === 'function' ? stats.get(report.selectedCandidatePairId) : null;
      if (pair) selectedPair = pair;
    });

    if (!selectedPair) {
      stats.forEach((report) => {
        if (selectedPair) return;
        if (report.type === 'candidate-pair' && report.selected) selectedPair = report;
      });
    }

    if (!selectedPair) {
      stats.forEach((report) => {
        if (selectedPair) return;
        if (report.type === 'candidate-pair' && report.nominated && report.state === 'succeeded') {
          selectedPair = report;
        }
      });
    }

    if (!selectedPair) return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };

    const local = selectedPair.localCandidateId && typeof stats.get === 'function'
      ? stats.get(selectedPair.localCandidateId)
      : null;
    const remote = selectedPair.remoteCandidateId && typeof stats.get === 'function'
      ? stats.get(selectedPair.remoteCandidateId)
      : null;

    const localType = String(local?.candidateType || '').trim().toLowerCase();
    const remoteType = String(remote?.candidateType || '').trim().toLowerCase();
    const localIp = String(local?.ip || local?.address || '');
    const remoteIp = String(remote?.ip || remote?.address || '');
    const localPort = String(local?.port || '');
    const remotePort = String(remote?.port || '');
    const relayProto = String(local?.relayProtocol || remote?.relayProtocol || '');
    const turnCandidateAddress = localType === 'relay'
      ? `${localIp}${localPort ? `:${localPort}` : ''}`
      : (remoteType === 'relay' ? `${remoteIp}${remotePort ? `:${remotePort}` : ''}` : '');
    const details = `local=${localType || '-'}(${localIp || '-'}${localPort ? `:${localPort}` : ''}); remote=${remoteType || '-'}(${remoteIp || '-'}${remotePort ? `:${remotePort}` : ''})`;

    if (localType === 'relay' || remoteType === 'relay') {
      const label = turnCandidateAddress
        ? `через TURN (${turnCandidateAddress})`
        : (relayProto ? `через TURN (${relayProto})` : 'через TURN');
      return { label, details, localIp, remoteIp, turnCandidateAddress };
    }
    if (localType || remoteType) {
      const sameLan = localIp && remoteIp && (
        (localIp.startsWith('10.') && remoteIp.startsWith('10.'))
        || (localIp.startsWith('192.168.') && remoteIp.startsWith('192.168.'))
        || (localIp.startsWith('172.16.') && remoteIp.startsWith('172.16.'))
      );
      return {
        label: sameLan ? 'напрямую в локальной сети' : 'напрямую через интернет',
        details,
        localIp,
        remoteIp,
        turnCandidateAddress: '',
      };
    }
    return { label: '', details, localIp, remoteIp, turnCandidateAddress: '' };
  } catch {
    return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
  }
}

function startTransportProbe(call) {
  if (!call?.timers || !call.pc) return;
  if (call.timers.transportProbe) {
    clearInterval(call.timers.transportProbe);
    call.timers.transportProbe = null;
  }

  const refresh = async () => {
    if (!calls.has(call.callId) || call.phase === 'ended') return;
    if (!call.pc || call.pc.connectionState !== 'connected') return;
    const route = await detectConnectionRoute(call);
    if (!route.label || route.label === call.connectionRouteLabel) return;
    call.connectionRouteLabel = route.label;
    call.connectionRouteDetails = route.details || '';
    setActiveStatus(call);
    await emitDebug(call, 'info', 'peer_connection_route', route.details || route.label);
  };

  void refresh();
  call.timers.transportProbe = window.setInterval(() => {
    void refresh();
  }, 4000);
}

function startReconnectFlow(call, reason = 'disconnected') {
  if (!call || call.phase === 'ended') return;
  if (!call.connectedAtMs) return;
  if (!call.pc) return;
  if (call.reconnectInProgress) return;

  call.reconnectInProgress = true;
  call.reconnectAttempts = 0;
  setStatus(call, 'Связь прервалась. Переподключаем…', 'reconnecting');
  void emitDebug(call, 'warn', 'peer_connection_reconnect_start', `reason=${reason}`);

  const maxAttempts = 6;
  const attemptDelayMs = 2500;
  const totalDeadlineMs = 17000;

  call.timers.reconnectDeadline = setTimeout(() => {
    if (!calls.has(call.callId) || call.phase === 'ended') return;
    if (call.pc?.connectionState === 'connected') return;
    stopReconnectFlow(call);
    void finalizeCall(call, { localReasonCode: 'error', debugReason: 'reconnect_timeout' });
  }, totalDeadlineMs);

  const runAttempt = async () => {
    if (!calls.has(call.callId) || call.phase === 'ended') return;
    if (!call.pc || call.pc.connectionState === 'connected') {
      stopReconnectFlow(call);
      return;
    }

    call.reconnectAttempts += 1;
    try {
      const offer = await call.pc.createOffer({ iceRestart: true });
      await call.pc.setLocalDescription(offer);
      await sendSignal(call, TYPES.OFFER, JSON.stringify(offer));
      await emitDebug(call, 'info', 'peer_connection_reconnect_offer_sent', `attempt=${call.reconnectAttempts}`);
    } catch (error) {
      await emitDebug(call, 'warn', 'peer_connection_reconnect_offer_failed', `attempt=${call.reconnectAttempts}; error=${toErrorText(error)}`);
    }

    if (call.pc?.connectionState === 'connected') {
      stopReconnectFlow(call);
      return;
    }
    if (call.reconnectAttempts >= maxAttempts) {
      stopReconnectFlow(call);
      await finalizeCall(call, { localReasonCode: 'error', debugReason: 'reconnect_attempts_exhausted' });
      return;
    }
    call.timers.reconnectStep = setTimeout(() => {
      void runAttempt();
    }, attemptDelayMs);
  };

  void runAttempt();
}

function pushCallSummary(call, summaryCode) {
  if (!call?.peerLogin) return;
  const outgoing = call.direction === 'out';
  const from = outgoing ? 'out' : 'in';

  if (summaryCode === 'busy') {
    const text = outgoing
      ? '[Звонок] Исходящий: не дозвонились (занято)'
      : `[Звонок] Пропущенный входящий от ${call.peerLogin} (занято)`;
    addSystemChatMessage(call.peerLogin, text, { from, kind: 'call-tech' });
    return;
  }
  if (summaryCode === 'no_answer') {
    const text = outgoing
      ? '[Звонок] Исходящий: не дозвонились (нет ответа)'
      : `[Звонок] Пропущенный входящий от ${call.peerLogin}`;
    addSystemChatMessage(call.peerLogin, text, { from, kind: 'call-tech' });
    return;
  }
  if (summaryCode === 'declined') {
    const text = outgoing
      ? '[Звонок] Исходящий: не дозвонились (отклонён)'
      : `[Звонок] Входящий звонок от ${call.peerLogin} отклонён`;
    addSystemChatMessage(call.peerLogin, text, { from, kind: 'call-tech' });
    return;
  }
  if (summaryCode === 'error') {
    const text = outgoing
      ? '[Звонок] Исходящий: не дозвонились (ошибка соединения)'
      : `[Звонок] Входящий звонок от ${call.peerLogin}: ошибка соединения`;
    addSystemChatMessage(call.peerLogin, text, { from, kind: 'call-tech' });
    return;
  }
  if (summaryCode === 'completed') {
    const duration = formatDuration(nowMs() - Number(call.connectedAtMs || call.startedAtMs || nowMs()));
    const label = outgoing ? 'Исходящий' : 'Входящий';
    addSystemChatMessage(call.peerLogin, `[Звонок] ${label}: разговор ${duration}`, {
      from,
      kind: 'call-tech',
    });
  }
}

async function finalizeCall(call, {
  localReasonCode = 'error',
  debugReason = '',
  notifyRemoteHangup = false,
} = {}) {
  if (!call) return;
  const diagnosticsBeforeClose = getCallDiagnosticsContext(call);
  cleanupTimers(call);
  stopReconnectFlow(call);
  stopTone();

  const shouldNotifyRemoteFailure =
    !notifyRemoteHangup
    && Boolean(call.remoteSessionId)
    && String(localReasonCode || '') !== 'completed'
    && String(debugReason || '') !== 'remote_hangup';

  if ((notifyRemoteHangup || shouldNotifyRemoteFailure) && call.remoteSessionId) {
    try {
      const dataValue = notifyRemoteHangup
        ? ''
        : `setup_failed:${String(localReasonCode || 'error')}:${String(debugReason || '').slice(0, 80)}`;
      await authService.callSignalToSession({
        toLogin: call.peerLogin,
        targetSessionId: call.remoteSessionId,
        callId: call.callId,
        type: TYPES.HANGUP,
        data: dataValue,
      });
    } catch {}
  }

  await closeMedia(call);
  if (String(localReasonCode || '') === 'completed') {
    await emitDebug(call, 'info', 'debug_connection_success', debugReason || 'completed');
  }
  if (debugReason) {
    await emitDebug(call, 'info', 'call_finalize', `${localReasonCode}:${debugReason}`);
  }

  const reasonText = debugReason || localReasonCode;
  if (String(localReasonCode || '') !== 'completed') {
    const failureStage = call.phase || '';
    const failureContext = {
      failureStage,
      connectedBeforeFailure: Boolean(call.connectedAtMs),
      ...diagnosticsBeforeClose,
    };
    if (call.direction === 'out') {
      await sendCallDeliveryReport(call, 'outgoing_failed', `outgoing_${localReasonCode}`, reasonText, failureContext);
    } else if (call.direction === 'in') {
      await sendCallDeliveryReport(call, 'incoming_failed', `incoming_${localReasonCode}`, reasonText, failureContext);
    }
    if (String(localReasonCode || '') === 'busy') {
      await sendCallDeliveryReport(call, 'call_busy', 'call_busy', reasonText, failureContext);
    } else if (String(localReasonCode || '') === 'declined') {
      await sendCallDeliveryReport(call, 'call_declined', 'call_declined', reasonText, failureContext);
    }
    if (String(localReasonCode || '') === 'error') {
      await sendCallDeliveryReport(call, 'unknown_error', 'call_unknown_error', reasonText, failureContext);
    }
  }

  pushCallSummary(call, localReasonCode);

  call.phase = 'ended';
  call.statusText = 'Звонок завершён';
  if (String(localReasonCode || '') === 'busy') {
    call.statusText = 'Пользователь занят';
  }
  notifyCallState();

  const finalHoldMs = String(localReasonCode || '') === 'busy' ? 2600 : 0;
  if (finalHoldMs > 0) {
    window.setTimeout(() => {
      calls.delete(call.callId);
      if (activeCallId === call.callId) {
        activeCallId = '';
      }
      notifyCallState();
    }, finalHoldMs);
    return;
  }

  calls.delete(call.callId);
  if (activeCallId === call.callId) {
    activeCallId = '';
  }
  notifyCallState();
}

async function emitDebug(call, level, message, details = '') {
  if (!call?.debugRunId || typeof debugReporter !== 'function') return;
  try {
    await debugReporter({
      runId: call.debugRunId,
      level,
      message,
      details,
    });
  } catch {}
}

async function sendSignal(call, type, data = '') {
  if (!call.remoteSessionId) return;
  try {
    await authService.callSignalToSession({
      toLogin: call.peerLogin,
      targetSessionId: call.remoteSessionId,
      callId: call.callId,
      type,
      data,
    });
    await emitDebug(call, 'info', `signal_sent_${type}`, `len=${String(data || '').length}`);
  } catch (error) {
    await emitDebug(call, 'error', `signal_send_failed_${type}`, toErrorText(error));
    throw error;
  }
}

async function ensurePeerConnection(call) {
  if (call.pc) return call.pc;

  const iceServers = await resolveIceServers(call);
  const pc = new RTCPeerConnection({
    iceServers,
  });

  if (call.debugMode && call.debugRole === 'initiator') {
    const dc = pc.createDataChannel('debug-ping');
    dc.onopen = () => {
      try { dc.send('ping'); } catch {}
      void emitDebug(call, 'info', 'debug_datachannel_open', 'sent ping');
    };
    dc.onmessage = (evt) => {
      void emitDebug(call, 'info', 'debug_datachannel_message', String(evt?.data || ''));
    };
  }

  pc.ondatachannel = (evt) => {
    const ch = evt?.channel;
    if (!ch) return;
    ch.onmessage = (msg) => {
      const incoming = String(msg?.data || '');
      void emitDebug(call, 'info', 'debug_datachannel_message_in', incoming);
      if (incoming === 'ping') {
        try { ch.send('pong'); } catch {}
      }
    };
  };

  pc.onicecandidate = async (event) => {
    if (!event.candidate || !call.remoteSessionId) return;
    try {
      await sendSignal(call, TYPES.ICE, JSON.stringify(event.candidate));
    } catch {}
  };

  pc.onconnectionstatechange = () => {
    const state = pc.connectionState;
    if (state === 'connected') {
      stopReconnectFlow(call);
      if (!call.connectedAtMs) {
        call.connectedAtMs = nowMs();
      }
      setActiveStatus(call);
      startTransportProbe(call);
      void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
      if (call.direction === 'out' && !call.connectionSuccessReported) {
        call.connectionSuccessReported = true;
        void (async () => {
          const route = await detectConnectionRoute(call);
          const candidateAnalytics = await collectIceCandidateAnalytics(call);
          if (route?.label) {
            call.connectionRouteLabel = route.label;
          }
          call.connectionRouteDetails = route?.details || '';
          await sendCallDeliveryReport(
            call,
            'call_connected',
            'call_connected_success',
            `connected:${route?.label || 'unknown_route'}`,
            {
              reportBy: 'initiator',
              routeLabel: route?.label || '',
              routeDetails: route?.details || '',
              localIp: route?.localIp || '',
              remoteIp: route?.remoteIp || '',
              turnCandidateAddress: route?.turnCandidateAddress || '',
              ...candidateAnalytics,
            },
          );
        })();
      }
      return;
    }
    if (state === 'failed') {
      const failedDetails = `failed;ice=${pc.iceConnectionState || ''};gather=${pc.iceGatheringState || ''};signal=${pc.signalingState || ''}`;
      if (call.connectedAtMs) {
        startReconnectFlow(call, 'failed');
        return;
      }
      void emitDebug(call, 'warn', 'peer_connection_closed', failedDetails);
      void finalizeCall(call, { localReasonCode: 'error', debugReason: failedDetails });
      return;
    }
    if (state === 'disconnected' && call.phase !== 'ended') {
      if (call.connectedAtMs) {
        startReconnectFlow(call, 'disconnected');
        return;
      }
      void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`);
      return;
    }
    if (state === 'closed' && call.phase !== 'ended') {
      void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`);
      if (call.connectedAtMs) {
        void finalizeCall(call, { localReasonCode: 'error', debugReason: state });
        return;
      }
      void finalizeCall(call, { localReasonCode: 'error', debugReason: state });
    }
  };

  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
    call.localStream = stream;
    call.audioSenders = [];
    call.connectionRouteLabel = '';
    call.connectionRouteDetails = '';
    stream.getTracks().forEach((track) => {
      track.enabled = track.kind === 'audio' ? !call.muted : true;
      const sender = pc.addTrack(track, stream);
      if (track.kind === 'audio') {
        call.audioSenders.push(sender);
      }
    });
  } catch (e) {
    setStatus(call, `Нет доступа к микрофону: ${e?.message || 'unknown'}`, 'failed');
    await emitDebug(call, 'warn', 'microphone_access_failed', toErrorText(e));
    throw e;
  }

  pc.ontrack = (evt) => {
    const audio = new Audio();
    audio.autoplay = true;
    audio.srcObject = evt.streams[0];
    call.remoteAudio = audio;
  };

  call.pc = pc;
  if (!Array.isArray(call.pendingRemoteIceCandidates)) {
    call.pendingRemoteIceCandidates = [];
  }
  return pc;
}

async function onAccept(call) {
  if (!call) return;
  if (call.initialOfferInProgress || call.initialOfferSent) {
    await emitDebug(call, 'warn', 'accept_duplicate_ignored', `phase=${call.phase || ''}`);
    return;
  }
  call.initialOfferInProgress = true;
  cleanupTimers(call);
  setStatus(call, 'Соединяем…', 'connecting');
  try {
    const pc = await ensurePeerConnection(call);
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    call.initialOfferSent = true;
    await sendSignal(call, TYPES.OFFER, JSON.stringify(offer));
    await emitDebug(call, 'info', 'offer_sent', 'offer created and sent');
  } finally {
    call.initialOfferInProgress = false;
  }
}

function ensureIncomingNotification(peerLogin) {
  if (typeof window === 'undefined') return;
  const text = `Вам звонит ${peerLogin}`;
  try {
    if ('Notification' in window && Notification.permission === 'granted') {
      new Notification('SHiNE: входящий звонок', { body: text });
    }
  } catch {}
  try {
    if ('vibrate' in navigator) {
      navigator.vibrate([180, 70, 180, 70, 260]);
    }
  } catch {}
}

function isIncomingCallPushFresh(payload) {
  const expiresAtMs = Number(payload?.expiresAtMs || 0);
  if (Number.isFinite(expiresAtMs) && expiresAtMs > 0 && Date.now() > expiresAtMs) {
    return false;
  }
  return true;
}

async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) {
  const callId = String(payload?.callId || '').trim();
  const fromLogin = String(payload?.fromLogin || '').trim();
  const fromSessionId = String(payload?.fromSessionId || '').trim();
  if (!callId || !fromLogin || !fromSessionId) return null;

  if (activeCallId && activeCallId !== callId) {
    try {
      await authService.callSignalToSession({
        toLogin: fromLogin,
        targetSessionId: fromSessionId,
        callId,
        type: TYPES.DECLINE_BUSY,
        data: 'busy',
      });
    } catch {}
    return null;
  }

  let call = getCall(callId);
  if (!call) {
    call = {
      callId,
      peerLogin: fromLogin,
      direction: 'in',
      phase: 'incoming',
      statusText: `Вам звонит ${fromLogin}`,
      remoteSessionId: fromSessionId,
      timers: {},
      startedAtMs: nowMs(),
      connectedAtMs: 0,
      pc: null,
      localStream: null,
      audioSenders: [],
      muted: false,
      connectionRouteLabel: '',
      reconnectInProgress: false,
      reconnectAttempts: 0,
      debugMode: false,
      debugRunId: '',
      debugRole: '',
      pendingRemoteIceCandidates: [],
      initialOfferInProgress: false,
      initialOfferSent: false,
    };
    calls.set(callId, call);
  } else if (!call.remoteSessionId && fromSessionId) {
    call.remoteSessionId = fromSessionId;
  }

  activeCallId = callId;
  setStatus(call, `Вам звонит ${fromLogin}`, 'incoming');
  ensureIncomingNotification(fromLogin);

  try {
    await sendSignal(call, TYPES.RINGING, `ringing:${source}`);
  } catch {}

  if (!call.timers.incoming20s) {
    call.timers.incoming20s = setTimeout(async () => {
      if (!calls.has(callId)) return;
      try {
        await sendSignal(call, TYPES.TIMEOUT, 'timeout_20s');
      } catch {}
      await finalizeCall(call, { localReasonCode: 'no_answer', debugReason: 'incoming_timeout_20s' });
    }, 20000);
  }
  return call;
}

export function setCallDebugReporter(fn) {
  debugReporter = typeof fn === 'function' ? fn : null;
}

export function subscribeCallState(listener) {
  if (typeof listener !== 'function') {
    return () => {};
  }
  callStateListeners.add(listener);
  try {
    listener(getCallStateSnapshot());
  } catch {}
  return () => {
    callStateListeners.delete(listener);
  };
}

export function getActiveCallState() {
  return getCallStateSnapshot();
}

async function applyMicState(call) {
  if (!call) return;
  const muted = Boolean(call.muted);
  const audioTracks = call.localStream?.getAudioTracks?.() || [];
  audioTracks.forEach((track) => {
    track.enabled = !muted;
  });

  const senders = Array.isArray(call.audioSenders) && call.audioSenders.length > 0
    ? call.audioSenders
    : (call.pc?.getSenders?.() || []);
  const sourceTrack = audioTracks[0] || null;

  for (const sender of senders) {
    if (!sender || typeof sender.replaceTrack !== 'function') continue;
    try {
      if (muted) {
        if (sender.track) {
          await sender.replaceTrack(null);
        }
      } else if (sourceTrack) {
        if (sender.track !== sourceTrack) {
          await sender.replaceTrack(sourceTrack);
        }
        sourceTrack.enabled = true;
      }
    } catch {}
  }
}

export async function setMicMuted(muted) {
  const call = getActiveCall();
  if (!call) return;
  call.muted = Boolean(muted);
  await applyMicState(call);
  notifyCallState();
}

export async function toggleMicMuted() {
  const call = getActiveCall();
  if (!call) return;
  await setMicMuted(!call.muted);
}

export async function startDebugConnectionAsResponder({ runId, callId, peerLogin, peerSessionId }) {
  const cleanCallId = String(callId || '').trim();
  const cleanPeerLogin = String(peerLogin || '').trim();
  const cleanPeerSessionId = String(peerSessionId || '').trim();
  if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;

  let call = getCall(cleanCallId);
  if (!call) {
    call = {
      callId: cleanCallId,
      peerLogin: cleanPeerLogin,
      direction: 'in',
      phase: 'incoming',
      statusText: 'Debug: responder ждёт offer',
      remoteSessionId: cleanPeerSessionId,
      timers: {},
      startedAtMs: nowMs(),
      connectedAtMs: 0,
      pc: null,
      localStream: null,
      audioSenders: [],
      muted: false,
      connectionRouteLabel: '',
      debugMode: true,
      debugRunId: String(runId || '').trim(),
      debugRole: 'responder',
      pendingRemoteIceCandidates: [],
      initialOfferInProgress: false,
      initialOfferSent: false,
    };
    calls.set(cleanCallId, call);
  }
  if (!Array.isArray(call.pendingRemoteIceCandidates)) call.pendingRemoteIceCandidates = [];
  if (typeof call.initialOfferInProgress !== 'boolean') call.initialOfferInProgress = false;
  if (typeof call.initialOfferSent !== 'boolean') call.initialOfferSent = false;

  activeCallId = cleanCallId;
  await emitDebug(call, 'info', 'debug_prepare_responder', `peerSessionId=${cleanPeerSessionId}`);
  setStatus(call, 'Debug: responder готов, ждём offer', 'incoming');
}

export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin, peerSessionId }) {
  const cleanCallId = String(callId || '').trim();
  const cleanPeerLogin = String(peerLogin || '').trim();
  const cleanPeerSessionId = String(peerSessionId || '').trim();
  if (!cleanCallId || !cleanPeerLogin || !cleanPeerSessionId) return;

  const call = {
    callId: cleanCallId,
    peerLogin: cleanPeerLogin,
    direction: 'out',
    phase: 'connecting',
    statusText: 'Debug: старт соединения',
    remoteSessionId: cleanPeerSessionId,
    timers: {},
    startedAtMs: nowMs(),
    connectedAtMs: 0,
    pc: null,
    localStream: null,
    audioSenders: [],
    muted: false,
    connectionRouteLabel: '',
    debugMode: true,
    debugRunId: String(runId || '').trim(),
    debugRole: 'initiator',
    pendingRemoteIceCandidates: [],
    initialOfferInProgress: false,
    initialOfferSent: false,
  };

  calls.set(cleanCallId, call);
  activeCallId = cleanCallId;
  notifyCallState();
  await emitDebug(call, 'info', 'debug_start_initiator', `peerSessionId=${cleanPeerSessionId}`);
  try {
    await onAccept(call);
  } catch (error) {
    await emitDebug(call, 'error', 'debug_initiator_start_failed', toErrorText(error));
    await finalizeCall(call, { localReasonCode: 'error', debugReason: toErrorText(error) });
  }
}

export async function startOutgoingCall(peerLogin) {
  const cleanPeer = String(peerLogin || '').trim();
  if (!cleanPeer) return;

  const active = getActiveCall();
  if (active) {
    throw new Error(`Уже есть активный звонок с ${active.peerLogin || 'другим пользователем'}`);
  }

  const callId = makeCallId();
  const preflightTimeoutMs = resolveCallPreflightTimeoutMs();
  const preflightOk = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: false });
  if (!preflightOk) {
    throw new Error('Сервер временно недоступен');
  }

  const call = {
    callId,
    peerLogin: cleanPeer,
    direction: 'out',
    phase: 'searching',
    statusText: 'Ищем пользователя…',
    remoteSessionId: '',
    timers: {},
    startedAtMs: nowMs(),
    connectedAtMs: 0,
    pc: null,
    localStream: null,
    audioSenders: [],
    muted: false,
    connectionRouteLabel: '',
    reconnectInProgress: false,
    reconnectAttempts: 0,
    debugMode: false,
    debugRunId: '',
    debugRole: '',
    pendingRemoteIceCandidates: [],
    initialOfferInProgress: false,
    initialOfferSent: false,
  };
  calls.set(callId, call);
  activeCallId = callId;
  setStatus(call, 'Ищем пользователя…', 'searching');

  call.timers.ack10s = setTimeout(() => {
    if (!calls.has(callId)) return;
    if (call.phase === 'searching') {
      void finalizeCall(call, { localReasonCode: 'no_answer', debugReason: 'no_ack_10s' });
    }
  }, 10000);

  call.timers.total35s = setTimeout(() => {
    if (!calls.has(callId)) return;
    if (!call.connectedAtMs) {
      void finalizeCall(call, { localReasonCode: 'no_answer', debugReason: 'total_timeout_35s' });
    }
  }, 35000);

  try {
    await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
  } catch (error) {
    const text = String(error?.message || '').toUpperCase();
    const isNotAuth = text.includes('NOT_AUTHENTICATED');
    if (isNotAuth) {
      const recovered = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: true });
      if (recovered) {
        try {
          await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
          return;
        } catch {}
      }
      await finalizeCall(call, { localReasonCode: 'error', debugReason: 'invite_failed:not_authenticated_after_retry' });
      throw new Error('Сервер временно недоступен');
    }
    await finalizeCall(call, { localReasonCode: 'error', debugReason: `invite_failed:${toErrorText(error)}` });
    throw error;
  }
}

export async function handleIncomingCallInvite(evt) {
  await handleIncomingInvitePayload(evt?.payload || {}, { source: 'ws' });
}

export async function acceptIncomingCall() {
  const call = getActiveCall();
  if (!call || call.direction !== 'in' || call.phase !== 'incoming') return;
  call.phase = 'connecting';
  setStatus(call, 'Соединяем…', 'connecting');
  cleanupTimers(call);
  await sendSignal(call, TYPES.ACCEPT, 'accept');
}

export async function declineIncomingCall() {
  const call = getActiveCall();
  if (!call || call.direction !== 'in' || call.phase !== 'incoming') return;
  try {
    await sendSignal(call, TYPES.DECLINE_BUSY, 'decline');
  } catch {}
  await finalizeCall(call, { localReasonCode: 'declined', debugReason: 'declined_by_user' });
}

export async function handleIncomingCallSignal(evt) {
  const payload = evt?.payload || {};
  const callId = String(payload.callId || '').trim();
  const fromLogin = String(payload.fromLogin || '').trim();
  const fromSessionId = String(payload.fromSessionId || '').trim();
  const type = Number(payload.type);
  const data = String(payload.data || '');
  if (!callId || !fromLogin || !Number.isFinite(type)) return;

  const call = getCall(callId);
  if (!call) return;
  if (call.direction === 'out') {
    if (type === TYPES.RINGING) {
      if (!call.remoteSessionId && fromSessionId) {
        call.remoteSessionId = fromSessionId;
      }
      if (call.remoteSessionId && fromSessionId && call.remoteSessionId !== fromSessionId) {
        await emitDebug(
          call,
          'info',
          'ringing_from_non_selected_session_ignored',
          `selected=${call.remoteSessionId}; from=${fromSessionId}`,
        );
        return;
      }
    } else if (type === TYPES.ACCEPT) {
      if (fromSessionId) {
        if (!call.remoteSessionId || !call.initialOfferSent) {
          call.remoteSessionId = fromSessionId;
        } else if (call.remoteSessionId !== fromSessionId) {
          await emitDebug(
            call,
            'warn',
            'accept_from_non_selected_session_ignored',
            `selected=${call.remoteSessionId}; from=${fromSessionId}`,
          );
          return;
        }
      }
    } else {
      if (call.remoteSessionId && fromSessionId && call.remoteSessionId !== fromSessionId) {
        await emitDebug(
          call,
          'info',
          'signal_from_non_selected_session_ignored',
          `type=${type}; selected=${call.remoteSessionId}; from=${fromSessionId}`,
        );
        return;
      }
      if (!call.remoteSessionId && fromSessionId) {
        call.remoteSessionId = fromSessionId;
      }
    }
  } else if (!call.remoteSessionId) {
    call.remoteSessionId = fromSessionId;
  }

  if (type === TYPES.RINGING) {
    if (call.direction === 'out' && call.phase === 'searching') {
      setStatus(call, 'Вызываем…', 'ringing');
    }
    return;
  }

  if (type === TYPES.ACCEPT) {
    if (call.direction !== 'out') {
      await emitDebug(call, 'warn', 'accept_ignored_for_non_outgoing_call', `direction=${call.direction || ''}`);
      return;
    }
    call.phase = 'connecting';
    setStatus(call, 'Соединяем…', 'connecting');
    await onAccept(call);
    return;
  }

  if (type === TYPES.DECLINE_BUSY) {
    const normalized = data.trim().toLowerCase();
    const isDeclined = normalized === 'decline' || normalized === 'declined';
    await finalizeCall(call, {
      localReasonCode: isDeclined ? 'declined' : 'busy',
      debugReason: isDeclined ? 'declined_by_remote' : 'busy_by_remote',
    });
    return;
  }

  if (type === TYPES.TIMEOUT) {
    await finalizeCall(call, { localReasonCode: 'no_answer', debugReason: 'remote_timeout' });
    return;
  }

  if (type === TYPES.HANGUP) {
    await finalizeCall(call, {
      localReasonCode: call.connectedAtMs ? 'completed' : 'no_answer',
      debugReason: 'remote_hangup',
    });
    return;
  }

  if (type === TYPES.OFFER) {
    try {
      const pc = await ensurePeerConnection(call);
      await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
      await flushPendingIceCandidates(call);
      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);
      await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
      setStatus(call, 'Соединяем…', 'connecting');
      await emitDebug(call, 'info', 'offer_processed', 'answer sent');
    } catch (error) {
      await emitDebug(call, 'error', 'offer_process_failed', toErrorText(error));
      await finalizeCall(call, { localReasonCode: 'error', debugReason: `offer_failed:${toErrorText(error)}` });
    }
    return;
  }

  if (type === TYPES.ANSWER) {
    try {
      if (call.direction !== 'out') {
        await emitDebug(call, 'warn', 'answer_ignored_for_non_outgoing_call', `direction=${call.direction || ''}`);
        return;
      }
      if (!call.pc) {
        await emitDebug(call, 'warn', 'answer_ignored_without_pc', 'no local peer connection');
        return;
      }
      const pc = call.pc;
      const localType = String(pc.localDescription?.type || '').trim().toLowerCase();
      if (localType !== 'offer') {
        await emitDebug(call, 'warn', 'answer_ignored_without_local_offer', `localType=${localType || 'none'}`);
        return;
      }
      if (pc.signalingState === 'stable' && pc.remoteDescription) {
        await emitDebug(call, 'warn', 'answer_duplicate_ignored', 'remote description already set');
        return;
      }
      await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
      await flushPendingIceCandidates(call);
      setStatus(call, 'Соединяем…', 'connecting');
      await emitDebug(call, 'info', 'answer_processed', 'remote description set');
    } catch (error) {
      await emitDebug(call, 'error', 'answer_process_failed', toErrorText(error));
      await finalizeCall(call, { localReasonCode: 'error', debugReason: `answer_failed:${toErrorText(error)}` });
    }
    return;
  }

  if (type === TYPES.ICE) {
    try {
      const candidate = JSON.parse(data);
      if (!call.pc) {
        if (!Array.isArray(call.pendingRemoteIceCandidates)) call.pendingRemoteIceCandidates = [];
        call.pendingRemoteIceCandidates.push(candidate);
        await emitDebug(call, 'info', 'ice_queued_before_pc', `queue=${call.pendingRemoteIceCandidates.length}`);
        return;
      }
      const pc = call.pc;
      if (!pc.remoteDescription) {
        if (!Array.isArray(call.pendingRemoteIceCandidates)) call.pendingRemoteIceCandidates = [];
        call.pendingRemoteIceCandidates.push(candidate);
        await emitDebug(call, 'info', 'ice_queued_before_remote_description', `queue=${call.pendingRemoteIceCandidates.length}`);
        return;
      }
      await pc.addIceCandidate(new RTCIceCandidate(candidate));
      await emitDebug(call, 'info', 'ice_processed', 'candidate added');
    } catch (error) {
      await emitDebug(call, 'error', 'ice_process_failed', toErrorText(error));
    }
  }
}

export async function hangupActiveCall() {
  if (!activeCallId) return;
  const call = getCall(activeCallId);
  await finalizeCall(call, {
    localReasonCode: call?.connectedAtMs ? 'completed' : 'no_answer',
    debugReason: 'hangup_by_user',
    notifyRemoteHangup: true,
  });
}

export async function handleIncomingCallPush(payload = {}) {
  if (!isIncomingCallPushFresh(payload)) return;
  await handleIncomingInvitePayload(payload, { source: 'push' });
}

export async function handleStopCallPush(payload = {}) {
  const callId = String(payload?.callId || '').trim();
  if (!callId) return;
  const call = getCall(callId);
  if (!call) return;
  const reason = String(payload?.reason || 'stop_call_push').trim() || 'stop_call_push';
  await finalizeCall(call, {
    localReasonCode: call.connectedAtMs ? 'completed' : 'no_answer',
    debugReason: `stop_call_push:${reason}`,
  });
}

export async function handleCallPushAction(action, payload = {}) {
  const normalized = String(action || '').trim().toLowerCase();
  if (normalized !== 'accept' && normalized !== 'decline') return;
  if (!isIncomingCallPushFresh(payload)) return;
  const timeoutMs = resolveCallPreflightTimeoutMs();
  const ok = await ensureSessionForCall({ timeoutMs, force: false });
  if (!ok) {
    throw new Error('Не удалось подключиться, вызов завершён');
  }
  await handleIncomingCallPush(payload);
  if (normalized === 'accept') {
    await acceptIncomingCall();
    return;
  }
  await declineIncomingCall();
}
