// wall-e/tools/project-detector.js
// Project detection for coding agent context.
// Inspired by OpenCode's Project service and VCS detection
// (https://github.com/anomalyco/opencode, packages/opencode/src/project/project.ts,
//  packages/opencode/src/project/vcs.ts)
//
// Detects project type, language, framework, package manager, and VCS
// by scanning marker files from the working directory upward.
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');

const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes

/** @type {Map<string, {result: ProjectInfo, expiresAt: number}>} */
const _cache = new Map();

/**
 * @typedef {object} ProjectInfo
 * @property {string} type - project type: 'node'|'go'|'rust'|'python'|'unknown'
 * @property {string} root - project root directory
 * @property {'git'|null} vcs - version control system
 * @property {string|null} packageManager - 'npm'|'yarn'|'pnpm'|'bun'|'pip'|'poetry'|'cargo'|null
 * @property {string|null} language - primary language
 * @property {string|null} framework - detected framework (next, react, vue, etc.)
 */

/**
 * Marker files and what they indicate.
 * Checked in order — first match determines project type.
 * Inspired by OpenCode Project.fromDirectory() which searches up for .git,
 * then inspects the worktree.
 */
const MARKERS = [
  { file: 'package.json', type: 'node', language: 'javascript' },
  { file: 'go.mod', type: 'go', language: 'go', packageManager: 'go' },
  { file: 'Cargo.toml', type: 'rust', language: 'rust', packageManager: 'cargo' },
  { file: 'pyproject.toml', type: 'python', language: 'python' },
  { file: 'requirements.txt', type: 'python', language: 'python', packageManager: 'pip' },
  { file: 'setup.py', type: 'python', language: 'python', packageManager: 'pip' },
  { file: 'Gemfile', type: 'ruby', language: 'ruby', packageManager: 'bundler' },
  { file: 'build.gradle', type: 'java', language: 'java', packageManager: 'gradle' },
  { file: 'pom.xml', type: 'java', language: 'java', packageManager: 'maven' },
];

/**
 * Node.js package manager detection by lockfile.
 */
const NODE_LOCKFILES = {
  'pnpm-lock.yaml': 'pnpm',
  'yarn.lock': 'yarn',
  'bun.lockb': 'bun',
  'bun.lock': 'bun',
  'package-lock.json': 'npm',
};

/**
 * Framework detection from package.json dependencies.
 */
const FRAMEWORK_DEPS = {
  'next': 'next',
  'nuxt': 'nuxt',
  'gatsby': 'gatsby',
  '@remix-run/react': 'remix',
  'react': 'react',
  'vue': 'vue',
  'svelte': 'svelte',
  '@angular/core': 'angular',
  'express': 'express',
  'fastify': 'fastify',
  'hono': 'hono',
  'electron': 'electron',
};

/**
 * Python package manager / framework detection from pyproject.toml.
 */
const PYTHON_TOOLS = {
  'poetry': 'poetry',
  'pdm': 'pdm',
  'hatch': 'hatch',
};

/**
 * Search upward from startDir for a file or directory.
 * Similar to OpenCode's AppFileSystem.up().
 *
 * @param {string} startDir
 * @param {string} target - filename to look for
 * @returns {string|null} directory containing the target, or null
 */
function findUp(startDir, target) {
  let dir = path.resolve(startDir);
  const root = path.parse(dir).root;
  while (true) {
    if (fs.existsSync(path.join(dir, target))) return dir;
    const parent = path.dirname(dir);
    if (parent === dir || dir === root) return null;
    dir = parent;
  }
}

/**
 * Detect VCS — currently only git.
 * Inspired by OpenCode Project.fromDirectory() git detection.
 *
 * @param {string} dir
 * @returns {{ vcs: 'git'|null, root: string|null, branch: string|null }}
 */
function detectVcs(dir) {
  const gitDir = findUp(dir, '.git');
  if (!gitDir) return { vcs: null, root: null, branch: null };

  let branch = null;
  try {
    branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
      cwd: gitDir, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
    }).trim() || null;
  } catch { /* git not available or not a repo */ }

  return { vcs: 'git', root: gitDir, branch };
}

/**
 * Detect Node.js framework from package.json.
 *
 * @param {string} projectRoot
 * @returns {{ framework: string|null, language: string, packageManager: string }}
 */
