import Foundation
import NucleicProtocol

/// Presentation for Nucleic's own sandbox-escape tools — the macOS/Linux VM pair, their
/// computer-use siblings, and `linux_container` — plus the shell tool, whose wire name is still
/// Claude Code's `Bash` but which runs nash (the Nucleic agent shell) — so a remote transcript
/// reads the same way the Mac's does: "macOS VM · $ swift build" rather than the raw
/// `mcp__nucleic__mac_vm_exec` wire name followed by a JSON gist.
///
/// The mobile echo of `NucleicCore/SandboxToolDisplay.swift`, trimmed to what this app shows (a
/// label, a glyph, a one-line body, the composer's working line — iOS has no tool-block verb). It
/// carries the tool names as literals because the remote links only `NucleicProtocol`, not
/// `NucleicCore` where they're declared; keep the two in step when a tool is added.
///
/// `host_exec` is deliberately absent — it already has `ToolGroup.displayName` and a structured
/// card of its own.
enum SandboxToolDisplay {

    /// The MCP prefix every Nucleic tool is advertised under; transcripts may record either the
    /// qualified or the bare name, so every lookup matches on the stripped form.
    private static let mcpPrefix = "mcp__nucleic__"

    private static func bareName(_ toolName: String) -> String {
        toolName.hasPrefix(mcpPrefix) ? String(toolName.dropFirst(mcpPrefix.count)) : toolName
    }

    /// The human label that replaces the raw tool name in a card headline. Nil for anything this
    /// file doesn't speak for, so the caller keeps the tool name unchanged.
    static func label(for toolName: String) -> String? {
        switch bareName(toolName) {
        // The wire name is Claude Code's, but the shell behind it is nash.
        case "Bash":                                        "Nash"
        case "BashOutput":                                  "Nash output"
        case "mac_vm_exec", "mac_vm_control":               "macOS VM"
        case "mac_vm_computer", "mac_vm_computer_batch":    "macOS VM screen"
        case "mac_vm_clear_notifications":                  "macOS VM notifications"
        case "mac_vm_request_operator":                     "macOS VM handoff"
        case "linux_vm_exec", "linux_vm_control":           "Linux VM"
        case "linux_vm_computer", "linux_vm_computer_batch": "Linux VM screen"
        case "linux_container":                             "Linux container"
        default:                                             nil
        }
    }

    /// The tool name for an *approval* surface, where identity is what the user is deciding on:
    /// the pretty label when we have one, and otherwise the tool name with the `mcp__nucleic__`
    /// plumbing prefix dropped. Never nil and never invented — a tool this file doesn't know
    /// still reads as its own identifier rather than as something friendlier than it is.
    static func gateLabel(for toolName: String) -> String {
        label(for: toolName) ?? bareName(toolName)
    }

    /// The present-progressive working-row line — "Running on a macOS VM…". Nil for tools this
    /// file doesn't speak for, so the caller keeps its own "Running <name>…" fallback. Mirrors
    /// `NucleicCore/SandboxToolDisplay.gerund(for:)` so the phone's composer names a call exactly
    /// the way the Mac's status line does.
    static func gerund(for toolName: String) -> String? {
        switch bareName(toolName) {
        case "mac_vm_exec":                    "Running on a macOS VM…"
        case "linux_vm_exec":                  "Running on a Linux VM…"
        case "linux_container":                "Running in a Linux container…"
        case "mac_vm_computer", "mac_vm_computer_batch":     "Operating a macOS VM…"
        case "linux_vm_computer", "linux_vm_computer_batch": "Operating a Linux VM…"
        case "mac_vm_clear_notifications":     "Clearing VM notifications…"
        case "mac_vm_request_operator":        "Waiting for you to drive the VM…"
        case "mac_vm_control":                 "Managing the macOS VM…"
        case "linux_vm_control":               "Managing the Linux VM…"
        default:                                nil
        }
    }

    /// SF Symbol for the tool — a screen for computer-use, a box for a container, a power switch
    /// for a lifecycle op — so the glyph carries the same distinction the label does.
    static func icon(for toolName: String) -> String? {
        switch bareName(toolName) {
        case "mac_vm_exec":                    "macwindow"
        case "linux_vm_exec":                  "server.rack"
        case "linux_container":                "shippingbox"
        case "mac_vm_computer", "mac_vm_computer_batch",
             "linux_vm_computer", "linux_vm_computer_batch": "cursorarrow.motionlines"
        case "mac_vm_clear_notifications":     "bell.slash"
        case "mac_vm_request_operator":        "hand.raised"
        case "mac_vm_control", "linux_vm_control": "power"
        default:                                nil
        }
    }

