import Foundation

/// Parses a `host_exec` shell command into a structured, **deterministic** breakdown — the
/// program, its actions (subcommands/targets), its flags, its operands, and an *inferred
/// purpose* — so the phone lays out, in plain language, what the agent is about to run on the
/// user's machine. The mobile mirror of the desktop's `HostCommandSummary` (NucleicCore): the
/// remote app links only `NucleicProtocol`, so the parser is duplicated here at phone fidelity,
/// exactly as `TranscriptProjection` is.
///
/// Everything is derived **only from the command string** — never from anything the agent said
/// about its own intent (which may be wrong, or a lie). The inference is computed from the
/// literal tokens, so the user can check "Likely purpose: Build the Swift package" against the
/// command shown right beside it before tapping Allow.
enum HostCommandSummary {
    /// The fully-qualified host-exec tool name on the sync wire — the gate that escapes the
    /// sandbox onto the macOS host.
    static let hostExecToolName = "mcp__nucleic__host_exec"

    /// A `NAME=value` environment assignment prefixed before a program (`NUCLEIC_CHANNEL=dev swift …`).
    struct EnvAssignment: Equatable, Identifiable {
        let name: String
        let value: String
        var id: String { name }
        var display: String { "\(name)=\(value)" }
    }

    /// A parsed flag: its name (leading dashes kept) and its value when it takes one, recovered
    /// from `--flag value`, `--flag=value`, or `-f value`. A boolean flag has a nil value.
    struct Flag: Equatable, Identifiable {
        let name: String
        let value: String?
        var id: String { name + "\u{1}" + (value ?? "") }
        var display: String { value.map { "\(name) \($0)" } ?? name }
    }

    /// A recognized shell control-flow header — a loop (`for`/`while`/`until`/`select`) or a
    /// conditional (`if`/`elif`/`case`). Surfaced so a scripted pipeline reads as its structure —
    /// one "for i in $(seq 1 50)" step under a loop glyph — instead of shredding into bogus
    /// `for`/`do`/`done`/`fi` program steps. The mobile mirror of the desktop's `ControlFlow`.
    struct ControlFlow: Equatable {
        enum Kind: Equatable { case loop, conditional }
        let kind: Kind
        /// The header rendered readably — verbatim from the command, so `$(…)` substitutions and
        /// tests survive: "for i in $(seq 1 50)", "if [ -f /tmp/x ]".
        let text: String
    }

    /// One program invocation within a command pipeline.
    struct Invocation: Equatable, Identifiable {
        let id: Int
        /// The program name, reduced to its basename (`/usr/bin/swift` → `swift`). Empty for a
        /// ``control`` step, which has no program.
        let program: String
        /// The subcommands / targets that follow it, in order (`build`, `run`, `test`).
        let actions: [String]
        /// The parsed flags, in order.
        let flags: [Flag]
        /// Positional operands that are neither actions nor flag values (a script path, a branch).
        let arguments: [String]
        /// `NAME=value` assignments prefixed before the program.
        let env: [EnvAssignment]
        /// True when the invocation is `sudo`-elevated.
        let elevated: Bool
        /// True for an inherently destructive program (`rm`/`rmdir`).
        let destructive: Bool
        /// Set when this "invocation" is actually a shell loop/conditional *header* rather than a
        /// program — rendered as one control-flow step. Nil for an ordinary program invocation.
        let control: ControlFlow?

        /// "swift build", for a compact one-liner — or the loop/conditional header for a
        /// ``control`` step ("for i in $(seq 1 50)").
        var headline: String {
            if let control { return control.text }
            return ([program] + actions).joined(separator: " ")
        }
    }

    /// A whole `host_exec` command, parsed.
    struct Summary: Equatable {
        let invocations: [Invocation]
        let workingDirectory: String?
        /// A plain-language guess at what the command does, inferred from the parsed tokens
        /// (never from the agent).
        let purpose: String

        var isElevated: Bool { invocations.contains { $0.elevated } }
        var isDestructive: Bool { invocations.contains { $0.destructive } }
    }

    /// Parses `command` into a structured ``Summary``. Returns nil only for a blank command.
    static func summary(for command: String) -> Summary? {
        let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }

        let (flattened, _) = Lexer.stripHeredocs(command)
        var invocations: [Invocation] = []
        var workingDirectory: String?
        var nextID = 0
        for segment in Lexer.splitSegments(flattened) {
            // Peel any leading `do`/`then`/`else`/`{` so the command a loop or conditional body
            // introduces (`then rm -rf x`) parses as that command, then strip env/`sudo` off it.
            let peeled = Lexer.stripBodyIntroducers(Lexer.tokenize(segment))
            let (env, elevated, rest) = stripPrefixes(peeled)
            guard let head = rest.first else { continue }
            // A pure structural keyword (`done`/`fi`/`esac`/`}`/`break`/…) carries no operation —
            // skip it like `cd`, so it never renders as a bogus program step.
            if Lexer.structuralKeywords.contains(head) { continue }
            // A loop or conditional *header* reads as one control-flow step rather than shredding.
            if let control = controlFlow(head: head, segment: segment) {
                invocations.append(makeControlInvocation(id: nextID, control: control))
                nextID += 1
                continue
            }
            if basename(head).lowercased() == "cd" {
                if workingDirectory == nil, rest.count >= 2 {
                    workingDirectory = normalizeDirectory(rest[1])
                }
                continue
            }
            invocations.append(makeInvocation(
                id: nextID, program: head, args: Array(rest.dropFirst()),
                env: env, elevated: elevated))
            nextID += 1
        }