function detectNodeDetails(projectRoot) {
  let framework = null;
  let language = 'javascript';
  let packageManager = 'npm';

  // Lockfile-based package manager detection
  for (const [lockfile, pm] of Object.entries(NODE_LOCKFILES)) {
    if (fs.existsSync(path.join(projectRoot, lockfile))) {
      packageManager = pm;
      break;
    }
  }

  // TypeScript detection
  if (fs.existsSync(path.join(projectRoot, 'tsconfig.json'))) {
    language = 'typescript';
  }

  // Framework detection from package.json deps
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
    const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
    // Check in order — more specific frameworks first
    for (const [dep, fw] of Object.entries(FRAMEWORK_DEPS)) {
      if (allDeps[dep]) {
        framework = fw;
        break;
      }
    }
  } catch { /* malformed package.json */ }

  return { framework, language, packageManager };
}

/**
 * Detect Python project details from pyproject.toml.
 *
 * @param {string} projectRoot
 * @returns {{ packageManager: string|null, framework: string|null }}
 */
function detectPythonDetails(projectRoot) {
  let packageManager = 'pip';
  let framework = null;

  try {
    const content = fs.readFileSync(path.join(projectRoot, 'pyproject.toml'), 'utf8');
    for (const [tool, pm] of Object.entries(PYTHON_TOOLS)) {
      if (content.includes(`[tool.${tool}]`)) {
        packageManager = pm;
        break;
      }
    }
    // Basic framework detection
    if (content.includes('django')) framework = 'django';
    else if (content.includes('fastapi')) framework = 'fastapi';
    else if (content.includes('flask')) framework = 'flask';
  } catch { /* no pyproject.toml or unreadable */ }

  return { packageManager, framework };
}

/**
 * Detect project information from a working directory.
 * Searches upward for marker files and extracts metadata.
 *
 * Inspired by OpenCode Project.fromDirectory() which resolves
 * project identity from .git + worktree, and Vcs service which
 * detects branch/default branch.
 *
 * @param {string} cwd - working directory to start detection from
 * @param {object} [opts]
 * @param {boolean} [opts.noCache] - skip cache
 * @returns {ProjectInfo}
 */
function detectProject(cwd, opts = {}) {
  const resolved = path.resolve(cwd);

  // Check cache
  if (!opts.noCache) {
    const cached = _cache.get(resolved);
    if (cached && cached.expiresAt > Date.now()) return cached.result;
  }

  // Detect VCS first (often the broadest indicator of project root)
  const vcsInfo = detectVcs(resolved);

  // Search for project type markers
  let projectType = 'unknown';
  let projectRoot = vcsInfo.root || resolved;
  let language = null;
  let packageManager = null;
  let framework = null;

  for (const marker of MARKERS) {
    const found = findUp(resolved, marker.file);
    if (found) {
      projectType = marker.type;
      projectRoot = found;
      language = marker.language || null;
      packageManager = marker.packageManager || null;
      break;
    }
  }

  // Type-specific detail detection
  if (projectType === 'node') {
    const details = detectNodeDetails(projectRoot);
    framework = details.framework;
    language = details.language;
    packageManager = details.packageManager;
  } else if (projectType === 'python') {
    const details = detectPythonDetails(projectRoot);
    if (details.packageManager) packageManager = details.packageManager;
    if (details.framework) framework = details.framework;
  }

  /** @type {ProjectInfo} */
  const result = {
    type: projectType,
    root: projectRoot,
    vcs: vcsInfo.vcs,
    packageManager,
    language,
    framework,
  };

  // Cache result
  _cache.set(resolved, { result, expiresAt: Date.now() + CACHE_TTL_MS });

  return result;
}

/**
 * Clear the detection cache (useful for testing or after project changes).
 */
function clearCache() {
  _cache.clear();
}

/**
 * Get cache size (for testing).
 * @returns {number}
 */
function cacheSize() {
  return _cache.size;
}

module.exports = {
  detectProject,
  clearCache,
  cacheSize,
  // Exported for testing
  findUp,
  detectVcs,
  detectNodeDetails,
  detectPythonDetails,
  MARKERS,
  CACHE_TTL_MS,
};
