UNPKG

casefile-repository-mcp-server

Version:

Vessel casefile management system with MCP integration for maritime operations

2,272 lines 111 kB
import { getMongoClient } from "../utils/mongodb.js";
import { getTypesenseClient } from "../utils/typesense.js";
import { config } from "../utils/config.js";
import axios from "axios";
import { ObjectId } from "mongodb";
import { logger } from "../utils/logger.js";
// Helper function to get full email details from MongoDB
async function get_full_email_details(emailId) {
    try {
        if (!emailId) {
            throw new Error("Email ID is required");
        }
        let objectId;
        try {
            objectId = new ObjectId(emailId);
        }
        catch (error) {
            throw new Error(`Invalid email ID format: ${emailId}`);
        }
        logger.info(`[get_full_email_details] Searching for email with ID: ${emailId}`);
        logger.info(`[get_full_email_details] Using MongoDB URI: ${config.etlDevMongoUri}`);
        logger.info(`[get_full_email_details] Using DB name: ${config.etlDevDbName}`);
        const mongoClient = await getMongoClient(config.etlDevMongoUri);
        const db = mongoClient.db(config.etlDevDbName);
        // Try to find the email in mail_temp collection first
        let email = await db.collection("mail_temp").findOne({ _id: objectId });
        // If not found, try mail_archive collection
        if (!email) {
            email = await db.collection("mail_archive").findOne({ _id: objectId });
        }
        if (!email) {
            return {
                found: false,
                message: `No email found with ID: ${emailId}`
            };
        }
        return {
            found: true,
            source: email._collection || "unknown",
            email: email
        };
    }
    catch (error) {
        console.error(`[get_full_email_details] Error: ${error}`);
        return {
            found: false,
            error: error instanceof Error ? error.message : String(error),
            message: `Failed to retrieve email details: ${error instanceof Error ? error.message : String(error)}`
        };
    }
}
// Internal utility function for vessel details lookup (not exposed as tool)
async function getVesselDetails(query) {
    try {
        if (!query) {
            throw new Error("Query parameter is required for vessel details search");
        }
        console.log(`[Internal] Searching for vessel details with vessel name: ${query}`);
        // Set up search parameters for the fleet-vessel-lookup collection
        const search_parameters = {
            q: query,
            query_by: 'vesselName',
            collection: 'fleet-vessel-lookup',
            per_page: 1,
            include_fields: 'vesselName,imo,class,flag,shippalmDoc,isV3',
            prefix: false,
            num_typos: 2,
        };
        // Execute search
        const typesenseClient = getTypesenseClient(config);
        const raw = await typesenseClient.collections('fleet-vessel-lookup').documents().search(search_parameters);
        const hits = raw.hits || [];
        if (!hits || hits.length === 0) {
            return null;
        }
        // Process and format results
        const doc = hits[0].document || {};
        return {
            vesselName: doc.vesselName,
            imo: doc.imo,
            class: doc.class,
            flag: doc.flag,
            shippalmDoc: doc.shippalmDoc,
            isV3: doc.isV3,
            score: hits[0].text_match || 0
        };
    }
    catch (error) {
        console.error(`[Internal] Error searching for vessel details: ${error}`);
        throw error;
    }
}
// --- Write Casefile Data Tool Implementation ---
// Helper to push casefile to Typesense
async function pushToTypesense(res, action) {
    const id = res.id || res._id?.toString();
    const casefileTxt = res.casefile;
    const summaryTxt = res.summary;
    const embeddingText = `Below casefile ${casefileTxt} with following summary ${summaryTxt} `;
    const link = await generateCasefileWeblink(id);
    // Update the casefile in MongoDB with the link
    const mongoClient = await getMongoClient(config.mongoUri);
    const db = mongoClient.db(config.dbName);
    const collection = db.collection("casefiles");
    await collection.updateOne({ _id: id }, { $set: { link } });
    // Prepare Typesense data
    const createdAt = res.createdAt instanceof Date ? Math.floor(res.createdAt.getTime() / 1000) : (typeof res.createdAt === 'number' ? res.createdAt : Date.now() / 1000);
    const updatedAt = res.updatedAt instanceof Date ? Math.floor(res.updatedAt.getTime() / 1000) : (typeof res.updatedAt === 'number' ? res.updatedAt : Date.now() / 1000);
    const importance = typeof res.importance === 'number' ? res.importance : 0;
    const data = {
        id: id,
        _id: id,
        casefile: res.casefile,
        currentStatus: res.currentStatus,
        casefileInitiationDate: createdAt,
        category: res.category,
        conversationTopic: [],
        embedding_text: embeddingText,
        imo: Number(res.imo),
        importance: String(importance), // <-- convert to string for Typesense
        importance_score: importance, // <-- keep as number for other uses
        lastcasefileUpdateDate: updatedAt,
        summary: res.summary,
        vesselId: res.vesselId ? String(res.vesselId) : null,
        vesselName: res.vesselName ? String(res.vesselName) : null,
        link: link,
        followUp: res.followUp || "",
        pages: JSON.stringify((res.pages || []).slice(-2)),
        index: JSON.stringify((res.index || []).slice(-2)),
    };
    if (res.plan_status) {
        data.plan_status = res.plan_status;
    }
    try {
        const typesenseClient = getTypesenseClient(config);
        console.info(`Data pushed to Typesense:`, data);
        const result = await typesenseClient.collections("emailCasefile").documents().import([data], { action });
        // Log after Typesense push
        console.info(`[pushToTypesense] Data pushed to Typesense successfully for id: ${id}`);
        console.info(result);
        return result;
    }
    catch (e) {
        if (typeof e === "object" && e !== null && "importResults" in e) {
            console.error("Typesense import error details:", e.importResults);
        }
        throw e;
    }
}
// Internal: create_casefile implementation
async function create_casefile(args) {
    const casefileName = args.casefileName || null;
    const casefileSummary = args.casefileSummary || null;
    const currentStatus = args.currentStatus || null;
    const originalImportance = args.importance ?? 0;
    const category = args.category || "General";
    const role = args.role || null;
    const imo = args.imo || null;
    let vesselName = null;
    let vesselId = null;
    if (imo) {
        try {
            const vesselDetails = await getVesselDetails(imo.toString());
            vesselName = vesselDetails?.vesselName || null;
            vesselId = vesselDetails?.id || null;
        }
        catch (e) {
            vesselName = null;
            vesselId = null;
        }
    }
    const mongoClient = await getMongoClient(config.mongoUri);
    const db = mongoClient.db(config.dbName);
    const collection = db.collection("casefiles");
    // Just search if that casefile is already there
    let result = null;
    if (category !== "General") {
        result = await collection.findOne({ imo: imo, category: category });
    }
    let casefileId = result ? result._id : null;
    let alreadyExists = !!result;
    // Prepare data
    let data = {
        vesselId: vesselId,
        imo: imo,
        vesselName: vesselName,
        casefile: casefileName,
        currentStatus: currentStatus,
        summary: casefileSummary,
        originalImportance: originalImportance,
        importance: originalImportance,
        category: category,
        role: role,
        followUp: "",
        createdAt: new Date(),
        updatedAt: new Date(),
        index: [],
        pages: []
    };
    if (casefileId) {
        delete data.createdAt;
        delete data.originalImportance;
        delete data.index;
        delete data.pages;
        delete data.followUp;
    }
    // Insert or update
    let mongoResult;
    if (casefileId) {
        mongoResult = await collection.updateOne({ _id: casefileId }, { $set: data });
    }
    else {
        mongoResult = await collection.insertOne(data);
        casefileId = mongoResult.insertedId;
    }
    // Log after MongoDB push
    console.info(`[create_casefile] Data pushed to MongoDB:`, { casefileId, data });
    // Generate casefile URL and update
    const casefileUrl = await generateCasefileWeblink(casefileId.toString());
    await collection.updateOne({ _id: casefileId }, { $set: { link: casefileUrl } });
    // If not already exists, push to Typesense
    if (!alreadyExists) {
        try {
            await pushToTypesense({ ...data, _id: casefileId }, 'create');
        }
        catch (e) {
            console.error(`Error importing data to Typesense: ${e}`);
        }
    }
    return {
        type: "text",
        text: `Casefile created with casefile url: ${casefileUrl}`
    };
}
// Internal: update_casefile implementation
async function update_casefile(args) {
    const { casefile_url, casefileSummary, importance, tags: tagsArg, topic, summary: summaryArg, mailId, currentStatus, casefileName, facts, links: linksArg, detailed_report } = args;
    let plan_status = "unprocessed";
    let tags = tagsArg || [];
    let summary = summaryArg || "";
    let links = Array.isArray(linksArg) ? linksArg.map((i) => ({ link: i })) : [];
    // Helper: markdown_to_html_link stub
    function markdown_to_html_link(md) {
        // Replace with actual markdown-to-HTML-link logic if needed
        return md ? `<a href='#'>${md.substring(0, 32)}...</a>` : "";
    }
    // Add detailed_report as first link
    links = [{ link: markdown_to_html_link(detailed_report || "") }, ...links];
    const mongoClient = await getMongoClient(config.mongoUri);
    const db = mongoClient.db(config.dbName);
    const collection = db.collection("casefiles");
    if (!casefile_url) {
        throw new Error("Casefile URL is required");
    }
    let casefile_id = casefile_url;
    // If not a valid ObjectId, try to resolve from link (stub for link_to_id)
    if (!ObjectId.isValid(casefile_url)) {
        // Implement link_to_id if needed; for now, throw
        throw new Error("Valid Casefile ID is required");
    }
    // Normalize tags
    if (typeof tags === "string") {
        tags = [tags];
    }
    // If facts present, append to summary
    if (facts) {
        summary = summary + " <br> " + facts;
    }
    // Build aggregation pipeline
    const now = new Date();
    const update_pipeline = [];
    // Stage 1: Conditional base field updates
    const set_stage = { updatedAt: now };
    if (casefileName !== undefined)
        set_stage.casefile = casefileName;
    if (currentStatus !== undefined)
        set_stage.currentStatus = currentStatus;
    if (casefileSummary !== undefined)
        set_stage.summary = casefileSummary;
    if (importance !== undefined)
        set_stage.importance = importance;
    if (plan_status !== undefined)
        set_stage.plan_status = plan_status;
    if (Object.keys(set_stage).length > 0) {
        update_pipeline.push({ $set: set_stage });
    }
    // Stage 2: Ensure arrays exist and compute new pagenum
    update_pipeline.push({
        $set: {
            pages: { $ifNull: ["$pages", []] },
            index: { $ifNull: ["$index", []] },
            _nextPageNum: {
                $add: [
                    {
                        $max: [
                            { $ifNull: [{ $max: "$pages.pagenum" }, 0] },
                            { $ifNull: [{ $max: "$index.pagenum" }, 0] }
                        ]
                    },
                    1
                ]
            }
        }
    });
    // Stage 3: Update tags as a unique set
    if (tags && tags.length > 0) {
        update_pipeline.push({
            $set: {
                tags: { $setUnion: [{ $ifNull: ["$tags", []] }, tags] }
            }
        });
    }
    // Stage 4: Append to pages and index arrays
    update_pipeline.push({
        $set: {
            pages: {
                $concatArrays: [
                    "$pages",
                    [
                        {
                            pagenum: "$_nextPageNum",
                            summary: summary,
                            createdAt: now,
                            subject: topic,
                            flag: topic,
                            type: "SIYA",
                            link: links,
                            plan_status: plan_status
                        }
                    ]
                ]
            },
            index: {
                $concatArrays: [
                    "$index",
                    [
                        {
                            pagenum: "$_nextPageNum",
                            type: "SIYA",
                            createdAt: now,
                            topic: topic,
                            plan_status: plan_status
                        }
                    ]
                ]
            }
        }
    });
    // Stage 5: Cleanup temporary field
    update_pipeline.push({ $unset: "_nextPageNum" });
    // Execute update
    await collection.updateOne({ _id: new ObjectId(casefile_id) }, update_pipeline // MongoDB Node driver supports pipeline updates with 'as any'
    );
    // Fetch updated document
    const mongoresult = await collection.findOne({ _id: new ObjectId(casefile_id) });
    if (!mongoresult) {
        throw new Error(`No casefile found with ID: ${casefile_id} after update.`);
    }
    // Push to Typesense
    try {
        await pushToTypesense(mongoresult, 'upsert');
    }
    catch (e) {
        console.error(`Error updating casefile in Typesense: ${e}`);
    }
    // Return text response
    return {
        type: "text",
        text: `Casefile updated with casefile url: ${mongoresult.link}`
    };
}
// Dispatcher for write operations
async function write_casefile_data(args) {
    const op = args.operation;
    if (op === "write_casefile") {
        return await create_casefile(args);
    }
    if (op === "write_page") {
        return await update_casefile(args);
    }
    throw new Error(`Unsupported operation for write_casefile_data: '${op}'`);
}
// Tool Definitions
export const toolDefinitions = [
    {
        name: "get_casefile_index",
        description: "Get the casefile index",
        inputSchema: {
            type: "object",
            properties: {
                casefile_id: {
                    type: "string",
                    description: "ID of the casefile"
                },
                limit: {
                    type: "number",
                    description: "Max log entries to return.",
                    minimum: 1,
                    default: 10
                },
                next_iter: {
                    type: "number",
                    description: "The pagination parameter. If 0, returns the latest entries. If 1, returns the next batch of older entries, and so on.",
                    default: 0
                }
            },
            required: ["casefile_id"]
        },
        handler: async (args) => {
            try {
                const casefile_id = args.casefile_id;
                if (!casefile_id) {
                    throw new Error("casefile_id is required");
                }
                const limit = args.limit || 10;
                const next_iter = args.next_iter || 0;
                console.log(`Getting casefile index for: ${casefile_id}, limit: ${limit}, next_iter: ${next_iter}`);
                const mongoClient = await getMongoClient(config.mongoUri);
                const db = mongoClient.db(config.dbName);
                const collection = db.collection("casefiles");
                // Get total entries count
                const pipelineLength = [
                    { $match: { _id: new ObjectId(casefile_id) } },
                    { $project: { index: { $size: "$index" } } }
                ];
                const result = await collection.aggregate(pipelineLength).toArray();
                if (!result || result.length === 0) {
                    return [{
                            type: "text",
                            text: `No casefile found with ID: ${casefile_id}`
                        }];
                }
                const total_entries = result[0].index;
                let skip;
                if (next_iter === 0) {
                    skip = Math.max(0, total_entries - limit);
                }
                else {
                    skip = Math.max(0, total_entries - (limit * (next_iter + 1)));
                    if (skip < 0)
                        skip = 0;
                }
                const entries_to_take = Math.min(limit, total_entries - skip);
                if (entries_to_take <= 0) {
                    return [{
                            type: "text",
                            text: `No more index entries to retrieve for casefile ID: ${casefile_id}`
                        }];
                }
                const pipelineFetch = [
                    { $match: { _id: new ObjectId(casefile_id) } },
                    {
                        $project: {
                            index: { $slice: ["$index", skip, entries_to_take] },
                            category: 1,
                            imo: 1,
                            link: 1,
                            casefile: 1, // ✅ Add casefile title
                            _id: 1
                        }
                    }
                ];
                const fetchResult = await collection.aggregate(pipelineFetch).toArray();
                if (!fetchResult || fetchResult.length === 0) {
                    return [{
                            type: "text",
                            text: `Failed to retrieve index entries for casefile ID: ${casefile_id}`
                        }];
                }
                const document = fetchResult[0];
                const response = {
                    casefile_id: document._id.toString(),
                    index: document.index || [],
                    category: document.category,
                    imo: document.imo,
                    link: document.link,
                    total_entries: total_entries,
                    current_batch: next_iter,
                    total_batches: Math.max(1, Math.ceil(total_entries / limit)),
                    has_more: skip > 0
                };
                // ✅ Artifact from casefile-level link
                const link_data = document.link
                    ? [{ title: document.casefile || `Casefile ${casefile_id}`, url: document.link }]
                    : [];
                const artifacts = getListOfArtifacts(document.casefile || `casefile index ${casefile_id}`, link_data);
                return [
                    {
                        type: "text",
                        text: JSON.stringify(response, null, 2),
                        title: `Casefile Index for ${casefile_id}`,
                        format: "json"
                    },
                    ...artifacts
                ];
            }
            catch (error) {
                throw new Error(`Error getting casefile index: ${error}`);
            }
        }
    },
    {
        name: "get_casefile_pages",
        description: "Get specific pages from a casefile by page number (1-based). For example, [1, 2, 3] returns the first three pages; [2, 5] returns only pages 2 and 5 if they exist.",
        inputSchema: {
            type: "object",
            properties: {
                casefile_id: {
                    type: "string",
                    description: "ID of the casefile"
                },
                pages: {
                    type: "array",
                    description: "List of 1-based page numbers to fetch. Can be contiguous (e.g. [1, 2, 3]) or sparse (e.g. [2, 5]).",
                    items: {
                        type: "number",
                        minimum: 1
                    },
                    minItems: 1
                }
            },
            required: ["casefile_id", "pages"]
        },
        handler: async (args) => {
            try {
                const casefile_id = args.casefile_id;
                let page_list = args.pages || [];
                if (!Array.isArray(page_list) || page_list.length === 0) {
                    throw new Error("pages must be a non-empty list");
                }
                try {
                    page_list = [...new Set(page_list.map(p => parseInt(p.toString())))].sort((a, b) => a - b);
                }
                catch (error) {
                    throw new Error("All entries in 'pages' must be integers or castable to integers");
                }
                if (!casefile_id) {
                    throw new Error("casefile_id is required");
                }
                console.log(`Getting casefile pages for: ${casefile_id}, pages: ${page_list}`);
                const mongoClient = await getMongoClient(config.mongoUri);
                const db = mongoClient.db(config.dbName);
                const collection = db.collection("casefiles");
                const doc = await collection.findOne({ _id: new ObjectId(casefile_id) }, { projection: { link: 1, casefile: 1 } });
                if (!doc) {
                    return [{
                            type: "text",
                            text: `No casefile found with ID: ${casefile_id}`
                        }];
                }
                const page_indices = page_list.map(p => p - 1); // convert to 0-based
                const min_index = Math.min(...page_indices);
                const max_index = Math.max(...page_indices);
                const slice_start = Math.max(0, min_index);
                const slice_count = max_index - min_index + 1;
                console.log(`Mongo slice: start=${slice_start}, count=${slice_count}`);
                const pipeline = [
                    { $match: { _id: new ObjectId(casefile_id) } },
                    {
                        $project: {
                            pages: { $slice: ["$pages", slice_start, slice_count] },
                            _id: 0
                        }
                    }
                ];
                const result = await collection.aggregate(pipeline).toArray();
                if (!result || result.length === 0) {
                    return [{
                            type: "text",
                            text: "No pages found for the given casefile ID."
                        }];
                }
                const sliced_pages = result[0].pages || [];
                const requested_pages = [];
                for (const page_num of page_list) {
                    const actual_index = page_num - 1; // 0-based
                    const i = actual_index - slice_start;
                    if (i >= 0 && i < sliced_pages.length) {
                        requested_pages.push(sliced_pages[i]);
                    }
                }
                if (requested_pages.length === 0) {
                    return [{
                            type: "text",
                            text: "No pages matched the requested indices"
                        }];
                }
                const response = {
                    casefile_id: casefile_id,
                    pages_requested: page_list,
                    pages_returned: requested_pages,
                    link: doc.link
                };
                const link_data = doc.link
                    ? [{ title: doc.casefile || `Casefile ${casefile_id}`, url: doc.link }]
                    : [];
                const artifacts = getListOfArtifacts(doc.casefile || `casefile pages ${casefile_id}`, link_data);
                return [
                    {
                        type: "text",
                        text: JSON.stringify(response, null, 2),
                        title: `Casefile Pages for ${casefile_id}`,
                        format: "json"
                    },
                    ...artifacts
                ];
            }
            catch (error) {
                throw new Error(`Error getting casefile pages: ${error}`);
            }
        }
    },
    {
        name: "get_latest_plan",
        description: "Get the latest plan for the casefile, the tool only accepts the casefile id whcih is the mongo db id of the case fiel , do not provide any other id like imo number or vessel name, that will not work.",
        inputSchema: {
            type: "object",
            properties: {
                casefile_id: {
                    type: "string",
                    description: "ID of the casefile it will the mongo db id of the case file."
                }
            },
            required: ["casefile_id"]
        },
        handler: async (args) => {
            try {
                const casefile_id = args.casefile_id;
                if (!casefile_id) {
                    throw new Error("casefile_id is required");
                }
                console.log(`Getting latest plan for casefile: ${casefile_id}`);
                const mongoClient = await getMongoClient(config.mongoUri);
                const db = mongoClient.db(config.dbName);
                const collection = db.collection("casefiles");
                const document = await collection.findOne({ _id: new ObjectId(casefile_id) }, { projection: { casefilePlans: 1 } });
                if (!document) {
                    return [{
                            type: "text",
                            text: `No casefile found with ID: ${casefile_id}`
                        }];
                }
                const plans = document.casefilePlans || [];
                if (!plans || !Array.isArray(plans) || plans.length === 0) {
                    return [{
                            type: "text",
                            text: "No plans found for the casefile"
                        }];
                }
                const sorted_plans = plans.sort((a, b) => {
                    const dateA = a.dateTime || "";
                    const dateB = b.dateTime || "";
                    return dateB.localeCompare(dateA);
                });
                const latest_plan = sorted_plans[0];
                const result = {
                    casefile_id: casefile_id,
                    latest_plan: {
                        dateTime: latest_plan.dateTime,
                        flag: latest_plan.flag,
                        plan: latest_plan.plan
                    }
                };
                return [{
                        type: "text",
                        text: JSON.stringify(result, null, 2),
                        title: `Latest Plan for ${casefile_id}`,
                        format: "json"
                    }];
            }
            catch (error) {
                throw new Error(`Error getting latest plan: ${error}`);
            }
        }
    },
    {
        name: "smart_casefile_search",
        description: `Universal, adaptive search tool for vessel case-files. Supports free-text semantic search, hybrid search, and fine-grained filtering by vessel, category, importance, plan status, and date-range.
  
  **PAGINATION & RESULTS:**
  - Returns up to 'max_results' (default: 10, max: 20) case-files per request
  - Response ALWAYS includes:
    • 'found': Total number of matching case-files in the entire database
    • 'has_more': Boolean indicating if more results exist beyond current page
    • 'hits': Array containing the actual case-file results for current page
    • 'page': Current page number
    • 'per_page': Number of results in current page
  
  **TO GET MORE RESULTS:**
  - If 'has_more' is true, increment 'page' parameter (e.g., page: 2, page: 3, etc.)
  - Example: If search returns found: 47, page: 1, per_page: 10, has_more: true
    → This means you're seeing results 1-10 of 47 total
    → Set page: 2 to get results 11-20
    → Set page: 3 to get results 21-30, and so on
  
  **VESSEL NAME HANDLING:**
  ✔️ If the user mentions a vessel **name**, ALWAYS:
  1. Call 'get_vessel_details' to convert that name to its IMO number
  2. Pass the returned IMO to the 'imo' parameter so that only case-files for the correct vessel are returned`,
        inputSchema: {
            type: "object",
            properties: {
                query: {
                    type: "string",
                    description: "Natural-language query. Leave blank to browse all case-files for the given filters. Example: 'main engine vibration root cause'"
                },
                search_type: {
                    type: "string",
                    description: "Search strategy: • 'semantic' → vector search on the 'embedding' field for conceptual matching • 'hybrid' → combines semantic search with keyword matching across embedding, text content, and vessel name fields for comprehensive results. Choose automatically based on the user's intent; default is 'hybrid'.",
                    enum: ["semantic", "hybrid"],
                    default: "hybrid"
                },
                // Flattened filter properties
                imo: {
                    type: "number",
                    description: "IMO number for vessel-specific searches. **Required** whenever the user's query refers to a specific vessel name (convert via 'get_vessel_details')."
                },
                vessel_name: {
                    type: "string",
                    description: "Vessel name (case-insensitive). Used only when IMO is not yet known; tool will internally resolve name→IMO."
                },
                category: {
                    type: "string",
                    enum: [
                        "General",
                        "LocationAndCargoActivity",
                        "sire",
                        "loReport",
                        "internalAudit",
                        "vir",
                        "FleetAlert",
                        "SmartShipAlert",
                        "fleetAlert",
                        "smartShipAlert",
                        "locationAndCargoActivity",
                        "classSurveyAndCertificateStatus"
                    ],
                    description: "Category tag (must match one of the enum values)."
                },
                importance: {
                    type: "string",
                    enum: ["Medium", "Low", "High"],
                    description: "Importance of the case file (must be one of: Medium, Low, High)"
                },
                from_date: {
                    type: "string",
                    description: "Include case-files with 'lastCasefileUpdateDate' **on or after** this date (dd/mm/yyyy)."
                },
                to_date: {
                    type: "string",
                    description: "Include case-files with 'lastCasefileUpdateDate' **up to** this date (dd/mm/yyyy)."
                },
                sort_by: {
                    type: "string",
                    description: "Sort order: • 'relevance' → by search score/relevance (default) • 'recent' → 'lastCasefileUpdateDate' desc • 'oldest' → 'lastCasefileUpdateDate' asc",
                    enum: [
                        "relevance",
                        "recent",
                        "oldest"
                    ],
                    default: "relevance"
                },
                max_results: {
                    type: "number",
                    description: `Number of results to return PER PAGE (not total).
  • Default: 10 results per page
  • Range: 1-20 results per page
  • To get MORE than 20 results, use multiple requests with increasing 'page' numbers
  • Example: max_results: 10, page: 1 → results 1-10
             max_results: 10, page: 2 → results 11-20`,
                    minimum: 1,
                    maximum: 20,
                    default: 10
                },
                page: {
                    type: "number",
                    description: `Which page of results to retrieve (1-based).
  • Default: 1 (first page)
  • Use this to paginate through large result sets
  • Example response will show: {found: 50, page: 1, per_page: 10, has_more: true}
    → This means 50 total results exist, showing results 1-10, more pages available
    → Set page: 2 to get results 11-20
    → Set page: 3 to get results 21-30, etc.
  • Continue incrementing until has_more: false`,
                    minimum: 1,
                    default: 1
                }
            },
            required: [],
            additionalProperties: true
        },
        handler: async (args) => {
            try {
                // Defaults & basic params
                const query = args.query || "*";
                const search_type = args.search_type || "hybrid";
                const sort_by_arg = args.sort_by || "relevance";
                const max_results = Math.max(1, Math.min(args.max_results || 10, 20));
                const page = Math.max(1, args.page || 1);
                // Reconstruct filters object from flattened parameters
                const filters = {
                    imo: args.imo,
                    vessel_name: args.vessel_name,
                    category: args.category,
                    importance: args.importance,
                    from_date: args.from_date,
                    to_date: args.to_date
                };
                console.log(`[smart_casefile_search] q='${query}' type=${search_type} filters=${JSON.stringify(filters)} sort=${sort_by_arg} limit=${max_results} page=${page}`);
                // 🚨 CRITICAL: Input Validation (Before Production)
                // 1. Enum Validation - Prevent silent failures
                const validCategories = [
                    "General",
                    "LocationAndCargoActivity",
                    "sire",
                    "loReport",
                    "internalAudit",
                    "vir",
                    "FleetAlert",
                    "SmartShipAlert",
                    "fleetAlert",
                    "smartShipAlert",
                    "locationAndCargoActivity",
                    "classSurveyAndCertificateStatus"
                ];
                const validImportance = ["Low", "Medium", "High"];
                if (filters.category && !validCategories.includes(filters.category)) {
                    return [{
                            type: "text",
                            text: `Invalid category '${filters.category}'. Valid categories: ${validCategories.join(', ')}`
                        }];
                }
                if (filters.importance && !validImportance.includes(filters.importance)) {
                    return [{
                            type: "text",
                            text: `Invalid importance '${filters.importance}'. Valid values: ${validImportance.join(', ')}`
                        }];
                }
                // 2. Date Validation - Prevent server crashes
                const validateAndParseDate = (dateStr, fieldName) => {
                    // Check basic format first
                    if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dateStr)) {
                        throw new Error(`Invalid ${fieldName} format '${dateStr}'. Please use dd/mm/yyyy format (e.g., 15/03/2024).`);
                    }
                    const [day, month, year] = dateStr.split('/').map(Number);
                    // Validate ranges
                    if (day < 1 || day > 31) {
                        throw new Error(`Invalid day in ${fieldName}: '${day}'. Day must be between 1-31.`);
                    }
                    if (month < 1 || month > 12) {
                        throw new Error(`Invalid month in ${fieldName}: '${month}'. Month must be between 1-12.`);
                    }
                    if (year < 1900 || year > 2100) {
                        throw new Error(`Invalid year in ${fieldName}: '${year}'. Year must be between 1900-2100.`);
                    }
                    // Create date and check if it's valid
                    const date = new Date(year, month - 1, day);
                    if (date.getDate() !== day || date.getMonth() !== month - 1 || date.getFullYear() !== year) {
                        throw new Error(`Invalid date in ${fieldName}: '${dateStr}' is not a valid calendar date.`);
                    }
                    const timestamp = Math.floor(date.getTime() / 1000);
                    if (isNaN(timestamp)) {
                        throw new Error(`Invalid ${fieldName} format '${dateStr}'. Please use dd/mm/yyyy format.`);
                    }
                    return timestamp;
                };
                // Validate and parse dates once
                let fromTimestamp = null;
                let toTimestamp = null;
                if (filters.from_date) {
                    try {
                        fromTimestamp = validateAndParseDate(filters.from_date, "from_date");
                    }
                    catch (error) {
                        return [{
                                type: "text",
                                text: error instanceof Error ? error.message : `Invalid from_date format '${filters.from_date}'. Please use dd/mm/yyyy format.`
                            }];
                    }
                }
                if (filters.to_date) {
                    try {
                        toTimestamp = validateAndParseDate(filters.to_date, "to_date");
                    }
                    catch (error) {
                        return [{
                                type: "text",
                                text: error instanceof Error ? error.message : `Invalid to_date format '${filters.to_date}'. Please use dd/mm/yyyy format.`
                            }];
                    }
                }
                // 🚨 END CRITICAL VALIDATION
                // Resolve vessel name → IMO (if required)
                let imo = filters.imo;
                let vesselNameFilter = null;
                if (!imo && filters.vessel_name) {
                    try {
                        const vessel_data = await getVesselDetails(filters.vessel_name);
                        if (vessel_data && vessel_data.imo) {
                            imo = vessel_data.imo;
                            console.log(`[smart_casefile_search] Successfully resolved vessel '${filters.vessel_name}' to IMO: ${imo}`);
                        }
                        else {
                            // IMO not found in response, use vessel name as fallback
                            vesselNameFilter = filters.vessel_name;
                            console.log(`[smart_casefile_search] No IMO found for vessel '${filters.vessel_name}', using vessel name as filter`);
                        }
                    }
                    catch (error) {
                        // Error occurred, use vessel name as fallback
                        vesselNameFilter = filters.vessel_name;
                        console.log(`[smart_casefile_search] Error resolving vessel '${filters.vessel_name}': ${error}. Using vessel name as filter`);
                    }
                }
                // Build Typesense `filter_by` string
                const fb = [];
                if (imo) {
                    fb.push(`imo:=${imo}`);
                }
                else if (vesselNameFilter) {
                    fb.push(`vesselName:='${vesselNameFilter}'`);
                }
                if (filters.category) {
                    fb.push(`category:='${filters.category}'`);
                }
                if (filters.importance) {
                    fb.push(`importance:='${filters.importance}'`);
                }
                // Date range filtering - using lastCasefileUpdateDate
                if (fromTimestamp) {
                    fb.push(`lastCasefileUpdateDate:>=${fromTimestamp}`);
                }
                if (toTimestamp) {
                    // Add 86399 seconds (23:59:59) to include the entire end date
                    fb.push(`lastCasefileUpdateDate:<=${toTimestamp + 86399}`);
                }
                const filter_by = fb.length > 0 ? fb.join(" && ") : undefined;
                // Prepare search payload
                const query_by = search_type === "semantic"
                    ? "embedding"
                    : "embedding,embedding_text,vesselName";
                // Fix the sort configuration
                const sort_map = {
                    "recent": "lastCasefileUpdateDate:desc",
                    "oldest": "lastCasefileUpdateDate:asc"
                };
                const payload = {
                    q: query,
                    query_by: query_by,
                    include_fields: "casefileInitiationDate,vesselName,_id,lastCasefileUpdateDate,casefile,summary,link,category,importance,imo",
                    exclude_fields: "embedding",
                    per_page: max_results,
                    page: page,
                    prefix: false
                };
                // Only add sort_by if not using relevance (let Typesense use default search score sorting)
                if (sort_by_arg !== "relevance") {
                    const sort_by = sort_map[sort_by_arg] || sort_map["recent"];
                    payload.sort_by = sort_by;
                }
                if (filter_by) {
                    payload.filter_by = filter_by;
                }
                // Execute search
                const typesenseClient = getTypesenseClient(config);
                const results = await typesenseClient.collections("emailCasefile").documents().search(payload);
                // Post-process hits
                const hits = results.hits || [];
                const processed_hits = [];
                for (const h of hits) {
                    const doc = convertCasefileDates(h.document);
                    processed_hits.push({
                        id: doc._id,
                        score: h.text_match || 0,
                        document: doc,
                    });
                }
                // Apply IMO filtering to hits before generating artifacts
                const { isValidImoForCompany, shouldBypassImoFiltering } = await import('../utils/imoUtils.js');
                const filteredHits = processed_hits.filter(hit => {
                    const companyName = config.companyName || '';
                    // Skip filtering for admin companies
                    if (shouldBypassImoFiltering(companyName)) {
                        return true;
                    }
                    // Check if hit has IMO and validate it
                    const document = hit.document;
                    if (document.imo) {
                        return isValidImoForCompany(document.imo);
                    }
                    return true; // Keep hits without IMO
                });
                const total_found = results.found || 0;
                const has_more = (page * max_results) < total_found;
                // Generate informative message if all hits were filtered out
                let response_obj = {
                    found: filteredHits.length, // Use filtered count
                    page: page,
                    per_page: max_results,
                    has_more: has_more,
                    hits: filteredHits, // Use filtered hits
                };
                // If we had results but they were all filtered out, add informative message
                if (processed_hits.length > 0 && filteredHits.length === 0) {
                    const companyName = config.companyName || 'current company';
                    // Get the IMO numbers that were filtered out
                    const filteredImos = processed_hits
                        .map(hit => hit.document.imo)
                        .filter(imo => imo && !isValidImoForCompany(imo))
                        .map(imo => String(imo));
                    const uniqueImos = [...new Set(filteredImos)];
                    let message;
                    if (uniqueImos.length === 1) {
                        message = `Vessel with this IMO ${uniqueImos[0]} is not part of ${companyName}`;
                    }
                    else if (uniqueImos.length > 1) {
                        message = `Vessels with IMO ${uniqueImos.join(', ')} are not part of ${companyName}`;
                    }
                    else {
                        message = `No vessels found that belong to ${companyName}`;
                    }
                    response_obj.message = message;
                }
                // Build artifacts from filtered hits (for your browser-view visualizations)
                const link_data = filteredHits.map(h => ({
                    title: h.document.casefile,
                    url: h.document.link
                })).filter(item => item.url);
                const artifacts = getListOfArtifacts(query !== "*" ? query : "smart search", link_data);
                return [
                    {
                        type: "text",
                        title: `Smart Casefile Search Results (page ${page})`,
                        text: JSON.stringify(response_obj, null, 2),
                        format: "json",
                    },
                    ...artifacts,
                ];
            }
            catch (error) {
                throw new Error(`Error performing smart casefile search: ${error}`);
            }
        }
    },
    {
        name: 'email_search',
        description: [
            'Use this tool to retrieve one or more emails from the company\'s email archive based on flexible search criteria.',
            'It can answer queries like "Get all emails for NEXUS VICTORIA",',
            '"Get all emails from shipping@bothwinnb.com in the last 3 days",',
            '"Get all emails to Captain John", "Get all emails cc\'d to Alex Lee",',
            '"Get all emails about pilot boarding", or any combination of these parameters.',
            'You can provide one or more query parameters to refine your search, including vessel name, IMO, sender/recipient/cc name or email, tags, subject, date range, attachments, and more.',
            'Multiple matching emails may be returned for a single query.'
        ].join(' '),
        inputSchema: {
            type: 'object',
            properties: {
                vessel_name: {
                    type: 'string',
                    description: "Vessel name to filter emails. Partial or full vessel names are accepted."
                },
                vessel_id: {
                    type: 'string',
                    description: "Vessel ID to filter emails for a specific vessel."
                },
                imo: {
                    type: 'string',
                    description: "IMO number of the vessel."
                },
                from_email: {
                    type: 'string',
                    description: "Sender's email address. Use to get all emails sent by this address."
                },
                from_name: {
                    type: 'string',
                    description: "Sender's name. Use to get all emails sent by this person (partial or full name accepted)."
                },
                to_email: {
                    type: 'string',
                    description: "Recipient's email address. Use to get all emails sent to this address."
                },
                to_name: {
                    type: 'string',
                    description: "Recipient's name. Use to get all emails sent to this person (partial or full name accepted)."
                },
                cc_email: {
                    type: 'string',
                    description: "CC recipient's email address. Use to get all emails where this address is CC'd."
                },
                cc_name: {
                    type: 'string',
                    description: "CC recipient's name. Use to get all emails where this person is CC'd (partial or full name accepted)."
                },
                subject_query: {
                    type: 'string',
                    description: "Search for specific words or phrases in the email subject."
                },
                casefile_query: {
                    type: 'string',
                    description: "Search for specific terms in the casefile field (e.g., terminal info, agent contact)."
                },
                narrative_query: {
                    type: 'string',
                    description: "Search for specific text in the narrative field of the email."
                },
                tags: {
                    type: 'array',
                    items: { type: 'string' },
                    description: "Filter emails by one or more tags (e.g., agent, crewlist)."
                },
                importance: {
                    type: 'string',
                    description: "Importance of the email (e.g., 'high', 'low')."
                },
                date_from: {
                    type: 'string',
                    format: 'date-time',
                    description: "Start date (inclusive) in ISO8601 format. Used to filter emails by earliest date."
                },
                date_to: {
                    type: 'string',
                    format: 'date-time',
                    description: "End date (inclusive) in ISO8601 format. Used to filter emails by latest date."
                },
                has_attachments: {
                    type: 'boolean',
                    description: "If true, only emails with one or more attachments are returned."
                },
                limit: {
                    type: 'integer',
                    description: "Maximum number of emails to return (default 50, max 200)."
                },
                sort_by_date: {
                    type: 'string',
                    enum: ['asc', 'desc'],
                    description: "Sort results by date in ascending ('asc') or descending ('desc', default) order."
                }
            }
        },
        handler: async (args) => {
            try {
                const limit = Math.max(1, Math.min(args.limit || 50, 200));
                const sort_by_date = args.sort_by_date || 'desc';
                console.log(`[email_search] Searching emails with filters: ${JSON.stringify(args)}`);
                // Build Typesense search query
                const query_parts = [];
                const filter_parts = [];
                // use a loop to add all the query_parts to the query
                const list_of_query_fields_from_args = ['vessel_name', 'vessel_id', 'imo', 'from_email', 'from_name', 'to_email', 'to_name', 'cc_email', 'cc_name', 'subject_query', 'casefile_query', 'narrative_query', 'tags', 'importance', 'date_from', 'date_to', 'has_attachments', 'limit', 'sort_by_date'];
                for (const part of list_of_query_fields_from_args) {
                    if (args[part]) {
                        query_parts.push(args[part]);
                    }
                }
                const query = query_parts.length > 0 ? query_parts.join(' ') : '*';
                // Build filters - mapping to vessel_mails schema
                // IMO filter
                if (args.imo) {
                    // imo is a string in the schema, not a number
                    filter_parts.push(`imo:=${args.imo}`);
                }
                // Date filters - using date_time field which is int64 in the schema
                if (args.date_from) {
                    const from_timestamp = Math.floor(new Date(args.date_from).getTime());
                    filter_parts.push(`date_time:>=${from_timestamp}`);
                }
                if (args.date_to) {
                    const to_timestamp = Math.floor(new Date(args.date_to).getTime());
                    filter_parts.push(`date_time:<=${to_timestamp}`);
                }
                const filter_by = filter_parts.length > 0 ? filter_parts.join(' && ') : undefined;
                // Prepare search payload - using fields from the schema
                const payload = {
                    q: query,
                    query_by: 'casefile,cc_recipients_email_addresses,subject,narrative,sender_name,sender_email_address,to_recipients_names,to_recipients_email_addresses,cc_recipients_names,vessel_name,tags',
                    include_fields: 'mail_body_id,subject,vessel_name,imo,vessel_id,sender_name,sender_email_address,to_recipients_names,to_recipients_email_addresses,cc_recipients_names,cc_recipients_email_addresses,narrative,casefile,attachments,date_time,tags,mail_body_link',
                    per_page: limit,
                    page: 1,
                    sort_by: `date_time:${sort_by_date}`,
                    prefix: false
                };
                if (filter_by) {
                    payload.filter_by = filter_by;
                }
                // Execute search on vessel_mails collection
                const typesenseClient = getTypesenseClient(config);
                const results = await typesenseClient.collections("vessel_mails").documents().search(payload);
                // Process results
                const hits = results.hits || [];
                // Format Typesense results with date formatting
                const processed_hits = hits.map((hit) => ({
                    id: hit.document._id,
                    score: hit.text_match || 0,
                    document: {
                        ...hit.document,
                        date: hit.document.date_time ? new Date((hit.document.date_time)).toISOString().slice(0, 19).replace('T', ' ') : null
                    }
                }));
                // Apply IMO filtering to hits before generating artifacts
                const { isValidImoForCompany, shouldBypassImoFiltering } = await import('../utils/imoUtils.js');
                const filteredHits = processed_hits.filter(hit => {
                    const companyName = config.companyName || '';
                    // Skip filtering for admin companies
                    if (shouldBypassImoFiltering(companyName)) {
                        return true;
                    }
                    // Check if hit has IMO and validate it
                    const document = hit.document;
                    if (document.imo) {
                        return isValidImoForCompany(document.imo);
                    }
                    return true; // Keep hits without IMO
                });
                // Collect email dump links for artifacts from filtered hits
                const emailArtifactLinks = [];
                // Check each filtered hit for mail_body_link directly from Typesense results
                for (const hit of filteredHits) {
                    const document = hit.document;
                    // Only add to links if mail_body_link is present in the Typesense document
                    if (document && document.mail_body_link) {
                        emailArtifactLinks.push({
                            title: document.subject || `Email ${document._id}`,
                            url: document.mail_body_link
                        });
                    }
                }
                // Generate informative message if all hits were filtered out
                let response_obj = {
                    found: filteredHits.length, // Use filtered count
                    hits: filteredHits // Use filtered hits
                };
                // If we had results but they were all filtered out, add informative message
                if (processed_hits.length > 0 && filteredHits.length === 0) {
                    const companyName = config.companyName || 'current company';
                    // Get the IMO numbers that were filtered out
                    const filteredImos = processed_hits
                        .map(hit => {
                        const doc = hit.document;
                        return doc.imo;
                    })
                        .filter(imo => imo && !isValidImoForCompany(imo))
                        .map(imo => String(imo));
                    const uniqueImos = [...new Set(filteredImos)];
                    let message;
                    if (uniqueImos.length === 1) {
                        message = `Vessel with this IMO ${uniqueImos[0]} is not part of ${companyName}`;
                    }
                    else if (uniqueImos.length > 1) {
                        message = `Vessels with IMO ${uniqueImos.join(', ')} are not part of ${companyName}`;
                    }
                    else {
                        message = `No vessels found that belong to ${companyName}`;
                    }
                    response_obj.message = message;
                }
                // Return Typesense results as main content and email as artifacts
                const response = [{
                        type: "text",
                        title: `Email Search Results (${response_obj.found} found)`,
                        text: JSON.stringify(response_obj, null, 2),
                        format: "json"
                    }];
                // Generate artifacts using the provided function
                const artifacts = getListOfArtifacts("email_search", emailArtifactLinks);
                // Add artifacts if any were found
                return [...response, ...artifacts];
            }
            catch (error) {
                console.error(`Error searching emails: ${error}`);
                return [{
                        type: "text",
                        title: "Email Search Error",
                        text: `Failed to search emails: ${error instanceof Error ? error.message : String(error)}`,
                        format: "text"
                    }];
            }
        }
    },
    {
        name: "get_email_details",
        description: "Retrieve the complete details of an email by its ID. This tool fetches the full email content, including body, attachments, and metadata from MongoDB.",
        inputSchema: {
            type: "object",
            properties: {
                mail_body_id: {
                    type: "string",
                    description: "The MongoDB Object ID of the email to retrieve. This is typically obtained from email_search results."
                }
            },
            required: ["mail_body_id"]
        },
        handler: async (args) => {
            try {
                const emailId = args.mail_body_id;
                if (!emailId) {
                    throw new Error("mail_body_id is required");
                }
                logger.info(`[get_email_details] Retrieving email details for ID: ${emailId}`);
                const result = await get_full_email_details(emailId);
                if (!result.found) {
                    return [{
                            type: "text",
                            title: "Email Not Found",
                            text: result.message,
                            format: "text"
                        }];
                }
                return [{
                        type: "text",
                        title: `Email Details (ID: ${emailId})`,
                        text: JSON.stringify(result, null, 2),
                        format: "json"
                    }];
            }
            catch (error) {
                logger.error(`Error retrieving email details: ${error}`);
                return [{
                        type: "text",
                        title: "Email Retrieval Error",
                        text: `Failed to retrieve email details: ${error instanceof Error ? error.message : String(error)}`,
                        format: "text"
                    }];
            }
        }
    },
    {
        name: "write_casefile_data",
        description: ("Creates or updates casefile-related data. " +
            "Supports two distinct operations:\n" +
            "- write_casefile: Create or update casefile metadata (e.g., summary, title, importance).\n" +
            "- write_page: Add or update a page under an existing casefile, including content and indexing." +
            "Only pass arguments explicitly required or allowed for the chosen operation."),
        inputSchema: {
            type: "object",
            properties: {
                operation: {
                    type: "string",
                    enum: ["write_casefile", "write_page"],
                    description: ("Specifies the writing operation: 'write_casefile' for creating new casefile or 'write_page' for page content of already existing casefile.")
                },
                casefile_url: {
                    type: "string",
                    description: ("The unique identifier of the casefile,  direct casefile url link." +
                        "Required for 'write_page'.")
                },
                casefileName: {
                    type: "string",
                    enum: ["Charter Party"],
                    description: ("Required for 'write_casefile'. Name of the casefile")
                },
                category: {
                    type: "string",
                    enum: ["charterParty"],
                    description: ("Required for 'write_casefile' . Category of the casefile")
                },
                currentStatus: {
                    type: "string",
                    description: ("<review the casefile and plan to create current status in one line, highlighting keywords>" +
                        "Required for 'write_casefile': Current status of the casefile, it will be of 4-5 words." +
                        "Required for 'write_page': update or kept it same status of the casefile based on recent received email. it willbe of 4-5 words.")
                },
                casefileSummary: {
                    type: "string",
                    description: ("Required for 'write_casefile'. Summary or high-level description of the casefile.\n" +
                        "Optional for 'write_page': can provide updated summary if needed.")
                },
                importance: {
                    type: "number",
                    minimum: 0,
                    maximum: 100,
                    description: ("It will show the importance of the casefile reference for the urgency and importance of the matter in the casefile." +
                        "Required for 'write_casefile'. Importance score of the casefile (0–100).\n" +
                        "required for 'write_page': can provide an updated score based on the new email content added to the casefile.")
                },
                imo: {
                    type: "integer",
                    description: ("Required for 'write_casefile'. IMO number of the associated vessel.")
                },
                role: {
                    type: "string",
                    enum: ["incident", "legal", "regulatory", "other"],
                    description: ("Required for 'write_casefile'. Role/category of the casefile.")
                },
                summary: {
                    type: "string",
                    description: ("Required for 'write_page'. Detailed content or summary of the new page.")
                },
                topic: {
                    type: "string",
                    description: ("Required for 'write_page'.It is of 4-8 words aboyt what this document is about.")
                },
                facts: {
                    type: "string",
                    description: ("Required for 'write_page'..It will  have the highlighted facts/information from the database.")
                },
                detailed_report: {
                    type: "string",
                    description: ("Required for 'write_page'. It will have the detailed report of the casefile in markdown format.")
                },
                links: {
                    type: "array",
                    items: {
                        type: "string"
                    },
                    description: ("Required for 'write_page'. Relevent links you want to add to the case file.")
                }
            },
            required: ["operation"],
            additionalProperties: false
        },
        handler: async (args) => {
            const result = await write_casefile_data(args);
            return [result];
        }
    },
    // {
    //   name: "update_casefile",
    //   description: "General-purpose tool to update editable fields in an existing casefile. Supports both direct field updates and individual page/index operations (add, delete, edit). Note: Some fields like imo, vesselName, vesselId, createdAt, updatedAt are protected and cannot be modified.",
    //   inputSchema: {
    //     type: "object",
    //     properties: {
    //       casefile_id: {
    //         type: "string",
    //         description: "The MongoDB ObjectId of the casefile to update. Must be a valid ObjectId format."
    //       },
    //       updates: {
    //         type: "object",
    //         description: "Object containing the field names and their new values to update. Supports: 1) Direct field updates: casefile, currentStatus, status, plan_status, importance, originalImportance, maxImportance, conversationTopic, summary, category, tag, isClone, importanceReason, ceoLevelProblemReason, role, casefilePlans, followUp, taskAssigned, index, pages, etc. 2) Individual operations: add_index (object), add_page (object), delete_index (pagenum), delete_page (pagenum), edit_index ({pagenum, ...fields}), edit_page ({pagenum, ...fields}). Protected fields (imo, vesselName, vesselId, createdAt, updatedAt, _id, link, weblinkKey) will be ignored if provided. Cannot mix direct updates with individual operations.",
    //         additionalProperties: true,
    //         minProperties: 1
    //       }
    //     },
    //     required: ["casefile_id", "updates"],
    //     additionalProperties: false
    //   },
    //   handler: async (args: {
    //     casefile_id: string;
    //     updates: Record<string, any>;
    //   }): Promise<Array<TextContent | ImageContent>> => {
    //     try {
    //       const { casefile_id, updates } = args;
    //       // Validate casefile_id
    //       if (!casefile_id || !ObjectId.isValid(casefile_id)) {
    //         throw new Error("Valid casefile_id (MongoDB ObjectId) is required");
    //       }
    //       // Validate updates object
    //       if (!updates || typeof updates !== 'object' || Object.keys(updates).length === 0) {
    //         throw new Error("Updates object with at least one field is required");
    //       }
    //       console.log(`[update_casefile] Updating casefile ${casefile_id} with fields: ${Object.keys(updates).join(', ')}`);
    //       const mongoClient = await getMongoClient(config.mongoUri);
    //       const db = mongoClient.db(config.dbName);
    //       const collection = db.collection("casefiles");
    //       // Check if casefile exists
    //       const existingCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
    //       if (!existingCasefile) {
    //         return [{
    //           type: "text" as const,
    //           text: `No casefile found with ID: ${casefile_id}`
    //         }];
    //       }
    //       // Define protected fields that cannot be updated
    //       const protectedFields = [
    //         '_id', 'imo', 'vesselName', 'vesselId', 'createdAt', 'updatedAt', 
    //         'link', 'weblinkKey', 'casefileMessageId'
    //       ];
    //       // Filter out protected fields and warn about them
    //       const filteredUpdates: Record<string, any> = {};
    //       const ignoredFields: string[] = [];
    //       for (const [key, value] of Object.entries(updates)) {
    //         if (protectedFields.includes(key)) {
    //           ignoredFields.push(key);
    //         } else {
    //           filteredUpdates[key] = value;
    //         }
    //       }
    //       if (Object.keys(filteredUpdates).length === 0) {
    //         return [{
    //           type: "text" as const,
    //           text: `No editable fields provided. Protected fields (${ignoredFields.join(', ')}) cannot be updated.`
    //         }];
    //       }
    //       // Handle special operations for index and pages
    //       const hasIndexOps = 'add_index' in filteredUpdates || 'delete_index' in filteredUpdates || 'edit_index' in filteredUpdates;
    //       const hasPageOps = 'add_page' in filteredUpdates || 'delete_page' in filteredUpdates || 'edit_page' in filteredUpdates;
    //       const hasDirectUpdate = 'index' in filteredUpdates || 'pages' in filteredUpdates;
    //       if (hasDirectUpdate && (hasIndexOps || hasPageOps)) {
    //         return [{
    //           type: "text" as const,
    //           text: "Error: Cannot mix direct 'index'/'pages' updates with individual operations (add_index, delete_index, edit_index, add_page, delete_page, edit_page)."
    //         }];
    //       }
    //       // If both index and pages are provided directly, validate they have matching lengths
    //       if ('index' in filteredUpdates && 'pages' in filteredUpdates) {
    //         const indexArray = Array.isArray(filteredUpdates.index) ? filteredUpdates.index : [];
    //         const pagesArray = Array.isArray(filteredUpdates.pages) ? filteredUpdates.pages : [];
    //         if (indexArray.length !== pagesArray.length) {
    //           return [{
    //             type: "text" as const,
    //             text: `Error: 'index' and 'pages' arrays must have the same length. Found index: ${indexArray.length}, pages: ${pagesArray.length}`
    //           }];
    //         }
    //       }
    //       // Handle individual page/index operations
    //       let mongoUpdates: any = {};
    //       const specialOps = ['add_index', 'delete_index', 'edit_index', 'add_page', 'delete_page', 'edit_page'];
    //       if (hasIndexOps || hasPageOps) {
    //         // Process individual operations using MongoDB array operators
    //         for (const [key, value] of Object.entries(filteredUpdates)) {
    //           if (specialOps.includes(key)) {
    //             switch (key) {
    //               case 'add_index':
    //                 if (value && typeof value === 'object') {
    //                   // Calculate next pagenum
    //                   const maxPageNum = Math.max(
    //                     ...(existingCasefile.index || []).map((item: any) => item.pagenum || 0),
    //                     ...(existingCasefile.pages || []).map((item: any) => item.pagenum || 0),
    //                     -1
    //                   );
    //                   const newPageNum = maxPageNum + 1;
    //                   const indexItem = { ...value, pagenum: newPageNum };
    //                   if (indexItem.referenceId && typeof indexItem.referenceId === 'string' && ObjectId.isValid(indexItem.referenceId)) {
    //                     indexItem.referenceId = new ObjectId(indexItem.referenceId);
    //                   }
    //                   mongoUpdates.$push = { ...mongoUpdates.$push, index: indexItem };
    //                 }
    //                 break;
    //               case 'add_page':
    //                 if (value && typeof value === 'object') {
    //                   // Calculate next pagenum (should match add_index)
    //                   const maxPageNum = Math.max(
    //                     ...(existingCasefile.index || []).map((item: any) => item.pagenum || 0),
    //                     ...(existingCasefile.pages || []).map((item: any) => item.pagenum || 0),
    //                     -1
    //                   );
    //                   const newPageNum = maxPageNum + 1;
    //                   const pageItem = { ...value, pagenum: newPageNum };
    //                   if (pageItem.referenceId && typeof pageItem.referenceId === 'string' && ObjectId.isValid(pageItem.referenceId)) {
    //                     pageItem.referenceId = new ObjectId(pageItem.referenceId);
    //                   }
    //                   mongoUpdates.$push = { ...mongoUpdates.$push, pages: pageItem };
    //                 }
    //                 break;
    //               case 'delete_index':
    //                 if (typeof value === 'number') {
    //                   mongoUpdates.$pull = { ...mongoUpdates.$pull, index: { pagenum: value } };
    //                 }
    //                 break;
    //               case 'delete_page':
    //                 if (typeof value === 'number') {
    //                   mongoUpdates.$pull = { ...mongoUpdates.$pull, pages: { pagenum: value } };
    //                 }
    //                 break;
    //               case 'edit_index':
    //                 if (value && typeof value === 'object' && 'pagenum' in value) {
    //                   const { pagenum, ...updateFields } = value;
    //                   if (updateFields.referenceId && typeof updateFields.referenceId === 'string' && ObjectId.isValid(updateFields.referenceId)) {
    //                     updateFields.referenceId = new ObjectId(updateFields.referenceId);
    //                   }
    //                   mongoUpdates.$set = { ...mongoUpdates.$set };
    //                   for (const [field, val] of Object.entries(updateFields)) {
    //                     mongoUpdates.$set[`index.$[elem].${field}`] = val;
    //                   }
    //                   mongoUpdates.arrayFilters = mongoUpdates.arrayFilters || [];
    //                   mongoUpdates.arrayFilters.push({ "elem.pagenum": pagenum });
    //                 }
    //                 break;
    //               case 'edit_page':
    //                 if (value && typeof value === 'object' && 'pagenum' in value) {
    //                   const { pagenum, ...updateFields } = value;
    //                   if (updateFields.referenceId && typeof updateFields.referenceId === 'string' && ObjectId.isValid(updateFields.referenceId)) {
    //                     updateFields.referenceId = new ObjectId(updateFields.referenceId);
    //                   }
    //                   mongoUpdates.$set = { ...mongoUpdates.$set };
    //                   for (const [field, val] of Object.entries(updateFields)) {
    //                     mongoUpdates.$set[`pages.$[elem].${field}`] = val;
    //                   }
    //                   mongoUpdates.arrayFilters = mongoUpdates.arrayFilters || [];
    //                   mongoUpdates.arrayFilters.push({ "elem.pagenum": pagenum });
    //                 }
    //                 break;
    //             }
    //           }
    //         }
    //         // Add updatedAt to $set
    //         mongoUpdates.$set = { ...mongoUpdates.$set, updatedAt: new Date() };
    //       } else {
    //         // Prepare standard update object with automatic updatedAt timestamp
    //         const standardUpdates = { ...filteredUpdates };
    //         // Remove special operations from standard updates
    //         for (const op of specialOps) {
    //           delete standardUpdates[op];
    //         }
    //         mongoUpdates = {
    //           $set: {
    //             ...standardUpdates,
    //             updatedAt: new Date()
    //           }
    //         };
    //       }
    //       // Handle array fields for standard updates that might contain ObjectIds
    //       if (mongoUpdates.$set) {
    //         if (mongoUpdates.$set.index && Array.isArray(mongoUpdates.$set.index)) {
    //           mongoUpdates.$set.index = mongoUpdates.$set.index.map((item: any) => {
    //             if (item.referenceId && typeof item.referenceId === 'string' && ObjectId.isValid(item.referenceId)) {
    //               return { ...item, referenceId: new ObjectId(item.referenceId) };
    //             }
    //             return item;
    //           });
    //         }
    //         if (mongoUpdates.$set.pages && Array.isArray(mongoUpdates.$set.pages)) {
    //           mongoUpdates.$set.pages = mongoUpdates.$set.pages.map((page: any) => {
    //             if (page.referenceId && typeof page.referenceId === 'string' && ObjectId.isValid(page.referenceId)) {
    //               return { ...page, referenceId: new ObjectId(page.referenceId) };
    //             }
    //             return page;
    //           });
    //         }
    //       }
    //       // Execute the update with appropriate options
    //       const updateOptions: any = { upsert: false };
    //       if (mongoUpdates.arrayFilters && mongoUpdates.arrayFilters.length > 0) {
    //         updateOptions.arrayFilters = mongoUpdates.arrayFilters;
    //       }
    //       const result = await collection.updateOne(
    //         { _id: new ObjectId(casefile_id) },
    //         mongoUpdates,
    //         updateOptions
    //       );
    //       if (result.matchedCount === 0) {
    //         return [{
    //           type: "text" as const,
    //           text: `No casefile found with ID: ${casefile_id}`
    //         }];
    //       }
    //       if (result.modifiedCount === 0) {
    //         return [{
    //           type: "text" as const,
    //           text: `Casefile ${casefile_id} found but no changes were made (values may be identical)`
    //         }];
    //       }
    //       // Fetch the updated document
    //       const updatedCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
    //       if (!updatedCasefile) {
    //         throw new Error(`Failed to retrieve updated casefile ${casefile_id}`);
    //       }
    //       // Generate new link with updated casefile data
    //       let newLink = updatedCasefile.link;
    //       try {
    //         newLink = await generateCasefileWeblink(casefile_id);
    //         // Update the link in the database
    //         await collection.updateOne(
    //           { _id: new ObjectId(casefile_id) },
    //           { $set: { link: newLink } }
    //         );
    //         console.log(`[update_casefile] Generated new link for casefile ${casefile_id}: ${newLink}`);
    //       } catch (error) {
    //         console.error(`[update_casefile] Failed to generate new link for casefile ${casefile_id}: ${error}`);
    //         // Continue with existing link - don't fail the entire operation
    //       }
    //       // Fetch the final updated document with new link for sync
    //       const finalCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
    //       if (finalCasefile) {
    //         try {
    //           await pushToTypesense(finalCasefile, 'upsert');
    //           console.log(`[update_casefile] Successfully synced casefile ${casefile_id}`);
    //         } catch (error) {
    //           console.error(`[update_casefile] Sync error for casefile ${casefile_id}: ${error}`);
    //         }
    //       }
    //       const response = {
    //         casefile_id: casefile_id,
    //         updated_fields: Object.keys(filteredUpdates),
    //         modified_count: result.modifiedCount,
    //         removed_protected_fields: ignoredFields.length > 0 ? ignoredFields : undefined,
    //         auto_injected: ["updatedAt", "link"],
    //         updated_at: new Date().toISOString(),
    //         new_link: newLink
    //       };
    //       return [{
    //         type: "text" as const,
    //         title: `Casefile Updated: ${casefile_id}`,
    //         text: JSON.stringify(response, null, 2),
    //         format: "json"
    //       }];
    //     } catch (error) {
    //       console.error(`[update_casefile] Error: ${error}`);
    //       return [{
    //         type: "text" as const,
    //         title: "Casefile Update Error",
    //         text: `Failed to update casefile: ${error instanceof Error ? error.message : String(error)}`,
    //         format: "text"
    //       }];
    //     }
    //   }
    // },
    {
        name: "update_casefile",
        description: `Advanced casefile update tool using raw MongoDB update queries. Provides maximum flexibility with built-in validation and safety checks. Supports all MongoDB update operators while protecting critical fields and maintaining data consistency.

## CASEFILE SCHEMA STRUCTURE:
### Core Fields (Editable):
- casefile: string - Title/name of the casefile (e.g., "🛢️ LUBE OIL ANALYSIS MONITORING - OCEAN BREEZER")
- currentStatus: string - Current status description (e.g., "Critical Issues Identified - **Steering Gear Water Contamination**")
- status: string - Status code (e.g., "actionRequired", "completed", "pending")
- plan_status: string - Plan status (e.g., "plan_created", "unprocessed", "processed")
- importance: number - Importance score 0-100 (e.g., 85)
- originalImportance: number - Initial importance score
- maxImportance: number - Maximum importance reached
- conversationTopic: string[] - Array of conversation topics
- summary: string - Detailed markdown summary with executive information
- category: string - Category type (e.g., "loReport", "sire", "General")
- tag: string[] - Array of tags for categorization
- isClone: boolean - Whether this is a cloned casefile
- importanceReason: string - Explanation of importance level
- ceoLevelProblemReason: string - CEO escalation reasoning
- role: string[] - Array of roles/responsibilities
- followUp: string - Next follow-up action description

### Array Fields (Complex Structures):
#### index: Array of index entries
Structure: {
  type: string (e.g., "email", "SIYA"),
  referenceId: ObjectId - Reference to related document,
  topic: string - Topic/title of the entry,
  pagenum: number - Page number (auto-calculated if not provided),
  dateTime: Date - Timestamp,
  plan_status: string - Processing status
}

#### pages: Array of page entries (must match index array length)
Structure: {
  referenceId: ObjectId - Must match corresponding index entry,
  summary: string - Detailed markdown content,
  dateTime: Date - Timestamp,
  toRecipientsEmailAddresses: string - Email recipients,
  senderEmailAddress: string - Sender email,
  senderName: string - Sender name,
  subject: string - Email/document subject,
  flag: string - A markdown text indicating if it is a serious issue , normal , informaiton ,mail etc. Add visual symbols to the markdonw text
  type: string - Type (email, SIYA, etc.),
  link: string - URL to document/email,
  attachments: string[] - Array of attachment URLs,
  tags: string[] - Array of tags,
  plan_status: string - Processing status,
  pagenum: number - Page number (must match index)
  content: It is markdown format text which is 2 lint gist of the summary field text
}

#### casefilePlans: Array of plan objects
Structure: {
  dateTime: Date - Plan creation date,
  flag: string - Plan title/flag,
  plan: string - Detailed markdown plan content,
  color: string - UI color code,
  content: string - Brief plan description
}

#### taskAssigned: Array of task objects
Structure: {
  id: string - Task identifier,
  task: string - Task description,
  status: string - Task status (pending, completed, in_progress),
  dependency: string[] - Array of dependent task IDs,
  taskDate: Date - Task due date,
  isUpdated: boolean - Whether task was updated,
  task_update_tracker: string - Update tracking info,
  assignee: string - Assigned person code
}

### Protected Fields (Auto-managed, cannot be updated):
- _id, imo, vesselName, vesselId, createdAt, updatedAt, link, weblinkKey, casefileMessageId

## MONGODB UPDATE EXAMPLES:

### Update basic fields:
{"$set": {"currentStatus": "Investigation Complete", "importance": 90, "status": "completed"}}

### Add new page and index (must be done together):
{"$push": {"index": {"type": "email", "referenceId": "686590168052c04f5b057069", "topic": "New findings"}, "pages": {"referenceId": "686590168052c04f5b057069", "summary": "Detailed analysis", "type": "email"}}}

### Update specific array elements by pagenum:
{"$set": {"pages.$[elem].plan_status": "completed"}, array_filters: [{"elem.pagenum": 2}]}

### Remove entries by pagenum:
{"$pull": {"index": {"pagenum": 3}, "pages": {"pagenum": 3}}}

### Add to arrays:
{"$addToSet": {"conversationTopic": "New topic", "tag": "urgent"}}

## VALIDATION RULES:
- **Flag, Content, Summary Dependency**: When updating any of the fields 'flag', 'content', or 'summary' (in pages or casefilePlans arrays), all three fields must be updated together in the same operation.`,
        inputSchema: {
            type: "object",
            properties: {
                casefile_id: {
                    type: "string",
                    description: "The MongoDB ObjectId of the casefile to update. Must be a valid ObjectId format."
                },
                mongodb_update: {
                    type: "object",
                    description: "MongoDB update query object supporting all MongoDB update operators ($set, $push, $pull, $addToSet, $unset, $inc, etc.). Example: {\"$set\": {\"currentStatus\": \"Updated\", \"importance\": 75}, \"$push\": {\"pages\": {\"summary\": \"New content\", \"type\": \"email\"}}, \"$pull\": {\"index\": {\"pagenum\": 5}}}. Protected fields (imo, vesselName, vesselId, createdAt, _id, link, weblinkKey, casefileMessageId) will be automatically filtered out. The system will auto-inject updatedAt timestamp and handle ObjectId conversions.",
                    additionalProperties: true,
                    minProperties: 1
                },
                array_filters: {
                    type: "array",
                    description: "MongoDB array filters for positional updates. Example: [{\"elem.pagenum\": 3}] to update specific array elements. Used with operators like $set on nested array fields (e.g., \"pages.$[elem].summary\").",
                    items: { type: "object" }
                }
            },
            required: ["casefile_id", "mongodb_update"],
            additionalProperties: false
        },
        handler: async (args) => {
            try {
                const { casefile_id, mongodb_update, array_filters } = args;
                // Validate casefile_id
                if (!casefile_id || !ObjectId.isValid(casefile_id)) {
                    throw new Error("Valid casefile_id (MongoDB ObjectId) is required");
                }
                // Validate mongodb_update
                if (!mongodb_update || typeof mongodb_update !== 'object' || Object.keys(mongodb_update).length === 0) {
                    throw new Error("mongodb_update object with at least one operation is required");
                }
                console.log(`[update_casefile] Updating casefile ${casefile_id} with MongoDB query:`, JSON.stringify(mongodb_update));
                const mongoClient = await getMongoClient(config.mongoUri);
                const db = mongoClient.db(config.dbName);
                const collection = db.collection("casefiles");
                // Check if casefile exists
                const existingCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
                if (!existingCasefile) {
                    return [{
                            type: "text",
                            text: `No casefile found with ID: ${casefile_id}`
                        }];
                }
                // Define protected fields that cannot be updated
                const protectedFields = [
                    '_id', 'imo', 'vesselName', 'vesselId', 'createdAt', 'updatedAt',
                    'link', 'weblinkKey', 'casefileMessageId'
                ];
                // Helper function to extract all field paths from a query object
                const extractFieldPaths = (obj, prefix = '') => {
                    const paths = [];
                    for (const [key, value] of Object.entries(obj)) {
                        const fullKey = prefix ? `${prefix}.${key}` : key;
                        if (key.startsWith('$')) {
                            // Handle MongoDB operators
                            if (key === '$set' || key === '$unset' || key === '$inc' || key === '$mul') {
                                if (typeof value === 'object' && value !== null) {
                                    for (const field of Object.keys(value)) {
                                        paths.push(field);
                                    }
                                }
                            }
                            else if (key === '$push' || key === '$addToSet' || key === '$pull') {
                                if (typeof value === 'object' && value !== null) {
                                    for (const field of Object.keys(value)) {
                                        paths.push(field);
                                    }
                                }
                            }
                        }
                        else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                            paths.push(...extractFieldPaths(value, fullKey));
                        }
                        else {
                            paths.push(fullKey);
                        }
                    }
                    return paths;
                };
                // Helper function to filter protected fields from query
                const filterProtectedFields = (obj) => {
                    const filtered = JSON.parse(JSON.stringify(obj)); // Deep clone
                    const removed = [];
                    const filterRecursive = (current, path = '') => {
                        for (const [key, value] of Object.entries(current)) {
                            const fullPath = path ? `${path}.${key}` : key;
                            if (key.startsWith('$')) {
                                // Handle MongoDB operators
                                if (typeof value === 'object' && value !== null) {
                                    const subResult = filterRecursive(value, fullPath);
                                    removed.push(...subResult);
                                }
                            }
                            else {
                                // Check if this field or any parent field is protected
                                const isProtected = protectedFields.some(pf => key === pf || key.startsWith(pf + '.') ||
                                    fullPath.includes('.' + pf) || fullPath.startsWith(pf + '.'));
                                if (isProtected) {
                                    removed.push(key);
                                    delete current[key];
                                }
                                else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                                    const subResult = filterRecursive(value, fullPath);
                                    removed.push(...subResult);
                                }
                            }
                        }
                        return removed;
                    };
                    filterRecursive(filtered);
                    return { filtered, removed };
                };
                // Helper function to handle ObjectId conversions in query
                const convertObjectIds = (obj) => {
                    if (Array.isArray(obj)) {
                        return obj.map(item => convertObjectIds(item));
                    }
                    else if (obj !== null && typeof obj === 'object') {
                        const converted = {};
                        for (const [key, value] of Object.entries(obj)) {
                            if (key === 'referenceId' && typeof value === 'string' && ObjectId.isValid(value)) {
                                converted[key] = new ObjectId(value);
                            }
                            else {
                                converted[key] = convertObjectIds(value);
                            }
                        }
                        return converted;
                    }
                    return obj;
                };
                // Helper function to calculate next pagenum for push operations
                const calculateNextPageNum = (existingCasefile) => {
                    const maxPageNum = Math.max(...(existingCasefile.index || []).map((item) => item.pagenum || 0), ...(existingCasefile.pages || []).map((item) => item.pagenum || 0), -1);
                    return maxPageNum + 1;
                };
                // Helper function to enhance push operations with pagenum
                const enhancePushOperations = (query, nextPageNum) => {
                    if (query.$push) {
                        for (const [field, value] of Object.entries(query.$push)) {
                            if ((field === 'pages' || field === 'index') && typeof value === 'object' && value !== null) {
                                if (!('pagenum' in value)) {
                                    value.pagenum = nextPageNum;
                                }
                            }
                        }
                    }
                    return query;
                };
                // Filter protected fields
                const { filtered: filteredQuery, removed: removedFields } = filterProtectedFields(mongodb_update);
                if (Object.keys(filteredQuery).length === 0) {
                    return [{
                            type: "text",
                            text: `No valid operations provided. All operations targeted protected fields: ${removedFields.join(', ')}`
                        }];
                }
                // Convert ObjectIds
                const queryWithObjectIds = convertObjectIds(filteredQuery);
                // Calculate next pagenum if needed
                const nextPageNum = calculateNextPageNum(existingCasefile);
                const enhancedQuery = enhancePushOperations(queryWithObjectIds, nextPageNum);
                // Auto-inject updatedAt timestamp
                if (!enhancedQuery.$set) {
                    enhancedQuery.$set = {};
                }
                enhancedQuery.$set.updatedAt = new Date();
                // Enhanced validation for flag, content, and summary dependency
                const validateFlagContentSummaryDependency = (query) => {
                    const checkFields = (obj, path = '') => {
                        let hasFlag = false, hasContent = false, hasSummary = false;
                        for (const [key, value] of Object.entries(obj)) {
                            const currentPath = path ? `${path}.${key}` : key;
                            if (currentPath.includes('flag') || key === 'flag')
                                hasFlag = true;
                            if (currentPath.includes('content') || key === 'content')
                                hasContent = true;
                            if (currentPath.includes('summary') || key === 'summary')
                                hasSummary = true;
                            if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                                const nested = checkFields(value, currentPath);
                                hasFlag = hasFlag || nested.flag;
                                hasContent = hasContent || nested.content;
                                hasSummary = hasSummary || nested.summary;
                            }
                        }
                        return { flag: hasFlag, content: hasContent, summary: hasSummary };
                    };
                    const fields = checkFields(query);
                    const fieldsPresent = [fields.flag, fields.content, fields.summary];
                    const presentCount = fieldsPresent.filter(Boolean).length;
                    // If any of these fields are being updated, all three must be updated together
                    if (presentCount > 0 && presentCount < 3) {
                        const missing = [];
                        if (!fields.flag)
                            missing.push('flag');
                        if (!fields.content)
                            missing.push('content');
                        if (!fields.summary)
                            missing.push('summary');
                        return `Error: When updating flag, content, or summary fields, all three must be updated together. Missing: ${missing.join(', ')}`;
                    }
                    return null;
                };
                // Check flag, content, summary dependency
                const dependencyError = validateFlagContentSummaryDependency(enhancedQuery);
                if (dependencyError) {
                    return [{
                            type: "text",
                            text: dependencyError
                        }];
                }
                // Enhanced validation for index/pages consistency
                const hasIndexPush = enhancedQuery.$push && 'index' in enhancedQuery.$push;
                const hasPagesPush = enhancedQuery.$push && 'pages' in enhancedQuery.$push;
                const hasIndexPull = enhancedQuery.$pull && 'index' in enhancedQuery.$pull;
                const hasPagesPull = enhancedQuery.$pull && 'pages' in enhancedQuery.$pull;
                // Validate push operations
                if (hasIndexPush && !hasPagesPush) {
                    return [{
                            type: "text",
                            text: "Error: Cannot push to 'index' without corresponding 'pages' push operation for data consistency."
                        }];
                }
                if (hasPagesPush && !hasIndexPush) {
                    return [{
                            type: "text",
                            text: "Error: Cannot push to 'pages' without corresponding 'index' push operation for data consistency."
                        }];
                }
                // Validate referenceId matching for push operations
                if (hasIndexPush && hasPagesPush) {
                    const indexRef = enhancedQuery.$push.index?.referenceId;
                    const pageRef = enhancedQuery.$push.pages?.referenceId;
                    if (indexRef && pageRef) {
                        const indexRefStr = indexRef.toString();
                        const pageRefStr = pageRef.toString();
                        if (indexRefStr !== pageRefStr) {
                            return [{
                                    type: "text",
                                    text: `Error: Index and pages referenceId must match. Found index: ${indexRefStr}, pages: ${pageRefStr}`
                                }];
                        }
                    }
                }
                // Validate pull operations
                if (hasIndexPull && !hasPagesPull) {
                    return [{
                            type: "text",
                            text: "Error: Cannot pull from 'index' without corresponding 'pages' pull operation for data consistency."
                        }];
                }
                if (hasPagesPull && !hasIndexPull) {
                    return [{
                            type: "text",
                            text: "Error: Cannot pull from 'pages' without corresponding 'index' pull operation for data consistency."
                        }];
                }
                // Validate pull criteria consistency
                if (hasIndexPull && hasPagesPull) {
                    const indexPull = enhancedQuery.$pull.index;
                    const pagesPull = enhancedQuery.$pull.pages;
                    // If pulling by pagenum, ensure both have same pagenum
                    if (indexPull?.pagenum !== undefined && pagesPull?.pagenum !== undefined) {
                        if (indexPull.pagenum !== pagesPull.pagenum) {
                            return [{
                                    type: "text",
                                    text: `Error: Index and pages pull operations must target same pagenum. Found index: ${indexPull.pagenum}, pages: ${pagesPull.pagenum}`
                                }];
                        }
                    }
                    // If pulling by referenceId, ensure both have same referenceId
                    if (indexPull?.referenceId && pagesPull?.referenceId) {
                        const indexRefStr = indexPull.referenceId.toString();
                        const pageRefStr = pagesPull.referenceId.toString();
                        if (indexRefStr !== pageRefStr) {
                            return [{
                                    type: "text",
                                    text: `Error: Index and pages pull operations must target same referenceId. Found index: ${indexRefStr}, pages: ${pageRefStr}`
                                }];
                        }
                    }
                }
                // Execute the update with appropriate options
                const updateOptions = { upsert: false };
                if (array_filters && array_filters.length > 0) {
                    updateOptions.arrayFilters = array_filters;
                }
                const result = await collection.updateOne({ _id: new ObjectId(casefile_id) }, enhancedQuery, updateOptions);
                if (result.matchedCount === 0) {
                    return [{
                            type: "text",
                            text: `No casefile found with ID: ${casefile_id}`
                        }];
                }
                if (result.modifiedCount === 0) {
                    return [{
                            type: "text",
                            text: `Casefile ${casefile_id} found but no changes were made (values may be identical)`
                        }];
                }
                // Fetch the updated document
                const updatedCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
                if (!updatedCasefile) {
                    throw new Error(`Failed to retrieve updated casefile ${casefile_id}`);
                }
                // Generate new link with updated casefile data
                let newLink = updatedCasefile.link;
                try {
                    newLink = await generateCasefileWeblink(casefile_id);
                    // Update the link in the database
                    await collection.updateOne({ _id: new ObjectId(casefile_id) }, { $set: { link: newLink } });
                    console.log(`[update_casefile] Generated new link for casefile ${casefile_id}: ${newLink}`);
                }
                catch (error) {
                    console.error(`[update_casefile] Failed to generate new link for casefile ${casefile_id}: ${error}`);
                    // Continue with existing link - don't fail the entire operation
                }
                // Fetch the final updated document with new link for sync
                const finalCasefile = await collection.findOne({ _id: new ObjectId(casefile_id) });
                if (finalCasefile) {
                    try {
                        await pushToTypesense(finalCasefile, 'upsert');
                        console.log(`[update_casefile] Successfully synced casefile ${casefile_id}`);
                    }
                    catch (error) {
                        console.error(`[update_casefile] Sync error for casefile ${casefile_id}: ${error}`);
                    }
                }
                const response = {
                    casefile_id: casefile_id,
                    operations_executed: Object.keys(enhancedQuery),
                    modified_count: result.modifiedCount,
                    removed_protected_fields: removedFields.length > 0 ? removedFields : undefined,
                    auto_injected: ["updatedAt", "link"],
                    updated_at: enhancedQuery.$set.updatedAt.toISOString(),
                    new_link: newLink
                };
                return [{
                        type: "text",
                        title: `Casefile Updated via MongoDB Query: ${casefile_id}`,
                        text: JSON.stringify(response, null, 2),
                        format: "json"
                    }];
            }
            catch (error) {
                console.error(`[update_casefile] Error: ${error}`);
                return [{
                        type: "text",
                        title: "MongoDB Casefile Update Error",
                        text: `Failed to update casefile: ${error instanceof Error ? error.message : String(error)}`,
                        format: "text"
                    }];
            }
        }
    },
];
// Helper function to convert casefile dates
function convertCasefileDates(document) {
    const dateFields = [
        'casefileInitiationDate',
        'lastCasefileUpdateDate'
    ];
    for (const field of dateFields) {
        if (document[field]) {
            try {
                document[field] = new Date(document[field] * 1000).toISOString().slice(0, 19).replace('T', ' ');
            }
            catch (error) {
                console.warn(`Failed to convert ${field}: ${error}`);
            }
        }
    }
    return document;
}
// Helper function to generate alpha-numeric ID
function generateAlphaNumericId(alphaSize = 3, numericSize = 3) {
    const letters = 'abcdefghijklmnopqrstuvwxyz';
    const digits = '0123456789';
    let alphaPart = '';
    let numericPart = '';
    for (let i = 0; i < alphaSize; i++) {
        alphaPart += letters.charAt(Math.floor(Math.random() * letters.length));
    }
    for (let i = 0; i < numericSize; i++) {
        numericPart += digits.charAt(Math.floor(Math.random() * digits.length));
    }
    return alphaPart + numericPart;
}
// Helper function to generate artifacts
function getListOfArtifacts(functionName, results) {
    const artifacts = [];
    for (let i = 0; i < results.length; i++) {
        const result = results[i];
        if (!result.url)
            continue;
        const artifactData = {
            id: `msg_browser_${generateAlphaNumericId()}`,
            parentTaskId: `task_casefile_${generateAlphaNumericId()}`,
            timestamp: Date.now(),
            agent: {
                id: "agent_siya_browser",
                name: "SIYA",
                type: "qna"
            },
            messageType: "action",
            action: {
                tool: "browser",
                operation: "browsing",
                params: {
                    url: result.title,
                    pageTitle: `Tool response for ${functionName}`,
                    visual: {
                        icon: "browser",
                        color: "#2D8CFF"
                    },
                    stream: {
                        type: "vnc",
                        streamId: "stream_browser_1",
                        target: "browser"
                    }
                }
            },
            content: `Viewed page: ${functionName}`,
            artifacts: [{
                    id: `artifact_webpage_${generateAlphaNumericId()}`,
                    type: "browser_view",
                    content: {
                        url: result.url,
                        title: functionName,
                        screenshot: "",
                        textContent: `Observed output of cmd \`${functionName}\` executed:`,
                        extractedInfo: {}
                    },
                    metadata: {
                        domainName: "example.com",
                        visitTimestamp: Date.now(),
                        category: "web_page"
                    }
                }],
            status: "completed"
        };
        artifacts.push({
            type: "text",
            text: JSON.stringify(artifactData, null, 2),
            title: `Casefile: ${result.title}`,
            format: "json"
        });
    }
    return artifacts;
}
// Helper to generate casefile weblink (calls diary API)
async function generateCasefileWeblink(casefileId) {
    const endpoints = [
        `${config.API_BASE_URL}/v1.0/diary/casefile-html/${casefileId}`,
        `${config.API_BASE_URL}/v1.0/diary/casefilehtml/${casefileId}`
    ];
    const headers = { Authorization: `Bearer ${config.API_TOKEN}` };
    for (const url of endpoints) {
        try {
            const resp = await axios.get(url, { headers });
            if (resp.status === 200) {
                const body = resp.data;
                const data = body.resultData || {};
                if (body.status === "OK" && data.url) {
                    return data.url;
                }
            }
        }
        catch (e) {
            // Continue to next endpoint if error
            continue;
        }
    }
    throw new Error(`Could not generate weblink for casefile ${casefileId}`);
}
//# sourceMappingURL=index.js.map