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

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

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) {
    const details = options.stdio === 'pipe'
      ? `\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`
      : '';
    throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}${details}`);
  }
  return result;
}

function parseJsonFromOutput(output, label) {
  const trimmed = output.trim();
  if (!trimmed) throw new Error(`${label} produced no output`);
  const start = trimmed.lastIndexOf('\n{');
  const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed;
  try {
    return JSON.parse(jsonText);
  } catch (error) {
    throw new Error(`${label} did not end with JSON: ${error.message}\n${output}`);
  }
}

function runRuntimeHost(runtimeHostBin, args, options = {}) {
  const command = runtimeHostBin.endsWith('.js') || runtimeHostBin.endsWith('.mjs')
    ? process.execPath
    : runtimeHostBin;
  const prefix = command === process.execPath ? [runtimeHostBin] : [];
  return run(command, [...prefix, '--workspace-source', ...args], options);
}

async function findTarball(tarballDir, packageName) {
  const packageDir = path.join(workspaceDir, 'packages', 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}`);
  return path.join(tarballDir, tarball);
}

async function getFreePort() {
  return new Promise((resolvePort, reject) => {
    const server = createServer();
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      const address = server.address();
      if (!address || typeof address === 'string') {
        server.close(() => reject(new Error('Unable to allocate a local port')));
        return;
      }
      const { port } = address;
      server.close(() => resolvePort(port));
    });
  });
}

function envString(...names) {
  for (const name of names) {
    const value = process.env[name];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return undefined;
}

function resolveLiveKeyConfig() {
  const key = envString('MATRIX_API_KEY', 'HIVECAST_TEST_KEY', 'HIVECAST_API_KEY');
  const cloud = envString('MATRIX_CLOUD', 'HIVECAST_TEST_CLOUD', 'HIVECAST_CLOUD');
  const space = envString('MATRIX_SPACE', 'HIVECAST_TEST_SPACE', 'HIVECAST_SPACE');
  const provided = [key, cloud, space].filter(Boolean).length;
  if (provided > 0 && provided < 3) {
    throw new Error('Live SDK CLI proof requires MATRIX_API_KEY/MATRIX_CLOUD/MATRIX_SPACE together');
  }
  if (!key || !cloud || !space) return null;
  return { key, cloud: cloud.replace(/\/+$/g, ''), space };
}

async function validateLiveKeyConfig(config) {
  const response = await fetch(`${config.cloud}/api/install/key-info`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${config.key}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ space: config.space }),
  });
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`Live SDK CLI key validation failed (${response.status}): ${text}`);
  }
  const body = text.trim() ? JSON.parse(text) : {};
  if (body?.ok !== true) {
    throw new Error(`Live SDK CLI key validation did not return ok:true: ${text}`);
  }
  return {
    ok: true,
    cloud: config.cloud,
    space: config.space,
    keyId: body.key?.id ?? body.id ?? null,
    fingerprint: body.key?.fingerprint ?? body.fingerprint ?? null,
  };
}

async function findOnlyTarball(dir, label) {
  const entries = (await readdir(dir)).filter((entry) => entry.endsWith('.tgz'));
  if (entries.length !== 1) {
    throw new Error(`${label} expected exactly one packed tarball, found ${entries.length}: ${entries.join(', ')}`);
  }
  return path.join(dir, entries[0]);
}

function shouldCopySdkPath(relativePath) {
  if (!relativePath) return true;
  const parts = relativePath.split(path.sep);
  if (parts.includes('node_modules')) return false;
  if (parts.includes('dist')) return false;
  if (parts.includes('.turbo')) return false;
  const base = parts.at(-1) ?? '';
  if (base === '.tsbuildinfo') return false;
  if (base.endsWith('.tgz')) return false;
  return true;
}

async function createProofWorkspace(proofRoot) {
  if (process.env.MATRIX_SDK_CLI_UAT_IN_PLACE === '1') {
    return {
      workspaceDir: sourceWorkspaceDir,
      copied: false,
    };
  }
  const copiedWorkspace = path.join(proofRoot, 'matrix-sdk-workspace');
  await cp(sourceWorkspaceDir, copiedWorkspace, {
    recursive: true,
    dereference: false,
    filter: (src) => shouldCopySdkPath(path.relative(sourceWorkspaceDir, src)),
  });
  return {
    workspaceDir: copiedWorkspace,
    copied: true,
  };
}

