import PostalMime from "postal-mime";
import { sanitizeEmailHTML } from "./sanitize-email-html";
/**
 * Email loading/parsing helpers for `limel-file-viewer`.
 *
 * Parses an RFC 5322 / MIME email message (commonly stored as a `.eml` file)
 * and returns a simplified `Email` view-model.
 */
/**
 * Fetches and parses an email message.
 *
 * - Prefers `email.html` if present, otherwise falls back to `email.text`.
 * - Attempts to resolve inline images referenced via `cid:` by replacing
 *   `<img src="cid:...">` with `data:` URLs generated from inline attachments.
 *
 * @param url - URL to an email message, usually ending in `.eml`.
 * @returns A simplified `Email` object for rendering.
 */
export async function loadEmail(url) {
    const buffer = await fetchEmailBuffer(url);
    const email = await parseEmail(url, buffer);
    const parsedEmail = {
        subject: email.subject || undefined,
        from: formatAddress(email.from),
        to: formatAddresses(email.to),
        cc: formatAddresses(email.cc),
        date: getRawHeader(email, 'date') || email.date || undefined,
    };
    const { attachments, cidUrlById } = extractAttachments(email);
    if (attachments.length > 0) {
        parsedEmail.attachments = attachments;
    }
    await applyBody(parsedEmail, email, cidUrlById);
    return parsedEmail;
}
async function fetchEmailBuffer(url) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`Failed to fetch email (${response.status} ${response.statusText})`);
        }
        return await response.arrayBuffer();
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new Error(`Failed to load email from ${url}: ${message}`);
    }
}
async function parseEmail(url, buffer) {
    try {
        return await PostalMime.parse(buffer, {
            attachmentEncoding: 'arraybuffer',
        });
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new Error(`Failed to parse email from ${url}: ${message}`);
    }
}
function getRawHeader(email, name) {
    const header = (email.headers || []).find((h) => h.key === name.toLowerCase());
    return (header === null || header === void 0 ? void 0 : header.value) || undefined;
}
function extractAttachments(email) {
    const attachments = [];
    const cidUrlById = new Map();
    for (const attachment of email.attachments || []) {
        const contentId = normalizeContentId(attachment.contentId);
        const hasContentId = Boolean(contentId);
        const isInline = (hasContentId && attachment.disposition !== 'attachment') ||
            attachment.related ||
            attachment.disposition === 'inline';
        const contentBytes = getAttachmentBytes(attachment.content);
        if (!isInline) {
            const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
            attachments.push({
                filename: attachment.filename || undefined,
                mimeType: attachment.mimeType || undefined,
                size,
            });
            continue;
        }
        const hasBinaryContent = Boolean(contentBytes);
        if (!contentId || !hasBinaryContent) {
            const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
            attachments.push({
                filename: attachment.filename || undefined,
                mimeType: attachment.mimeType || 'application/octet-stream',
                size,
            });
            continue;
        }
        const mimeType = resolveDataUrlMimeType(attachment.mimeType, contentBytes, attachment.filename);
        const base64 = byteArrayToBase64(contentBytes);
        const dataUrl = `data:${mimeType};base64,${base64}`;
        cidUrlById.set(contentId, dataUrl);
    }
    return { attachments, cidUrlById };
}
async function applyBody(parsedEmail, email, cidUrlById) {
    const html = (email.html || '').trim();
    if (html) {
        const withCidsResolved = replaceCidReferences(html, cidUrlById);
        parsedEmail.bodyHtml = await sanitizeEmailHTML(withCidsResolved);
        return;
    }
    parsedEmail.bodyText = (email.text || '').trim() || undefined;
}
/**
 * Normalizes a Content-ID by removing surrounding angle brackets.
 *
 * Example: `<image@id>` -> `image@id`
 *
 * @param contentId - The Content-ID to normalize, optionally surrounded by angle brackets.
 */
