#!/usr/bin/env node

import { execFile } from 'node:child_process';
import { once } from 'node:events';
import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import {
  npmSmokePackageJson,
  npmSmokePnpmWorkspaceYaml,
  packageArtifactLayout,
} from './package-artifacts.mjs';

const POSTGRES_IMAGE = process.env.KTX_ARTIFACT_POSTGRES_IMAGE ?? 'postgres:16-alpine';
const POSTGRES_USER = 'ktx';
const POSTGRES_PASSWORD = 'postgres'; // pragma: allowlist secret
const POSTGRES_DB = 'warehouse';

export function smokeContainerName(pid = process.pid, now = Date.now()) {
  return `ktx-live-db-smoke-${pid}-${now}`;
}

export function buildPostgresUrl(hostPort) {
  return `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${hostPort}/${POSTGRES_DB}`; // pragma: allowlist secret
}

export function buildDockerRunArgs({ containerName, hostPort, image = POSTGRES_IMAGE }) {
  return [
    'run',
    '--rm',
    '-d',
    '--name',
    containerName,
    '-e',
    `POSTGRES_PASSWORD=${POSTGRES_PASSWORD}`,
    '-e',
    `POSTGRES_USER=${POSTGRES_USER}`,
    '-e',
    `POSTGRES_DB=${POSTGRES_DB}`,
    '-p',
    `127.0.0.1:${hostPort}:5432`,
    image,
  ];
}

export function buildPostgresReadyArgs(containerName) {
  return [
    'exec',
    containerName,
    'psql',
    '-U',
    POSTGRES_USER,
    '-d',
    POSTGRES_DB,
    '-v',
    'ON_ERROR_STOP=1',
    '-c',
    'SELECT 1;',
  ];
}

export function buildSeedSql() {
  return [
    'DROP TABLE IF EXISTS orders;',
    'DROP TABLE IF EXISTS customers;',
    'CREATE TABLE customers (',
    '  id integer PRIMARY KEY,',
    '  name text NOT NULL',
    ');',
    "COMMENT ON TABLE customers IS 'Customers captured by the artifact smoke';",
    "COMMENT ON COLUMN customers.name IS 'Customer display name';",
    'CREATE TABLE orders (',
    '  id integer PRIMARY KEY,',
    '  customer_id integer NOT NULL REFERENCES customers(id),',
    '  status text NOT NULL,',
    '  amount integer NOT NULL',
    ');',
    "COMMENT ON TABLE orders IS 'Orders captured by the artifact smoke';",
    "COMMENT ON COLUMN orders.amount IS 'Order amount in cents';",
    "INSERT INTO customers (id, name) VALUES (1, 'Acme'), (2, 'Globex');",
    "INSERT INTO orders (id, customer_id, status, amount) VALUES (10, 1, 'paid', 2000), (11, 2, 'open', 3500);",
    '',
  ].join('\n');
}

export function buildKtxYaml(postgresUrl) {
  return [
    'connections:',
    '  warehouse:',
    '    driver: postgres',
    `    url: "${postgresUrl}"`,
    'storage:',
    '  state: sqlite',
    '  search: sqlite-fts5',
    '',
  ].join('\n');
}

export function buildLiveDatabaseIngestArgs(projectDir, _databaseIntrospectionUrl, connectionId = 'warehouse') {
  return [
    'exec',
    'ktx',
    'ingest',
    connectionId,
    '--project-dir',
    projectDir,
    '--no-input',
  ];
}

async function run(command, args, options = {}) {
  process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
  return new Promise((resolve) => {
    const child = execFile(
      command,
      args,
      {
        cwd: options.cwd,
        env: options.env ?? process.env,
        encoding: 'utf8',
        maxBuffer: 1024 * 1024 * 20,
        timeout: options.timeout ?? 60_000,
      },
      (error, stdout, stderr) => {
        if (stdout) {
          process.stdout.write(stdout);
        }
        if (stderr) {
          process.stderr.write(stderr);
        }
        resolve({
          code: error && typeof error.code === 'number' ? error.code : error ? 1 : 0,
          stdout,
          stderr: stderr || (error instanceof Error ? error.message : ''),
        });
      },
    );
    if (options.input !== undefined) {
      child.stdin?.end(options.input);
    }
  });
}

function requireSuccess(label, result) {
  if (result.code !== 0) {
    throw new Error(
      `${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
    );
  }
}

function requireFailure(label, result) {
  if (result.code === 0) {
    throw new Error(
      `${label} unexpectedly succeeded\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
    );
  }
}

function requireOutput(label, result, pattern) {
  if (!pattern.test(result.stdout)) {
    throw new Error(`${label} output did not match ${pattern}\nstdout:\n${result.stdout}`);
  }
}

async function requireDocker() {
  const result = await run('docker', ['info'], { timeout: 20_000 });
  if (result.code !== 0) {
    throw new Error(
      'Docker is required for the installed live-database artifact smoke. Start Docker and rerun `pnpm run artifacts:live-db-smoke`.',
    );
  }
}

async function getAvailablePort() {
  const server = createServer();
  server.listen(0, '127.0.0.1');
  await once(server, 'listening');
  const address = server.address();
  if (!address || typeof address === 'string') {
    server.close();
    throw new Error('expected TCP server address');
  }
  const port = address.port;
  server.close();
  await once(server, 'close');
  return port;
}

