const { Document } = require("../models/documents.js");
const { DocumentSyncQueue } = require("../models/documentSyncQueue.js");
const { CollectorApi } = require("../utils/collectorApi");
const { fileData } = require("../utils/files");
const { log, conclude, updateSourceDocument } = require("./helpers/index.js");
const { getVectorDbClass } = require("../utils/helpers/index.js");
const { DocumentSyncRun } = require("../models/documentSyncRun.js");

(async () => {
  try {
    const queuesToProcess = await DocumentSyncQueue.staleDocumentQueues();
    if (queuesToProcess.length === 0) {
      log("No outstanding documents to sync. Exiting.");
      return;
    }

    const collector = new CollectorApi();
    if (!(await collector.online())) {
      log("Could not reach collector API. Exiting.");
      return;
    }

    log(
      `${queuesToProcess.length} watched documents have been found to be stale and will be updated now.`
    );
    for (const queue of queuesToProcess) {
      let newContent = null;
      const document = queue.workspaceDoc;
      const workspace = document.workspace;
      const { metadata, type, source } =
        Document.parseDocumentTypeAndSource(document);

      if (!metadata || !DocumentSyncQueue.validFileTypes.includes(type)) {
        // Document is either broken, invalid, or not supported so drop it from future queues.
        log(
          `Document ${document.filename} has no metadata, is broken, or invalid and has been removed from all future runs.`
        );
        await DocumentSyncQueue.unwatch(document);
        continue;
      }

      if (["link", "youtube"].includes(type)) {
        const response = await collector.forwardExtensionRequest({
          endpoint: "/ext/resync-source-document",
          method: "POST",
          body: JSON.stringify({
            type,
            options: { link: source },
          }),
        });
        newContent = response?.content;
      }

      if (["confluence", "github", "gitlab", "drupalwiki"].includes(type)) {
        const response = await collector.forwardExtensionRequest({
          endpoint: "/ext/resync-source-document",
          method: "POST",
          body: JSON.stringify({
            type,
            options: { chunkSource: metadata.chunkSource },
          }),
        });
        newContent = response?.content;
      }

      if (!newContent) {
        // Check if the last "x" runs were all failures (not exits!). If so - remove the job entirely since it is broken.
        const failedRunCount = (
          await DocumentSyncRun.where(
            { queueId: queue.id },
            DocumentSyncQueue.maxRepeatFailures,
            { createdAt: "desc" }
          )
        ).filter(
          (run) => run.status === DocumentSyncRun.statuses.failed
        ).length;
        if (failedRunCount >= DocumentSyncQueue.maxRepeatFailures) {
          log(
            `Document ${document.filename} has failed to refresh ${failedRunCount} times continuously and will now be removed from the watched document set.`
          );
          await DocumentSyncQueue.unwatch(document);
          continue;
        }

        log(
          `Failed to get a new content response from collector for source ${source}. Skipping, but will retry next worker interval. Attempt ${failedRunCount === 0 ? 1 : failedRunCount}/${DocumentSyncQueue.maxRepeatFailures}`
        );
        await DocumentSyncQueue.saveRun(
          queue.id,
          DocumentSyncRun.statuses.failed,
          {
            filename: document.filename,
            workspacesModified: [],
            reason: "No content found.",
          }
        );
        continue;
      }

      const currentDocumentData = await fileData(document.docpath);
      if (currentDocumentData.pageContent === newContent) {
        const nextSync = DocumentSyncQueue.calcNextSync(queue);
        log(
          `Source ${source} is unchanged and will be skipped. Next sync will be ${nextSync.toLocaleString()}.`
        );
        await DocumentSyncQueue._update(queue.id, {
          lastSyncedAt: new Date().toISOString(),
          nextSyncAt: nextSync.toISOString(),
        });
        await DocumentSyncQueue.saveRun(
          queue.id,
          DocumentSyncRun.statuses.exited,
          {
            filename: document.filename,
            workspacesModified: [],
            reason: "Content unchanged.",
          }
        );
        continue;
      }

      // update the defined document and workspace vectorDB with the latest information
      // it will skip cache and create a new vectorCache file.
      const vectorDatabase = getVectorDbClass();
      await vectorDatabase.deleteDocumentFromNamespace(
        workspace.slug,
        document.docId
      );
      await vectorDatabase.addDocumentToNamespace(
        workspace.slug,
        {
          ...currentDocumentData,
          pageContent: newContent,
          docId: document.docId,
        },
        document.docpath,
        true
      );
      updateSourceDocument(document.docpath, {
        ...currentDocumentData,
        pageContent: newContent,
        docId: document.docId,
        published: new Date().toLocaleString(),
        // Todo: Update word count and token_estimate?
      });
      log(
        `Workspace "${workspace.name}" vectors of ${source} updated. Document and vector cache updated.`
      );

      // Now we can bloom the results to all matching documents in all other workspaces
      const workspacesModified = [workspace.slug];
      const moreReferences = await Document.where(
        {
          id: { not: document.id },
          filename: document.filename,
        },
        null,
        null,
        { workspace: true }
      );

      if (moreReferences.length !== 0) {
        log(
          `${source} is referenced in ${moreReferences.length} other workspaces. Updating those workspaces as well...`
        );
        for (const additionalDocumentRef of moreReferences) {
          const additionalWorkspace = additionalDocumentRef.workspace;
          workspacesModified.push(additionalWorkspace.slug);

          await vectorDatabase.deleteDocumentFromNamespace(
            additionalWorkspace.slug,
            additionalDocumentRef.docId
          );
          await vectorDatabase.addDocumentToNamespace(
            additionalWorkspace.slug,
            {
              ...currentDocumentData,
              pageContent: newContent,
              docId: additionalDocumentRef.docId,
            },
            additionalDocumentRef.docpath
          );
          log(
            `Workspace "${additionalWorkspace.name}" vectors for ${source} was also updated with the new content from cache.`
          );
        }
      }

      const nextRefresh = DocumentSyncQueue.calcNextSync(queue);
      log(
        `${source} has been refreshed in all workspaces it is currently referenced in. Next refresh will be ${nextRefresh.toLocaleString()}.`
      );
      await DocumentSyncQueue._update(queue.id, {
        lastSyncedAt: new Date().toISOString(),
        nextSyncAt: nextRefresh.toISOString(),
      });
      await DocumentSyncQueue.saveRun(
        queue.id,
        DocumentSyncRun.statuses.success,
        { filename: document.filename, workspacesModified }
      );
    }
  } catch (e) {
    console.error(e);
    log(`errored with ${e.message}`);
  } finally {
    conclude();
  }
})();
