/**
 * Asset Upload Service
 *
 * Single primitive for "store a binary, create a wrapper entity that
 * points at it." Used by both the standalone `POST /api/repos/:repo/assets`
 * route and the multi-file import dispatcher when it sees a binary
 * extension.
 *
 * Flow per file:
 *   1. Validate extension against `ASSET_UPLOAD_EXTENSIONS`.
 *   2. Create the `asset` entity with stub frontmatter (collision-retry
 *      on entity_id, same `foo`→`foo-2` semantics as import-service).
 *   3. Upload the binary via `graph.uploadAsset` — picks R2 or local
 *      filesystem from the workspace's asset config.
 *   4. PDFs: extract text + page count via `pdf-parse`, write the text
 *      to a sidecar at `<workspace>/.studiograph/assets-text/asset/{id}.txt`
 *      so the vector indexer can RAG it without bloating the markdown body.
 *   5. Update the entity with the resolved `asset_url` and per-kind
 *      metadata (`pages`, future `width`/`height`).
 */
import { extname, basename, join } from 'path';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { log } from '../utils/log.js';
import { detectAssetKind, detectContentType, isAssetUploadExtension, safeFilename, } from './assets/base.js';
const slugify = (str) => str.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const formatName = (slug) => slug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
export async function uploadAsset(opts) {
    const { graph, buffer, folderPath, user, skipCommit = false, workspacePath } = opts;
    const onlyName = basename(opts.filename);
    const ext = extname(onlyName).toLowerCase();
    if (!isAssetUploadExtension(onlyName)) {
        throw new Error(`Unsupported asset extension: ${ext || '(none)'}`);
    }
    if (!buffer || buffer.length === 0) {
        throw new Error('Asset upload received an empty buffer');
    }
    const assetKind = detectAssetKind(onlyName);
    const mime = detectContentType(onlyName);
    // Probe asset configuration before mutating the graph. listAssets() is
    // cheap and surfaces a helpful error when R2/local storage isn't
    // wired up — better to fail before creating a stub entity that has
    // nowhere to point at.
    try {
        await graph.listAssets('asset', '__check__');
    }
    catch (err) {
        if (err.message?.includes('not configured')) {
            throw new Error('Asset storage is not configured for this workspace.');
        }
        // Other errors (e.g. entity-not-found) just mean the service IS configured.
    }
    const stem = onlyName.replace(/\.[^.]+$/, '');
    const baseId = (slugify(stem).slice(0, 80).replace(/-$/, '')) || `asset-${Date.now()}`;
    const name = opts.name ?? formatName(baseId);
    const now = new Date().toISOString();
    const userHandle = user?.email?.split('@')[0] ?? user?.name ?? 'upload';
    const stubData = {
        entity_type: 'asset',
        entity_id: baseId,
        name,
        // Stub URL — populated post-upload. Required by the schema; '__pending__'
        // is replaced before the second commit lands.
        asset_url: '__pending__',
        asset_kind: assetKind,
        asset_extension: ext,
        asset_size: buffer.length,
        asset_mime: mime,
        created_at: now,
        updated_at: now,
        created_by: userHandle,
        updated_by: userHandle,
    };
    // Collision retry on (asset, entity_id). Mirrors the suffix scheme in
    // import-service.createWithCollisionRetry — keeps re-uploads from
    // silently dropping files.
    let entityId = baseId;
    let createdPath;
    let suffix = 0;
    while (true) {
        entityId = suffix === 0 ? baseId : `${baseId}-${suffix + 1}`;
        try {
            const created = await graph.create({
                entityType: 'asset',
                entityId,
                data: { ...stubData, entity_id: entityId },
                content: '',
                folderPath,
                skipCommit: true,
                user,
            });
            createdPath = created.path;
            break;
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            if (!msg.includes('already exists') || suffix >= 50)
                throw err;
            suffix++;
        }
    }
    // Everything past this point operates on a real on-disk stub entity
    // with `asset_url: '__pending__'`. If any step throws, the stub must be
    // rolled back — otherwise the entity persists with a placeholder URL
    // that surfaces as a broken asset in list/gallery/search/RAG. Rollback
    // commits a delete (graph.delete is auto-commit-only by design); on the
    // batch import path that's one extra commit per failure, which is
    // acceptable on a failure path.
    let pages;
    let sidecarPath;
    let asset_url;
    try {
        // Storage filename gets a Date.now() prefix to dodge same-name re-uploads.
        const storedFilename = `${Date.now()}-${safeFilename(onlyName)}`;
        const asset = await graph.uploadAsset('asset', entityId, storedFilename, buffer, undefined, { uploaderUserId: opts.uploaderUserId ?? null });
        // PDF metadata extraction. pdf-parse is CPU-bound and runs on the
        // request thread — gate by size so a 100MB PDF doesn't stall the
        // event loop for every other in-flight Fastify request. Asset still
        // uploads; it just won't be RAG-searchable until a future re-extract.
        if (assetKind === 'pdf') {
            const PDF_TEXT_BYTES_CAP = 20 * 1024 * 1024;
            if (buffer.length > PDF_TEXT_BYTES_CAP) {
                log.warn(`[asset-upload] PDF text extraction skipped for ${entityId}: ${buffer.length} bytes > ${PDF_TEXT_BYTES_CAP} cap`);
            }
            else {
                let extractedText = '';
                try {
                    const { PDFParse } = await import('pdf-parse');
                    const parser = new PDFParse({ data: buffer });
                    const result = await parser.getText();
                    await parser.destroy();
                    if (result.total && result.total > 0)
                        pages = result.total;
                    extractedText = result.text?.trim() ?? '';
                }
                catch (err) {
                    // Non-fatal — extraction can fail on encrypted/scanned PDFs.
                    // Don't conflate with sidecar write failures (infra problem,
                    // not content problem) — those get their own warn below.
                    log.warn(`[asset-upload] PDF text extraction failed for ${entityId}: ${err instanceof Error ? err.message : err}`);
                }
                if (extractedText && workspacePath) {
                    try {
                        const sidecarDir = join(workspacePath, '.studiograph', 'assets-text', 'asset');
                        if (!existsSync(sidecarDir))
                            mkdirSync(sidecarDir, { recursive: true });
                        sidecarPath = join(sidecarDir, `${entityId}.txt`);
                        writeFileSync(sidecarPath, extractedText, 'utf-8');
                    }
                    catch (err) {
                        // Sidecar write failures (disk full, permissions) leave the
                        // asset un-RAG-able but otherwise usable. Re-indexing later
                        // picks it back up if the sidecar appears.
                        sidecarPath = undefined;
                        log.warn(`[asset-upload] PDF sidecar write failed for ${entityId}: ${err instanceof Error ? err.message : err}`);
                    }
                }
            }
        }
        // Canonical URL from uploadAsset (immutable-key scheme when wired, else
        // legacy). Don't re-derive — the assetId isn't recoverable from (type,id).
        asset_url = asset.publicUrl;
        const updateData = { asset_url };
        if (pages !== undefined)
            updateData.pages = pages;
        await graph.update({
            entityType: 'asset',
            entityId,
            data: updateData,
            skipCommit: true,
            user,
        });
    }
    catch (err) {
        try {
            await graph.delete('asset', entityId, `Rollback failed asset upload: ${entityId}`, user);
        }
        catch (cleanupErr) {
            log.warn(`[asset-upload] rollback delete failed for ${entityId}: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
        }
        throw err;
    }
    if (!skipCommit) {
        const git = graph.getGitService();
        if (git) {
            const commitUser = user ?? { id: 'upload', name: 'Upload', email: 'upload@studiograph' };
            git.commitAll(`Upload asset: ${entityId}`, commitUser);
        }
    }
    return {
        entityId,
        entityType: 'asset',
        filePath: createdPath,
        asset_url,
        asset_kind: assetKind,
        asset_size: buffer.length,
        pages,
        sidecarPath,
    };
}
//# sourceMappingURL=asset-upload-service.js.map