const { Notification } = require('../../../backend/models/notification');
const {
  OrganizationConnection,
} = require('../../../backend/models/organizationConnection');
const {
  OrganizationWorkspace,
} = require('../../../backend/models/organizationWorkspace');
const { Queue } = require('../../../backend/models/queue');
const { RagTest } = require('../../../backend/models/ragTest');
const {
  selectConnector,
} = require('../../../backend/utils/vectordatabases/providers');
const { InngestClient } = require('../../utils/inngest');

const DELTA_THRESHOLD = 0.25;
const runRAGTest = InngestClient.createFunction(
  { name: 'RAG Test for Workspace' },
  { event: 'rag-test/run' },
  async ({ event, step: _step, logger }) => {
    var result = {};
    const { organization, testId, jobId = null } = event.data;
    const { run } = await RagTest.createRun(testId, RagTest.status.running);

    try {
      const test = await RagTest.get({ id: Number(testId) });
      const workspace = await OrganizationWorkspace.get({
        id: test.workspace_id,
      });
      const connector = await OrganizationConnection.get({
        organization_id: Number(organization.id),
      });
      if (!test) throw new Error(`No test found for id ${testId}`);
      if (!connector) throw new Error(`No vector database connection found.`);

      const errorLog = [];
      const knownVectors = {};
      Object.values(test.comparisons).forEach((obj) => {
        knownVectors[obj.vectorId] = obj;
      });

      const vectorDB = selectConnector(connector);
      const { vectorIds, contextTexts, scores } =
        await vectorDB.similarityResponse(
          workspace.fname,
          test.promptVector,
          test.topK
        );

      // Check if searched vectorIds all appear in known ids
      const newVectorIds = vectorIds.filter(
        (id) => !knownVectors.hasOwnProperty(id)
      );
      const missingVectorIds = Object.keys(knownVectors).filter(
        (id) => !vectorIds.includes(id)
      );

      // Calculate Score deviations for known vectors
      const highScoreDeltaVectorIds = [];
      const matchedVectorIds = vectorIds.filter((id) =>
        knownVectors.hasOwnProperty(id)
      );
      matchedVectorIds.forEach((vectorId, i) => {
        // idx wont be -1 because matchedVectorIds already
        // checked this vectorId was returned.
        const matchingIdx = vectorIds.indexOf(vectorId);
        const currentScore = knownVectors[vectorId].score;
        const score = scores[matchingIdx];
        const signMultiple = score < currentScore ? -1 : 1;
        const delta = (currentScore - score) * signMultiple;

        if (Math.abs(delta) >= DELTA_THRESHOLD)
          highScoreDeltaVectorIds.push(vectorId);
        knownVectors[vectorId].newScore = score;
        knownVectors[vectorId].deltaScore = delta;

        // Append the text content of the search to the data so we
        // can compare with testing text later.
        knownVectors[vectorId].textContent = contextTexts[matchingIdx];
      });

      newVectorIds.forEach((vectorId) => {
        errorLog.push({
          vectorId,
          message: `A new vector ${vectorId} was returned. It was not previously seen in the comparison data.`,
        });
      });

      missingVectorIds.forEach((vectorId) => {
        errorLog.push({
          vectorId,
          message: `Vector ${vectorId} was expected to be present, but is missing.`,
        });
      });

      highScoreDeltaVectorIds.forEach((vectorId) => {
        const delta = knownVectors[vectorId].deltaScore;
        errorLog.push({
          vectorId,
          message: `Known vector ${vectorId} had a relevancy score deviation greater than ±${
            DELTA_THRESHOLD * 100
          }% with ${(delta * 100).toFixed(2)}%.`,
        });
      });

      const compactResult = {};
      Object.keys(knownVectors).map((vector) => {
        const {
          score: baseScore,
          newScore,
          deltaScore,
          textContent,
        } = knownVectors[vector];
        compactResult[vector] = {
          baseScore,
          newScore,
          deltaScore,
          textContent,
        };
      });

      await RagTest.updateRun(run?.id, {
        status:
          errorLog.length > 0 ? RagTest.status.alert : RagTest.status.complete,
        results: {
          errorLog,
          newVectorIds,
          missingVectorIds,
          highScoreDeltaVectorIds,
          vectorMap: compactResult,
        },
      });

      // If test failed - push to notifications
      if (errorLog.length > 0) {
        await Notification.create(test.organization_id, {
          textContent: 'Your Context Drift test did not pass.',
          symbol: Notification.symbols.error,
          link: `/dashboard/${organization.slug}/tools/rag-testing/${test.id}`,
          target: '_blank',
        });
      }

      result = {
        message: `Context Drift test #${test.id} completed without exiting. Test run was logged.`,
      };
      await Queue.updateJob(jobId, Queue.status.complete, result);
      return { result };
    } catch (e) {
      const result = {
        message: `Job failed with error`,
        error: e.message,
        details: e,
      };
      await Notification.create(organization.id, {
        textContent: 'Your Context Drift test failed to run.',
        symbol: Notification.symbols.error,
      });
      await RagTest.updateRun(run?.id, {
        status: RagTest.status.failed,
        results: {
          message: e.message,
        },
      });
      await Queue.updateJob(jobId, Queue.status.failed, result);
      return { result };
    }
  }
);

module.exports = {
  runRAGTest,
};
