/**
 * Package Help System
 *
 * Provides discoverability commands for OmegaLLM:
 * - :packages
 * - :help <package>
 * - :doc <primitive>
 * - :apropos <pattern> [--category <name>] [--source <core|first-party|third-party>]
 * - :man <topic> [--category <name>] [--source <core|first-party|third-party>]
 */

import * as fs from "fs";
import * as path from "path";

export type Provenance = "core" | "first-party package" | "third-party package";
export type LearningPath = "intro" | "intermediate" | "advanced";
export type SourceFilter = "core" | "first-party package" | "third-party package";

export interface PackageInfo {
  name: string;
  description: string;
  primitiveCount: number;
  docPath: string;
  provenance: Provenance;
  learningPath: LearningPath;
  keywords: string[];
  capabilities: string[];
  commands: string[];
  manifestPath?: string;
}

export interface PrimitiveInfo {
  name: string;
  signature: string;
  description: string;
  package: string;
  provenance: Provenance;
  learningPath: LearningPath;
  categories: string[];
}

export interface SearchOptions {
  category?: string;
  source?: SourceFilter;
  exact?: boolean;
  limit?: number;
}

export interface ParsedSearchQuery {
  pattern: string;
  options: SearchOptions;
  errors: string[];
}

interface ParsedPackage {
  pkg: PackageInfo;
  primitives: PrimitiveInfo[];
}

interface ManifestDoc {
  id?: string;
  name?: string;
  version?: string;
  summary?: string;
  description?: string;
  exports?: string[];
  keywords?: string[];
  capabilities?: string[];
  commands?: string[];
  learningPath?: string;
}

const packageRegistry: Map<string, PackageInfo> = new Map();
const primitiveIndex: Map<string, PrimitiveInfo> = new Map();

const HELP_QUERY_USAGE = [
  "Usage:",
  "  :apropos <pattern> [--category <name>] [--source <core|first-party|third-party>]",
  "  :man <topic> [--category <name>] [--source <core|first-party|third-party>]",
].join("\n");

function normalizeLearningPath(value: string | undefined): LearningPath {
  const normalized = (value ?? "").trim().toLowerCase();
  if (normalized === "intro" || normalized === "intermediate" || normalized === "advanced") {
    return normalized;
  }
  return "intermediate";
}

function isThirdPartyExampleManifest(manifestPath: string | undefined): boolean {
  if (!manifestPath) return false;
  const normalized = manifestPath.replace(/\\/g, "/");
  return normalized.includes("/packages/examples/");
}

function deriveProvenance(identifier: string, manifestPath?: string): Provenance {
  if (identifier === "core" || identifier.startsWith("core/")) {
    return "core";
  }
  if (identifier.startsWith("@") || isThirdPartyExampleManifest(manifestPath)) {
    return "third-party package";
  }
  return "first-party package";
}

function sourceFilterFromToken(value: string): SourceFilter | null {
  const normalized = value.trim().toLowerCase();
  if (normalized === "core") return "core";
  if (normalized === "first-party" || normalized === "first-party-package" || normalized === "first-party package") {
    return "first-party package";
  }
  if (normalized === "third-party" || normalized === "third-party-package" || normalized === "third-party package") {
    return "third-party package";
  }
  return null;
}

function tokenizeQuery(input: string): string[] {
  const tokens = input.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
  return tokens.map((token) => token.replace(/^"(.*)"$/, "$1"));
}

function compileRegex(pattern: string): RegExp | null {
  try {
    return new RegExp(pattern, "i");
  } catch {
    return null;
  }
}

function trimCategory(value: string | undefined): string | undefined {
  const category = (value ?? "").trim();
  return category.length > 0 ? category.toLowerCase() : undefined;
}

function categoryMatches(category: string | undefined, info: PrimitiveInfo): boolean {
  if (!category) return true;
  return info.categories.some((item) => item.includes(category));
}

function toTextList(items: string[]): string {
  if (items.length === 0) return "(none)";
  return items.join(", ");
}