        let purpose = inferPurpose(
            invocations: invocations, workingDirectory: workingDirectory)
        return Summary(invocations: invocations, workingDirectory: workingDirectory, purpose: purpose)
    }

    // MARK: - Segment parsing

    private static func stripPrefixes(_ tokens: [String]) -> (env: [EnvAssignment], elevated: Bool, rest: [String]) {
        var env: [EnvAssignment] = []
        var elevated = false
        var rest = tokens[...]
        while let first = rest.first {
            if first == "sudo" {
                elevated = true
                rest = rest.dropFirst()
            } else if Lexer.isEnvAssignment(first), let eq = first.firstIndex(of: "=") {
                env.append(EnvAssignment(
                    name: String(first[..<eq]), value: String(first[first.index(after: eq)...])))
                rest = rest.dropFirst()
            } else {
                break
            }
        }
        return (env, elevated, Array(rest))
    }

    /// Classifies a segment whose (post-peel) head word opens a shell control-flow construct — a
    /// loop (`for`/`while`/`until`/`select`) or a conditional (`if`/`elif`/`case`) — into a
    /// ``ControlFlow`` carrying the verbatim header for display. Nil for an ordinary program.
    private static func controlFlow(head: String, segment: String) -> ControlFlow? {
        let text = segment.trimmingCharacters(in: .whitespacesAndNewlines)
        if Lexer.loopKeywords.contains(head) { return ControlFlow(kind: .loop, text: text) }
        if Lexer.conditionalKeywords.contains(head) { return ControlFlow(kind: .conditional, text: text) }
        return nil
    }

    /// A control-flow step — a loop/conditional header — as an ``Invocation`` with only its
    /// ``Invocation/control`` set, so it slots into the same step list.
    private static func makeControlInvocation(id: Int, control: ControlFlow) -> Invocation {
        Invocation(id: id, program: "", actions: [], flags: [], arguments: [],
                   env: [], elevated: false, destructive: false, control: control)
    }

    private static func makeInvocation(
        id: Int, program rawProgram: String, args: [String],
        env: [EnvAssignment], elevated: Bool
    ) -> Invocation {
        let program = basename(rawProgram)
        let spec = spec(for: program)
        let (actions, rest) = splitActions(args, spec: spec)
        let (flags, arguments) = splitFlags(rest, valueFlags: spec.valueFlags)
        let destructive = program == "rm" || program == "rmdir"
        return Invocation(
            id: id, program: program, actions: actions, flags: flags, arguments: arguments,
            env: env, elevated: elevated, destructive: destructive, control: nil)
    }

    private static func splitActions(_ args: [String], spec: ProgramSpec) -> (actions: [String], rest: [String]) {
        var actions: [String] = []
        var index = 0
        while index < args.count, actions.count < spec.maxActions {
            let token = args[index]
            guard isIdentifierLike(token) else { break }
            guard spec.openActions || spec.subcommands.contains(token) else { break }
            actions.append(token)
            index += 1
        }
        return (actions, Array(args[index...]))
    }

    private static func splitFlags(_ tokens: [String], valueFlags: Set<String>) -> (flags: [Flag], arguments: [String]) {
        var flags: [Flag] = []
        var arguments: [String] = []
        var index = 0
        var endOfOptions = false
        while index < tokens.count {
            let token = tokens[index]
            if endOfOptions { arguments.append(token); index += 1; continue }
            if token == "--" { endOfOptions = true; index += 1; continue }
            if token.hasPrefix("-"), token.count > 1 {
                if let eq = token.firstIndex(of: "=") {
                    flags.append(Flag(name: String(token[..<eq]),
                                      value: String(token[token.index(after: eq)...])))
                    index += 1
                } else if valueFlags.contains(token), index + 1 < tokens.count,
                          !tokens[index + 1].hasPrefix("-") {
                    flags.append(Flag(name: token, value: tokens[index + 1]))
                    index += 2
                } else {
                    flags.append(Flag(name: token, value: nil))
                    index += 1
                }
            } else {
                arguments.append(token)
                index += 1
            }
        }
        return (flags, arguments)
    }

    // MARK: - Program specs

    private struct ProgramSpec {
        var openActions: Bool = false
        var subcommands: Set<String> = []
        var maxActions: Int = 1
        var valueFlags: Set<String> = []
    }

    private static func spec(for program: String) -> ProgramSpec {
        switch program {
        case "swift":
            return ProgramSpec(
                subcommands: ["build", "run", "test", "package", "sdk"], maxActions: 2,
                valueFlags: ["--product", "--target", "--build-system", "-c", "--configuration",
                             "--filter", "--package-path", "--scratch-path", "--jobs", "-j",
                             "-Xswiftc", "-Xcc", "-Xlinker", "--triple"])
        case "make":
            return ProgramSpec(openActions: true, maxActions: 4, valueFlags: ["-f", "-C", "-j"])
        case "npm", "pnpm", "yarn", "bun":
            return ProgramSpec(
                subcommands: ["install", "i", "ci", "run", "run-script", "test", "build", "start",
                              "exec", "publish", "add", "remove", "update", "lint", "dev"],
                valueFlags: ["--prefix", "-w", "--workspace", "--filter"])
        case "npx":
            return ProgramSpec(openActions: true, maxActions: 1)
        case "cargo":
            return ProgramSpec(
                subcommands: ["build", "test", "run", "check", "clippy", "fmt", "bench", "doc",
                              "install", "update", "publish", "clean"],
                valueFlags: ["--package", "-p", "--bin", "--example", "--features", "--target",
                             "--manifest-path", "--jobs", "-j"])
        case "git":
            return ProgramSpec(
                subcommands: ["commit", "push", "pull", "fetch", "clone", "merge", "rebase",
                              "checkout", "switch", "branch", "tag", "stash", "restore", "reset",
                              "revert", "cherry-pick", "add", "rm", "mv", "status", "log", "diff",
                              "show", "blame", "rev-parse", "worktree", "init", "clean"],
                maxActions: 2,
                valueFlags: ["-m", "--message", "-F", "--file", "-b", "-B", "-C", "-c"])
        case "xcodebuild":
            return ProgramSpec(
                openActions: true, maxActions: 3,
                valueFlags: ["-scheme", "-project", "-workspace", "-configuration", "-sdk",
                             "-destination", "-derivedDataPath", "-arch", "-target"])
        case "python", "python3", "python2":
            return ProgramSpec(valueFlags: ["-m", "-c", "-W", "-X"])
        case "pip", "pip3":
            return ProgramSpec(
                subcommands: ["install", "uninstall", "download", "list", "show", "freeze",
                              "wheel", "check"],
                valueFlags: ["-r", "--requirement", "-c", "--constraint", "-t", "--target"])
        case "node":
            return ProgramSpec(valueFlags: ["-e", "--eval", "-r", "--require"])
        case "docker", "podman":
            return ProgramSpec(
                subcommands: ["build", "run", "exec", "compose", "push", "pull", "up", "down",
                              "start", "stop", "ps", "images", "logs"],
                maxActions: 2,
                valueFlags: ["-t", "--tag", "-f", "--file", "-p", "--publish", "-v", "--volume"])
        case "gh":
            return ProgramSpec(
                subcommands: ["pr", "issue", "repo", "release", "run", "workflow", "auth", "api",
                              "create", "list", "view", "merge", "checkout", "status"],
                maxActions: 3)
        case "brew":
            return ProgramSpec(
                subcommands: ["install", "uninstall", "upgrade", "update", "list", "info",
                              "search", "tap", "bundle"],
                maxActions: 2)
        case "sh", "bash", "zsh", "fish":
            return ProgramSpec(valueFlags: ["-c"])
        default:
            return ProgramSpec(openActions: true, maxActions: 1)
        }
    }

    // MARK: - Inferred purpose

    private static func inferPurpose(invocations: [Invocation], workingDirectory: String?) -> String {
        guard !invocations.isEmpty else {
            if let workingDirectory { return "Change directory to \(workingDirectory)" }
            return "Run a host command"
        }
        let interpreted = invocations.map { interpret($0) }
        let best = interpreted.max { $0.salience < $1.salience }
        return best?.purpose ?? "Run \(invocations[0].headline)"
    }

    private static func interpret(_ inv: Invocation) -> (purpose: String, salience: Int) {
        // A control-flow header describes the shape of the script, not what it does — rank it low
        // so a real body command (a build/test/delete) leads the headline instead.
        if let control = inv.control {
            return (control.kind == .loop ? "Run a loop" : "Run a conditional", 18)
        }
        if inv.destructive {
            let target = inv.arguments.first.map { " \($0)" } ?? " files"
            return ("Delete\(target) on the host", 100)
        }
        let action = inv.actions.first
        switch inv.program {
        case "swift":
            return swiftPurpose(inv, action: action)
        case "make":
            let targets = inv.actions
            if targets.isEmpty { return ("Run the default make target", 60) }
            let list = backticked(targets.joined(separator: ", "))
            return ("Run the make target\(targets.count == 1 ? "" : "s") \(list)", 64)
        case "npm", "pnpm", "yarn", "bun":
            return packageManagerPurpose(inv, action: action)
        case "npx":
            let tool = inv.arguments.first ?? inv.actions.first
            return (tool.map { "Run \(backticked($0)) via npx" } ?? "Run a tool via npx", 55)
        case "cargo":
            return cargoPurpose(action: action)
        case "git":
            return gitPurpose(inv)
        case "xcodebuild":
            let testing = inv.actions.contains("test") || inv.actions.contains("test-without-building")
            let scheme = flagValue(inv, ["-scheme"]).map { " (scheme \($0))" } ?? ""
            return (testing ? "Run Xcode tests\(scheme)" : "Build the Xcode project\(scheme)", testing ? 70 : 66)
        case "python", "python3", "python2":
            if let module = flagValue(inv, ["-m"]) { return ("Run the Python module \(backticked(module))", 62) }
            if let script = inv.arguments.first { return ("Run the Python script \(backticked(script))", 62) }
            if flagValue(inv, ["-c"]) != nil { return ("Run inline Python code", 58) }
            return ("Run Python", 50)
        case "pip", "pip3":
            if action == "install" {
                let pkgs = inv.arguments.isEmpty ? "" : " " + backticked(inv.arguments.prefix(3).joined(separator: ", "))
                return ("Install Python packages\(pkgs)", 60)
            }
            return ("Run pip\(action.map { " \($0)" } ?? "")", 45)
        case "node":
            if let script = inv.arguments.first { return ("Run the Node script \(backticked(script))", 60) }
            if flagValue(inv, ["-e", "--eval"]) != nil { return ("Run inline Node code", 56) }
            return ("Run Node", 50)
        case "docker", "podman":
            let sub = inv.actions.joined(separator: " ")
            return (sub.isEmpty ? "Run a container command" : "Run `\(inv.program) \(sub)`", 60)
        case "gh":
            let sub = inv.actions.joined(separator: " ")
            return (sub.isEmpty ? "Run a GitHub CLI command" : "Run `gh \(sub)`", 55)
        case "brew":
            switch action {
            case "install": return ("Install Homebrew packages", 60)
            case "uninstall": return ("Uninstall Homebrew packages", 60)
            case "upgrade", "update": return ("Update Homebrew packages", 55)
            default: return ("Run a Homebrew command", 45)
            }
        case "sh", "bash", "zsh", "fish":
            if let script = inv.arguments.first { return ("Run the shell script \(backticked(script))", 58) }
            if flagValue(inv, ["-c"]) != nil { return ("Run an inline shell command", 54) }
            return ("Start a \(inv.program) shell", 40)
        default:
            return defaultPurpose(inv)
        }
    }

    private static func swiftPurpose(_ inv: Invocation, action: String?) -> (String, Int) {
        let channel = flagValue(inv, [], env: "NUCLEIC_CHANNEL")
        let channelSuffix = channel.map { " (\($0) channel)" } ?? ""
        let release = (flagValue(inv, ["-c", "--configuration"]) == "release")
        let releaseSuffix = release ? " in release" : ""
        switch action {
        case "build":
            let what = flagValue(inv, ["--product"]).map { "the Swift product \(backticked($0))" }
                ?? "the Swift package"
            return ("Build \(what)\(releaseSuffix)\(channelSuffix)", 66)
        case "test":
            let filter = flagValue(inv, ["--filter"]).map { " (filter: \($0))" } ?? ""
            return ("Run the Swift test suite\(filter)", 70)
        case "run":
            let what = (inv.arguments.first ?? flagValue(inv, ["--product"])).map(backticked) ?? "the Swift package"
            return ("Build and run \(what)\(channelSuffix)", 68)
        case "package":
            let sub = inv.actions.count > 1 ? inv.actions[1] : (inv.arguments.first ?? "")
            return (sub.isEmpty ? "Run a SwiftPM package command" : "Run SwiftPM `package \(sub)`", 55)
        default:
            return defaultPurpose(inv)
        }
    }

    private static func packageManagerPurpose(_ inv: Invocation, action: String?) -> (String, Int) {
        let pm = inv.program
        switch action {
        case "install", "i", "ci", "add":
            return ("Install \(pm) dependencies", 60)
        case "run", "run-script", "exec", "dev":
            let script = inv.arguments.first ?? (inv.actions.count > 1 ? inv.actions[1] : nil)
            return (script.map { "Run the \(backticked($0)) \(pm) script" } ?? "Run an \(pm) script", 60)
        case "test":
            return ("Run \(pm) tests", 68)
        case "build":
            return ("Run the \(pm) build", 66)
        case "start":
            return ("Start the \(pm) app", 60)
        case "publish":
            return ("Publish the \(pm) package", 80)
        case "lint":
            return ("Lint with \(pm)", 50)
        default:
            return defaultPurpose(inv)
        }
    }

    private static func cargoPurpose(action: String?) -> (String, Int) {
        switch action {
        case "build": return ("Build the Rust crate", 66)
        case "test": return ("Run the Rust tests", 70)
        case "run": return ("Build and run the Rust crate", 68)
        case "check": return ("Type-check the Rust crate", 60)
        case "clippy": return ("Lint the Rust crate", 55)
        case "fmt": return ("Format the Rust code", 50)
        case "publish": return ("Publish the Rust crate", 80)
        default: return ("Run cargo\(action.map { " \($0)" } ?? "")", 50)
        }
    }

    /// Inline git phrasing — a phone-sized echo of the desktop's `GitCommandSummary` (which the
    /// remote can't link), covering the common host-run git ops.
    private static func gitPurpose(_ inv: Invocation) -> (String, Int) {
        let action = inv.actions.first ?? ""
        let ops = inv.arguments
        func remote(_ verb: String, _ preposition: String) -> String {
            guard let first = ops.first else { return "\(verb) changes" }
            return ops.count >= 2 ? "\(verb) \(preposition) \(first)/\(ops[1])" : "\(verb) \(preposition) \(first)"
        }
        switch action {
        case "commit": return ("Commit changes", 78)
        case "push": return (remote("Push", "to"), 80)
        case "pull": return (remote("Pull", "from"), 75)
        case "fetch": return (remote("Fetch", "from"), 70)
        case "merge": return (ops.first.map { "Merge \($0)" } ?? "Merge branches", 76)
        case "rebase": return (ops.first.map { "Rebase onto \($0)" } ?? "Rebase commits", 76)
        case "checkout": return (ops.first.map { "Check out \($0)" } ?? "Check out a branch", 60)
        case "switch": return (ops.first.map { "Switch to \($0)" } ?? "Switch branches", 60)
        case "reset": return ("Reset the working tree", 72)
        case "add": return ("Stage changes", 40)
        case "status": return ("Check git status", 30)
        case "log": return ("Show recent commits", 25)
        case "diff": return ("Show changes", 25)
        case "rev-parse": return ("Resolve a git revision", 20)
        case "worktree": return ("Manage worktrees", 55)
        case "clone": return ("Clone a repository", 70)
        default: return (action.isEmpty ? "Run a git command" : "Run `git \(action)`", 55)
        }
    }

    private static func defaultPurpose(_ inv: Invocation) -> (String, Int) {
        let readOnly: Set<String> = ["ls", "cat", "pwd", "echo", "which", "head", "tail", "grep",
                                     "find", "file", "stat", "env", "printenv", "whoami", "date"]
        if readOnly.contains(inv.program) {
            return ("Inspect the host (\(backticked(inv.program)))", 15)
        }
        return ("Run \(backticked(inv.headline))", 30)
    }

    // MARK: - Helpers

    private static func flagValue(_ inv: Invocation, _ names: [String], env: String? = nil) -> String? {
        if let env, let assignment = inv.env.first(where: { $0.name == env }) { return assignment.value }
        for name in names {
            if let flag = inv.flags.first(where: { $0.name == name }), let value = flag.value {
                return value
            }
        }
        return nil
    }

    private static func backticked(_ text: String) -> String { "`\(text)`" }

    private static func basename(_ token: String) -> String {
        (token as NSString).lastPathComponent
    }

    private static func isIdentifierLike(_ token: String) -> Bool {
        guard let first = token.first, first.isLetter else { return false }
        return token.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" }
    }

    private static func normalizeDirectory(_ raw: String) -> String {
        if raw.contains("git rev-parse --show-toplevel") { return "the repository root" }
        return raw
    }

    // MARK: - Path abbreviation

    /// Abbreviates an absolute filesystem *directory* path for display in a host card, anchoring it
    /// at the nearest meaningful directory so a "cd /Users/me/…" reads with context but sheds the
    /// noisy base above it. The mobile mirror of the desktop's `HeuristicSummary.abbreviatePath`
    /// (NucleicCore) — the remote app links only `NucleicProtocol`, so it's duplicated here:
    ///  - a path inside a **worktree** (a sibling under the same base as `worktree`, the session's
    ///    own worktree root) trims to "<worktree name>/<rest>", dropping the ".nucleic/worktrees/"
    ///    plumbing above it;
    ///  - otherwise a path inside the **project** (`project`, the repo root) trims to
    ///    "<project dir>/<rest>", so "/Users/me/projects/proj1/subdir" → "proj1/subdir";
    ///  - otherwise a path under the user's home shortens to "~/<rest>";
    ///  - anything else (or an already-relative path) is returned unchanged.
    ///
    /// Unlike the desktop, no symlink-canonicalization is done: the remote only ever sees *host*
    /// paths over the wire, with no local filesystem to resolve them against.
    static func abbreviatePath(_ path: String, worktree: String?, project: String?) -> String {
        guard path.hasPrefix("/") else { return path }

        // A worktree lives at "<base>/<name>"; anchoring at that parent base catches the session's
        // own worktree *and* any sibling one, keeping "<name>/<rest>".
        if let worktree, !worktree.isEmpty,
           let rel = pathComponent(path, under: (worktree as NSString).deletingLastPathComponent) {
            return rel
        }
        // A path inside the repo keeps the project's own folder name as its anchor.
        if let project, !project.isEmpty,
           let rel = pathComponent(path, under: (project as NSString).deletingLastPathComponent) {
            return rel
        }
        // Anything else under the user's home shortens to "~".
        if let home = homePrefix(worktree ?? project), let rel = pathComponent(path, under: home) {
            return "~/" + rel
        }
        return path
    }

    /// The repo root inferred from a worktree path — the segment before "/.nucleic/worktrees/…", so
    /// ".../proj1/.nucleic/worktrees/amber" → ".../proj1" — used to feed `abbreviatePath` a project
    /// anchor the remote doesn't otherwise carry. Nil for a non-default-layout path (a custom
    /// `worktreeBase` won't match; the worktree anchor itself still does).
    static func repoRoot(forWorktree worktree: String?) -> String? {
        guard let worktree, let range = worktree.range(of: "/.nucleic/worktrees/") else { return nil }
        return String(worktree[worktree.startIndex..<range.lowerBound])
    }

    /// The portion of `path` strictly inside directory `base` ("/base/a/b" under "/base" → "a/b"),
    /// or nil when `path` isn't within `base`. The root "/" and an empty base are rejected so they
    /// can't swallow every path into a bare component.
    private static func pathComponent(_ path: String, under base: String) -> String? {
        guard base.hasPrefix("/"), base != "/", !base.isEmpty else { return nil }
        let prefix = base.hasSuffix("/") ? base : base + "/"
        guard path.hasPrefix(prefix) else { return nil }
        let rel = String(path.dropFirst(prefix.count))
        return rel.isEmpty ? nil : rel
    }

    /// The user's home as "/Users/<name>" (or "/home/<name>"), read from `anchor`'s first two
    /// components. Nil when `anchor` (or nil) doesn't look like a per-user home path.
    private static func homePrefix(_ anchor: String?) -> String? {
        guard let anchor else { return nil }
        let comps = anchor.split(separator: "/", omittingEmptySubsequences: true)
        guard comps.count >= 2, comps[0] == "Users" || comps[0] == "home" else { return nil }
        return "/\(comps[0])/\(comps[1])"
    }
}