function waitForRunReady(child) {
  return new Promise((resolveReady, reject) => {
    let stdout = '';
    let stderr = '';
    let settled = false;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      reject(new Error(`matrix run did not become ready\nstdout:\n${stdout}\nstderr:\n${stderr}`));
    }, 10_000);

    child.stdout.setEncoding('utf8');
    child.stderr.setEncoding('utf8');
    child.stdout.on('data', (chunk) => {
      stdout += chunk;
      if (!stdout.includes('"ok": true') || !stdout.includes('"command": "run"')) return;
      if (!settled) {
        settled = true;
        clearTimeout(timer);
        resolveReady({ ok: true, command: 'run' });
      }
    });
    child.stderr.on('data', (chunk) => {
      stderr += chunk;
    });
    child.once('exit', (code) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      reject(new Error(`matrix run exited before ready (${code})\nstdout:\n${stdout}\nstderr:\n${stderr}`));
    });
  });
}

async function stopChild(child) {
  if (child.exitCode !== null || child.signalCode !== null) return;
  child.kill('SIGTERM');
  await new Promise((resolveStop) => {
    const timer = setTimeout(() => {
      child.kill('SIGKILL');
      resolveStop();
    }, 3000);
    child.once('exit', () => {
      clearTimeout(timer);
      resolveStop();
    });
  });
}

