"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); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileService = void 0;
const api_locale_1 = require("@hed-hog/api-locale");
const api_pagination_1 = require("@hed-hog/api-pagination");
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const fs_1 = require("fs");
const path_1 = require("path");
const promises_1 = require("stream/promises");
const setting_service_1 = require("../setting/setting.service");
const provider_enum_1 = require("./provider/provider.enum");
const provider_factory_1 = require("./provider/provider.factory");
let FileService = class FileService {
    hasMojibake(value) {
        return /Ã.|Â.|â[\u0080-\u00BF]/.test(value);
    }
    normalizeUploadedFilename(filename) {
        if (!filename) {
            return filename;
        }
        if (!this.hasMojibake(filename)) {
            return filename;
        }
        const decoded = Buffer.from(filename, 'latin1').toString('utf8');
        return this.hasMojibake(decoded) ? filename : decoded;
    }
    constructor(prismaService, paginationService, settingService, jwtService) {
        this.prismaService = prismaService;
        this.paginationService = paginationService;
        this.settingService = settingService;
        this.jwtService = jwtService;
        this.mimetypes = {};
    }
    async onModuleInit() {
        await this.getProvider();
    }
    async getProvider() {
        const settings = await this.settingService.getSettingValues([
            'storage',
            'storage-local-path',
            'storage-s3-key',
            'storage-s3-secret',
            'storage-s3-region',
            'storage-s3-bucket',
            'storage-max-size',
            'storage-accept-mimetype',
            'storage-abs-account',
            'storage-abs-key',
            'storage-abs-container',
            'storage-gcs-keyfile',
        ]);
        if (!settings['storage']) {
            throw new common_1.BadRequestException(`You must set the storage provider in the setting.`);
        }
        const providerName = settings['storage'];
        const provider = provider_factory_1.ProviderFactory.create(providerName, settings, this.jwtService);
        const providerData = await this.prismaService.file_provider.findFirst({
            where: {
                slug: providerName,
            },
            select: {
                id: true,
            },
        });
        if (!providerData) {
            throw new common_1.BadRequestException(`Provider ${providerName} not found.`);
        }
        this.providerId = providerData.id;
        return provider;
    }
    async getMimeType(mimetype) {
        if (!mimetype) {
            throw new common_1.BadRequestException(`No mimetype provided`);
        }
        if (this.mimetypes[mimetype]) {
            return this.mimetypes[mimetype];
        }
        let result = await this.prismaService.file_mimetype.findFirst({
            where: {
                name: mimetype,
            },
        });
        if (!result) {
            result = await this.prismaService.file_mimetype.create({
                data: {
                    name: mimetype,
                },
            });
        }
        return (this.mimetypes[mimetype] = result.id);
    }
    async acceptMimetypes(mimetype) {
        if (!mimetype) {
            throw new common_1.BadRequestException(`No mimetype provided`);
        }
        await this.getProvider();
        const settings = await this.settingService.getSettingValues([
            'storage',
            'storage-local-path',
            'storage-s3-key',
            'storage-s3-secret',
            'storage-s3-region',
            'storage-s3-bucket',
            'storage-max-size',
            'storage-accept-mimetype',
            'storage-abs-account',
            'storage-abs-key',
            'storage-abs-container',
            'storage-gcs-keyfile',
        ]);
        const acceptMimetypes = settings['storage-accept-mimetype'];
        return acceptMimetypes.indexOf(mimetype) !== -1;
    }
    async maxFileSize(size) {
        if (!size) {
            throw new common_1.BadRequestException(`No size provided`);
        }
        await this.getProvider();
        const settings = await this.settingService.getSettingValues([
            'storage',
            'storage-local-path',
            'storage-s3-key',
            'storage-s3-secret',
            'storage-s3-region',
            'storage-s3-bucket',
            'storage-max-size',
            'storage-accept-mimetype',
            'storage-abs-account',
            'storage-abs-key',
            'storage-abs-container',
            'storage-gcs-keyfile',
        ]);
        const maxSize = settings['storage-max-size'];
        return size <= Number(maxSize);
    }
    async uploadFromString(destination, filename, filecontent, mimetype = 'text/plain') {
        if (!filecontent) {
            throw new common_1.BadRequestException(`No file content provided`);
        }
        if (!destination) {
            throw new common_1.BadRequestException(`No destination provided`);
        }
        if (!filename) {
            throw new common_1.BadRequestException(`No filename provided`);
        }
        const provider = await this.getProvider();
        if (!(await this.acceptMimetypes(mimetype))) {
            throw new common_1.BadRequestException(`Invalid file type: ${mimetype}`);
        }
        const url = await provider.uploadFromString(destination, filename, filecontent, mimetype);
        const file = await this.prismaService.file.create({
            data: {
                filename,
                path: url,
                provider_id: this.providerId,
                location: destination,
                mimetype_id: await this.getMimeType(mimetype),
                size: filecontent.length,
            },
        });
        return file;
    }
    async getStorage() {
        const settings = await this.settingService.getSettingValues([
            'storage',
        ]);
        return settings['storage'];
    }
    getProviderPath(storage, filepath) {
        if (storage === provider_enum_1.EnumProvider.LOCAL) {
            return filepath;
        }
        try {
            return new URL(filepath).pathname;
        }
        catch (_a) {
            return filepath;
        }
    }
    mapStorageNotFoundError(error, fileId) {
        var _a;
        const missingInStorage = (error === null || error === void 0 ? void 0 : error.name) === 'NoSuchKey' ||
            (error === null || error === void 0 ? void 0 : error.Code) === 'NoSuchKey' ||
            ((_a = error === null || error === void 0 ? void 0 : error.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode) === 404;
        if (missingInStorage) {
            throw new common_1.NotFoundException(`File not found in storage: ${fileId}`);
        }
        throw error;
    }
    async openReadStreamById(fileId) {
        var _a;
        if (!fileId) {
            throw new common_1.BadRequestException(`No file ID provided`);
        }
        const file = await this.prismaService.file.findUnique({
            where: {
                id: fileId,
            },
            include: {
                file_mimetype: true,
            },
        });
        if (!file) {
            throw new common_1.NotFoundException(`File not found: ${fileId}`);
        }
        const storage = await this.getStorage();
        const provider = await this.getProvider();
        const providerPath = this.getProviderPath(storage, file.path);
        let metadata;
        try {
            metadata = await provider.metaData(providerPath);
        }
        catch (_b) {
            metadata = undefined;
        }
        try {
            const stream = await provider.readStream(providerPath);
            return {
                file,
                stream,
                size: (_a = file.size) !== null && _a !== void 0 ? _a : this.getFileSizeFromMetadata(metadata),
            };
        }
        catch (error) {
            this.mapStorageNotFoundError(error, fileId);
        }
    }
    async downloadToPath(fileId, destinationPath, options) {
        if (!destinationPath) {
            throw new common_1.BadRequestException(`No destination path provided`);
        }
        const { file, stream, size } = await this.openReadStreamById(fileId);
        if ((options === null || options === void 0 ? void 0 : options.maxBytes) && size && size > options.maxBytes) {
            throw new common_1.BadRequestException(`File too large for streaming download: ${size} bytes (max ${options.maxBytes})`);
        }
        await fs_1.promises.mkdir((0, path_1.dirname)(destinationPath), { recursive: true });
        try {
            await (0, promises_1.pipeline)(stream, (0, fs_1.createWriteStream)(destinationPath));
            const stats = await fs_1.promises.stat(destinationPath);
            if ((options === null || options === void 0 ? void 0 : options.maxBytes) && stats.size > options.maxBytes) {
                await fs_1.promises.unlink(destinationPath).catch(() => undefined);
                throw new common_1.BadRequestException(`File too large for streaming download: ${stats.size} bytes (max ${options.maxBytes})`);
            }
            return {
                file,
                path: destinationPath,
                size: stats.size,
            };
        }
        catch (error) {
            await fs_1.promises.unlink(destinationPath).catch(() => undefined);
            throw error;
        }
    }
    async getBuffer(fileId) {
        var _a;
        if (!fileId) {
            throw new common_1.BadRequestException(`No file ID provided`);
        }
        const file = await this.prismaService.file.findUnique({
            where: {
                id: fileId,
            },
            include: {
                file_mimetype: true,
            },
        });
        if (!file) {
            throw new common_1.NotFoundException(`File not found: ${fileId}`);
        }
        const storage = await this.getStorage();
        const provider = await this.getProvider();
        const providerPath = this.getProviderPath(storage, file.path);
        let providerBuffer;
        try {
            providerBuffer = await provider.buffer(providerPath);
        }
        catch (error) {
            const missingInStorage = (error === null || error === void 0 ? void 0 : error.name) === 'NoSuchKey' ||
                (error === null || error === void 0 ? void 0 : error.Code) === 'NoSuchKey' ||
                ((_a = error === null || error === void 0 ? void 0 : error.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode) === 404;
            if (missingInStorage) {
                throw new common_1.NotFoundException(`File not found in storage: ${fileId}`);
            }
            throw error;
        }
        const buffer = Buffer.from(providerBuffer);
        return {
            file,
            buffer,
        };
    }
    async getLocalFile(fileId, res) {
        const { buffer, file } = await this.getBuffer(fileId);
        res.set({
            'Content-Type': file.file_mimetype.name,
            'Content-Length': buffer.length,
        });
        res.send(buffer);
    }
    async upload(destination, fileBuffer) {
        if (!fileBuffer) {
            throw new common_1.BadRequestException(`No file provided`);
        }
        if (!destination) {
            throw new common_1.BadRequestException(`No destination provided`);
        }
        const provider = await this.getProvider();
        if (!(await this.acceptMimetypes(fileBuffer.mimetype))) {
            throw new common_1.BadRequestException(`Invalid file type: ${fileBuffer.mimetype}`);
        }
        if (!(await this.maxFileSize(fileBuffer.size))) {
            throw new common_1.BadRequestException(`File too large: ${fileBuffer.size} bytes`);
        }
        const normalizedFilename = this.normalizeUploadedFilename(fileBuffer.originalname);
        const normalizedFile = Object.assign(Object.assign({}, fileBuffer), { originalname: normalizedFilename });
        const url = await provider.upload(destination, normalizedFile);
        const file = await this.prismaService.file.create({
            data: {
                filename: normalizedFilename,
                path: url,
                provider_id: this.providerId,
                location: destination,
                mimetype_id: await this.getMimeType(fileBuffer.mimetype),
                size: fileBuffer.size,
            },
        });
        return file;
    }
    async uploadFromPath(destination, filePath, metadata) {
        var _a, _b, _c, _d, _e;
        if (!filePath) {
            throw new common_1.BadRequestException(`No file path provided`);
        }
        if (!destination) {
            throw new common_1.BadRequestException(`No destination provided`);
        }
        const stats = await fs_1.promises.stat(filePath).catch(() => null);
        if (!stats || !stats.isFile()) {
            throw new common_1.NotFoundException(`File path not found: ${filePath}`);
        }
        const size = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.size) !== null && _a !== void 0 ? _a : stats.size;
        const mimetype = (_b = metadata === null || metadata === void 0 ? void 0 : metadata.mimetype) !== null && _b !== void 0 ? _b : 'application/octet-stream';
        const originalname = this.normalizeUploadedFilename((_d = (_c = metadata === null || metadata === void 0 ? void 0 : metadata.originalname) !== null && _c !== void 0 ? _c : metadata === null || metadata === void 0 ? void 0 : metadata.filename) !== null && _d !== void 0 ? _d : (0, path_1.basename)(filePath));
        if (!(await this.acceptMimetypes(mimetype))) {
            throw new common_1.BadRequestException(`Invalid file type: ${mimetype}`);
        }
        if (!(await this.maxFileSize(size))) {
            throw new common_1.BadRequestException(`File too large: ${size} bytes`);
        }
        const provider = await this.getProvider();
        const providerWithPathUpload = provider;
        let url;
        if (typeof providerWithPathUpload.uploadFromPath === 'function') {
            url = await providerWithPathUpload.uploadFromPath(destination, filePath, {
                filename: originalname,
                mimetype,
            });
        }
        else {
            // Fallback explícito para providers legados sem suporte nativo por path.
            const buffer = await fs_1.promises.readFile(filePath);
            url = await provider.upload(destination, {
                fieldname: 'file',
                originalname,
                encoding: (_e = metadata === null || metadata === void 0 ? void 0 : metadata.encoding) !== null && _e !== void 0 ? _e : '7bit',
                mimetype,
                size,
                destination: '',
                filename: originalname,
                path: filePath,
                buffer,
            });
        }
        const file = await this.prismaService.file.create({
            data: {
                filename: originalname,
                path: url,
                provider_id: this.providerId,
                location: destination,
                mimetype_id: await this.getMimeType(mimetype),
                size,
            },
        });
        return file;
    }
    async delete(locale, { ids }) {
        const files = await this.prismaService.file.findMany({
            where: {
                id: {
                    in: ids,
                },
            },
            select: {
                id: true,
                path: true,
            },
        });
        if (files.length !== ids.length) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('file.delete.some_not_found', locale, `Some files not found`));
        }
        for (const file of files) {
            await (await this.getProvider()).delete(file.path);
            await this.prismaService.file.deleteMany({
                where: {
                    id: file.id,
                },
            });
        }
        return files.map((file) => file.id);
    }
    async getFiles(paginationParams) {
        const fields = ['filename', 'path'];
        const OR = this.prismaService.createInsensitiveSearch(fields, paginationParams);
        return this.paginationService.paginate(this.prismaService.file, paginationParams, {
            where: {
                OR,
            },
        });
    }
    async get(fileId) {
        const file = await this.prismaService.file.findUnique({
            where: { id: fileId },
        });
        if (!file) {
            throw new common_1.NotFoundException(`File not found: ${fileId}`);
        }
        return file;
    }
    async readStream(filepath) {
        return (await this.getProvider()).readStream(filepath);
    }
    getFileSizeFromMetadata(metadata) {
        if (!metadata) {
            return undefined;
        }
        if (typeof metadata.size === 'number') {
            return metadata.size;
        }
        if (typeof metadata.ContentLength === 'number') {
            return metadata.ContentLength;
        }
        if (typeof metadata.contentLength === 'number') {
            return metadata.contentLength;
        }
        if (Array.isArray(metadata) &&
            metadata[0] &&
            metadata[0].size !== undefined) {
            const gcsSize = Number(metadata[0].size);
            return Number.isNaN(gcsSize) ? undefined : gcsSize;
        }
        return undefined;
    }
    async download(locale, token) {
        var _a;
        try {
            const { filepath } = this.jwtService.verify(token, {
                secret: process.env.JWT_SECRET,
            });
            // Buscar o arquivo no banco para pegar nome original e mimetype
            const file = await this.prismaService.file.findFirst({
                where: { path: filepath },
                include: {
                    file_mimetype: true,
                },
            });
            if (!file) {
                throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('file.download.not_found', locale, `Not found: ${filepath}`));
            }
            const provider = await this.getProvider();
            const metadata = await provider.metaData(filepath);
            const stream = await provider.readStream(filepath);
            const size = this.getFileSizeFromMetadata(metadata);
            return {
                stream,
                filename: (file === null || file === void 0 ? void 0 : file.filename) || 'download',
                mimetype: ((_a = file === null || file === void 0 ? void 0 : file.file_mimetype) === null || _a === void 0 ? void 0 : _a.name) || 'application/octet-stream',
                size,
            };
        }
        catch (error) {
            throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('file.download.invalid_token', locale, error.message));
        }
    }
    async openById(locale, fileId) {
        var _a;
        const file = await this.prismaService.file.findFirst({
            where: { id: fileId },
            include: {
                file_mimetype: true,
            },
        });
        if (!file) {
            throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('file.download.not_found', locale, `Not found: ${fileId}`));
        }
        const storage = await this.getStorage();
        const provider = await this.getProvider();
        const providerPath = this.getProviderPath(storage, file.path);
        const metadata = await provider.metaData(providerPath);
        const stream = await provider.readStream(providerPath);
        const size = this.getFileSizeFromMetadata(metadata);
        return {
            stream,
            filename: (file === null || file === void 0 ? void 0 : file.filename) || 'download',
            mimetype: ((_a = file === null || file === void 0 ? void 0 : file.file_mimetype) === null || _a === void 0 ? void 0 : _a.name) || 'application/octet-stream',
            size,
        };
    }
    async tempURL(filepath, expires = 3600) {
        return (await this.getProvider()).tempURL(filepath, expires);
    }
    async tempOpenURL(filepath, expires = 3600) {
        const url = await this.tempURL(filepath, expires);
        if (typeof url === 'string') {
            return url.replace('/file/download/', '/file/open/');
        }
        return url;
    }
    createDownloadToken(filepath, expires = 3600) {
        if (!filepath) {
            throw new common_1.BadRequestException(`No file path provided`);
        }
        return this.jwtService.sign({ filepath }, {
            secret: process.env.JWT_SECRET,
            expiresIn: `${Math.max(1, Number(expires) || 3600)}s`,
        });
    }
};
exports.FileService = FileService;
exports.FileService = FileService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_prisma_1.PrismaService))),
    __param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_pagination_1.PaginationService))),
    __param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => setting_service_1.SettingService))),
    __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => jwt_1.JwtService))),
    __metadata("design:paramtypes", [api_prisma_1.PrismaService,
        api_pagination_1.PaginationService,
        setting_service_1.SettingService,
        jwt_1.JwtService])
], FileService);
//# sourceMappingURL=file.service.js.map