import Foundation
import os

/// Command-interceptor tracing. Every observed command is logged (prefixed `CMDTRACE`) so the
/// agent's shell activity can be followed from `log stream --predicate 'subsystem == "com.nucleic"'`
/// (Console.app, category "commands") independently of the GUI feed. Session ids, command names,
/// argv, cwd, and exit codes are `.public` (a local project's own data); captured output is logged
/// only as a short, truncated preview.
let commandLog = Logger(subsystem: "com.nucleic", category: "commands")

/// The non-git half of the in-container interceptor (the git/gh shims live in
/// ``ContainerEngine/gitInterceptorSource`` / ``ContainerEngine/ghInterceptorSource`` and are
/// installed separately, always on). A single, **opt-in** capture layer, reporting to the host's
/// `POST /command-event` route (bearer-token gated, best-effort):
///
///  **Bash command tracer** — a `BASH_ENV`-sourced script (``traceScriptSource``) that installs a
///  `DEBUG`/`EXIT` trap recording EVERY command the agent runs in a Bash tool call (command line,
///  cwd, exit code, duration) and batch-posts them via ``batchPosterSource`` at shell exit. It
///  captures **metadata only** — no stdout/stderr — and catches everything: builtins, control flow,
///  coreutils, and build/run tools (`npm`, `make`, `python`, …). Because the `DEBUG` trap fires
///  before every simple command, it is **off by default**: ``hookEnv`` injects `BASH_ENV` (which
///  activates it) only when ``ContainerServiceSettings/commandTracingEnabled`` is set.
///
/// **No per-command output-capturing shims.** Earlier this layer also symlinked a Node "multi-call"
/// shim over ~40 coreutils (`cat`, `grep`, `find`, …) to capture a head of their stdout/stderr. That
/// made every such command boot a full Node interpreter (in `node:22`, a ~90 MB runtime) and spawn
/// the real binary — a large, sustained CPU multiplier inside a small guest during shell-heavy
/// turns. The shims were removed so those commands run their **native binaries** directly; the
/// tracer still records each one (without an output preview), so the activity feed keeps breadth at
/// a fraction of the cost. Reinstating richer per-command output capture should use a far cheaper
/// mechanism than a Node process per invocation.
public enum CommandInterceptor {
    /// Install root for the interceptor's scripts inside the container.
    public static let libDir = "/usr/local/lib/nucleic"
    /// Path of the batch poster the bash tracer hands its buffer to.
    public static let posterPath = "\(libDir)/post-batch.js"
    /// Path of the `BASH_ENV` tracer script — injected as `BASH_ENV` into the agent's env.
    public static let traceScriptPath = "\(libDir)/trace.sh"

    /// The dedicated POST path the tracer reports to (the command analogue of `/git-event`).
    /// Bearer-token gated like the MCP route, but a plain JSON POST.
    public static let eventPath = "/command-event"

    /// Env that points the in-container git/gh interceptor shims (and, when `commandTracing` is on,
    /// the bash command tracer) at the host control endpoint, under the per-session bearer `token`.
    /// `host:port` is the VM gateway+TCP (legacy) or the in-guest control bridge's loopback (vsock
    /// control plane); the shims POST plain HTTP either way. The single source of the interceptor
    /// hook contract, so every containerized backend wires it identically.
    ///
    /// The git/gh keys are always present — that interception feeds the conflict coordinator / merge
    /// queue / autoship and is not optional. The command-tracer keys (`NUCLEIC_CMD_HOOK_URL`,
    /// `NUCLEIC_HOOK_TOKEN`, and crucially `BASH_ENV`, which is what makes bash source the tracer and
    /// install the per-command `DEBUG` trap) are emitted ONLY when `commandTracing` is on. So with it
    /// off, no `BASH_ENV` is set → the tracer never runs → no per-command overhead, regardless of
    /// whether the tracer script is present in the container's rootfs.
    public static func hookEnv(
        host: String, port: UInt16, token: String, sessionID: SessionID, commandTracing: Bool
    ) -> [String: String] {
        var env = [
            "NUCLEIC_GIT_HOOK_URL": "http://\(host):\(port)\(MCPApprovalServer.gitEventPath)",
            "NUCLEIC_GIT_HOOK_TOKEN": token,
            "NUCLEIC_GH_HOOK_URL": "http://\(host):\(port)\(MCPApprovalServer.ghEventPath)",
            "NUCLEIC_GH_HOOK_TOKEN": token,
            "NUCLEIC_SESSION_ID": sessionID.rawValue,
        ]
        if commandTracing {
            env["NUCLEIC_CMD_HOOK_URL"] = "http://\(host):\(port)\(eventPath)"
            env["NUCLEIC_HOOK_TOKEN"] = token
            env["BASH_ENV"] = traceScriptPath
        }
        return env
    }

    /// The `BASH_ENV` tracer. Sourced by every non-interactive bash the agent spawns (`BASH_ENV`
    /// is inherited from the claude process env). Records each command (line, cwd, exit, duration)
    /// into a temp buffer via a `DEBUG` trap and batch-posts at `EXIT` — metadata only, no output.
    /// Records every command (coreutils included, since the per-command shims were removed). All
    /// best-effort: any failure silently disables tracing without affecting the agent's command.
    public static let traceScriptSource = #"""
        # Nucleic bash command tracer. See CommandInterceptor.traceScriptSource.
        [ -n "$NUCLEIC_TRACE_ACTIVE" ] && return 0
        [ -z "$BASH_VERSION" ] && return 0
        [ -z "$NUCLEIC_CMD_HOOK_URL" ] && return 0
        [ -z "$EPOCHREALTIME" ] && return 0
        export NUCLEIC_TRACE_ACTIVE=1

