import Foundation

/// The edit gate for Codex, built on codex's **`PreToolUse` hook** (docs/ADAPTERS.md §2.7,
/// docs/LOCKING.md §4.6).
///
/// **Why this exists.** Claude routes *every* tool call through Nucleic's `canUseTool` bridge, so
/// each `Edit`/`Write`/`MultiEdit` — and each raw-shell write — acquires its file locks before it
/// lands (LOCKING §4.2, NASH §5.6). Codex has no such choke point: it edits with the internal
/// `apply_patch` tool (the `*** Update File:` patch envelope, invoked directly or from inside a
/// code-mode `exec` isolate), and Nucleic only ever saw that as an **approval request** —
/// `item/fileChange/requestApproval`, which codex sends *only* when its approval policy requires
/// one. Nucleic runs codex with `approvalPolicy = on-request` and, inside a control container,
/// `sandbox = danger-full-access` (its own bwrap sandbox can't nest in the container's capability
/// set), and codex auto-approves a patch that stays inside its writable roots. So the approval never
/// fired, the arbitration in ``CodexAppServerBackend`` never ran, and every Codex edit landed
/// **unlocked** — parallel sessions clobbered each other and the damage surfaced later as autoship
/// merge conflicts. Confirmed against codex-cli 0.146.0: a `tools.apply_patch(…)` call inside a
/// code-mode turn writes the file with no `requestApproval` on the wire at all.
///
/// **The seam.** codex-cli ≥ 0.14x runs Claude-Code-compatible hooks, and a `PreToolUse` hook is
/// the exact analogue of `canUseTool`: it runs **synchronously before the tool executes** (codex
/// waits for it, so a lock wait parks the tool call rather than racing it), it fires for the
/// *nested* code-mode tools as well as top-level ones, and returning `permissionDecision: "deny"`
/// blocks the call and hands the model our reason ("Command blocked by PreToolUse hook: …").
/// `allow`/`ask` are not honored by codex — deny-or-nothing, which is all a lock gate needs.
///
/// Two tool shapes reach the gate, and both already have a Nucleic path:
///
/// | `tool_name` | `tool_input` | arbitrated as |
/// | --- | --- | --- |
/// | `apply_patch` | `{command: "*** Begin Patch…"}` | the patch's paths → `arbitrate` (an `Edit`) |
/// | `Bash` | `{command: "echo … > f"}` | `arbitrateShellWrites` (NASH §5.6) |
///
/// (codex normalizes its shell tool to `Bash` for hook compatibility, so the Claude/Grok shell-write
/// logic applies verbatim.) Everything else — `view_image`, `update_plan`, `web_search`, MCP tools
/// (already gated by Nucleic's own handlers) — is allowed without a round-trip.
///
/// **Where the gate lives.** The hook is declared in the container's **root-owned** managed config
/// (``managedConfigPath``), written by the container seed like nash's operator policy
/// (`CommandInterceptor.nashPolicyInstallScript`), not in the agent-writable `$HOME/.codex`. Codex
/// reports such a hook as `trustStatus: "managed"`, so it runs with no trust prompt and no
/// `hooks.state` hash bookkeeping — and it covers **every** codex in the box (app-server, `codex
/// exec`, a nested `codex` the agent runs itself), not just the process Nucleic spawned.
///
/// Scope matches locking's (LOCKING §4.4): Nucleic Control containers only. A host (unsandboxed)
/// codex run starts no control server, so there is no gate endpoint to consult — the same boundary
/// the `git`/`gh` interceptors already have (NASH §1).
public enum CodexToolGate {
    /// Root-owned managed config declaring the hook. Codex reads `/etc/codex/managed_config.toml`
    /// as a managed layer, which is what makes the hook `managed` (auto-trusted) and beyond the
    /// agent's reach — `$HOME/.codex/config.toml` is the agent's own file and a hook there would be
    /// both untrusted (needing a `hooks.state` trusted_hash) and one `sed` away from removal.
    public static let managedConfigPath = "/etc/codex/managed_config.toml"
    /// Parent of ``managedConfigPath``.
    static let managedConfigDir = "/etc/codex"
    /// The hook program, installed beside the other interceptor scripts. Node, like
    /// `CommandInterceptor.posterPath`: every image that can run the mandatory control bridge has
    /// `node` (`ContainerEngine.verifyControlBridge`).
    public static let hookScriptPath = "\(CommandInterceptor.libDir)/codex-pre-tool-use.js"

