'use strict';

/**
 * EduTrack Canteen Scanner — Service Worker
 * Handles: page caching, entitlement prefetch, offline IndexedDB queue, background sync.
 */

const SW_VERSION    = 'canteen-sw-v1';
const PAGE_CACHE    = SW_VERSION + '-pages';
const ENT_CACHE_PFX = 'canteen-entitlements-';
const DB_NAME       = 'canteen_offline';
const DB_VERSION    = 1;
const STORE_NAME    = 'pending_confirmations';

// Relative to SW location (/canteen/scanner_sw.js → /ajax/...)
const PREFETCH_PATH = '../ajax/canteen_prefetch.php';
const SYNC_PATH     = '../ajax/canteen_sync.php';

// ── Install ────────────────────────────────────────────────────────────────────
self.addEventListener('install', (e) => {
    e.waitUntil(
        caches.open(PAGE_CACHE)
            .then(cache => cache.addAll([
                './scanner.php',
                './scanner_login.php',
                'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css',
                'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css',
                'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js',
                'https://cdnjs.cloudflare.com/ajax/libs/jsQR/1.4.0/jsQR.min.js',
            ]).catch(() => {}))
            .then(() => self.skipWaiting())
    );
});

// ── Activate ───────────────────────────────────────────────────────────────────
self.addEventListener('activate', (e) => {
    e.waitUntil(
        caches.keys()
            .then(keys => Promise.all(
                keys.filter(k => k !== PAGE_CACHE && !k.startsWith(ENT_CACHE_PFX))
                    .map(k => caches.delete(k))
            ))
            .then(() => self.clients.claim())
    );
});

// ── Fetch — serve pages from cache when offline ────────────────────────────────
self.addEventListener('fetch', (e) => {
    const { request } = e;
    if (request.method !== 'GET') return;

    const url = new URL(request.url);

    // AJAX calls — don't cache, let them fail naturally (page JS handles offline)
    if (url.pathname.includes('/ajax/')) return;

    e.respondWith(
        fetch(request)
            .then(resp => {
                // Cache scanner pages
                if (resp.ok && (url.pathname.endsWith('scanner.php') ||
                                url.pathname.endsWith('scanner_login.php'))) {
                    const clone = resp.clone();
                    caches.open(PAGE_CACHE).then(c => c.put(request, clone));
                }
                return resp;
            })
            .catch(() => caches.match(request)
                .then(cached => cached || new Response(
                    '<h2 style="font-family:sans-serif;padding:2rem;color:#fff;background:#1a1a2e">Offline — loading cached scanner...</h2>',
                    { headers: { 'Content-Type': 'text/html' } }
                ))
            )
    );
});

// ── Message bus ────────────────────────────────────────────────────────────────
self.addEventListener('message', (e) => {
    if (!e.data) return;
    switch (e.data.type) {
        case 'PREFETCH':
            prefetchEntitlements(e.data.date || todayStr());
            break;
        case 'SYNC_NOW':
            syncOfflineQueue();
            break;
    }
});

// ── Background Sync API (when available) ──────────────────────────────────────
self.addEventListener('sync', (e) => {
    if (e.tag === 'canteen-confirmations') {
        e.waitUntil(syncOfflineQueue());
    }
});

// ── Helpers ────────────────────────────────────────────────────────────────────
function todayStr() {
    return new Date().toISOString().slice(0, 10);
}

