import env from "../utils/env.js";
import { promises as fs, createWriteStream } from "fs";
import path from "path";
import os from "os";
import crypto from "crypto";
import { getProjectPath } from "../utils/dirname.js";
import sharp from "sharp";
// @ts-ignore - No type definitions available
import ffprobe from "ffprobe";
// @ts-ignore - No type definitions available
import { path as ffprobePath } from "ffprobe-static";
import { lookup } from "mime-types";
import ItemsService from "./ItemsService.js";
import storageService from "./StorageService.js";
import { APIError } from "../utils/errorHandler.js";
import axios from "axios";
// Import baasix_File table schema
// This will need to be imported from your schema file
// For now, we'll assume it's available
class FilesService {
    accountability;
    storageService;
    itemService;
    tempDir;
    constructor(params = {}) {
        const { accountability } = params;
        this.accountability = accountability;
        this.storageService = storageService;
        this.itemService = new ItemsService("baasix_File", { accountability });
        this.tempDir = env.get("STORAGE_TEMP_PATH") || os.tmpdir();
    }
    /**
     * Coerce metadata values to correct types
     * Form data sends booleans as strings, need to convert them
     */
    coerceMetadataTypes(metadata) {
        const coerced = { ...metadata };
        // Handle isPublic boolean field
        if ('isPublic' in coerced) {
            if (typeof coerced.isPublic === 'string') {
                coerced.isPublic = coerced.isPublic.toLowerCase() === 'true';
            }
        }
        return coerced;
    }
    async createOne(fileData, metadata = {}) {
        const { file } = fileData;
        if (!file)
            throw new APIError("File is required", 400);
        // Coerce metadata types (form data sends booleans as strings)
        const coercedMetadata = this.coerceMetadataTypes(metadata);
        const storage = coercedMetadata.storage || env.get("STORAGE_DEFAULT_SERVICE");
        let uniqueid = null;
        try {
            // Upload to temp location
            const tempPath = file.path || (await this.uploadToTemp(file));
            // Create a temporary entry with required fields
            const { title, name, description, originalFilename, ...remainingMetadata } = coercedMetadata;
            // Use originalFilename from metadata if provided, otherwise use uploaded file name
            const resolvedOriginalFilename = originalFilename || file.name;
            const initFileDetails = {
                title: title || file.name,
                filename: name || file.name,
                originalFilename: resolvedOriginalFilename,
                description: description || null,
                storage: storage,
                size: file.size,
                type: file.mimetype || file.type || (await this.getFileType(tempPath)) || "application/octet-stream",
                ...remainingMetadata
            };
            uniqueid = await this.itemService.createOne(initFileDetails);
            const uploadedFile = await this.handleFileUpload(tempPath, file.name, storage, uniqueid);
            const fileDetails = await this.getFileDetails(uploadedFile, tempPath, {
                ...coercedMetadata,
                storage,
                filename: uploadedFile.filename,
                originalFilename: resolvedOriginalFilename,
            });
            // Update the entry with full file details
            // Using Drizzle instead of Sequelize
            // Bypass permissions since user already passed create permission check
            // and this is an internal update to the record they just created
            await this.itemService.updateOne(uniqueid, fileDetails, { bypassPermissions: true });
            // Clean up temp file
            await fs.unlink(tempPath);
            return uniqueid;
        }
        catch (error) {
            // If an error occurs, attempt to delete the temporary entry
            // Bypass permissions since this is cleanup for a failed create operation
            if (uniqueid) {
                await this.itemService.deleteOne(uniqueid, { bypassPermissions: true }).catch(console.error);
            }
            throw error;
        }
    }
    async uploadToTemp(file) {
        console.log("Uploading to temp", file.name);
        const originalExtension = path.extname(file.name);
        const tempFilename = crypto.randomBytes(16).toString("hex") + originalExtension;
        // Create temp directory if it doesn't exist
        await fs.mkdir(this.tempDir, { recursive: true });
        const tempPath = path.join(this.tempDir, tempFilename);
        if (file.path) {
            // File is on disk, move it to temp
            await fs.copyFile(file.path, tempPath);
        }
        else if (file.data) {
            // File is in memory, write it to temp
            await fs.writeFile(tempPath, file.data);
        }
        else {
            throw new APIError("Invalid file object", 400);
        }
        return tempPath;
    }
    async handleFileUpload(tempPath, originalFilename, storage, uniqueid) {
        const provider = this.storageService.getProvider(storage);
        const extension = path.extname(originalFilename);
        const filename = `${uniqueid}-${path.basename(originalFilename, extension).substring(0, 40)}${extension}`;
        let filePath;
        const fileContent = await fs.readFile(tempPath);
        if (provider.driver === "LOCAL") {
            filePath = path.join(provider.basePath, filename);
            const destinationDir = path.dirname(filePath);
            await fs.mkdir(destinationDir, { recursive: true });
            await fs.writeFile(filePath, fileContent);
        }
        else if (provider.driver === "S3") {
            filePath = filename; // For S3, the path is just the key
            await provider.saveFile(filePath, fileContent);
        }
        else {
            throw new APIError(`Unsupported storage driver: ${provider.driver}`, 400);
        }
        return {
            path: filePath,
            filename: filename,
            name: originalFilename,
        };
    }
    async getFileDetails(file, tempPath, { storage, folder, filename, title, description, type, originalFilename }) {
        if (!type)
            type = await this.getFileType(tempPath);
        const fileDetails = {
            filename: filename || file.name,
            title: title || file.name,
            description: description || "",
            storage: storage,
            type: type,
            size: (await fs.stat(tempPath)).size,
            originalFilename: originalFilename || file.name,
        };
        if (fileDetails.type.startsWith("image/")) {
            const metadata = await sharp(tempPath).metadata();
            fileDetails.width = metadata.width;
            fileDetails.height = metadata.height;
            fileDetails.metadata = metadata;
            delete fileDetails.metadata.icc;
        }
        else if (fileDetails.type.startsWith("video/") || fileDetails.type.startsWith("audio/")) {
            const metadata = await ffprobe(tempPath, { path: ffprobePath });
            fileDetails.duration = Math.round(metadata.streams[0].duration);
            fileDetails.metadata = metadata;
        }
        return fileDetails;
    }
    async getFileType(filePath) {
        console.log("Getting file type for", filePath);
        const mimeType = lookup(filePath);
        if (!mimeType)
            return "application/octet-stream";
        return mimeType;
    }
    async readByQuery(query) {
        return this.itemService.readByQuery(query);
    }
    async readOne(id, query, bypassPermissions = false) {
        return this.itemService.readOne(id, query, bypassPermissions);
    }
    async updateOne(id, fileData, metadata = {}) {
        const existingFile = await this.itemService.readOne(id);
        if (!existingFile)
            throw new APIError("File not found", 404);
        // Coerce metadata types (form data sends booleans as strings)
        const coercedMetadata = this.coerceMetadataTypes(metadata);
        const { file } = fileData;
        let fileDetails = {
            ...coercedMetadata,
        };
        if (file) {
            const storage = coercedMetadata.storage || existingFile.storage;
            const uploadedFile = await this.handleFileUpload(file.path, file.name, storage, id);
            fileDetails = {
                ...fileDetails,
                ...(await this.getFileDetails(uploadedFile, file.path, {
                    ...coercedMetadata,
                    storage,
                    filename: uploadedFile.filename,
                })),
            };
        }
        await this.itemService.updateOne(id, fileDetails);
        if (file) {
            await this.deleteFile(existingFile);
        }
        return id;
    }
    async deleteOne(id) {
        const file = await this.itemService.readOne(id);
        if (!file)
            throw new APIError("File not found", 404);
        await this.itemService.deleteOne(id);
        await this.deleteFile(file);
        return id;
    }
    async deleteFile(file) {
        if (!file)
            throw new APIError("File not found", 404);
        const provider = this.storageService.getProvider(file.storage);
        if (provider.driver === "LOCAL") {
            const filePath = path.join(provider.basePath, file.filename);
            if (await fs
                .access(filePath)
                .then(() => true)
                .catch(() => false)) {
                await fs.unlink(filePath);
            }
        }
        else if (provider.driver === "S3") {
            await provider.deleteFile(file.filename);
        }
        else {
            throw new APIError(`Unsupported storage driver: ${provider.driver}`, 400);
        }
        // Clean up any processed/resized versions of this image
        try {
            await this.deleteProcessedVersions(file);
        }
        catch (error) {
            console.error("Failed to delete processed versions:", error);
        }
    }
    /**
     * Delete processed/resized versions of an image file.
     * Override in AssetsService for the actual implementation.
     */
    async deleteProcessedVersions(_file) {
        // No-op in base FilesService; AssetsService overrides this
    }
    async uploadFromUrl(fileUrl, metadata = {}) {
        const fileName = fileUrl.split("/").pop() || "download";
        const tempPath = getProjectPath(env.get("STORAGE_TEMP_PATH") || "temp", `temp-${fileName}`);
        try {
            await fs.mkdir(path.dirname(tempPath), { recursive: true });
            await this.downloadFile(fileUrl, tempPath);
            const fileStats = await fs.stat(tempPath);
            const file = {
                path: tempPath,
                name: fileName,
                size: fileStats.size,
            };
            return this.createOne({ file }, metadata);
        }
        catch (error) {
            console.error(error);
            throw new APIError("Error downloading or uploading the file", 500);
        }
    }
    async downloadFile(url, outputLocationPath) {
        const writer = createWriteStream(outputLocationPath);
        const response = await axios({
            url,
            method: "GET",
            responseType: "stream",
        });
        response.data.pipe(writer);
        return new Promise((resolve, reject) => {
            writer.on("finish", resolve);
            writer.on("error", reject);
        });
    }
}
export default FilesService;
//# sourceMappingURL=FilesService.js.map