import Foundation

// os.Logger on Darwin; the stderr shim in LinuxSupport.swift elsewhere. (This file is otherwise
// portable — it only *generates* the in-container shim sources + env — and compiles on Linux.)
#if canImport(os)
import os
#endif

/// 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,
        shellCapture: String = "tap", environment: NashEnvironment = .agent
    ) -> [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,
            "NUCLEIC_SHELL_ENVIRONMENT_KIND": environment.kind.rawValue,
            "NUCLEIC_SHELL_ENVIRONMENT_ID": environment.id,
            "NUCLEIC_SHELL_ENVIRONMENT_LABEL": environment.label,
        ]
        // nash agent-shell observation (docs/NASH.md §6.2). Always wired: nash reads these to
        // report its exec gate to `POST /shell-event`. Emitting them is inert until nash is the
        // container's shell (seeded/forced in M3) — nothing else reads `NUCLEIC_SHELL_*`. Points at
        // the loopback control bridge, the same transport as the git/gh shims; the direct
        // unix-socket path (`NUCLEIC_SHELL_SOCKET`) is a later container-plumbing optimization.
        env["NUCLEIC_SHELL_HOOK_URL"] = "http://\(host):\(port)\(MCPApprovalServer.shellEventPath)"
        // Codex edit gate (docs/LOCKING.md §4.6): the `PreToolUse` hook the container's root-owned
        // managed config installs reads this URL (and `NUCLEIC_HOOK_TOKEN`, set below) to ask the
        // host whether an `apply_patch`/`Bash` call may proceed. Always wired, like the git/gh
        // shims: it is the only thing that makes a Codex edit take its file locks, and it is inert
        // for a backend whose runs don't install the hook. An env without it means "not wired" and
        // the hook allows — a coordination gap must never block an agent (ConflictCoordinator).
        env[CodexToolGate.urlEnvKey] = "http://\(host):\(port)\(CodexToolGate.eventPath)"
        env["NUCLEIC_SHELL_CAPTURE"] = shellCapture
        env["NUCLEIC_HOOK_TOKEN"] = token
        // No `NUCLEIC_NASH_DISABLE` here, deliberately (docs/NASH.md §4.3). The rollback lever
        // used to ride in this env and reach every shell the agent spawned — but the agent's
        // environment is the agent's to write, so the same variable was a one-word, silent opt-out
        // of the layer whose whole job is to observe that agent. nash now reads the lever only
        // from the trusted, root-owned policy file (``nashPolicyPath``), which
        // ``ContainerEngine/seed(_:in:)`` writes from this same setting; setting the variable is
        // reported as an ignored `policy` event instead of honored. `agentShellArgv` still honors
        // the lever directly for Nucleic's own exec argv.
        // The `BASH_ENV` bash tracer is superseded by nash (docs/NASH.md §1, §7.2) and retired once
        // nash is the default shell; until then it remains opt-in behind `commandTracing`.
        if commandTracing {
            env["NUCLEIC_CMD_HOOK_URL"] = "http://\(host):\(port)\(eventPath)"
            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)
            """
    }

    /// The path nash (the Nucleic agent shell, docs/NASH.md) installs at in container images /
    /// seeded rootfs.
    public static let nashPath = "/usr/local/bin/nash"

    /// The path the real (pre-divert) bash is preserved at in narOS images
    /// (`nash-default-shell`'s dpkg-divert — docs/NASH.md §7.1); absent everywhere else.
    public static let realBashPath = "/usr/bin/bash.real"

    /// nash's trusted operator policy (docs/NASH.md §4.3) — the *only* channel nash takes its
    /// levers from. Root-owned and not group/other-writable, which is the whole point: the agent's
    /// environment is agent-writable, so levers that lived there (`NUCLEIC_NASH_DISABLE`,
    /// `NUCLEIC_REAL_BASH`) let the observed process turn its own observation off in one word.
    /// nash now refuses them from the environment and reports the attempt.
    public static let nashPolicyPath = "/etc/nucleic/nash.conf"

    /// The trusted policy's contents for a Nucleic-managed container.
    ///
    /// `require_observation=1` is the half that closes the *quiet* bypass — with it set, a shell
    /// that finds its transport env stripped or `NUCLEIC_SHELL_CAPTURE=off` still observes, spooling
    /// to `/var/spool/nucleic-nash` for the drain rather than running silently.
    /// `disable` carries the ``ContainerServiceSettings/legacyShell`` rollback lever, which is why
    /// the seed recipe (``ContainerEngine/seedRecipe``) includes the lever: flipping it re-seeds
    /// reused containers so the file, not an agent-settable variable, moves the whole session.
    public static func nashPolicySource(legacyShell: Bool = ContainerServiceSettings.legacyShell)
        -> String
    {
        """
        # nash operator policy — written by Nucleic (docs/NASH.md §4.3). nash reads its
        # levers only from this file: it is root-owned and the agent's environment is not.
        # Editing it by hand is an operator action; nothing an agent's command sets in the
        # environment reaches these.
        require_observation=1
        real_bash=\(realBashPath)
        disable=\(legacyShell ? "1" : "0")
        """
    }

    /// The `sh` snippet that installs ``nashPolicySource(legacyShell:)`` at ``nashPolicyPath``.
    /// Run as root from the container seed, so the file lands `root:root` — the ownership nash
    /// checks before honoring a single line of it. `0644`: nash runs as the agent uid and has to
    /// *read* it, so this is a trust boundary on **writes** only, and the file holds no secret.
    public static func nashPolicyInstallScript(
        legacyShell: Bool = ContainerServiceSettings.legacyShell
    ) -> String {
        let policyB64 = base64(nashPolicySource(legacyShell: legacyShell))
        return """
            mkdir -p \(nashPolicyDir) \
            && printf %s \(shellQuote(policyB64)) | base64 -d > \(nashPolicyPath) \
            && chown 0:0 \(nashPolicyPath) && chmod 0644 \(nashPolicyPath)
            """
    }

    /// Parent of ``nashPolicyPath``.
    static let nashPolicyDir = "/etc/nucleic"

    /// The argv for running a shell `command` inside a container under the agent shell
    /// (docs/NASH.md §7.2): prefer nash when present (baked into the image or seeded), and
    /// degrade transparently to `/bin/sh` when it isn't (custom images, pre-nash images —
    /// §7.6 "never refuse to run"). The command rides as `$0` of the probe script, so it needs
    /// no quoting and is passed to the chosen shell byte-for-byte.
    ///
    /// Under the ``ContainerServiceSettings/legacyShell`` rollback lever the preference inverts:
    /// run the preserved real bash where the image has one (narOS divert), else plain `/bin/sh`
    /// — which the seeded policy's `disable=1` (§4.3) makes safe even where `/bin/sh` IS nash.
    public static func agentShellArgv(_ command: String) -> [String] {
        if ContainerServiceSettings.legacyShell {
            return [
                "/bin/sh", "-c",
                "if [ -x \(realBashPath) ]; then exec \(realBashPath) -lc \"$0\"; else exec /bin/sh -lc \"$0\"; fi",
                command,
            ]
        }
        return [
            "/bin/sh", "-c",
            "if [ -x \(nashPath) ]; then exec \(nashPath) -lc \"$0\"; else exec /bin/sh -lc \"$0\"; fi",
            command,
        ]
    }

    /// The **host** twin of ``agentShellArgv(_:)`` (docs/NASH.md §7.7.6), and simpler — no `/bin/sh`
    /// shim layer to route through: `[bundled nash, "-c", command]` when the host shell is set to
    /// nash (``HostShellSettings/nashEnabled``) and the app bundle carries the binary, else the
    /// historical `["/bin/zsh", "-lc", command]`. Degradation is silent and total (§7.6 doctrine —
    /// never refuse to run). `-c`, not `-lc`: with the ``LoginShellEnv`` snapshot injected there is
    /// nothing left for login sourcing to contribute, and skipping it is strictly faster than
    /// re-sourcing the user's rc files per command (§7.7.2). A per-call zsh escape hatch
    /// (`shell:"zsh"` on `host_exec`) rides `forceZsh`.
    public static func hostShellArgv(_ command: String, forceZsh: Bool = false) -> [String] {
        if !forceZsh, HostShellSettings.nashEnabled, let nash = bundledNashPath() {
            return [nash, "-c", command]
        }
        return ["/bin/zsh", "-lc", command]
    }

    /// The in-app-bundle host nash (docs/NASH.md §8: `Nucleic.app/Contents/MacOS/nash`, signed with
    /// the app; no installer, no `/usr/local/bin` write on the user's Mac). Dev/SwiftPM runs have no
    /// app bundle, so they fall back to the `scripts/build-nash-macos.sh` output in the source tree —
    /// same convention as `MacVMEngine.resolveNashBinary`. `nil` (→ zsh) when neither exists.
    public static func bundledNashPath(bundle: Bundle = .main) -> String? {
        let bundled = bundle.bundleURL.appendingPathComponent("Contents/MacOS/nash").path
        if FileManager.default.isExecutableFile(atPath: bundled) { return bundled }
        var dir = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
        for _ in 0..<6 {
            if FileManager.default.fileExists(
                atPath: dir.appendingPathComponent("scripts/build-nash-macos.sh").path)
            {
                let dist = dir.appendingPathComponent("shell/dist/macos/nash").path
                return FileManager.default.isExecutableFile(atPath: dist) ? dist : nil
            }
            dir = dir.deletingLastPathComponent()
        }
        return nil
    }

    /// Whether THIS process is itself running inside a narOS rootfs — `/etc/os-release` says
    /// `ID=naros` (docs/NAROS.md §2.3). True for `nucleicd` on the `naros-runner` image
    /// (NAROS.md N3), where the host-of-record container IS the agent sandbox
    /// (`RunSpec.container = nil`) and `/bin/sh` is already nash via the base image's divert.
    /// Host-run backends use this to wire the nash observation env — and register the report
    /// routes — for otherwise-unsandboxed spawns: with no `ContainerSpec` there is no shim
    /// seeding on that surface, so the forced shell is its one interception layer
    /// (docs/NASH.md §7.3). Always false on macOS (the user's real Mac is a non-goal,
    /// NASH.md §1) and on non-narOS Linux hosts, where the emitted env would be inert anyway.
    public static let hostIsNarOS: Bool = isNarOSRelease(
        (try? String(contentsOfFile: "/etc/os-release", encoding: .utf8)) ?? "")

    /// `ID=naros` parse of an os-release document (factored from ``hostIsNarOS`` for tests).
    /// os-release values may be quoted; `ID_LIKE=debian` must not match.
    static func isNarOSRelease(_ contents: String) -> Bool {
        for line in contents.split(separator: "\n") where line.hasPrefix("ID=") {
            return line.dropFirst(3).trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
                == "naros"
        }
        return false
    }

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

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