"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Neo4jGraph = exports.BASE_ENTITY_LABEL = void 0;
const neo4j_driver_1 = __importDefault(require("neo4j-driver"));
const hash_1 = require("@langchain/core/utils/hash");
exports.BASE_ENTITY_LABEL = "__Entity__";
const DISTINCT_VALUE_LIMIT = 10;
const LIST_LIMIT = 128;
const EXHAUSTIVE_SEARCH_LIMIT = 10000;
const EXCLUDED_LABELS = ["Bloom_Perspective", "Bloom_Scene"];
const EXCLUDED_RELS = ["Bloom_HAS_SCENE"];
const INCLUDE_DOCS_QUERY = `
MERGE (d:Document {id:$document.metadata.id})
SET d.text = $document.pageContent
SET d += $document.metadata
WITH d
`;
const NODE_PROPERTIES_QUERY = `
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE NOT type = "RELATIONSHIP" AND elementType = "node"
  AND NOT label IN $EXCLUDED_LABELS
WITH label AS nodeLabels, collect({property:property, type:type}) AS properties
RETURN {labels: nodeLabels, properties: properties} AS output
`;
const REL_PROPERTIES_QUERY = `
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE NOT type = "RELATIONSHIP" AND elementType = "relationship"
      AND NOT label in $EXCLUDED_LABELS
WITH label AS nodeLabels, collect({property:property, type:type}) AS properties
RETURN {type: nodeLabels, properties: properties} AS output
`;
const REL_QUERY = `
CALL apoc.meta.data()
YIELD label, other, elementType, type, property
WHERE type = "RELATIONSHIP" AND elementType = "node"
UNWIND other AS other_node
WITH * WHERE NOT label IN $EXCLUDED_LABELS
    AND NOT other_node IN $EXCLUDED_LABELS
RETURN {start: label, type: property, end: toString(other_node)} AS output
`;
function isDistinctMoreThanLimit(distinct_count = 11, limit = DISTINCT_VALUE_LIMIT) {
    return distinct_count !== undefined && distinct_count > limit;
}
function cleanStringValues(text) {
    return text.replace("\n", " ").replace("\r", " ");
}
function formatSchema(schema, isEnhanced) {
    let formattedNodeProps = [];
    let formattedRelProps = [];
    if (isEnhanced) {
        // Enhanced formatting for nodes
        for (const [nodeType, properties] of Object.entries(schema.nodeProps)) {
            formattedNodeProps.push(`- **${nodeType}**`);
            for (const prop of properties) {
                let example = "";
                if (prop.type === "STRING") {
                    if (prop.values.length > 0) {
                        if (isDistinctMoreThanLimit(prop.distinct_count)) {
                            example = `Example: ${cleanStringValues(prop.values[0])}`;
                        }
                        else {
                            example = `Available options: ${prop.values
                                .map(cleanStringValues)
                                .join(", ")}`;
                        }
                    }
                }
                else if (["INTEGER", "FLOAT", "DATE", "DATE_TIME", "LOCAL_DATE_TIME"].includes(prop.type)) {
                    if (prop.min !== undefined) {
                        example = `Min: ${prop.min}, Max: ${prop.max}`;
                    }
                    else {
                        if (prop.values.length > 0) {
                            example = `Example: ${prop.values[0]}`;
                        }
                    }
                }
                else if (prop.type === "LIST") {
                    if (!prop.min_size || prop.min_size > LIST_LIMIT) {
                        continue;
                    }
                    example = `Min Size: ${prop.min_size}, Max Size: ${prop.max_size}`;
                }
                formattedNodeProps.push(`  - \`${prop.property}\`: ${prop.type} ${example}`);
            }
        }
        // Enhanced formatting for relationships
        for (const [relType, properties] of Object.entries(schema.relProps)) {
            formattedRelProps.push(`- **${relType}**`);
            for (const prop of properties) {
                let example = "";
                if (prop.type === "STRING") {
                    if (prop.values.length > 0) {
                        if (isDistinctMoreThanLimit(prop.distinct_count)) {
                            example = `Example: ${cleanStringValues(prop.values[0])}`;
                        }
                        else {
                            example = `Available options: ${prop.values
                                .map(cleanStringValues)
                                .join(", ")}`;
                        }
                    }
                }
                else if (["INTEGER", "FLOAT", "DATE", "DATE_TIME", "LOCAL_DATE_TIME"].includes(prop.type)) {
                    if (prop.min) {
                        example = `Min: ${prop.min}, Max: ${prop.max}`;
                    }
                    else {
                        if (prop.values) {
                            example = `Example: ${prop.values[0]}`;
                        }
                    }
                }
                else if (prop.type === "LIST") {
                    if (prop.min_size > LIST_LIMIT) {
                        continue;
                    }
                    example = `Min Size: ${prop.min_size}, Max Size: ${prop.max_size}`;
                }
                formattedRelProps.push(`  - \`${prop.property}\`: ${prop.type} ${example}`);
            }
        }
    }
    else {
        // Format node properties
        formattedNodeProps = Object.entries(schema.nodeProps).map(([key, value]) => {
            const propsStr = value
                .map((prop) => `${prop.property}: ${prop.type}`)
                .join(", ");
            return `${key} {${propsStr}}`;
        });
        // Format relationship properties
        formattedRelProps = Object.entries(schema.relProps).map(([key, value]) => {
            const propsStr = value
                .map((prop) => `${prop.property}: ${prop.type} `)
                .join(", ");
            return `${key} {${propsStr} } `;
        });
    }
    // Format relationships
    const formattedRels = schema.relationships.map((el) => `(: ${el.start}) - [: ${el.type}] -> (:${el.end})`);
    return [
        "Node properties are the following:",
        formattedNodeProps?.join(", "),
        "Relationship properties are the following:",
        formattedRelProps?.join(", "),
        "The relationships are the following:",
        formattedRels?.join(", "),
    ].join("\n");
}
/**
 * @security *Security note*: Make sure that the database connection uses credentials
 * that are narrowly-scoped to only include necessary permissions.
 * Failure to do so may result in data corruption or loss, since the calling
 * code may attempt commands that would result in deletion, mutation
 * of data if appropriately prompted or reading sensitive data if such
 * data is present in the database.
 * The best way to guard against such negative outcomes is to (as appropriate)
 * limit the permissions granted to the credentials used with this tool.
 * For example, creating read only users for the database is a good way to
 * ensure that the calling code cannot mutate or delete data.
 *
 * @link See https://js.langchain.com/docs/security for more information.
 */
