"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3StorageBackend = void 0;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const client_s3_1 = require("@aws-sdk/client-s3");
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
const mime_1 = require("./mime");
const STAGING_DIR = path.join(os.tmpdir(), 'lt-staging');
class S3StorageBackend {
    constructor() {
        this.signingClient = null;
        const endpoint = process.env.LT_S3_ENDPOINT;
        const publicEndpoint = process.env.LT_S3_PUBLIC_ENDPOINT;
        const region = process.env.LT_S3_REGION || 'us-east-1';
        const forcePathStyle = process.env.LT_S3_FORCE_PATH_STYLE === 'true';
        this.bucket = process.env.LT_S3_BUCKET || 'long-tail-files';
        const clientConfig = {
            region,
            forcePathStyle,
        };
        if (endpoint) {
            clientConfig.endpoint = endpoint;
        }
        // Explicit credentials (MinIO, HMAC keys). If absent, SDK falls through
        // to IAM role / instance metadata (standard AWS behavior).
        const accessKey = process.env.LT_S3_ACCESS_KEY;
        const secretKey = process.env.LT_S3_SECRET_KEY;
        if (accessKey && secretKey) {
            clientConfig.credentials = {
                accessKeyId: accessKey,
                secretAccessKey: secretKey,
            };
        }
        this.client = new client_s3_1.S3Client(clientConfig);
        // Signed URLs need the public-facing endpoint so browsers can reach them.
        // In Docker, LT_S3_ENDPOINT is the internal hostname (e.g. http://minio:9000)
        // while LT_S3_PUBLIC_ENDPOINT is the host-accessible URL (e.g. http://localhost:9000).
        if (publicEndpoint && publicEndpoint !== endpoint) {
            this.signingClient = new client_s3_1.S3Client({ ...clientConfig, endpoint: publicEndpoint });
        }
        this.bucketReady = this.ensureBucket();
    }
    async ensureBucket() {
        try {
            await this.client.send(new client_s3_1.HeadBucketCommand({ Bucket: this.bucket }));
        }
        catch {
            try {
                await this.client.send(new client_s3_1.CreateBucketCommand({ Bucket: this.bucket }));
            }
            catch (err) {
                // Bucket may have been created concurrently
                if (err.name !== 'BucketAlreadyOwnedByYou' && err.name !== 'BucketAlreadyExists') {
                    throw err;
                }
            }
        }
    }
    async write(key, data) {
        await this.bucketReady;
        await this.client.send(new client_s3_1.PutObjectCommand({
            Bucket: this.bucket,
            Key: normalizeKey(key),
            Body: data,
        }));
        return { ref: key, size: data.length };
    }
    async read(key) {
        await this.bucketReady;
        const resp = await this.client.send(new client_s3_1.GetObjectCommand({
            Bucket: this.bucket,
            Key: normalizeKey(key),
        }));
        const chunks = [];
        for await (const chunk of resp.Body) {
            chunks.push(Buffer.from(chunk));
        }
        const data = Buffer.concat(chunks);
        return { data, size: data.length };
    }
    async list(prefix, pattern) {
        await this.bucketReady;
        const normalizedPrefix = prefix ? normalizeKey(prefix) : undefined;
        // Use Delimiter to mimic single-directory listing (like readdirSync)
        const listPrefix = normalizedPrefix ? (normalizedPrefix.endsWith('/') ? normalizedPrefix : normalizedPrefix + '/') : undefined;
        const resp = await this.client.send(new client_s3_1.ListObjectsV2Command({
            Bucket: this.bucket,
            Prefix: listPrefix,
            Delimiter: '/',
        }));
        const files = [];
        for (const obj of resp.Contents || []) {
            if (!obj.Key || !obj.Size)
                continue;
            // Extract filename from key for pattern matching
            const fileName = obj.Key.split('/').pop() || '';
            if (pattern && !fileName.match(new RegExp(pattern.replace(/\*/g, '.*'))))
                continue;
            files.push({
                path: obj.Key,
                size: obj.Size,
                modified_at: obj.LastModified?.toISOString() || new Date().toISOString(),
            });
        }
        return { files };
    }
    async delete(key) {
        await this.bucketReady;
        try {
            await this.client.send(new client_s3_1.DeleteObjectCommand({
                Bucket: this.bucket,
                Key: normalizeKey(key),
            }));
            return { deleted: true };
        }
        catch {
            return { deleted: false };
        }
    }
    async getLocalPath(key) {
        const localPath = path.join(STAGING_DIR, normalizeKey(key));
        const dir = path.dirname(localPath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        return localPath;
    }
    async commitLocalPath(key, localPath) {
        const data = fs.readFileSync(localPath);
        await this.write(key, data);
        // Clean up staging file
        try {
            fs.unlinkSync(localPath);
        }
        catch { /* ignore */ }
        return { size: data.length };
    }
    async createReadStream(key) {
        await this.bucketReady;
        const resp = await this.client.send(new client_s3_1.GetObjectCommand({
            Bucket: this.bucket,
            Key: normalizeKey(key),
        }));
        return resp.Body;
    }
    async listWithPrefixes(prefix, pageSize, continuationToken) {
        await this.bucketReady;
        const normalizedPrefix = prefix ? normalizeKey(prefix) : undefined;
        const listPrefix = normalizedPrefix
            ? (normalizedPrefix.endsWith('/') ? normalizedPrefix : normalizedPrefix + '/')
            : undefined;
        const resp = await this.client.send(new client_s3_1.ListObjectsV2Command({
            Bucket: this.bucket,
            Prefix: listPrefix,
            Delimiter: '/',
            MaxKeys: pageSize || 100,
            ContinuationToken: continuationToken || undefined,
        }));
        const files = [];
        for (const obj of resp.Contents || []) {
            if (!obj.Key)
                continue;
            // Skip the prefix itself if it appears as a "file"
            if (obj.Key === listPrefix)
                continue;
            files.push({
                path: obj.Key,
                size: obj.Size || 0,
                modified_at: obj.LastModified?.toISOString() || new Date().toISOString(),
            });
        }
        const directories = [];
        for (const cp of resp.CommonPrefixes || []) {
            if (cp.Prefix) {
                directories.push(cp.Prefix);
            }
        }
        return {
            files,
            directories,
            nextToken: resp.NextContinuationToken || undefined,
        };
    }
    async getMetadata(key) {
        await this.bucketReady;
        const resp = await this.client.send(new client_s3_1.HeadObjectCommand({
            Bucket: this.bucket,
            Key: normalizeKey(key),
        }));
        return {
            size: resp.ContentLength || 0,
            modified_at: resp.LastModified?.toISOString() || new Date().toISOString(),
            content_type: resp.ContentType || (0, mime_1.mimeFromPath)(key),
        };
    }
    async getSignedUrl(key, expiresInSeconds) {
        await this.bucketReady;
        const command = new client_s3_1.GetObjectCommand({
            Bucket: this.bucket,
            Key: normalizeKey(key),
        });
        // Use the public-facing client so the signed URL hostname is reachable from browsers
        const client = this.signingClient || this.client;
        return (0, s3_request_presigner_1.getSignedUrl)(client, command, { expiresIn: expiresInSeconds });
    }
}
exports.S3StorageBackend = S3StorageBackend;
/** Strip leading slashes to create a valid S3 key; reject traversal. */
function normalizeKey(key) {
    const normalized = key.replace(/^\/+/, '');
    if (normalized.includes('..')) {
        throw new Error(`Path traversal denied: ${key}`);
    }
    return normalized;
}
