// FU-120: Raw Storage Service
// Content-addressed storage with SHA-256 hashing and local/cloud storage
import crypto from "crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { config } from "../config.js";
import { db } from "../db.js";
import { generateDeterministicSourceId } from "./source_identity.js";
import { getCurrentAgentIdentity, getCurrentAttribution } from "./request_context.js";
import { enforceAttributionPolicy } from "./attribution_policy.js";
/**
 * Compute SHA-256 hash of file content
 */
export function computeContentHash(buffer) {
    return crypto.createHash("sha256").update(buffer).digest("hex");
}
/**
 * Store raw file content with content-addressed deduplication.
 * Idempotent: second calls with the same content return the existing source
 * (or claim the existing file in DB) instead of throwing "file already exists".
 *
 * Storage path: sources/{user_id}/{content_hash}
 * Deduplication: Per-user uniqueness on (user_id, content_hash)
 */
export async function storeRawContent(options) {
    enforceAttributionPolicy("sources", getCurrentAgentIdentity());
    const { userId, fileBuffer, mimeType, originalFilename, sourceType, provenance = {}, idempotencyKey, } = options;
    // Compute content hash
    const contentHash = computeContentHash(fileBuffer);
    const fileSize = fileBuffer.length;
    if (idempotencyKey) {
        const { data: existingByKey, error: existingByKeyError } = await db
            .from("sources")
            .select("id, storage_url, content_hash, file_size, idempotency_key")
            .eq("user_id", userId)
            .eq("idempotency_key", idempotencyKey)
            .maybeSingle();
        if (existingByKeyError) {
            throw new Error(`Failed to check idempotency key: ${existingByKeyError.message}`);
        }
        if (existingByKey) {
            if (existingByKey.content_hash !== contentHash) {
                throw new Error("Idempotency key reuse detected with different content. Use a new idempotency_key.");
            }
            return {
                sourceId: existingByKey.id,
                contentHash,
                storageUrl: existingByKey.storage_url,
                fileSize: existingByKey.file_size,
                deduplicated: true,
                idempotencyKey,
            };
        }
    }
    // Check for existing source (deduplication)
    const { data: existing, error: checkError } = await db
        .from("sources")
        .select("id, storage_url, idempotency_key")
        .eq("user_id", userId)
        .eq("content_hash", contentHash)
        .maybeSingle();
    if (checkError) {
        throw new Error(`Failed to check for existing source: ${checkError.message}`);
    }
    // If already exists, return existing source
    if (existing) {
        if (idempotencyKey && !existing.idempotency_key) {
            const { error: updateError } = await db
                .from("sources")
                .update({ idempotency_key: idempotencyKey })
                .eq("id", existing.id);
            if (updateError) {
                throw new Error(`Failed to set idempotency key: ${updateError.message}`);
            }
        }
        return {
            sourceId: existing.id,
            contentHash,
            storageUrl: existing.storage_url,
            fileSize,
            deduplicated: true,
            idempotencyKey,
        };
    }
    // Upload to storage
    const storagePath = `${userId}/${contentHash}`;
    const bucketName = "sources";
    // Check if we're in test environment
    const isTestEnv = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
    if (!isTestEnv) {
        const { error: uploadError } = await db.storage
            .from(bucketName)
            .upload(storagePath, fileBuffer, {
            upsert: false,
            contentType: mimeType,
        });
        if (uploadError) {
            const isAlreadyExists = uploadError.message?.toLowerCase().includes("already exists") ||
                uploadError.message?.toLowerCase().includes("file already exists") ||
                uploadError.code === "STORAGE_FILE_EXISTS";
            if (isAlreadyExists) {
                const { data: existingAfterUpload, error: recheckError } = await db
                    .from("sources")
                    .select("id, storage_url")
                    .eq("user_id", userId)
                    .eq("content_hash", contentHash)
                    .maybeSingle();
                if (!recheckError && existingAfterUpload) {
                    return {
                        sourceId: existingAfterUpload.id,
                        contentHash,
                        storageUrl: existingAfterUpload.storage_url,
                        fileSize,
                        deduplicated: true,
                    };
                }
                // File on storage but no source row (e.g. previous insert failed). Insert to claim it.
            }
            else {
                throw new Error(`Failed to upload to storage: ${uploadError.message}`);
            }
        }
    }
    // Create source record
    const deterministicSourceId = generateDeterministicSourceId(userId, contentHash);
    const sourceRow = {
        id: deterministicSourceId,
        user_id: userId,
        content_hash: contentHash,
        mime_type: mimeType,
        storage_url: storagePath,
        file_size: fileSize,
        original_filename: originalFilename,
        idempotency_key: idempotencyKey,
        provenance: {
            ...provenance,
            uploaded_at: new Date().toISOString(),
            ...getCurrentAttribution(),
        },
    };
    if (sourceType)
        sourceRow.source_type = sourceType;
    const { data: source, error: insertError } = await db
        .from("sources")
        .insert(sourceRow)
        .select()
        .single();
    if (insertError) {
        const isDuplicate = insertError.message?.toLowerCase().includes("duplicate") ||
            insertError.message?.toLowerCase().includes("unique") ||
            insertError.code === "23505";
        if (isDuplicate) {
            const { data: existingSource, error: recheckError } = await db
                .from("sources")
                .select("id, storage_url")
                .eq("user_id", userId)
                .eq("content_hash", contentHash)
                .maybeSingle();
            if (!recheckError && existingSource) {
                return {
                    sourceId: existingSource.id,
                    contentHash,
                    storageUrl: existingSource.storage_url,
                    fileSize,
                    deduplicated: true,
                };
            }
        }
        throw new Error(`Failed to create source record: ${insertError.message}`);
    }
    return {
        sourceId: source.id,
        contentHash,
        storageUrl: storagePath,
        fileSize,
        deduplicated: false,
        idempotencyKey,
    };
}
/**
 * Get source metadata by ID
 */
