/// <reference lib="webworker" />

import { clientsClaim } from 'workbox-core';
import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';

clientsClaim();
cleanupOutdatedCaches();

const navigationFallbackDenylist = [
  /^\/api\/chat\/resources(?:\/|$)/,
  /^\/(?:public\/)?\.codex_chat(?:\/|$)/,
];

const rawManifest = self.__WB_MANIFEST;
const manifest = Array.isArray(rawManifest)
  ? rawManifest.filter((entry) => {
      const url = typeof entry === "string" ? entry : entry?.url;
      return typeof url === "string" && !/\.(?:png|webp)$/i.test(url);
    })
  : [];

if (Array.isArray(manifest) && manifest.length > 0) {
  precacheAndRoute(manifest);
  const navigationHandler = createHandlerBoundToURL('/index.html');
  registerRoute(new NavigationRoute(navigationHandler, { denylist: navigationFallbackDenylist }));
} else {
  registerRoute(
    new NavigationRoute(({ request }) => {
      return fetch(request);
    }, { denylist: navigationFallbackDenylist }),
  );
}

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

  if (event.data?.type === 'WEB_PUSH_SYNC_METADATA') {
    event.waitUntil(writeWebPushRegistrationMetadata(event.data?.payload));
  }

  if (event.data?.type === 'WEB_PUSH_CLEAR_METADATA') {
    event.waitUntil(clearWebPushRegistrationMetadata());
  }
});

const WEB_PUSH_METADATA_CACHE_NAME = 'ai-code-app-web-push-meta-v1';
const WEB_PUSH_METADATA_REQUEST_PATH = '/__web-push-registration-meta__';

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let index = 0; index < rawData.length; index += 1) {
    outputArray[index] = rawData.charCodeAt(index);
  }

  return outputArray;
}

function normalizeWebPushRegistrationMetadata(value) {
  if (!value || typeof value !== 'object') {
    return null;
  }

  const deviceId = normalizeNotificationValue(value.deviceId);
  const clientId = normalizeNotificationValue(value.clientId);
  const appOrigin = normalizeNotificationValue(value.appOrigin);
  const appDomain = normalizeNotificationValue(value.appDomain);
  const userAgent = normalizeNotificationValue(value.userAgent);
  const enabled = value.enabled !== false;

  if (!enabled || (!deviceId && !clientId)) {
    return null;
  }

  return {
    deviceId,
    clientId,
    appOrigin,
    appDomain,
    userAgent,
    enabled: true,
    updatedAt: normalizeNotificationValue(value.updatedAt),
  };
}

async function writeWebPushRegistrationMetadata(value) {
  const metadata = normalizeWebPushRegistrationMetadata(value);

  if (!metadata) {
    await clearWebPushRegistrationMetadata();
    return;
  }

  const cache = await caches.open(WEB_PUSH_METADATA_CACHE_NAME);
  const request = new Request(WEB_PUSH_METADATA_REQUEST_PATH);
  await cache.put(
    request,
    new Response(JSON.stringify(metadata), {
      headers: {
        'content-type': 'application/json',
      },
    }),
  );
}

async function readWebPushRegistrationMetadata() {
  try {
    const cache = await caches.open(WEB_PUSH_METADATA_CACHE_NAME);
    const response = await cache.match(new Request(WEB_PUSH_METADATA_REQUEST_PATH));

    if (!response) {
      return null;
    }

    return normalizeWebPushRegistrationMetadata(await response.json());
  } catch {
    return null;
  }
}

async function clearWebPushRegistrationMetadata() {
  try {
    const cache = await caches.open(WEB_PUSH_METADATA_CACHE_NAME);
    await cache.delete(new Request(WEB_PUSH_METADATA_REQUEST_PATH));
  } catch {
    // ignore cache cleanup failures
  }
}

function normalizeNotificationValue(value) {
  return typeof value === 'string' ? value.trim() : '';
}

function normalizeNotificationAliases(value) {
  if (!Array.isArray(value)) {
    return [];
  }

  return value.map((item) => normalizeNotificationValue(item)).filter(Boolean);
}

function appendNotificationOriginToBody(body, data) {
  const normalizedBody = normalizeNotificationValue(body);
  const appOrigin = normalizeNotificationValue(data?.appOrigin);
  const appDomain = normalizeNotificationValue(data?.appDomain);
  const originLabel = appOrigin || appDomain;

  if (!originLabel) {
    return normalizedBody;
  }

  const originLine = `origin: ${originLabel}`;

  if (normalizedBody.includes(originLine)) {
    return normalizedBody;
  }

  return normalizedBody ? `${normalizedBody}\n${originLine}` : originLine;
}

function shouldSuppressWhenVisible(payload) {
  const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
  return normalizeNotificationValue(data.suppressIfVisible).toLowerCase() === 'true';
}

