import { spawn, spawnSync } from 'node:child_process';
import { createServer, createConnection } from 'node:net';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveNatsServer } from '../../scripts/lib/nats-server-tool.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..');
const demoDir = __dirname;
const matrixCli = resolve(repoRoot, 'packages', 'cli', 'dist', 'index.js');

function run(command, args, options = {}) {
  const result = spawnSync(command, args, {
    cwd: options.cwd ?? repoRoot,
    stdio: options.stdio ?? 'inherit',
    env: process.env,
    encoding: 'utf8',
  });
  if (result.error) throw result.error;
  if (result.status !== 0) {
    throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
  }
  return result;
}

async function freePort() {
  return await new Promise((resolvePort, reject) => {
    const server = createServer();
    server.on('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      if (!address || typeof address === 'string') {
        server.close(() => reject(new Error('Could not allocate demo port')));
        return;
      }
      const { port } = address;
      server.close(() => resolvePort(port));
    });
  });
}

async function waitForTcp(port, child) {
  const deadline = Date.now() + 10_000;
  let lastError;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) throw new Error(`nats-server exited early with ${child.exitCode}`);
    try {
      await new Promise((resolveConnect, rejectConnect) => {
        const socket = createConnection({ host: '127.0.0.1', port });
        socket.once('connect', () => {
          socket.end();
          resolveConnect();
        });
        socket.once('error', rejectConnect);
      });
      return;
    } catch (error) {
      lastError = error;
    }
    await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
  }
  throw lastError ?? new Error('Timed out waiting for nats-server');
}

function stop(child) {
  return new Promise((resolveStop) => {
    if (child.exitCode !== null) {
      resolveStop();
      return;
    }
    child.once('exit', () => resolveStop());
    child.kill('SIGTERM');
    setTimeout(() => {
      if (child.exitCode === null) child.kill('SIGKILL');
    }, 3000).unref();
  });
}

const port = await freePort();
let natsServer;

try {
  run('pnpm', ['run', 'build']);
  run('pnpm', ['--filter', 'matrix-sdk-demo-standalone-greeter', 'run', 'build']);

  const nats = resolveNatsServer(repoRoot);
  if (!nats) {
    throw new Error([
      'nats-server is required for pnpm demo:standalone.',
      'Run pnpm setup:nats, install NATS Server on PATH, or set MATRIX_NATS_SERVER=/path/to/nats-server.',
    ].join(' '));
  }

  natsServer = spawn(nats.path, ['-p', String(port), '-a', '127.0.0.1'], {
    cwd: repoRoot,
    stdio: ['ignore', 'pipe', 'pipe'],
    env: process.env,
  });
  natsServer.stderr.on('data', (chunk) => process.stderr.write(chunk));
  await waitForTcp(port, natsServer);

  const invoked = run(process.execPath, [
    matrixCli,
    'package',
    'run',
    demoDir,
    '--nats-url',
    `nats://127.0.0.1:${port}`,
    '--root',
    'demo',
    '--check-op',
    'echo',
    '--check-payload',
    '{"message":"hello-from-standalone-demo"}',
    '--json',
  ], { cwd: repoRoot, stdio: 'pipe' });
  const body = JSON.parse(invoked.stdout.trim());
  if (body?.ok !== true || body?.check?.result?.message !== 'hello-from-standalone-demo') {
    throw new Error(`Unexpected package run result: ${invoked.stdout}`);
  }

  console.log(JSON.stringify({
    ok: true,
    demo: 'standalone-greeter',
    mode: body.mode,
    mount: body.mount,
    root: body.root,
    natsUrl: body.natsUrl,
    natsServer: {
      source: nats.source,
      path: nats.path,
    },
    authMode: body.authMode,
    check: body.check,
  }, null, 2));
} finally {
  if (natsServer) await stop(natsServer);
}