// MARK: - Shell lexing

/// The just-enough shell-awareness the parser needs — a phone-local copy of the desktop's
/// `ShellLexer` (which lives in NucleicCore, unavailable to the remote): split a command into
/// segments, tokenize one segment honoring quotes, and strip heredoc bodies so a here-doc payload
/// isn't parsed as commands.
private enum Lexer {
    static func stripHeredocs(_ command: String) -> (flattened: String, bodies: [String]) {
        let lines = command.components(separatedBy: "\n")
        var kept: [String] = []
        var bodies: [String] = []
        var index = 0
        while index < lines.count {
            let line = lines[index]
            guard let here = heredocStart(in: line) else {
                kept.append(line)
                index += 1
                continue
            }
            kept.append(here.lineWithoutOperator)
            var body: [String] = []
            index += 1
            while index < lines.count {
                let candidate = here.dashIndented
                    ? lines[index].trimmingCharacters(in: CharacterSet(charactersIn: " \t"))
                    : lines[index]
                if candidate == here.delimiter { index += 1; break }
                body.append(lines[index])
                index += 1
            }
            bodies.append(body.joined(separator: "\n"))
        }
        return (kept.joined(separator: "\n"), bodies)
    }

    private struct Heredoc {
        let delimiter: String
        let dashIndented: Bool
        let lineWithoutOperator: String
    }

