import { execFile } from 'node:child_process';
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

import {
  PUBLIC_NPM_PACKAGE_NAME,
  PUBLIC_NPM_PACKAGE_VERSION,
} from './public-npm-release-metadata.mjs';
import { npmSmokePnpmWorkspaceYaml } from './package-artifacts.mjs';

const execFileAsync = promisify(execFile);
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const DEFAULT_ROOT_DIR = resolve(SCRIPT_DIR, '..');
const PUBLIC_NPM_ARTIFACT_DIR = join('dist', 'artifacts', 'npm');
const OPT_IN_MESSAGE =
  'Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.';

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

export function expectedPublicKtxVersionPattern() {
  return new RegExp(
    `${escapeRegExp(PUBLIC_NPM_PACKAGE_NAME)} ${escapeRegExp(PUBLIC_NPM_PACKAGE_VERSION)}`,
  );
}

export function localEmbeddingsSmokeOptIn(env = process.env, args = process.argv.slice(2)) {
  if (env.KTX_RUN_LOCAL_EMBEDDINGS_SMOKE === '1' || args.includes('--force')) {
    return { run: true };
  }
  return { run: false, message: OPT_IN_MESSAGE };
}

export function publicKtxTarballName(files) {
  const matches = files.filter((file) => /^kaelio-ktx-.+\.tgz$/.test(file)).sort();
  if (matches.length !== 1) {
    throw new Error(
      `Expected exactly one @kaelio/ktx tarball in ${PUBLIC_NPM_ARTIFACT_DIR}, found ${matches.length}: ${
        matches.join(', ') || 'none'
      }. Run pnpm run artifacts:build first.`,
    );
  }
  return matches[0];
}

export async function selectPublicKtxTarball(rootDir = DEFAULT_ROOT_DIR) {
  const npmArtifactDir = join(rootDir, PUBLIC_NPM_ARTIFACT_DIR);
  const files = await readdir(npmArtifactDir);
  return join(npmArtifactDir, publicKtxTarballName(files));
}

export function buildLocalEmbeddingsSmokeEnv(root, baseEnv = process.env) {
  return {
    ...baseEnv,
    KTX_RUN_LOCAL_EMBEDDINGS_SMOKE: '1',
    KTX_RUNTIME_ROOT: join(root, 'managed-runtime'),
    HF_HOME: join(root, 'hf-home'),
    TRANSFORMERS_CACHE: join(root, 'transformers-cache'),
    SENTENCE_TRANSFORMERS_HOME: join(root, 'sentence-transformers-home'),
    TORCH_HOME: join(root, 'torch-home'),
  };
}

export function localEmbeddingsSmokeCommands(input) {
  return [
    {
      label: 'ktx public package version',
      command: 'pnpm',
      args: ['exec', 'ktx', '--version'],
      timeoutMs: 60_000,
    },
    {
      label: 'ktx admin runtime status missing',
      command: 'pnpm',
      args: ['exec', 'ktx', 'admin', 'runtime', 'status', '--json'],
      timeoutMs: 60_000,
    },
    {
      label: 'ktx admin runtime install local embeddings',
      command: 'pnpm',
      args: ['exec', 'ktx', 'admin', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
      timeoutMs: 1_200_000,
    },
    {
      label: 'ktx admin runtime status local embeddings ready',
      command: 'pnpm',
      args: ['exec', 'ktx', 'admin', 'runtime', 'status', '--json'],
      timeoutMs: 60_000,
    },
    {
      label: 'ktx admin runtime start local embeddings',
      command: 'pnpm',
      args: ['exec', 'ktx', 'admin', 'runtime', 'start', '--feature', 'local-embeddings'],
      timeoutMs: 300_000,
    },
    {
      label: 'ktx setup local embeddings',
      command: 'pnpm',
      args: [
        'exec',
        'ktx',
        'setup',
        '--project-dir',
        input.projectDir,
        '--no-input',
        '--yes',
        '--skip-llm',
        '--embedding-backend',
        'sentence-transformers',
        '--skip-databases',
        '--skip-sources',
        '--skip-agents',
      ],
      timeoutMs: 900_000,
    },
    {
      label: 'ktx admin runtime stop local embeddings',
      command: 'pnpm',
      args: ['exec', 'ktx', 'admin', 'runtime', 'stop'],
      timeoutMs: 60_000,
    },
  ];
}

export 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];
}

