// wall-e/tools/formatter.js
// Ported from OpenCode (https://github.com/anomalyco/opencode)
// Source: packages/opencode/src/format/formatter.ts + packages/opencode/src/format/index.ts
// Smart formatter detection: checks binary availability AND project configuration.
'use strict';

const path = require('node:path');
const fs = require('node:fs');
const { execFile, execFileSync } = require('node:child_process');
const { promisify } = require('node:util');
const execFileAsync = promisify(execFile);

/**
 * Check if a binary is on PATH.
 * @param {string} name
 * @returns {string|null}
 */
function which(name) {
  try {
    const cmd = process.platform === 'win32' ? 'where' : 'which';
    return execFileSync(cmd, [name], { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
  } catch { return null; }
}

/**
 * Walk up from startDir to stopDir looking for a file.
 * @param {string} filename
 * @param {string} startDir
 * @param {string} [stopDir]
 * @returns {string|null} Full path to the found file, or null
 */
function findUp(filename, startDir, stopDir) {
  let dir = path.resolve(startDir);
  const stop = stopDir ? path.resolve(stopDir) : path.parse(dir).root;
  while (true) {
    const candidate = path.join(dir, filename);
    try {
      fs.statSync(candidate);
      return candidate;
    } catch { /* not found */ }
    const parent = path.dirname(dir);
    if (dir === parent || !dir.startsWith(stop)) return null;
    dir = parent;
  }
}

/**
 * Read a JSON file, returns parsed or null.
 */
function readJson(filePath) {
  try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
  catch { return null; }
}

/**
 * Read a text file, returns string or ''.
 */
function readText(filePath) {
  try { return fs.readFileSync(filePath, 'utf8'); }
  catch { return ''; }
}

// -- Formatter definitions --
// Each has: name, extensions, enabled(cwd) -> string[]|false
// enabled() checks BOTH binary existence AND project usage.

const FORMATTERS = [
  {
    name: 'prettier',
    extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts',
                 '.html', '.htm', '.css', '.scss', '.sass', '.less', '.vue', '.svelte',
                 '.json', '.jsonc', '.yaml', '.yml', '.toml', '.xml', '.md', '.mdx',
                 '.graphql', '.gql'],
    async enabled(cwd) {
      const pkgPath = findUp('package.json', cwd);
      if (!pkgPath) return false;
      const pkg = readJson(pkgPath);
      if (!pkg) return false;
      if (!pkg.dependencies?.prettier && !pkg.devDependencies?.prettier) return false;
      const bin = which('prettier');
      return bin ? [bin, '--write', '$FILE'] : false;
    },
  },
  // Ported from OpenCode formatter.ts — oxfmt (experimental JS/TS formatter)
  {
    name: 'oxfmt',
    extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'],
    async enabled(cwd) {
      const pkgPath = findUp('package.json', cwd);
      if (!pkgPath) return false;
      const pkg = readJson(pkgPath);
      if (!pkg) return false;
      if (!pkg.dependencies?.oxfmt && !pkg.devDependencies?.oxfmt) return false;
      const bin = which('oxfmt');
      return bin ? [bin, '$FILE'] : false;
    },
  },
  {
    name: 'biome',
    extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts',
                 '.html', '.htm', '.css', '.scss', '.sass', '.less', '.vue', '.svelte',
                 '.json', '.jsonc', '.yaml', '.yml', '.toml', '.xml', '.md', '.mdx',
                 '.graphql', '.gql'],
    async enabled(cwd) {
      if (!findUp('biome.json', cwd) && !findUp('biome.jsonc', cwd)) return false;
      const bin = which('biome');
      return bin ? [bin, 'format', '--write', '$FILE'] : false;
    },
  },
  {
    name: 'ruff',
    extensions: ['.py', '.pyi'],
    async enabled(cwd) {
      if (!which('ruff')) return false;
      // Check for ruff config
      for (const config of ['ruff.toml', '.ruff.toml']) {
        if (findUp(config, cwd)) return ['ruff', 'format', '$FILE'];
      }
      const pyproject = findUp('pyproject.toml', cwd);
      if (pyproject && readText(pyproject).includes('[tool.ruff]'))
        return ['ruff', 'format', '$FILE'];
      // Check if ruff is in deps
      for (const dep of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
        const found = findUp(dep, cwd);
        if (found && readText(found).includes('ruff')) return ['ruff', 'format', '$FILE'];
      }
      return false;
    },
  },
  // Ported from OpenCode formatter.ts — validates uv format subcommand exists
  {
    name: 'uv',
    extensions: ['.py', '.pyi'],
    async enabled(cwd) {
      // Only use uv format if ruff is not available
      const ruffFmt = FORMATTERS.find(f => f.name === 'ruff');
      if (ruffFmt && await ruffFmt.enabled(cwd)) return false;
      const uv = which('uv');
      if (!uv) return false;
      // Verify uv supports the format subcommand
      try {
        execFileSync(uv, ['format', '--help'], { encoding: 'utf8', timeout: 5000 });
        return [uv, 'format', '--', '$FILE'];
      } catch { return false; }
    },
  },
  {
    name: 'gofmt',
    extensions: ['.go'],
    async enabled() {
      const bin = which('gofmt');
      return bin ? [bin, '-w', '$FILE'] : false;
    },
  },
  {
    name: 'rustfmt',
    extensions: ['.rs'],
    async enabled() {
      const bin = which('rustfmt');
      return bin ? [bin, '$FILE'] : false;
    },
  },
  {
    name: 'shfmt',
    extensions: ['.sh', '.bash'],
    async enabled() {
      const bin = which('shfmt');
      return bin ? [bin, '-w', '$FILE'] : false;
    },
  },
  {
    name: 'clang-format',
    // Ported from OpenCode formatter.ts — added .ino, .C, .H extensions
    extensions: ['.c', '.cc', '.cpp', '.cxx', '.c++', '.h', '.hh', '.hpp', '.hxx', '.h++', '.ino', '.C', '.H'],
    async enabled(cwd) {
      if (!findUp('.clang-format', cwd)) return false;
      const bin = which('clang-format');
      return bin ? [bin, '-i', '$FILE'] : false;
    },
  },
  {
    name: 'ktlint',
    extensions: ['.kt', '.kts'],
    async enabled() {
      const bin = which('ktlint');
      return bin ? [bin, '-F', '$FILE'] : false;
    },
  },
  {
    name: 'mix',
    extensions: ['.ex', '.exs', '.eex', '.heex', '.leex', '.neex', '.sface'],
    async enabled() {
      const bin = which('mix');
      return bin ? [bin, 'format', '$FILE'] : false;
    },
  },
  {
    name: 'zig',
    extensions: ['.zig', '.zon'],
    async enabled() {
      const bin = which('zig');
      return bin ? [bin, 'fmt', '$FILE'] : false;
    },
  },
  {
    name: 'dart',
    extensions: ['.dart'],
    async enabled() {
      const bin = which('dart');
      return bin ? [bin, 'format', '$FILE'] : false;
    },
  },
  {
    name: 'ocamlformat',
    extensions: ['.ml', '.mli'],
    async enabled(cwd) {
      if (!which('ocamlformat')) return false;
      if (!findUp('.ocamlformat', cwd)) return false;
      return ['ocamlformat', '-i', '$FILE'];
    },
  },
  {
    name: 'terraform',
    extensions: ['.tf', '.tfvars'],
    async enabled() {
      const bin = which('terraform');
      return bin ? [bin, 'fmt', '$FILE'] : false;
    },
  },
  {
    name: 'latexindent',
    extensions: ['.tex'],
    async enabled() {
      const bin = which('latexindent');
      return bin ? [bin, '-w', '-s', '$FILE'] : false;
    },
  },
  {
    name: 'gleam',
    extensions: ['.gleam'],
    async enabled() {
      const bin = which('gleam');
      return bin ? [bin, 'format', '$FILE'] : false;
    },
  },
  {
    name: 'nixfmt',
    extensions: ['.nix'],
    async enabled() {
      const bin = which('nixfmt');
      return bin ? [bin, '$FILE'] : false;
    },
  },
  {
    name: 'pint',
    extensions: ['.php'],
    async enabled(cwd) {
      const composerPath = findUp('composer.json', cwd);
      if (!composerPath) return false;
      const composer = readJson(composerPath);
      if (!composer) return false;
      if (composer.require?.['laravel/pint'] || composer['require-dev']?.['laravel/pint'])
        return ['./vendor/bin/pint', '$FILE'];
      return false;
    },
  },
  {
    name: 'ormolu',
    extensions: ['.hs'],
    async enabled() {
      const bin = which('ormolu');
      return bin ? [bin, '-i', '$FILE'] : false;
    },
  },
  {
    name: 'rubocop',
    extensions: ['.rb', '.rake', '.gemspec', '.ru'],
    async enabled() {
      const bin = which('rubocop');
      return bin ? [bin, '--autocorrect', '$FILE'] : false;
    },
  },
  {
    name: 'standardrb',
    extensions: ['.rb', '.rake', '.gemspec', '.ru'],
    async enabled() {
      const bin = which('standardrb');
      return bin ? [bin, '--fix', '$FILE'] : false;
    },
  },
  {
    name: 'htmlbeautifier',
    extensions: ['.erb', '.html.erb'],
    async enabled() {
      const bin = which('htmlbeautifier');
      return bin ? [bin, '$FILE'] : false;
    },
  },
  // Ported from OpenCode formatter.ts — validates air is the R formatter (not another 'air' binary)
  {
    name: 'air',
    extensions: ['.R'],
    async enabled() {
      const bin = which('air');
      if (!bin) return false;
      // Verify it's the R language formatter, not some other 'air' binary
      try {
        const output = execFileSync(bin, ['--help'], { encoding: 'utf8', timeout: 5000 });
        const firstLine = output.split('\n')[0];
        if (firstLine.includes('R language') && firstLine.includes('formatter'))
          return [bin, 'format', '$FILE'];
      } catch { /* not the right air */ }
      return false;
    },
  },
  {
    name: 'cljfmt',
    extensions: ['.clj', '.cljs', '.cljc', '.edn'],
    async enabled() {
      const bin = which('cljfmt');
      return bin ? [bin, 'fix', '--quiet', '$FILE'] : false;
    },
  },
  {
    name: 'dfmt',
    extensions: ['.d'],
    async enabled() {
      const bin = which('dfmt');
      return bin ? [bin, '-i', '$FILE'] : false;
    },
  },
];

