Source: backend/controllers/docUploadController.js

/**
 * @fileoverview Diese Datei enthält Controller-Funktionen für das Hochladen und Verarbeiten von Dokumenten.
 * Sie ermöglicht das Hochladen von Dateien, die Extraktion von Textinhalten, die Generierung von Embeddings und die Durchführung von Clustering.
 * Zudem werden Ordnervorschläge basierend auf den Clustering-Ergebnissen bereitgestellt.
 * 
 * @author Luca, Miray, Ayoub
 * Die Funktionen wurden mit Unterstützung von KI-Tools angepasst und optimiert.
 */

const db = require("../../ConnectPostgres");
const path = require("path");
const mammoth = require("mammoth");

const { extractTextContent } = require("../models/modelFileReader");
const modelEmbedding = require("../models/modelEmbedding");
const modelClustering = require("../models/modelClustering");
const { generateKeywords } = require("../models/modelKeywords");
const File = require("../../database/File.js");
const sequelize = require("../../sequelize.config.js");
const folderSuggestion = require("../models/modelFolderSuggestion");

/**
 * Rendert das Upload-Formular für Dokumente.
 * 
 * @function renderUploadForm
 * @param {Object} req - Das Request-Objekt.
 * @param {Object} res - Das Response-Objekt.
 */
exports.renderUploadForm = (req, res) => {
  // Liefere die statische HTML-Datei aus
  res.sendFile(path.join(__dirname, "../../frontend/html/docupload.html"));
};

/**
 * Verarbeitet den Upload einer Datei, extrahiert Text, generiert Embeddings und führt Clustering durch.
 * 
 * @async
 * @function uploadFile
 * @param {Object} req - Das Request-Objekt mit Datei- und Benutzerinformationen.
 * @param {Object} res - Das Response-Objekt für die Serverantwort.
 * @returns {Promise<void>} Antwort mit Upload- und Clustering-Ergebnissen.
 * @throws {Error} Falls ein Fehler beim Hochladen oder Verarbeiten der Datei auftritt.
 */
exports.uploadFile = async (req, res) => {
  try {
    // Überprüfen, ob req.file tatsächlich vorhanden ist
    if (!req.file) {
      return res.status(400).send("No file uploaded");
    }

    const { originalname, buffer, mimetype } = req.file;
    const { folderId, clusteringParams } = req.body; // clusteringParams hinzufügen
    const userId = req.session.userId;

    // Konvertiere folderId in eine Ganzzahl, wenn möglich
    const folderIdInt = parseInt(folderId, 10);
    // Falls folderId leer ist oder keine gültige Zahl ist, auf NULL setzen
    const folderIdToUse = isNaN(folderIdInt) ? null : folderIdInt;

    console.log("Extracting text content...");
    const textContent = await extractTextContent(
      buffer,
      mimetype,
      originalname
    );
    console.log(`Extracted text length: ${textContent.length} characters`);

    const embedding = await modelEmbedding.generateEmbedding(textContent);

    const existingFile = await File.findOne({
      where: {
        file_name: originalname,
        user_id: userId,
      },
      order: [["version", "DESC"]],
    });

    let version = 1;
    let originalFileId = null;

    if (existingFile) {
      version = existingFile.version + 1;
      originalFileId = existingFile.file_id;
    }

    const newFile = await File.create({
      user_id: userId,
      file_name: originalname,
      file_type: mimetype,
      file_data: buffer,
      folder_id: folderIdToUse,
      embedding: sequelize.literal(`'[${embedding.join(", ")}]'`),
      version: version,
      original_file_id: originalFileId,
    });

    const fileId = newFile.file_id;
    await generateKeywordsInBackground(textContent, fileId);

    const allEmbeddings = await File.findAll({
      attributes: ["file_id", "embedding"],
    });

    const existingEmbeddings = allEmbeddings.map((item) => item.embedding);
    existingEmbeddings.push(embedding);

    // Clustering parameters
    const defaultParams = {
      minClusterSize: 3,
      minSamples: 2,
      clusterSelectionMethod: "eom",
      clusterSelectionEpsilon: 0.18,
      anchorInfluence: 0.36,
      semanticThreshold: 0.52,
    };


    const clusteringConfig = {
      ...defaultParams,
      ...JSON.parse(clusteringParams || "{}"), // Allow overriding defaults through API
    };

    // Run clustering with parameters and debug info
    console.log(
      "Starting clustering process with parameters:",
      clusteringConfig
    );
    const clusteringResult = await modelClustering.runClustering(
      existingEmbeddings,
      clusteringConfig,
      userId
    );

    const clusterLabels = clusteringResult.labels;
    const clusterStats = clusteringResult.clusterStats;
    const folderContext = clusteringResult.folderContext;

    console.log("Clustering complete. Results:", clusterLabels);

    // Update cluster labels
    for (let i = 0; i < clusterLabels.length; i++) {
      const fileIdToUpdate =
        i < allEmbeddings.length ? allEmbeddings[i].file_id : fileId;
      await File.update(
        { cluster_label: clusterLabels[i] },
        { where: { file_id: fileIdToUpdate } }
      );
    }

    res.status(201).json({
      message: "File uploaded successfully",
      fileId: fileId,
      clusteringResults: {
        totalDocuments: allEmbeddings.length + 1,
        uniqueClusters: clusterStats.num_clusters,
        noisePoints: clusterStats.noise_points,
        assignedCluster: clusterLabels[clusterLabels.length - 1],
        clusterSizes: clusterStats.cluster_sizes,
      },
      ...(folderContext && {
        folderSuggestions: {
          statistics: folderContext.statistics,
          topAffinities: Object.entries(
            folderContext.affinities[clusterLabels.length - 1] || {}
          )
            .sort(([, a], [, b]) => b - a)
            .slice(0, 5)
            .map(([folderId, score]) => ({
              folderId,
              folderName: folderContext.folderInfo.names[folderId],
              score: Math.round(score * 100) / 100,
            })),
        },
      }),
    });
  } catch (error) {
    console.error("Error processing file:", error);
    res
      .status(422)
      .json({ message: "Error processing file", error: error.message });
  }
};