function isChatNotificationPayload(payload) {
  const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
  const notificationCategory = normalizeNotificationValue(data.category);
  const notificationType = normalizeNotificationValue(data.type);
  const notificationThreadId = normalizeNotificationValue(payload?.threadId ?? data.threadId);

  return (
    notificationCategory === 'chat' ||
    notificationType.startsWith('chat') ||
    notificationThreadId.startsWith('chat:')
  );
}

function resolveChatNotificationSessionId(payload) {
  const data = payload?.data && typeof payload.data === 'object' ? payload.data : {};
  const directSessionId = normalizeNotificationValue(payload?.sessionId ?? data.sessionId);

  if (directSessionId) {
    return directSessionId;
  }

  const threadId = normalizeNotificationValue(payload?.threadId ?? data.threadId);

  if (threadId.startsWith('chat:')) {
    return threadId.slice('chat:'.length).trim();
  }

  return '';
}

function isChatLiveClientForSession(clientUrl, payloadSessionId) {
  const pathname = normalizeNotificationValue(clientUrl.pathname);
  const clientSessionId = normalizeNotificationValue(clientUrl.searchParams.get('sessionId'));

  if (pathname !== '/chat/live') {
    return false;
  }

  if (!payloadSessionId) {
    return true;
  }

  return clientSessionId === payloadSessionId;
}

function isMatchingForegroundChatClient(client, payload) {
  if (!client || typeof client.url !== 'string') {
    return false;
  }

  try {
    const clientUrl = new URL(client.url);

    if (clientUrl.origin !== self.location.origin) {
      return false;
    }

    if (client.visibilityState !== 'visible' || client.focused !== true) {
      return false;
    }

    return isChatLiveClientForSession(clientUrl, resolveChatNotificationSessionId(payload));
  } catch {
    return false;
  }
}

function shouldSuppressChatNotificationForVisibleApp(payload) {
  if (!isChatNotificationPayload(payload) || !shouldSuppressWhenVisible(payload)) {
    return Promise.resolve(false);
  }

  return self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) =>
    clientList.some((client) => isMatchingForegroundChatClient(client, payload)),
  );
}

function shouldCloseExistingNotification(notification, payload) {
  const data = payload.data ?? {};
  const notificationScope = normalizeNotificationValue(data.notificationScope);
  const notificationSource = normalizeNotificationValue(data.source);
  const notificationKey = normalizeNotificationValue(data.notificationKey);
  const notificationAliases = normalizeNotificationAliases(data.notificationAliases);
  const replaceExistingScope =
    data.replaceExistingScope === true || normalizeNotificationValue(data.replaceExistingScope).toLowerCase() === 'true';

  if (!notificationScope || (!replaceExistingScope && notificationScope !== 'automation')) {
    return false;
  }

  const existingData = notification.data && typeof notification.data === 'object' ? notification.data : {};
  const existingScope = normalizeNotificationValue(existingData.notificationScope);
  const existingSource = normalizeNotificationValue(existingData.source);
  const existingNotificationKey = normalizeNotificationValue(existingData.notificationKey);
  const existingTag = normalizeNotificationValue(notification.tag);
  const replaceTargets = new Set([
    notificationScope,
    notificationSource,
    notificationKey,
    ...notificationAliases,
  ].filter(Boolean));

  return (
    replaceTargets.has(existingScope) ||
    replaceTargets.has(existingSource) ||
    replaceTargets.has(existingNotificationKey) ||
    replaceTargets.has(existingTag)
  );
}

