import crypto from 'crypto-js';
import pako from 'pako';
import { randomBytes } from "@noble/ciphers/webcrypto";
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import * as secp256k1 from '@noble/secp256k1';


self.onmessage = async (event: MessageEvent) => {
    const { id, type, payload } = event.data;

    try {
        let result;
        switch (type) {
            case 'encodeWithPassword':
                result = await encodeWithPassword(payload.password, payload.data);
                break;
            case 'decodeWithPassword':
                result = await decodeWithPassword(payload.password, payload.data);
                break;
            case 'encrypt':
                result = await encrypt(payload.data, payload.publicKey);
                break;
            case 'decrypt':
                result = await decrypt(payload.data, payload.privateKey);
                break;
            case 'chacha20Encrypt':
                result = await chacha20Encrypt(payload.data);
                break;
            case 'chacha20Decrypt':
                result = await chacha20Decrypt(payload.ciphertext, payload.nonce, payload.key);
                break;
            default:
                throw new Error(`Unknown action: ${type}`);
        }
        self.postMessage({ id, ok: true, data: result });
    } catch (error) {
        self.postMessage({ id, ok: false, error: String(error) });
    }
};


export const encrypt = async (data: string, publicKey: string) => {
    // Generate ephemeral key pair
    const ephemeralPrivateKey = secp256k1.utils.randomSecretKey()

    // Parse recipient's public key
    const recipientPublicKey = Buffer.from(publicKey, 'hex');
    
    // Compute shared secret using ECDH
    const sharedPoint = secp256k1.getSharedSecret(ephemeralPrivateKey, recipientPublicKey, false);
    const sharedKey = Buffer.from(sharedPoint.slice(1, 33)).toString('hex'); // Use x-coordinate
    
    // Encrypt data
    const iv = crypto.lib.WordArray.random(16);
    const keyBytes = crypto.enc.Hex.parse(sharedKey);
    const encrypted = crypto.AES.encrypt(data, keyBytes, { iv });
    
    return btoa(iv.toString(crypto.enc.Hex) + ':' + 
        encrypted.ciphertext.toString(crypto.enc.Hex) + ':' + 
        Buffer.from(ephemeralPrivateKey).toString('hex'));
}

export const decrypt = async (data: string, privateKey: string) => {
    const [ivHex, encryptedHex, ephemeralPrivateKeyHex] = atob(data).split(":");
    
    // Parse keys
    const ephemeralPrivateKey = Buffer.from(ephemeralPrivateKeyHex, 'hex');
    const privateKeyBytes = Buffer.from(privateKey, 'hex');
    
    // Compute ephemeral public key
    const ephemeralPublicKey = secp256k1.getPublicKey(ephemeralPrivateKey, false);
    
    // Compute shared secret using ECDH
    const sharedPoint = secp256k1.getSharedSecret(privateKeyBytes, ephemeralPublicKey, false);
    const sharedKey = Buffer.from(sharedPoint.slice(1, 33)).toString('hex'); // Use x-coordinate
    
    // Decrypt data
    const iv = crypto.enc.Hex.parse(ivHex);
    const keyBytes = crypto.enc.Hex.parse(sharedKey);
    
    const decrypted = crypto.AES.decrypt(crypto.lib.CipherParams.create({
        ciphertext: crypto.enc.Hex.parse(encryptedHex)
    }), keyBytes, { iv });
    
    let decryptedText = "";
    try {
        decryptedText = decrypted.toString(crypto.enc.Utf8);
    } catch(e) {
        console.info("pass: ", privateKey);
        console.info("data: ", data);
        console.error("Decryption error: ", e);
    }
    
    if (!decryptedText) {
        console.info("pass: ", privateKey);
        console.info("data: ", data);
        throw new Error("Decryption failed or resulted in an empty string.");
    }
    
    return decryptedText;
}

export const chacha20Encrypt = async (data : string) => {
    const key = randomBytes(32); 
    const nonce = randomBytes(24);
    const cipher = xchacha20poly1305(key, nonce);
    const ciphertext = cipher.encrypt(Buffer.from(data));
    return {
        ciphertext: Buffer.from(ciphertext).toString("hex"),
        nonce: Buffer.from(nonce).toString("hex"),
        key: Buffer.from(key).toString("hex"),
    }
}

export const chacha20Decrypt = async (ciphertext : string, nonce : string, key : string) => {
    const cipher = xchacha20poly1305(Buffer.from(key, "hex"), Buffer.from(nonce, "hex"));
    const decrypted = cipher.decrypt(Buffer.from(ciphertext, "hex"));
    return Buffer.from(decrypted).toString("utf-8");
}


// Utility to check if data is old format (base64 + ":")
function isOldFormat(data: string): boolean {
    try {
        const decoded = atob(data);
        return decoded.includes(':');
    } catch {
        return false;
    }
}

// Helper: chunk Uint8Array
function chunkArrayBuffer(buffer: Uint8Array, chunkSize: number): Uint8Array[] {
    const chunks: Uint8Array[] = [];
    for (let i = 0; i < buffer.length; i += chunkSize) {
        chunks.push(buffer.subarray(i, Math.min(i + chunkSize, buffer.length)));
    }
    return chunks;
}

// Helper: join base64 chunks with a marker
function joinChunksBase64(chunks: string[]): string {
    // Use "::" as a separator, and prefix with "CHNK:" to indicate chunked
    return 'CHNK:' + chunks.join('::');
}

// Helper: split chunked base64 string
function splitChunksBase64(data: string): string[] {
    // Remove "CHNK:" prefix and split by "::"
    return data.slice(5).split('::');
}