async function startPostgresContainer(containerName, hostPort) {
  await requireDocker();
  const result = await run('docker', buildDockerRunArgs({ containerName, hostPort }), { timeout: 120_000 });
  requireSuccess('docker run postgres', result);
}

async function stopPostgresContainer(containerName) {
  await run('docker', ['rm', '-f', containerName], { timeout: 30_000 });
}

async function waitForPostgres(containerName) {
  const deadline = Date.now() + 60_000;
  while (Date.now() < deadline) {
    const result = await run('docker', buildPostgresReadyArgs(containerName), { timeout: 10_000 });
    if (result.code === 0) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  throw new Error(`Timed out waiting for Postgres container ${containerName}`);
}

async function seedPostgres(containerName) {
  const result = await run(
    'docker',
    ['exec', '-i', containerName, 'psql', '-U', POSTGRES_USER, '-d', POSTGRES_DB, '-v', 'ON_ERROR_STOP=1'],
    { input: buildSeedSql(), timeout: 30_000 },
  );
  requireSuccess('seed postgres catalog', result);
}

function managedRuntimeEnv(cleanInstallDir) {
  return {
    ...process.env,
    KTX_RUNTIME_ROOT: join(cleanInstallDir, 'managed-runtime'),
  };
}

function parseDaemonBaseUrl(stdout) {
  const match = stdout.match(/^url: (http:\/\/127\.0\.0\.1:\d+)$/m);
  if (!match) {
    throw new Error(`Daemon URL was not printed by runtime start:\n${stdout}`);
  }
  return match[1];
}

async function startDaemon(cleanInstallDir) {
  const result = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'start'], {
    cwd: cleanInstallDir,
    env: managedRuntimeEnv(cleanInstallDir),
    timeout: 120_000,
  });
  requireSuccess('ktx admin runtime start', result);
  return parseDaemonBaseUrl(result.stdout);
}

async function stopDaemon(cleanInstallDir) {
  await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop'], {
    cwd: cleanInstallDir,
    env: managedRuntimeEnv(cleanInstallDir),
    timeout: 30_000,
  });
}

async function assertPathExists(path, label) {
  try {
    await access(path);
  } catch {
    throw new Error(`Missing ${label}: ${path}`);
  }
}

async function prepareCleanInstall(layout, cleanInstallDir) {
  await assertPathExists(layout.cliTarball, 'CLI tarball');
  await mkdir(cleanInstallDir, { recursive: true });
  await writeFile(join(cleanInstallDir, 'package.json'), `${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`);
  await writeFile(join(cleanInstallDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
  await run('pnpm', ['install'], { cwd: cleanInstallDir, timeout: 120_000 }).then((result) =>
    requireSuccess('pnpm install clean artifact project', result),
  );
  await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'install', '--yes'], {
    cwd: cleanInstallDir,
    env: managedRuntimeEnv(cleanInstallDir),
    timeout: 120_000,
  }).then((result) => requireSuccess('install managed runtime', result));
}

async function main() {
  const layout = packageArtifactLayout();
  const root = await mkdtemp(join(tmpdir(), 'ktx-live-db-artifact-smoke-'));
  const containerName = smokeContainerName();
  let cleanInstallDir;
  let daemonStarted = false;
  try {
    const postgresPort = await getAvailablePort();
    const postgresUrl = buildPostgresUrl(postgresPort);
    cleanInstallDir = join(root, 'npm-clean-install');
    const projectDir = join(root, 'project');

    await startPostgresContainer(containerName, postgresPort);
    await waitForPostgres(containerName);
    await seedPostgres(containerName);
    await prepareCleanInstall(layout, cleanInstallDir);

    await mkdir(projectDir, { recursive: true });
    const init = await run('pnpm', ['exec', 'ktx', 'init', projectDir, '--name', 'artifact-live-database'], {
      cwd: cleanInstallDir,
      timeout: 30_000,
    });
    requireSuccess('ktx init', init);
    await writeFile(join(projectDir, 'ktx.yaml'), buildKtxYaml(postgresUrl), 'utf8');

    const databaseIntrospectionUrl = await startDaemon(cleanInstallDir);
    daemonStarted = true;

    const ingestRun = await run('pnpm', buildLiveDatabaseIngestArgs(projectDir, databaseIntrospectionUrl), {
      cwd: cleanInstallDir,
      env: managedRuntimeEnv(cleanInstallDir),
      timeout: 120_000,
    });
    // ktx ingest now always builds enriched context and requires a configured
    // model and embeddings. This smoke project has neither, so the database
    // target fails the enrichment-readiness preflight before any work runs.
    // This still exercises the packaged binary, daemon startup, and the live
    // database connection end to end.
    requireFailure('ktx ingest warehouse', ingestRun);
    requireOutput('ktx ingest warehouse', ingestRun, /Ingest finished with partial failures/);
    requireOutput('ktx ingest warehouse', ingestRun, /enrichment is not configured/);

    await assertPathExists(join(projectDir, '.ktx', 'db.sqlite'), 'SQLite local ingest state');
    process.stdout.write('Installed live-database artifact smoke passed: enrichment-readiness guard verified\n');
  } finally {
    if (daemonStarted && cleanInstallDir) {
      await stopDaemon(cleanInstallDir);
    }
    await stopPostgresContainer(containerName);
    await rm(root, { recursive: true, force: true });
  }
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
  try {
    await main();
  } catch (error) {
    process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
    process.exitCode = 1;
  }
}
