/* ============================================================
   BAN YARO — Service Worker
   Offline-Cache + Push Notifications + Tile-Cache
   ============================================================ */

const CACHE_VERSION = 'by-v660';
const CACHE_STATIC  = `${CACHE_VERSION}-static`;
const CACHE_TILES   = 'ban-yaro-tiles-v1';  // bleibt über SW-Updates erhalten
const CACHE_API     = 'ban-yaro-api-v1';    // API-Response-Cache

// index.html wird NICHT pre-gecacht (immer Network-First)
const STATIC_ASSETS = [
  '/css/design-system.css?v=545',
  '/css/layout.css?v=545',
  '/css/components.css?v=545',
  '/icons/phosphor.svg',
  '/js/api.js',
  '/js/ui.js',
  '/js/app.js',
  '/js/leaflet.markercluster.js',
  '/css/MarkerCluster.css',
  '/css/MarkerCluster.Default.css',
  '/manifest.json',
  '/icons/icon-192.png',
];

// ----------------------------------------------------------
// HILFSFUNKTIONEN
// ----------------------------------------------------------

// Fetch mit Timeout (verhindert Hängen bei schlechtem Netz)
function _fetchTimeout(request, ms = 8000) {
  const ctrl = new AbortController();
  const tid  = setTimeout(() => ctrl.abort(), ms);
  return fetch(request, { signal: ctrl.signal }).finally(() => clearTimeout(tid));
}

// IndexedDB Write-Queue (in SW-Kontext verfügbar)
function _queueDb() {
  return new Promise((res, rej) => {
    const req = indexedDB.open('by-offline-queue', 1);
    req.onupgradeneeded = e => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains('q'))
        db.createObjectStore('q', { keyPath: 'id', autoIncrement: true });
    };
    req.onsuccess = e => res(e.target.result);
    req.onerror   = e => rej(e.target.error);
  });
}
async function _queueAdd(entry) {
  const db = await _queueDb();
  return new Promise((res, rej) => {
    const tx = db.transaction('q', 'readwrite');
    const r  = tx.objectStore('q').add(entry);
    r.onsuccess = () => res(r.result);
    r.onerror   = () => rej(r.error);
  });
}
async function _queueAll() {
  const db = await _queueDb();
  return new Promise((res, rej) => {
    const tx = db.transaction('q', 'readonly');
    const r  = tx.objectStore('q').getAll();
    r.onsuccess = () => res(r.result);
    r.onerror   = () => rej(r.error);
  });
}
async function _queueDel(id) {
  const db = await _queueDb();
  return new Promise((res, rej) => {
    const tx = db.transaction('q', 'readwrite');
    const r  = tx.objectStore('q').delete(id);
    r.onsuccess = () => res();
    r.onerror   = () => rej(r.error);
  });
}

// Queue abarbeiten (bei Background-Sync oder manuellem Trigger)
async function _processQueue() {
  const items = await _queueAll();
  let synced = 0, failed = 0;
  for (const entry of items) {
    try {
      const resp = await fetch(entry.url, {
        method:  entry.method,
        headers: entry.headers,
        body:    entry.body || undefined,
      });
      // 2xx und 4xx aus Queue entfernen (4xx = Userfehler, nicht Netzwerk)
      if (resp.status < 500) { await _queueDel(entry.id); synced++; }
      else                   { failed++; }
    } catch { failed++; }
  }
  // Clients benachrichtigen
  const clients = await self.clients.matchAll({ includeUncontrolled: true });
  clients.forEach(c => c.postMessage({ type: 'QUEUE_PROCESSED', synced, failed, total: items.length }));
  return { synced, failed };
}