    /// The POST path the hook consults on the per-container control server. Bearer-token gated
    /// like `/git-event`, but **blocking**: the reply carries the arbitration verdict, so the
    /// request stays open for as long as the session waits for the lock.
    public static let eventPath = "/pre-tool-use"

    /// Env var carrying ``eventPath``'s URL to the hook (token rides in `NUCLEIC_HOOK_TOKEN`, as
    /// for every other interceptor). Set by `CommandInterceptor.hookEnv`.
    public static let urlEnvKey = "NUCLEIC_PRETOOL_HOOK_URL"

    /// How long codex waits for the hook before giving up. **A timed-out hook fails OPEN** — codex
    /// runs the tool anyway (verified against 0.146.0) — so this bound, not the host, is the outer
    /// limit of the gate, and the hook itself must answer *before* it (``clientDeadlineMs``).
    static let hookTimeoutSeconds = 900
    /// The hook's own deadline, comfortably inside ``hookTimeoutSeconds``: if the host has not
    /// answered by then the hook returns **deny** (a retryable "still queued" denial) rather than
    /// letting codex's timeout elapse into an unlocked edit.
    static let clientDeadlineMs = 840_000

    // MARK: - Classification

    /// What a `PreToolUse` call means for the lock system.
    public enum Gate: Sendable, Equatable {
        /// Not a worktree mutation (or nothing lockable in it) — allow without a round-trip.
        case ignore
        /// A structured edit: the paths its patch writes, in the order the patch names them.
        case patch([String])
        /// A shell command, arbitrated for the files it writes (`arbitrateShellWrites`).
        case shell(String)
    }

    /// Classify a hook call. Lenient by contract (the decoder principle, ADAPTERS §2.5): an
    /// unrecognized tool or an input shape we can't read becomes ``Gate/ignore`` rather than a
    /// spurious denial — the gate must not invent a lock for a call it doesn't understand.
    public static func classify(toolName: String, input: JSONValue) -> Gate {
        switch toolName {
        case "apply_patch", "applyPatch", "ApplyPatch":
            let paths = patchPaths(in: patchText(input) ?? "")
            return paths.isEmpty ? .ignore : .patch(paths)
        case "Bash", "shell", "exec_command", "local_shell":
            guard let command = shellCommand(input), !command.isEmpty else { return .ignore }
            return .shell(command)
        default:
            return .ignore
        }
    }

    /// The patch envelope out of an `apply_patch` input. `apply_patch` is a *freeform* tool, so the
    /// text arrives as `command` (what the hook wire uses), as a bare string input, or — for the
    /// function-tool shape — under `input`/`patch`.
    static func patchText(_ input: JSONValue) -> String? {
        if let text = input.stringValue { return text }
        for key in ["command", "input", "patch"] {
            if let text = input[key]?.stringValue { return text }
            // A `command` that arrived as argv (`["apply_patch", "*** Begin Patch…"]`).
            if let argv = input[key]?.arrayValue {
                if let envelope = argv.compactMap(\.stringValue).first(where: {
                    $0.contains("*** Begin Patch")
                }) {
                    return envelope
                }
            }
        }
        return nil
    }

    /// Every path a codex patch envelope writes: the `Add`/`Delete`/`Update File` targets plus a
    /// rename's `Move to:` destination (both sides are locked — the source is deleted, so a sibling
    /// editing it would lose its work exactly as with a `git mv`, LOCKING §4.4).
    public static func patchPaths(in patch: String) -> [String] {
        var paths: [String] = []
        for rawLine in patch.split(separator: "\n", omittingEmptySubsequences: false) {
            let line = rawLine.trimmingCharacters(in: .whitespaces)
            for prefix in ["*** Add File:", "*** Delete File:", "*** Update File:", "*** Move to:"]
            where line.hasPrefix(prefix) {
                let path = line.dropFirst(prefix.count).trimmingCharacters(in: .whitespaces)
                if !path.isEmpty, !paths.contains(path) { paths.append(path) }
            }
        }
        return paths
    }

