Source

controllers/investigation.controller.js

import { Chat } from "@models/Chat.model";
import { Investigation } from "@models/investigation/investigation.model";
import { AuthGrpcService } from "@services/auth.service";
import { ContradictionDetectorService } from "@services/contradictionDetector.service";
import { ContradictionProcessingService } from "@services/contradictionProcessing.service";
import { InvestigationService } from "@services/investigation.service";
import { INVESTIGATION_SCHEMA_FORMAT } from "@typez/investigation";
import { toInvestigationResponseDto, } from "@typez/investigation/dto";
import { getLogger } from "@utils/asyncLocalStorage";
import { enrichInvestigationResponse } from "@utils/investigation-response.utils";
import { fetchUserProfile } from "@utils/user-profile.utils";
import yamlModule from "js-yaml";
import { Types } from "mongoose";
import { formatHistory } from "../helpers/history";
import { convertInvestigationFromModel } from "../helpers/investigation";
// TODO: Add updateInvestigation method for AI assistant updates
// This method should accept investigationId and key-value pairs for updates
// Example: PUT /api/investigation/:id/ai-update
// Body: { field: "title", value: "New Title", aiGenerated: true }
// TODO: Add getInvestigation method for retrieving single investigation
// GET /api/investigation/:id
// TODO: Add listInvestigations method for listing with filters
// GET /api/investigations?status=draft&grade=5&curriculum=math
// TODO: Add deleteInvestigation method for soft/hard deletion
// DELETE /api/investigation/:id
// TODO: Add lockInvestigation method for editing locks
// POST /api/investigation/:id/lock
// TODO: Add unlockInvestigation method
// DELETE /api/investigation/:id/lock
// TODO: Add updateInvestigationStatus method for status transitions
// PUT /api/investigation/:id/status
// TODO: Update DTO to send data to client (statuses need to be renamed for example)
// - Consider renaming status values to be more client-friendly:
//   - "draft_incomplete" -> "draft"
//   - "draft_non_contradictory_complete" -> "review_ready"
//   - "draft_contradictory_complete" -> "needs_review"
//   - "in_development" -> "in_progress"
//   - "published" -> "published" (keep as is)
// TODO: Remember to change the author to required in the future!
// - Make author field required in the future - currently optional
// - This should be enforced in validation schemas and service layer
// TODO: Implement a service function that will take a key and a value from AI assistant and update it in the db
// - Create updateInvestigationByAI method in InvestigationService
// - Method should accept investigationId, field name, value, and optional userId
// - Should validate field names and update the specified field
// - Should handle AI-generated data properly (aiGeneratedValue, humanEdited: false, etc.)
/**
 * Investigation Controller
 * Handles investigation-related HTTP requests
 * @category Controllers
 */