export function validateEmbeddingResponse(raw, expectedDimensions) {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
    throw new Error('Embedding response must be a JSON object');
  }
  const embedding = raw.embedding;
  if (!Array.isArray(embedding)) {
    throw new Error('Embedding response must include an embedding array');
  }
  if (embedding.length !== expectedDimensions) {
    throw new Error(`Expected embedding dimension ${expectedDimensions}, got ${embedding.length}`);
  }
  for (const [index, value] of embedding.entries()) {
    if (typeof value !== 'number' || !Number.isFinite(value)) {
      throw new Error(`Embedding value at index ${index} is not a finite number`);
    }
  }
}

async function run(command, args, options = {}) {
  process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
  try {
    const result = await execFileAsync(command, args, {
      cwd: options.cwd,
      env: { ...process.env, ...options.env },
      encoding: 'utf8',
      maxBuffer: 1024 * 1024 * 20,
      timeout: options.timeoutMs ?? 120_000,
    });
    if (result.stdout) {
      process.stdout.write(result.stdout);
    }
    if (result.stderr) {
      process.stderr.write(result.stderr);
    }
    return { code: 0, stdout: result.stdout, stderr: result.stderr };
  } catch (error) {
    const stdout = typeof error.stdout === 'string' ? error.stdout : '';
    const stderr = typeof error.stderr === 'string' ? error.stderr : error.message;
    if (stdout) {
      process.stdout.write(stdout);
    }
    if (stderr) {
      process.stderr.write(stderr);
    }
    return {
      code: typeof error.code === 'number' ? error.code : 1,
      stdout,
      stderr,
    };
  }
}

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

function parseJsonStdout(label, result) {
  requireSuccess(label, result);
  try {
    return JSON.parse(result.stdout);
  } catch (error) {
    throw new Error(`${label} did not write JSON stdout: ${error.message}\nstdout:\n${result.stdout}`);
  }
}

