import env from "../utils/env.js";
import fs from "fs/promises";
import path from "path";
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { existsSync, mkdirSync } from "fs";
import { getProjectPath } from "../utils/dirname.js";
/**
 * Resolve a storage key/filename to an absolute path confined within `basePath`.
 * Defense-in-depth against path traversal: rejects null bytes and any key that
 * resolves outside the storage root (e.g. "../../etc/passwd"). Use this for EVERY
 * local-disk operation that joins a (potentially client-influenced) filename.
 * @throws Error if the key is invalid or escapes basePath.
 */
export function resolveStorageKey(basePath, key) {
    if (typeof key !== "string" || key.length === 0) {
        throw new Error("Invalid storage path");
    }
    // Null bytes are always rejected — never a valid filename, no reason to allow.
    if (key.includes("\0")) {
        throw new Error("Invalid storage path: null byte");
    }
    const resolvedBase = path.resolve(basePath);
    const fullPath = path.resolve(resolvedBase, key);
    // Path-traversal confinement. Configurable: STORAGE_PATH_CONFINEMENT=false
    // relaxes the "must stay within storage root" check (NOT recommended).
    const confine = env.get("STORAGE_PATH_CONFINEMENT") !== "false";
    if (confine && fullPath !== resolvedBase && !fullPath.startsWith(resolvedBase + path.sep)) {
        throw new Error("Invalid storage path: escapes storage root");
    }
    return fullPath;
}
class StorageService {
    providers = {};
    constructor() {
        console.info("Initializing Storage Service");
    }
    initialize() {
        const servicesEnabled = env.get("STORAGE_SERVICES_ENABLED")?.split(",") || [];
        if (servicesEnabled.length === 0) {
            console.warn("No storage services enabled. Check your STORAGE_SERVICES_ENABLED environment variable.");
            return;
        }
        servicesEnabled.forEach((service) => this.initializeProvider(service.trim()));
        console.info("Storage Service Initialization Complete");
    }
    initializeProvider(service) {
        const upperService = service.toUpperCase();
        const driver = env.get(`${upperService}_STORAGE_DRIVER`);
        if (!driver) {
            console.warn(`Storage driver not specified for ${service}. Skipping.`);
            return;
        }
        switch (driver) {
            case "LOCAL":
                this.initializeLocalStorage(service, upperService);
                break;
            case "S3":
                this.initializeS3Storage(service, upperService);
                break;
            default:
                console.warn(`Unknown storage driver '${driver}' for ${service}. Skipping.`);
        }
    }
    initializeLocalStorage(service, upperService) {
        const storagePath = env.get(`${upperService}_STORAGE_PATH`);
        if (!storagePath) {
            throw new Error(`${upperService}_STORAGE_PATH is not defined`);
        }
        if (!existsSync(storagePath)) {
            mkdirSync(storagePath, { recursive: true });
        }
        const basePath = path.resolve(getProjectPath(storagePath));
        this.providers[service] = {
            driver: "LOCAL",
            basePath,
            async saveFile(filePath, fileContent) {
                const fullPath = resolveStorageKey(basePath, filePath);
                await fs.mkdir(path.dirname(fullPath), { recursive: true });
                await fs.writeFile(fullPath, fileContent);
                return filePath;
            },
            async getFile(filePath) {
                const fullPath = resolveStorageKey(basePath, filePath);
                return fs.readFile(fullPath);
            },
            async deleteFile(filePath) {
                const fullPath = resolveStorageKey(basePath, filePath);
                await fs.unlink(fullPath);
            },
            async listFiles(prefix) {
                // Recursively walk the storage root and return keys RELATIVE to basePath,
                // using "/" separators, filtered by prefix. (The previous flat readdir only
                // saw top-level entries, so it missed files under tenant/user subfolders.)
                const results = [];
                async function walk(dir) {
                    let entries;
                    try {
                        entries = await fs.readdir(dir, { withFileTypes: true });
                    }
                    catch {
                        return;
                    }
                    for (const entry of entries) {
                        const abs = path.join(dir, entry.name);
                        if (entry.isDirectory()) {
                            await walk(abs);
                        }
                        else {
                            const rel = path.relative(basePath, abs).split(path.sep).join("/");
                            if (rel.startsWith(prefix))
                                results.push(rel);
                        }
                    }
                }
                await walk(basePath);
                return results;
            },
            getPublicUrl(filePath) {
                // Encode each path segment so subfolder slashes are preserved as separators
                // but special characters in segments don't break the URL.
                const encoded = String(filePath)
                    .split("/")
                    .map((seg) => encodeURIComponent(seg))
                    .join("/");
                return `/storage/${service}/${encoded}`;
            },
        };
        console.info(`Loaded LOCAL Storage Config for: ${service}`);
    }
    initializeS3Storage(service, upperService) {
        const requiredEnvVars = [
            `${upperService}_STORAGE_ACCESS_KEY_ID`,
            `${upperService}_STORAGE_SECRET_ACCESS_KEY`,
            `${upperService}_STORAGE_REGION`,
            `${upperService}_STORAGE_BUCKET`,
            `${upperService}_STORAGE_ENDPOINT`,
        ];
        requiredEnvVars.forEach((envVar) => {
            if (!env.get(envVar)) {
                throw new Error(`${envVar} is not defined`);
            }
        });
        const s3Client = new S3Client({
            region: env.get(`${upperService}_STORAGE_REGION`),
            credentials: {
                accessKeyId: env.get(`${upperService}_STORAGE_ACCESS_KEY_ID`),
                secretAccessKey: env.get(`${upperService}_STORAGE_SECRET_ACCESS_KEY`),
            },
            endpoint: "https://" + env.get(`${upperService}_STORAGE_ENDPOINT`),
        });
        const bucketName = env.get(`${upperService}_STORAGE_BUCKET`);
        this.providers[service] = {
            driver: "S3",
            s3Client,
            bucketName,
            async saveFile(filePath, fileContent) {
                const command = new PutObjectCommand({
                    Bucket: bucketName,
                    Key: filePath,
                    Body: fileContent,
                });
                await s3Client.send(command);
                return filePath;
            },
            async getFile(filePath) {
                const command = new GetObjectCommand({
                    Bucket: bucketName,
                    Key: filePath,
                });
                const response = await s3Client.send(command);
                return response.Body;
            },
            async deleteFile(filePath) {
                const command = new DeleteObjectCommand({
                    Bucket: bucketName,
                    Key: filePath,
                });
                await s3Client.send(command);
            },
            async listFiles(prefix) {
                const command = new ListObjectsV2Command({
                    Bucket: bucketName,
                    Prefix: prefix,
                });
                const response = await s3Client.send(command);
                return (response.Contents || []).map((obj) => obj.Key).filter(Boolean);
            },
            async getPublicUrl(filePath) {
                const command = new GetObjectCommand({
                    Bucket: bucketName,
                    Key: filePath,
                });
                const expiration = 3600; // 1 hour
                return getSignedUrl(s3Client, command, { expiresIn: expiration });
            },
        };
        console.info(`Loaded S3 Storage Config for: ${service}`);
    }
    getProvider(service) {
        const provider = this.providers[service];
        if (!provider) {
            throw new Error(`Storage provider '${service}' not found`);
        }
        return provider;
    }
    async saveFile(service, filePath, fileContent) {
        const provider = this.getProvider(service);
        return provider.saveFile(filePath, fileContent);
    }
    async getFile(service, filePath) {
        const provider = this.getProvider(service);
        return provider.getFile(filePath);
    }
    async deleteFile(service, filePath) {
        const provider = this.getProvider(service);
        return provider.deleteFile(filePath);
    }
    async getPublicUrl(service, filePath) {
        const provider = this.getProvider(service);
        const result = provider.getPublicUrl(filePath);
        return result instanceof Promise ? await result : result;
    }
}
// Create singleton instance only if it doesn't exist
if (!globalThis.__baasix_storageService) {
    globalThis.__baasix_storageService = new StorageService();
}
const storageService = globalThis.__baasix_storageService;
export default storageService;
//# sourceMappingURL=StorageService.js.map