// Kann dieser Endpunkt offline in die Queue?
const _QUEUEABLE = [
  { re: /^\/api\/dogs\/\d+\/diary$/,               methods: ['POST'] },
  { re: /^\/api\/dogs\/\d+\/diary\/\d+$/,          methods: ['PATCH'] },
  { re: /^\/api\/dogs\/\d+\/health$/,              methods: ['POST'] },
  { re: /^\/api\/dogs\/\d+\/diary\/\d+\/media$/,   methods: ['POST'], skipBody: true },
  { re: /^\/api\/training\/sessions$/,             methods: ['POST'] },
  { re: /^\/api\/training\/progress$/,             methods: ['POST'] },
  { re: /^\/api\/poison$/,                         methods: ['POST'] },
];
function _isQueueable(pathname, method) {
  return _QUEUEABLE.some(q => q.methods.includes(method) && q.re.test(pathname));
}
function _isMediaUpload(request) {
  return (request.headers.get('Content-Type') || '').includes('multipart');
}

// Welche GET-API-Endpoints sollen gecacht werden?
const _CACHEABLE_GET = [
  /^\/api\/dogs(\/\d+)?$/,
  /^\/api\/dogs\/\d+\/welcome-dashboard/,
  /^\/api\/dogs\/\d+\/diary/,
  /^\/api\/dogs\/\d+\/health/,
  /^\/api\/training\/exercises/,
  /^\/api\/training\/progress/,
  /^\/api\/wiki\/rassen/,
  /^\/api\/dogs\/\d+\/diary\/stats/,
  // Drei Welten — offline-fähig
  /^\/api\/streak\/\d+/,
  /^\/api\/forum\/threads/,
  /^\/api\/weather$/,
  /^\/api\/passport\/\d+$/,
];
function _isCacheableGet(pathname) {
  return _CACHEABLE_GET.some(re => re.test(pathname));
}

// Cache-TTL: stabile Daten länger, dynamische kürzer
const _STABLE_GET = [/^\/api\/training\/exercises/, /^\/api\/wiki\/rassen/];
const _TTL_STABLE  = 60 * 60 * 1000;   // 1 Stunde
const _TTL_DEFAULT =  5 * 60 * 1000;   // 5 Minuten

const _cacheTs = new Map(); // pathname → timestamp (in-memory, ok bei SW-Neustart)

function _cacheTTL(pathname) {
  return _STABLE_GET.some(re => re.test(pathname)) ? _TTL_STABLE : _TTL_DEFAULT;
}
function _cacheStale(pathname) {
  const ts = _cacheTs.get(pathname);
  return !ts || (Date.now() - ts) > _cacheTTL(pathname);
}
function _cacheMark(pathname) {
  _cacheTs.set(pathname, Date.now());
}

// ----------------------------------------------------------
// INSTALL — App Shell cachen
// ----------------------------------------------------------
self.addEventListener('install', event => {
  self.skipWaiting(); // Sofort übernehmen — kein Warten auf Cache-Aufbau
  event.waitUntil(
    caches.open(CACHE_STATIC)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => caches.open(CACHE_API).then(c =>
        fetch('/api/training/exercises').then(r => { if (r.ok) c.put('/api/training/exercises', r); }).catch(() => {})
      ))
  );
});

// ----------------------------------------------------------
// ACTIVATE — alte Caches aufräumen (CACHE_TILES + CACHE_API behalten)
// ----------------------------------------------------------
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys
          .filter(k => k !== CACHE_STATIC && k !== CACHE_TILES && k !== CACHE_API)
          .map(k => caches.delete(k))
      ))
      .then(() => self.clients.claim())
  );
});