function parseJsonStdoutWithExitCode(label, result, expectedCode) {
  if (result.code !== expectedCode) {
    throw new Error(`${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
  }
  try {
    return JSON.parse(result.stdout);
  } catch (error) {
    throw new Error(`${label} did not write JSON stdout: ${error.message}\nstdout:\n${result.stdout}`);
  }
}

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

async function postJson(baseUrl, path, payload, timeoutMs) {
  const response = await fetch(new URL(path, baseUrl), {
    method: 'POST',
    headers: {
      accept: 'application/json',
      'content-type': 'application/json',
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await response.text();
  if (!response.ok) {
    throw new Error(`POST ${path} failed with ${response.status}: ${text}`);
  }
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error(`POST ${path} returned non-JSON response: ${error.message}\n${text}`);
  }
}

async function writeSmokePackage(projectDir, tarballPath) {
  await mkdir(projectDir, { recursive: true });
  await writeFile(
    join(projectDir, 'package.json'),
    `${JSON.stringify(
      {
        name: 'ktx-local-embeddings-runtime-smoke',
        version: '0.0.0',
        private: true,
        type: 'module',
        dependencies: {
          '@kaelio/ktx': `file:${tarballPath}`,
        },
      },
      null,
      2,
    )}\n`,
  );
  await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
}

export async function runLocalEmbeddingsRuntimeSmoke(options = {}) {
  const rootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
  const tarballPath = options.tarballPath ?? (await selectPublicKtxTarball(rootDir));
  const root = await mkdtemp(join(tmpdir(), 'ktx-local-embeddings-smoke-'));
  const keepTemp = options.keepTemp ?? process.env.KTX_KEEP_LOCAL_EMBEDDINGS_SMOKE === '1';
  const installDir = join(root, 'installed-package');
  const projectDir = join(root, 'project');
  const smokeEnv = buildLocalEmbeddingsSmokeEnv(root);
  const commands = localEmbeddingsSmokeCommands({ projectDir });
  let daemonStarted = false;

  try {
    await writeSmokePackage(installDir, tarballPath);
    requireSuccess(
      'pnpm install public package',
      await run('pnpm', ['install', '--ignore-scripts=false'], {
        cwd: installDir,
        env: smokeEnv,
        timeoutMs: 300_000,
      }),
    );

    const version = await run(commands[0].command, commands[0].args, {
      cwd: installDir,
      env: smokeEnv,
      timeoutMs: commands[0].timeoutMs,
    });
    requireSuccess(commands[0].label, version);
    requireOutput(commands[0].label, version, expectedPublicKtxVersionPattern());

    const missingStatus = parseJsonStdoutWithExitCode(
      commands[1].label,
      await run(commands[1].command, commands[1].args, {
        cwd: installDir,
        env: smokeEnv,
        timeoutMs: commands[1].timeoutMs,
      }),
      1,
    );
    if (missingStatus.kind !== 'missing') {
      throw new Error(`Expected missing runtime before install, got ${JSON.stringify(missingStatus)}`);
    }

    const install = await run(commands[2].command, commands[2].args, {
      cwd: installDir,
      env: smokeEnv,
      timeoutMs: commands[2].timeoutMs,
    });
    requireSuccess(commands[2].label, install);
    requireOutput(commands[2].label, install, /Installed KTX Python runtime/);
    requireOutput(commands[2].label, install, /features: core, local-embeddings/);

    const readyStatus = parseJsonStdout(
      commands[3].label,
      await run(commands[3].command, commands[3].args, {
        cwd: installDir,
        env: smokeEnv,
        timeoutMs: commands[3].timeoutMs,
      }),
    );
    if (readyStatus.kind !== 'ready') {
      throw new Error(`Expected ready runtime after install, got ${JSON.stringify(readyStatus)}`);
    }
    if (!readyStatus.manifest?.features?.includes('local-embeddings')) {
      throw new Error(`Runtime manifest did not include local-embeddings: ${JSON.stringify(readyStatus.manifest)}`);
    }

    const start = await run(commands[4].command, commands[4].args, {
      cwd: installDir,
      env: smokeEnv,
      timeoutMs: commands[4].timeoutMs,
    });
    requireSuccess(commands[4].label, start);
    daemonStarted = true;
    const baseUrl = parseDaemonBaseUrl(start.stdout);

    const embeddingResponse = await postJson(
      baseUrl,
      '/embeddings/compute',
      { text: 'KTX local embeddings release smoke' },
      900_000,
    );
    validateEmbeddingResponse(embeddingResponse, 384);
    process.stdout.write('KTX daemon computed a 384-dimensional embedding\n');

    const setup = await run(commands[5].command, commands[5].args, {
      cwd: installDir,
      env: smokeEnv,
      timeoutMs: commands[5].timeoutMs,
    });
    requireSuccess(commands[5].label, setup);
    requireOutput(commands[5].label, setup, /Embeddings ready: yes \(all-MiniLM-L6-v2\)/);

    const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf8');
    if (!/backend:\s*sentence-transformers/.test(config)) {
      throw new Error(`ktx.yaml did not declare sentence-transformers embedding backend:\n${config}`);
    }
    if (/base_url:/.test(config)) {
      throw new Error(`ktx.yaml should omit base_url for managed local embeddings:\n${config}`);
    }
    process.stdout.write('KTX setup persisted managed local embeddings (no base_url)\n');

    const stop = await run(commands[6].command, commands[6].args, {
      cwd: installDir,
      env: smokeEnv,
      timeoutMs: commands[6].timeoutMs,
    });
    requireSuccess(commands[6].label, stop);
    daemonStarted = false;
    requireOutput(commands[6].label, stop, /Stopped KTX daemon/);

    process.stdout.write('KTX local embeddings runtime smoke verified\n');
  } finally {
    if (daemonStarted) {
      await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop'], {
        cwd: installDir,
        env: smokeEnv,
        timeoutMs: 60_000,
      });
    }
    if (!keepTemp) {
      await rm(root, { recursive: true, force: true });
    } else {
      process.stdout.write(`Kept local embeddings smoke root: ${root}\n`);
    }
  }
}

async function main() {
  const args = process.argv.slice(2);
  const optIn = localEmbeddingsSmokeOptIn(process.env, args);
  if (!optIn.run) {
    process.stdout.write(`Skipping KTX local embeddings runtime smoke. ${optIn.message}\n`);
    if (args.includes('--require-opt-in')) {
      process.exitCode = 1;
    }
    return;
  }

  await runLocalEmbeddingsRuntimeSmoke();
}

if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
  main().catch((error) => {
    process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
    process.exitCode = 1;
  });
}
