/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var docs_exports = {};
__export(docs_exports, {
  createDocsSearchTool: () => createDocsSearchTool,
  createReadDocEntryTool: () => createReadDocEntryTool,
  describeDocModules: () => describeDocModules,
  getDocModuleKeys: () => getDocModuleKeys,
  loadDocsIndexes: () => loadDocsIndexes,
  readDocEntry: () => readDocEntry,
  searchDocsModule: () => searchDocsModule
});
module.exports = __toCommonJS(docs_exports);
var import_zod = require("zod");
var import_path = __toESM(require("path"));
var import_fs_extra = __toESM(require("fs-extra"));
var import_fast_glob = __toESM(require("fast-glob"));
var import_flexsearch = require("flexsearch");
const DEFAULT_DOCS_DIR = import_path.default.resolve(process.cwd(), "storage/ai/docs");
const docsModules = /* @__PURE__ */ new Map();
let docsBaseDir = DEFAULT_DOCS_DIR;
let docsMeta = {};
const DOCS_INDEX_TTL_MS = 5 * 60 * 1e3;
const DOCS_INDEX_MAX_CACHE = 5;
const DOCS_SEARCH_MAX_KEYWORDS = 5;
const DOCS_READ_MAX_PATHS = 3;
const docsIndexCache = /* @__PURE__ */ new Map();
const docsIndexLoading = /* @__PURE__ */ new Map();
async function loadDocsIndexes(baseDir = DEFAULT_DOCS_DIR) {
  var _a, _b;
  docsBaseDir = baseDir;
  docsModules.clear();
  docsMeta = {};
  docsIndexCache.clear();
  docsIndexLoading.clear();
  const exists = await import_fs_extra.default.pathExists(docsBaseDir);
  if (!exists) {
    return;
  }
  const metaPath = import_path.default.join(docsBaseDir, "meta.json");
  if (await import_fs_extra.default.pathExists(metaPath)) {
    try {
      const meta = await import_fs_extra.default.readJson(metaPath);
      if (meta && typeof meta === "object") {
        docsMeta = meta;
      }
    } catch (error) {
      console.error("[docs-tools] Failed to read docs meta", { metaPath, error });
    }
  }
  const indexFiles = await (0, import_fast_glob.default)("**/index.json", { cwd: docsBaseDir, onlyFiles: true, absolute: true });
  for (const indexFile of indexFiles) {
    const pluginDir = import_path.default.dirname(indexFile);
    const filesPath = import_path.default.join(pluginDir, "files.json");
    const docsDir = pluginDir;
    try {
      const [filesExists, docsExists] = await Promise.all([import_fs_extra.default.pathExists(filesPath), import_fs_extra.default.pathExists(docsDir)]);
      if (!filesExists || !docsExists) {
        continue;
      }
      const relativeKey = import_path.default.relative(docsBaseDir, pluginDir).split(import_path.default.sep).join("/");
      if (!relativeKey) {
        continue;
      }
      const description = ((_b = (_a = docsMeta == null ? void 0 : docsMeta[relativeKey]) == null ? void 0 : _a.description) == null ? void 0 : _b.trim()) || "";
      docsModules.set(relativeKey, {
        key: relativeKey,
        description,
        docsDir: import_path.default.resolve(docsDir),
        indexFile,
        filesMapFile: filesPath
      });
    } catch (error) {
      console.error("[docs-tools] Failed to load docs index metadata", { indexFile, error });
    }
  }
}
function getDocModuleKeys() {
  return Array.from(docsModules.keys()).sort();
}
function describeDocModules(emptyMessage = "No document modules available.") {
  const keys = getDocModuleKeys();
  if (!keys.length) {
    return emptyMessage;
  }
  const items = keys.map((key) => {
    var _a;
    const desc = (_a = docsModules.get(key)) == null ? void 0 : _a.description;
    return desc ? `${key} (${desc})` : key;
  });
  return `Available modules: ${items.join(", ")}`;
}
async function searchDocsModule(moduleKey, keywords, limit = 5) {
  const key = normalizeModuleKey(moduleKey);
  const entry = docsModules.get(key);
  if (!entry) {
    throw new Error(`Document module "${key}" not found.`);
  }
  const { index: flexIndex, fileMap } = await getOrLoadDocsIndex(entry);
  const searchTerms = buildSearchTerms(keywords);
  if (!searchTerms.length) {
    throw new Error("At least one valid keyword is required.");
  }
  const boundedLimit = Math.min(Math.max(limit ?? 5, 1), 20);
  const perKeywordLimit = Math.min(Math.max(boundedLimit * 2, 4), 20);
  const keywordHits = await Promise.all(
    searchTerms.map(
      async (keyword) => (await flexIndex.searchAsync(keyword, { limit: perKeywordLimit }) || []).map(
        (id) => String(id)
      )
    )
  );
  const aggregate = /* @__PURE__ */ new Map();
  for (const hits of keywordHits) {
    for (let i = 0; i < hits.length; i++) {
      const filePath = fileMap[hits[i]];
      if (!filePath) {
        continue;
      }
      const rankScore = perKeywordLimit - i;
      const record = aggregate.get(filePath);
      if (record) {
        record.score += rankScore;
        record.bestRank = Math.min(record.bestRank, i);
      } else {
        aggregate.set(filePath, { score: rankScore, bestRank: i });
      }
    }
  }
  const matches = Array.from(aggregate.entries()).sort((a, b) => {
    if (b[1].score !== a[1].score) {
      return b[1].score - a[1].score;
    }
    if (a[1].bestRank !== b[1].bestRank) {
      return a[1].bestRank - b[1].bestRank;
    }
    return a[0].localeCompare(b[0]);
  }).slice(0, boundedLimit).map(([filePath]) => filePath);
  return {
    key,
    keywords: searchTerms,
    matches
  };
}
async function readDocEntry(docPath) {
  const resolved = resolveDocPath(docPath);
  if (!await import_fs_extra.default.pathExists(resolved.absolutePath)) {
    throw new Error(`Path "${resolved.canonicalPath}" does not exist.`);
  }
  const stat = await import_fs_extra.default.stat(resolved.absolutePath);
  if (stat.isDirectory()) {
    const names = await import_fs_extra.default.readdir(resolved.absolutePath);
    const entries = await Promise.all(
      names.sort().map(async (name) => {
        const absoluteChild = import_path.default.join(resolved.absolutePath, name);
        const childStat = await import_fs_extra.default.stat(absoluteChild);
        const relativeChild = import_path.default.relative(resolved.meta.docsDir, absoluteChild).split(import_path.default.sep).join("/");
        const childPath = relativeChild ? `${resolved.meta.key}/${relativeChild}` : resolved.meta.key;
        const entryType = childStat.isDirectory() ? "directory" : "file";
        return {
          name,
          type: entryType,
          path: childPath
        };
      })
    );
    return {
      type: "directory",
      path: resolved.canonicalPath,
      entries
    };
  }
  if (!stat.isFile()) {
    throw new Error(`Path "${resolved.canonicalPath}" is not a regular file.`);
  }
  const content = await import_fs_extra.default.readFile(resolved.absolutePath, "utf8");
  return {
    type: "file",
    path: resolved.canonicalPath,
    content
  };
}
function createDocsSearchTool() {
  const docsModulesDescription = describeDocModules("Docs modules unavailable. Run ai:create-docs-index first.");
  return {
    scope: "GENERAL",
    defaultPermission: "ALLOW",
    introduction: {
      title: '{{t("Search documentation")}}',
      about: '{{t("Search docs tool description")}}'
    },
    definition: {
      name: "searchDocs",
      description: `Search indexed documentation using a FlexSearch-based keyword index.
Provide a small keyword list (identifiers, API names, module terms). The tool will search them concurrently with limited parallelism.
Return matching document paths only. ${docsModulesDescription}`,
      schema: import_zod.z.object({
        module: import_zod.z.string().min(1, "module is required").describe("Module key, e.g. runjs"),
        keywords: import_zod.z.array(import_zod.z.string().min(1)).min(1, "keywords is required").max(DOCS_SEARCH_MAX_KEYWORDS, `keywords can contain up to ${DOCS_SEARCH_MAX_KEYWORDS} items`).describe('Keywords to search, e.g. ["router", "ctx.state", "runjs"]'),
        limit: import_zod.z.number().int().min(1).max(20).optional().describe("Maximum number of hits (default 5, max 20)")
      })
    },
    invoke: async (ctx, args) => {
      var _a, _b, _c;
      const available = getDocModuleKeys();
      if (!available.length) {
        return {
          status: "error",
          content: "No document indexes available."
        };
      }
      const moduleKey = (_a = args == null ? void 0 : args.module) == null ? void 0 : _a.trim();
      const keywords = Array.isArray(args == null ? void 0 : args.keywords) ? args.keywords : [];
      const limit = typeof (args == null ? void 0 : args.limit) === "number" ? args.limit : void 0;
      if (!moduleKey || !keywords.length) {
        return {
          status: "error",
          content: `Both module and keywords are required. Available modules: ${available.join(", ")}`
        };
      }
      try {
        const result = await searchDocsModule(moduleKey, keywords, limit);
        return {
          status: "success",
          content: JSON.stringify({
            module: result.key,
            keywords: result.keywords,
            matches: result.matches,
            availableModules: available
          })
        };
      } catch (error) {
        (_c = (_b = ctx.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, error, {
          module: "ai",
          subModule: "toolCalling",
          toolName: "searchDocs"
        });
        return {
          status: "error",
          content: `Failed to search docs: ${error.message}`
        };
      }
    }
  };
}
function createReadDocEntryTool() {
  return {
    scope: "GENERAL",
    defaultPermission: "ALLOW",
    introduction: {
      title: '{{t("Read documentation file")}}',
      about: '{{t("Read docs tool description")}}'
    },
    definition: {
      name: "readDocEntry",
      description: "Read files or list directories inside storage/ai/docs using canonical module-based paths. Supports up to 3 paths per call.",
      schema: import_zod.z.object({
        paths: import_zod.z.array(
          import_zod.z.string().min(1, "path is required").describe(
            "Canonical path like runjs/context/router/index.md or /runjs/context/router/index.md. No wildcards, relative segments, or globbing."
          )
        ).min(1, "paths is required").max(DOCS_READ_MAX_PATHS, `paths can contain up to ${DOCS_READ_MAX_PATHS} items`)
      })
    },
    invoke: async (ctx, args) => {
      var _a, _b;
      const available = getDocModuleKeys();
      if (!available.length) {
        return {
          status: "error",
          content: "No document indexes available."
        };
      }
      const rawPaths = Array.isArray(args == null ? void 0 : args.paths) ? args.paths : [];
      const paths = Array.from(
        new Set(
          rawPaths.map((p) => String(p ?? "").trim()).filter((p) => !!p).slice(0, DOCS_READ_MAX_PATHS)
        )
      );
      if (!paths.length) {
        return {
          status: "error",
          content: `Paths are required. Available indexes: ${available.join(", ")}`
        };
      }
      try {
        const entries = await Promise.all(paths.map((targetPath) => readDocEntry(targetPath)));
        const blocks = entries.map((entry) => {
          if (entry.type === "directory") {
            const listing = entry.entries.map((item) => `${item.type === "directory" ? "DIR " : "FILE"} ${item.path}`).join("\n");
            return `DIRECTORY ${entry.path}
${listing}`;
          }
          const clean = entry.content.replace(/\r\n/g, "\n");
          return `FILE ${entry.path}
\`\`\`
${clean}
\`\`\``;
        });
        return {
          status: "success",
          content: blocks.join("\n\n")
        };
      } catch (error) {
        (_b = (_a = ctx.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, error, {
          module: "ai",
          subModule: "toolCalling",
          toolName: "readDocEntry"
        });
        return {
          status: "error",
          content: `Failed to read docs entry: ${error.message}`
        };
      }
    }
  };
}
function normalizeModuleKey(input) {
  const { pluginKey, relativePath } = parseDocPath(input);
  if (relativePath) {
    throw new Error("Index key should only contain the module key, e.g. runjs.");
  }
  return pluginKey;
}
function resolveDocPath(input) {
  const { pluginKey, relativePath, canonicalPath } = parseDocPath(input);
  const meta = docsModules.get(pluginKey);
  if (!meta) {
    throw new Error(`Document module "${pluginKey}" not found.`);
  }
  const safeRelative = relativePath || "";
  const absolutePath = import_path.default.resolve(meta.docsDir, safeRelative);
  if (!absolutePath.startsWith(meta.docsDir)) {
    throw new Error("Access denied for the requested path.");
  }
  return {
    meta,
    absolutePath,
    canonicalPath
  };
}
function parseDocPath(input) {
  const segments = normalizeDocInput(input);
  let pluginKey;
  let relativeStart = 0;
  if (segments[0].startsWith("@")) {
    if (segments.length < 2) {
      throw new Error("Missing package name segment in the path.");
    }
    pluginKey = `${segments[0]}/${segments[1]}`;
    relativeStart = 2;
  } else {
    pluginKey = segments[0];
    relativeStart = 1;
  }
  const relativeSegments = segments.slice(relativeStart);
  const relativePath = relativeSegments.join("/");
  const canonicalPath = relativePath ? `${pluginKey}/${relativePath}` : pluginKey;
  return {
    pluginKey,
    relativePath,
    canonicalPath
  };
}
function normalizeDocInput(input) {
  let value = (input ?? "").trim();
  if (!value) {
    throw new Error("Path is required.");
  }
  value = value.replace(/\\/g, "/");
  value = value.replace(/^\/+/, "");
  if (value.startsWith("storage/ai/docs/")) {
    value = value.slice("storage/ai/docs/".length);
  }
  if (!value) {
    throw new Error("Path is required.");
  }
  const rawSegments = value.split("/");
  const segments = [];
  for (const raw of rawSegments) {
    const segment = raw.trim();
    if (!segment || segment === ".") {
      continue;
    }
    if (segment === "..") {
      throw new Error("Relative segments are not allowed in document paths.");
    }
    if (segment.includes("*")) {
      throw new Error("Wildcards are not allowed in document paths.");
    }
    segments.push(segment);
  }
  if (!segments.length) {
    throw new Error("Path is required.");
  }
  return segments;
}
async function getOrLoadDocsIndex(entry) {
  const now = Date.now();
  const cached = docsIndexCache.get(entry.key);
  if (cached && now - cached.lastAccess <= DOCS_INDEX_TTL_MS) {
    cached.lastAccess = now;
    return cached;
  }
  const loading = docsIndexLoading.get(entry.key);
  if (loading) {
    const result = await loading;
    docsIndexCache.set(entry.key, { ...result, lastAccess: Date.now() });
    return docsIndexCache.get(entry.key);
  }
  const loadPromise = (async () => {
    const [indexData, fileMap] = await Promise.all([import_fs_extra.default.readJSON(entry.indexFile), import_fs_extra.default.readJSON(entry.filesMapFile)]);
    const flexIndex = new import_flexsearch.Index();
    Object.entries(indexData).forEach(([importKey, data]) => {
      if (typeof data === "string") {
        flexIndex.import(importKey, data);
      }
    });
    return { index: flexIndex, fileMap };
  })();
  docsIndexLoading.set(entry.key, loadPromise);
  try {
    const result = await loadPromise;
    docsIndexCache.set(entry.key, { ...result, lastAccess: Date.now() });
    enforceDocsIndexCacheLimit();
    return docsIndexCache.get(entry.key);
  } finally {
    docsIndexLoading.delete(entry.key);
  }
}
function enforceDocsIndexCacheLimit() {
  if (docsIndexCache.size <= DOCS_INDEX_MAX_CACHE) {
    return;
  }
  const entries = Array.from(docsIndexCache.entries()).sort((a, b) => a[1].lastAccess - b[1].lastAccess);
  const overflow = entries.length - DOCS_INDEX_MAX_CACHE;
  for (let i = 0; i < overflow; i++) {
    docsIndexCache.delete(entries[i][0]);
  }
}
function buildSearchTerms(keywords) {
  const seen = /* @__PURE__ */ new Set();
  const terms = [];
  for (const raw of keywords) {
    const keyword = String(raw ?? "").trim().replace(/\s+/g, " ");
    if (!keyword || seen.has(keyword)) {
      continue;
    }
    seen.add(keyword);
    terms.push(keyword);
    if (terms.length >= DOCS_SEARCH_MAX_KEYWORDS) {
      break;
    }
  }
  return terms;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  createDocsSearchTool,
  createReadDocEntryTool,
  describeDocModules,
  getDocModuleKeys,
  loadDocsIndexes,
  readDocEntry,
  searchDocsModule
});
