/// <reference lib="WebWorker" />
/// <reference types="vite/client" />

import { DAY } from '../utils/time';

const CACHE_NAME_PREFIX = 'mastodon-';
const CACHE_HEADER_TTL = 'x-timestamp';

export async function cacheRoot() {
  const cache = await openWebCache();
  const response = await fetch('/', {
    credentials: 'include',
    redirect: 'manual',
  });
  await cache.put('/', response);
}

export function handleFetch(event: FetchEvent) {
  const url = new URL(event.request.url);

  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    return;
  }

  if (url.pathname === '/auth/sign_out') {
    event.respondWith(handleLogout(event));
  } else if (/intl\/.*\.js$/.test(url.pathname)) {
    event.respondWith(cacheFirst({ event, name: 'locales' }));
  } else if (event.request.destination === 'font') {
    event.respondWith(cacheFirst({ event, name: 'fonts' }));
  } else if (event.request.destination === 'image') {
    event.respondWith(cacheFirst({ event, name: 'images', ttl: DAY * 7 }));
  }
}

async function cacheFirst({
  event,
  name,
  ttl = DAY * 30,
  max = 5,
}: {
  event: FetchEvent;
  name: string;
  ttl?: number;
  max?: number;
}) {
  const cache = await caches.open(`${CACHE_NAME_PREFIX}${name}`);
  const request = event.request;
  const cachedResponse = await cache.match(request);

  // Start expiring cache items while the process continues.
  void expireCachedItems({ name, ttl, max });

  if (cachedResponse) {
    // If we have a cached response, check the TTL header.
    const ttlHeader = Number.parseInt(
      cachedResponse.headers.get(CACHE_HEADER_TTL) ?? '0',
    );

    if (!ttlHeader || ttlHeader + ttl > Date.now()) {
      return cachedResponse;
    }
  }

  const networkResponse = await fetch(request);

  // For opaque responses, the status will be zero so we can't clone them.
  if (networkResponse.status !== 0) {
    // Clone request with a custom header to store timestamp.
    const cloneHeaders = new Headers(networkResponse.headers);
    cloneHeaders.set(CACHE_HEADER_TTL, Date.now().toString());

    const cloneResponse = new Response(networkResponse.clone().body, {
      headers: cloneHeaders,
      status: networkResponse.status,
      statusText: networkResponse.statusText,
    });

    await cache.put(request, cloneResponse);
  }

  return networkResponse;
}

export async function expireCachedItems({
  name,
  ttl = DAY * 30,
  max = 5,
}: {
  name: string;
  ttl?: number;
  max?: number;
}) {
  const cache = await caches.open(`${CACHE_NAME_PREFIX}${name}`);

  const keys = await cache.keys();
  const now = Date.now();
  const validKeys: { key: Request; timestamp: number }[] = [];

  for (const key of keys) {
    const cachedResponse = await cache.match(key);

    if (!cachedResponse) {
      await cache.delete(key);
      continue;
    }

    const timestamp = Number.parseInt(
      cachedResponse.headers.get(CACHE_HEADER_TTL) ?? '0',
    );

    if (!timestamp || timestamp + ttl > now) {
      validKeys.push({ key, timestamp: timestamp || Number.POSITIVE_INFINITY });
      continue;
    }

    await cache.delete(key);
  }

  if (validKeys.length <= max) {
    return;
  }

  const sortedValidKeys = validKeys.toSorted(
    ({ timestamp: a }, { timestamp: b }) => a - b,
  );
  await Promise.all(
    sortedValidKeys
      .slice(0, sortedValidKeys.length - max)
      .map(({ key }) => cache.delete(key)),
  );
}

function openWebCache() {
  return caches.open(`${CACHE_NAME_PREFIX}web`);
}

async function handleLogout(event: FetchEvent) {
  const response = await fetch(event.request);

  if (response.ok || response.type === 'opaqueredirect') {
    const cache = await openWebCache();
    await cache.delete('/');
  }

  return response;
}
