import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import type { GeneratorSpec } from '@bmm/types';

const here = path.dirname(fileURLToPath(import.meta.url));
// apps/generator/src/lib -> repo root
const REPO_ROOT = path.resolve(here, '..', '..', '..', '..');
const RUNNER_TEMPLATE_DIR = path.resolve(REPO_ROOT, 'apps', 'runner-template');
const BUILD_CONTEXT_ROOT = path.resolve(REPO_ROOT, 'build-context');

export interface BuildContext {
  contextDir: string;
  imageTag: string;
}

export async function prepareBuildContext(
  serverId: string,
  version: number,
  slug: string,
  generatedCode: string,
  spec: GeneratorSpec,
): Promise<BuildContext> {
  const contextDir = path.join(BUILD_CONTEXT_ROOT, `${slug}-v${version}-${serverId.slice(0, 8)}`);
  await fs.rm(contextDir, { recursive: true, force: true });
  await fs.mkdir(contextDir, { recursive: true });

  // Copy runner-template files into context
  await copyDir(RUNNER_TEMPLATE_DIR, contextDir);

  // Overwrite the server.ts with generated code
  await fs.mkdir(path.join(contextDir, 'src'), { recursive: true });
  await fs.writeFile(path.join(contextDir, 'src', 'server.ts'), generatedCode, 'utf8');

  // Merge generated deps into package.json
  const pkgPath = path.join(contextDir, 'package.json');
  const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
  pkg.dependencies = { ...pkg.dependencies, ...spec.dependencies };
  await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');

  // Include serverId in the tag: `slug` is unique only per-org, so two orgs
  // sharing a slug at the same version would otherwise collide on one global
  // image tag and run each other's code. Matches the contextDir scheme. (GEN-009)
  const imageTag = `bmm-mcp-${serverId.slice(0, 8)}-${slug}:v${version}`;
  return { contextDir, imageTag };
}

async function copyDir(src: string, dest: string): Promise<void> {
  const entries = await fs.readdir(src, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.name === 'node_modules' || entry.name === 'dist') continue;
    const s = path.join(src, entry.name);
    const d = path.join(dest, entry.name);
    if (entry.isDirectory()) {
      await fs.mkdir(d, { recursive: true });
      await copyDir(s, d);
    } else {
      await fs.copyFile(s, d);
    }
  }
}

export async function staticCheck(contextDir: string): Promise<void> {
  // Skip in CI without a tsc binary; we trust the renderer + spec validation in dev.
  // A future iteration will run `pnpm --filter ./build-context/... typecheck` here.
  const code = await fs.readFile(path.join(contextDir, 'src', 'server.ts'), 'utf8');
  if (code.includes('eval(') || code.includes('new Function(')) {
    throw new Error('static_check_banned_token');
  }
  if (!code.includes("StreamableHTTPServerTransport") || !code.includes("McpServer")) {
    throw new Error('static_check_missing_mcp_primitives');
  }
}

// A hung `docker build` (stalled npm install, wedged daemon) must not pin a
// worker slot forever — concurrency is 2, so two stuck builds = zero throughput
// with no alarm. Kill and fail the build past this ceiling. (GEN-008)
const BUILD_TIMEOUT_MS = 10 * 60 * 1000;

export async function dockerBuild(contextDir: string, imageTag: string, onLog: (msg: string) => void): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    const child = spawn('docker', ['build', '-t', imageTag, '.'], {
      cwd: contextDir,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    const timer = setTimeout(() => {
      child.kill('SIGKILL');
      reject(new Error(`docker_build_timeout (exceeded ${BUILD_TIMEOUT_MS / 1000}s)`));
    }, BUILD_TIMEOUT_MS);
    child.stdout.on('data', (d) => {
      for (const line of d.toString().split(/\r?\n/)) {
        if (line.trim()) onLog(line.trim());
      }
    });
    child.stderr.on('data', (d) => {
      for (const line of d.toString().split(/\r?\n/)) {
        if (line.trim()) onLog(line.trim());
      }
    });
    child.on('error', (e) => {
      clearTimeout(timer);
      reject(e);
    });
    child.on('close', (code) => {
      clearTimeout(timer);
      if (code === 0) resolve();
      else reject(new Error(`docker_build_failed (exit ${code})`));
    });
  });
}
