const fs = require('fs-extra');
const path = require('node:path');

// Deno/Node compatibility: explicitly import process
const process = require('node:process');
const { execFile } = require('node:child_process');
const { promisify } = require('node:util');
const execFileAsync = promisify(execFile);

// Simple memoization across calls (keyed by realpath of startDir)
const _cache = new Map();

async function _tryRun(cmd, args, cwd, timeoutMs = 500) {
  try {
    const { stdout } = await execFileAsync(cmd, args, {
      cwd,
      timeout: timeoutMs,
      windowsHide: true,
      maxBuffer: 1024 * 1024,
    });
    const out = String(stdout || '').trim();
    return out || null;
  } catch {
    return null;
  }
}

async function _detectVcsTopLevel(startDir) {
  // Run common VCS root queries in parallel; ignore failures
  const gitP = _tryRun('git', ['rev-parse', '--show-toplevel'], startDir);
  const hgP = _tryRun('hg', ['root'], startDir);
  const svnP = (async () => {
    const show = await _tryRun('svn', ['info', '--show-item', 'wc-root'], startDir);
    if (show) return show;
    const info = await _tryRun('svn', ['info'], startDir);
    if (info) {
      const line = info.split(/\r?\n/).find((l) => l.toLowerCase().startsWith('working copy root path:'));
      if (line) return line.split(':').slice(1).join(':').trim();
    }
    return null;
  })();
  const [git, hg, svn] = await Promise.all([gitP, hgP, svnP]);
  return git || hg || svn || null;
}

/**
 * Attempt to find the project root by walking up from startDir.
 * Uses a robust, prioritized set of ecosystem markers (VCS > workspaces/monorepo > lock/build > language config).
 * Also recognizes package.json with "workspaces" as a workspace root.
 * You can augment markers via env PROJECT_ROOT_MARKERS as a comma-separated list of file/dir names.
 * @param {string} startDir
 * @returns {Promise<string|null>} project root directory or null if not found
 */
async function findProjectRoot(startDir) {
  try {
    // Resolve symlinks for robustness (e.g., when invoked from a symlinked path)
    let dir = path.resolve(startDir);
    try {
      dir = await fs.realpath(dir);
    } catch {
      // ignore if realpath fails; continue with resolved path
    }
    const startKey = dir; // preserve starting point for caching
    if (_cache.has(startKey)) return _cache.get(startKey);
    const fsRoot = path.parse(dir).root;

    // Helper to safely check for existence
    const exists = (p) => fs.pathExists(p);

    // Build checks: an array of { makePath: (dir) => string, weight }
    const checks = [];

    const add = (rel, weight) => {
      const makePath = (d) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel));
      checks.push({ makePath, weight });
    };

    // Highest priority: explicit sentinel markers
    add('.project-root', 110);
    add('.workspace-root', 110);
    add('.repo-root', 110);

    // Highest priority: VCS roots
    add('.git', 100);
    add('.hg', 95);
    add('.svn', 95);

    // Monorepo/workspace indicators
    add('pnpm-workspace.yaml', 90);
    add('lerna.json', 90);
    add('turbo.json', 90);
    add('nx.json', 90);
    add('rush.json', 90);
    add('go.work', 90);
    add('WORKSPACE', 90);
    add('WORKSPACE.bazel', 90);
    add('MODULE.bazel', 90);
    add('pants.toml', 90);

    // Lockfiles and package-manager/top-level locks
    add('yarn.lock', 85);
    add('pnpm-lock.yaml', 85);
    add('package-lock.json', 85);
    add('bun.lockb', 85);
    add('Cargo.lock', 85);
    add('composer.lock', 85);
    add('poetry.lock', 85);
    add('Pipfile.lock', 85);
    add('Gemfile.lock', 85);

    // Build-system root indicators
    add('settings.gradle', 80);
    add('settings.gradle.kts', 80);
    add('gradlew', 80);
    add('pom.xml', 80);
    add('build.sbt', 80);
    add(['project', 'build.properties'], 80);

    // Language/project config markers
    add('deno.json', 75);
    add('deno.jsonc', 75);
    add('pyproject.toml', 75);
    add('Pipfile', 75);
    add('requirements.txt', 75);
    add('go.mod', 75);
    add('Cargo.toml', 75);
    add('composer.json', 75);
    add('mix.exs', 75);
    add('Gemfile', 75);
    add('CMakeLists.txt', 75);
    add('stack.yaml', 75);
    add('cabal.project', 75);
    add('rebar.config', 75);
    add('pubspec.yaml', 75);
    add('flake.nix', 75);
    add('shell.nix', 75);
    add('default.nix', 75);
    add('.tool-versions', 75);
    add('package.json', 74); // generic Node project (lower than lockfiles/workspaces)

    // Changesets
    add(['.changeset', 'config.json'], 70);
    add('.changeset', 70);

    // Custom markers via env (comma-separated names)
    if (process.env.PROJECT_ROOT_MARKERS) {
      for (const name of process.env.PROJECT_ROOT_MARKERS.split(',')
        .map((s) => s.trim())
        .filter(Boolean)) {
        add(name, 72);
      }
    }

    /** Check for package.json with "workspaces" */
    const hasWorkspacePackageJson = async (d) => {
      const pkgPath = path.join(d, 'package.json');
      if (!(await exists(pkgPath))) return false;
      try {
        const raw = await fs.readFile(pkgPath, 'utf8');
        const pkg = JSON.parse(raw);
        return Boolean(pkg && pkg.workspaces);
      } catch {
        return false;
      }
    };

    let best = null; // { dir, weight }

    // Try to detect VCS toplevel once up-front; treat as authoritative slightly above .git marker
    const vcsTop = await _detectVcsTopLevel(dir);
    if (vcsTop) {
      best = { dir: vcsTop, weight: 101 };
    }

    while (true) {
      // Special check: package.json with "workspaces"
      if ((await hasWorkspacePackageJson(dir)) && (!best || 90 >= best.weight)) best = { dir, weight: 90 };

      // Evaluate all other checks in parallel
      const results = await Promise.all(checks.map(async (c) => ({ c, ok: await exists(c.makePath(dir)) })));

      for (const { c, ok } of results) {
        if (!ok) continue;
        if (!best || c.weight >= best.weight) {
          best = { dir, weight: c.weight };
        }
      }

      if (dir === fsRoot) break;
      dir = path.dirname(dir);
    }

    const out = best ? best.dir : null;
    _cache.set(startKey, out);
    return out;
  } catch {
    return null;
  }
}

module.exports = { findProjectRoot };