    /// The one-line rendering of a call's arguments, parsed per tool rather than dumped as JSON.
    /// Nil when the tool isn't one of ours or the call carries nothing worth showing, in which
    /// case the caller falls back to the generic input gist.
    static func detail(for toolName: String, input: JSONValue) -> String? {
        switch bareName(toolName) {
        case "mac_vm_exec", "linux_vm_exec":
            return string(input, "command").map { "$ \(singleLine($0))" }
        case "mac_vm_computer", "linux_vm_computer":
            return computerAction(input)
        case "mac_vm_computer_batch", "linux_vm_computer_batch":
            return batchSummary(input)
        case "linux_container":
            return containerOp(input)
        case "mac_vm_control", "linux_vm_control":
            return string(input, "op").map(controlOpPhrase)
        // The instructions are what the user is being asked to do — the point of the hand-off.
        case "mac_vm_request_operator":
            return string(input, "instructions").map(singleLine)
        // Takes no arguments: nothing to print beside the label.
        case "mac_vm_clear_notifications":
            return nil
        default:
            return nil
        }
    }

    /// One computer-use step as prose — "left click (120, 400)", "type “hello”", "launch Xcode".
    private static func computerAction(_ step: JSONValue) -> String? {
        guard let action = string(step, "action") else { return nil }
        let point: String? = {
            guard let x = step["x"]?.intValue, let y = step["y"]?.intValue else { return nil }
            return "(\(x), \(y))"
        }()
        let verb = actionVerb(action)
        switch action {
        case "screenshot", "cursor_position", "ax_dump":
            return verb
        case "left_click", "right_click", "double_click", "mouse_move", "left_click_drag",
             "ax_element_at":
            return [verb, point].compactMap { $0 }.joined(separator: " ")
        case "type":
            return string(step, "text").map { "\(verb) “\(clip(singleLine($0)))”" } ?? verb
        case "key", "launch_app":
            return string(step, "text").map { "\(verb) \(clip($0))" } ?? verb
        // The starting page is the informative half of an `open_browser` with a `url` — "open chrome"
        // says far less than the address it lands on.
        case "open_browser":
            let target = string(step, "url") ?? string(step, "text")
            return target.map { "\(verb) \(clip($0))" } ?? verb
        case "scroll":
            let amount = step["scroll_amount"]?.intValue.map { String($0) }
            return [verb, string(step, "scroll_direction"), amount].compactMap { $0 }
                .joined(separator: " ")
        case "wait":
            return step["duration_ms"]?.intValue.map { "\(verb) \($0)ms" } ?? verb
        case "ax_press", "ax_focus":
            return string(step, "ref").map { "\(verb) \(clip($0))" } ?? verb
        case "ax_set_value":
            let value = string(step, "value").map { "“\(clip(singleLine($0)))”" }
            return [verb, string(step, "ref"), value].compactMap { $0 }.joined(separator: " ")
        default:
            return [verb, point].compactMap { $0 }.joined(separator: " ")
        }
    }

    /// "5 steps — left click, type, key…": the count plus the leading verbs, so the row says what
    /// the sequence does without unrolling every step's coordinates.
    private static func batchSummary(_ input: JSONValue) -> String? {
        guard let steps = input["steps"]?.arrayValue, !steps.isEmpty else { return nil }
        let count = "\(steps.count) step\(steps.count == 1 ? "" : "s")"
        let verbs = steps.prefix(3).compactMap { $0["action"]?.stringValue }.map(actionVerb)
        guard !verbs.isEmpty else { return count }
        let tail = steps.count > verbs.count ? "…" : ""
        return "\(count) — \(verbs.joined(separator: ", "))\(tail)"
    }

    /// A `linux_container` call as "op — argument", showing whichever field that op uses.
    private static func containerOp(_ input: JSONValue) -> String? {
        guard let op = string(input, "op") else { return nil }
        let subject: String? = switch op {
        case "exec":   string(input, "command").map { "$ \(singleLine($0))" }
        case "create": string(input, "image") ?? string(input, "name")
        default:       string(input, "name")
        }
        guard let subject else { return op }
        return "\(op) — \(subject)"
    }

    /// A VM lifecycle op as a phrase, so "status" doesn't read as a state label.
    private static func controlOpPhrase(_ op: String) -> String {
        switch op {
        case "status":  "check status"
        case "stop":    "power off"
        case "suspend": "suspend"
        case "resume":  "resume"
        case "restart": "restart"
        case "kill":    "delete and reset"
        default:        op
        }
    }

    /// `left_click` → "left click"; the `ax_` prefix expands, since the abbreviation means
    /// nothing to a reader.
    private static func actionVerb(_ action: String) -> String {
        switch action {
        case "ax_dump":       return "accessibility dump"
        case "ax_element_at": return "accessibility element at"
        case "ax_press":      return "press"
        case "ax_set_value":  return "set"
        case "ax_focus":      return "focus"
        default:              return action.replacingOccurrences(of: "_", with: " ")
        }
    }

    /// A non-empty, whitespace-trimmed string argument, or nil.
    private static func string(_ value: JSONValue, _ key: String) -> String? {
        guard let raw = value[key]?.stringValue else { return nil }
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }

    /// Collapses a multi-line argument onto one line — a heredoc would blow out a single-line row.
    private static func singleLine(_ text: String) -> String {
        text.split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .joined(separator: " ⏎ ")
    }

    /// Keeps an inline argument short enough that the label beside it stays readable.
    private static func clip(_ text: String, limit: Int = 40) -> String {
        text.count <= limit ? text : String(text.prefix(limit)) + "…"
    }
}