    private static func heredocStart(in line: String) -> Heredoc? {
        guard let opRange = line.range(of: #"<<-?\s*(['"]?)[A-Za-z_][A-Za-z0-9_]*\1"#,
                                       options: .regularExpression) else { return nil }
        let op = String(line[opRange])
        let dashIndented = op.hasPrefix("<<-")
        let delimiter = op
            .replacingOccurrences(of: "<<-", with: "")
            .replacingOccurrences(of: "<<", with: "")
            .trimmingCharacters(in: CharacterSet(charactersIn: " \t'\""))
        let without = line.replacingCharacters(in: opRange, with: " ")
        return Heredoc(delimiter: delimiter, dashIndented: dashIndented, lineWithoutOperator: without)
    }

    static func splitSegments(_ command: String) -> [String] {
        var segments: [String] = []
        var current = ""
        var quote: Character? = nil
        let chars = Array(command)
        var i = 0
        func flush() {
            let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
            if !trimmed.isEmpty { segments.append(trimmed) }
            current = ""
        }
        while i < chars.count {
            let c = chars[i]
            if let q = quote {
                current.append(c)
                if c == q { quote = nil }
                i += 1
                continue
            }
            switch c {
            case "'", "\"":
                quote = c; current.append(c); i += 1
            case "\n", ";":
                flush(); i += 1
            case "&" where i + 1 < chars.count && chars[i + 1] == "&":
                flush(); i += 2
            case "|" where i + 1 < chars.count && chars[i + 1] == "|":
                flush(); i += 2
            case "|":
                flush(); i += 1
            default:
                current.append(c); i += 1
            }
        }
        flush()
        return segments
    }