    /// The shell command out of a command-tool input: a plain string (`Bash`, the normalized shape),
    /// `cmd` (codex's own `exec_command`), or an argv whose `-c`/`-lc` payload is the real command.
    static func shellCommand(_ input: JSONValue) -> String? {
        if let text = input.stringValue { return text }
        for key in ["command", "cmd"] {
            if let text = input[key]?.stringValue { return text }
            if let argv = input[key]?.arrayValue {
                let parts = argv.compactMap(\.stringValue)
                guard !parts.isEmpty else { continue }
                // `["bash", "-lc", "…"]` → the payload; anything else → the words as written.
                if parts.count >= 3, parts.contains(where: { $0 == "-lc" || $0 == "-c" }) {
                    return parts.last
                }
                return parts.joined(separator: " ")
            }
        }
        return nil
    }

    /// Resolve a patch's paths for arbitration: relative paths are joined onto the turn's `cwd`
    /// (the worktree, which the container mounts at its host path), and anything that lands outside
    /// `root` is dropped — a write outside the worktree isn't a worktree mutation, so it takes no
    /// lock (its gate is the sandbox + approval policy, exactly as for a raw-shell write, NASH §5.6).
    public static func lockablePaths(_ paths: [String], cwd: String?, root: String?) -> [String] {
        let base = (cwd?.isEmpty == false ? cwd : root) ?? root
        var out: [String] = []
        for path in paths {
            let absolute: String
            if path.hasPrefix("/") {
                absolute = (path as NSString).standardizingPath
            } else if let base, !base.isEmpty {
                absolute = ((base as NSString).appendingPathComponent(path) as NSString)
                    .standardizingPath
            } else {
                absolute = path  // no base to resolve against — pass through; `arbitrate` normalizes
            }
            if let root, !root.isEmpty {
                let normalizedRoot = (root as NSString).standardizingPath
                guard absolute == normalizedRoot || absolute.hasPrefix(normalizedRoot + "/") else {
                    continue
                }
            }
            if !out.contains(absolute) { out.append(absolute) }
        }
        return out
    }

    // MARK: - Arbitration