// New: compress, encrypt, base64 encode (not hex), with chunking for large payloads
export const encodeWithPassword = async (password: string, payload: any) => {
    // Convert payload to string and compress
    const input = typeof payload === 'string' ? payload : JSON.stringify(payload);
    const compressed = pako.deflate(input);

    // If compressed > 10MB, chunk it
    const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
    if (compressed.length > CHUNK_SIZE) {
        const chunks = chunkArrayBuffer(compressed, CHUNK_SIZE);
        const encryptedChunks: string[] = [];
        for (const chunk of chunks) {
            const key = crypto.PBKDF2(password, 'rosetta', {
                keySize: 256 / 32,
                iterations: 1000
            });
            const iv = crypto.lib.WordArray.random(16);
            const wordArray = crypto.lib.WordArray.create(chunk as any);
            const encrypted = crypto.AES.encrypt(wordArray, key, { iv });
            // iv:ciphertext base64
            const ivBase64 = crypto.enc.Base64.stringify(iv);
            const ctBase64 = crypto.enc.Base64.stringify(encrypted.ciphertext);
            encryptedChunks.push(ivBase64 + ':' + ctBase64);
        }
        return joinChunksBase64(encryptedChunks);
    } else {
        // Single chunk (as before)
        const key = crypto.PBKDF2(password, 'rosetta', {
            keySize: 256 / 32,
            iterations: 1000
        });
        const iv = crypto.lib.WordArray.random(16);
        const wordArray = crypto.lib.WordArray.create(compressed as any);
        const encrypted = crypto.AES.encrypt(wordArray, key, { iv });
        const ivBase64 = crypto.enc.Base64.stringify(iv);
        const ctBase64 = crypto.enc.Base64.stringify(encrypted.ciphertext);
        return ivBase64 + ':' + ctBase64;
    }
};

export const decodeWithPassword = async (password: string, data: any) => {
    // Handle old format (base64-encoded "iv:ciphertext")
    if (isOldFormat(data)) {
        const [ivHex, encryptedHex] = atob(data).split(':');
        const iv = crypto.enc.Hex.parse(ivHex);
        const key = crypto.PBKDF2(password, crypto.enc.Utf8.parse('rosetta'), {
            keySize: 256 / 32,
            iterations: 1000
        });
        const decrypted = crypto.AES.decrypt(
            crypto.lib.CipherParams.create({
                ciphertext: crypto.enc.Hex.parse(encryptedHex)
            }),
            key,
            { iv }
        );
        const decryptedUtf8 = decrypted.toString(crypto.enc.Utf8);
        if (!decryptedUtf8) throw new Error('Decryption failed or resulted in an empty string.');
        return decryptedUtf8;
    }

    // Check for chunked format
    if (typeof data === 'string' && data.startsWith('CHNK:')) {
        const chunkBase64s = splitChunksBase64(data);
        const decompressedParts: Uint8Array[] = [];
        for (const chunkBase64 of chunkBase64s) {
            const [ivBase64, ctBase64] = chunkBase64.split(':');
            if (!ivBase64 || !ctBase64) throw new Error('Invalid encrypted chunk format.');
            const iv = crypto.enc.Base64.parse(ivBase64);
            const key = crypto.PBKDF2(password, 'rosetta', {
                keySize: 256 / 32,
                iterations: 1000
            });
            const decrypted = crypto.AES.decrypt(
                crypto.lib.CipherParams.create({
                    ciphertext: crypto.enc.Base64.parse(ctBase64)
                }),
                key,
                { iv }
            );
            // Convert decrypted to Uint8Array
            const decryptedWords = decrypted.words;
            const decryptedSigBytes = decrypted.sigBytes;
            const bytes = new Uint8Array(decryptedSigBytes);
            for (let i = 0; i < decryptedSigBytes; ++i) {
                bytes[i] = (decryptedWords[(i / 4) | 0] >> (24 - 8 * (i % 4))) & 0xff;
            }
            decompressedParts.push(bytes);
        }
        // Concatenate all parts
        const totalLength = decompressedParts.reduce((sum, arr) => sum + arr.length, 0);
        const allBytes = new Uint8Array(totalLength);
        let offset = 0;
        for (const arr of decompressedParts) {
            allBytes.set(arr, offset);
            offset += arr.length;
        }
        let decompressed: string;
        try {
            decompressed = new TextDecoder().decode(pako.inflate(allBytes));
        } catch {
            throw new Error('Failed to decompress decrypted data.');
        }
        if (!decompressed) throw new Error('Decryption failed or resulted in an empty string.');
        return decompressed;
    }

    // New format: base64 "iv:ciphertext"
    const [ivBase64, ctBase64] = data.split(':');
    if (!ivBase64 || !ctBase64) throw new Error('Invalid encrypted data format.');

    const iv = crypto.enc.Base64.parse(ivBase64);
    const key = crypto.PBKDF2(password, 'rosetta', {
        keySize: 256 / 32,
        iterations: 1000
    });

    const decrypted = crypto.AES.decrypt(
        crypto.lib.CipherParams.create({
            ciphertext: crypto.enc.Base64.parse(ctBase64)
        }),
        key,
        { iv }
    );

    // Decompress
    const decryptedWords = decrypted.words;
    const decryptedSigBytes = decrypted.sigBytes;
    const decryptedBytes = new Uint8Array(decryptedSigBytes);
    for (let i = 0; i < decryptedSigBytes; ++i) {
        decryptedBytes[i] = (decryptedWords[(i / 4) | 0] >> (24 - 8 * (i % 4))) & 0xff;
    }

    let decompressed: string;
    try {
        decompressed = new TextDecoder().decode(pako.inflate(decryptedBytes));
    } catch {
        throw new Error('Failed to decompress decrypted data.');
    }

    if (!decompressed) {
        throw new Error('Decryption failed or resulted in an empty string.');
    }

    return decompressed;
};