        __nuc_buf="$(mktemp /tmp/nuc-trace.XXXXXX 2>/dev/null)" || return 0
        __nuc_last=""
        __nuc_t0=0
        __nuc_n=0

        # Microseconds since the epoch from EPOCHREALTIME, keeping ONLY digits — robust to the
        # locale's radix char (a comma in some locales would otherwise break the arithmetic below).
        __nuc_us() { local r="${EPOCHREALTIME//[!0-9]/}"; printf '%s' "$r"; }

        # Post a finished buffer to the host (detached, never blocks the shell) and drop it.
        __nuc_send() {
          if [ -s "$1" ]; then
            node /usr/local/lib/nucleic/post-batch.js "$1" >/dev/null 2>&1 &
            disown 2>/dev/null
          else
            rm -f "$1" 2>/dev/null
          fi
        }

        __nuc_record() {
          local code=$1
          [ -z "$__nuc_last" ] && return 0
          local first="${__nuc_last%% *}"; first="${first##*/}"
          case "$first" in __nuc_*|trap|return|"") __nuc_last=""; return 0;; esac
          local now; now="$(__nuc_us)"
          local dur=$(( (now - __nuc_t0) / 1000 )); [ "$dur" -lt 0 ] && dur=0
          printf '%s\037%s\037%s\037%s\036' "${__nuc_last//$'\n'/ }" "$PWD" "$code" "$dur" >> "$__nuc_buf" 2>/dev/null
          __nuc_last=""
          # Flush incrementally so a long Bash call neither grows one giant POST (the host caps body
          # size) nor delays the feed until exit: roll the buffer every 200 events.
          __nuc_n=$((__nuc_n + 1))
          if [ "$__nuc_n" -ge 200 ]; then
            __nuc_send "$__nuc_buf"
            __nuc_buf="$(mktemp /tmp/nuc-trace.XXXXXX 2>/dev/null)" || __nuc_buf=/dev/null
            __nuc_n=0
          fi
        }

        __nuc_debug() {
          local code=$?
          __nuc_record "$code"
          case "$BASH_COMMAND" in __nuc_*|trap*) return 0;; esac
          __nuc_last="$BASH_COMMAND"
          __nuc_t0="$(__nuc_us)"
        }

        __nuc_flush() {
          local code=$?
          __nuc_record "$code"
          __nuc_send "$__nuc_buf"
        }

        trap '__nuc_debug' DEBUG
        trap '__nuc_flush' EXIT
        """#

    /// The batch poster the tracer hands its buffer file to: parse the record-separated buffer
    /// into events and POST `{type:"command-batch", events:[…]}` once, then delete the buffer.
    /// Detached from the shell, so posting never blocks the agent's command.
    public static let batchPosterSource = #"""
        #!/usr/bin/env node
        'use strict';
        // Nucleic command-tracer batch poster. See CommandInterceptor.batchPosterSource.
        const fs = require('fs');
        const http = require('http');
        const url = process.env.NUCLEIC_CMD_HOOK_URL;
        const token = process.env.NUCLEIC_HOOK_TOKEN;
        if (!url || !token) process.exit(0);
        let target; try { target = new URL(url); } catch (e) { process.exit(0); }
        let raw = '';
        try { raw = fs.readFileSync(process.argv[2], 'utf8'); } catch (e) { process.exit(0); }
        try { fs.unlinkSync(process.argv[2]); } catch (e) {}
        const events = [];
        for (const rec of raw.split('\x1e')) {
          if (!rec) continue;
          const f = rec.split('\x1f');
          if (f.length < 4) continue;
          events.push({
            line: f[0], cwd: f[1],
            exitCode: parseInt(f[2], 10) || 0, durationMs: parseInt(f[3], 10) || 0,
          });
        }
        if (!events.length) process.exit(0);
        const body = JSON.stringify({
          type: 'command-batch', source: 'tracer',
          sessionId: process.env.NUCLEIC_SESSION_ID || null, events: events,
        });
        const req = http.request({
          hostname: target.hostname, port: target.port, path: target.pathname || '/command-event',
          method: 'POST',
          headers: {
            'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token,
            'Content-Length': Buffer.byteLength(body),
          },
          timeout: 2000,
        }, (res) => { res.resume(); });
        req.on('error', () => process.exit(0));
        req.on('timeout', () => { req.destroy(); process.exit(0); });
        req.end(body);
        """#

    /// The `sh` snippet that installs the bash command tracer inside the container (appended to the
    /// git interceptor's seed script). Writes the tracer + its batch poster from base64 (so the
    /// multi-line Node/bash needs no shell escaping). No per-command symlinks: coreutils run their
    /// native binaries, and the `BASH_ENV` tracer records them as metadata. Returns a single `sh -c`
    /// body.
    ///
    /// The scripts are written unconditionally, but the tracer only *runs* when ``hookEnv`` injects
    /// `BASH_ENV` (i.e. when ``ContainerServiceSettings/commandTracingEnabled`` is on). Installing
    /// regardless means the opt-in toggle takes effect on the next agent turn without recreating the
    /// container's (persisted, not re-seeded) rootfs; left off, the unused scripts just sit idle.
    public static func installScript() -> String {
        let posterB64 = base64(batchPosterSource)
        let traceB64 = base64(traceScriptSource)
        return """
            mkdir -p \(libDir) \
            && printf %s \(shellQuote(posterB64)) | base64 -d > \(posterPath) && chmod 0755 \(posterPath) \
            && printf %s \(shellQuote(traceB64)) | base64 -d > \(traceScriptPath) && chmod 0555 \(traceScriptPath)
            """
    }

    private static func base64(_ s: String) -> String {
        Data(s.utf8).base64EncodedString()
    }

    private static func shellQuote(_ s: String) -> String {
        "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
    }
}
