/**
 * Printing Press tool adapters.
 *
 * Four tools exposed to the registry:
 *   printing_press_install   — install/refresh the starter-pack sub-package
 *   printing_press_generate  — generate a new CLI from a spec / URL / starter-pack slug
 *   printing_press_run       — invoke an installed generated CLI with bounded args
 *   printing_press_query     — read-only SQL against a CLI's local SQLite mirror
 *
 * Security invariants enforced HERE in addition to the registry gates:
 *   - All filesystem writes confined to workspace/generated-clis/.
 *   - All spawns use spawn() with shell:false; never execSync.
 *   - In strict mode, refuse to run CLIs declaring "write" scopes.
 *   - Bounded timeouts + capped output buffer; output truncated, never silently dropped.
 *   - Secrets resolved via vault (getSecret) and passed to child env explicitly;
 *     spawned child does NOT inherit process.env wholesale.
 *
 * Registry-level gates (in lib/tools/registry.js executeRegistryTool) still apply:
 *   identity gate, irreversible_external gate, trust-mode gate, allowlist gate,
 *   rate-limit gate, agent-permission gate, audit logging.
 */

import { spawn } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { DatabaseSync } from 'node:sqlite';

import { ROOT, WORKSPACE } from '../../paths.js';
import {
  GENERATED_CLIS_DIR,
  SKILLS_INSTALL_DIR,
  assertUnderGeneratedClis,
  getCli,
  listInstalledClis,
  recordInstall,
  refreshMirrorJson,
  isValidSlug,
} from '../../printing-press-registry.js';
import { capabilitiesFor, resolveMode } from '../../printing-press-security.js';
import { getSecret } from '../../secrets.js';

const SUB_PACKAGE_DIR = path.join(ROOT, 'scripts', 'printing-press');
const INSTALLER = path.join(SUB_PACKAGE_DIR, 'install.mjs');
const CLI_INSTALLER = path.join(SUB_PACKAGE_DIR, 'install-cli.mjs');
const MAX_OUTPUT_BYTES = 256 * 1024;          // 256 KB stdout/stderr cap per call
const DEFAULT_TIMEOUT_MS = 60_000;
const DEFAULT_GENERATE_TIMEOUT_MS = 300_000;  // generation may pull docs
const MAX_QUERY_ROWS = 1000;

function caps() {
  return capabilitiesFor(resolveMode());
}

/**
 * Spawn a child synchronously through promises with bounded output. shell:false
 * is non-negotiable — input is never concatenated into a shell string.
 */
function spawnBounded(command, args, { cwd, timeoutMs, env } = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd,
      shell: false,
      env: env || { PATH: process.env.PATH, HOME: process.env.HOME, NODE_ENV: process.env.NODE_ENV },
    });

    let stdout = Buffer.alloc(0);
    let stderr = Buffer.alloc(0);
    let stdoutTruncated = false;
    let stderrTruncated = false;
    let killed = false;

    const timer = setTimeout(() => {
      killed = true;
      try { child.kill('SIGKILL'); } catch { /* ignore */ }
    }, timeoutMs || DEFAULT_TIMEOUT_MS);

    child.stdout.on('data', (chunk) => {
      if (stdout.length + chunk.length > MAX_OUTPUT_BYTES) {
        stdout = Buffer.concat([stdout, chunk.slice(0, MAX_OUTPUT_BYTES - stdout.length)]);
        stdoutTruncated = true;
      } else {
        stdout = Buffer.concat([stdout, chunk]);
      }
    });
    child.stderr.on('data', (chunk) => {
      if (stderr.length + chunk.length > MAX_OUTPUT_BYTES) {
        stderr = Buffer.concat([stderr, chunk.slice(0, MAX_OUTPUT_BYTES - stderr.length)]);
        stderrTruncated = true;
      } else {
        stderr = Buffer.concat([stderr, chunk]);
      }
    });

    child.on('error', (err) => {
      clearTimeout(timer);
      reject(err);
    });
    child.on('close', (code, signal) => {
      clearTimeout(timer);
      resolve({
        exit_code: code,
        signal,
        timed_out: killed,
        stdout: stdout.toString('utf8'),
        stderr: stderr.toString('utf8'),
        stdout_truncated: stdoutTruncated,
        stderr_truncated: stderrTruncated,
      });
    });
  });
}

