(function (app) {
    'use strict';

    const util = app.util = app.util || {};

    util.hasCrypto = function hasCrypto() {
        return !!(window.crypto && window.crypto.subtle && typeof window.crypto.subtle.digest === 'function');
    };

    util.canonicalStringify = function canonicalStringify(input) {
        if (input === null) {
            return 'null';
        }
        const type = typeof input;
        if (type === 'number' || type === 'boolean' || type === 'string') {
            return JSON.stringify(input);
        }
        if (Array.isArray(input)) {
            return '[' + input.map(util.canonicalStringify).join(',') + ']';
        }
        const keys = Object.keys(input).sort();
        return '{' + keys.map(function (key) {
            return JSON.stringify(key) + ':' + util.canonicalStringify(input[key]);
        }).join(',') + '}';
    };

    util.hashString = function hashString(str) {
        return zddc.crypto.sha256String(str);
    };

    util.arrayBufferToHex = function arrayBufferToHex(buffer) {
        return zddc.crypto.bytesToHex(buffer);
    };

    util.base64ToArrayBuffer = function base64ToArrayBuffer(base64Value) {
        if (!base64Value) {
            return new ArrayBuffer(0);
        }
        const binaryString = atob(base64Value);
        const length = binaryString.length;
        const bytes = new Uint8Array(length);
        for (let index = 0; index < length; index += 1) {
            bytes[index] = binaryString.charCodeAt(index);
        }
        return bytes.buffer;
    };

    util.arrayBufferToBase64 = function arrayBufferToBase64(buffer) {
        const bytes = new Uint8Array(buffer);
        let output = '';
        for (let index = 0; index < bytes.length; index += 1) {
            output += String.fromCharCode(bytes[index]);
        }
        return btoa(output);
    };

    // Revision comparison delegates to shared zddc library
    util.compareRevisionPriority = function compareRevisionPriority(aRevision, bRevision) {
        return zddc.compareRevisions(aRevision, bRevision);
    };

    util.compareFilesByTrackingRevision = function compareFilesByTrackingRevision(a, b) {
        const trackingA = (a && a.trackingNumber ? String(a.trackingNumber) : '').toLowerCase();
        const trackingB = (b && b.trackingNumber ? String(b.trackingNumber) : '').toLowerCase();
        if (trackingA < trackingB) {
            return -1;
        }
        if (trackingA > trackingB) {
            return 1;
        }

        const revisionCompare = util.compareRevisionPriority(a && a.revision, b && b.revision);
        if (revisionCompare !== 0) {
            return revisionCompare;
        }

        const extA = (a && a.extension ? String(a.extension) : '').toLowerCase();
        const extB = (b && b.extension ? String(b.extension) : '').toLowerCase();
        if (extA < extB) {
            return -1;
        }
        if (extA > extB) {
            return 1;
        }

        return 0;
    };

    util.canonicalizePublicJwk = function canonicalizePublicJwk(pk) {
        if (!pk) {
            return { kty: 'EC', crv: 'P-256', x: '', y: '' };
        }
        return {
            kty: pk.kty || 'EC',
            crv: pk.crv || 'P-256',
            x: pk.x || '',
            y: pk.y || ''
        };
    };

    util.publicKeyFingerprint = async function publicKeyFingerprint(pk) {
        try {
            if (!pk) {
                return '';
            }
            if (!util.hasCrypto()) {
                return null;
            }
            const canonical = util.canonicalizePublicJwk(pk);
            const canonicalStr = util.canonicalStringify(canonical);
            const hash = await util.hashString(canonicalStr);
            return (hash || '').slice(0, 12);
        } catch (err) {
            console.error('[transmittal] publicKeyFingerprint error', err);
            return '';
        }
    };

    util.hashFile = function hashFile(file, onProgress) {
        return zddc.crypto.sha256File(file, onProgress);
    };

    util.escapeHtml = function escapeHtml(value) {
        return String(value || '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    };

    util.escapeHtmlAttribute = function escapeHtmlAttribute(value) {
        return String(value || '')
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;');
    };

    util.downloadBlob = function downloadBlob(filename, contents, mime) {
        var blob = (contents instanceof Blob)
            ? contents
            : new Blob([contents], { type: mime || 'application/octet-stream' });
        var anchor = document.createElement('a');
        anchor.href = URL.createObjectURL(blob);
        anchor.download = filename;
        document.body.appendChild(anchor);
        anchor.click();
        setTimeout(function () {
            URL.revokeObjectURL(anchor.href);
            anchor.remove();
        }, 0);
    };

    util.createEmptyData = function createEmptyData(date) {
        return {
            envelope: {
                version: 1,
                digestAlgorithm: app.constants.digestAlgorithm,
                digest: '',
                digestedAt: '',
                signatureAlgorithm: app.constants.signatureAlgorithm,
                signatures: []
            },
            payload: {
                version: 1,
                type: 'Transmittal',
                title: '',
                client: '',
                project: '',
                projectNumber: '',
                date: date || '',
                trackingNumber: '',
                from: '',
                to: '',
                purpose: '',
                responseDue: '',
                subject: '',
                remarks: '',
                files: []
            },
            presentation: {
                leftLogo: '',
                rightLogo: '',
                theme: 'default',
                customCss: ''
            }
        };
    };

    util.fetchTrustedTime = function fetchTrustedTime() {
        return new Date().toISOString();
    };

    util.formatISOWithTZ = function formatISOWithTZ(isoStr) {
        if (!isoStr) { return 'Unknown'; }
        var d = new Date(isoStr);
        if (isNaN(d.getTime())) { return isoStr; }
        try {
            return d.toLocaleDateString('en-US', {
                weekday: 'long',
                year: 'numeric',
                month: 'long',
                day: 'numeric',
                hour: 'numeric',
                minute: '2-digit',
                timeZoneName: 'short'
            });
        } catch (_) {
            return d.toLocaleString();
        }
    };

    util.signEnvelope = async function signEnvelope(envelope, jwk) {
        if (!jwk || jwk.kty !== 'EC') {
            throw new Error('A valid EC private key (JWK) is required to sign.');
        }
        var envelopeToSign = {
            version: envelope.version || 1,
            digestAlgorithm: envelope.digestAlgorithm || app.constants.digestAlgorithm,
            digest: envelope.digest,
            digestedAt: envelope.digestedAt,
            signatureAlgorithm: envelope.signatureAlgorithm || app.constants.signatureAlgorithm
        };
        var envelopeStr = util.canonicalStringify(envelopeToSign);
        var key = await window.crypto.subtle.importKey(
            'jwk',
            jwk,
            { name: 'ECDSA', namedCurve: 'P-256' },
            false,
            ['sign']
        );
        var raw = await window.crypto.subtle.sign(
            { name: 'ECDSA', hash: { name: 'SHA-256' } },
            key,
            new TextEncoder().encode(envelopeStr)
        );
        return {
            signature: util.arrayBufferToBase64(raw),
            signedAt: util.fetchTrustedTime()
        };
    };


    util.formatShortHash = function formatShortHash(hex) {
        const normalized = (hex || '').trim();
        if (!normalized) {
            return '';
        }
        if (normalized.length <= 20) {
            return normalized;
        }
        return normalized.slice(0, 12) + '\u2026' + normalized.slice(-8);
    };

    util.formatShortFileHash = function formatShortFileHash(hex) {
        const normalized = (hex || '').trim();
        if (!normalized) {
            return '';
        }
        if (normalized.length <= 12) {
            return normalized;
        }
        return normalized.slice(0, 6) + '\u2026' + normalized.slice(-5);
    };

    util.formatFileSize = function formatFileSize(bytes) {
        const num = Number(bytes) || 0;
        if (num === 0) {
            return '0 B';
        }
        if (num < 1024) {
            return num + ' B';
        }
        if (num < 1024 * 1024) {
            return (num / 1024).toFixed(1) + ' KB';
        }
        if (num < 1024 * 1024 * 1024) {
            return (num / (1024 * 1024)).toFixed(1) + ' MB';
        }
        return (num / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
    };

    util.fileToImage = function fileToImage(file) {
        return new Promise(function (resolve, reject) {
            const url = URL.createObjectURL(file);
            const img = new Image();
            img.onload = function () {
                URL.revokeObjectURL(url);
                resolve(img);
            };
            img.onerror = function (err) {
                URL.revokeObjectURL(url);
                reject(err);
            };
            img.src = url;
        });
    };

    util.imageFileToPngDataUrl = async function imageFileToPngDataUrl(file, maxWidth, maxHeight) {
        const img = await util.fileToImage(file);
        const scale = Math.min(1, maxWidth / img.width, maxHeight / img.height);
        const w = Math.round(img.width * scale);
        const h = Math.round(img.height * scale);
        const canvas = document.createElement('canvas');
        canvas.width = w;
        canvas.height = h;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, w, h);
        return canvas.toDataURL('image/png');
    };

    // ── ZDDC Signing Key utilities ─────────────────────────
    var KEY_FORMAT = 'zddc-signing-key-v1';
    var KDF_ITERATIONS = 100000;

    function deriveWrappingKey(password, salt) {
        var enc = new TextEncoder();
        return window.crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
            .then(function (baseKey) {
                return window.crypto.subtle.deriveKey(
                    { name: 'PBKDF2', salt: salt, iterations: KDF_ITERATIONS, hash: 'SHA-256' },
                    baseKey,
                    { name: 'AES-GCM', length: 256 },
                    false,
                    ['encrypt', 'decrypt']
                );
            });
    }

    util.encryptPrivateKey = async function encryptPrivateKey(jwk, password, publicFingerprint) {
        var plaintext = new TextEncoder().encode(JSON.stringify(jwk));
        var salt = window.crypto.getRandomValues(new Uint8Array(16));
        var iv = window.crypto.getRandomValues(new Uint8Array(12));
        var wrappingKey = await deriveWrappingKey(password, salt);
        var ciphertext = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, wrappingKey, plaintext);
        return {
            format: KEY_FORMAT,
            publicFingerprint: publicFingerprint || '',
            encrypted: true,
            kdf: 'PBKDF2-SHA256',
            iterations: KDF_ITERATIONS,
            salt: util.arrayBufferToBase64(salt),
            iv: util.arrayBufferToBase64(iv),
            ciphertext: util.arrayBufferToBase64(ciphertext)
        };
    };

    util.decryptPrivateKey = async function decryptPrivateKey(keyData, password) {
        var salt = util.base64ToArrayBuffer(keyData.salt);
        var iv = util.base64ToArrayBuffer(keyData.iv);
        var ciphertext = util.base64ToArrayBuffer(keyData.ciphertext);
        var wrappingKey = await deriveWrappingKey(password, salt);
        var plaintext = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, wrappingKey, ciphertext);
        return JSON.parse(new TextDecoder().decode(plaintext));
    };

    util.wrapKeyFile = function wrapKeyFile(jwk, publicFingerprint) {
        return {
            format: KEY_FORMAT,
            publicFingerprint: publicFingerprint || '',
            encrypted: false,
            key: jwk
        };
    };

    util.loadKeyFile = async function loadKeyFile(text, promptForPassword) {
        var data = JSON.parse(text);
        // ZDDC key format
        if (data.format === KEY_FORMAT) {
            if (data.encrypted) {
                if (typeof promptForPassword !== 'function') {
                    throw new Error('Password required but no prompt available.');
                }
                var password = await promptForPassword(data.publicFingerprint);
                if (!password && password !== '') { return null; } // user cancelled
                return util.decryptPrivateKey(data, password);
            }
            return data.key;
        }
        throw new Error('Unrecognized key file format. Expected a .zddc-key file.');
    };

    util.cloneDocumentHtml = function cloneDocumentHtml() {
        // Temporarily remove inline scripts from the LIVE DOM so that
        // outerHTML produces a clean string with zero script bodies.
        // We never create a clone via innerHTML because the HTML parser
        // would corrupt the JSON block if base64 data contains a close-script tag.
        var scripts = document.querySelectorAll('script:not([src])');
        var saved = [];
        for (var i = 0; i < scripts.length; i++) {
            var el = scripts[i];
            saved.push({
                el: el,
                parent: el.parentNode,
                next: el.nextSibling,
                body: el.textContent || '',
                attrs: ''
            });
            for (var a = 0; a < el.attributes.length; a++) {
                var attr = el.attributes[a];
                saved[i].attrs += ' ' + attr.name + '="' +
                    attr.value.replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '"';
            }
            el.parentNode.removeChild(el);
        }

        var html = document.documentElement.outerHTML;

        // Restore every script element to the live DOM immediately
        for (var r = 0; r < saved.length; r++) {
            saved[r].parent.insertBefore(saved[r].el, saved[r].next);
        }

        // Build each script tag as a safe string and insert before </body>
        var scriptStrings = '';
        for (var j = 0; j < saved.length; j++) {
            var s = saved[j];
            var safeBody = s.body;
            var scriptType = (s.el.getAttribute('type') || '').toLowerCase();
            if (scriptType === 'application/json') {
                // Re-serialize from parsed data so we control the output.
                // \u003c is valid JSON; JSON.parse converts it back to <
                var jsonData = app.json.parse();
                safeBody = JSON.stringify(jsonData, null, 2).replace(/</g, '\\u003c');
            }
            scriptStrings += '\n<script' + s.attrs + '>' + safeBody +
                '</' + 'script>';
        }

        // Insert scripts before closing </body>
        var bodyClose = html.lastIndexOf('</' + 'body>');
        if (bodyClose !== -1) {
            html = html.substring(0, bodyClose) + scriptStrings + '\n' +
                html.substring(bodyClose);
        } else {
            html += scriptStrings;
        }

        return '<!DOCTYPE html>\n' + html;
    };

    /**
     * Fetch the current page's own source HTML and replace only the
     * transmittal-data JSON block with the supplied data object.
     *
     * This is the preferred save mechanism for drafts because it produces
     * an exact copy of the source file with only the data changed, rather
     * than a DOM snapshot that may contain stale or mutated content.
     *
     * @param {object} jsonData - The data object to embed as JSON.
     * @returns {Promise<string>} The patched HTML string.
     * @throws {Error} If the fetch fails or the JSON block is not found.
     */
    util.fetchAndPatchHtml = async function fetchAndPatchHtml(jsonData) {
        var response = await fetch(location.href, { cache: 'no-cache' });
        if (!response.ok) {
            throw new Error('fetch failed with status ' + response.status);
        }
        var html = await response.text();
        // \u003c is valid JSON; JSON.parse converts it back to <
        var jsonStr = JSON.stringify(jsonData, null, 2).replace(/</g, '\\u003c');
        // Replace the transmittal-data script body. The non-greedy [\s\S]*? stops
        // at the first close-script tag, which is safe because < is escaped above.
        var patched = html.replace(
            new RegExp(
                '(<script\\b[^>]*\\bid\\s*=\\s*["\']transmittal-data["\'][^>]*>)[\\s\\S]*?(<\\/' + 'script>)',
                'i'
            ),
            '$1\n' + jsonStr + '\n$2'
        );
        if (patched === html) {
            throw new Error('transmittal-data script block not found in fetched HTML');
        }
        return patched;
    };
})(window.transmittalApp);