    static func tokenize(_ segment: String) -> [String] {
        var tokens: [String] = []
        var current = ""
        var quote: Character? = nil
        var sawChar = false
        for c in segment {
            if let q = quote {
                if c == q { quote = nil } else { current.append(c) }
                continue
            }
            switch c {
            case "'", "\"":
                quote = c; sawChar = true
            case " ", "\t":
                if sawChar { tokens.append(current); current = ""; sawChar = false }
            default:
                current.append(c); sawChar = true
            }
        }
        if sawChar { tokens.append(current) }
        return tokens
    }

    static func isEnvAssignment(_ token: String) -> Bool {
        guard let eq = token.firstIndex(of: "="), eq != token.startIndex else { return false }
        return token[..<eq].allSatisfy { $0.isLetter || $0.isNumber || $0 == "_" }
    }

    // MARK: - Shell control flow

    /// Keywords that merely *introduce* a command body — `do`/`then`/`else` and the `{` group
    /// opener; the command they precede is what matters, so they're peeled off.
    static let bodyIntroducerKeywords: Set<String> = ["do", "then", "else", "{"]

    /// Pure structural keywords with no operation of their own — a loop/conditional terminator
    /// (`done`/`fi`/`esac`/`}`), the `for … in` connective, and `break`/`continue`; skipped like `cd`.
    static let structuralKeywords: Set<String> = ["done", "fi", "esac", "}", "in", "break", "continue"]

