/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" /> 

class SimpleIndexedDB {
    /**
     * Creates an instance of SimpleIndexedDB and opens the database.
     * @param {string} dbName - The name of the database.
     * @param {string} storeName - The name of the object store.
     */
    constructor(dbName, storeName) {
        /** @private @type {string} - The name of the database. */
        this.dbName = dbName;
        /** @private @type {string} - The name of the object store. */
        this.storeName = storeName;
        /** @private @type {IDBDatabase | null} - The database instance. */
        this.db = null;
        this._openDB();
    }

    /**
     * Opens the database and sets it up if it doesn't exist.
     * @private
     */
    _openDB() {
        const request = indexedDB.open(this.dbName, 1);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(this.storeName)) {
                db.createObjectStore(this.storeName, { keyPath: 'key' });
            }
        };

        request.onsuccess = (event) => {
            this.db = event.target.result;
        };

        request.onerror = (event) => {
            console.error('Error opening IndexedDB:', event.target.error);
        };
    }

    /**
     * Adds a key-value pair to the database.
     * @param {string} key - The key to store.
     * @param {string} value - The value to store.
     * @returns {Promise<void>} - A promise that resolves when the key-value pair is added.
     */
    add(key, value) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([this.storeName], 'readwrite');
            const store = transaction.objectStore(this.storeName);
            const request = store.add({ key, value });

            request.onsuccess = () => {
                resolve();
            };

            request.onerror = (event) => {
                reject(event.target.error);
            };
        });
    }

    /**
     * Retrieves a value by key from the database.
     * @param {string} key - The key to retrieve.
     * @returns {Promise<string | null>} - A promise that resolves with the value or null if the key is not found.
     */
    get(key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([this.storeName], 'readonly');
            const store = transaction.objectStore(this.storeName);
            const request = store.get(key);

            request.onsuccess = (event) => {
                const result = event.target.result;
                resolve(result ? result.value : null);
            };

            request.onerror = (event) => {
                reject(event.target.error);
            };
        });
    }

    /**
     * Deletes a key-value pair by key from the database.
     * @param {string} key - The key to delete.
     * @returns {Promise<void>} - A promise that resolves when the key-value pair is deleted.
     */
    delete(key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([this.storeName], 'readwrite');
            const store = transaction.objectStore(this.storeName);
            const request = store.delete(key);

            request.onsuccess = () => {
                resolve();
            };

            request.onerror = (event) => {
                reject(event.target.error);
            };
        });
    }

    /**
     * Closes the database connection.
     */
    close() {
        if (this.db) {
            this.db.close();
            this.db = null;
        }
    }
}

const db = new SimpleIndexedDB('push-sw', 'deviceEncryption');

async function decryptAesCbc(encrypted, key, iv) {
    // Convert the base64 string to an ArrayBuffer
    const encryptedData = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0)).buffer;

    // Decrypt the data
    const decryptedData = await crypto.subtle.decrypt(
        { name: 'AES-CBC', iv: iv },
        key,
        encryptedData
    );

    // Convert the decrypted ArrayBuffer to a string
    const decoder = new TextDecoder('utf-8');
    const decryptedString = decoder.decode(decryptedData);

    // Parse the decrypted string as JSON
    const json = JSON.parse(decryptedString);
    return json;
}

/**@type {ServiceWorkerGlobalScope} sw */
const sw = self;

sw.addEventListener('install', async () => {
    sw.skipWaiting();
});

sw.addEventListener('push', event => {
    event.waitUntil((async () => {
        try {
            let json = event.data.json();

            if (json.encrypted) {
                const deviceEncryptionKey = await db.get('deviceEncryptionKey');
                const deviceEncryptionIv = await db.get('deviceEncryptionIv');
                const key = Uint8Array.from(atob(deviceEncryptionKey), c => c.charCodeAt(0)).buffer;
                const iv = Uint8Array.from(atob(deviceEncryptionIv), c => c.charCodeAt(0)).buffer;
                // Import the key for use with the Web Crypto API
                const importedKey = await crypto.subtle.importKey(
                    'raw',
                    key,
                    { name: 'AES-CBC' },
                    false,
                    ['decrypt']
                );

                const decrypted = await decryptAesCbc(json.encrypted, importedKey, iv);
                json = {
                    ...json,
                    ...decrypted,
                };
            }

            const { type } = json;
            if (type !== 'notification') {
                console.warn('unknown push', json);
                throw new Error('unknown push')
            }

            json.badge = new URL('img/icons/safari-pinned-tab.svg', self.location.href).toString();

            if (json.tag) {
                const notifications = await self.registration.getNotifications({ tag: json.tag });
                for (const notification of notifications) {
                    notification.close();
                }
            }
            await sw.registration.showNotification(json.title, json);
        }
        catch (e) {
            console.error('push error', e);
        }
    })());
});

sw.addEventListener('notificationclick', async event => {
    if (!event.action) {
        const { data } = event.notification;
        const { hash } = data;

        event.notification.close()

        const p = sw.clients.matchAll();;
        event.waitUntil(p)
        const [active] = await p;
        if (!active) {
            const url = new URL(sw.registration.scope);
            url.hash = hash;
            sw.clients.openWindow(url);
            return;
        }

        active.postMessage({
            type: 'hash',
            hash,
        });
        active.focus();
    }
    else {
        const { data } = event.notification;
        const { actionUrl, snoozeId } = data;

        const json = {
            snoozeId,
            actionId: event.action,
        }

        fetch(actionUrl, {
            method: 'POST',
            body: JSON.stringify(json),
            headers: {
                'Content-Type': 'application/json'
            },
        });
    }
});