/**
 * Führt den "Smart Upload" durch: Die Datei wird hochgeladen, verarbeitet und erhält anschließend Ordnervorschläge.
 * 
 * @async
 * @function smartUploadFile
 * @param {Object} req - Das Request-Objekt mit Datei- und Benutzerinformationen.
 * @param {Object} res - Das Response-Objekt für die Serverantwort.
 * @returns {Promise<void>} Antwort mit Upload-Ergebnissen und Ordnervorschlägen.
 * @throws {Error} Falls ein Fehler während der Verarbeitung auftritt.
 */
exports.smartUploadFile = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).send("No file uploaded");
    }

    const { originalname, buffer, mimetype } = req.file;
    const { clusteringParams } = req.body;
    const userId = req.session.userId;

    console.log("Extracting text content...");
    const textContent = await extractTextContent(
      buffer,
      mimetype,
      originalname
    );
    console.log(`Extracted text length: ${textContent.length} characters`);

    const embedding = await modelEmbedding.generateEmbedding(textContent);
    const formattedEmbedding = `[${embedding.join(",")}]`;

    // Check versioning
    const checkQuery = `
            SELECT file_id, version
            FROM main.files
            WHERE file_name = $1 AND user_id = $2
            ORDER BY version DESC
            LIMIT 1;
        `;
    const checkResult = await db.query(checkQuery, [originalname, userId]);

    let version = 1;
    let originalFileId = null;
    if (checkResult.rows.length > 0) {
      version = checkResult.rows[0].version + 1;
      originalFileId = checkResult.rows[0].file_id;
    }

    // Datei zunächst mit null für folder_id einfügen
    const insertQuery = `
            INSERT INTO main.files (
                user_id, file_name, file_type, file_data, 
                folder_id, embedding, version, original_file_id
            )
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 
            RETURNING file_id;
        `;
    const values = [
      userId,
      originalname,
      mimetype,
      buffer,
      null, // folder_id ist bei Smart-Upload immer null
      formattedEmbedding,
      version,
      originalFileId,
    ];

    const result = await db.query(insertQuery, values);
    const fileId = result.rows[0].file_id;

    // Generate keywords im Hintergrund
    generateKeywordsInBackground(textContent, fileId);

    //holt die embeddings 
    const embeddingsQuery = `
            SELECT file_id, embedding 
            FROM main.files 
            WHERE user_id = $1
        `;
    const embeddingsResult = await db.query(embeddingsQuery, [userId]);
    const existingEmbeddings = embeddingsResult.rows.map((row) => {
      // Convert string embedding back to array
      let emb = row.embedding;
      if (typeof emb === "string") {
        emb = emb
          .replace(/[\[\]]/g, "")
          .split(",")
          .map(Number);
      }
      return emb;
    });
    existingEmbeddings.push(embedding);

    // Clustering parameters
    const defaultParams = {
      minClusterSize: 3,
      minSamples: 2,
      clusterSelectionMethod: "eom",
      clusterSelectionEpsilon: 0.18,
      anchorInfluence: 0.36,
      semanticThreshold: 0.52,
    };

    const clusteringConfig = {
      ...defaultParams,
      ...JSON.parse(clusteringParams || "{}"),
    };

    // Run clustering
    console.log(
      "Starting clustering process with parameters:",
      clusteringConfig
    );
    const clusteringResult = await modelClustering.runClustering(
      existingEmbeddings,
      clusteringConfig,
      userId
    );

    const clusterLabels = clusteringResult.labels;
    const clusterStats = clusteringResult.clusterStats;
    const folderContext = clusteringResult.folderContext;

    // Update cluster labels
    for (let i = 0; i < clusterLabels.length; i++) {
      const fileIdToUpdate =
        i < embeddingsResult.rows.length
          ? embeddingsResult.rows[i].file_id
          : fileId;

      const updateQuery = `
                UPDATE main.files 
                SET cluster_label = $1 
                WHERE file_id = $2
            `;
      await db.query(updateQuery, [clusterLabels[i], fileIdToUpdate]);
    }

    // Get folder suggestions
    const suggestions = await folderSuggestion.getSuggestedFolders({
      docEmbedding: embedding,
      userId,
    });

    res.status(201).json({
      message: "File uploaded successfully",
      fileId: fileId,
      folderSuggestions: suggestions.suggestedFolders,
      processingTime: suggestions.processingTime,
      clusteringResults: {
        totalDocuments: existingEmbeddings.length,
        uniqueClusters: clusterStats.num_clusters,
        noisePoints: clusterStats.noise_points,
        assignedCluster: clusterLabels[clusterLabels.length - 1],
        clusterSizes: clusterStats.cluster_sizes,
      },
      ...(folderContext && {
        folderContext: {
          statistics: folderContext.statistics,
          topAffinities: Object.entries(
            folderContext.affinities[clusterLabels.length - 1] || {}
          )
            .sort(([, a], [, b]) => b - a)
            .slice(0, 5)
            .map(([folderId, score]) => ({
              folderId,
              folderName: folderContext.folderInfo.names[folderId],
              score: Math.round(score * 100) / 100,
            })),
        },
      }),
    });
  } catch (error) {
    console.error("Error processing file:", error);
    res.status(422).json({
      message: "Error processing file",
      error: error.message,
    });
  }
};

