/**
 * @fileoverview Shared disk cache for expensive Office document previews.
 */
import { createHash } from 'node:crypto';
import { execFile } from 'node:child_process';
import fs from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, dirname, extname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
import { runWithConversionLimit } from './document-conversion-limiter.js';
const execFileAsync = promisify(execFile);
const OFFICE_CONVERSION_TIMEOUT_MS = 5 * 60_000;
const DOCUMENT_PREVIEW_CACHE_DIR = join(tmpdir(), 'codeman-document-preview-cache');
/**
 * Cap on persistent converted-PDF files kept in DOCUMENT_PREVIEW_CACHE_DIR.
 * The cache key embeds the source mtime, so every edit to a doc orphans its
 * prior PDF; without a cap the dir grows unbounded across long-running sessions.
 * Override with CODEMAN_MAX_PREVIEW_CACHE_FILES (clamped to >= 1).
 */
const MAX_PREVIEW_CACHE_FILES = (() => {
    const raw = Number(process.env.CODEMAN_MAX_PREVIEW_CACHE_FILES);
    return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 100;
})();
function buildWordExportPdfScript(sourcePath, outputPath) {
    return `
$ErrorActionPreference = "Stop"
$source = ${toPowerShellSingleQuotedString(sourcePath)}
$output = ${toPowerShellSingleQuotedString(outputPath)}
$word = $null
$doc = $null
try {
  $word = New-Object -ComObject Word.Application
  $word.Visible = $false
  $word.DisplayAlerts = 0
  $doc = $word.Documents.Open($source)
  $doc.ExportAsFixedFormat($output, 17)
} finally {
  if ($null -ne $doc) {
    $doc.Close($false) | Out-Null
    [System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc) | Out-Null
  }
  if ($null -ne $word) {
    $word.Quit() | Out-Null
    [System.Runtime.InteropServices.Marshal]::ReleaseComObject($word) | Out-Null
  }
  [System.GC]::Collect()
  [System.GC]::WaitForPendingFinalizers()
}
`.trim();
}
const inFlightOfficeConversions = new Map();
export function clearDocumentPreviewCache() {
    inFlightOfficeConversions.clear();
}
/**
 * Best-effort LRU-ish eviction for the persistent converted-PDF cache: keeps at
 * most MAX_PREVIEW_CACHE_FILES `*.pdf` files in `cacheDir`, deleting the oldest
 * by mtime once over the cap. Never throws — a pruning failure must not fail the
 * conversion that triggered it. Only `*.pdf` files are considered, so the
 * transient `work-*` mkdtemp dirs are ignored.
 */
