#!/usr/bin/env node
/**
 * Per-CLI installer. Installs a printed CLI from the Printing Press catalog
 * and records it in the SQLite registry.
 *
 * Usage:
 *   node scripts/printing-press/install-cli.mjs <slug> [--source <kind>] [--source-url <url>] [--cli-only|--skill-only]
 *
 *   <slug>            catalog name (e.g. 'espn', 'flight-goat', 'linear-pp-cli')
 *                     or a bundle name ('starter-pack').
 *   --source <kind>   For documentation only: 'starter-pack' | 'github' | 'local' | 'spec'.
 *                     Only 'starter-pack' / 'catalog' map directly to the npm CLI today;
 *                     github/local/spec are stored as metadata but ultimately call
 *                     `printing-press install <slug>` (the upstream binary picks the source).
 *   --source-url      Stored as metadata.
 *   --cli-only        Pass --cli-only to printing-press (Go binary, skip skill).
 *   --skill-only      Pass --skill-only to printing-press (skill, skip Go binary).
 *
 * After install we read the upstream `printing-press list --json` output to
 * locate the actual install path on disk, then record it in printing_press_clis.
 */

import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';

const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..', '..');
const SUB_NODE_MODULES = path.join(HERE, 'node_modules');
const GENERATED_CLIS_DIR = path.join(ROOT, 'workspace', 'generated-clis');

function log(msg) {
  console.log(`[printing-press:install-cli] ${msg}`);
}

function fail(msg, code = 1) {
  console.error(`[printing-press:install-cli] ${msg}`);
  process.exit(code);
}

function parseArgs(argv) {
  const args = { slug: null, source: 'catalog', source_url: null, cli_only: false, skill_only: false };
  const rest = [];
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--source') args.source = argv[++i];
    else if (a === '--source-url') args.source_url = argv[++i];
    else if (a === '--cli-only') args.cli_only = true;
    else if (a === '--skill-only') args.skill_only = true;
    else rest.push(a);
  }
  args.slug = rest[0] || null;
  return args;
}

const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;

function augmentedPath() {
  const candidates = [
    process.env.GOBIN,
    process.env.GOPATH ? `${process.env.GOPATH}/bin` : null,
    process.env.HOME ? `${process.env.HOME}/go/bin` : null,
  ].filter(Boolean);
  return [...candidates, process.env.PATH || ''].join(':');
}

function run(cmd, args, opts = {}) {
  const env = { ...process.env, PATH: augmentedPath(), ...(opts.env || {}) };
  const result = spawnSync(cmd, args, { stdio: 'inherit', ...opts, env });
  if (result.error) fail(`spawn ${cmd} failed: ${result.error.message}`);
  if (result.status !== 0) fail(`${cmd} ${args.join(' ')} exited ${result.status}`, result.status || 1);
  return result;
}

function readManifestIfPresent(cliPath) {
  const candidates = [
    path.join(cliPath, 'printing-press.json'),
    path.join(cliPath, 'manifest.json'),
  ];
  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      try {
        return JSON.parse(fs.readFileSync(candidate, 'utf8'));
      } catch {
        return null;
      }
    }
  }
  return null;
}

function deriveScopes(manifest) {
  if (!manifest) return [];
  if (Array.isArray(manifest.scopes)) return manifest.scopes;
  if (Array.isArray(manifest.permissions)) return manifest.permissions;
  return [];
}

function deriveRequiredSecrets(manifest) {
  if (!manifest) return [];
  if (Array.isArray(manifest.required_secrets)) return manifest.required_secrets;
  if (Array.isArray(manifest.env)) return manifest.env;
  return [];
}

function runCapture(cmd, args, opts = {}) {
  const result = spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], ...opts });
  if (result.error) fail(`spawn ${cmd} failed: ${result.error.message}`);
  return {
    status: result.status,
    stdout: result.stdout?.toString('utf8') || '',
    stderr: result.stderr?.toString('utf8') || '',
  };
}