// ----------------------------------------------------------
// FETCH — Network-First für HTML, Cache-First für Assets, API mit Offline-Logik
// ----------------------------------------------------------
self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // API-Calls mit Timeout, Caching und Write-Queue
  if (url.pathname.startsWith('/api/')) {
    const method = event.request.method;

    // GET: Network-First mit Timeout, Cache-Fallback, im Hintergrund aktualisieren
    if (method === 'GET' && _isCacheableGet(url.pathname)) {
      event.respondWith((async () => {
        const cached = await caches.match(event.request);
        const stale  = _cacheStale(url.pathname);

        const networkPromise = _fetchTimeout(event.request.clone(), 8000)
          .then(resp => {
            if (resp.ok) {
              _cacheMark(url.pathname);
              caches.open(CACHE_API).then(c => c.put(event.request, resp.clone()));
            }
            return resp;
          })
          .catch(() => null);

        // Cache noch frisch → sofort zurückgeben, Netz im Hintergrund
        if (cached && !stale) {
          networkPromise.catch(() => {});
          return cached;
        }
        // Cache vorhanden aber abgelaufen → Netz zuerst, Cache als Fallback
        const fresh = await networkPromise;
        if (fresh) return fresh;
        if (cached) return cached;   // lieber veraltet als nichts
        return new Response(JSON.stringify({ detail: 'Offline — keine Daten im Cache.' }),
          { status: 503, headers: { 'Content-Type': 'application/json' } });
      })());
      return;
    }

    // Mutationen (POST/PATCH/PUT/DELETE): mit Timeout, bei Offline → Queue
    if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
      event.respondWith((async () => {
        try {
          return await _fetchTimeout(event.request.clone(), 10000);
        } catch {
          if (!_isQueueable(url.pathname, method))
            return new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }),
              { status: 503, headers: { 'Content-Type': 'application/json' } });
          if (_isMediaUpload(event.request))
            return new Response(JSON.stringify({ detail: 'Medien-Upload offline nicht möglich.' }),
              { status: 503, headers: { 'Content-Type': 'application/json' } });
          // In Queue speichern
          let body = '';
          try { body = await event.request.text(); } catch {}
          await _queueAdd({
            url:       url.pathname + url.search,
            method,
            headers:   { 'Content-Type': event.request.headers.get('Content-Type') || 'application/json' },
            body,
            createdAt: new Date().toISOString(),
          });
          try { await self.registration.sync.register('by-offline-mutations'); } catch {}
          return new Response(JSON.stringify({ _queued: true, detail: 'Offline gespeichert — wird synchronisiert wenn Verbindung besteht.' }),
            { status: 202, headers: { 'Content-Type': 'application/json' } });
        }
      })());
      return;
    }

    // Alle anderen API-Calls (GET ohne Caching): mit Timeout
    event.respondWith(
      _fetchTimeout(event.request, 8000).catch(() =>
        new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }),
          { status: 503, headers: { 'Content-Type': 'application/json' } })
      )
    );
    return;
  }

  // OSM-Kartenkacheln: eigener persistenter Cache
  if (url.hostname.endsWith('tile.openstreetmap.org')) {
    event.respondWith(
      caches.open(CACHE_TILES).then(cache =>
        cache.match(event.request).then(cached => {
          if (cached) return cached;
          return fetch(event.request).then(response => {
            if (response.ok) cache.put(event.request, response.clone());
            return response;
          });
        })
      ).catch(() => new Response('', { status: 503 }))
    );
    return;
  }

  // CSS, app.js + Seiten-Module: immer Network-First — damit iOS nie veraltete Versionen cached
  if (url.pathname.startsWith('/css/') || url.pathname.startsWith('/js/pages/')
      || url.pathname === '/js/app.js' || url.pathname === '/js/ui.js' || url.pathname === '/js/api.js') {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          if (response.ok) {
            const clone = response.clone();
            caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
          }
          return response;
        })
        .catch(() => caches.match(event.request))
    );
    return;
  }

  // Navigation (index.html): immer Network-First
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const clone = response.clone();
          caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
          return response;
        })
        .catch(() => caches.match('/'))
    );
    return;
  }

  // Statische Assets: Cache-First
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request)
        .then(response => {
          if (response.ok && event.request.method === 'GET') {
            const clone = response.clone();
            caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
          }
          return response;
        })
      )
      .catch(() => {
        if (event.request.mode === 'navigate') {
          return caches.match('/');
        }
      })
  );
});

// ----------------------------------------------------------
// BACKGROUND SYNC — Queue abarbeiten wenn Verbindung wiederhergestellt
// ----------------------------------------------------------
self.addEventListener('sync', event => {
  if (event.tag === 'by-offline-mutations') {
    event.waitUntil(_processQueue());
  }
});