/**
 * Formatter service -- detects and runs project-appropriate formatters.
 * Caches enabled() results per session (don't re-check config every edit).
 */
class Formatter {
  /**
   * @param {object} opts
   * @param {string} opts.cwd - Project working directory
   */
  constructor(opts = {}) {
    this.cwd = opts.cwd || process.cwd();
    this._cache = new Map(); // formatter.name -> string[]|false
  }

  /**
   * Get the enabled formatter command for an extension.
   * @param {string} ext - File extension (e.g., '.js')
   * @returns {Promise<{ name: string, cmd: string[] }|null>}
   */
  async getFormatter(ext) {
    const matching = FORMATTERS.filter(f => f.extensions.includes(ext));
    for (const f of matching) {
      let cmd = this._cache.get(f.name);
      if (cmd === undefined) {
        cmd = await f.enabled(this.cwd);
        this._cache.set(f.name, cmd);
      }
      if (cmd) return { name: f.name, cmd };
    }
    return null;
  }

  /**
   * Format a file using the project's formatter.
   * Failures are swallowed (formatter not installed is not an error).
   *
   * @param {string} filePath - Absolute path to file
   * @returns {Promise<{ formatted: boolean, formatter: string }|null>}
   */
  async formatFile(filePath) {
    const ext = path.extname(filePath);
    const formatter = await this.getFormatter(ext);
    if (!formatter) return null;

    const resolved = formatter.cmd.map(a => a.replace('$FILE', filePath));
    try {
      await execFileAsync(resolved[0], resolved.slice(1), {
        timeout: 10_000,
        cwd: this.cwd,
      });
      return { formatted: true, formatter: formatter.name };
    } catch {
      // Formatter failed -- swallow (not installed, config error, etc.)
      return null;
    }
  }

  /** Clear the enabled() cache (for testing). */
  clearCache() {
    this._cache.clear();
  }
}

module.exports = { FORMATTERS, Formatter, findUp, which };