/**
 * Generiert Schlüsselwörter für eine Datei im Hintergrund und speichert sie in der Datenbank.
 * 
 * @async
 * @function generateKeywordsInBackground
 * @param {string} textContent - Der extrahierte Text aus der Datei.
 * @param {number} file_id - Die ID der Datei.
 * @returns {Promise<void>} Speichert die Keywords in der Datenbank.
 */
const generateKeywordsInBackground = async (textContent, file_id) => {
  try {
    const keywords = await generateKeywords(textContent);

    // Verbinde die Keywords in eine Zeichenkette
    const keywordsString = keywords.join(", ");

    // Keywords in der Datenbank mit Sequelize speichern
    await File.update(
      { keywords: keywordsString },
      { where: { file_id: file_id } }
    );

    console.log(
      `Keywords erfolgreich für Datei mit ID ${file_id} aktualisiert.`
    );
  } catch (error) {
    console.error("Error generating keywords:", error);
  }
};

/**
 * Überprüft, ob für eine Datei bereits Schlüsselwörter generiert wurden.
 * 
 * @async
 * @function checkKeywordStatus
 * @param {Object} req - Das Request-Objekt mit der Datei-ID.
 * @param {Object} res - Das Response-Objekt mit dem Keyword-Status.
 * @returns {Promise<void>} Antwort mit den Keywords oder Status "pending".
 */
exports.checkKeywordStatus = async (req, res) => {
  const fileId = req.params.fileId;
  try {
    // database query. um keywords für den jeweiligen fileid abzurufen
    const query = "SELECT keywords FROM main.files WHERE file_id = $1";
    const result = await db.query(query, [fileId]);

    // reszlt in keywords einpacken und als res. senden.
    const keywords = result["rows"][0]["keywords"];
    if (keywords) {
      // wenn keywords verfügbar sind.
      res.json({ keywords });
    } else {
      // warten wenn keywords noch nicht fertif sind.
      res.json({ status: "pending" });
    }
  } catch (error) {
    // Handle any errors that occur during the query
    console.error("Error checking keyword status:", error);
    res
      .status(500)
      .json({ status: "error", message: "Fehler beim Abrufen der Keywords" });
  }
};

