import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { GatewayLogger } from './dispatch-adapter.js';
import type { AgentCapability } from './platform-config.js';

// Runtime-side materializer for channel capabilities — a pure PLACEMENT
// layer. The server declares WHAT the agent has (agents.capabilities[]);
// the credential logic lives in this package's channel-exec entry
// (bin/channel-exec.ts, reusing channel-token.ts); this module only drops
// constant POINTER shims onto the capability PATH so the vendor command name
// (e.g. `lark-cli`, which the official skills invoke directly) routes into
// that entry. Pointers reference channel-exec by IN-PACKAGE ABSOLUTE PATH —
// no PATH lookup, no dependence on agent-writable install state; the engine
// ships atomically with the bridge.
//
// Revocation keeps the pointer in place (nothing to remove): the pointer's
// content is independent of grant state, and revocation semantics are
// enforced by the platform mint endpoint (403 with an agent-readable
// message). Deleting would let PATH fall through to a directly-installed
// real CLI past the mint gate — the failure mode the old deny-stub existed
// to prevent; a constant pointer prevents it structurally.
// Design: docs/engineering-design/agent-capability-fragments-design.md §5.1.

export const CAPABILITY_FEISHU_CLI = 'feishu-cli';

// Marker embedded in every generated pointer. channel-exec skips any PATH
// candidate whose head carries it, so a pointer (or a stray copy of one) can
// never be mistaken for the real vendor binary — self-recursion is
// structurally impossible even if the skip-dir hint is wrong.
export const CHANNEL_POINTER_MAGIC = 'parall channel capability pointer';

// SSOT for the shim directory: bridges prepend this to the child PATH once,
// unconditionally — the directory is constant, its content tracks grants, so
// a grant reaches even long-lived subprocesses (the shell re-resolves PATH
// per command) without any respawn.
export function capabilityBinDir(stateDir: string): string {
  return path.join(stateDir, 'bin');
}

// Absolute path of the channel-exec entry, resolved across BOTH distribution
// layouts — mirroring the daemon's resolveBbBrowserDaemonPath:
//   - npm / hosted image: agent-core's dist tree exists on disk →
//     ./bin/channel-exec.js sits under this module's dir.
//   - standalone bundle (@parall/daemon / CDN self-update / desktop): esbuild
//     flattens this module INTO bundle/parall-claude-agent.js, so ./bin/…
//     doesn't exist; the bundle ships parall-channel-exec.js as a sibling flat
//     artifact (scripts/bundle-daemon.mjs), resolved next to this file.
// Prefer the sibling (bundle) and fall through to the in-package path (dev /
// npm), like the bb-browser resolver — the two candidates are mutually
// exclusive so order only affects which stat wins in the impossible case that
// both exist.
export function channelExecEntryPath(): string {
  const selfDir = path.dirname(fileURLToPath(import.meta.url));
  const sibling = path.join(selfDir, 'parall-channel-exec.js');
  if (fs.existsSync(sibling)) return sibling;
  return fileURLToPath(new URL('./bin/channel-exec.js', import.meta.url));
}

/**
 * Idempotently reconcile local capability pointers with the delivered
 * capability list. Call at boot (after the first config fetch) and on every
 * config refresh. Never throws — a pointer write failure must not take down
 * a config refresh (the capability simply stays unusable until the next pass).
 */
export function materializeChannelCapabilities(
  stateDir: string,
  capabilities: AgentCapability[],
  log?: GatewayLogger,
): void {
  try {
    reconcileFeishuCli(stateDir, capabilities, log);
  } catch (err) {
    log?.warn(`channel capability materialization failed: ${String(err)}`);
  }
}

