// src/core/require/resolver.ts
// Job 035a: Package resolution with npm, local, and global search paths

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

export interface ResolverConfig {
  /** Project root directory (defaults to cwd) */
  projectRoot?: string;
  /** Extra search paths beyond defaults */
  extraPaths?: string[];
  /** Whether to search node_modules/@omega packages */
  searchNpm?: boolean;
  /** Whether to search ~/.omega/lib/ */
  searchGlobal?: boolean;
}

export interface ResolvedPackage {
  /** Resolved absolute path to the .lisp file */
  filePath: string;
  /** Original package name as requested */
  packageName: string;
  /** Where it was found: "local", "npm", "global", "extra" */
  source: "local" | "npm" | "global" | "extra";
  /** The search path that contained it */
  searchPath: string;
}

/**
 * RequireResolver: resolves package names to .lisp file paths.
 *
 * Search order:
 *   1. Project-local: ./lib/, ./ relative to project root
 *   2. Extra paths: user-supplied directories
 *   3. npm packages: node_modules/@omega/<pkg>/lib/
 *   4. Global: ~/.omega/lib/
 *
 * Results are cached for performance.
 */
export class RequireResolver {
  private readonly projectRoot: string;
  private readonly extraPaths: string[];
  private readonly searchNpm: boolean;
  private readonly searchGlobal: boolean;
  private readonly cache = new Map<string, ResolvedPackage | null>();
  private npmPaths: string[] | null = null;

  constructor(config: ResolverConfig = {}) {
    this.projectRoot = config.projectRoot ?? process.cwd();
    this.extraPaths = config.extraPaths ?? [];
    this.searchNpm = config.searchNpm ?? true;
    this.searchGlobal = config.searchGlobal ?? true;
  }