/**
 * Ermöglicht das Herunterladen einer Datei aus der Datenbank.
 * 
 * @async
 * @function downloadFile
 * @param {Object} req - Das Request-Objekt mit der Datei-ID.
 * @param {Object} res - Das Response-Objekt mit der Datei als Anhang.
 * @throws {Error} Falls die Datei nicht gefunden wird oder ein Download-Fehler auftritt.
 */
exports.downloadFile = async (req, res) => {
  try {
    const fileName = req.params.fileId;
    const userId = req.session.userId;

    const file = await File.findOne({
      where: {
        file_name: fileName,
        user_id: userId,
      },
      attributes: ["file_data", "file_type", "file_name"],
    });

    if (!file) {
      return res.status(404).send("File not found");
    }

    const { file_data, file_type, file_name } = file;

    res.setHeader("Content-Disposition", `attachment; filename="${file_name}"`);
    res.setHeader("Content-Type", file_type);
    res.send(file_data);
  } catch (error) {
    console.error("Error downloading file:", error);
    res.status(500).send("Error downloading file");
  }
};

/**
 * Löscht eine Datei aus der Datenbank.
 * 
 * @async
 * @function deleteFile
 * @param {Object} req - Das Request-Objekt mit der Datei-ID.
 * @param {Object} res - Das Response-Objekt mit der Löschbestätigung.
 * @throws {Error} Falls die Datei nicht gefunden wird oder ein Löschfehler auftritt.
 */
exports.deleteFile = async (req, res) => {
  try {
    const fileId = req.params.fileId;
    const userId = req.session.userId;

    // Versuche, die Datei zu finden
    const fileToDelete = await File.findOne({
      where: {
        file_id: fileId,
        user_id: userId,
      },
    });

    // Überprüfe, ob die Datei gefunden wurde
    if (!fileToDelete) {
      return res.status(404).json({
        message: "File not found or you do not have permission to delete it",
      });
    }

    // Lösche die Datei
    await fileToDelete.destroy();

    res.json({ message: "File deleted successfully" });
  } catch (error) {
    console.error("Error deleting file:", error);
    res.status(500).json({ message: "Error deleting file" });
  }
};

/**
 * Zeigt eine Datei im Browser an, falls sie ein unterstütztes Format hat.
 * 
 * @async
 * @function viewFile
 * @param {Object} req - Das Request-Objekt mit der Datei-ID.
 * @param {Object} res - Das Response-Objekt mit der Datei.
 * @throws {Error} Falls die Datei nicht gefunden wird oder ein Anzeige-Fehler auftritt.
 */
exports.viewFile = async (req, res) => {
  try {
    const fileName = req.params.fileId;
    const userId = req.session.userId;

    // Datei mit Sequelize abrufen
    const document = await File.findOne({
      attributes: ["file_name", "file_type", "file_data", "version", "file_id"],
      where: {
        file_name: fileName,
        user_id: userId,
      },
    });

    // Überprüfen, ob die Datei gefunden wurde
    if (!document) {
      return res.status(404).json({ error: "File not found" });
    }

    // Unterschiedliche Dateitypen behandeln
    if (document.file_type === "pdf") {
      res.setHeader("Content-Type", "application/pdf");
      res.send(document.file_data);
    } else if (document.file_type === "text/plain") {
      res.setHeader("Content-Type", "text/html");
      res.send(`
                <!DOCTYPE html>
                <html lang="de">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>View Text File</title>
                    <style>
                        .file-content {
                            background-color: #f4f4f4;
                            padding: 10px;
                            border: 1px solid #ddd;
                            margin-top: 20px;
                            white-space: pre-wrap;
                        }
                    </style>
                </head>
                <body>
                    <h1>${document.file_name} (Version ${document.version})</h1>
                    <pre class="file-content">${document.file_data.toString()}</pre>
                </body>
                </html>
            `);
    } else if (
      document.file_type ===
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    ) {
      const docxBuffer = Buffer.from(document.file_data);
      const { value: htmlContent } = await mammoth.convertToHtml({
        buffer: docxBuffer,
      });

      res.setHeader("Content-Type", "text/html");
      res.send(`
                <!DOCTYPE html>
                <html lang="de">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>View DOCX File</title>
                    <style>
                        .file-content {
                            background-color: #f4f4f4;
                            padding: 10px;
                            border: 1px solid #ddd;
                            margin-top: 20px;
                            white-space: pre-wrap;
                        }
                    </style>
                </head>
                <body>
                    <h1>${document.file_name} (Version ${document.version})</h1>
                    <div class="file-content">${htmlContent}</div>
                </body>
                </html>
            `);
    } else {
      res.setHeader("Content-Type", document.file_type);
      res.send(document.file_data);
    }
  } catch (err) {
    console.error("Error fetching document:", err.stack);
    res
      .status(500)
      .json({ error: "Error fetching document", details: err.stack });
  }
};