async function prefetchEntitlements(date) {
    const cacheKey = ENT_CACHE_PFX + date;
    try {
        const resp = await fetch(PREFETCH_PATH, { credentials: 'same-origin' });
        if (!resp.ok) return;

        const data = await resp.json();
        if (!data.success || !Array.isArray(data.entries)) return;

        // Verify blob signature (tamper detection)
        const blobStr  = JSON.stringify(data.entries);
        const expected = await hmacSha256Base64(data.daily_key, blobStr);
        if (expected !== data.signature) {
            console.warn('[CanteenSW] Prefetch signature mismatch — discarding blob');
            return;
        }

        // Store in Cache API
        const cache = await caches.open(cacheKey);
        await cache.put(
            new Request(PREFETCH_PATH + '?date=' + date),
            new Response(JSON.stringify(data), {
                headers: { 'Content-Type': 'application/json', 'X-SW-Cached': '1' }
            })
        );

        // Notify all clients
        const clients = await self.clients.matchAll({ includeUncontrolled: true });
        clients.forEach(c => c.postMessage({
            type:  'PREFETCH_READY',
            date,
            count: data.entries.length,
        }));

    } catch (err) {
        console.warn('[CanteenSW] Prefetch error:', err);
    }
}

// Read cached entitlement blob for a given date
async function getCachedEntries(date) {
    const cacheKey = ENT_CACHE_PFX + date;
    try {
        const cache = await caches.open(cacheKey);
        const resp  = await cache.match(new Request(PREFETCH_PATH + '?date=' + date));
        if (!resp) return null;
        const data = await resp.json();
        return data.entries || null;
    } catch {
        return null;
    }
}

// ── IndexedDB helpers ──────────────────────────────────────────────────────────
function openDB() {
    return new Promise((resolve, reject) => {
        const req = indexedDB.open(DB_NAME, DB_VERSION);
        req.onupgradeneeded = (e) => {
            const db = e.target.result;
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                const store = db.createObjectStore(STORE_NAME, {
                    keyPath: 'id', autoIncrement: true
                });
                store.createIndex('synced', 'synced', { unique: false });
            }
        };
        req.onsuccess  = () => resolve(req.result);
        req.onerror    = () => reject(req.error);
    });
}

async function getPendingItems() {
    const db = await openDB();
    return new Promise((resolve, reject) => {
        const tx  = db.transaction(STORE_NAME, 'readonly');
        const req = tx.objectStore(STORE_NAME).index('synced').getAll(0);
        req.onsuccess = () => resolve(req.result);
        req.onerror   = () => reject(req.error);
    });
}

async function markItemsSynced(ids) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
        const tx    = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        ids.forEach(id => {
            const req = store.get(id);
            req.onsuccess = () => {
                const rec = req.result;
                if (rec) { rec.synced = 1; store.put(rec); }
            };
        });
        tx.oncomplete = () => resolve();
        tx.onerror    = () => reject(tx.error);
    });
}

// ── Sync offline queue to server ───────────────────────────────────────────────
async function syncOfflineQueue() {
    const pending = await getPendingItems();
    if (!pending.length) return;

    try {
        const resp = await fetch(SYNC_PATH, {
            method:      'POST',
            credentials: 'same-origin',
            headers:     { 'Content-Type': 'application/json' },
            body:        JSON.stringify({ confirmations: pending }),
        });

        if (!resp.ok) return;
        const data = await resp.json();
        if (!data.success) return;

        // Mark synced items in IndexedDB
        await markItemsSynced(pending.map(p => p.id));

        // Notify clients
        const clients = await self.clients.matchAll({ includeUncontrolled: true });
        clients.forEach(c => c.postMessage({
            type:   'SYNC_COMPLETE',
            synced: data.synced || 0,
        }));

    } catch (err) {
        console.warn('[CanteenSW] Sync error:', err);
    }
}

// ── Crypto helper — HMAC-SHA256 then base64 ────────────────────────────────────
async function hmacSha256Base64(keyHex, message) {
    const keyBytes = hexToBytes(keyHex);
    const msgBytes = new TextEncoder().encode(message);

    const cryptoKey = await crypto.subtle.importKey(
        'raw', keyBytes,
        { name: 'HMAC', hash: 'SHA-256' },
        false, ['sign']
    );
    const sigBuf = await crypto.subtle.sign('HMAC', cryptoKey, msgBytes);
    return btoa(String.fromCharCode(...new Uint8Array(sigBuf)));
}

function hexToBytes(hex) {
    const buf = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
        buf[i / 2] = parseInt(hex.slice(i, i + 2), 16);
    }
    return buf;
}