function pickEnvForCli(cli, options = {}) {
  // Whitelist env. Refuse process.env wholesale so generated CLIs cannot read
  // unrelated secrets, AWS profile vars, or auth cookies.
  const env = {
    PATH: process.env.PATH,
    HOME: process.env.HOME,
    NODE_ENV: process.env.NODE_ENV,
    PRINTING_PRESS_CLI_SLUG: cli.slug,
    PRINTING_PRESS_DATA_DIR: cli.path,
  };
  for (const secretName of cli.required_secrets || []) {
    if (typeof secretName !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(secretName)) continue;
    try {
      const value = getSecret(secretName, { agent_id: options.agent_id, project_id: options.project_id });
      if (value) env[secretName] = value;
    } catch {
      // Skip — vault not ready or secret missing; CLI may error out, that's fine.
    }
  }
  return env;
}

function hasNetworkWriteScope(cli) {
  const scopes = cli?.declared_scopes || [];
  return scopes.some((s) => typeof s === 'string' && /write|mutate|delete|push/i.test(s));
}

// ── printing_press_install ─────────────────────────────────────────────────

export async function printingPressInstall(input = {}, options = {}) {
  const c = caps();
  // Always-on irreversible_external gate already handled by registry. We still
  // refuse here if the bin path's parent (sub-package) was tampered with.
  if (!fs.existsSync(SUB_PACKAGE_DIR)) {
    return { ok: false, status: 'misconfigured', reason: `sub-package missing at ${SUB_PACKAGE_DIR}` };
  }
  const result = await spawnBounded('node', [INSTALLER], {
    cwd: SUB_PACKAGE_DIR,
    timeoutMs: DEFAULT_GENERATE_TIMEOUT_MS,
  });
  if (result.timed_out) return { ok: false, status: 'timeout', stderr: result.stderr };
  if (result.exit_code !== 0) {
    return { ok: false, status: 'failed', exit_code: result.exit_code, stderr: result.stderr, security_mode: c.mode };
  }
  return {
    ok: true,
    status: 'installed',
    skills_dir: SKILLS_INSTALL_DIR,
    security_mode: c.mode,
    stdout_tail: result.stdout.slice(-1024),
  };
}

// ── printing_press_generate ────────────────────────────────────────────────

export async function printingPressGenerate(input = {}, options = {}) {
  const c = caps();
  const slug = String(input.slug || '').trim();
  if (!isValidSlug(slug)) {
    return { ok: false, status: 'invalid_input', reason: 'slug must match /^[a-z0-9][a-z0-9-]{0,63}$/' };
  }
  const source = String(input.source || 'starter-pack');
  const allowedSources = new Set(['starter-pack', 'github', 'local', 'spec']);
  if (!allowedSources.has(source)) {
    return { ok: false, status: 'invalid_input', reason: `source must be one of ${[...allowedSources].join(', ')}` };
  }
  const sourceUrl = input.source_url ? String(input.source_url) : null;
  if (source !== 'starter-pack' && !sourceUrl) {
    return { ok: false, status: 'invalid_input', reason: `source_url required when source=${source}` };
  }

  const cliPath = path.join(GENERATED_CLIS_DIR, slug);
  try { assertUnderGeneratedClis(cliPath); } catch (err) {
    return { ok: false, status: 'unsafe_path', reason: err.message };
  }

  const args = [CLI_INSTALLER, slug, '--source', source];
  if (sourceUrl) args.push('--source-url', sourceUrl);

  const result = await spawnBounded('node', args, {
    cwd: SUB_PACKAGE_DIR,
    timeoutMs: DEFAULT_GENERATE_TIMEOUT_MS,
  });
  if (result.timed_out) return { ok: false, status: 'timeout', stderr: result.stderr };
  if (result.exit_code !== 0) {
    return { ok: false, status: 'failed', exit_code: result.exit_code, stderr: result.stderr };
  }
  const cli = getCli(slug);
  if (cli && hasNetworkWriteScope(cli) && !c.allow_network_write_clis) {
    return {
      ok: false,
      status: 'refused',
      reason: `Security mode "${c.mode}" forbids CLIs declaring write scopes. Declared: ${JSON.stringify(cli.declared_scopes)}`,
      cli,
    };
  }
  return { ok: true, status: 'generated', cli, security_mode: c.mode };
}