function normalizeContentId(contentId) {
    if (!contentId) {
        return '';
    }
    let normalized = contentId.trim();
    if (normalized.toLowerCase().startsWith('cid:')) {
        normalized = normalized.slice(4).trim();
    }
    if (normalized.startsWith('<')) {
        normalized = normalized.slice(1);
    }
    if (normalized.endsWith('>')) {
        normalized = normalized.slice(0, -1);
    }
    return normalized.trim();
}
function replaceCidReferences(html, cidUrlById) {
    if (cidUrlById.size === 0) {
        return html;
    }
    return html.replaceAll(/(src\s*=\s*["']?)cid:([^"'\s>]+)(["']?)/gi, (match, prefix, cid, suffix) => {
        const normalized = normalizeContentId(decodeCidReference(cid));
        const replacement = cidUrlById.get(normalized);
        if (!replacement) {
            return match;
        }
        return `${prefix}${replacement}${suffix}`;
    });
}
function decodeCidReference(cid) {
    try {
        return decodeURIComponent(cid);
    }
    catch (_a) {
        return cid;
    }
}
function getAttachmentBytes(content) {
    if (content instanceof ArrayBuffer) {
        return new Uint8Array(content);
    }
    if (ArrayBuffer.isView(content)) {
        return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
    }
    return undefined;
}
function byteArrayToBase64(bytes) {
    if (typeof btoa === 'function') {
        let binary = '';
        for (const byte of bytes) {
            binary += String['fromCharCode'](byte);
        }
        return btoa(binary);
    }
    // Jest/Node fallback
    return globalThis.Buffer.from(bytes).toString('base64');
}
function resolveDataUrlMimeType(mimeType, bytes, filename) {
    const normalizedMimeType = typeof mimeType === 'string' ? mimeType.trim().toLowerCase() : '';
    const detectedFromBytes = detectImageMimeTypeFromBytes(bytes);
    if (normalizedMimeType.startsWith('image/')) {
        if (isTrustedDeclaredImageMimeType(normalizedMimeType, detectedFromBytes)) {
            return normalizedMimeType;
        }
        if (detectedFromBytes) {
            return detectedFromBytes;
        }
    }
    if (detectedFromBytes) {
        return detectedFromBytes;
    }
    const detectedFromFilename = detectImageMimeTypeFromFilename(filename);
    if (detectedFromFilename) {
        return detectedFromFilename;
    }
    return normalizedMimeType || 'application/octet-stream';
}
function isTrustedDeclaredImageMimeType(declaredMimeType, detectedMimeType) {
    if (!detectedMimeType) {
        return true;
    }
    if (declaredMimeType === detectedMimeType) {
        return true;
    }
    if (declaredMimeType === 'image/jpg' && detectedMimeType === 'image/jpeg') {
        return true;
    }
    return (declaredMimeType === 'image/vnd.microsoft.icon' &&
        detectedMimeType === 'image/x-icon');
}
function detectImageMimeTypeFromBytes(bytes) {
    var _a, _b, _c, _d, _e;
    return ((_e = (_d = (_c = (_b = (_a = detectPngMimeType(bytes)) !== null && _a !== void 0 ? _a : detectJpegMimeType(bytes)) !== null && _b !== void 0 ? _b : detectGifMimeType(bytes)) !== null && _c !== void 0 ? _c : detectWebpMimeType(bytes)) !== null && _d !== void 0 ? _d : detectIconMimeType(bytes)) !== null && _e !== void 0 ? _e : detectSvgMimeType(bytes));
}
function detectPngMimeType(bytes) {
    if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
        return 'image/png';
    }
}
function detectJpegMimeType(bytes) {
    if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) {
        return 'image/jpeg';
    }
}
function detectGifMimeType(bytes) {
    const firstSix = bytesToAscii(bytes, 0, 6);
    if (firstSix === 'GIF87a' || firstSix === 'GIF89a') {
        return 'image/gif';
    }
}
function detectWebpMimeType(bytes) {
    const riff = bytesToAscii(bytes, 0, 4);
    const webp = bytesToAscii(bytes, 8, 12);
    if (riff === 'RIFF' && webp === 'WEBP') {
        return 'image/webp';
    }
}
function detectIconMimeType(bytes) {
    if (startsWithBytes(bytes, [0x00, 0x00, 0x01, 0x00])) {
        return 'image/x-icon';
    }
}
function detectSvgMimeType(bytes) {
    if (bytes.length < 5) {
        return;
    }
    const utf8Prefix = new TextDecoder('utf8', { fatal: false }).decode(bytes.slice(0, Math.min(bytes.length, 256)));
    const normalizedPrefix = utf8Prefix.trimStart().toLowerCase();
    if (normalizedPrefix.startsWith('<svg') ||
        (normalizedPrefix.startsWith('<?xml') &&
            normalizedPrefix.includes('<svg'))) {
        return 'image/svg+xml';
    }
}
function startsWithBytes(bytes, prefix) {
    if (bytes.length < prefix.length) {
        return false;
    }
    return prefix.every((value, index) => bytes[index] === value);
}
function bytesToAscii(bytes, start, endExclusive) {
    if (bytes.length < endExclusive) {
        return '';
    }
    return String.fromCodePoint(...bytes.slice(start, endExclusive));
}
function detectImageMimeTypeFromFilename(filename) {
    const normalized = filename === null || filename === void 0 ? void 0 : filename.trim().toLowerCase();
    if (!normalized || !normalized.includes('.')) {
        return;
    }
    const extension = normalized.slice(normalized.lastIndexOf('.') + 1);
    const mimeTypes = {
        png: 'image/png',
        jpg: 'image/jpeg',
        jpeg: 'image/jpeg',
        gif: 'image/gif',
        webp: 'image/webp',
        svg: 'image/svg+xml',
        ico: 'image/x-icon',
        bmp: 'image/bmp',
    };
    return mimeTypes[extension];
}
function isNonEmptyString(value) {
    return typeof value === 'string' && value.length > 0;
}
/**
 * Formats one or many address objects returned by PostalMime.
 *
 * @param addresses - Address object(s) to format.
 * @returns Formatted address string, or undefined if no valid addresses.
 */
