self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
self.__shineStoppedCalls = self.__shineStoppedCalls || new Map();

self.addEventListener('message', (event) => {
  const data = event?.data || {};
  if (data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

async function broadcastToClients(payload) {
  const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
  clients.forEach((client) => {
    client.postMessage({
      type: 'SHINE_WEB_PUSH_EVENT',
      payload,
    });
  });
}

async function broadcastCallActionToClients(action, payload) {
  const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
  clients.forEach((client) => {
    client.postMessage({
      type: 'SHINE_CALL_PUSH_ACTION',
      action,
      payload,
    });
  });
}

function rememberStoppedCall(callId, sentAtMs = 0) {
  if (!callId) return;
  const now = Date.now();
  const markAtMs = Number.isFinite(Number(sentAtMs)) ? Number(sentAtMs) : now;
  self.__shineStoppedCalls.set(callId, Math.max(now, markAtMs));
  const cutoff = now - 10 * 60 * 1000;
  for (const [id, ts] of self.__shineStoppedCalls.entries()) {
    if (Number(ts || 0) < cutoff) self.__shineStoppedCalls.delete(id);
  }
}

function isCallStopped(callId, sentAtMs = 0) {
  if (!callId) return false;
  const stoppedAt = Number(self.__shineStoppedCalls.get(callId) || 0);
  if (!stoppedAt) return false;
  const incomingAt = Number.isFinite(Number(sentAtMs)) ? Number(sentAtMs) : 0;
  return incomingAt <= 0 || incomingAt <= stoppedAt;
}

async function closeCallNotification(callId) {
  if (!callId) return;
  const list = await self.registration.getNotifications({ tag: callId });
  list.forEach((n) => {
    try { n.close(); } catch {}
  });
}

function decodePushJson(rawText) {
  try {
    if (!rawText) return {};
    return JSON.parse(rawText);
  } catch {
    return {};
  }
}

function encodeCallPushPayloadForUrl(payload) {
  try {
    return encodeURIComponent(JSON.stringify(payload || {}));
  } catch {
    return '';
  }
}

self.addEventListener('push', (event) => {
  let body = '';
  let rawText = '';
  let kind = '';
  let fromLogin = '';
  let title = '';
  try {
    if (event.data) {
      const text = event.data.text();
      rawText = text || '';
      const json = decodePushJson(rawText);
      kind = String(json.kind || '');
      title = String(json.title || '');
      body = String(json.text || '');
      fromLogin = String(json.fromLogin || '');
      if (!kind && rawText) {
        body = rawText || '';
      }
    }
  } catch {
    // ignore
  }

  const json = decodePushJson(rawText);
  const callId = String(json.callId || '').trim();
  const fromSessionId = String(json.fromSessionId || '').trim();
  const toLogin = String(json.toLogin || '').trim();
  const reason = String(json.reason || '').trim();
  const sentAtMs = Number(json.sentAtMs || 0);
  const expiresAtMs = Number(json.expiresAtMs || 0);
  const nowMs = Date.now();

  if (kind === 'stop_call' && callId) {
    rememberStoppedCall(callId, sentAtMs || nowMs);
  }

  const isExpiredIncomingCall = kind === 'incoming_call'
    && Number.isFinite(expiresAtMs)
    && expiresAtMs > 0
    && nowMs > expiresAtMs;
  const isIncomingCallAlreadyStopped = kind === 'incoming_call' && callId && isCallStopped(callId, sentAtMs || nowMs);

  const shouldNotify = (
    kind === 'new_message'
    || kind === 'test_push'
    || (kind === 'incoming_call' && !isExpiredIncomingCall && !isIncomingCallAlreadyStopped)
    || (!kind && body)
  );
  const notificationTitle = kind === 'test_push'
    ? (title || 'SHiNE: тестовый push')
    : (kind === 'incoming_call'
      ? 'SHiNE: входящий звонок'
      : 'SHiNE: входящее сообщение');

  const notifyPromise = shouldNotify
    ? self.registration.showNotification(notificationTitle, {
      body: body || (fromLogin ? `Вам пришло сообщение от ${fromLogin}` : 'Вам пришло сообщение'),
      tag: callId || (kind === 'test_push' ? 'shine-test-push' : 'shine-direct-message'),
      renotify: true,
      requireInteraction: kind === 'incoming_call',
      data: {
        kind,
        callId,
        fromLogin,
        fromSessionId,
        toLogin,
        sentAtMs,
        expiresAtMs,
        reason,
      },
      actions: kind === 'incoming_call'
        ? [
          { action: 'accept', title: 'Ответить' },
          { action: 'decline', title: 'Сбросить' },
        ]
        : [],
    })
    : Promise.resolve();

  const closeOnStopPromise = kind === 'stop_call' && callId
    ? closeCallNotification(callId)
    : Promise.resolve();

  event.waitUntil(Promise.all([
    notifyPromise,
    closeOnStopPromise,
    broadcastToClients({
      kind,
      body,
      fromLogin,
      fromSessionId,
      toLogin,
      callId,
      sentAtMs,
      expiresAtMs,
      reason,
      stale: isExpiredIncomingCall || isIncomingCallAlreadyStopped,
      rawText,
      receivedAt: nowMs,
    }),
  ]));
});

self.addEventListener('notificationclick', (event) => {
  event.notification?.close();
  const action = String(event?.action || '').trim().toLowerCase();
  const data = event?.notification?.data || {};
  const payload = {
    kind: String(data.kind || '').trim(),
    callId: String(data.callId || '').trim(),
    fromLogin: String(data.fromLogin || '').trim(),
    fromSessionId: String(data.fromSessionId || '').trim(),
    toLogin: String(data.toLogin || '').trim(),
    sentAtMs: Number(data.sentAtMs || 0),
    expiresAtMs: Number(data.expiresAtMs || 0),
    reason: String(data.reason || '').trim(),
  };

  event.waitUntil((async () => {
    if ((action === 'accept' || action === 'decline') && payload.callId) {
      await broadcastCallActionToClients(action, payload);
    }

    const allClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    const existing = allClients.find((client) => {
      try {
        return client.url.includes('/index.html') || client.url.endsWith('/');
      } catch {
        return false;
      }
    });

    const openUrlBase = './index.html';
    const encodedPayload = encodeCallPushPayloadForUrl(payload);
    const openUrl = (action === 'accept' || action === 'decline')
      ? `${openUrlBase}?callPushAction=${encodeURIComponent(action)}&callPushPayload=${encodedPayload}`
      : openUrlBase;

    if (existing) {
      try {
        if (action === 'accept' || action === 'decline') {
          existing.postMessage({
            type: 'SHINE_CALL_PUSH_ACTION',
            action,
            payload,
          });
        }
      } catch {}
      await existing.focus();
      return;
    }

    await self.clients.openWindow(openUrl);
  })());
});
