import SwiftUI
import NucleicProtocol

/// A `mcp__nucleic__host_exec` tool call in the transcript, rendered as a structured card — the
/// mobile echo of the Mac's `HostExecToolCard`. It leads with the deterministic, command-derived
/// purpose (the easy-to-read headline), shows the literal `$ command` beneath it, and reveals the
/// parsed breakdown + output on tap — instead of the generic `mcp__nucleic__host_exec {…}` row the
/// catch-all tool card would show. The desktop glyph and "Host" tag flag it as running on the
/// macOS host, outside the sandbox.
struct HostExecToolCard: View {
    let group: ToolGroup
    @State private var expanded = false

    private var command: String { group.hostCommand ?? group.input.compactSummary }
    /// The breakdown is parsed from the command string alone — never from anything the agent said.
    private var parsed: HostCommandSummary.Summary? { HostCommandSummary.summary(for: command) }
    private var output: String? {
        let text = group.result?.compactSummary ?? ""
        return text.isEmpty ? nil : text
    }
    private var hasOutput: Bool { output != nil }

    /// Whether the parse carries structure worth revealing (a working dir, more than one step, or
    /// any flags/args/env) — so a richer command is expandable even before it has output.
    private var hasBreakdown: Bool {
        guard let parsed else { return false }
        if parsed.workingDirectory != nil || parsed.invocations.count > 1 { return true }
        return parsed.invocations.contains {
            !$0.flags.isEmpty || !$0.arguments.isEmpty || !$0.env.isEmpty
        }
    }
    private var canExpand: Bool { hasOutput || hasBreakdown }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Button { if canExpand { withAnimation(.easeInOut(duration: 0.15)) { expanded.toggle() } } }
                label: { header }
                .buttonStyle(.plain)
            if expanded {
                if hasBreakdown, let parsed {
                    Divider().overlay(Color.primary.opacity(0.06))
                    HostCommandBreakdown(summary: parsed, showPurpose: false)
                }
                if let output {
                    ToolHostBlock(label: group.isError ? "Error" : "Output",
                                  text: output, danger: group.isError)
                }
            }
        }
        .padding(10)
        .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 10))
        .overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(
            group.isError ? Palette.danger.opacity(0.5) : Color.primary.opacity(0.05), lineWidth: 1))
    }

    private var header: some View {
        HStack(alignment: .top, spacing: 8) {
            Image(systemName: "desktopcomputer")
                .font(.caption).foregroundStyle(Palette.accent).frame(width: 16)
            VStack(alignment: .leading, spacing: 3) {
                HStack(alignment: .firstTextBaseline, spacing: 6) {
                    Text("HOST").font(.caption2.weight(.bold)).foregroundStyle(Palette.accent)
                    if let parsed {
                        // The command-derived purpose leads as the bold headline; "HOST" is just a
                        // quiet tag (the desktop glyph already marks it as a host call).
                        Text(MessageBubble.markdown(parsed.purpose))
                            .font(.callout.weight(.semibold)).lineLimit(1).truncationMode(.tail)
                    } else {
                        Text("Host").font(.callout.weight(.semibold))
                    }
                }
                HStack(alignment: .firstTextBaseline, spacing: 5) {
                    Text("$").font(.caption.monospaced()).foregroundStyle(Palette.accent.opacity(0.7))
                    Text(command)
                        .font(.caption.monospaced()).foregroundStyle(.secondary)
                        // Expanded, the literal stays bounded — a long chain's command is itself
                        // long, and the compact step list above carries the structure.
                        .lineLimit(expanded ? 6 : 1).truncationMode(.middle)
                        .textSelection(.enabled)
                }
            }
            Spacer(minLength: 4)
            if !group.finished { ProgressView().controlSize(.mini) }
            if canExpand {
                Image(systemName: expanded ? "chevron.down" : "chevron.right")
                    .font(.caption2).foregroundStyle(.tertiary).padding(.top, 2)
            }
        }
        .contentShape(Rectangle())
    }
}