async function recoverWebPushSubscription() {
  const metadata = await readWebPushRegistrationMetadata();

  if (!metadata) {
    return;
  }

  const configResponse = await fetch('/api/notifications/webpush/config', {
    headers: {
      Accept: 'application/json',
    },
  });

  if (!configResponse.ok) {
    throw new Error(`web-push-config:${configResponse.status}`);
  }

  const config = await configResponse.json();

  if (!config?.enabled || !config?.publicKey) {
    return;
  }

  let subscription = null;

  try {
    subscription = await self.registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(String(config.publicKey)),
    });
  } catch {
    subscription = await self.registration.pushManager.getSubscription();
  }

  if (!subscription) {
    return;
  }

  const subscriptionJson = subscription.toJSON();
  const payload = {
    subscription: {
      endpoint: subscription.endpoint,
      expirationTime: subscription.expirationTime,
      keys: {
        p256dh: subscriptionJson.keys?.p256dh ?? '',
        auth: subscriptionJson.keys?.auth ?? '',
      },
    },
    deviceId: metadata.deviceId || undefined,
    clientId: metadata.clientId || undefined,
    userAgent: metadata.userAgent || '',
    appOrigin: metadata.appOrigin || self.location.origin,
    appDomain: metadata.appDomain || self.location.hostname,
    enabled: true,
  };

  await fetch('/api/notifications/subscriptions/web', {
    method: 'PUT',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  await writeWebPushRegistrationMetadata({
    ...metadata,
    updatedAt: new Date().toISOString(),
  });
}

self.addEventListener('push', (event) => {
  if (!event.data) {
    return;
  }

  let payload = {};

  try {
    payload = event.data.json();
  } catch {
    payload = {
      title: '알림',
      body: event.data.text(),
      data: {},
    };
  }

  const title = payload.title || 'AI Code App';
  const body = appendNotificationOriginToBody(payload.body || '새 알림이 도착했습니다.', payload.data ?? {});
  const notificationKey =
    payload.data?.notificationKey ||
    [payload.threadId ?? 'ai-code-app-notification', payload.data?.eventType ?? 'event', Date.now()].join(':');

  event.waitUntil(
    shouldSuppressChatNotificationForVisibleApp(payload).then((shouldSuppress) => {
      if (shouldSuppress) {
        return undefined;
      }

      return self.registration.getNotifications().then((notifications) => {
        notifications.filter((notification) => shouldCloseExistingNotification(notification, payload)).forEach((notification) => notification.close());

        return self.registration.showNotification(title, {
          body,
          data: payload.data ?? {},
          tag: notificationKey,
          badge: '/pwa-192x192.svg',
          icon: '/pwa-192x192.svg',
        });
      });
    }),
  );
});

self.addEventListener('pushsubscriptionchange', (event) => {
  event.waitUntil(recoverWebPushSubscription());
});

self.addEventListener('notificationclick', (event) => {
  const notificationData = event.notification.data ?? {};
  const notificationSessionId = typeof notificationData.sessionId === 'string' ? notificationData.sessionId.trim() : '';
  const notificationCategory = typeof notificationData.category === 'string' ? notificationData.category.trim() : '';
  const notificationType = typeof notificationData.type === 'string' ? notificationData.type.trim() : '';
  const notificationThreadId = typeof notificationData.threadId === 'string' ? notificationData.threadId.trim() : '';
  const isChatNotification =
    notificationCategory === 'chat' ||
    notificationType.startsWith('chat') ||
    notificationThreadId.startsWith('chat:');

  event.notification.close();
  let targetUrl;

  try {
    targetUrl = notificationData.targetUrl ? new URL(String(notificationData.targetUrl)) : new URL('/', self.location.origin);
  } catch {
    targetUrl = new URL('/', self.location.origin);
  }

  if (targetUrl.origin !== self.location.origin) {
    const fallbackUrl = new URL('/', self.location.origin);
    targetUrl.searchParams.forEach((value, key) => {
      fallbackUrl.searchParams.set(key, value);
    });
    targetUrl = fallbackUrl;
  }

  if (!targetUrl.searchParams.has('topMenu')) {
    targetUrl.searchParams.set('topMenu', notificationData.category === 'chat' ? 'chat' : 'plans');
  }

  if (isChatNotification) {
    targetUrl.pathname = '/chat/live';
    targetUrl.searchParams.set('topMenu', 'chat');
    targetUrl.searchParams.delete('chatView');
    targetUrl.searchParams.delete('runtimeRequestId');

    if (notificationSessionId) {
      targetUrl.searchParams.set('sessionId', notificationSessionId);
    }
  }

  if (notificationData.planId && !targetUrl.searchParams.has('planId')) {
    targetUrl.searchParams.set('planId', String(notificationData.planId));
  }

  if (notificationData.workId && !targetUrl.searchParams.has('workId')) {
    targetUrl.searchParams.set('workId', String(notificationData.workId));
  }

  event.waitUntil(
    Promise.all([
      self.registration.getNotifications().then((notifications) => {
        if (!notificationSessionId || !isChatNotification) {
          return;
        }

        notifications.forEach((notification) => {
          const candidateData = notification.data && typeof notification.data === 'object' ? notification.data : null;
          const candidateSessionId =
            candidateData && typeof candidateData.sessionId === 'string' ? candidateData.sessionId.trim() : '';
          const candidateCategory =
            candidateData && typeof candidateData.category === 'string' ? candidateData.category.trim() : '';
          const candidateType =
            candidateData && typeof candidateData.type === 'string' ? candidateData.type.trim() : '';
          const candidateThreadId =
            candidateData && typeof candidateData.threadId === 'string' ? candidateData.threadId.trim() : '';
          const isCandidateChatNotification =
            candidateCategory === 'chat' ||
            candidateType.startsWith('chat') ||
            candidateThreadId.startsWith('chat:');

          if (isCandidateChatNotification && candidateSessionId === notificationSessionId) {
            notification.close();
          }
        });
      }),
      self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
        const existingClient = clientList[0];

        if (existingClient) {
          existingClient.focus();

          try {
            const clientUrl = new URL(existingClient.url);
            if (clientUrl.origin === targetUrl.origin) {
              return existingClient.navigate(targetUrl.toString());
            }
          } catch {
            return existingClient.navigate(targetUrl.toString());
          }
        }

        return self.clients.openWindow(targetUrl.toString());
      }),
    ]),
  );
});