class Neo4jGraph {
    constructor({ url, username, password, database = "neo4j", timeoutMs, enhancedSchema = false, }) {
        Object.defineProperty(this, "driver", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "database", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "timeoutMs", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "enhancedSchema", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "schema", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: ""
        });
        Object.defineProperty(this, "structuredSchema", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: {
                nodeProps: {},
                relProps: {},
                relationships: [],
                metadata: {
                    constraint: {},
                    index: {},
                },
            }
        });
        try {
            this.driver = neo4j_driver_1.default.driver(url, neo4j_driver_1.default.auth.basic(username, password));
            this.database = database;
            this.timeoutMs = timeoutMs;
            this.enhancedSchema = enhancedSchema;
        }
        catch (error) {
            throw new Error("Could not create a Neo4j driver instance. Please check the connection details.");
        }
    }
    static async initialize(config) {
        const graph = new Neo4jGraph(config);
        await graph.verifyConnectivity();
        try {
            await graph.refreshSchema();
        }
        catch (error) {
            if (error.code === "Neo.ClientError.Procedure.ProcedureNotFound") {
                throw new Error("Could not use APOC procedures. Please ensure the APOC plugin is installed in Neo4j and that 'apoc.meta.data()' is allowed in Neo4j configuration.");
            }
            throw error;
        }
        finally {
            console.log("Schema refreshed successfully.");
        }
        return graph;
    }
    getSchema() {
        return this.schema;
    }
    getStructuredSchema() {
        return this.structuredSchema;
    }
    async query(query, params = {}, routing = neo4j_driver_1.default.routing.WRITE) {
        const result = await this.driver.executeQuery(query, params, {
            database: this.database,
            routing,
            transactionConfig: { timeout: this.timeoutMs },
        });
        return toObjects(result.records);
    }
    async verifyConnectivity() {
        await this.driver.getServerInfo();
    }
    async refreshSchema() {
        // Assuming query method is defined and returns a Promise
        const nodeProperties = (await this.query(NODE_PROPERTIES_QUERY, {
            EXCLUDED_LABELS: EXCLUDED_LABELS.concat([exports.BASE_ENTITY_LABEL]),
        }))?.map((el) => el.output);
        const relationshipsProperties = (await this.query(REL_PROPERTIES_QUERY, {
            EXCLUDED_LABELS: EXCLUDED_RELS,
        }))?.map((el) => el.output);
        const relationships = (await this.query(REL_QUERY, {
            EXCLUDED_LABELS: EXCLUDED_LABELS.concat([exports.BASE_ENTITY_LABEL]),
        }))?.map((el) => el.output);
        const constraint = await this.query("SHOW CONSTRAINTS");
        const index = await this.query("SHOW INDEXES YIELD *");
        // Structured schema similar to Python's dictionary comprehension
        this.structuredSchema = {
            nodeProps: Object.fromEntries(nodeProperties?.map((el) => [el.labels, el.properties]) || []),
            relProps: Object.fromEntries(relationshipsProperties?.map((el) => [el.type, el.properties]) || []),
            relationships: relationships || [],
            metadata: {
                constraint,
                index,
            },
        };
        if (this.enhancedSchema) {
            const schemaCounts = await this.query(`CALL apoc.meta.graphSample() YIELD nodes, relationships ` +
                `RETURN nodes, [rel in relationships | {name: apoc.any.property(rel, 'type'), count: apoc.any.property(rel, 'count')}] AS relationships`);
            // Update node info
            for (const node of schemaCounts[0].nodes) {
                // Skip bloom labels
                if (EXCLUDED_LABELS.includes(node.name)) {
                    continue;
                }
                const nodeProps = this.structuredSchema.nodeProps[node.name];
                if (!nodeProps) {
                    // The node has no properties
                    continue;
                }
                const enhancedCypher = await this.enhancedSchemaCypher(node.name, nodeProps, node.count < EXHAUSTIVE_SEARCH_LIMIT);
                const enhancedInfoPromise = await this.query(enhancedCypher);
                const enhancedInfo = enhancedInfoPromise[0].output;
                for (const prop of nodeProps) {
                    if (enhancedInfo[prop.property]) {
                        Object.assign(prop, enhancedInfo[prop.property]);
                    }
                }
            }
            // Update rel info
            for (const rel of schemaCounts[0].relationships) {
                // Skip bloom labels
                if (EXCLUDED_RELS.includes(rel.name)) {
                    continue;
                }
                const relProps = this.structuredSchema.relProps[rel.name];
                if (!relProps) {
                    // The rel has no properties
                    continue;
                }
                const enhancedCypher = await this.enhancedSchemaCypher(rel.name, relProps, rel.count < EXHAUSTIVE_SEARCH_LIMIT, true);
                const enhancedInfoPromise = await this.query(enhancedCypher);
                const enhancedInfo = enhancedInfoPromise[0].output;
                for (const prop of relProps) {
                    if (prop.property in enhancedInfo) {
                        Object.assign(prop, enhancedInfo[prop.property]);
                    }
                }
            }
        }
        // Combine all formatted elements into a single string
        this.schema = formatSchema(this.structuredSchema, this.enhancedSchema);
    }
    async enhancedSchemaCypher(labelOrType, properties, exhaustive, isRelationship = false) {
        let matchClause = isRelationship
            ? `MATCH ()-[n:\`${labelOrType}\`]->()`
            : `MATCH (n:\`${labelOrType}\`)`;
        const withClauses = [];
        const returnClauses = [];
        const outputDict = {};
        if (exhaustive) {
            for (const prop of properties) {
                const propName = prop.property;
                const propType = prop.type;
                if (propType === "STRING") {
                    withClauses.push(`collect(distinct substring(n.\`${propName}\`, 0, 50)) AS \`${propName}_values\``);
                    returnClauses.push(`values: \`${propName}_values\`[..${DISTINCT_VALUE_LIMIT}], distinct_count: size(\`${propName}_values\`)`);
                }
                else if (["INTEGER", "FLOAT", "DATE", "DATE_TIME", "LOCAL_DATE_TIME"].includes(propType)) {
                    withClauses.push(`min(n.\`${propName}\`) AS \`${propName}_min\``);
                    withClauses.push(`max(n.\`${propName}\`) AS \`${propName}_max\``);
                    withClauses.push(`count(distinct n.\`${propName}\`) AS \`${propName}_distinct\``);
                    returnClauses.push(`min: toString(\`${propName}_min\`), max: toString(\`${propName}_max\`), distinct_count: \`${propName}_distinct\``);
                }
                else if (propType === "LIST") {
                    withClauses.push(`min(size(n.\`${propName}\`)) AS \`${propName}_size_min\`, max(size(n.\`${propName}\`)) AS \`${propName}_size_max\``);
                    returnClauses.push(`min_size: \`${propName}_size_min\`, max_size: \`${propName}_size_max\``);
                }
                else if (["BOOLEAN", "POINT", "DURATION"].includes(propType)) {
                    continue;
                }
                outputDict[propName] = `{${returnClauses.pop()}}`;
            }
        }
        else {
            matchClause += ` WITH n LIMIT 5`;
            for (const prop of properties) {
                const propName = prop.property;
                const propType = prop.type;
                const propIndex = this.structuredSchema?.metadata?.index.filter((el) => el.label === labelOrType &&
                    el.properties[0] === propName &&
                    el.type === "RANGE");
                if (propType === "STRING") {
                    if (propIndex.length > 0 &&
                        propIndex[0].size > 0 &&
                        propIndex[0].distinctValues <= DISTINCT_VALUE_LIMIT) {
                        const distinctValuesPromise = await this.query(`CALL apoc.schema.properties.distinct('${labelOrType}', '${propName}') YIELD value`);
                        const distinctValues = distinctValuesPromise[0].value;
                        returnClauses.push(`values: ${distinctValues}, distinct_count: ${distinctValues.length}`);
                    }
                    else {
                        withClauses.push(`collect(distinct substring(n.\`${propName}\`, 0, 50)) AS \`${propName}_values\``);
                        returnClauses.push(`values: ${propName}_values`);
                    }
                }
                else if (["INTEGER", "FLOAT", "DATE", "DATE_TIME", "LOCAL_DATE_TIME"].includes(propType)) {
                    if (!propIndex) {
                        withClauses.push(`collect(distinct toString(n.\`${propName}\`)) AS \`${propName}_values\``);
                        returnClauses.push(`values: ${propName}_values`);
                    }
                    else {
                        withClauses.push(`min(n.\`${propName}\`) AS \`${propName}_min\``);
                        withClauses.push(`max(n.\`${propName}\`) AS \`${propName}_max\``);
                        withClauses.push(`count(distinct n.\`${propName}\`) AS \`${propName}_distinct\``);
                        returnClauses.push(`min: toString(\`${propName}_min\`), max: toString(\`${propName}_max\`), distinct_count: \`${propName}_distinct\``);
                    }
                }
                else if (propType === "LIST") {
                    withClauses.push(`min(size(n.\`${propName}\`)) AS \`${propName}_size_min\`, max(size(n.\`${propName}\`)) AS \`${propName}_size_max\``);
                    returnClauses.push(`min_size: \`${propName}_size_min\`, max_size: \`${propName}_size_max\``);
                }
                else if (["BOOLEAN", "POINT", "DURATION"].includes(propType)) {
                    continue;
                }
                outputDict[propName] = `{${returnClauses.pop()}}`;
            }
        }
        const withClause = `WITH ${withClauses.join(", ")}`;
        const returnClause = `RETURN {${Object.entries(outputDict)
            .map(([k, v]) => `\`${k}\`: ${v}`)
            .join(", ")}} AS output`;
        const cypherQuery = [matchClause, withClause, returnClause].join("\n");
        return cypherQuery;
    }
    async addGraphDocuments(graphDocuments, config = {}) {
        const { baseEntityLabel } = config;
        if (baseEntityLabel) {
            const constraintExists = this.structuredSchema?.metadata?.constraint?.some((el) => JSON.stringify(el.labelsOrTypes) ===
                JSON.stringify([exports.BASE_ENTITY_LABEL]) &&
                JSON.stringify(el.properties) === JSON.stringify(["id"])) ?? false;
            if (!constraintExists) {
                await this.query(`
          CREATE CONSTRAINT IF NOT EXISTS FOR (b:${exports.BASE_ENTITY_LABEL})
          REQUIRE b.id IS UNIQUE;
        `);
                await this.refreshSchema();
            }
        }
        const nodeImportQuery = getNodeImportQuery(config);
        const relImportQuery = getRelImportQuery(config);
        for (const document of graphDocuments) {
            if (!document.source.metadata.id) {
                document.source.metadata.id = (0, hash_1.insecureHash)(document.source.pageContent);
            }
            // Import nodes
            await this.query(nodeImportQuery, {
                data: document.nodes.map((el) => ({ ...el })),
                document: { ...document.source },
            });
            // Import relationships
            await this.query(relImportQuery, {
                data: document.relationships.map((el) => ({
                    source: el.source.id,
                    source_label: el.source.type,
                    target: el.target.id,
                    target_label: el.target.type,
                    type: el.type.replace(/ /g, "_").toUpperCase(),
                    properties: el.properties,
                })),
            });
        }
    }
    async close() {
        await this.driver.close();
    }
}
exports.Neo4jGraph = Neo4jGraph;
function getNodeImportQuery({ baseEntityLabel, includeSource, }) {
    if (baseEntityLabel) {
        return `
          ${includeSource ? INCLUDE_DOCS_QUERY : ""}
          UNWIND $data AS row
              MERGE(source: \`${exports.BASE_ENTITY_LABEL}\` {id: row.id})
          SET source += row.properties
          ${includeSource ? "MERGE (d)-[:MENTIONS]->(source)" : ""}
          WITH source, row
          CALL apoc.create.addLabels(source, [row.type]) YIELD node
          RETURN distinct 'done' AS result
      `;
    }
    else {
        return `
          ${includeSource ? INCLUDE_DOCS_QUERY : ""}
          UNWIND $data AS row
          CALL apoc.merge.node([row.type], {id: row.id},
          row.properties, {}) YIELD node
          ${includeSource ? "MERGE (d)-[:MENTIONS]->(node)" : ""}
          RETURN distinct 'done' AS result
      `;
    }
}
function getRelImportQuery({ baseEntityLabel, }) {
    if (baseEntityLabel) {
        return `
          UNWIND $data AS row
          MERGE (source:\`${exports.BASE_ENTITY_LABEL}\` {id: row.source})
          MERGE (target:\`${exports.BASE_ENTITY_LABEL}\` {id: row.target})
          WITH source, target, row
          CALL apoc.merge.relationship(source, row.type,
          {}, row.properties, target) YIELD rel
          RETURN distinct 'done'
      `;
    }
    else {
        return `
          UNWIND $data AS row
          CALL apoc.merge.node([row.source_label], {id: row.source},
          {}, {}) YIELD node as source
          CALL apoc.merge.node([row.target_label], {id: row.target},
          {}, {}) YIELD node as target
          CALL apoc.merge.relationship(source, row.type,
          {}, row.properties, target) YIELD rel
          RETURN distinct 'done'
      `;
    }
}
function toObjects(records) {
    return records.map((record) => {
        const rObj = record.toObject();
        const out = {};
        Object.keys(rObj).forEach((key) => {
            out[key] = itemIntToString(rObj[key]);
        });
        return out;
    });
}
function itemIntToString(item) {
    if (neo4j_driver_1.default.isInt(item))
        return item.toString();
    if (Array.isArray(item))
        return item.map((ii) => itemIntToString(ii));
    if (["number", "string", "boolean"].indexOf(typeof item) !== -1)
        return item;
    if (item === null)
        return item;
    if (typeof item === "object")
        return objIntToString(item);
}
function objIntToString(obj) {
    const entry = extractFromNeoObjects(obj);
    let newObj = null;
    if (Array.isArray(entry)) {
        newObj = entry.map((item) => itemIntToString(item));
    }
    else if (entry !== null && typeof entry === "object") {
        newObj = {};
        Object.keys(entry).forEach((key) => {
            newObj[key] = itemIntToString(entry[key]);
        });
    }
    return newObj;
}
function extractFromNeoObjects(obj) {
    if (
    // eslint-disable-next-line
    obj instanceof neo4j_driver_1.default.types.Node ||
        // eslint-disable-next-line
        obj instanceof neo4j_driver_1.default.types.Relationship) {
        return obj.properties;
        // eslint-disable-next-line
    }
    else if (obj instanceof neo4j_driver_1.default.types.Path) {
        // eslint-disable-next-line
        return [].concat.apply([], extractPathForRows(obj));
    }
    return obj;
}
const extractPathForRows = (path) => {
    let { segments } = path;
    // Zero length path. No relationship, end === start
    if (!Array.isArray(path.segments) || path.segments.length < 1) {
        segments = [{ ...path, end: null }];
    }
    return segments.map((segment) => [
        objIntToString(segment.start),
        objIntToString(segment.relationship),
        objIntToString(segment.end),
    ].filter((part) => part !== null));
};
