// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * NERF Benchmark — Builder (clone, enumerate, build, run, teardown).
 *
 * Uses `docker compose` CLI (not dockerode) for build/start/stop.
 * Uses `git` CLI for clone/pull.
 */

import { execSync, spawnSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync, mkdirSync, statSync } from 'node:fs';
import { join, basename } from 'node:path';
import { BenchmarkConfig } from './models.js';
import { HarnessConfig } from './config.js';

const CONFIG_FILES = ['benchmark.json', 'benchmark-config.json'];
const YAML_CONFIG_FILES = ['benchmark.yaml'];

export function enumerateBenchmarks(benchmarksDir) {
  const configs = [];
  if (!existsSync(benchmarksDir)) return configs;

  for (const entry of readdirSync(benchmarksDir).sort()) {
    const entryPath = join(benchmarksDir, entry);
    if (!statSync(entryPath).isDirectory()) continue;

    let config = null;
    for (const jsonName of CONFIG_FILES) {
      const jsonPath = join(entryPath, jsonName);
      if (existsSync(jsonPath)) {
        try { config = BenchmarkConfig.fromJsonFile(jsonPath); break; }
        catch { /* skip */ }
      }
    }
    if (!config) {
      for (const yamlName of YAML_CONFIG_FILES) {
        const yamlPath = join(entryPath, yamlName);
        if (existsSync(yamlPath)) {
          try { config = BenchmarkConfig.fromYamlFile(yamlPath); break; }
          catch { /* skip */ }
        }
      }
    }
    if (config) configs.push(config);
  }
  return configs;
}

export class BenchmarkBuilder {
  constructor(config) {
    this.config = config || new HarnessConfig();
    this._benchmarks = null;
  }

  clone() {
    const cloneDir = this.config.cloneDir;
    if (existsSync(join(cloneDir, '.git'))) {
      spawnSync('git', ['-C', cloneDir, 'pull', '--ff-only'], { timeout: 120000, stdio: 'pipe' });
    } else {
      mkdirSync(join(cloneDir, '..'), { recursive: true });
      spawnSync('git', ['clone', '--depth=1', this.config.repoUrl, cloneDir], { timeout: 300000, stdio: 'pipe' });
    }
    return cloneDir;
  }

  listBenchmarks(forceRefresh = false) {
    if (this._benchmarks && !forceRefresh) return this._benchmarks;
    const dir = this.config.benchmarksDir;
    if (!existsSync(dir)) throw new Error(`Benchmarks directory not found: ${dir}. Run clone() first.`);
    this._benchmarks = enumerateBenchmarks(dir);
    return this._benchmarks;
  }

  getBenchmark(name) {
    const all = this.listBenchmarks();
    const found = all.find((b) => b.dirName === name);
    if (!found) throw new Error(`Benchmark not found: ${name}`);
    return found;
  }

  _projectName(benchmark) {
    return `nerf-bench-${benchmark.dirName}`.toLowerCase();
  }

  _composeCmd(benchmark, ...args) {
    return ['docker', 'compose', '-p', this._projectName(benchmark), '-f', join(benchmark.path, 'docker-compose.yml'), ...args];
  }

  build(name, flag) {
    const benchmark = this.getBenchmark(name);
    flag = flag || this.config.generateFlag(benchmark.dirName);
    const cacheArgs = this.config.noCache ? ['--no-cache'] : [];
    const cmd = this._composeCmd(benchmark, 'build', '--build-arg', `FLAG=${flag}`, '--build-arg', `flag=${flag}`, ...cacheArgs);
    const result = spawnSync(cmd[0], cmd.slice(1), { timeout: this.config.dockerBuildTimeout * 1000, stdio: 'pipe' });
    if (result.status !== 0) throw new Error(`Failed to build ${name}: ${(result.stderr || '').toString().slice(0, 500)}`);
  }

  start(name) {
    const benchmark = this.getBenchmark(name);
    const cmd = this._composeCmd(benchmark, 'up', '-d', '--wait');
    const result = spawnSync(cmd[0], cmd.slice(1), { timeout: this.config.dockerStartTimeout * 1000, stdio: 'pipe' });
    if (result.status !== 0) throw new Error(`Failed to start ${name}: ${(result.stderr || '').toString().slice(0, 500)}`);
  }

  stop(name) {
    const benchmark = this.getBenchmark(name);
    const cmd = this._composeCmd(benchmark, 'down', '-v', '--remove-orphans');
    spawnSync(cmd[0], cmd.slice(1), { timeout: this.config.dockerStopTimeout * 1000, stdio: 'pipe' });
  }

  getTargetUrl(name) {
    const benchmark = this.getBenchmark(name);
    const cmd = this._composeCmd(benchmark, 'ps', '--format', 'json');
    const result = spawnSync(cmd[0], cmd.slice(1), { timeout: 10000, stdio: 'pipe' });
    if (result.status !== 0) throw new Error(`Failed to get ports for ${name}`);

    for (const line of (result.stdout || '').toString().trim().split('\n')) {
      if (!line.trim()) continue;
      try {
        const container = JSON.parse(line);
        for (const pub of container.Publishers || []) {
          if (pub.PublishedPort > 0 && [80, 443, 8080, 8443, 3000, 5000].includes(pub.TargetPort)) {
            const proto = [443, 8443].includes(pub.TargetPort) ? 'https' : 'http';
            return `${proto}://localhost:${pub.PublishedPort}`;
          }
        }
        for (const pub of container.Publishers || []) {
          if (pub.PublishedPort > 0) return `http://localhost:${pub.PublishedPort}`;
        }
      } catch { /* skip */ }
    }
    throw new Error(`No exposed ports found for ${name}`);
  }

  runBenchmark(name, flag) {
    flag = flag || this.config.generateFlag(name);
    this.build(name, flag);
    this.start(name);
    const targetUrl = this.getTargetUrl(name);
    return { targetUrl, expectedFlag: flag };
  }
}