export async function getSourceMetadata(sourceId) {
    const { data, error } = await db.from("sources").select("*").eq("id", sourceId).single();
    if (error) {
        throw new Error(`Failed to get source metadata: ${error.message}`);
    }
    return data;
}
/** Thrown when the stored file is missing (e.g. ENOENT on local storage). Callers can catch and skip or retry. */
export class SourceFileNotFoundError extends Error {
    storageUrl;
    code = "SOURCE_FILE_NOT_FOUND";
    constructor(message, storageUrl) {
        super(message);
        this.storageUrl = storageUrl;
        this.name = "SourceFileNotFoundError";
    }
}
/**
 * Download raw content from storage
 */
export async function downloadRawContent(storageUrl, bucketName = "sources") {
    const { data, error } = await db.storage.from(bucketName).download(storageUrl);
    if (error || !data) {
        const msg = error?.message ?? "Unknown error";
        if (msg.includes("ENOENT") || msg.includes("no such file or directory")) {
            throw new SourceFileNotFoundError(`Source file not found: ${msg}`, storageUrl);
        }
        throw new Error(`Failed to download content: ${msg}`);
    }
    return Buffer.from(await data.arrayBuffer());
}
/**
 * Absolute filesystem path for a row's `storage_url` when using local raw storage
 * (`sources` bucket → {@link config.rawStorageDir} + object key).
 * Returns null for non-file keys (e.g. `internal://…`) or non-local backends.
 */
export function resolveLocalSourceFilePath(storageUrl) {
    if (storageUrl == null || typeof storageUrl !== "string")
        return null;
    const u = storageUrl.trim();
    if (!u || u.startsWith("internal://"))
        return null;
    if (u.startsWith("file://")) {
        try {
            return fileURLToPath(u);
        }
        catch {
            return null;
        }
    }
    if (config.storageBackend !== "local")
        return null;
    return path.resolve(path.join(config.rawStorageDir, u));
}
//# sourceMappingURL=raw_storage.js.map