function formatAddresses(addresses) {
    if (!addresses) {
        return undefined;
    }
    const list = Array.isArray(addresses) ? addresses : [addresses];
    const parts = list
        .map((addr) => formatAddress(addr))
        .filter(isNonEmptyString);
    return parts.length > 0 ? parts.join(', ') : undefined;
}
function formatAddress(address) {
    if (!address) {
        return undefined;
    }
    if (Array.isArray(address)) {
        return formatAddresses(address);
    }
    if (address.group && Array.isArray(address.group)) {
        const groupName = (address.name || '').trim();
        const groupMembers = address.group
            .map((m) => formatAddress(m))
            .filter(isNonEmptyString)
            .join(', ');
        if (groupName && groupMembers) {
            return `${groupName}: ${groupMembers}`;
        }
        return groupName || groupMembers || undefined;
    }
    const name = (address.name || '').trim();
    const email = (address.address || '').trim();
    if (name && email) {
        const displayName = quoteDisplayNameIfNeeded(name);
        return `${displayName} <${email}>`;
    }
    return name || email || undefined;
}
function quoteDisplayNameIfNeeded(name) {
    if (!name) {
        return '';
    }
    // If the display name contains a comma, it must be quoted for safe parsing
    // of comma-separated address lists.
    if (!name.includes(',') && !name.includes('"')) {
        return name;
    }
    const escaped = name.replaceAll('\\', '\\\\').replaceAll('"', '\\' + '"');
    return `"${escaped}"`;
}
export const emailLoaderHelpers = {
    normalizeContentId,
    decodeCidReference,
    replaceCidReferences,
    getAttachmentBytes,
    detectImageMimeTypeFromBytes,
    detectImageMimeTypeFromFilename,
    resolveDataUrlMimeType,
    extractAttachments,
};