//////// getVersionHistory --- neue Dokumentation für Frontend !!!
//          |
//          |
//          |
//          |
//          V

/**
 * Ruft die komplette Versionshistorie eines Dokuments basierend auf einer beliebigen Version ab.
 *
 * Die Funktion arbeitet folgendermaßen:
 * 1. Sucht das ursprüngliche Dokument anhand der bereitgestellten fileId.
 * 2. Verwendet den Dateinamen, um alle Versionen des Dokuments zu ermitteln.
 * 3. Gibt eine sortierte Liste aller Versionen mit Metadaten zurück.
 *
 * @async
 * @function getVersionHistory
 * @route GET /versions/:fileId
 * @param {Object} req - Das Request-Objekt.
 * @param {string} req.params.fileId - Die ID einer beliebigen Version der Datei.
 * @param {Object} req.session - Sitzungsinformationen des Benutzers.
 * @param {string} req.session.userId - Benutzer-ID für die Autorisierung.
 * @param {Object} res - Das Response-Objekt.
 * @returns {Promise<Object>} Ein Objekt mit der Versionshistorie.
 * @property {string} fileName - Der ursprüngliche Name der Datei.
 * @property {Array<Object>} versions - Eine Liste von Versionen des Dokuments.
 * @property {string} versions[].file_id - Eindeutiger Bezeichner für jede Version.
 * @property {number} versions[].version - Versionsnummer (beginnend bei 1).
 * @property {Date} versions[].created_at - Zeitstempel der Versionserstellung.
 *
 * @example
 * // API-Aufruf im Frontend
 * const response = await fetch(`/api/versions/${fileId}`);
 * const versionHistory = await response.json();
 *
 * // Beispielhafte API-Antwort:
 * {
 *   fileName: "document.pdf",
 *   versions: [
 *     {
 *       file_id: "123",
 *       version: 2,
 *       created_at: "2024-11-07T14:30:00Z"
 *     },
 *     {
 *       file_id: "122",
 *       version: 1,
 *       created_at: "2024-11-07T12:00:00Z"
 *     }
 *   ]
 * }
 *
 * @throws {Error} Falls die Datei nicht gefunden wird oder ein Serverfehler auftritt.
 * @author Lennart
 */

exports.getVersionHistory = async (req, res) => {
  try {
    const fileId = req.params.fileId;
    const userId = req.session.userId;

    // Zuerst die Dateidetails für die gegebene fileId abrufen
    const originalFile = await File.findOne({
      attributes: ["original_file_id", "file_name"],
      where: {
        file_id: fileId,
        user_id: userId,
      },
    });

    if (!originalFile) {
      return res.status(404).json({
        error: "File not found",
        details:
          "The specified file ID does not exist or you do not have access to it",
      });
    }

    const fileName = originalFile.file_name;

    // Alle Versionen der Datei mit demselben Dateinamen abrufen
    const versions = await File.findAll({
      attributes: ["file_id", "version", "created_at"],
      where: {
        file_name: fileName,
        user_id: userId,
      },
      order: [["version", "DESC"]],
    });

    // Antwort mit Dateinamen und einer Liste von Versionen senden
    res.json({
      fileName: fileName,
      versions: versions,
    });
  } catch (error) {
    console.error("Error fetching version history:", error);
    res.status(500).json({
      error: "Error fetching version history",
      details:
        "An internal server error occurred while retrieving the version history",
    });
  }
};