export class InvestigationController {
    /**
     * Create a new investigation.
     * @param {ICreateInvestigationRequest} req Express request with investigation payload and user context.
     * @param {Response} res Express response used to return the created investigation.
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async createInvestigation(req, res) {
        const logger = getLogger();
        logger.info({ body: req.body }, "Creating new investigation");
        logger.info({ user: req.user }, "User information");
        try {
            const userId = req.user?.userId;
            const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
            if (userId && Types.ObjectId.isValid(userId) && !req.body.author) {
                req.body.author = new Types.ObjectId(userId);
            }
            const investigationService = new InvestigationService();
            const contradictionProcessor = new ContradictionProcessingService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            // Create the investigation
            const createdInvestigation = await investigationService.createInvestigation({
                ...req.body,
                lastChangeWithAI: false,
            });
            // Process contradictions after creation
            try {
                // Get the full investigation to process contradictions
                const fullInvestigation = await investigationService.getInvestigationById(createdInvestigation._id.toString());
                // Process contradictions using the service
                const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(fullInvestigation, async (updateFields) => {
                    const result = await investigationService.updateInvestigation(createdInvestigation._id.toString(), updateFields, { updatedBy: authenticatedAuthorId, skipVersioning: true });
                    if (!result.updatedInvestigation) {
                        throw new Error("Failed to update investigation during contradiction processing");
                    }
                    return { updatedInvestigation: result.updatedInvestigation };
                });
                const dto = toInvestigationResponseDto(finalInvestigation);
                await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
                res.status(201).json({
                    success: true,
                    message: "Investigation created successfully",
                    data: dto,
                });
            }
            catch (contradictionError) {
                logger.warn({ error: contradictionError, investigationId: createdInvestigation._id.toString() }, "Contradiction processing failed, returning original investigation");
                const dto = toInvestigationResponseDto(createdInvestigation);
                await enrichInvestigationResponse(createdInvestigation, dto, grpcClient, profileCache);
                res.status(201).json({
                    success: true,
                    message: "Investigation created successfully",
                    data: dto,
                });
            }
        }
        catch (error) {
            logger.error({ error, body: req.body }, "Failed to create investigation");
            res.status(500).json({
                success: false,
                message: "Failed to create investigation",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Get investigations with pagination, grouped by curriculum.
     * @param {IAuthenticatedRequest} req Express request containing validated query parameters.
     * @param {Response} res Express response used to return the investigations list and pagination metadata.
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async getInvestigations(req, res) {
        const logger = getLogger();
        logger.info({ query: req.query, user: req.user }, "Getting investigations grouped by curriculum");
        // Extract query parameters (already validated by middleware)
        const searchParams = {
            limit: req.query.limit,
            offset: req.query.offset,
            sortBy: req.query.sortBy,
            sortOrder: req.query.sortOrder,
            search: req.query.search,
        };
        // Pass search parameters to service
        const investigationService = new InvestigationService();
        const result = await investigationService.getInvestigations(searchParams);
        let itemsWithAuthorProfiles = result.items;
        if (result.items.length) {
            try {
                const authHeader = req.headers.authorization;
                const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
                const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
                const profileCache = new Map();
                itemsWithAuthorProfiles = await Promise.all(result.items.map(async (item) => {
                    const authorId = item.author?.id ?? undefined;
                    if (!authorId) {
                        return {
                            ...item,
                            author: {
                                id: null,
                                name: null,
                                email: null,
                                avatar: null,
                            },
                        };
                    }
                    try {
                        const profile = await fetchUserProfile(authorId, profileCache, grpcClient);
                        return {
                            ...item,
                            author: {
                                id: authorId,
                                name: profile.name,
                                email: profile.email,
                                avatar: profile.avatar,
                            },
                        };
                    }
                    catch (profileError) {
                        logger.warn({ profileError, authorId }, "Failed to fetch author profile for investigation list item");
                        return {
                            ...item,
                            author: {
                                id: authorId,
                                name: null,
                                email: null,
                                avatar: null,
                            },
                        };
                    }
                }));
            }
            catch (error) {
                logger.warn({ error }, "Failed to enrich investigation list with author profiles");
            }
        }
        res.status(200).json({
            data: {
                items: itemsWithAuthorProfiles,
                pagination: result.pagination,
            },
        });
    }
    /**
     * Get a single investigation by ID.
     * @param {IAuthenticatedRequest} req Express request containing the investigation identifier.
     * @param {Response} res Express response used to return the investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async getInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        logger.info({ investigationId: id, user: req.user }, "Fetching investigation");
        try {
            const investigationService = new InvestigationService();
            const investigation = await investigationService.getInvestigationById(id);
            const responseDto = toInvestigationResponseDto(investigation);
            try {
                const authHeader = req.headers.authorization;
                const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
                const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
                const profileCache = new Map();
                await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
            }
            catch (profileError) {
                logger.warn({ profileError, investigationId: id }, "Failed to enrich investigation metadata with user profiles");
            }
            res.status(200).json({
                success: true,
                message: "Investigation retrieved successfully",
                data: responseDto,
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to fetch investigation");
            res.status(500).json({
                success: false,
                message: "Failed to fetch investigation",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Update an investigation.
     * @param {IUpdateInvestigationRequest} req Express request with update payload and params.
     * @param {Response} res Express response used to return the updated investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async updateInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        logger.info({ user: req.user }, "User information");
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        logger.info({ investigationId: id, updateData: req.body }, "Updating investigation");
        try {
            const investigationService = new InvestigationService();
            const contradictionProcessor = new ContradictionProcessingService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            const userId = req.user?.userId;
            const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
            const bodyWithMetadata = {
                ...req.body,
            };
            let authorOverride = null;
            const { metadata: incomingMetadata, ...restPayload } = bodyWithMetadata;
            if (incomingMetadata && typeof incomingMetadata === "object") {
                const providedAuthor = incomingMetadata.author;
                if (providedAuthor instanceof Types.ObjectId) {
                    authorOverride = providedAuthor;
                }
                else if (typeof providedAuthor === "string") {
                    const trimmed = providedAuthor.trim();
                    if (trimmed && Types.ObjectId.isValid(trimmed)) {
                        authorOverride = new Types.ObjectId(trimmed);
                    }
                }
            }
            const updatePayload = restPayload;
            // Update the investigation
            let result = await investigationService.updateInvestigation(id, updatePayload, {
                needsBuildUpdate: true,
                updatedBy: authenticatedAuthorId,
                isAiGenerated: false,
            });
            if ((authorOverride || authenticatedAuthorId) &&
                (!result.updatedInvestigation?.metadata || !result.updatedInvestigation.metadata.author)) {
                const ensured = await investigationService.ensureInvestigationAuthor(id, authorOverride ?? authenticatedAuthorId);
                if (ensured) {
                    result = {
                        ...result,
                        updatedInvestigation: ensured,
                    };
                }
            }
            if (result.updatedInvestigation) {
                // Process contradictions after update
                try {
                    // Process contradictions using the service
                    const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(result.updatedInvestigation, async (updateFields) => {
                        const updateResult = await investigationService.updateInvestigation(id, updateFields, {
                            updatedBy: authenticatedAuthorId,
                            isAiGenerated: false,
                            skipVersioning: true,
                        });
                        if (!updateResult.updatedInvestigation) {
                            throw new Error("Failed to update investigation during contradiction processing");
                        }
                        return { updatedInvestigation: updateResult.updatedInvestigation };
                    });
                    const dto = toInvestigationResponseDto(finalInvestigation);
                    await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
                    res.status(200).json({
                        success: result.success,
                        updatedInvestigation: dto,
                    });
                }
                catch (contradictionError) {
                    logger.warn({ error: contradictionError, investigationId: id }, "Contradiction processing failed, returning original update");
                    // Get the original investigation response
                    const originalInvestigationResponse = await investigationService.getInvestigationById(id);
                    const dto = toInvestigationResponseDto(originalInvestigationResponse);
                    await enrichInvestigationResponse(originalInvestigationResponse, dto, grpcClient, profileCache);
                    res.status(200).json({
                        success: result.success,
                        updatedInvestigation: dto,
                    });
                }
            }
            else {
                res.status(500).json({
                    success: false,
                    message: "Failed to update investigation",
                });
            }
        }
        catch (error) {
            logger.error({ error, investigationId: id, updateData: req.body }, "Failed to update investigation");
            res.status(500).json({
                success: false,
                message: "Failed to update investigation",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Delete an investigation and its related chat history.
     * @param {IAuthenticatedRequest} req Express request with the investigation identifier to remove.
     * @param {Response} res Express response used to confirm the deletion result.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async deleteInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        logger.info({ investigationId: id, user: req.user }, "Deleting investigation");
        try {
            const investigationService = new InvestigationService();
            const { investigationDeleted, deletedChats } = await investigationService.deleteInvestigationById(id);
            if (!investigationDeleted) {
                res.status(404).json({
                    success: false,
                    message: "Investigation not found",
                });
                return;
            }
            res.status(200).json({
                success: true,
                message: "Investigation deleted successfully",
                data: {
                    investigationId: id,
                    deletedChats,
                },
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to delete investigation");
            res.status(500).json({
                success: false,
                message: "Failed to delete investigation",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Get investigation schema format.
     * @param {IAuthenticatedRequest} req Express request.
     * @param {Response} res Express response used to return the schema format.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {void}
     */
    static getInvestigationFormat(req, res, _next) {
        const logger = getLogger();
        logger.info("Fetching investigation schema format");
        try {
            res.status(200).json({ format: INVESTIGATION_SCHEMA_FORMAT });
        }
        catch (error) {
            logger.error({ error }, "Failed to fetch investigation schema format");
            res.status(500).json({
                success: false,
                message: "Failed to fetch investigation schema format",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Clone an investigation.
     * @param {ICloneInvestigationRequest} req Express request with the investigation identifier to clone.
     * @param {Response} res Express response used to return the cloned investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async cloneInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        logger.info({ investigationId: id, user: req.user }, "Cloning investigation");
        try {
            const userId = req.user?.userId;
            const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
            const investigationService = new InvestigationService();
            const contradictionProcessor = new ContradictionProcessingService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            // Clone the investigation (without contradiction processing)
            const clonedInvestigation = await investigationService.cloneInvestigation(id);
            // Process contradictions after cloning
            try {
                logger.info({
                    investigationId: clonedInvestigation._id,
                    investigationStatus: clonedInvestigation.status,
                }, "Starting contradiction detection and update processes after investigation cloning");
                // Process contradictions using the service
                const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(clonedInvestigation, async (updateFields) => {
                    const result = await investigationService.updateInvestigation(clonedInvestigation._id.toString(), updateFields, { updatedBy: authenticatedAuthorId });
                    if (!result.updatedInvestigation) {
                        throw new Error("Failed to update investigation during contradiction processing");
                    }
                    return { updatedInvestigation: result.updatedInvestigation };
                });
                logger.info({
                    investigationId: clonedInvestigation._id,
                    finalInvestigationId: finalInvestigation._id,
                    finalInvestigationStatus: finalInvestigation.status,
                }, "Contradiction processing completed successfully after investigation cloning");
                const dto = toInvestigationResponseDto(finalInvestigation);
                await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
                res.status(201).json({
                    success: true,
                    message: "Investigation cloned successfully",
                    data: dto,
                });
            }
            catch (contradictionError) {
                logger.warn({
                    error: contradictionError,
                    investigationId: clonedInvestigation._id.toString(),
                    investigationStatus: clonedInvestigation.status,
                }, "Contradiction processing failed after investigation cloning, returning original cloned investigation");
                const dto = toInvestigationResponseDto(clonedInvestigation);
                await enrichInvestigationResponse(clonedInvestigation, dto, grpcClient, profileCache);
                res.status(201).json({
                    success: true,
                    message: "Investigation cloned successfully",
                    data: dto,
                });
            }
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to clone investigation");
            res.status(500).json({
                success: false,
                message: "Failed to clone investigation",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Get history for a specific field.
     * @param {IAuthenticatedRequest} req Express request containing investigation ID and field path.
     * @param {Response} res Express response used to return the field history.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async getFieldHistory(req, res, _next) {
        const logger = getLogger();
        const { id, fieldPath } = req.params;
        const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        if (!fieldPath) {
            res.status(400).json({
                success: false,
                message: "Field path is required",
            });
            return;
        }
        logger.info({ investigationId: id, fieldPath, limit }, "Fetching field history");
        try {
            const investigationService = new InvestigationService();
            const result = await investigationService.getFieldHistory(id, fieldPath, limit);
            res.status(200).json({
                success: true,
                message: "Field history retrieved successfully",
                data: result,
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id, fieldPath }, "Failed to fetch field history");
            res.status(500).json({
                success: false,
                message: "Failed to fetch field history",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Get investigation versions.
     * @param {IAuthenticatedRequest} req Express request with the investigation identifier to get versions.
     * @param {Response} res Express response used to return the investigation versions.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async getInvestigationVersions(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        const limit = req.query.limit ? parseInt(req.query.limit, 10) : 20;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        try {
            const investigationService = new InvestigationService();
            const versions = await investigationService.getInvestigationVersions(id, limit);
            res.status(200).json({
                success: true,
                message: "Investigation versions retrieved successfully",
                data: versions,
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to fetch investigation versions");
            res.status(500).json({
                success: false,
                message: "Failed to fetch investigation versions",
                error: error instanceof Error ? error.message : "Unknown error",
            });
        }
    }
    /**
     * Undo an investigation.
     * @param {IAuthenticatedRequest} req Express request with the investigation identifier to undo.
     * @param {Response} res Express response used to return the undone investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async undoInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        try {
            const investigationService = new InvestigationService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            const investigation = await investigationService.undoInvestigation(id);
            const responseDto = toInvestigationResponseDto(investigation);
            await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
            res.status(200).json({
                success: true,
                message: "Investigation undone successfully",
                data: responseDto,
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to undo investigation");
            res.status(500).json({
                success: false,
                message: error instanceof Error ? error.message : "Failed to undo investigation",
            });
        }
    }
    /**
     * Redo an investigation.
     * @param {IAuthenticatedRequest} req Express request with the investigation identifier to redo.
     * @param {Response} res Express response used to return the redone investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async redoInvestigation(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        try {
            const investigationService = new InvestigationService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            const investigation = await investigationService.redoInvestigation(id);
            const responseDto = toInvestigationResponseDto(investigation);
            await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
            res.status(200).json({
                success: true,
                message: "Investigation redone successfully",
                data: responseDto,
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to redo investigation");
            res.status(500).json({
                success: false,
                message: error instanceof Error ? error.message : "Failed to redo investigation",
            });
        }
    }
    /**
     * Resolve contradiction.
     * @param {IAuthenticatedRequest} req Express request with the investigation identifier to redo.
     * @param {Response} res Express response used to return the investigation with resolved contradiction.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async resolveContradiction(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        const { fieldName } = req.body;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        if (!fieldName) {
            res.status(400).json({
                success: false,
                message: "Field name is required",
            });
            return;
        }
        try {
            const contradictionService = new ContradictionDetectorService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            const investigation = await Investigation.findById(id);
            if (!investigation) {
                throw new Error("Investigation not found");
            }
            const currentChat = await Chat.findOne({ investigationId: investigation.id });
            if (!currentChat) {
                throw new Error(`Chat not found for the investigation with id: ${id}`);
            }
            let oldInvestigation = JSON.parse(JSON.stringify(investigation));
            const resolvedContradictionMessage = await contradictionService.resolveContradiction(investigation, fieldName);
            const responseDto = toInvestigationResponseDto(investigation);
            await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
            const investigationService = new InvestigationService();
            await investigationService.updateInvestigationByVersion(oldInvestigation, investigation, new Types.ObjectId(req.user?.userId));
            currentChat.history.push({
                user: { content: "" },
                assistant: {
                    content: resolvedContradictionMessage,
                    metadata: JSON.stringify(convertInvestigationFromModel(investigation)),
                    content_for_llm: "[investigation_provided]",
                },
            });
            await currentChat.save();
            await investigation.save();
            res.status(200).json({
                success: true,
                message: "Successfully resolved contradictions",
                data: { investigation: responseDto, message: resolvedContradictionMessage },
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id }, "Failed to resolve contradiction");
            res.status(500).json({
                success: false,
                message: error instanceof Error ? error.message : "Failed to resolve contradiction",
            });
        }
    }
    /**
     * Regenerate other fields based on a specific field.
     * @param {IAuthenticatedRequest} req Express request containing investigation ID and field name.
     * @param {Response} res Express response used to return the updated investigation.
     * @param {NextFunction} _next Express next function (unused).
     * @returns {Promise<void>} Resolves when the response has been sent.
     */
    static async regenerateOtherFields(req, res, _next) {
        const logger = getLogger();
        const { id } = req.params;
        const { fieldName } = req.body;
        if (!id) {
            res.status(400).json({
                success: false,
                message: "Investigation ID is required",
            });
            return;
        }
        if (!fieldName) {
            res.status(400).json({
                success: false,
                message: "Field name is required",
            });
            return;
        }
        logger.info({ investigationId: id, fieldName }, "Regenerating other fields");
        try {
            const investigationService = new InvestigationService();
            const contradictionProcessor = new ContradictionProcessingService();
            const authHeader = req.headers.authorization;
            const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
            const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
            const profileCache = new Map();
            const userId = req.user?.userId;
            const updatedBy = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
            const investigation = await Investigation.findById(id);
            if (!investigation) {
                throw new Error("Investigation not found");
            }
            const currentChat = await Chat.findOne({ investigationId: investigation.id });
            if (!currentChat) {
                throw new Error(`Chat not found for the investigation with id: ${id}`);
            }
            const formattedHistory = formatHistory(currentChat.history || []);
            const yaml = yamlModule;
            const chatHistoryYaml = yaml.dump(formattedHistory);
            const updatedInvestigation = await investigationService.regenerateOtherFields(id, fieldName, updatedBy, chatHistoryYaml);
            if (!updatedInvestigation) {
                throw new Error("Failed to regenerate other fields");
            }
            // Process contradictions after regeneration
            if (updatedInvestigation) {
                try {
                    await contradictionProcessor.processAndApplyContradictions(updatedInvestigation, async (updateFields) => {
                        const updateResult = await investigationService.updateInvestigation(id, updateFields, {
                            updatedBy,
                            isAiGenerated: false,
                            skipVersioning: true,
                        });
                        if (!updateResult.updatedInvestigation) {
                            throw new Error("Failed to update investigation during contradiction processing");
                        }
                        return { updatedInvestigation: updateResult.updatedInvestigation };
                    });
                }
                catch (contradictionError) {
                    logger.warn({ error: contradictionError, investigationId: id, fieldName }, "Failed to process contradictions after regeneration, but regeneration succeeded");
                    // Continue with the investigation even if contradiction detection fails
                }
            }
            const finalInvestigationDocument = await Investigation.findById(id);
            if (!finalInvestigationDocument) {
                throw new Error("Failed to reload investigation after regeneration");
            }
            const responseDto = toInvestigationResponseDto(finalInvestigationDocument);
            await enrichInvestigationResponse(finalInvestigationDocument, responseDto, grpcClient, profileCache);
            const hasChanges = updatedInvestigation
                ._regenerationHasChanges ?? true;
            const changedFields = updatedInvestigation
                ._regenerationChangedFields ?? [];
            let regenerationMessage;
            if (!hasChanges || changedFields.length === 0) {
                regenerationMessage = `AI assistant is unable to regenerate the remaining fields as no modifications are required or the input is invalid (contains useless or invalid data)`;
            }
            else {
                regenerationMessage = `Successfully regenerated other fields`;
            }
            currentChat.history.push({
                user: { content: "" },
                assistant: {
                    content: regenerationMessage,
                    metadata: JSON.stringify(convertInvestigationFromModel(finalInvestigationDocument)),
                    content_for_llm: "[investigation_provided]",
                },
            });
            await currentChat.save();
            await finalInvestigationDocument.save();
            res.status(200).json({
                success: true,
                message: hasChanges
                    ? "Other fields regenerated successfully"
                    : "Regeneration completed with no changes",
                data: {
                    investigation: responseDto,
                    message: regenerationMessage,
                    hasChanges,
                    changedFields: hasChanges ? changedFields : [],
                },
            });
        }
        catch (error) {
            logger.error({ error, investigationId: id, fieldName }, "Failed to regenerate other fields");
            res.status(500).json({
                success: false,
                message: error instanceof Error ? error.message : "Failed to regenerate other fields",
            });
        }
    }
}