/**
 * @fileoverview In-memory attachment registry for live external document references.
 *
 * Session-local files keep using the existing workspace-scoped file routes. This
 * registry is only for explicit, live external attachments that need a stable ID
 * so browser requests never contain arbitrary absolute paths.
 */
import { randomUUID } from 'node:crypto';
import { realpathSync } from 'node:fs';
import fs from 'node:fs/promises';
import { basename, extname, isAbsolute } from 'node:path';
import { isBlockedAttachmentPath, loadAttachmentGuardConfig } from './config/attachment-guard.js';
import { validateSessionFilePath } from './web/route-helpers.js';
const SUPPORTED_ATTACHMENT_EXTENSIONS = new Set(['png', 'pdf', 'docx', 'pptx', 'md', 'txt']);
export class AttachmentRegistrationError extends Error {
    statusCode;
    constructor(message, statusCode = 400) {
        super(message);
        this.statusCode = statusCode;
    }
}
/** Per-session attachment cap. Bounds memory against a client (or a
 *  prompt-injected magic-link flood) registering unbounded distinct paths. */
const MAX_ATTACHMENTS_PER_SESSION = 200;
class AttachmentRegistry {
    recordsBySession = new Map();
    register(record) {
        let records = this.recordsBySession.get(record.sessionId);
        if (!records) {
            records = new Map();
            this.recordsBySession.set(record.sessionId, records);
        }
        records.set(record.attachmentId, record);
        // Evict oldest (insertion-order) entries beyond the cap.
        while (records.size > MAX_ATTACHMENTS_PER_SESSION) {
            const oldest = records.keys().next().value;
            if (oldest === undefined)
                break;
            records.delete(oldest);
        }
    }
    get(sessionId, attachmentId) {
        return this.recordsBySession.get(sessionId)?.get(attachmentId);
    }
    findByFilePath(sessionId, filePath) {
        const records = this.recordsBySession.get(sessionId);
        if (!records)
            return undefined;
        for (const record of records.values()) {
            if (record.filePath === filePath)
                return record;
        }
        return undefined;
    }
    clearSession(sessionId) {
        this.recordsBySession.delete(sessionId);
    }
}
export const attachmentRegistry = new AttachmentRegistry();
export function isSupportedAttachmentExtension(extension) {
    return SUPPORTED_ATTACHMENT_EXTENSIONS.has(extension.toLowerCase().replace(/^\./, ''));
}
export function getAttachmentType(extension) {
    const normalized = extension.toLowerCase().replace(/^\./, '');
    if (normalized === 'png')
        return 'image';
    if (normalized === 'pdf')
        return 'pdf';
    if (normalized === 'pptx')
        return 'presentation';
    if (normalized === 'md')
        return 'markdown';
    if (normalized === 'txt')
        return 'text';
    return 'document';
}
export function buildAttachmentRoutes(sessionId, attachmentId) {
    const encodedId = encodeURIComponent(attachmentId);
    return {
        rawUrl: `/api/sessions/${sessionId}/attachments/${encodedId}/raw`,
        previewUrl: `/api/sessions/${sessionId}/attachments/${encodedId}/preview`,
        thumbnailUrl: `/api/sessions/${sessionId}/attachments/${encodedId}/thumbnail`,
    };
}
export function buildFileThumbnailRoute(sessionId, relativePath) {
    return `/api/sessions/${sessionId}/file-thumbnail?path=${encodeURIComponent(relativePath)}`;
}
export function attachmentRecordToEvent(record) {
    const routes = buildAttachmentRoutes(record.sessionId, record.attachmentId);
    return {
        sessionId: record.sessionId,
        filePath: record.fileName,
        relativePath: '',
        fileName: record.fileName,
        extension: record.extension,
        attachmentType: record.attachmentType,
        timestamp: record.timestamp,
        size: record.size,
        attachmentId: record.attachmentId,
        source: record.source,
        ...routes,
    };
}
export async function registerExternalAttachment(sessionId, requestedPath, options = {}) {
    if (!requestedPath || !isAbsolute(requestedPath)) {
        throw new AttachmentRegistrationError('Attachment path must be an absolute local path');
    }
    let resolvedPath;
    try {
        resolvedPath = realpathSync(requestedPath);
    }
    catch {
        throw new AttachmentRegistrationError('Attachment file not found', 404);
    }
    // COD-53: enforce the active attachment-guard policy on the symlink-resolved
    // path before doing anything else.
    const guard = await loadAttachmentGuardConfig();
    if (guard.confineToWorkspace || options.forceWorkspaceConfinement) {
        // Workspace-confined: the file MUST resolve inside the session's workspace.
        // Applies when the global strict mode is on (opt-in, default OFF) OR when
        // the caller forces it for this registration (the magic-link scanner — see
        // forceWorkspaceConfinement). Strictly more restrictive than the blocklist.
        const workingDir = options.sessionWorkingDir;
        if (!workingDir || !validateSessionFilePath(workingDir, resolvedPath)) {
            throw new AttachmentRegistrationError('Access to this file is blocked', 403);
        }
    }
    // Blocklist (DEFAULT, also applied alongside confinement as defense in
    // depth): pre-populated secret locations + the /root and /etc trees + any
    // operator-configured extra trees. Symlinks are already resolved above.
    // Cross-workspace attachment of non-blocked files stays allowed, so
    // codeman-publish and the ~/.codeman review loop keep working.
    if (isBlockedAttachmentPath(resolvedPath, guard.blockedTrees)) {
        throw new AttachmentRegistrationError('Access to this file is blocked', 403);
    }
    const extension = extname(resolvedPath).toLowerCase().replace(/^\./, '');
    if (!isSupportedAttachmentExtension(extension)) {
        throw new AttachmentRegistrationError('Unsupported attachment type');
    }
    const stat = await fs.stat(resolvedPath);
    if (typeof stat.isFile === 'function' && !stat.isFile()) {
        throw new AttachmentRegistrationError('Attachment path is not a file');
    }
    const existing = attachmentRegistry.findByFilePath(sessionId, resolvedPath);
    if (existing) {
        existing.size = stat.size;
        existing.mtimeMs = stat.mtimeMs ?? 0;
        existing.timestamp = Date.now();
        return attachmentRecordToEvent(existing);
    }
    const record = {
        attachmentId: `att_${randomUUID()}`,
        sessionId,
        filePath: resolvedPath,
        fileName: basename(resolvedPath),
        extension,
        attachmentType: getAttachmentType(extension),
        size: stat.size,
        mtimeMs: stat.mtimeMs ?? 0,
        timestamp: Date.now(),
        source: 'external',
    };
    attachmentRegistry.register(record);
    return attachmentRecordToEvent(record);
}
//# sourceMappingURL=attachment-registry.js.map