// channel-exec — the short-lived exec form of the channel-capability
// credential engine. Invoked by the pointer shims the materializer drops on
// the capability PATH (e.g. `lark-cli` → `node <this file> --channel feishu
// --bin lark-cli --skip-dir <shimDir> -- <args…>`): mints a fresh short-lived
// token from the platform on EVERY call, injects it as the vendor CLI's own
// documented env credentials, resolves the real binary past the shim
// directory, and execs it with stdio passthrough.
//
// This is a process entry point, not a library — importing it runs main().
// It lives inside @parall/agent-core so it ships atomically with the bridge
// (image / daemon bundle), never depending on agent-writable install state.
// Design: docs/engineering-design/agent-capability-fragments-design.md §5.1.

import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { CHANNEL_POINTER_MAGIC } from '../channel-capability.js';
import { ChannelTokenError, mintChannelToken } from '../channel-token.js';

// The vendor binary each channel targets is fixed HERE, not taken from the
// caller — channel-exec must never mint a token and hand it to a
// caller-named program. `--bin` is deliberately not an argument, so a request
// like `--channel feishu --bin evil` (with a tampered PATH) cannot redirect
// the minted token to an attacker-chosen executable.
const CHANNEL_BIN: Record<string, string> = { feishu: 'lark-cli' };

interface ExecArgs {
  channel: string;
  skipDir: string;
  rest: string[];
}

function fail(prefix: string, msg: string, code: number): never {
  // Write to fd 2 SYNCHRONOUSLY: process.exit() right after an async
  // process.stderr.write() can truncate the message when stderr is a pipe
  // (notably on Windows) — and this path carries the install hint / revocation
  // message the agent must relay to the user. writeSync flushes before exit;
  // exit still runs so fail() keeps its `never` contract.
  try {
    fs.writeSync(2, `${prefix}: ${msg}\n`);
  } catch {
    // best-effort — still exit with the intended status
  }
  process.exit(code);
}

function parseArgs(argv: string[]): ExecArgs {
  const out: ExecArgs = { channel: '', skipDir: '', rest: [] };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--') {
      out.rest = argv.slice(i + 1);
      break;
    }
    if (a === '--channel') out.channel = argv[++i] ?? '';
    else if (a === '--skip-dir') out.skipDir = argv[++i] ?? '';
    else fail('channel-exec', `unknown argument ${a}`, 2);
  }
  if (!out.channel) {
    fail('channel-exec', 'usage: channel-exec --channel <type> [--skip-dir <dir>] -- <args…>', 2);
  }
  return out;
}

// A candidate that carries the pointer magic in its head is one of OUR
// pointers (or a stray copy of one) — never the real vendor binary. This
// makes self-recursion structurally impossible even when the skip-dir hint
// is wrong or the pointer got copied elsewhere on PATH.
function isCapabilityPointer(candidate: string): boolean {
  try {
    const fd = fs.openSync(candidate, 'r');
    try {
      const buf = Buffer.alloc(256);
      const n = fs.readSync(fd, buf, 0, buf.length, 0);
      return buf.toString('utf8', 0, n).includes(CHANNEL_POINTER_MAGIC);
    } finally {
      fs.closeSync(fd);
    }
  } catch {
    return false;
  }
}

// Resolve the REAL vendor binary along PATH, skipping the shim's own
// directory (realpath comparison, so symlinked layouts don't fool it).
function resolveRealBin(bin: string, skipDir: string): string | null {
  const isWin = process.platform === 'win32';
  // npm on Windows installs `<bin>` (sh) + `<bin>.cmd` + `<bin>.ps1`; the sh
  // file is not spawnable by CreateProcess, so prefer the .cmd/.exe forms.
  const exts = isWin ? ['.cmd', '.exe', '.bat'] : [''];
  let skipReal: string | null = null;
  if (skipDir) {
    try {
      skipReal = fs.realpathSync(skipDir);
    } catch {
      skipReal = null;
    }
  }
  for (const dir of (process.env.PATH || '').split(path.delimiter)) {
    if (!dir) continue;
    let real: string;
    try {
      real = fs.realpathSync(dir);
    } catch {
      continue;
    }
    if (skipReal && real === skipReal) continue;
    for (const ext of exts) {
      const candidate = path.join(dir, bin + ext);
      try {
        if (fs.statSync(candidate).isFile() && !isCapabilityPointer(candidate)) return candidate;
      } catch {
        // keep scanning
      }
    }
  }
  return null;
}

// The exact lark-cli env credentials the broker OWNS: the values it injects
// plus the ambient auth inputs it must strip so nothing widens the identity
// beyond the minted grant. Scrubbing is confined to THESE names — never a
// blanket LARKSUITE_CLI_* wipe — so legitimate operator controls
// (LARKSUITE_CLI_CONFIG_DIR / LOG_DIR / CONTENT_SAFETY_MODE / …) survive.
const FEISHU_OWNED_ENV = [
  // injected (canonical values set below)
  'LARKSUITE_CLI_APP_ID',
  'LARKSUITE_CLI_BRAND',
  'LARKSUITE_CLI_TENANT_ACCESS_TOKEN',
  'LARKSUITE_CLI_DEFAULT_AS',
  'LARKSUITE_CLI_STRICT_MODE',
  // ambient auth bypass inputs that must not reach the child
  'LARKSUITE_CLI_APP_SECRET',
  'LARKSUITE_CLI_USER_ACCESS_TOKEN',
  'LARKSUITE_CLI_AUTH_PROXY',
  'LARKSUITE_CLI_PROXY_KEY',
];

