/**
 * SecNote Crypto Module (secnote-crypto.js)
 *
 * Clientseitige Verschlüsselung mit AES-256-GCM über die node-forge-Bibliothek.
 * Funktioniert sowohl auf HTTP als auch HTTPS (kein Secure Context erforderlich,
 * da forge statt der Web Crypto API verwendet wird).
 *
 * Verschlüsselungskonzept:
 * - Jede Nachricht bekommt einen zufälligen 256-Bit AES-Schlüssel
 * - Der Schlüssel wird als Base64url-Fragment (#) in die URL eingebettet
 *   (Fragment wird NICHT an den Server gesendet!)
 * - Optional: Zusätzliche Passwort-Verschlüsselung mit PBKDF2 + AES-GCM (doppelte Schicht)
 *
 * Datenformat (ohne Passwort): IV(12 Bytes) + Ciphertext + AuthTag(16 Bytes)
 * Datenformat (mit Passwort):  Salt(16) + IV(12) + OuterCiphertext + OuterTag(16)
 *                              wobei OuterCiphertext = verschlüsseltes [IV(12) + InnerCiphertext + InnerTag(16)]
 */
const SecNoteCrypto = (function() {
    'use strict';

    // === KONSTANTEN ===============================================================

    var IV_LENGTH = 12;            // Initialisierungsvektor: 12 Bytes (96 Bit, empfohlen für GCM)
    var SALT_LENGTH = 16;          // Salt für PBKDF2: 16 Bytes (128 Bit)
    var KEY_LENGTH = 32;           // Schlüssellänge: 32 Bytes (256 Bit)
    var TAG_LENGTH = 16;           // GCM-Authentifizierungstag: 16 Bytes (128 Bit)
    var PBKDF2_ITERATIONS = 600000; // PBKDF2-Iterationen (OWASP-Empfehlung für SHA-256)

    // === KONVERTIERUNGSFUNKTIONEN (forge <-> Uint8Array) ==========================

    /**
     * Konvertiert ein Uint8Array in einen forge-kompatiblen Binary-String.
     * forge arbeitet intern mit Binary-Strings statt mit Typed Arrays.
     *
     * @param {Uint8Array} uint8arr - Byte-Array
     * @returns {string} Binary-String für forge-Funktionen
     */
    function toStr(uint8arr) {
        var s = '';
        for (var i = 0; i < uint8arr.length; i++) {
            s += String.fromCharCode(uint8arr[i]);
        }
        return s;
    }

    /**
     * Konvertiert einen forge Binary-String zurück in ein Uint8Array.
     *
     * @param {string} str - Binary-String von forge
     * @returns {Uint8Array} Byte-Array
     */
    function fromStr(str) {
        var arr = new Uint8Array(str.length);
        for (var i = 0; i < str.length; i++) {
            arr[i] = str.charCodeAt(i);
        }
        return arr;
    }

    /**
     * Erzeugt kryptographisch sichere Zufallsbytes über forge.
     *
     * @param {number} n - Anzahl der gewünschten Zufallsbytes
     * @returns {Uint8Array} Zufällige Bytes
     */
    function randomBytes(n) {
        return fromStr(forge.random.getBytesSync(n));
    }

    // === TEXT-HILFSFUNKTIONEN =====================================================

    /**
     * Konvertiert einen UTF-8-String in ein Uint8Array.
     * Verwendet die native TextEncoder-API.
     *
     * @param {string} str - Zu konvertierender Text
     * @returns {Uint8Array} UTF-8-kodierte Bytes
     */
    function stringToBytes(str) {
        return new TextEncoder().encode(str);
    }

    /**
     * Konvertiert ein Uint8Array zurück in einen UTF-8-String.
     * Verwendet die native TextDecoder-API.
     *
     * @param {Uint8Array} bytes - UTF-8-kodierte Bytes
     * @returns {string} Dekodierter Text
     */
    function bytesToString(bytes) {
        return new TextDecoder().decode(bytes);
    }

    // === BASE64URL-KODIERUNG (für URL-Fragment) ==================================

    /**
     * Kodiert Bytes als Base64url (URL-sicheres Base64 ohne Padding).
     * Wird für den Verschlüsselungsschlüssel im URL-Fragment (#) verwendet.
     *
     * Unterschied zu Standard-Base64:
     * - '+' wird zu '-'
     * - '/' wird zu '_'
     * - Padding '=' wird entfernt
     *
     * @param {Uint8Array|ArrayBuffer} bytes - Zu kodierende Bytes
     * @returns {string} Base64url-kodierter String
     */
    function base64urlEncode(bytes) {
        var arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
        var binary = '';
        for (var i = 0; i < arr.length; i++) {
            binary += String.fromCharCode(arr[i]);
        }
        return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    }

    /**
     * Dekodiert einen Base64url-String zurück in Bytes.
     * Stellt Padding wieder her und konvertiert URL-sichere Zeichen zurück.
     *
     * @param {string} str - Base64url-kodierter String
     * @returns {Uint8Array} Dekodierte Bytes
     */
    function base64urlDecode(str) {
        // URL-sichere Zeichen zurück in Standard-Base64 konvertieren
        var s = str.replace(/-/g, '+').replace(/_/g, '/');
        // Padding wiederherstellen
        while (s.length % 4 !== 0) { s += '='; }
        var binary = atob(s);
        var bytes = new Uint8Array(binary.length);
        for (var i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes;
    }

    // === STANDARD-BASE64-KODIERUNG (für Datenübertragung) ========================

    /**
     * Kodiert Bytes als Standard-Base64.
     * Wird für die verschlüsselten Nachrichtendaten verwendet,
     * die an den Server gesendet werden.
     *
     * @param {Uint8Array|ArrayBuffer} bytes - Zu kodierende Bytes
     * @returns {string} Base64-kodierter String
     */
    function base64Encode(bytes) {
        var arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
        var binary = '';
        for (var i = 0; i < arr.length; i++) {
            binary += String.fromCharCode(arr[i]);
        }
        return btoa(binary);
    }

    /**
     * Dekodiert einen Standard-Base64-String zurück in Bytes.
     *
     * @param {string} str - Base64-kodierter String
     * @returns {Uint8Array} Dekodierte Bytes
     */
    function base64Decode(str) {
        var binary = atob(str);
        var bytes = new Uint8Array(binary.length);
        for (var i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes;
    }

    // === KERN-KRYPTOGRAPHIE (AES-256-GCM über forge) =============================

    /**
     * Generiert einen neuen zufälligen AES-256-Schlüssel (32 Bytes).
     * Wird für jede neue Nachricht aufgerufen.
     *
     * @returns {Promise<Uint8Array>} 256-Bit AES-Schlüssel
     */
    async function generateKey() {
        return randomBytes(KEY_LENGTH);
    }

    /**
     * Exportiert einen AES-Schlüssel als Base64url-String.
     * Der exportierte Schlüssel wird als URL-Fragment (#) an die URL angehängt.
     *
     * @param {Uint8Array} key - AES-Schlüssel
     * @returns {Promise<string>} Base64url-kodierter Schlüssel
     */
    async function exportKeyToBase64url(key) {
        return base64urlEncode(key);
    }

    /**
     * Importiert einen AES-Schlüssel aus einem Base64url-String.
     * Wird beim Öffnen einer Nachricht verwendet, um den Schlüssel aus der URL zu lesen.
     *
     * @param {string} str - Base64url-kodierter Schlüssel
     * @returns {Promise<Uint8Array>} AES-Schlüssel
     */
    async function importKeyFromBase64url(str) {
        return base64urlDecode(str);
    }

    /**
     * Verschlüsselt Daten mit AES-256-GCM.
     *
     * Erzeugt einen zufälligen IV (12 Bytes) und verschlüsselt die Daten.
     * Das Ergebnis enthält IV + Ciphertext + AuthTag in einem zusammenhängenden Array.
     *
     * Ausgabeformat: [IV (12 Bytes)] [Ciphertext (variabel)] [AuthTag (16 Bytes)]
     *
     * @param {Uint8Array} plaintextBytes - Klartextdaten als Byte-Array
     * @param {Uint8Array} key - 256-Bit AES-Schlüssel
     * @returns {Promise<Uint8Array>} Verschlüsselte Daten (IV + Ciphertext + Tag)
     */
    async function encrypt(plaintextBytes, key) {
        // Zufälligen Initialisierungsvektor erzeugen (muss für jeden Verschlüsselungsvorgang einzigartig sein)
        var iv = randomBytes(IV_LENGTH);

        // AES-GCM Cipher erstellen und konfigurieren
        var cipher = forge.cipher.createCipher('AES-GCM', toStr(key));
        cipher.start({ iv: toStr(iv), tagLength: 128 });  // tagLength in Bits
        cipher.update(forge.util.createBuffer(toStr(plaintextBytes)));
        cipher.finish();

        // Ciphertext und Authentifizierungstag extrahieren
        var ciphertext = fromStr(cipher.output.getBytes());
        var tag = fromStr(cipher.mode.tag.getBytes());

        // Ergebnis zusammensetzen: IV(12) + Ciphertext + AuthTag(16)
        var result = new Uint8Array(IV_LENGTH + ciphertext.length + TAG_LENGTH);
        result.set(iv, 0);                                    // IV am Anfang
        result.set(ciphertext, IV_LENGTH);                     // Ciphertext in der Mitte
        result.set(tag, IV_LENGTH + ciphertext.length);        // AuthTag am Ende
        return result;
    }

    /**
     * Entschlüsselt AES-256-GCM-verschlüsselte Daten.
     *
     * Erwartet das Format: [IV (12 Bytes)] [Ciphertext] [AuthTag (16 Bytes)]
     * Prüft die Authentizität über den GCM-Tag und wirft einen Fehler bei Manipulation.
     *
     * @param {Uint8Array} ciphertextBytes - Verschlüsselte Daten (IV + Ciphertext + Tag)
     * @param {Uint8Array} key - 256-Bit AES-Schlüssel
     * @returns {Promise<Uint8Array>} Entschlüsselte Klartextdaten
     * @throws {Error} Bei fehlgeschlagener Authentifizierung (falscher Schlüssel oder Manipulation)
     */
    async function decrypt(ciphertextBytes, key) {
        // Bestandteile aus dem zusammengesetzten Array extrahieren
        var iv = ciphertextBytes.slice(0, IV_LENGTH);                                    // Ersten 12 Bytes = IV
        var tag = ciphertextBytes.slice(ciphertextBytes.length - TAG_LENGTH);             // Letzte 16 Bytes = AuthTag
        var data = ciphertextBytes.slice(IV_LENGTH, ciphertextBytes.length - TAG_LENGTH); // Mitte = Ciphertext

        // AES-GCM Decipher erstellen und konfigurieren
        var decipher = forge.cipher.createDecipher('AES-GCM', toStr(key));
        decipher.start({
            iv: toStr(iv),
            tag: forge.util.createBuffer(toStr(tag)),
            tagLength: 128  // tagLength in Bits
        });
        decipher.update(forge.util.createBuffer(toStr(data)));
        var pass = decipher.finish();

        // GCM-Authentifizierung prüfen (Integrität + Authentizität)
        if (!pass) {
            throw new Error('Decryption failed: authentication tag mismatch');
        }

        return fromStr(decipher.output.getBytes());
    }

    // === PASSWORT-BASIERTE VERSCHLÜSSELUNG =======================================

    /**
     * Leitet einen AES-256-Schlüssel aus einem Passwort mittels PBKDF2 ab.
     *
     * Verwendet SHA-256 als Hash-Funktion mit 600.000 Iterationen
     * (gemäß OWASP-Empfehlung 2023+). Der Salt verhindert Rainbow-Table-Angriffe.
     *
     * @param {string} password - Benutzerpasswort im Klartext
     * @param {Uint8Array} salt - Zufälliger Salt (16 Bytes)
     * @returns {Promise<Uint8Array>} 256-Bit abgeleiteter Schlüssel
     */
    async function deriveKeyFromPassword(password, salt) {
        return new Promise(function(resolve, reject) {
            forge.pkcs5.pbkdf2(
                password, toStr(salt),
                PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256',
                function(err, derivedKey) {
                    if (err) { reject(err); return; }
                    resolve(fromStr(derivedKey));
                }
            );
        });
    }

    /**
     * Verschlüsselt Daten mit doppelter Verschlüsselungsschicht (Schlüssel + Passwort).
     *
     * Ablauf:
     * 1. Innere Verschlüsselung: Klartext wird mit dem zufälligen Schlüssel verschlüsselt (AES-GCM)
     * 2. Äußere Verschlüsselung: Das Ergebnis wird mit einem PBKDF2-abgeleiteten Passwort-Schlüssel
     *    nochmals verschlüsselt (AES-GCM)
     *
     * Sicherheitsvorteil: Zum Entschlüsseln braucht man SOWOHL den URL-Schlüssel ALS AUCH das Passwort.
     *
     * Ausgabeformat: [Salt(16)] [IV(12)] [OuterCiphertext] [OuterTag(16)]
     *
     * @param {Uint8Array} plaintextBytes - Klartextdaten
     * @param {Uint8Array} key - Zufälliger AES-Schlüssel (aus URL)
     * @param {string} password - Benutzerpasswort
     * @returns {Promise<Uint8Array>} Doppelt verschlüsselte Daten
     */
    async function encryptWithPassword(plaintextBytes, key, password) {
        // Schritt 1: Innere Verschlüsselung mit dem zufälligen Schlüssel
        var innerCiphertext = await encrypt(plaintextBytes, key);

        // Schritt 2: Passwort-Schlüssel via PBKDF2 ableiten
        var salt = randomBytes(SALT_LENGTH);
        var pwKey = await deriveKeyFromPassword(password, salt);
        var iv = randomBytes(IV_LENGTH);

        // Schritt 3: Äußere Verschlüsselung mit dem Passwort-Schlüssel
        var cipher = forge.cipher.createCipher('AES-GCM', toStr(pwKey));
        cipher.start({ iv: toStr(iv), tagLength: 128 });
        cipher.update(forge.util.createBuffer(toStr(innerCiphertext)));
        cipher.finish();

        var outerCiphertext = fromStr(cipher.output.getBytes());
        var tag = fromStr(cipher.mode.tag.getBytes());

        // Ergebnis zusammensetzen: Salt(16) + IV(12) + OuterCiphertext + Tag(16)
        var result = new Uint8Array(SALT_LENGTH + IV_LENGTH + outerCiphertext.length + TAG_LENGTH);
        result.set(salt, 0);                                              // Salt am Anfang (für PBKDF2)
        result.set(iv, SALT_LENGTH);                                       // IV nach dem Salt
        result.set(outerCiphertext, SALT_LENGTH + IV_LENGTH);              // Ciphertext in der Mitte
        result.set(tag, SALT_LENGTH + IV_LENGTH + outerCiphertext.length); // Tag am Ende
        return result;
    }

    /**
     * Entschlüsselt doppelt verschlüsselte Daten (Schlüssel + Passwort).
     *
     * Ablauf (umgekehrt zu encryptWithPassword):
     * 1. Äußere Entschlüsselung: Mit PBKDF2-abgeleitetem Passwort-Schlüssel
     * 2. Innere Entschlüsselung: Mit dem zufälligen URL-Schlüssel
     *
     * @param {Uint8Array} ciphertextBytes - Doppelt verschlüsselte Daten
     * @param {Uint8Array} key - Zufälliger AES-Schlüssel (aus URL)
     * @param {string} password - Benutzerpasswort
     * @returns {Promise<Uint8Array>} Entschlüsselte Klartextdaten
     * @throws {Error} Bei falschem Passwort oder manipulierten Daten
     */
    async function decryptWithPassword(ciphertextBytes, key, password) {
        // Bestandteile aus dem zusammengesetzten Array extrahieren
        var salt = ciphertextBytes.slice(0, SALT_LENGTH);                                         // Salt für PBKDF2
        var iv = ciphertextBytes.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);                      // IV für äußere Entschlüsselung
        var tag = ciphertextBytes.slice(ciphertextBytes.length - TAG_LENGTH);                      // AuthTag
        var data = ciphertextBytes.slice(SALT_LENGTH + IV_LENGTH, ciphertextBytes.length - TAG_LENGTH); // Ciphertext

        // Schritt 1: Passwort-Schlüssel via PBKDF2 ableiten (gleicher Salt wie beim Verschlüsseln)
        var pwKey = await deriveKeyFromPassword(password, salt);

        // Schritt 2: Äußere Entschlüsselung mit dem Passwort-Schlüssel
        var decipher = forge.cipher.createDecipher('AES-GCM', toStr(pwKey));
        decipher.start({
            iv: toStr(iv),
            tag: forge.util.createBuffer(toStr(tag)),
            tagLength: 128
        });
        decipher.update(forge.util.createBuffer(toStr(data)));
        var pass = decipher.finish();

        // Authentifizierung prüfen (falsches Passwort = Tag-Mismatch)
        if (!pass) {
            throw new Error('Decryption failed: wrong password or corrupted data');
        }

        // Schritt 3: Innere Entschlüsselung mit dem zufälligen URL-Schlüssel
        var innerCiphertext = fromStr(decipher.output.getBytes());
        return decrypt(innerCiphertext, key);
    }

    // === ÖFFENTLICHE API =========================================================
    // Exportiert alle Funktionen, die von secnote-app.js benötigt werden
    return {
        generateKey,              // Neuen AES-256-Schlüssel erzeugen
        exportKeyToBase64url,     // Schlüssel für URL-Fragment exportieren
        importKeyFromBase64url,   // Schlüssel aus URL-Fragment importieren
        encrypt,                  // AES-GCM Verschlüsselung (ohne Passwort)
        decrypt,                  // AES-GCM Entschlüsselung (ohne Passwort)
        deriveKeyFromPassword,    // PBKDF2-Schlüsselableitung
        encryptWithPassword,      // Doppelte Verschlüsselung (Schlüssel + Passwort)
        decryptWithPassword,      // Doppelte Entschlüsselung (Schlüssel + Passwort)
        stringToBytes,            // String -> Uint8Array (UTF-8)
        bytesToString,            // Uint8Array -> String (UTF-8)
        base64urlEncode,          // Bytes -> Base64url (für URL-Fragment)
        base64urlDecode,          // Base64url -> Bytes
        base64Encode,             // Bytes -> Base64 (für Datenübertragung)
        base64Decode              // Base64 -> Bytes
    };
})();