  /**
   * Resolve a package name to a file path.
   * @param pkgName Package name like "document-search/core" or "workflow/pipeline"
   * @returns ResolvedPackage or null if not found
   */
  resolve(pkgName: string): ResolvedPackage | null {
    const cleaned = pkgName.replace(/'/g, "");

    if (this.cache.has(cleaned)) {
      return this.cache.get(cleaned)!;
    }

    const result = this.doResolve(cleaned);
    this.cache.set(cleaned, result);
    return result;
  }

  /**
   * List all search paths in priority order.
   */
  getSearchPaths(): Array<{ path: string; source: string }> {
    const paths: Array<{ path: string; source: string }> = [];

    // Local paths
    paths.push({ path: path.resolve(this.projectRoot), source: "local" });
    const packagesDir = path.resolve(this.projectRoot, "packages");
    if (fs.existsSync(packagesDir)) {
      paths.push({ path: packagesDir, source: "local" });
    }
    const matrixPackagesDir = path.resolve(this.projectRoot, ".matrix", "packages");
    if (fs.existsSync(matrixPackagesDir)) {
      paths.push({ path: matrixPackagesDir, source: "local" });
    }
    const libDir = path.resolve(this.projectRoot, "lib");
    if (fs.existsSync(libDir)) {
      paths.push({ path: libDir, source: "local" });
    }

    // Extra paths
    for (const p of this.extraPaths) {
      const resolved = path.resolve(p);
      if (fs.existsSync(resolved)) {
        paths.push({ path: resolved, source: "extra" });
      }
    }

    // npm paths
    if (this.searchNpm) {
      for (const p of this.getNpmPaths()) {
        paths.push({ path: p, source: "npm" });
      }
    }

    // Global
    if (this.searchGlobal) {
      const globalLib = path.join(os.homedir(), ".omega", "lib");
      if (fs.existsSync(globalLib)) {
        paths.push({ path: globalLib, source: "global" });
      }
    }

    return paths;
  }

  /**
   * Clear the resolution cache (e.g., after installing new packages).
   */
  clearCache(): void {
    this.cache.clear();
    this.npmPaths = null;
  }

  /**
   * Add extra search paths at runtime.
   */
  addSearchPath(p: string): void {
    const resolved = path.resolve(p);
    if (!this.extraPaths.includes(resolved)) {
      this.extraPaths.push(resolved);
      this.cache.clear(); // Invalidate cache
    }
  }

  // --- Internal ---

  private doResolve(pkgName: string): ResolvedPackage | null {
    const fileName = pkgName + ".lisp";
    const lastSegment = pkgName.split("/").filter(Boolean).pop() ?? pkgName;

    // 1. Project-local: ./ and ./lib/
    const localResult = this.tryLocal(pkgName, fileName, lastSegment);
    if (localResult) return localResult;

    // 2. Extra paths
    const extraResult = this.tryExtra(pkgName, fileName, lastSegment);
    if (extraResult) return extraResult;

    // 3. npm: node_modules/@omega/*/lib/
    if (this.searchNpm) {
      const npmResult = this.tryNpm(pkgName, fileName, lastSegment);
      if (npmResult) return npmResult;
    }

    // 4. Global: ~/.omega/lib/
    if (this.searchGlobal) {
      const globalResult = this.tryGlobal(pkgName, fileName, lastSegment);
      if (globalResult) return globalResult;
    }

    return null;
  }

  /**
   * Resolve package modules from package-first layout roots.
   *
   * Supports both top-level package entrypoints:
   *   packages/<pkg>/lib/<leaf>.lisp
   * and nested package modules:
   *   packages/<pkg>/lib/<nested/path>.lisp
   *
   * Example:
   *   problem-recognizer/internal/ch51 ->
   *   packages/problem-recognizer/lib/internal/ch51.lisp
   */
  private resolveFromPackageBase(baseDir: string, pkgName: string, source: ResolvedPackage["source"]): ResolvedPackage | null {
    if (!fs.existsSync(baseDir)) return null;

    const parts = pkgName.split("/").filter(Boolean);
    if (parts.length === 0) return null;

    for (let prefixLen = parts.length; prefixLen >= 1; prefixLen--) {
      const packageParts = parts.slice(0, prefixLen);
      const packageRoot = path.join(baseDir, ...packageParts);
      const hasPackageRoot =
        fs.existsSync(path.join(packageRoot, "manifest.json")) ||
        fs.existsSync(path.join(packageRoot, "lib"));
      if (!hasPackageRoot) continue;

      const subParts = parts.slice(prefixLen);
      const entryRelative =
        subParts.length > 0
          ? path.join(...subParts) + ".lisp"
          : `${parts[parts.length - 1]}.lisp`;
      const entryPath = path.join(packageRoot, "lib", entryRelative);

      if (fs.existsSync(entryPath)) {
        return {
          filePath: entryPath,
          packageName: pkgName,
          source,
          searchPath: packageRoot,
        };
      }
    }

    return null;
  }

  private tryLocal(pkgName: string, fileName: string, lastSegment: string): ResolvedPackage | null {
    // Canonical package-first path with nested module support.
    const packageBase = path.resolve(this.projectRoot, "packages");
    const packageEntry = this.resolveFromPackageBase(packageBase, pkgName, "local");
    if (packageEntry) return packageEntry;

    // Local matrix package surface: .matrix/packages/<pkg>/lib/<entry>.lisp
    const matrixPackageBase = path.resolve(this.projectRoot, ".matrix", "packages");
    const matrixEntry = this.resolveFromPackageBase(matrixPackageBase, pkgName, "local");
    if (matrixEntry) return matrixEntry;

    // Try direct: <root>/<fileName>
    const direct = path.resolve(this.projectRoot, fileName);
    if (fs.existsSync(direct)) {
      return { filePath: direct, packageName: pkgName, source: "local", searchPath: this.projectRoot };
    }

    // Try lib: <root>/lib/<fileName>
    const libPath = path.resolve(this.projectRoot, "lib", fileName);
    if (fs.existsSync(libPath)) {
      return { filePath: libPath, packageName: pkgName, source: "local", searchPath: path.resolve(this.projectRoot, "lib") };
    }

    return null;
  }

  private tryExtra(pkgName: string, fileName: string, lastSegment: string): ResolvedPackage | null {
    for (const searchPath of this.extraPaths) {
      const resolved = path.resolve(searchPath);

      // Canonical package-first path in extra roots, supporting either:
      //   <extra>/packages/<pkg>/...
      // or
      //   <extra>/<pkg>/... (when extra points directly at a package base).
      const nestedPackageBase = path.join(resolved, "packages");
      const packageEntry =
        this.resolveFromPackageBase(nestedPackageBase, pkgName, "extra") ??
        this.resolveFromPackageBase(resolved, pkgName, "extra");
      if (packageEntry) return packageEntry;

      // Direct match
      const direct = path.join(resolved, fileName);
      if (fs.existsSync(direct)) {
        return { filePath: direct, packageName: pkgName, source: "extra", searchPath: resolved };
      }

      // Try with lib/ prefix
      const libPath = path.join(resolved, "lib", fileName);
      if (fs.existsSync(libPath)) {
        return { filePath: libPath, packageName: pkgName, source: "extra", searchPath: resolved };
      }
    }
    return null;
  }

  private tryNpm(pkgName: string, fileName: string, lastSegment: string): ResolvedPackage | null {
    // Canonical npm package-first path with nested module support:
    //   <root>/node_modules/@omega/<pkg>/lib/<nested/path>.lisp
    const npmPackageBase = path.join(this.projectRoot, "node_modules", "@omega");
    const packageEntry = this.resolveFromPackageBase(npmPackageBase, pkgName, "npm");
    if (packageEntry) return packageEntry;

    for (const npmPath of this.getNpmPaths()) {
      const fullPath = path.join(npmPath, fileName);
      if (fs.existsSync(fullPath)) {
        return { filePath: fullPath, packageName: pkgName, source: "npm", searchPath: npmPath };
      }
    }
    return null;
  }

  private tryGlobal(pkgName: string, fileName: string, lastSegment: string): ResolvedPackage | null {
    const globalLib = path.join(os.homedir(), ".omega", "lib");
    if (!fs.existsSync(globalLib)) return null;

    // Canonical package-first path with nested module support:
    //   ~/.omega/packages/<pkg>/lib/<nested/path>.lisp
    const globalPackageBase = path.join(os.homedir(), ".omega", "packages");
    const packageEntry = this.resolveFromPackageBase(globalPackageBase, pkgName, "global");
    if (packageEntry) return packageEntry;

    const fullPath = path.join(globalLib, fileName);
    if (fs.existsSync(fullPath)) {
      return { filePath: fullPath, packageName: pkgName, source: "global", searchPath: globalLib };
    }

    return null;
  }

  /**
   * Discover @omega/* packages in node_modules and return their lib/ dirs.
   * Cached after first scan.
   */
  private getNpmPaths(): string[] {
    if (this.npmPaths !== null) return this.npmPaths;

    this.npmPaths = [];
    const nmDir = path.join(this.projectRoot, "node_modules", "@omega");

    if (!fs.existsSync(nmDir)) return this.npmPaths;

    try {
      const entries = fs.readdirSync(nmDir, { withFileTypes: true });
      for (const entry of entries) {
        if (entry.isDirectory()) {
          const libDir = path.join(nmDir, entry.name, "lib");
          if (fs.existsSync(libDir)) {
            this.npmPaths.push(libDir);
          }
        }
      }
    } catch {
      // node_modules not readable — skip silently
    }

    return this.npmPaths;
  }
}

/**
 * Create a resolver with default configuration for a project.
 */
export function createResolver(projectRoot?: string): RequireResolver {
  return new RequireResolver({ projectRoot });
}