/// The shared, deterministic breakdown of a host command — driven by ``HostCommandSummary``
/// (parsed from the command string, never from the agent). The mobile echo of the Mac's
/// `HostCommandBreakdown`: the inferred purpose (optionally), the working directory, and each
/// program invocation as program + actions + flags + operands, so the user can verify *exactly*
/// what's about to run. Used by ``HostExecToolCard`` (in chat) and the approval card.
struct HostCommandBreakdown: View {
    let summary: HostCommandSummary.Summary
    /// Whether to render the prominent "Likely purpose" row and any `sudo`/destructive banner.
    /// The chat card heads its row with the purpose already, so it passes `false`.
    var showPurpose: Bool = true

    private let accent = Palette.accent

    var body: some View {
        VStack(alignment: .leading, spacing: 9) {
            if showPurpose {
                purposeRow
                if summary.isElevated || summary.isDestructive { riskBanner }
            }
            if let dir = summary.workingDirectory {
                detailRow(icon: "folder", lines: ["in \(dir)"])
            }
            steps
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    /// At most this many steps render in the compact chain list before the rest fold into a
    /// "+N more" tail — so a long `cmd1 && cmd2 && …` pipeline can't grow the card unbounded
    /// (the exact command shown alongside is always the complete source of truth).
    private static let maxVisibleSteps = 7

    /// The invocations, rendered to fit the chain length: a lone command gets the full block
    /// (program + actions + every env/flag/operand spelled out); a *chain* collapses to one
    /// compact line per command so a long pipeline stays short, not a tall stack of blocks.
    @ViewBuilder private var steps: some View {
        if summary.invocations.count <= 1 {
            if let only = summary.invocations.first { invocationView(only) }
        } else {
            VStack(alignment: .leading, spacing: 6) {
                ForEach(Array(summary.invocations.prefix(Self.maxVisibleSteps))) { inv in
                    stepRow(inv)
                }
                let hidden = summary.invocations.count - Self.maxVisibleSteps
                if hidden > 0 {
                    Text("+\(hidden) more step\(hidden == 1 ? "" : "s")")
                        .font(.caption2).foregroundStyle(.tertiary)
                        .padding(.leading, 22)
                }
            }
        }
    }

    /// One command in a chain, compacted to a single line: a step glyph, the program + actions
    /// emphasized, then a truncated tail of its env/flags/operands. A destructive/elevated step is
    /// tinted and glyphed in the danger color so it still stands out.
    private func stepRow(_ inv: HostCommandSummary.Invocation) -> some View {
        let risky = inv.destructive || inv.elevated
        let tail = stepTail(inv)
        return HStack(alignment: .firstTextBaseline, spacing: 8) {
            Image(systemName: stepGlyph(inv))
                .font(.caption2).foregroundStyle(risky ? Palette.danger : accent).frame(width: 14)
            (Text(inv.headline)
                .font(.caption.monospaced().weight(.semibold))
                .foregroundColor(.primary)
             + Text(tail.isEmpty ? "" : " " + tail)
                .font(.caption.monospaced())
                .foregroundColor(.secondary))
                .lineLimit(1).truncationMode(.middle)
                .textSelection(.enabled)
            Spacer(minLength: 0)
        }
    }

    /// The trailing detail of a compacted step — its env, flags, and operands joined on one line,
    /// truncated by the row. The literal command carries the untruncated form.
    private func stepTail(_ inv: HostCommandSummary.Invocation) -> String {
        (inv.env.map(\.display) + inv.flags.map(\.display) + inv.arguments).joined(separator: " ")
    }

    /// The leading glyph for a compacted step: a delete/elevation marker takes priority so the
    /// risky step is unmistakable; otherwise a plain terminal glyph.
    private func stepGlyph(_ inv: HostCommandSummary.Invocation) -> String {
        if inv.destructive { return "trash" }
        if inv.elevated { return "lock.shield" }
        return "terminal"
    }

    /// The deterministic, command-derived purpose — labeled so the user knows it's *Nucleic's*
    /// reading of the command, not the agent's claim about it.
    private var purposeRow: some View {
        HStack(alignment: .top, spacing: 10) {
            Image(systemName: "sparkles").font(.title3).foregroundStyle(accent).frame(width: 22)
            VStack(alignment: .leading, spacing: 3) {
                Text(MessageBubble.markdown(summary.purpose))
                    .font(.title3.weight(.semibold))
                    .fixedSize(horizontal: false, vertical: true)
                    .textSelection(.enabled)
                Text("Likely purpose · inferred from the command")
                    .font(.caption2.weight(.medium)).foregroundStyle(.secondary)
            }
            Spacer(minLength: 0)
        }
        .padding(.horizontal, 11).padding(.vertical, 9)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
        .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(accent.opacity(0.22), lineWidth: 1))
    }

    /// A red banner for the two things a host command most needs flagged before "Allow": running
    /// as root (`sudo`) and deleting files (`rm`).
    private var riskBanner: some View {
        let icon = summary.isElevated ? "lock.shield" : "trash"
        let text = summary.isElevated
            ? "Runs with elevated privileges (sudo)."
            : "Deletes files on the host."
        return HStack(alignment: .firstTextBaseline, spacing: 7) {
            Image(systemName: icon).font(.caption).foregroundStyle(Palette.danger).frame(width: 16)
            Text(text).font(.caption.weight(.medium)).foregroundStyle(Palette.danger)
            Spacer(minLength: 0)
        }
        .padding(.horizontal, 8).padding(.vertical, 5)
        .background(Palette.danger.opacity(0.1), in: RoundedRectangle(cornerRadius: 6))
    }

    /// One program invocation: the program name as a filled badge, its actions as outlined chips,
    /// and — beneath — its env, flags, and operands each as a labeled, monospaced list.
    private func invocationView(_ inv: HostCommandSummary.Invocation) -> some View {
        VStack(alignment: .leading, spacing: 5) {
            HStack(spacing: 6) {
                if inv.elevated { chip("sudo", color: Palette.danger, filled: true) }
                chip(inv.program, color: accent, filled: true)
                ForEach(inv.actions, id: \.self) { chip($0, color: accent, filled: false) }
                if inv.destructive { chip("deletes", color: Palette.danger, filled: false) }
                Spacer(minLength: 0)
            }
            if !inv.env.isEmpty {
                detailRow(icon: "leaf", lines: inv.env.map(\.display))
            }
            if !inv.flags.isEmpty {
                detailRow(icon: "slider.horizontal.3", lines: inv.flags.map(\.display))
            }
            if !inv.arguments.isEmpty {
                detailRow(icon: "chevron.right", lines: [inv.arguments.joined(separator: " ")])
            }
        }
    }

    /// A small chip — a filled program badge or an outlined action/marker pill.
    private func chip(_ text: String, color: Color, filled: Bool) -> some View {
        Text(text)
            .font(.caption2.monospaced().weight(.medium))
            .foregroundStyle(color)
            .padding(.horizontal, 6).padding(.vertical, 2)
            .background(color.opacity(filled ? 0.16 : 0), in: Capsule())
            .overlay(Capsule().strokeBorder(color.opacity(filled ? 0 : 0.4), lineWidth: 0.75))
    }

    /// A labeled detail block: a small tertiary glyph and a monospaced list (env vars, one flag
    /// per line, or the operands) — the granular pieces the user scans to verify the command.
    private func detailRow(icon: String, lines: [String]) -> some View {
        HStack(alignment: .top, spacing: 7) {
            Image(systemName: icon).font(.caption2).foregroundStyle(.tertiary)
                .frame(width: 14).padding(.top, 1)
            VStack(alignment: .leading, spacing: 2) {
                ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
                    Text(line)
                        .font(.caption.monospaced()).foregroundStyle(.secondary)
                        .textSelection(.enabled)
                        .fixedSize(horizontal: false, vertical: true)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
            }
        }
    }
}

/// A labeled block of monospaced text for a host call's output — mirrors the transcript's other
/// tool blocks, bounded in height and selectable.
private struct ToolHostBlock: View {
    let label: String
    let text: String
    var danger: Bool = false

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label.uppercased()).font(.caption2.weight(.semibold)).foregroundStyle(.tertiary)
            Text(text)
                .font(.caption.monospaced())
                .foregroundStyle(danger ? Palette.danger : .secondary)
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
                .frame(maxHeight: 220)
        }
    }
}
