// TimeMaster Kiosk ServiceWorker
// Signs all /api/v1/kiosk/ requests with Ed25519 credentials stored in IndexedDB.

const DB_NAME = 'kiosk-credentials';
const DB_VERSION = 1;
const STORE_NAME = 'credentials';
const CRED_KEY = 'active';
const CLIENT_VERSION = '1.0.0';

// ── Lifecycle ──────────────────────────────────────────────────────────────

self.addEventListener('install', (event) => {
  event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

// ── Fetch interception ─────────────────────────────────────────────────────

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (!url.pathname.startsWith('/api/v1/kiosk/')) return;
  event.respondWith(signAndFetch(event.request));
});

async function signAndFetch(request) {
  const creds = await getCredentials();
  if (!creds) {
    // No keypair stored yet – pass through unsigned (e.g. during setup)
    return fetch(request);
  }

  // Read body bytes for hashing (clone before consuming)
  let bodyBytes = new Uint8Array(0);
  if (request.body) {
    bodyBytes = new Uint8Array(await request.clone().arrayBuffer());
  }

  // SHA-256 of body as lowercase hex
  const bodyHashBuffer = await crypto.subtle.digest('SHA-256', bodyBytes);
  const bodyHash = Array.from(new Uint8Array(bodyHashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomUUID();

  const url = new URL(request.url);
  const path = url.pathname;
  const method = request.method.toUpperCase();

  // Signature message: "METHOD PATH TIMESTAMP NONCE BODYHASH"
  const message = `${method} ${path} ${timestamp} ${nonce} ${bodyHash}`;
  const messageBytes = new TextEncoder().encode(message);

  const signatureBuffer = await crypto.subtle.sign(
    'Ed25519',
    creds.privateKey,
    messageBytes
  );
  const signature = btoa(
    String.fromCharCode(...new Uint8Array(signatureBuffer))
  );

  const newHeaders = new Headers(request.headers);
  newHeaders.set('X-Kiosk-Key-Id', creds.deviceId);
  newHeaders.set('X-Kiosk-Timestamp', timestamp);
  newHeaders.set('X-Kiosk-Nonce', nonce);
  newHeaders.set('X-Kiosk-Signature', signature);
  newHeaders.set('X-Kiosk-Client-Version', CLIENT_VERSION);

  const newRequest = new Request(request, { headers: newHeaders });
  return fetch(newRequest);
}

// ── Message handler ────────────────────────────────────────────────────────
// Handles messages from the page via MessageChannel.

self.addEventListener('message', async (event) => {
  const { type, deviceId, port } = event.data;
  const replyPort = port || (event.ports && event.ports[0]);

  if (type === 'GENERATE_KEYPAIR') {
    try {
      // Generate non-extractable Ed25519 keypair
      const keyPair = await crypto.subtle.generateKey(
        'Ed25519',
        false, // non-extractable – private key stays inside SW
        ['sign']
      );

      // Export public key in SPKI format → PEM
      const spkiBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey);
      const publicKeyPem = spkiToPem(spkiBuffer);

      // Persist in IndexedDB
      await saveCredentials(deviceId, keyPair.privateKey);

      if (replyPort) {
        replyPort.postMessage({ success: true, publicKeyPem });
      }
    } catch (err) {
      if (replyPort) {
        replyPort.postMessage({ success: false, error: err.message });
      }
    }
    return;
  }

  if (type === 'CHECK_CREDENTIALS') {
    const creds = await getCredentials();
    if (replyPort) {
      replyPort.postMessage({ hasCredentials: !!creds, deviceId: creds?.deviceId ?? null });
    }
    return;
  }

  if (type === 'CLEAR_CREDENTIALS') {
    await clearCredentials();
    if (replyPort) {
      replyPort.postMessage({ success: true });
    }
    return;
  }
});

// ── IndexedDB helpers ──────────────────────────────────────────────────────

function openDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        db.createObjectStore(STORE_NAME, { keyPath: 'key' });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror  = () => reject(req.error);
  });
}

async function getCredentials() {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readonly');
    const store = tx.objectStore(STORE_NAME);
    const req = store.get(CRED_KEY);
    req.onsuccess = () => {
      if (!req.result) { resolve(null); return; }
      resolve({ deviceId: req.result.deviceId, privateKey: req.result.privateKey });
    };
    req.onerror = () => reject(req.error);
  });
}

async function saveCredentials(deviceId, privateKey) {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite');
    const store = tx.objectStore(STORE_NAME);
    const req = store.put({ key: CRED_KEY, deviceId, privateKey });
    req.onsuccess = () => resolve();
    req.onerror   = () => reject(req.error);
  });
}

async function clearCredentials() {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite');
    const store = tx.objectStore(STORE_NAME);
    const req = store.delete(CRED_KEY);
    req.onsuccess = () => resolve();
    req.onerror   = () => reject(req.error);
  });
}

// ── Utilities ──────────────────────────────────────────────────────────────

function spkiToPem(spkiBuffer) {
  const b64 = btoa(String.fromCharCode(...new Uint8Array(spkiBuffer)));
  const lines = b64.match(/.{1,64}/g).join('\n');
  return `-----BEGIN PUBLIC KEY-----\n${lines}\n-----END PUBLIC KEY-----`;
}