async function main() {
  const proofRoot = await mkdtemp(path.join(tmpdir(), 'matrix-sdk-cli-uat.'));
  const proofWorkspace = await createProofWorkspace(proofRoot);
  workspaceDir = proofWorkspace.workspaceDir;
  const tarballDir = path.join(proofRoot, 'tarballs');
  const packageTarballDir = path.join(proofRoot, 'package-tarballs');
  const toolingDir = path.join(proofRoot, 'tooling');
  const authorDir = path.join(proofRoot, 'author-package');
  const packageConsumerDir = path.join(proofRoot, 'package-manager-consumer');
  const port = await getFreePort();
  const packagePort = await getFreePort();
  const liveKeyConfig = resolveLiveKeyConfig();
  let liveKeyValidation = null;
  let keep = process.env.MATRIX_SDK_KEEP_CLI_UAT === '1';
  let runner;
  let packageRunner;
  const runtimeHostBin = envString('MATRIX_SDK_RUNTIME_HOST_BIN');
  const managedHostProofEnabled = Boolean(runtimeHostBin);
  let runtimeHostStarted = false;
  let managedHostProof = null;

  try {
    await mkdir(tarballDir, { recursive: true });
    await mkdir(packageTarballDir, { recursive: true });
    await mkdir(toolingDir, { recursive: true });
    await mkdir(authorDir, { recursive: true });
    await mkdir(packageConsumerDir, { recursive: true });

    if (proofWorkspace.copied) {
      run('pnpm', ['install', '--ignore-scripts']);
    }
    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}`])
    );
    const authorOverrides = Object.fromEntries(
      [
        '@open-matrix/contracts',
        '@open-matrix/federation',
        '@open-matrix/omega-core',
        '@open-matrix/core',
        '@open-matrix/browser-host',
        '@open-matrix/browser-kit',
        '@open-matrix/sdk',
      ].map((packageName) => [packageName, tarballDependencies[packageName]])
    );

    await writeFile(path.join(toolingDir, 'package.json'), JSON.stringify({
      name: 'matrix-sdk-cli-uat-tooling',
      version: '0.0.0',
      private: true,
      type: 'module',
      dependencies: {
        '@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
      },
      pnpm: {
        overrides: tarballDependencies,
      },
    }, null, 2));

    run('pnpm', ['install', '--ignore-scripts'], { cwd: toolingDir });
    run('pnpm', ['exec', 'matrix', 'init', authorDir, '--name', 'fresh-sdk-author'], { cwd: toolingDir });

    const authorPackagePath = path.join(authorDir, 'package.json');
    const authorPackage = JSON.parse(await readFile(authorPackagePath, 'utf8'));
    authorPackage.dependencies['@open-matrix/sdk'] = tarballDependencies['@open-matrix/sdk'];
    authorPackage.pnpm = { overrides: authorOverrides };
    await writeFile(authorPackagePath, `${JSON.stringify(authorPackage, null, 2)}\n`);

    run('pnpm', ['exec', 'matrix', 'add', 'actor', 'greeter', '--dir', authorDir, '--mount', 'demo.greeter'], { cwd: toolingDir });
    if (liveKeyConfig) {
      liveKeyValidation = await validateLiveKeyConfig(liveKeyConfig);
    }
    const configureKey = liveKeyConfig?.key ?? 'matrix_sdk_test_key_redacted';
    const configureCloud = liveKeyConfig?.cloud ?? 'https://example.invalid';
    const configureSpace = liveKeyConfig?.space ?? 'fresh-sdk-space';

    const configureArgs = [
      'exec', 'matrix', 'configure',
      '--dir', authorDir,
      '--key', configureKey,
      '--cloud', configureCloud,
      '--space', configureSpace,
    ];
    if (runtimeHostBin) {
      configureArgs.push(
        '--runtime-host-bin', runtimeHostBin,
        '--runtime-host-routing', 'workspace-source',
      );
    }
    run('pnpm', configureArgs, { cwd: toolingDir });
    run('pnpm', ['install', '--ignore-scripts'], { cwd: authorDir });
    run('pnpm', ['run', 'build'], { cwd: authorDir });

    runner = spawn('pnpm', [
      'exec', 'matrix', 'run',
      '--dir', authorDir,
      '--port', String(port),
    ], {
      cwd: toolingDir,
      stdio: ['ignore', 'pipe', 'pipe'],
      env: process.env,
    });
    await waitForRunReady(runner);

    const invoke = run('pnpm', [
      'exec', 'matrix', 'invoke',
      'demo.greeter',
      'echo',
      '{"message":"hello-from-sdk-cli"}',
      '--dir', authorDir,
      '--port', String(port),
    ], { cwd: toolingDir, stdio: 'pipe' });
    const invoked = JSON.parse(invoke.stdout.trim());
    if (invoked.ok !== true || invoked.result?.message !== 'hello-from-sdk-cli') {
      throw new Error(`Unexpected matrix invoke output: ${invoke.stdout}`);
    }

    const [freshPackageJson, tsconfig, actorSource] = await Promise.all([
      readFile(path.join(authorDir, 'package.json'), 'utf8'),
      readFile(path.join(authorDir, 'tsconfig.json'), 'utf8'),
      readFile(path.join(authorDir, 'src', 'GreeterActor.ts'), 'utf8'),
    ]);
    const combinedAuthorFiles = `${freshPackageJson}\n${tsconfig}\n${actorSource}`;
    const forbidden = [
      '@matrix/mx-cli',
      '@open-matrix/host-service',
      '@open-matrix/host-control',
      '@open-matrix/system-auth',
      '@open-matrix/system-gateway-http',
      '@open-matrix/system-',
      '@open-matrix/cli',
      'hivecast',
      'projects/matrix-3',
      'matrix-work-harness',
    ];
    const foundForbidden = forbidden.find((needle) => combinedAuthorFiles.includes(needle));
    if (foundForbidden) throw new Error(`Fresh author package contains forbidden dependency/path: ${foundForbidden}`);
    if (!actorSource.includes("from '@open-matrix/sdk'")) {
      throw new Error('Fresh actor source does not import @open-matrix/sdk');
    }
    if (!existsSync(path.join(authorDir, '.matrix', 'run.json'))) {
      throw new Error('matrix run did not write package-local .matrix/run.json');
    }
    await stopChild(runner);
    runner = undefined;

    if (managedHostProofEnabled) {
      if (!runtimeHostBin || !existsSync(runtimeHostBin)) {
        throw new Error(`Managed host matrix command not found for optional managed proof: ${runtimeHostBin}`);
      }
      const up = run('pnpm', [
        'exec', 'matrix', 'up',
        '--dir', authorDir,
        '--http-port', 'auto',
        '--host-nats-port', 'auto',
        '--host-nats-ws-port', 'auto',
        '--runtime-id', 'SDK-UAT-GREETER',
        '--json',
        '--timeout', '45000',
      ], { cwd: toolingDir, stdio: 'pipe' });
      runtimeHostStarted = true;
      const upResult = parseJsonFromOutput(up.stdout, 'matrix up');
      if (upResult.ok !== true || upResult.mode !== 'natural-launch-up') {
        throw new Error(`Unexpected matrix up output: ${up.stdout}`);
      }
      const upService = upResult.services?.find?.((service) => service.runtimeId === 'SDK-UAT-GREETER');
      if (!upService || upService.ok !== true) {
        throw new Error(`matrix up did not start SDK-UAT-GREETER: ${up.stdout}`);
      }
      const managedInvokeOutput = runRuntimeHost(runtimeHostBin, [
        'invoke',
        'demo.greeter',
        'getStatus',
        '{}',
        '--json',
        '--timeout', '10000',
      ], { cwd: authorDir, stdio: 'pipe' });
      const managedInvoke = parseJsonFromOutput(managedInvokeOutput.stdout, 'managed matrix invoke');
      if (managedInvoke.ok !== true || managedInvoke.result?.mount !== 'demo.greeter') {
        throw new Error(`Unexpected managed matrix invoke output: ${managedInvokeOutput.stdout}`);
      }
      managedHostProof = {
        runtimeHostBin,
        up: {
          ok: upResult.ok,
          mode: upResult.mode,
          runtimeId: upService.runtimeId,
          packageDir: upService.packageDir,
        },
        invoke: {
          ok: managedInvoke.ok,
          target: managedInvoke.target,
          result: managedInvoke.result,
        },
      };
    }

    run('pnpm', ['pack', '--pack-destination', packageTarballDir], { cwd: authorDir, stdio: 'pipe' });
    const authorPackageTarball = await findOnlyTarball(packageTarballDir, 'generated author package');
    await writeFile(path.join(packageConsumerDir, 'package.json'), JSON.stringify({
      name: 'matrix-sdk-package-manager-consumer',
      version: '0.0.0',
      private: true,
      type: 'module',
      dependencies: {
        '@open-matrix/cli': tarballDependencies['@open-matrix/cli'],
        [authorPackage.name]: `file:${authorPackageTarball}`,
      },
      pnpm: {
        overrides: tarballDependencies,
      },
    }, null, 2));
    run('pnpm', ['install', '--ignore-scripts'], { cwd: packageConsumerDir });
    const installedPackageDir = path.join(packageConsumerDir, 'node_modules', authorPackage.name);
    const installedPackageJson = JSON.parse(await readFile(path.join(installedPackageDir, 'package.json'), 'utf8'));
    if (installedPackageJson.private === true) {
      throw new Error('Generated author package is still private after package-manager install');
    }
    const installedActorSource = await readFile(path.join(installedPackageDir, 'src', 'GreeterActor.ts'), 'utf8');
    if (!installedActorSource.includes("from '@open-matrix/sdk'")) {
      throw new Error('Package-manager installed source does not keep @open-matrix/sdk import');
    }
    packageRunner = spawn('pnpm', [
      'exec', 'matrix', 'run',
      installedPackageDir,
      '--dir', packageConsumerDir,
      '--port', String(packagePort),
    ], {
      cwd: packageConsumerDir,
      stdio: ['ignore', 'pipe', 'pipe'],
      env: process.env,
    });
    await waitForRunReady(packageRunner);
    const packageInvoke = run('pnpm', [
      'exec', 'matrix', 'invoke',
      'demo.greeter',
      'echo',
      '{"message":"hello-from-package-manager"}',
      '--dir', packageConsumerDir,
      '--port', String(packagePort),
    ], { cwd: packageConsumerDir, stdio: 'pipe' });
    const packageInvoked = JSON.parse(packageInvoke.stdout.trim());
    if (packageInvoked.ok !== true || packageInvoked.result?.message !== 'hello-from-package-manager') {
      throw new Error(`Unexpected package-manager matrix invoke output: ${packageInvoke.stdout}`);
    }
    await stopChild(packageRunner);
    packageRunner = undefined;

    console.log(JSON.stringify({
      ok: true,
      proof: 'sdk-cli-uat',
      sourceWorkspaceDir,
      sdkWorkspaceDir: workspaceDir,
      destructiveBuildIsolated: proofWorkspace.copied,
      toolingDir,
      authorDir,
      cliInstalledSeparately: true,
      authorPackageDependsOnCli: false,
      monorepoPathReferences: false,
      actorImport: '@open-matrix/sdk',
      configuredKeyStored: existsSync(path.join(authorDir, '.matrix', 'credentials', 'matrix-api-key.json')),
      validKeyConfigSupplied: Boolean(liveKeyConfig),
      liveKeyValidation,
      runState: '.matrix/run.json',
      invoke: {
        ok: invoked.ok,
        result: invoked.result,
      },
      managedHostProof: managedHostProof ?? {
        skipped: true,
        reason: 'MATRIX_SDK_RUNTIME_HOST_BIN not supplied',
      },
      packageManagerInstall: {
        tarball: path.basename(authorPackageTarball),
        consumerDir: packageConsumerDir,
        installedPackage: authorPackage.name,
        sourceImportUnchanged: true,
        packagePrivate: installedPackageJson.private === true,
        invoke: {
          ok: packageInvoked.ok,
          result: packageInvoked.result,
        },
      },
    }, null, 2));
  } catch (error) {
    keep = true;
    throw error;
  } finally {
    if (runner) await stopChild(runner);
    if (packageRunner) await stopChild(packageRunner);
    if (runtimeHostStarted && runtimeHostBin) {
      try {
        runRuntimeHost(runtimeHostBin, ['stop', '--timeout', '15000'], { cwd: authorDir, stdio: 'pipe' });
      } catch (error) {
        console.error(`[prove-sdk-cli-uat] failed to stop managed host: ${error instanceof Error ? error.message : String(error)}`);
      }
    }
    if (!keep) {
      await rm(proofRoot, { recursive: true, force: true });
    } else {
      console.error(`[prove-sdk-cli-uat] kept proof directory: ${proofRoot}`);
    }
  }
}

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