function locateInstallPath(binCli, slug) {
  // Ask the upstream CLI where it installed things. Fall back to
  // workspace/generated-clis/<slug> if the JSON output is unexpected.
  try {
    const r = runCapture(binCli, ['list', '--json'], { cwd: HERE });
    if (r.status !== 0) return null;
    const data = JSON.parse(r.stdout);
    const items = Array.isArray(data) ? data : (data.installed || data.clis || []);
    for (const it of items) {
      if (it?.name === slug || it?.slug === slug) {
        return it.path || it.install_path || it.location || null;
      }
    }
  } catch {
    // ignore
  }
  return null;
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!args.slug) fail('missing <slug> argument. usage: install-cli.mjs <slug> [--source <kind>] [--source-url <url>]');
  if (!SLUG_RE.test(args.slug)) fail(`invalid slug: ${args.slug} (must match /^[a-z0-9][a-z0-9-]{0,63}$/)`);

  const allowedSources = new Set(['starter-pack', 'catalog', 'github', 'local', 'spec']);
  if (!allowedSources.has(args.source)) fail(`invalid --source: ${args.source}`);

  if (['github', 'local', 'spec'].includes(args.source) && !args.source_url) {
    fail(`--source-url is required for source kind "${args.source}"`);
  }

  const binCli = path.join(SUB_NODE_MODULES, '.bin', 'printing-press');
  if (!fs.existsSync(binCli)) {
    fail('printing-press bin not found. Run npm run printing-press:install first.');
  }

  fs.mkdirSync(GENERATED_CLIS_DIR, { recursive: true });
  // Mirror entry — populated even if upstream installs elsewhere, so the
  // workspace dir always has at least the recorded manifest.
  const mirrorPath = path.join(GENERATED_CLIS_DIR, args.slug);
  fs.mkdirSync(mirrorPath, { recursive: true });

  log(`installing ${args.slug} (source=${args.source}) via printing-press`);

  // Upstream CLI shape (v0.1.3): `printing-press install <name|bundle>...`
  // Source kinds beyond catalog/starter-pack are stored as metadata only;
  // the upstream binary picks the actual source based on its catalog index.
  const installArgs = ['install', args.slug, '--json'];
  if (args.cli_only) installArgs.push('--cli-only');
  if (args.skill_only) installArgs.push('--skill-only');

  run(binCli, installArgs, { cwd: HERE });

  // Try to find the real install path via `list --json`; fall back to mirror.
  const realPath = locateInstallPath(binCli, args.slug) || mirrorPath;
  const manifest = readManifestIfPresent(realPath) || readManifestIfPresent(mirrorPath);
  const declared_scopes = deriveScopes(manifest);
  const required_secrets = deriveRequiredSecrets(manifest);

  // Persist a stub manifest in the workspace dir for adapter visibility.
  try {
    fs.writeFileSync(
      path.join(mirrorPath, 'printing-press.json'),
      JSON.stringify({
        slug: args.slug,
        source: args.source,
        source_url: args.source_url,
        upstream_path: realPath,
        manifest,
        installed_at: new Date().toISOString(),
      }, null, 2)
    );
  } catch { /* non-fatal */ }

  // Path stored in DB must remain under workspace/generated-clis/ to satisfy
  // assertUnderGeneratedClis — that's our security boundary. The upstream
  // install path lives in the mirror manifest as informational metadata.
  const { recordInstall, refreshMirrorJson } = await import('../../lib/printing-press-registry.js');

  const row = recordInstall({
    slug: args.slug,
    source_kind: args.source,
    source_url: args.source_url,
    declared_scopes,
    required_secrets,
    manifest,
    path: mirrorPath,
  });
  refreshMirrorJson();

  log(`registered ${args.slug} → ${row.path}`);
  log(`upstream_install_path: ${realPath}`);
  log(`declared_scopes: ${JSON.stringify(declared_scopes)}`);
  log(`required_secrets: ${JSON.stringify(required_secrets)}`);
}

main().catch((err) => fail(err?.message || String(err)));