// Per-channel env injection: the ONLY channel-specific knowledge in this
// entry.
function buildChildEnv(
  channel: string,
  minted: { app_id: string; brand: string; token: string },
): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = { ...process.env };
  if (channel === 'feishu') {
    // Delete the broker-owned names case-insensitively: Windows env keys are
    // case-insensitive and a plain-object spread keeps the host's casing, so
    // an uppercase-only delete would miss e.g. `Larksuite_Cli_Auth_Proxy` (a
    // bypass leak) or leave a stale-cased duplicate shadowing the injected
    // token. Non-owned LARKSUITE_CLI_* operator settings are left untouched.
    const owned = new Set(FEISHU_OWNED_ENV);
    for (const key of Object.keys(env)) {
      if (owned.has(key.toUpperCase())) delete env[key];
    }
    env.LARKSUITE_CLI_APP_ID = minted.app_id;
    env.LARKSUITE_CLI_BRAND = minted.brand || 'feishu';
    env.LARKSUITE_CLI_TENANT_ACCESS_TOKEN = minted.token;
    // Bot lock: the credential is the app's, not a human's.
    env.LARKSUITE_CLI_DEFAULT_AS = 'bot';
    env.LARKSUITE_CLI_STRICT_MODE = 'bot';
    return env;
  }
  fail('channel-exec', `unsupported channel type "${channel}"`, 2);
}

// Windows argv-preserving quoting, ported from cross-spawn (the npm-ecosystem
// standard for correctly launching .cmd shims). Node's `shell: true` does NOT
// escape arguments — it concatenates them, so JSON/message args split or
// inject on cmd.exe — and CreateProcess cannot run a .cmd directly. The proven
// path is cmd.exe /d /s /c with each token escaped for BOTH the CreateProcess
// argv layer and the cmd.exe metachar layer (double-escaped because a .cmd
// re-parses). Still Windows-only and NO Windows CI: validate on a real Windows
// host before the first Windows selfhost user (the round-trip tests lock the
// escaping rules, not the live cmd.exe behavior).
// cmd.exe metacharacters, matching cross-spawn's metaCharsRegExp (v7.0.6). ONE
// set shared by BOTH the command and the arguments — cross-spawn uses a single
// regex for both, and a split set is a real correctness hole: a metachar
// caret-escaped in the command path but left bare in an argument (or the
// reverse) survives only one of cmd.exe's two parse passes. Includes the
// separators (space, `;`, `,`) and glob chars (`*`, `?`) so a quoted argument's
// separators are still neutralized for the `.cmd` re-parse, exactly as
// cross-spawn does.
const CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;

function escapeCmdArgument(arg: string): string {
  let out = `${arg}`;
  // Double backslashes before a quote, and trailing backslashes, then wrap.
  out = out.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, '$1$1');
  out = `"${out}"`;
  // cmd metachars, escaped twice (the .cmd forwarder re-parses).
  out = out.replace(CMD_META_CHARS, '^$1').replace(CMD_META_CHARS, '^$1');
  return out;
}

function escapeCmdCommand(command: string): string {
  return command.replace(CMD_META_CHARS, '^$1');
}

function runReal(realBin: string, args: string[], env: NodeJS.ProcessEnv, prefix: string): never {
  const isWinScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(realBin);
  const result = isWinScript
    ? spawnSync(
        process.env.comspec || 'cmd.exe',
        [
          '/d',
          '/s',
          '/c',
          `"${[escapeCmdCommand(realBin), ...args.map(escapeCmdArgument)].join(' ')}"`,
        ],
        { stdio: 'inherit', env, windowsVerbatimArguments: true },
      )
    : spawnSync(realBin, args, { stdio: 'inherit', env });
  if (result.error) {
    fail(prefix, `failed to run ${realBin}: ${String(result.error)}`, 1);
  }
  if (result.signal) {
    process.kill(process.pid, result.signal);
    // Unreachable in practice; satisfy the `never` contract if the signal is trapped.
    process.exit(1);
  }
  process.exit(result.status === null ? 1 : result.status);
}

// Exported for round-trip unit tests (Windows spawn can't run in CI).
export { escapeCmdArgument, escapeCmdCommand };

async function main(): Promise<void> {
  const { channel, skipDir, rest } = parseArgs(process.argv.slice(2));
  const bin = CHANNEL_BIN[channel];
  if (!bin) fail('channel-exec', `unsupported channel type "${channel}"`, 2);
  const prefix = bin;

  let minted: Awaited<ReturnType<typeof mintChannelToken>>;
  try {
    minted = await mintChannelToken(channel, process.env);
  } catch (err) {
    if (err instanceof ChannelTokenError) fail(prefix, err.message, 1);
    fail(prefix, String(err), 1);
  }

  const real = resolveRealBin(bin, skipDir);
  if (!real) {
    const hint =
      channel === 'feishu'
        ? 'Install it once with:\n  npm i -g @larksuite/cli && npx skills add larksuite/cli -y -g'
        : 'Install it and retry.';
    fail(prefix, `the ${bin} binary is not installed. ${hint}`, 127);
  }

  runReal(real, rest, buildChildEnv(channel, minted), prefix);
}

// Run main() only when invoked AS A PROGRAM (the pointer shim runs
// `node <this>`), not when a test imports this module for the escape helpers.
// Holds in both layouts: npm (argv[1] = channel-exec.js) and bundle
// (argv[1] = parall-channel-exec.js, which is also this module's own url).
const invokedPath = process.argv[1];
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
  void main();
}
