/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { createHash, randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { extractDocxToMarkdown, extractPdfToMarkdown } from './transcript-extractor.js';
const CONTEXT_DIR = '.setup-agents/context';
const MANIFEST_FILE = 'manifest.json';
const DEFAULT_ALLOWED_FOLDERS = ['docs', 'sow', 'recordings', 'specs'];
const DEFAULT_CONFIG = {
    maxFileSizeMb: 50,
    allowedMimeTypes: [
        'application/pdf',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'text/markdown',
        'text/plain',
        'application/x-yaml',
        'text/yaml',
        'application/json',
        'image/png',
        'image/jpeg',
        'audio/mpeg',
        'video/mp4',
        'audio/wav',
    ],
};
export { DEFAULT_ALLOWED_FOLDERS, DEFAULT_CONFIG };
// ─── Path helpers ──────────────────────────────────────────────────────────
function contextRoot(workspaceCwd) {
    return join(workspaceCwd, CONTEXT_DIR);
}
function manifestPath(workspaceCwd) {
    return join(contextRoot(workspaceCwd), MANIFEST_FILE);
}
// ─── Sanitization & validation ─────────────────────────────────────────────
/**
 * Strips path traversal sequences, null bytes, and non-printable chars from a filename.
 * Returns just the basename with unsafe characters replaced.
 */
export function sanitizeFilename(name) {
    // Remove null bytes
    let sanitized = name.replace(/\0/g, '');
    // Take only the basename (strips directory components)
    sanitized = basename(sanitized);
    // Strip path traversal patterns
    sanitized = sanitized.replace(/\.\./g, '');
    // Replace non-printable and unsafe chars but keep dots, dashes, underscores
    sanitized = sanitized.replace(/[^\w.-]/g, '_');
    // Collapse multiple underscores
    sanitized = sanitized.replace(/_+/g, '_');
    // Remove leading/trailing underscores and dots
    sanitized = sanitized.replace(/^[_.]+|[_.]+$/g, '');
    return sanitized || 'unnamed';
}
/**
 * Validates that a MIME type is in the allowlist.
 */
export function validateMime(mimeType, config = DEFAULT_CONFIG) {
    return config.allowedMimeTypes.includes(mimeType);
}
/**
 * Resolves a file path within the vault context directory, preventing path traversal.
 * Throws if the resolved path escapes the context root.
 */
function safeResolvePath(workspaceCwd, relativePath) {
    const root = contextRoot(workspaceCwd);
    const resolved = resolve(root, relativePath);
    if (!resolved.startsWith(root)) {
        throw new Error('Path traversal detected');
    }
    return resolved;
}
// ─── Manifest operations ───────────────────────────────────────────────────
/**
 * Initializes the context vault directory structure and manifest file.
 * Creates the context root and all default folders. If the manifest already exists, it is preserved.
 */
export function initVault(workspaceCwd) {
    const root = contextRoot(workspaceCwd);
    mkdirSync(root, { recursive: true });
    for (const folder of DEFAULT_ALLOWED_FOLDERS) {
        mkdirSync(join(root, folder), { recursive: true });
    }
    const mPath = manifestPath(workspaceCwd);
    if (existsSync(mPath)) {
        return readManifest(workspaceCwd);
    }
    const manifest = {
        version: 1,
        maxFileSizeMb: DEFAULT_CONFIG.maxFileSizeMb,
        entries: [],
    };
    writeManifest(workspaceCwd, manifest);
    return manifest;
}
/**
 * Reads and parses the manifest file. Returns a fresh empty manifest if the file does not exist.
 */
export function readManifest(workspaceCwd) {
    const mPath = manifestPath(workspaceCwd);
    if (!existsSync(mPath)) {
        return { version: 1, maxFileSizeMb: DEFAULT_CONFIG.maxFileSizeMb, entries: [] };
    }
    const content = readFileSync(mPath, 'utf-8');
    return JSON.parse(content);
}
/**
 * Writes the manifest atomically using temp + rename.
 */
export function writeManifest(workspaceCwd, manifest) {
    const mPath = manifestPath(workspaceCwd);
    const dir = join(mPath, '..');
    mkdirSync(dir, { recursive: true });
    const tempPath = `${mPath}.${Date.now()}.tmp`;
    writeFileSync(tempPath, JSON.stringify(manifest, null, 2), 'utf-8');
    try {
        renameSync(tempPath, mPath);
    }
    catch (err) {
        try {
            unlinkSync(tempPath);
        }
        catch {
            // best-effort cleanup
        }
        throw err;
    }
}
/** MIME types that support automatic transcript extraction. */
const EXTRACTABLE_MIME_PDF = 'application/pdf';
const EXTRACTABLE_MIME_DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
/**
 * Adds a new file entry to the vault.
 * Stores the physical file as `<folder>/<uuid>-<sanitized-filename>` and updates the manifest.
 * If the file is a PDF or DOCX, automatically extracts text and stores a linked markdown transcript.
 * Returns the created entry (with transcriptId populated when extraction succeeds).
 */
export async function addEntry(workspaceCwd, input) {
    const manifest = readManifest(workspaceCwd);
    const config = {
        ...DEFAULT_CONFIG,
        maxFileSizeMb: manifest.maxFileSizeMb,
    };
    // Validate MIME
    if (!validateMime(input.mimeType, config)) {
        throw new Error(`Unsupported MIME type: ${input.mimeType}`);
    }
    // Validate size
    const maxBytes = config.maxFileSizeMb * 1024 * 1024;
    if (input.data.length > maxBytes) {
        throw new Error(`File exceeds maximum size of ${config.maxFileSizeMb}MB`);
    }
    // Validate folder
    if (!DEFAULT_ALLOWED_FOLDERS.includes(input.folder)) {
        throw new Error(`Invalid folder: ${input.folder}. Allowed: ${DEFAULT_ALLOWED_FOLDERS.join(', ')}`);
    }
    const id = randomUUID();
    const sanitized = sanitizeFilename(input.filename);
    const storedName = `${id}-${sanitized}`;
    const folderPath = safeResolvePath(workspaceCwd, input.folder);
    mkdirSync(folderPath, { recursive: true });
    const filePath = join(folderPath, storedName);
    // Verify the final path is still within context root
    const root = contextRoot(workspaceCwd);
    if (!filePath.startsWith(root)) {
        throw new Error('Path traversal detected');
    }
    // Compute checksum
    const checksum = createHash('sha256').update(input.data).digest('hex');
    // Write file atomically
    const tempPath = `${filePath}.${Date.now()}.tmp`;
    writeFileSync(tempPath, input.data);
    try {
        renameSync(tempPath, filePath);
    }
    catch (err) {
        try {
            unlinkSync(tempPath);
        }
        catch {
            // best-effort cleanup
        }
        throw err;
    }
    const now = new Date().toISOString();
    const entry = {
        id,
        filename: input.filename,
        folder: input.folder,
        mimeType: input.mimeType,
        size: input.data.length,
        uploadedAt: now,
        updatedAt: now,
        tags: input.tags ?? [],
        description: input.description ?? '',
        transcriptId: null,
        checksum,
    };
    manifest.entries.push(entry);
    writeManifest(workspaceCwd, manifest);
    // Auto-extract transcript for PDF and DOCX files (non-blocking: upload succeeds even if extraction fails)
    if (input.mimeType === EXTRACTABLE_MIME_PDF || input.mimeType === EXTRACTABLE_MIME_DOCX) {
        try {
            const transcriptText = input.mimeType === EXTRACTABLE_MIME_PDF
                ? await extractPdfToMarkdown(input.data)
                : await extractDocxToMarkdown(input.data);
            if (transcriptText.length > 0) {
                const transcriptId = randomUUID();
                const transcriptFilename = `${sanitized.replace(/\.[^.]+$/, '')}.transcript.md`;
                const transcriptStoredName = `${transcriptId}-${sanitizeFilename(transcriptFilename)}`;
                const relativePath = `${input.folder}/${storedName}`;
                const formattedTranscript = `# ${input.filename}\n\n_Auto-extracted transcript. Source: ${relativePath}_\n\n---\n\n${transcriptText}`;
                const transcriptData = Buffer.from(formattedTranscript, 'utf-8');
                const transcriptChecksum = createHash('sha256').update(transcriptData).digest('hex');
                // Write transcript file
                const transcriptPath = join(folderPath, transcriptStoredName);
                const transcriptTempPath = `${transcriptPath}.${Date.now()}.tmp`;
                writeFileSync(transcriptTempPath, transcriptData);
                try {
                    renameSync(transcriptTempPath, transcriptPath);
                }
                catch (err) {
                    try {
                        unlinkSync(transcriptTempPath);
                    }
                    catch {
                        // best-effort cleanup
                    }
                    throw err;
                }
                const transcriptNow = new Date().toISOString();
                const transcriptEntry = {
                    id: transcriptId,
                    filename: transcriptFilename,
                    folder: input.folder,
                    mimeType: 'text/markdown',
                    size: transcriptData.length,
                    uploadedAt: transcriptNow,
                    updatedAt: transcriptNow,
                    tags: [...(input.tags ?? []), 'transcript'],
                    description: `Auto-extracted transcript of ${input.filename}`,
                    transcriptId: null,
                    checksum: transcriptChecksum,
                };
                // Link transcript to original entry
                entry.transcriptId = transcriptId;
                // Re-read manifest (in case of concurrent writes), update, and save
                const freshManifest = readManifest(workspaceCwd);
                const originalIdx = freshManifest.entries.findIndex((e) => e.id === id);
                if (originalIdx !== -1) {
                    freshManifest.entries[originalIdx].transcriptId = transcriptId;
                }
                freshManifest.entries.push(transcriptEntry);
                writeManifest(workspaceCwd, freshManifest);
            }
        }
        catch {
            // Transcript extraction failed (encrypted PDF, corrupted file, disk error).
            // Leave transcriptId as null and allow the upload to succeed.
        }
    }
    return entry;
}
/**
 * Removes an entry from the manifest and deletes the physical file.
 * Returns true if the entry was found and removed, false otherwise.
 */
export function removeEntry(workspaceCwd, entryId) {
    const manifest = readManifest(workspaceCwd);
    const idx = manifest.entries.findIndex((e) => e.id === entryId);
    if (idx === -1)
        return false;
    const entry = manifest.entries[idx];
    const sanitized = sanitizeFilename(entry.filename);
    const storedName = `${entry.id}-${sanitized}`;
    const filePath = safeResolvePath(workspaceCwd, join(entry.folder, storedName));
    // Remove physical file
    if (existsSync(filePath)) {
        unlinkSync(filePath);
    }
    manifest.entries.splice(idx, 1);
    writeManifest(workspaceCwd, manifest);
    return true;
}
/**
 * Updates metadata fields (tags, description, folder) of an existing entry.
 * If folder changes, the physical file is moved.
 * Returns the updated entry or null if not found.
 */
export function updateEntry(workspaceCwd, entryId, updates) {
    const manifest = readManifest(workspaceCwd);
    const entry = manifest.entries.find((e) => e.id === entryId);
    if (!entry)
        return null;
    const oldFolder = entry.folder;
    if (updates.tags !== undefined)
        entry.tags = updates.tags;
    if (updates.description !== undefined)
        entry.description = updates.description;
    if (updates.folder !== undefined) {
        if (!DEFAULT_ALLOWED_FOLDERS.includes(updates.folder)) {
            throw new Error(`Invalid folder: ${updates.folder}. Allowed: ${DEFAULT_ALLOWED_FOLDERS.join(', ')}`);
        }
        entry.folder = updates.folder;
    }
    entry.updatedAt = new Date().toISOString();
    // Move file if folder changed
    if (updates.folder !== undefined && updates.folder !== oldFolder) {
        const sanitized = sanitizeFilename(entry.filename);
        const storedName = `${entry.id}-${sanitized}`;
        const oldPath = safeResolvePath(workspaceCwd, join(oldFolder, storedName));
        const newFolderPath = safeResolvePath(workspaceCwd, updates.folder);
        mkdirSync(newFolderPath, { recursive: true });
        const newPath = join(newFolderPath, storedName);
        if (existsSync(oldPath)) {
            renameSync(oldPath, newPath);
        }
    }
    writeManifest(workspaceCwd, manifest);
    return entry;
}
/**
 * Returns the absolute file path for an entry, or null if the entry doesn't exist.
 */
export function getEntryFile(workspaceCwd, entryId) {
    const manifest = readManifest(workspaceCwd);
    const entry = manifest.entries.find((e) => e.id === entryId);
    if (!entry)
        return null;
    const sanitized = sanitizeFilename(entry.filename);
    const storedName = `${entry.id}-${sanitized}`;
    const filePath = safeResolvePath(workspaceCwd, join(entry.folder, storedName));
    if (!existsSync(filePath))
        return null;
    return filePath;
}
//# sourceMappingURL=context-vault.js.map