function parseMarkdownPackageDoc(name: string, content: string, docPath: string): ParsedPackage | null {
  const descMatch = content.match(/^#[^\n]+\n+([^\n#]+)/);
  const description = descMatch ? descMatch[1].trim() : "";
  const primitives: PrimitiveInfo[] = [];

  const tableRowRegex = /\|\s*`([^`]+)`\s*\|\s*([^|]*)\s*\|\s*([^|]+)\s*\|/g;
  let match;
  while ((match = tableRowRegex.exec(content)) !== null) {
    const primName = match[1].trim();
    const signature = match[2].trim() || primName;
    const desc = match[3].trim();
    if (primName === "Primitive" || /^-+$/.test(primName)) continue;

    primitives.push({
      name: primName,
      signature: signature || `(${primName} ...)`,
      description: desc,
      package: name,
      provenance: deriveProvenance(name),
      learningPath: name.startsWith("core") ? "intro" : "intermediate",
      categories: [name.toLowerCase()],
    });
  }

  return {
    pkg: {
      name,
      description,
      primitiveCount: primitives.length,
      docPath,
      provenance: deriveProvenance(name),
      learningPath: name.startsWith("core") ? "intro" : "intermediate",
      keywords: [name.toLowerCase()],
      capabilities: [],
      commands: [],
    },
    primitives,
  };
}

function findManifestFiles(root: string): string[] {
  const manifests: string[] = [];
  const stack: string[] = [root];
  while (stack.length > 0) {
    const current = stack.pop();
    if (!current) continue;
    let entries: fs.Dirent[] = [];
    try {
      entries = fs.readdirSync(current, { withFileTypes: true });
    } catch {
      continue;
    }

    for (const entry of entries) {
      const full = path.join(current, entry.name);
      if (entry.isDirectory()) {
        stack.push(full);
        continue;
      }
      if (entry.isFile() && entry.name === "manifest.json") {
        manifests.push(full);
      }
    }
  }
  return manifests;
}

function findBestDocPath(packageRoot: string): string {
  const helpDir = path.join(packageRoot, "help");
  if (fs.existsSync(helpDir)) {
    const helpFiles = fs.readdirSync(helpDir).filter((file) => file.endsWith(".md"));
    if (helpFiles.length > 0) {
      return path.join(helpDir, helpFiles[0]);
    }
  }

  const readmePath = path.join(packageRoot, "README.md");
  if (fs.existsSync(readmePath)) {
    return readmePath;
  }

  return readmePath;
}

function loadPackageManifestDocs(packagesRoot: string): void {
  if (!fs.existsSync(packagesRoot)) {
    return;
  }

  const manifestPaths = findManifestFiles(packagesRoot);
  for (const manifestPath of manifestPaths) {
    try {
      const raw = fs.readFileSync(manifestPath, "utf8");
      const manifest = JSON.parse(raw) as ManifestDoc;
      const packageRoot = path.dirname(manifestPath);
      const packageName = manifest.name || path.relative(packagesRoot, packageRoot).replace(/\\/g, "/");
      const id = manifest.id || packageName;
      const exportsList = Array.isArray(manifest.exports) ? manifest.exports : [];
      const keywords = Array.isArray(manifest.keywords) ? manifest.keywords : [];
      const capabilities = Array.isArray(manifest.capabilities) ? manifest.capabilities : [];
      const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
      const provenance = deriveProvenance(id, manifestPath);
      const learningPath = normalizeLearningPath(manifest.learningPath);
      const categories = [...new Set([...keywords, ...capabilities].map((item) => item.toLowerCase()))];

      packageRegistry.set(packageName, {
        name: packageName,
        description: manifest.description || manifest.summary || "Package loaded from manifest metadata.",
        primitiveCount: exportsList.length,
        docPath: findBestDocPath(packageRoot),
        provenance,
        learningPath,
        keywords,
        capabilities,
        commands,
        manifestPath,
      });

      for (const symbol of exportsList) {
        if (!primitiveIndex.has(symbol)) {
          primitiveIndex.set(symbol, {
            name: symbol,
            signature: `(${symbol} ...)`,
            description: `Export from package ${packageName}`,
            package: packageName,
            provenance,
            learningPath,
            categories,
          });
        } else {
          const existing = primitiveIndex.get(symbol)!;
          primitiveIndex.set(symbol, {
            ...existing,
            provenance,
            learningPath,
            categories: [...new Set([...existing.categories, ...categories])],
          });
        }
      }
    } catch {
      // Keep discoverability resilient; strict validation runs in scripts/discoverability-verify.mjs.
    }
  }
}

export function parseSearchQuery(query: string): ParsedSearchQuery {
  const errors: string[] = [];
  const options: SearchOptions = {};
  const patternParts: string[] = [];
  const tokens = tokenizeQuery(query.trim());

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i];
    if (token === "--category") {
      const value = tokens[i + 1];
      if (!value) {
        errors.push("Missing value for --category");
      } else {
        options.category = value;
        i += 1;
      }
      continue;
    }

    if (token === "--source") {
      const value = tokens[i + 1];
      if (!value) {
        errors.push("Missing value for --source");
      } else {
        const parsed = sourceFilterFromToken(value);
        if (!parsed) {
          errors.push(`Invalid --source value: ${value}`);
        } else {
          options.source = parsed;
        }
        i += 1;
      }
      continue;
    }

    patternParts.push(token);
  }

  return {
    pattern: patternParts.join(" ").trim() || ".*",
    options,
    errors,
  };
}

export function loadPackageDocs(helpDir: string): void {
  const workspacePackagesRoot = path.join(process.cwd(), "packages");

  if (fs.existsSync(helpDir)) {
    const files = fs.readdirSync(helpDir).filter((file) => file.endsWith(".md") && file !== "index.md");
    for (const file of files) {
      const filePath = path.join(helpDir, file);
      const content = fs.readFileSync(filePath, "utf-8");
      const pkgName = path.basename(file, ".md");
      const info = parseMarkdownPackageDoc(pkgName, content, filePath);
      if (!info) continue;
      packageRegistry.set(pkgName, info.pkg);
      for (const prim of info.primitives) {
        primitiveIndex.set(prim.name, prim);
      }
    }
  }

  loadPackageManifestDocs(workspacePackagesRoot);
}

export function getPackages(): PackageInfo[] {
  return Array.from(packageRegistry.values()).sort((a, b) => a.name.localeCompare(b.name));
}

export function getPackage(name: string): PackageInfo | undefined {
  return packageRegistry.get(name);
}

export function getPrimitive(name: string): PrimitiveInfo | undefined {
  return primitiveIndex.get(name);
}

export function searchPrimitives(pattern: string, options: SearchOptions = {}): PrimitiveInfo[] {
  const regex = compileRegex(pattern);
  if (!regex) {
    return [];
  }

  const category = trimCategory(options.category);
  const source = options.source;
  const exact = options.exact === true;

  let results = Array.from(primitiveIndex.values()).filter((info) => {
    const textMatch = exact
      ? info.name.toLowerCase() === pattern.toLowerCase()
      : (regex.test(info.name) || regex.test(info.description));
    if (!textMatch) return false;
    if (!categoryMatches(category, info)) return false;
    if (source && info.provenance !== source) return false;
    return true;
  });

  results = results.sort((a, b) => {
    const pkgCmp = a.package.localeCompare(b.package);
    if (pkgCmp !== 0) return pkgCmp;
    return a.name.localeCompare(b.name);
  });

  if (typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0) {
    return results.slice(0, options.limit);
  }
  return results;
}

export function getPackageDoc(name: string): string | null {
  const pkg = packageRegistry.get(name);
  if (!pkg) return null;
  try {
    return fs.readFileSync(pkg.docPath, "utf-8");
  } catch {
    return null;
  }
}

export function handlePackagesCommand(): string {
  const packages = getPackages();
  if (packages.length === 0) {
    return "No package documentation loaded. Ensure lib/help/ exists.";
  }

  const lines: string[] = [];
  lines.push("OMEGALLM PACKAGES");
  lines.push("=================");
  for (const pkg of packages) {
    lines.push(
      `- ${pkg.name} (${pkg.primitiveCount} exports) [${pkg.provenance}; ${pkg.learningPath}] :: ${pkg.description}`,
    );
  }
  lines.push("");
  lines.push("Commands:");
  lines.push("  :help <pkg>                                  full package documentation");
  lines.push("  :doc <prim>                                  exact primitive lookup");
  lines.push("  :apropos <pat> [--category <c>] [--source <core|first-party|third-party>]");
  lines.push("  :man <topic> [--category <c>] [--source <core|first-party|third-party>]");
  return lines.join("\n");
}

export function handleHelpPackageCommand(packageName: string): string {
  const pkg = getPackage(packageName);
  if (pkg) {
    const doc = getPackageDoc(packageName);
    if (doc) {
      return [
        `Package: ${pkg.name}`,
        `Source: ${pkg.provenance}`,
        `Learning Path: ${pkg.learningPath}`,
        `Categories: ${toTextList([...new Set([...pkg.keywords, ...pkg.capabilities])])}`,
        "",
        doc,
      ].join("\n");
    }
  }

  const similar = searchPrimitives(packageName, { limit: 5 });
  if (similar.length > 0) {
    return `Package "${packageName}" not found. Did you mean one of these primitives?\n` +
      similar.map((entry) => `  ${entry.name} (${entry.package}; ${entry.provenance})`).join("\n") +
      "\n\nUse :packages to see available packages.";
  }

  return `Package "${packageName}" not found. Use :packages to see available packages.`;
}

export function handleDocCommand(primitiveName: string): string {
  const prim = getPrimitive(primitiveName);
  if (prim) {
    return [
      prim.name,
      "-".repeat(prim.name.length),
      `Package:   ${prim.package}`,
      `Source:    ${prim.provenance}`,
      `Learning:  ${prim.learningPath}`,
      `Category:  ${toTextList(prim.categories)}`,
      `Signature: ${prim.signature}`,
      "",
      prim.description,
      "",
      `Use :help ${prim.package} for package documentation.`,
    ].join("\n");
  }

  const similar = searchPrimitives(primitiveName, { limit: 8 });
  if (similar.length > 0) {
    return `Primitive "${primitiveName}" not found. Similar:\n` +
      similar.map((entry) => `  ${entry.name.padEnd(24)} ${entry.package} [${entry.provenance}]`).join("\n");
  }

  return `Primitive "${primitiveName}" not found. Use :apropos to search.`;
}

export function handleAproposCommand(query: string): string {
  const parsed = parseSearchQuery(query);
  if (parsed.errors.length > 0) {
    return `${HELP_QUERY_USAGE}\n\nErrors:\n${parsed.errors.map((err) => `- ${err}`).join("\n")}`;
  }

  const results = searchPrimitives(parsed.pattern, parsed.options);
  if (results.length === 0) {
    const filters = [
      parsed.options.category ? `category=${parsed.options.category}` : null,
      parsed.options.source ? `source=${parsed.options.source}` : null,
    ].filter(Boolean).join(", ");
    return `No primitives matching "${parsed.pattern}"${filters ? ` (${filters})` : ""}.`;
  }

  const lines: string[] = [];
  lines.push(`Found ${results.length} primitives matching "${parsed.pattern}":`);
  if (parsed.options.category || parsed.options.source) {
    lines.push(`Filters: category=${parsed.options.category ?? "*"}, source=${parsed.options.source ?? "*"}`);
  }
  lines.push("");

  const byPackage = new Map<string, PrimitiveInfo[]>();
  for (const info of results) {
    if (!byPackage.has(info.package)) {
      byPackage.set(info.package, []);
    }
    byPackage.get(info.package)!.push(info);
  }

  for (const [pkg, entries] of Array.from(byPackage.entries()).sort(([a], [b]) => a.localeCompare(b))) {
    const source = entries[0]?.provenance ?? "first-party package";
    const learning = entries[0]?.learningPath ?? "intermediate";
    lines.push(`[${pkg}] source=${source} learning=${learning}`);
    for (const entry of entries.slice(0, 12)) {
      lines.push(`  ${entry.name.padEnd(28)} ${entry.description.slice(0, 64)}`);
    }
    if (entries.length > 12) {
      lines.push(`  ... and ${entries.length - 12} more`);
    }
    lines.push("");
  }

  return lines.join("\n");
}

export function handleManCommand(query: string): string {
  const parsed = parseSearchQuery(query);
  if (parsed.errors.length > 0) {
    return `${HELP_QUERY_USAGE}\n\nErrors:\n${parsed.errors.map((err) => `- ${err}`).join("\n")}`;
  }

  const topic = parsed.pattern.trim();
  if (!topic || topic === ".*") {
    return `Usage: :man <topic> [--category <name>] [--source <core|first-party|third-party>]`;
  }

  const primitive = getPrimitive(topic);
  if (primitive) {
    const category = trimCategory(parsed.options.category);
    const categoryOk = categoryMatches(category, primitive);
    const sourceOk = !parsed.options.source || primitive.provenance === parsed.options.source;
    if (categoryOk && sourceOk) {
      return handleDocCommand(topic);
    }
  }

  const pkg = getPackage(topic);
  if (pkg) {
    const category = trimCategory(parsed.options.category);
    const sourceOk = !parsed.options.source || pkg.provenance === parsed.options.source;
    const categoryPool = [...pkg.keywords, ...pkg.capabilities].map((item) => item.toLowerCase());
    const categoryOk = !category || categoryPool.some((item) => item.includes(category));
    if (sourceOk && categoryOk) {
      return handleHelpPackageCommand(topic);
    }
  }

  const fallback = handleAproposCommand(query);
  if (!fallback.startsWith("Found ")) {
    return `No manual entry for "${topic}".\n${fallback}`;
  }
  return `No exact manual entry for "${topic}". Closest matches:\n\n${fallback}`;
}
