"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var StorageService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const uuid_1 = require("uuid");
const file_entity_1 = require("../entities/file.entity");
const pending_upload_entity_1 = require("../entities/pending-upload.entity");
const storage_usage_entity_1 = require("../entities/storage-usage.entity");
const s3_provider_1 = require("../providers/s3.provider");
const ALLOWED_MIME_TYPES = {
    images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
    documents: [
        'application/pdf',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ],
    spreadsheets: [
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    ],
    data: ['text/csv', 'application/json', 'text/plain'],
};
const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js', '.cmd', '.com', '.scr'];
let StorageService = StorageService_1 = class StorageService {
    constructor(fileRepository, pendingUploadRepository, usageRepository, s3Provider) {
        this.fileRepository = fileRepository;
        this.pendingUploadRepository = pendingUploadRepository;
        this.usageRepository = usageRepository;
        this.s3Provider = s3Provider;
        this.logger = new common_1.Logger(StorageService_1.name);
    }
    async getUploadUrl(tenantId, userId, dto) {
        if (!this.s3Provider.isConfigured()) {
            throw new common_1.BadRequestException('Storage not configured');
        }
        if (!this.isAllowedMimeType(dto.mimeType)) {
            throw new common_1.BadRequestException(`File type not allowed: ${dto.mimeType}`);
        }
        if (this.hasBlockedExtension(dto.filename)) {
            throw new common_1.BadRequestException('File extension not allowed');
        }
        const canUpload = await this.canUpload(tenantId, dto.sizeBytes);
        if (!canUpload.allowed) {
            throw new common_1.ForbiddenException(canUpload.reason);
        }
        const uploadId = (0, uuid_1.v4)();
        const folder = dto.folder || 'files';
        const path = this.s3Provider.generatePath(tenantId, folder, uploadId, dto.filename);
        const bucket = this.s3Provider.getBucket();
        const presigned = await this.s3Provider.getUploadUrl({
            bucket,
            key: path,
            contentType: dto.mimeType,
            contentLength: dto.sizeBytes,
            metadata: {
                'tenant-id': tenantId,
                'upload-id': uploadId,
                'user-id': userId,
            },
        });
        const pendingUpload = this.pendingUploadRepository.create({
            id: uploadId,
            tenant_id: tenantId,
            user_id: userId,
            filename: this.sanitizeFilename(dto.filename),
            original_name: dto.filename,
            mime_type: dto.mimeType,
            size_bytes: dto.sizeBytes,
            folder,
            bucket,
            path,
            provider: this.s3Provider.getProvider(),
            expires_at: presigned.expiresAt,
        });
        await this.pendingUploadRepository.save(pendingUpload);
        return {
            uploadId,
            url: presigned.url,
            expiresAt: presigned.expiresAt,
            maxSize: dto.sizeBytes,
        };
    }
    async confirmUpload(tenantId, userId, dto) {
        const pending = await this.pendingUploadRepository.findOne({
            where: {
                id: dto.uploadId,
                tenant_id: tenantId,
                status: 'pending',
            },
        });
        if (!pending) {
            throw new common_1.NotFoundException('Upload not found or expired');
        }
        if (pending.expires_at < new Date()) {
            await this.pendingUploadRepository.update(pending.id, { status: 'expired' });
            throw new common_1.BadRequestException('Upload expired');
        }
        const headResult = await this.s3Provider.headObject(pending.path);
        if (!headResult) {
            await this.pendingUploadRepository.update(pending.id, { status: 'failed' });
            throw new common_1.BadRequestException('File not found in storage');
        }
        const file = this.fileRepository.create({
            tenant_id: tenantId,
            uploaded_by: userId,
            filename: pending.filename,
            original_name: pending.original_name,
            mime_type: pending.mime_type,
            size_bytes: headResult.contentLength,
            bucket: pending.bucket,
            path: pending.path,
            provider: pending.provider,
            folder: pending.folder,
            status: file_entity_1.FileStatus.READY,
            visibility: file_entity_1.FileVisibility.PRIVATE,
            metadata: dto.metadata || {},
        });
        await this.fileRepository.save(file);
        await this.pendingUploadRepository.update(pending.id, {
            status: 'completed',
            completed_at: new Date(),
        });
        return this.toFileResponse(file);
    }
    async listFiles(tenantId, dto) {
        const { page = 1, limit = 20, folder, mimeType, search } = dto;
        const skip = (page - 1) * limit;
        const where = {
            tenant_id: tenantId,
            deleted_at: (0, typeorm_2.IsNull)(),
            status: file_entity_1.FileStatus.READY,
        };
        if (folder) {
            where.folder = folder;
        }
        if (mimeType) {
            where.mime_type = (0, typeorm_2.Like)(`${mimeType}%`);
        }
        if (search) {
            where.original_name = (0, typeorm_2.ILike)(`%${search}%`);
        }
        const [files, total] = await this.fileRepository.findAndCount({
            where,
            order: { created_at: 'DESC' },
            skip,
            take: limit,
        });
        return {
            data: files.map((f) => this.toFileResponse(f)),
            total,
            page,
            limit,
            totalPages: Math.ceil(total / limit),
        };
    }
    async getFile(tenantId, fileId) {
        const file = await this.fileRepository.findOne({
            where: {
                id: fileId,
                tenant_id: tenantId,
                deleted_at: (0, typeorm_2.IsNull)(),
            },
        });
        if (!file) {
            throw new common_1.NotFoundException('File not found');
        }
        return this.toFileResponse(file);
    }
    async getDownloadUrl(tenantId, fileId) {
        const file = await this.fileRepository.findOne({
            where: {
                id: fileId,
                tenant_id: tenantId,
                deleted_at: (0, typeorm_2.IsNull)(),
                status: file_entity_1.FileStatus.READY,
            },
        });
        if (!file) {
            throw new common_1.NotFoundException('File not found');
        }
        const result = await this.s3Provider.getDownloadUrl(file.path);
        return {
            url: result.url,
            expiresAt: result.expiresAt,
        };
    }
    async updateFile(tenantId, fileId, dto) {
        const file = await this.fileRepository.findOne({
            where: {
                id: fileId,
                tenant_id: tenantId,
                deleted_at: (0, typeorm_2.IsNull)(),
            },
        });
        if (!file) {
            throw new common_1.NotFoundException('File not found');
        }
        if (dto.folder)
            file.folder = dto.folder;
        if (dto.visibility)
            file.visibility = dto.visibility;
        if (dto.metadata)
            file.metadata = { ...file.metadata, ...dto.metadata };
        await this.fileRepository.save(file);
        return this.toFileResponse(file);
    }
    async deleteFile(tenantId, fileId) {
        const file = await this.fileRepository.findOne({
            where: {
                id: fileId,
                tenant_id: tenantId,
                deleted_at: (0, typeorm_2.IsNull)(),
            },
        });
        if (!file) {
            throw new common_1.NotFoundException('File not found');
        }
        file.deleted_at = new Date();
        file.status = file_entity_1.FileStatus.DELETED;
        await this.fileRepository.save(file);
        try {
            await this.s3Provider.deleteObject(file.path);
        }
        catch (error) {
            this.logger.warn(`Failed to delete file from S3: ${file.path}`, error);
        }
    }
    async getUsage(tenantId) {
        let usage = await this.usageRepository.findOne({
            where: { tenant_id: tenantId },
        });
        if (!usage) {
            usage = this.usageRepository.create({
                tenant_id: tenantId,
                total_files: 0,
                total_bytes: 0,
            });
            await this.usageRepository.save(usage);
        }
        const folderStats = await this.fileRepository
            .createQueryBuilder('f')
            .select('f.folder', 'folder')
            .addSelect('COUNT(*)', 'count')
            .where('f.tenant_id = :tenantId', { tenantId })
            .andWhere('f.deleted_at IS NULL')
            .andWhere('f.status = :status', { status: file_entity_1.FileStatus.READY })
            .groupBy('f.folder')
            .getRawMany();
        const filesByFolder = {};
        for (const stat of folderStats) {
            filesByFolder[stat.folder] = parseInt(stat.count, 10);
        }
        const usagePercent = usage.max_bytes
            ? Math.round((Number(usage.total_bytes) / Number(usage.max_bytes)) * 10000) / 100
            : 0;
        return {
            totalFiles: usage.total_files,
            totalBytes: Number(usage.total_bytes),
            maxBytes: usage.max_bytes ? Number(usage.max_bytes) : null,
            maxFileSize: usage.max_file_size ? Number(usage.max_file_size) : null,
            usagePercent,
            filesByFolder,
        };
    }
    isAllowedMimeType(mimeType) {
        const allAllowed = Object.values(ALLOWED_MIME_TYPES).flat();
        return allAllowed.includes(mimeType);
    }
    hasBlockedExtension(filename) {
        const lower = filename.toLowerCase();
        return BLOCKED_EXTENSIONS.some((ext) => lower.endsWith(ext));
    }
    sanitizeFilename(filename) {
        return filename.replace(/[^a-zA-Z0-9.-]/g, '_');
    }
    async canUpload(tenantId, sizeBytes) {
        const usage = await this.usageRepository.findOne({
            where: { tenant_id: tenantId },
        });
        if (!usage) {
            return { allowed: true };
        }
        if (usage.max_file_size && sizeBytes > Number(usage.max_file_size)) {
            return {
                allowed: false,
                reason: `File size exceeds limit (max: ${this.formatBytes(Number(usage.max_file_size))})`,
            };
        }
        if (usage.max_bytes &&
            Number(usage.total_bytes) + sizeBytes > Number(usage.max_bytes)) {
            return {
                allowed: false,
                reason: `Storage limit exceeded (max: ${this.formatBytes(Number(usage.max_bytes))})`,
            };
        }
        return { allowed: true };
    }
    formatBytes(bytes) {
        if (bytes < 1024)
            return `${bytes} B`;
        if (bytes < 1024 * 1024)
            return `${(bytes / 1024).toFixed(1)} KB`;
        if (bytes < 1024 * 1024 * 1024)
            return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
        return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
    }
    toFileResponse(file) {
        return {
            id: file.id,
            filename: file.filename,
            originalName: file.original_name,
            mimeType: file.mime_type,
            sizeBytes: Number(file.size_bytes),
            folder: file.folder,
            visibility: file.visibility,
            thumbnails: file.thumbnails,
            metadata: file.metadata,
            createdAt: file.created_at,
            updatedAt: file.updated_at,
        };
    }
};
exports.StorageService = StorageService;
exports.StorageService = StorageService = StorageService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(file_entity_1.FileEntity)),
    __param(1, (0, typeorm_1.InjectRepository)(pending_upload_entity_1.PendingUploadEntity)),
    __param(2, (0, typeorm_1.InjectRepository)(storage_usage_entity_1.StorageUsageEntity)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        s3_provider_1.S3Provider])
], StorageService);
//# sourceMappingURL=storage.service.js.map