    /// The keywords that open a *loop* header — `for`/`while`/`until`/`select`.
    static let loopKeywords: Set<String> = ["for", "while", "until", "select"]

    /// The keywords that open a *conditional* header — `if`/`elif`/`case`.
    static let conditionalKeywords: Set<String> = ["if", "elif", "case"]

    /// Drops any leading body-introducer keywords (`do`/`then`/`else`/`{`) from a tokenized
    /// segment, so the command they precede lands at the front (`then rm -rf x` → `rm -rf x`).
    static func stripBodyIntroducers(_ tokens: [String]) -> [String] {
        var rest = tokens[...]
        while let first = rest.first, bodyIntroducerKeywords.contains(first) {
            rest = rest.dropFirst()
        }
        return Array(rest)
    }
}

// MARK: - Git commit summary

/// Pulls the commit message out of a command that runs `git commit`, so a commit reads as a
/// structured card (subject headline + Markdown body) instead of a raw `git commit -F - <<'EOF' …`
/// blob — the remote's echo of the desktop `GitCommandSummary`/`GitBlockCard` (which live in
/// NucleicCore, unavailable here). Handles the two shapes agents actually use: `-m`/`--message`
/// arguments and a `-F -`/`--file=-` heredoc body. Parsed from the command string alone, reusing
/// the same shell-aware ``Lexer`` the host-exec summary uses.
enum GitCommitSummary {
    struct Commit: Equatable {
        var subject: String
        var body: String
        var amend: Bool
    }