    /// Arbitrate one `PreToolUse` call — the Codex twin of Claude's `canUseTool` pre-check
    /// (`ClaudeCodeBackend.handleApprovalCall`), shared by both codex backends so the app-server and
    /// `codex exec` runs enforce one contract.
    ///
    /// Both branches are the paths Claude/Grok already take: a structured edit arbitrates the paths
    /// its patch writes, a shell command arbitrates the files it authors (NASH §5.6). Contention
    /// suspends the call inside `arbitrate` until the holder's work lands; a defer/cancel or an
    /// update-on-grant re-ground comes back as a denial carrying the reason the model reads.
    ///
    /// `worktreeRoot` is the run's worktree (the container workdir, which is mounted at the host
    /// path): it bounds which of a patch's paths are worktree mutations at all.
    public static func arbitrate(
        _ call: MCPApprovalServer.PreToolUseCall,
        sessionID: SessionID,
        worktreeRoot: String?,
        coordinator: ConflictCoordinator?
    ) async -> MCPApprovalServer.PreToolUseDecision {
        switch classify(toolName: call.toolName, input: call.input) {
        case .ignore:
            return .allow

        case .shell(let command):
            // Without a coordinator (tests / early startup) there is no lock to take, so only the
            // unlockable-target refusal is left — a rule about the command, not about contention.
            guard let coordinator else {
                if let reason = WorktreeMutationGuard.denialReason(command: command) {
                    return .deny(reason: reason)
                }
                return .allow
            }
            // The hook's `cwd` is the directory the command runs in, which is what turns a relative
            // redirect target into a lockable path; in a Nucleic run it *is* the worktree (the
            // container mounts it at the host path), so the two agree and the run's workdir is only
            // a fallback. A target that resolves outside it takes no lock either way, and nash's
            // `redirect` event still claims what the static parse missed (NASH §5.6).
            let clearance = await coordinator.arbitrateShellWrites(
                sessionID: sessionID, command: command, workdir: call.cwd ?? worktreeRoot,
                toolUseID: call.toolUseID)
            if case .deny(let reason) = clearance { return .deny(reason: reason) }
            return .allow

        case .patch(let declared):
            let paths = lockablePaths(declared, cwd: call.cwd, root: worktreeRoot)
            guard let coordinator, !paths.isEmpty else { return .allow }
            let outcome = await coordinator.arbitrate(
                sessionID: sessionID, task: "", files: paths, toolUseID: call.toolUseID)
            switch outcome.resolution {
            case .deferred:
                return .deny(reason:
                    "This edit conflicts with another active Nucleic agent and was added to the "
                        + "to-do list. Stop now and do not edit — tell the user it was deferred.")
            case .cancelled:
                return .deny(reason:
                    "This edit conflicts with another active Nucleic agent and the user cancelled "
                        + "it. Stop now and do not edit.")
            case .proceed, .noConflict:
                // Lock held. If update-on-grant pulled a parent change into a file this patch
                // targets, block THIS call once with the re-ground notice so the agent re-reads
                // before it overwrites; the lock is held, so the retry re-grants instantly and
                // proceeds (LOCKING §4.5).
                if let notice = outcome.refreshNotice { return .deny(reason: notice) }
                return .allow
            }
        }
    }

    // MARK: - Container install (root-owned, seeded once per container)

    /// The managed config declaring the `PreToolUse` hook for every codex in the container.
    /// `matcher = "*"` so it fires for each tool call; a call that isn't a mutation is answered
    /// without touching the lock system.
    static func managedConfigSource() -> String {
        """
        # Codex managed config — written by Nucleic (docs/LOCKING.md §4.6). Root-owned so the
        # agent can't drop the gate: the PreToolUse hook below is what makes a Codex edit take
        # its file locks before it lands, exactly as Claude's tool-permission bridge does.
        [[hooks.PreToolUse]]
        matcher = "*"
        [[hooks.PreToolUse.hooks]]
        type = "command"
        command = "node \(hookScriptPath)"
        timeout = \(hookTimeoutSeconds)
        """
    }

    /// The hook program. Reads codex's `PreToolUse` JSON on stdin, asks the host to arbitrate, and
    /// either exits 0 (allow — codex runs the tool) or prints codex's deny envelope.
    ///
    /// Failure posture, deliberately asymmetric:
    /// * **Not wired** (no URL/token in the env — a codex outside a Nucleic run, or a run with no
    ///   conflict coordinator): allow. There is no lock domain to enforce, and
    ///   `ConflictCoordinator` is fail-open by contract — a coordination gap must never block an
    ///   agent.
    /// * **Wired but unanswerable** (connection refused, 401 from a retired token, malformed reply,
    ///   or the ``clientDeadlineMs`` deadline): deny, with a retryable reason. Here we know a lock
    ///   *should* be taken and can't be, and an unlocked edit is the one outcome that silently
    ///   corrupts a sibling's work.
    static let hookScriptSource = #"""
        #!/usr/bin/env node
        'use strict';
        // Nucleic Codex PreToolUse gate. See CodexToolGate.hookScriptSource.
        const http = require('http');
        const url = process.env.NUCLEIC_PRETOOL_HOOK_URL;
        const token = process.env.NUCLEIC_HOOK_TOKEN;
        const DEADLINE_MS = __DEADLINE_MS__;

        function allow() { process.exit(0); }
        function deny(reason) {
          process.stdout.write(JSON.stringify({
            hookSpecificOutput: {
              hookEventName: 'PreToolUse',
              permissionDecision: 'deny',
              permissionDecisionReason: reason,
            },
          }) + '\n');
          process.exit(0);
        }