export async function pruneDocumentPreviewCache(cacheDir) {
    try {
        const entries = await fs.readdir(cacheDir);
        const pdfs = entries.filter((name) => name.toLowerCase().endsWith('.pdf'));
        if (pdfs.length <= MAX_PREVIEW_CACHE_FILES)
            return;
        const stats = await Promise.all(pdfs.map(async (name) => {
            const fullPath = join(cacheDir, name);
            try {
                const stat = await fs.stat(fullPath);
                return { fullPath, mtimeMs: stat.mtimeMs ?? 0 };
            }
            catch {
                return null;
            }
        }));
        const sorted = stats.filter((s) => s !== null);
        sorted.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
        const toRemove = sorted.slice(0, Math.max(0, sorted.length - MAX_PREVIEW_CACHE_FILES));
        await Promise.all(toRemove.map((entry) => fs.rm(entry.fullPath, { force: true }).catch(() => { })));
    }
    catch {
        // Best-effort: pruning must never break a conversion.
    }
}
export async function getOfficePreviewPdfPath(filePath, extension) {
    const ext = extension.toLowerCase().replace(/^\./, '');
    if (ext !== 'docx' && ext !== 'pptx')
        return null;
    let sourceStat;
    try {
        sourceStat = await fs.stat(filePath);
    }
    catch {
        return null;
    }
    for (const converter of getOfficePreviewConverters(filePath, ext)) {
        const cacheKey = createDocumentPreviewCacheKey(filePath, ext, sourceStat.size, sourceStat.mtimeMs ?? 0, converter);
        const cachePath = getOfficePreviewCachePath(filePath, cacheKey, converter);
        if (await fileExists(cachePath)) {
            return cachePath;
        }
        const inFlightKey = `${converter}:${cacheKey}`;
        const inFlight = inFlightOfficeConversions.get(inFlightKey);
        if (inFlight) {
            const converted = await inFlight;
            if (converted)
                return converted;
            continue;
        }
        const conversion = converter === 'msword'
            ? convertWordDocumentToCachedPdf(filePath, cachePath)
            : convertLibreOfficeDocumentToCachedPdf(filePath, cachePath);
        inFlightOfficeConversions.set(inFlightKey, conversion);
        try {
            const converted = await conversion;
            if (converted)
                return converted;
        }
        finally {
            inFlightOfficeConversions.delete(inFlightKey);
        }
    }
    return null;
}
function getOfficePreviewConverters(filePath, extension) {
    if (extension === 'docx' && wslMountPathToWindowsPath(filePath)) {
        return ['msword', 'libreoffice'];
    }
    return ['libreoffice'];
}
function createDocumentPreviewCacheKey(filePath, extension, size, mtimeMs, converter) {
    return createHash('sha256')
        .update(JSON.stringify({ cacheVersion: 2, converter, filePath, extension, size, mtimeMs }))
        .digest('hex')
        .slice(0, 32);
}
function getOfficePreviewCachePath(filePath, cacheKey, converter) {
    if (converter === 'msword') {
        const windowsCacheDir = getWindowsUserTempCacheDir(filePath);
        if (windowsCacheDir) {
            return join(windowsCacheDir, `${cacheKey}.pdf`);
        }
    }
    return join(DOCUMENT_PREVIEW_CACHE_DIR, `${cacheKey}.pdf`);
}
async function fileExists(filePath) {
    try {
        const stat = await fs.stat(filePath);
        return typeof stat.isFile !== 'function' || stat.isFile();
    }
    catch {
        return false;
    }
}
async function convertWordDocumentToCachedPdf(filePath, cachePath) {
    const outputPath = wslMountPathToWindowsPath(cachePath);
    if (!outputPath)
        return null;
    let sourceCopyPath;
    try {
        await fs.mkdir(dirname(cachePath), { recursive: true });
        sourceCopyPath = join(dirname(cachePath), `${basename(cachePath, '.pdf')}.docx`);
        await fs.copyFile(filePath, sourceCopyPath);
        const sourcePath = wslMountPathToWindowsPath(sourceCopyPath);
        if (!sourcePath)
            return null;
        await runWithConversionLimit(() => execFileAsync('powershell.exe', [
            '-NoProfile',
            '-NonInteractive',
            '-ExecutionPolicy',
            'Bypass',
            '-EncodedCommand',
            encodePowerShellCommand(buildWordExportPdfScript(sourcePath, outputPath)),
        ], {
            timeout: OFFICE_CONVERSION_TIMEOUT_MS,
            maxBuffer: 1024 * 1024,
        }));
        if (await fileExists(cachePath)) {
            await pruneDocumentPreviewCache(dirname(cachePath));
            return cachePath;
        }
        console.warn(`[DocumentPreviewCache] Microsoft Word did not produce PDF output for ${filePath}`);
        return null;
    }
    catch (err) {
        console.warn(`[DocumentPreviewCache] Failed to convert DOCX with Microsoft Word (${filePath}):`, getCacheErrorMessage(err));
        return null;
    }
    finally {
        if (sourceCopyPath) {
            await fs.rm(sourceCopyPath, { force: true }).catch(() => { });
        }
    }
}
async function convertLibreOfficeDocumentToCachedPdf(filePath, cachePath) {
    let workDir;
    try {
        await fs.mkdir(DOCUMENT_PREVIEW_CACHE_DIR, { recursive: true });
        const outDir = await fs.mkdtemp(join(DOCUMENT_PREVIEW_CACHE_DIR, 'work-'));
        workDir = outDir;
        const profileDir = join(outDir, 'profile');
        await fs.mkdir(profileDir, { recursive: true });
        await runWithConversionLimit(() => execFileAsync('soffice', [
            '--headless',
            '--nologo',
            '--nofirststartwizard',
            `-env:UserInstallation=${pathToFileURL(profileDir).href}`,
            '--convert-to',
            'pdf',
            '--outdir',
            outDir,
            filePath,
        ], {
            timeout: OFFICE_CONVERSION_TIMEOUT_MS,
            maxBuffer: 1024 * 1024,
        }));
        const converted = (await fs.readdir(outDir)).find((name) => name.toLowerCase().endsWith('.pdf'));
        if (!converted)
            return null;
        await fs.rename(join(workDir, converted), cachePath);
        await pruneDocumentPreviewCache(DOCUMENT_PREVIEW_CACHE_DIR);
        return cachePath;
    }
    catch (err) {
        console.warn(`[DocumentPreviewCache] Failed to convert Office file to PDF (${filePath}):`, getCacheErrorMessage(err));
        return null;
    }
    finally {
        if (workDir) {
            await fs.rm(workDir, { recursive: true, force: true }).catch(() => { });
        }
    }
}
function wslMountPathToWindowsPath(filePath) {
    const match = filePath.match(/^\/mnt\/([a-zA-Z])\/(.+)$/);
    if (!match)
        return null;
    return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`;
}
function getWindowsUserTempCacheDir(filePath) {
    const match = filePath.match(/^\/mnt\/([a-zA-Z])\/Users\/([^/]+)\//);
    if (!match)
        return null;
    return `/mnt/${match[1].toLowerCase()}/Users/${match[2]}/AppData/Local/Temp/codeman-document-preview-cache`;
}
function toPowerShellSingleQuotedString(value) {
    return `'${value.replace(/'/g, "''")}'`;
}
function encodePowerShellCommand(script) {
    return Buffer.from(script, 'utf16le').toString('base64');
}
export function getPreviewPdfDownloadName(fileName, extension) {
    return `${basename(fileName, extname(fileName) || `.${extension}`)}.pdf`;
}
function getCacheErrorMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
//# sourceMappingURL=document-preview-cache.js.map