/**
 * Berechnet Vorschläge für Ordner basierend auf dem hochgeladenen Dokument.
 * 
 * @async
 * @function getFolderSuggestions
 * @param {Object} req - Das Request-Objekt mit Text- und Embedding-Daten.
 * @param {Object} res - Das Response-Objekt mit den Ordner-Vorschlägen.
 * @returns {Promise<void>} Antwort mit passenden Ordnern oder ähnlichen existierenden Ordnern.
 * @throws {Error} Falls die Berechnung fehlschlägt.
 */
exports.getFolderSuggestions = async (req, res) => {
  try {
    const { textContent, embedding } = req.body;
    const userId = req.session.userId;

    if (!textContent || !embedding) {
      return res.status(400).json({
        error: "Missing required parameters",
      });
    }

    const folderDecision = await folderSuggestion.shouldCreateNewFolder(
      embedding,
      userId
    );

    if (folderDecision.shouldCreate) {
      const suggestions = await folderSuggestion.generateFolderNames(
        textContent,
        userId,
        {
          language: "auto",
          numSuggestions: 3,
          temperature: 0.7,
        }
      );

      res.json({
        names: suggestions.suggestions,
        language: suggestions.language,
        similarFolders: folderDecision.topSimilarities,
        processingTime: folderDecision.processingTime,
      });
    } else {
      res.json({
        suggestedFolder: folderDecision.similarFolder,
        otherSimilarFolders: folderDecision.topSimilarities,
        processingTime: folderDecision.processingTime,
      });
    }
  } catch (error) {
    console.error("Error generating folder suggestions:", error);
    res.status(500).json({
      error: "Failed to generate folder suggestions",
      details: error.message,
    });
  }
};

/**
 * Weist einer hochgeladenen Datei einen Ordner zu.
 * 
 * @async
 * @function assignFolder
 * @param {Object} req - Das Request-Objekt mit Datei- und Ordner-ID.
 * @param {Object} res - Das Response-Objekt mit der Bestätigung der Ordnerzuweisung.
 * @returns {Promise<void>} Antwort mit der erfolgreichen Zuweisung.
 * @throws {Error} Falls ein Fehler beim Zuweisen des Ordners auftritt.
 */
exports.assignFolder = async (req, res) => {
  try {
    const { fileId, folderId } = req.body;
    const userId = req.session.userId;
    console.log(req.body);
    console.log(
      "checkData: fileId, folderId, userId ",
      req.body.fileId,
      req.body.folderId,
      userId
    );
    // Verify file exists and belongs to user
    const fileQuery = `
            SELECT * FROM main.files 
            WHERE file_id = $1 AND user_id = $2 AND folder_id IS NULL
        `;
    const fileResult = await db.query(fileQuery, [fileId, userId]);

    if (fileResult.rows.length === 0) {
      return res.status(404).json({
        error: "File not found or folder already assigned",
      });
    }

    // Datei mit ausgewählter folder_id aktualisieren
    const updateQuery = `
            UPDATE main.files 
            SET folder_id = $1
            WHERE file_id = $2 AND user_id = $3
            RETURNING *;
        `;
    await db.query(updateQuery, [folderId, fileId, userId]);

    // Run clustering nach der Ordnerzuweisung
    const allEmbeddings = await modelEmbedding.getAllEmbeddings(userId);
    const clusteringResult = await modelClustering.runClustering(
      allEmbeddings.map((item) => {
        let emb = item.embedding;
        if (typeof emb === "string") {
          emb = emb
            .replace(/[\[\]]/g, "")
            .split(",")
            .map(Number);
        }
        return emb;
      }),
      {
        minClusterSize: 3,
        minSamples: 2,
        clusterSelectionMethod: "eom",
        clusterSelectionEpsilon: 0.18,
      },
      userId
    );

    // Update cluster labels
    for (let i = 0; i < clusteringResult.labels.length; i++) {
      const updateClusterQuery =
        "UPDATE main.files SET cluster_label = $1 WHERE file_id = $2";
      await db.query(updateClusterQuery, [
        clusteringResult.labels[i],
        allEmbeddings[i].fileId,
      ]);
    }

    res.json({
      message: "Folder assigned successfully",
      fileId: fileId,
      folderId: folderId,
      clusteringResults: {
        totalDocuments: allEmbeddings.length,
        uniqueClusters: clusteringResult.clusterStats.num_clusters,
        noisePoints: clusteringResult.clusterStats.noise_points,
        clusterSizes: clusteringResult.clusterStats.cluster_sizes,
      },
    });
  } catch (error) {
    console.error("Error assigning folder:", error);
    res.status(500).json({
      error: "Failed to assign folder",
      details: error.message,
    });
  }
};