        let raw = '';
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', (chunk) => { raw += chunk; });
        process.stdin.on('error', () => allow());
        process.stdin.on('end', () => gate(raw));

        // The host only needs a patch's file headers to know what to lock, so send those and drop
        // the diff body: a large refactor's envelope can be megabytes, and the control server caps a
        // request (`MCPApprovalServer.maxRequestBytes`) — which, since an unanswerable gate denies,
        // would otherwise turn a big edit into an edit that can never succeed.
        function trimmed(body) {
          let call;
          try { call = JSON.parse(body); } catch (e) { return body; }
          const patch = call && call.tool_input && call.tool_input.command;
          if (typeof patch !== 'string' || patch.indexOf('*** Begin Patch') < 0) return body;
          call.tool_input = Object.assign({}, call.tool_input, {
            command: patch.split('\n').filter((line) => line.startsWith('*** ')).join('\n') + '\n',
          });
          return JSON.stringify(call);
        }

        function gate(body) {
          // Nothing wired → nothing to enforce (a codex outside a Nucleic Control run).
          if (!url || !token) return allow();
          let target;
          try { target = new URL(url); } catch (e) { return allow(); }
          const payload = Buffer.from(trimmed(body || '{}'), 'utf8');
          const req = http.request({
            hostname: target.hostname, port: target.port,
            path: target.pathname || '/pre-tool-use', method: 'POST',
            headers: {
              'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token,
              'Content-Length': payload.length,
            },
            timeout: DEADLINE_MS,
          }, (res) => {
            let data = '';
            res.setEncoding('utf8');
            res.on('data', (chunk) => { data += chunk; });
            res.on('end', () => {
              if (res.statusCode !== 200) {
                return deny(
                  'Nucleic could not check this file against the other agents working in this '
                  + 'project (its lock service answered ' + res.statusCode + '), so the edit was '
                  + 'not applied. Wait a moment and try again.');
              }
              let reply;
              try { reply = JSON.parse(data); } catch (e) { reply = null; }
              if (!reply || typeof reply !== 'object') {
                return deny(
                  'Nucleic could not check this file against the other agents working in this '
                  + 'project (unreadable reply from its lock service), so the edit was not '
                  + 'applied. Wait a moment and try again.');
              }
              if (reply.decision === 'deny') {
                return deny(reply.reason || 'Nucleic denied this call.');
              }
              allow();
            });
          });
          req.on('error', () => deny(
            'Nucleic could not check this file against the other agents working in this project '
            + '(its lock service is unreachable), so the edit was not applied. Wait a moment and '
            + 'try again.'));
          req.on('timeout', () => {
            req.destroy();
            deny(
              'Nucleic did not clear this call in time — most likely another agent still holds a '
              + 'lock on the file(s) it writes — so it was not applied. That wait clears when the '
              + "other agent's work lands: re-read the file (it may have changed since) and try "
              + 'again, or work on something else in the meantime.');
          });
          req.end(payload);
        }
        """#

    /// `sh` snippet installing the hook program + managed config, run as root from the container
    /// seed (``ContainerEngine/seed(_:in:)``). `0555`/`0644` and `root:root`: the agent must read
    /// and execute both but never rewrite them — the gate is only worth having if the gated party
    /// can't remove it (the same reasoning as nash's operator policy, NASH §4.3).
    public static func installScript() -> String {
        let hookB64 = base64(hookScriptSource.replacingOccurrences(
            of: "__DEADLINE_MS__", with: String(clientDeadlineMs)))
        let configB64 = base64(managedConfigSource())
        return """
            mkdir -p \(CommandInterceptor.libDir) \(managedConfigDir) \
            && printf %s \(shellQuote(hookB64)) | base64 -d > \(hookScriptPath) \
            && chown 0:0 \(hookScriptPath) && chmod 0555 \(hookScriptPath) \
            && printf %s \(shellQuote(configB64)) | base64 -d > \(managedConfigPath) \
            && chown 0:0 \(managedConfigPath) && chmod 0644 \(managedConfigPath)
            """
    }

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

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