function reconcileFeishuCli(
  stateDir: string,
  capabilities: AgentCapability[],
  log?: GatewayLogger,
): void {
  const binDir = capabilityBinDir(stateDir);
  const posixPath = path.join(binDir, 'lark-cli');
  const granted = capabilities.some((c) => c.key === CAPABILITY_FEISHU_CLI);
  const hadPointer = fs.existsSync(posixPath);

  // Write/refresh pointers when GRANTED, or when a pointer already exists (a
  // previously-granted, now-revoked agent). The refresh-on-revoke case is
  // load-bearing for bundle self-update: the pointer embeds channel-exec's
  // ABSOLUTE path, which resolves through the daemon's `current` symlink into
  // a versioned dir; after an upgrade prunes the old version, a stale retained
  // pointer would fail MODULE_NOT_FOUND instead of reaching the mint 403.
  // Re-rendering every pass keeps the pointer aimed at the LIVE channel-exec,
  // so a revoked agent still gets the self-explanatory 403. A NEVER-granted
  // agent (no pointer) is left untouched — the operator's own lark-cli install
  // stays clean.
  if (!granted && !hadPointer) return;

  fs.mkdirSync(binDir, { recursive: true });
  const entry = channelExecEntryPath();
  // The node binary is referenced by ABSOLUTE path (process.execPath), not the
  // bare name `node`: packaged installs (desktop / daemon bundle) embed the
  // runtime as `parall-node` with no plain `node` on PATH, so a bare `node`
  // would die before channel-exec ever runs and break bundle parity at the
  // last hop. process.execPath is the very node currently running the bridge —
  // the parall-node in a bundle, the system node under npm.
  const nodeExec = process.execPath;
  writePointerIfChanged(posixPath, renderPosixPointer(nodeExec, entry, binDir, 'feishu'), log);
  // Windows companion: cmd/PowerShell resolve executables via PATHEXT and
  // ignore extensionless shebang files. (The agent's own Bash tool on Windows
  // is git-bash, which uses the sh pointer above — the .cmd is the cmd/
  // PowerShell fallback; its %* follows standard batch semantics, same as any
  // npm-installed .cmd bin.)
  writePointerIfChanged(
    path.join(binDir, 'lark-cli.cmd'),
    renderCmdPointer(nodeExec, entry, binDir, 'feishu'),
    log,
  );
}

function writePointerIfChanged(filePath: string, content: string, log?: GatewayLogger): void {
  let existing: string | null = null;
  try {
    existing = fs.readFileSync(filePath, 'utf8');
  } catch {
    existing = null;
  }
  if (existing !== content) {
    fs.writeFileSync(filePath, content, { mode: 0o755 });
    log?.info(`channel capability: pointer materialized (${path.basename(filePath)})`);
  }
  // Mode is enforced even when content is unchanged (a prior partial write
  // or umask drift must not leave the pointer non-executable).
  fs.chmodSync(filePath, 0o755);
}

// Pointer content is versioned HERE (never delivered by the server — config
// carries declarations, not code). It embeds the entry's AND the bin dir's
// absolute paths at write time — no $(dirname)/external commands (a minimal
// PATH must not break the pointer), and the skip hint cannot drift. A
// package upgrade that moves paths changes the rendered content, and the
// boot-time materialize pass rewrites it (self-healing).
// The target binary is NOT passed on the command line — channel-exec derives
// it from the channel (feishu → lark-cli), so a caller cannot redirect the
// minted token to a different program. binDir is still passed as the skip hint.
export function renderPosixPointer(
  nodeExecPath: string,
  entryJsPath: string,
  binDir: string,
  channel: string,
): string {
  return [
    '#!/bin/sh',
    `# Generated by @parall/agent-core — ${CHANNEL_POINTER_MAGIC} (do not edit).`,
    '# Credential + exec logic lives in the agent-core package; revocation is',
    '# enforced by the platform mint endpoint, so this pointer stays constant.',
    // Strip Node preload-hijack vars BEFORE launching node: NODE_OPTIONS
    // (e.g. --require=/evil.js) and NODE_PATH would execute caller-supplied code
    // at interpreter startup — BEFORE channel-exec's own env scrub, i.e. before
    // the mint. The pointer is a platform-authored trust-boundary artifact whose
    // whole job is a CONTROLLED launch of the broker (absolute node, magic
    // guard, skip-dir); this closes the same env-hijack class for node startup
    // that the absolute node path closes for PATH, keeping the launch deterministic.
    'unset NODE_OPTIONS NODE_PATH',
    // "$@" preserves argv exactly (this is the agent's main path via git-bash).
    `exec "${nodeExecPath}" "${entryJsPath}" --channel ${channel} --skip-dir "${binDir}" -- "$@"`,
    '',
  ].join('\n');
}

export function renderCmdPointer(
  nodeExecPath: string,
  entryJsPath: string,
  binDir: string,
  channel: string,
): string {
  return [
    '@echo off',
    `rem Generated by @parall/agent-core - ${CHANNEL_POINTER_MAGIC} (do not edit).`,
    // Clear Node preload-hijack vars before launching node (see the sh pointer).
    'set "NODE_OPTIONS="',
    'set "NODE_PATH="',
    `"${nodeExecPath}" "${entryJsPath}" --channel ${channel} --skip-dir "${binDir}" -- %*`,
    '',
  ].join('\r\n');
}