    static func parse(_ command: String) -> Commit? {
        let (flattened, bodies) = Lexer.stripHeredocs(command)
        guard let args = commitArguments(in: flattened) else { return nil }

        let amend = args.contains("--amend")
        var messages: [String] = []
        var usesStdinFile = false
        var i = 0
        while i < args.count {
            let a = args[i]
            if a == "-m" || a == "--message" {
                if i + 1 < args.count { messages.append(args[i + 1]); i += 2; continue }
            } else if a.hasPrefix("--message=") {
                messages.append(String(a.dropFirst("--message=".count)))
            } else if a.hasPrefix("-m"), a.count > 2 {
                messages.append(String(a.dropFirst(2)))
            } else if (a == "-F" || a == "--file"), i + 1 < args.count, args[i + 1] == "-" {
                usesStdinFile = true
            } else if a == "--file=-" || a == "-F-" {
                usesStdinFile = true
            }
            i += 1
        }

        // Prefer explicit `-m` messages; otherwise a `-F -` heredoc body is the message.
        let message: String
        if !messages.isEmpty {
            message = messages.joined(separator: "\n\n")
        } else if usesStdinFile,
                  let body = bodies.first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) {
            message = body
        } else {
            return nil
        }

        // Subject = first non-blank line; body = everything after it.
        let lines = message.components(separatedBy: "\n")
        guard let subjectIndex = lines.firstIndex(where: { !$0.trimmingCharacters(in: .whitespaces).isEmpty }) else {
            return nil
        }
        let subject = lines[subjectIndex].trimmingCharacters(in: .whitespaces)
        let body = lines[(subjectIndex + 1)...].joined(separator: "\n")
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return Commit(subject: subject, body: body, amend: amend)
    }

    /// The arguments after `git commit` in whichever segment runs it (skipping env assignments and
    /// git's global flags like `-C <dir>` / `-c k=v`), or nil when no segment is a `git commit`.
    private static func commitArguments(in flattened: String) -> [String]? {
        for segment in Lexer.splitSegments(flattened) {
            var tokens = Lexer.tokenize(segment)
            while let first = tokens.first, Lexer.isEnvAssignment(first) { tokens.removeFirst() }
            guard tokens.first == "git" else { continue }
            var rest = Array(tokens.dropFirst())
            while let flag = rest.first, flag.hasPrefix("-") {
                let takesValue = (flag == "-C" || flag == "-c")
                rest.removeFirst()
                if takesValue, !rest.isEmpty { rest.removeFirst() }
            }
            guard rest.first == "commit" else { continue }
            return Array(rest.dropFirst())
        }
        return nil
    }
}

// MARK: - ToolGroup convenience

extension ToolGroup {
    /// Whether this is the host-exec gate (a command run on the macOS host, outside the sandbox).
    var isHostExec: Bool { name == HostCommandSummary.hostExecToolName }
    /// The host command this call runs, when it's a host-exec call.
    var hostCommand: String? { input["command"]?.stringValue }
    /// The tool name as shown to the user — the host-exec gate reads as a clean "Host" tag, and
    /// the VM/container tools as where they run ("macOS VM"), rather than their raw
    /// `mcp__nucleic__…` wire names.
    var displayName: String {
        if isHostExec { return "Host" }
        return SandboxToolDisplay.label(for: name) ?? name
    }

    /// The one-line body beside the name: the sandbox tools parse their own arguments (a shell
    /// line as `$ command`, a computer-use step as "left click (120, 400)"); everything else keeps
    /// the generic JSON gist.
    var displayDetail: String {
        SandboxToolDisplay.detail(for: name, input: input) ?? input.compactSummary
    }
}