// ----------------------------------------------------------
// MESSAGE — Tile-Vorausladung (Offline-Speicherung) + Queue-Steuerung
// ----------------------------------------------------------
self.addEventListener('message', event => {
  if (event.data?.type === 'PROCESS_QUEUE') {
    event.waitUntil(_processQueue());
    return;
  }
  if (event.data?.type === 'QUEUE_COUNT') {
    _queueAll().then(items =>
      event.source?.postMessage({ type: 'QUEUE_COUNT', count: items.length })
    );
    return;
  }

  if (event.data?.type !== 'CACHE_TILES') return;

  const urls   = event.data.urls || [];
  const source = event.source;
  let done     = 0;
  const total  = urls.length;

  if (total === 0) {
    source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: 0, total: 0 });
    return;
  }

  caches.open(CACHE_TILES).then(cache => {
    const queue = [...urls];

    function fetchBatch() {
      if (queue.length === 0) {
        source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: total, total });
        return;
      }
      const batch = queue.splice(0, 8);
      Promise.all(batch.map(url =>
        cache.match(url).then(cached => {
          if (cached) { done++; return; }
          return fetch(url, { mode: 'cors' })
            .then(r => { if (r.ok) cache.put(url, r); done++; })
            .catch(() => { done++; });
        })
      )).then(() => {
        source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done, total });
        setTimeout(fetchBatch, 30);
      });
    }

    fetchBatch();
  });
});

// ----------------------------------------------------------
// PUSH NOTIFICATIONS
// ----------------------------------------------------------
self.addEventListener('push', event => {
  if (!event.data) return;

  const data = event.data.json();

  const options = {
    body:    data.body    || '',
    icon:    data.icon    || '/icons/icon-192.png',
    badge:   data.badge   || '/icons/icon-192.png',
    tag:     data.tag     || 'ban-yaro',
    data:    data.data    || {},
    actions: data.actions || [],
    vibrate: data.vibrate || [100, 50, 100],
    requireInteraction: data.requireInteraction || false,
  };

  // Chat-Nachricht
  if (data.type === 'chat_message') {
    options.tag  = data.tag || `chat-${data.data?.conversation_id}`;
    options.renotify = true;
    options.actions  = [
      { action: 'reply', title: 'Antworten' },
      { action: 'dismiss', title: 'Schließen' },
    ];
  }

  // Freundschaftsanfrage
  if (data.type === 'friend_request') {
    options.tag     = 'friend-request';
    options.actions = [{ action: 'view', title: 'Anzeigen' }];
  }

  // Giftköder-Alarm: besondere Darstellung
  if (data.type === 'poison_alert') {
    options.tag               = 'poison-alert';
    options.requireInteraction = true;
    options.vibrate           = [200, 100, 200, 100, 200];
    options.actions = [
      { action: 'view',    title: 'Auf Karte zeigen' },
      { action: 'dismiss', title: 'Verstanden'       },
    ];
  }

  event.waitUntil(
    self.registration.showNotification(data.title || 'Ban Yaro', options)
  );

  // App informieren damit sie Nearby-Alerts neu prüft
  if (data.type === 'poison_alert' || data.type === 'lost_dog_alert') {
    self.clients.matchAll({ type: 'window' }).then(clients => {
      clients.forEach(c => c.postMessage({ type: 'CHECK_NEARBY_ALERTS' }));
    });
  }
});

// ----------------------------------------------------------
// NOTIFICATION CLICK
// ----------------------------------------------------------
self.addEventListener('notificationclick', event => {
  event.notification.close();

  const data   = event.notification.data;
  const action = event.action;

  let url = '/';

  if (action === 'view' || action === 'reply' || data?.page) {
    url = `/#${data.page || 'poison'}`;
    if (data.conversation_id) url += `?conversation_id=${data.conversation_id}`;
    else if (data.id) url += `?id=${data.id}`;
  }

  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then(windowClients => {
        for (const client of windowClients) {
          if (client.url.includes(self.location.origin)) {
            client.focus();
            client.navigate(url);
            return;
          }
        }
        return clients.openWindow(url);
      })
  );
});
