#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process';
import net from 'node:net';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { chromium } from 'playwright';

const root = resolve(new URL('..', import.meta.url).pathname);

function run(command, args, options = {}) {
  const result = spawnSync(command, args, {
    cwd: options.cwd ?? root,
    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;
}

function delay(ms) {
  return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}

function freePort() {
  return new Promise((resolvePort, reject) => {
    const server = net.createServer();
    server.on('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      const port = typeof address === 'object' && address ? address.port : 0;
      server.close(() => resolvePort(port));
    });
  });
}

async function waitForHttp(url, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  let lastError = null;
  while (Date.now() < deadline) {
    try {
      const response = await fetch(url);
      if (response.ok) return;
      lastError = new Error(`${url} returned ${response.status}`);
    } catch (error) {
      lastError = error;
    }
    await delay(100);
  }
  throw lastError ?? new Error(`Timed out waiting for ${url}`);
}

function hasHiveCastEnv() {
  return Boolean(process.env.MATRIX_API_KEY && process.env.MATRIX_CLOUD && process.env.MATRIX_SPACE);
}

function chromiumLaunchOptions() {
  const explicit = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE;
  if (explicit) return { headless: true, executablePath: explicit };
  for (const candidate of ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser']) {
    if (existsSync(candidate)) return { headless: true, executablePath: candidate };
  }
  return { headless: true };
}

function assertContains(file, text) {
  const body = readFileSync(resolve(root, file), 'utf8');
  if (!body.includes(text)) {
    throw new Error(`${file} does not contain expected text: ${text}`);
  }
}

function parseLastJsonObject(stdout) {
  const text = stdout.trim();
  for (let i = 0; i < text.length; i += 1) {
    if (text[i] !== '{') continue;
    try {
      return JSON.parse(text.slice(i));
    } catch {
      // Keep scanning later object starts.
    }
  }
  throw new Error(`No JSON proof object found in stdout:\n${stdout}`);
}

async function proveWebLive() {
  const port = await freePort();
  const origin = `http://127.0.0.1:${port}`;
  const vite = spawn('pnpm', [
    '--filter',
    'matrix-sdk-demo-fresh-web-page',
    'exec',
    'vite',
    '--host',
    '127.0.0.1',
    '--port',
    String(port),
    '--strictPort',
  ], {
    cwd: root,
    env: process.env,
    stdio: ['ignore', 'pipe', 'pipe'],
  });
  let stdout = '';
  let stderr = '';
  vite.stdout.on('data', (chunk) => {
    stdout += String(chunk);
  });
  vite.stderr.on('data', (chunk) => {
    stderr += String(chunk);
  });

  try {
    await waitForHttp(origin, 15_000);

    const browser = await chromium.launch(chromiumLaunchOptions());
    const page = await browser.newPage();
    const consoleErrors = [];
    const failedRequests = [];
    page.on('console', (msg) => {
      if (msg.type() === 'error') consoleErrors.push(msg.text());
    });
    page.on('requestfailed', (request) => {
      failedRequests.push({
        url: request.url(),
        failure: request.failure()?.errorText ?? 'unknown',
      });
    });

    try {
      await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 15_000 });
      await page.waitForFunction(() => {
        const text = document.querySelector('pre')?.textContent ?? '';
        return text.includes('"ok": true') && text.includes('fresh-web-page');
      }, { timeout: 20_000 });
      const body = await page.locator('pre').innerText();
      const proof = JSON.parse(body);
      if (proof.ok !== true || proof.demo !== 'fresh-web-page') {
        throw new Error(`Unexpected fresh web page proof: ${body}`);
      }
      if (consoleErrors.length > 0 || failedRequests.length > 0) {
        throw new Error(`Browser proof had errors: ${JSON.stringify({ consoleErrors, failedRequests })}`);
      }
      return {
        ...proof,
        origin,
        consoleErrors,
        failedRequests,
      };
    } finally {
      await browser.close();
    }
  } catch (error) {
    throw new Error([
      error instanceof Error ? error.message : String(error),
      stdout ? `vite stdout:\n${stdout}` : '',
      stderr ? `vite stderr:\n${stderr}` : '',
    ].filter(Boolean).join('\n'));
  } finally {
    vite.kill('SIGTERM');
    await delay(250);
    if (!vite.killed) vite.kill('SIGKILL');
  }
}

run('pnpm', ['--filter', '@open-matrix/core', 'build']);
run('pnpm', ['--filter', '@open-matrix/sdk', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-demo-fresh-server-actor', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-demo-fresh-web-page', 'build']);

assertContains('demos/fresh-server-actor/src/server.ts', "from '@open-matrix/sdk'");
assertContains('demos/fresh-server-actor/src/server.ts', 'exchangeHiveCastActorCredential');
assertContains('demos/fresh-web-page/src/main.ts', "from '@open-matrix/sdk'");
assertContains('demos/fresh-web-page/vite.config.ts', 'exchangeHiveCastActorCredential');
assertContains('demos/fresh-web-page/README.md', 'served by Vite, not by Matrix Web');

const live = hasHiveCastEnv();
let serverLive = null;
let webLive = null;
if (live) {
  const result = run('pnpm', ['--filter', 'matrix-sdk-demo-fresh-server-actor', 'check'], { stdio: 'pipe' });
  serverLive = parseLastJsonObject(result.stdout);
  if (serverLive.ok !== true || serverLive.demo !== 'fresh-server-actor') {
    throw new Error(`Unexpected fresh server actor proof: ${result.stdout}`);
  }
  webLive = await proveWebLive();
}

console.log(JSON.stringify({
  ok: true,
  proof: 'fresh-samples',
  built: [
    'matrix-sdk-demo-fresh-server-actor',
    'matrix-sdk-demo-fresh-web-page',
  ],
  hivecastEnvPresent: live,
  serverLive,
  webLive,
}, null, 2));
