#!/usr/bin/env node
import { mkdtemp, rm, mkdir, writeFile, readdir, readFile, readlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const workspaceDir = path.resolve(scriptDir, '..');

const packages = [
  '@open-matrix/contracts',
  '@open-matrix/federation',
  '@open-matrix/omega-core',
  '@open-matrix/core',
  '@open-matrix/browser-host',
  '@open-matrix/browser-kit',
  '@open-matrix/sdk',
  '@open-matrix/cli',
];

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

async function findTarball(tarballDir, packageName) {
  const packageDir = path.join(workspaceDir, 'packages', packageName.split('/').at(-1) === 'omega-core'
    ? 'omega-core'
    : packageName.split('/').at(-1));
  const packageJson = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
  const expectedPrefix = packageJson.name.replace('@', '').replace('/', '-');
  const entries = await readdir(tarballDir);
  const tarball = entries.find((entry) => entry.startsWith(expectedPrefix) && entry.endsWith('.tgz'));
  if (!tarball) {
    throw new Error(`Packed tarball missing for ${packageName} (${expectedPrefix}-*.tgz)`);
  }
  return path.join(tarballDir, tarball);
}

async function assertNoSymlinkToHarness(dir) {
  const pending = [dir];
  while (pending.length > 0) {
    const current = pending.pop();
    const entries = await readdir(current, { withFileTypes: true });
    for (const entry of entries) {
      const full = path.join(current, entry.name);
      if (entry.isSymbolicLink()) {
        const target = await readlink(full);
        if (target.includes('matrix-work-harness')) {
          throw new Error(`Consumer node_modules symlink points at harness: ${full}`);
        }
        continue;
      }
      if (entry.isDirectory()) {
        if (entry.name === '.pnpm' || entry.name === '.bin') continue;
        pending.push(full);
      }
    }
  }
}

async function main() {
  const proofRoot = await mkdtemp(path.join(tmpdir(), 'matrix-sdk-consumer-proof.'));
  const tarballDir = path.join(proofRoot, 'tarballs');
  const consumerDir = path.join(proofRoot, 'consumer');
  let keep = process.env.MATRIX_SDK_KEEP_CONSUMER_PROOF === '1';

  try {
    await mkdir(tarballDir, { recursive: true });
    await mkdir(path.join(consumerDir, 'src'), { recursive: true });

    run('pnpm', ['run', 'clean']);
    run('pnpm', ['run', 'build']);
    for (const packageName of packages) {
      run('pnpm', ['--filter', packageName, 'pack', '--pack-destination', tarballDir]);
    }

    const tarballs = Object.fromEntries(
      await Promise.all(packages.map(async (packageName) => [packageName, await findTarball(tarballDir, packageName)]))
    );

    const tarballDependencies = Object.fromEntries(
      Object.entries(tarballs).map(([packageName, tarballPath]) => [packageName, `file:${tarballPath}`])
    );

    await writeFile(path.join(consumerDir, 'package.json'), JSON.stringify({
      name: 'matrix-sdk-external-consumer-proof',
      version: '0.0.0',
      private: true,
      type: 'module',
      scripts: {
        build: 'tsc -p tsconfig.json',
        run: 'node dist/index.js',
      },
      dependencies: tarballDependencies,
      devDependencies: {
        '@types/node': '^25.0.10',
        typescript: '^5.7.0',
      },
      pnpm: {
        overrides: tarballDependencies,
      },
    }, null, 2));

    await writeFile(path.join(consumerDir, 'tsconfig.json'), JSON.stringify({
      compilerOptions: {
        target: 'ES2022',
        module: 'NodeNext',
        moduleResolution: 'NodeNext',
        strict: true,
        skipLibCheck: true,
        outDir: 'dist',
      },
      include: ['src/**/*.ts'],
    }, null, 2));

    await writeFile(path.join(consumerDir, 'src', 'browser-actor.ts'), `import { MatrixActorHtmlElement } from '@open-matrix/core';
import type { MatrixAppReadyDetail } from '@open-matrix/browser-host/app-ready';

export class DemoBrowserActor extends MatrixActorHtmlElement {
  static accepts = { 'ui.ping': {} };

  describeReady(detail: MatrixAppReadyDetail): string {
    return detail.appName;
  }
}
`);

    await writeFile(path.join(consumerDir, 'src', 'index.ts'), `import assert from 'node:assert/strict';
import {
  InMemoryBroker,
  InMemoryTransport,
  MatrixActor,
  MatrixRuntime,
  TopicRouter,
} from '@open-matrix/core';
import { MatrixActor as SdkMatrixActor } from '@open-matrix/sdk';

class EchoActor extends MatrixActor {
  static accepts = {
    echo: {},
  };

  onEcho(payload: { message?: string }) {
    return {
      ok: true,
      message: payload.message ?? null,
      source: 'external-sdk-consumer',
    };
  }
}

async function request(transport: InMemoryTransport, mount: string, op: string, payload: unknown) {
  const correlationId = 'consumer-proof-1';
  const replyTo = '$replies.consumer-proof-1';
  const response = new Promise<unknown>((resolve, reject) => {
    const timeout = setTimeout(() => {
      unsubscribe();
      reject(new Error('request timed out'));
    }, 2000);
    const unsubscribe = transport.subscribe(replyTo, (value) => {
      clearTimeout(timeout);
      unsubscribe();
      resolve(value);
    });
  });

  transport.publish(TopicRouter.inbox(mount), {
    op,
    payload,
    replyTo,
    correlationId,
  });

  return response;
}

const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker, { name: 'external-consumer' });
const runtime = new MatrixRuntime({ transport, logging: false });

try {
  assert.equal(SdkMatrixActor, MatrixActor);
  await runtime.create(EchoActor, 'demo.echo');
  const response = await request(transport, 'demo.echo', 'echo', { message: 'hello-sdk' });
  assert.deepEqual(response, {
    ok: true,
    correlationId: 'consumer-proof-1',
    result: {
      ok: true,
      message: 'hello-sdk',
      source: 'external-sdk-consumer',
    },
  });
  console.log(JSON.stringify({ ok: true, mount: 'demo.echo', op: 'echo' }));
} finally {
  await runtime.shutdown();
}
`);

    run('pnpm', ['install', '--ignore-scripts'], { cwd: consumerDir });
    run('pnpm', ['exec', 'matrix', '--help'], { cwd: consumerDir, stdio: 'pipe' });
    run('pnpm', ['run', 'build'], { cwd: consumerDir });
    const runtimeProof = run('pnpm', ['run', 'run'], { cwd: consumerDir, stdio: 'pipe' });
    const stdout = runtimeProof.stdout.trim();
    const parsed = JSON.parse(stdout.split('\n').at(-1));
    if (parsed.ok !== true || parsed.mount !== 'demo.echo' || parsed.op !== 'echo') {
      throw new Error(`Unexpected runtime proof output: ${stdout}`);
    }

    const consumerFiles = [
      await readFile(path.join(consumerDir, 'package.json'), 'utf8'),
      await readFile(path.join(consumerDir, 'tsconfig.json'), 'utf8'),
      await readFile(path.join(consumerDir, 'src', 'index.ts'), 'utf8'),
      await readFile(path.join(consumerDir, 'src', 'browser-actor.ts'), 'utf8'),
    ].join('\n');
    if (consumerFiles.includes('matrix-work-harness') || consumerFiles.includes('projects/matrix-3')) {
      throw new Error('Consumer proof contains a Matrix-3/harness path reference');
    }
    if (existsSync(path.join(consumerDir, 'node_modules'))) {
      await assertNoSymlinkToHarness(path.join(consumerDir, 'node_modules'));
    }

    console.log(JSON.stringify({
      ok: true,
      proof: 'external-consumer',
      consumerDir,
      packages,
      runtime: parsed,
      matrix3PathReferences: false,
    }, null, 2));
  } catch (error) {
    keep = true;
    throw error;
  } finally {
    if (!keep) {
      await rm(proofRoot, { recursive: true, force: true });
    } else {
      console.error(`[prove-external-consumer] kept proof directory: ${proofRoot}`);
    }
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack : error);
  process.exit(1);
});