// ── printing_press_run ─────────────────────────────────────────────────────

export async function printingPressRun(input = {}, options = {}) {
  const c = caps();
  const slug = String(input.slug || '').trim();
  if (!isValidSlug(slug)) return { ok: false, status: 'invalid_input', reason: 'invalid slug' };
  const cli = getCli(slug);
  if (!cli || cli.status === 'removed') return { ok: false, status: 'not_installed', slug };
  if (cli.status === 'disabled') return { ok: false, status: 'disabled', slug };

  if (hasNetworkWriteScope(cli) && !c.allow_network_write_clis) {
    return { ok: false, status: 'refused', reason: `Security mode "${c.mode}" forbids running CLIs with write scopes.` };
  }

  const args = Array.isArray(input.args) ? input.args.map(String) : [];
  // Forbid argv shell metachars defensively even though shell:false is set —
  // helps catch CLIs that re-exec under sh internally.
  for (const a of args) {
    if (/[;&|`$<>]/.test(a)) {
      return { ok: false, status: 'unsafe_args', reason: `argv contains shell metacharacter: ${a}` };
    }
  }

  try { assertUnderGeneratedClis(cli.path); } catch (err) {
    return { ok: false, status: 'unsafe_path', reason: err.message };
  }

  // Locate entry point. Convention order:
  //   1. Go binary at $HOME/go/bin/<slug>-pp-cli — printing-press v0.1.x layout.
  //   2. <cli_path>/bin                          — bundled binary inside workspace.
  //   3. <cli_path>/index.js                     — Node entry.
  // The Go-bin path lives OUTSIDE workspace/generated-clis/ on purpose: it's the
  // canonical output of `go install`. We allow that single exception because
  // the binary name pattern is non-spoofable (slug-pp-cli) and the registry row
  // (cli.path under generated-clis) is still the trust anchor.
  const goBinDir = process.env.GOBIN
    || (process.env.GOPATH ? `${process.env.GOPATH}/bin` : null)
    || (process.env.HOME ? `${process.env.HOME}/go/bin` : null);
  const goBinPath = goBinDir ? path.join(goBinDir, `${slug}-pp-cli`) : null;
  const binPath = path.join(cli.path, 'bin');
  const indexPath = path.join(cli.path, 'index.js');
  let command;
  let cmdArgs;
  if (goBinPath && fs.existsSync(goBinPath)) {
    command = goBinPath;
    cmdArgs = args;
  } else if (fs.existsSync(binPath)) {
    command = binPath;
    cmdArgs = args;
  } else if (fs.existsSync(indexPath)) {
    command = process.execPath;  // node
    cmdArgs = [indexPath, ...args];
  } else {
    return { ok: false, status: 'no_entry_point', reason: `No entry point found. Tried: ${goBinPath || '(no go bin dir)'} , ${binPath}, ${indexPath}` };
  }

  const env = pickEnvForCli(cli, options);
  const timeoutMs = Math.min(Number(input.timeout_ms) || DEFAULT_TIMEOUT_MS, 5 * 60_000);
  const result = await spawnBounded(command, cmdArgs, { cwd: cli.path, timeoutMs, env });

  return {
    ok: result.exit_code === 0,
    status: result.timed_out ? 'timeout' : (result.exit_code === 0 ? 'complete' : 'failed'),
    slug,
    exit_code: result.exit_code,
    timed_out: result.timed_out,
    stdout: result.stdout,
    stderr: result.stderr,
    stdout_truncated: result.stdout_truncated,
    stderr_truncated: result.stderr_truncated,
    security_mode: c.mode,
  };
}

// ── printing_press_query ───────────────────────────────────────────────────

const SQL_FORBIDDEN = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|ATTACH|DETACH|PRAGMA|VACUUM|REINDEX|TRUNCATE)\b/i;

export async function printingPressQuery(input = {}, options = {}) {
  const slug = String(input.slug || '').trim();
  if (!isValidSlug(slug)) return { ok: false, status: 'invalid_input', reason: 'invalid slug' };
  const cli = getCli(slug);
  if (!cli || cli.status === 'removed') return { ok: false, status: 'not_installed', slug };

  const sql = String(input.sql || '').trim();
  if (!sql) return { ok: false, status: 'invalid_input', reason: 'sql is required' };
  if (sql.includes(';')) {
    return { ok: false, status: 'unsafe_sql', reason: 'multi-statement queries are not allowed' };
  }
  if (SQL_FORBIDDEN.test(sql)) {
    return { ok: false, status: 'unsafe_sql', reason: 'only SELECT-style read queries are permitted' };
  }
  if (!/^\s*(SELECT|WITH)\b/i.test(sql)) {
    return { ok: false, status: 'unsafe_sql', reason: 'query must begin with SELECT or WITH' };
  }

  // Mirror path candidates (printing-press v0.1.x stores at
  // ~/.local/share/<slug>-pp-cli/data.db on Linux/macOS XDG paths).
  // The workspace mirror is checked first so a bundled CLI takes precedence.
  const xdgData = process.env.XDG_DATA_HOME
    || (process.env.HOME ? path.join(process.env.HOME, '.local', 'share') : null);
  const candidates = [
    path.join(cli.path, 'data.sqlite'),
    path.join(cli.path, `${slug}.sqlite`),
    xdgData ? path.join(xdgData, `${slug}-pp-cli`, 'data.db') : null,
    xdgData ? path.join(xdgData, `${slug}-pp-cli`, 'data.sqlite') : null,
  ].filter(Boolean);
  const dbPath = candidates.find((p) => fs.existsSync(p));
  if (!dbPath) return { ok: false, status: 'no_mirror', reason: 'CLI has no local SQLite mirror', tried: candidates };
  // Workspace paths must pass the boundary check; XDG paths are accepted as a
  // known printing-press v0.1.x layout exception (same reasoning as Go-bin path).
  const isWorkspace = dbPath.startsWith(path.resolve(cli.path) + path.sep);
  if (isWorkspace) {
    try { assertUnderGeneratedClis(dbPath); } catch (err) {
      return { ok: false, status: 'unsafe_path', reason: err.message };
    }
  }

  const params = Array.isArray(input.params) ? input.params : [];
  // Open read-only — DatabaseSync supports { readOnly: true }.
  let db;
  try {
    db = new DatabaseSync(dbPath, { readOnly: true });
  } catch (err) {
    return { ok: false, status: 'open_failed', reason: err.message };
  }

  try {
    const stmt = db.prepare(sql);
    const rows = stmt.all(...params).slice(0, MAX_QUERY_ROWS);
    return {
      ok: true,
      status: 'complete',
      slug,
      row_count: rows.length,
      truncated: rows.length === MAX_QUERY_ROWS,
      rows,
    };
  } catch (err) {
    return { ok: false, status: 'query_failed', reason: err.message };
  } finally {
    try { db.close(); } catch { /* ignore */ }
  }
}

// Expose helpers for tests
export const __internals = {
  hasNetworkWriteScope,
  pickEnvForCli,
  spawnBounded,
  MAX_OUTPUT_BYTES,
  MAX_QUERY_ROWS,
};
