import SwiftUI
import NucleicProtocol

/// Read-friendly rendering of one projected transcript row (UX_IOS §4) — the mobile echo of the
/// Mac transcript. Assistant/user prose as Markdown bubbles, tool calls as collapsible cards,
/// Orchestra spawns in gold, plus the informational rows (usage/cost, rate limits, turn
/// boundaries, run outcome) the older remote dropped.
struct TranscriptRow: View {
    let item: TranscriptItem

    var body: some View {
        switch item.kind {
        case .message(let role, let text):
            MessageBubble(role: role, text: text)
        case .thinking(let text):
            ThinkingRow(text: text)
        case .tool(let group):
            // A completed AskUserQuestion reads as a pretty "You answered" Q&A card; a git-commit
            // pipeline as a structured commit card; a host_exec call as a host card (purpose +
            // command + breakdown); a lone subagent spawn as its own gold subagent card; every
            // other tool — and an as-yet-unanswered question — is a plain collapsible card. Mirrors
            // the Mac's `toolRow` routing so each family reads the same on both platforms.
            if group.isOrchestration {
                SubagentCard(group: group)
            } else if let card = answeredQuestionCard(group) {
                card
            } else if let plan = planCard(group) {
                plan
            } else if let card = gitCommitRowCard(group) {
                card
            } else if group.isHostExec {
                HostExecToolCard(group: group)
            } else {
                ToolCallCard(group: group)
            }
        case .toolBlock(let groups):
            // A run made entirely of subagent spawns is a multi-agent fan-out — the gold
            // orchestration card; any other run is one contiguous tool block.
            if groups.allSatisfy(\.isOrchestration) {
                OrchestrationCard(groups: groups)
            } else {
                ToolBlockCard(groups: groups)
            }
        case .sessionStarted(let model, let cwd):
            InfoLine(icon: "play.circle", text: "Session started · \(modelLabel(model))", detail: cwd)
        case .usage(let usage):
            UsageRow(usage: usage)
        case .rateLimit:
            // Rate-limit notices are intentionally hidden from the transcript.
            EmptyView()
        case .turnBoundary:
            TurnBoundaryRow()
        case .approval(let toolName):
            Label("Approval requested: \(toolName)", systemImage: "exclamationmark.triangle.fill")
                .font(.caption).foregroundStyle(Palette.attention)
        case .runFinished(let outcome):
            RunFinishedRow(outcome: outcome)
        case .error(let message):
            Label(message, systemImage: "xmark.octagon.fill")
                .font(.caption).foregroundStyle(Palette.danger)
        case .note(let text, let icon, _, _, let processStall):
            // A live "host command looks hung" alert reads as a distinct amber row with Kill /
            // Keep-waiting buttons (control parity with the Mac's `ProcessStallRow`); once resolved
            // — or for any non-stall note — it falls back to the plain secondary label.
            if let stall = processStall, stall.resolution == nil {
                ProcessStallRow(text: text, stall: stall)
            } else {
                Label(text, systemImage: icon ?? "arrow.triangle.branch")
                    .font(.caption).foregroundStyle(.secondary)
            }
        case .raw(let type, let body):
            Text("[\(type)] \(body)").font(.caption2.monospaced()).foregroundStyle(.secondary).lineLimit(2)
        }
    }

    private func modelLabel(_ model: String) -> String { model.isEmpty ? "agent" : model }

    /// The pretty card for a *completed* AskUserQuestion whose result records the user's choices,
    /// or nil when this isn't that (wrong tool, still pending, errored, or an unexpected result
    /// shape) — in which case the row falls back to a normal tool card. Mirrors the Mac's
    /// `answeredQuestionCard`.
    private func answeredQuestionCard(_ group: ToolGroup) -> AskUserQuestionResultCard? {
        guard group.isAskUserQuestion, !group.isError, let result = group.result,
              let questions = AskUserQuestion.questions(from: group.input) else { return nil }
        let answers = AskUserQuestion.answers(in: result.resultText, for: questions)
        guard !answers.isEmpty else { return nil }
        return AskUserQuestionResultCard(questions: questions, answers: answers)
    }

    /// The plan an `ExitPlanMode` call presents, rendered as formatted Markdown rather than the
    /// escaped-JSON blob the generic tool card would show — the phone echo of the Mac's `planCard`.
    /// Nil for any other tool (which keeps the normal collapsible card) and for a call with no plan
    /// text. The plan is on the call input, so it shows whether or not the user has approved it yet.
    private func planCard(_ group: ToolGroup) -> ExitPlanModeCard? {
        guard group.isExitPlanMode, let plan = ExitPlanMode.plan(from: group.input) else { return nil }
        return ExitPlanModeCard(plan: plan)
    }

    /// A git-commit `Bash` pipeline rendered as the structured commit card the Mac shows at the row
    /// level (subject + Markdown body + the new hash + "Show command"), or nil for any non-commit
    /// call — which keeps the normal collapsible tool card. Mirrors the Mac's `gitCommitCard`.
    private func gitCommitRowCard(_ group: ToolGroup) -> GitCommitCard? {
        guard let command = group.bashCommand, let commit = GitCommitSummary.parse(command)
        else { return nil }
        let output = group.result.map(\.resultText).flatMap { $0.isEmpty ? nil : $0 }
        return GitCommitCard(commit: commit, rawCommand: command, output: output)
    }
}

/// The in-chat rendering for a live "host command looks hung" alert — the mobile echo of the Mac's
/// `ProcessStallRow`, for control parity (UX_IOS §5). The harness raises it when a running
/// `host_exec` command goes silent with near-zero CPU (see `ProcessStallMonitor`). It reads as an
/// amber warning row with **Kill** (tears down the command's whole process tree) and **Keep
/// waiting** (dismiss the alert; the command runs on). Once resolved — killed, dismissed, or the
/// command ended on its own — the buttons give way to a muted "resolved" note. Buttons are hidden
/// when the device lacks control scope or the host can't resolve stalls remotely (older host),
/// degrading to a plain warning line.
struct ProcessStallRow: View {
    @EnvironmentObject var store: RemoteStore
    let text: String
    let stall: ProcessStallNote

    private var resolved: Bool { store.resolvedStalls.contains(stall.id) }
    private var canResolve: Bool {
        store.canControl && store.capabilities.canResolveProcessStall && store.openSessionID != nil
    }

    var body: some View {
        HStack(alignment: .firstTextBaseline, spacing: 6) {
            Image(systemName: "exclamationmark.triangle.fill")
                .font(.caption).foregroundStyle(Palette.attention).frame(width: 16)
            Text(text)
                .font(.callout).foregroundStyle(.primary)
                .fixedSize(horizontal: false, vertical: true).textSelection(.enabled)
            if resolved {
                Text("resolved").font(.caption).foregroundStyle(.secondary)
            } else if canResolve, let sessionID = store.openSessionID {
                Button {
                    store.resolveProcessStall(stall.id, kill: true, inSession: sessionID)
                } label: {
                    Label(
                        stall.pidCount > 1 ? "Kill (\(stall.pidCount) processes)" : "Kill",
                        systemImage: "xmark.octagon")
                        .font(.caption.weight(.semibold))
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.small)
                .tint(Palette.danger)
                Button("Keep waiting") {
                    store.resolveProcessStall(stall.id, kill: false, inSession: sessionID)
                }
                .buttonStyle(.bordered)
                .controlSize(.small)
            }
            Spacer(minLength: 0)
        }
    }
}

/// Render an assistant/user message with the full Markdown renderer the Mac transcript uses
/// (fenced code blocks, headings, lists, tables, and inline emphasis/links/`code`), preserving
/// the line breaks of multi-paragraph replies. Falls back to plain text if parsing fails.
/// The user's messages sit right-aligned in a soft accent bubble; the assistant's prose runs
/// open on the background — no box — so the transcript reads as a page, not a stack of cards.
struct MessageBubble: View {
    let role: TranscriptItem.Role
    let text: String

    var body: some View {
        if role == .user {
            UserMessageBubble(text: text)
        } else {
            MarkdownText(markdown: text)
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }

    /// Inline-only Markdown for one-line contexts (host-exec purpose lines etc.), where the
    /// block renderer above would be overkill.
    static func markdown(_ string: String) -> AttributedString {
        (try? AttributedString(markdown: string, options: .init(
            interpretedSyntax: .inlineOnlyPreservingWhitespace,
            failurePolicy: .returnPartiallyParsedIfPossible))) ?? AttributedString(string)
    }
}

/// The user's own message: a right-aligned accent bubble that collapses a long message to a
/// capped height with a "Show more"/"Show less" toggle — the phone echo of the Mac's
/// `UserMessageBubble`. The Mac clamps a plain `Text` by line count; here the bubble renders
/// through `MarkdownText` (a stack of per-line/-block `Text`s that `.lineLimit` can't reach), so
/// we clamp by height instead. A hidden full-height copy of the bubble — laid out at the visible
/// bubble's width but with no height limit — measures the natural content height, so the toggle
/// appears only when collapsing actually hides something. The last visible line fades when
/// collapsed so the cut reads as "more below" rather than a glyph sliced in half.
private struct UserMessageBubble: View {
    let text: String
    @State private var expanded = false
    @State private var fullHeight: CGFloat = 0
    /// Collapsed bubbles show at most this much prose (~12 wrapped lines at the base size, matching
    /// the Mac's 12-line clamp) before a "Show more". A `@ScaledMetric` so it grows with the user's
    /// text-size setting alongside `MarkdownText`'s own scaled prose size.
    @ScaledMetric(relativeTo: .body) private var collapsedMaxHeight: CGFloat = 288
    /// Collapsing only hides something — and the toggle is only meaningful — when the full,
    /// unconstrained content is taller than the collapsed cap. The +1 absorbs sub-point rounding.
    private var isTruncated: Bool { fullHeight > collapsedMaxHeight + 1 }
    private var collapsed: Bool { isTruncated && !expanded }

    var body: some View {
        VStack(alignment: .trailing, spacing: 4) {
            HStack {
                Spacer(minLength: 48)
                MarkdownText(markdown: text)
                    .textSelection(.enabled)
                    .padding(.horizontal, 14)
                    .padding(.vertical, 9)
                    // Cap the height whenever collapsed; harmless for a short message (the cap sits
                    // above its natural height, so nothing clips). `.clipped` cuts the overflow.
                    .frame(maxHeight: expanded ? nil : collapsedMaxHeight, alignment: .top)
                    .clipped()
                    .mask {
                        if collapsed { collapseFade } else { Color.black }
                    }
                    // The bubble fill is added after the mask, so only the prose fades — not the
                    // accent behind it, which stays a solid rounded rect.
                    .background(alignment: .top) { heightProbe }
                    .background(Palette.accent.opacity(0.15),
                                in: RoundedRectangle(cornerRadius: 18, style: .continuous))
            }
            if isTruncated {
                Button { expanded.toggle() } label: {
                    Text(expanded ? "Show less" : "Show more")
                        .font(.caption.weight(.medium))
                        .foregroundStyle(Palette.accent)
                }
                .buttonStyle(.plain)
                .padding(.trailing, 4)
            }
        }
    }

    /// A hidden copy of the bubble laid out at the visible bubble's width but with no height limit,
    /// so its measured height is the full content height regardless of the collapse cap. Attached as
    /// a background so it inherits the visible bubble's width and wraps identically.
    private var heightProbe: some View {
        MarkdownText(markdown: text)
            .padding(.horizontal, 14)
            .padding(.vertical, 9)
            .fixedSize(horizontal: false, vertical: true)
            .background(GeometryReader { geo in
                Color.clear.preference(key: FullHeightKey.self, value: geo.size.height)
            })
            .hidden()
            .onPreferenceChange(FullHeightKey.self) { fullHeight = $0 }
    }

    /// Solid for all but the bottom sliver, then fades to clear — so a collapsed bubble's last
    /// visible line dissolves instead of being clipped mid-glyph.
    private var collapseFade: some View {
        LinearGradient(
            stops: [
                .init(color: .black, location: 0),
                .init(color: .black, location: 0.86),
                .init(color: .clear, location: 1),
            ],
            startPoint: .top, endPoint: .bottom)
    }
}

private struct FullHeightKey: PreferenceKey {
    static let defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) }
}

/// The agent's reasoning, set apart from its prose by a quiet left rule and a `sparkles` glyph,
/// italic and muted — the phone echo of the Mac's `thinkingRow`. Renders nothing for an
/// empty/redacted thinking block (matching the desktop), so a placeholder row never shows.
struct ThinkingRow: View {
    let text: String
    var body: some View {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmed.isEmpty {
            HStack(alignment: .top, spacing: 8) {
                Rectangle().fill(.quaternary).frame(width: 2).cornerRadius(1)
                HStack(alignment: .firstTextBaseline, spacing: 6) {
                    Image(systemName: "sparkles").font(.caption2).foregroundStyle(.tertiary)
                    Text(trimmed)
                        .font(.callout.italic()).foregroundStyle(.secondary)
                        .lineSpacing(4)
                        .textSelection(.enabled)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

/// A muted single-line note with an optional trailing detail (e.g. session-started cwd).
struct InfoLine: View {
    let icon: String
    let text: String
    var detail: String? = nil
    var body: some View {
        HStack(spacing: 6) {
            Image(systemName: icon)
            Text(text)
            if let detail { Text(detail).lineLimit(1).truncationMode(.middle).foregroundStyle(.tertiary) }
            Spacer(minLength: 0)
        }
        .font(.caption).foregroundStyle(.secondary)
    }
}

/// Per-turn token accounting — the signal the desktop shows and the old remote dropped.
struct UsageRow: View {
    let usage: Usage
    var body: some View {
        HStack(spacing: 10) {
            if let input = usage.inputTokens { metric("arrow.down", tokens(input)) }
            if let output = usage.outputTokens { metric("arrow.up", tokens(output)) }
            Spacer(minLength: 0)
        }
        .font(.caption2.monospacedDigit()).foregroundStyle(.secondary)
    }
    private func metric(_ icon: String, _ value: String) -> some View {
        HStack(spacing: 2) { Image(systemName: icon); Text(value) }
    }
    private func tokens(_ n: Int) -> String {
        n >= 1000 ? String(format: "%.1fk", Double(n) / 1000) : "\(n)"
    }
}

/// A rate-limit warning so the user sees a window tightening before a turn fails — first-class,
/// not buried as a raw event.
struct RateLimitRow: View {
    let limit: RateLimit
    private var warn: Bool { (limit.status ?? "").contains("warning") || limit.status == "rejected" }
    var body: some View {
        Label(text, systemImage: warn ? "exclamationmark.circle.fill" : "clock.arrow.circlepath")
            .font(.caption).foregroundStyle(warn ? Palette.attention : .secondary)
            .frame(maxWidth: .infinity, alignment: .leading)
    }
    private var text: String {
        var parts = ["Rate limit"]
        if let type = limit.rateLimitType { parts.append(type.replacingOccurrences(of: "_", with: " ")) }
        if let status = limit.status { parts.append(status.replacingOccurrences(of: "_", with: " ")) }
        if let resets = limit.resetsAt {
            parts.append("· resets \(resets.formatted(date: .omitted, time: .shortened))")
        }
        return parts.joined(separator: " ")
    }
}

struct TurnBoundaryRow: View {
    var body: some View {
        HStack(spacing: 8) {
            Rectangle().fill(Color.primary.opacity(0.08)).frame(height: 1)
            Text("turn").font(.caption2).foregroundStyle(.tertiary)
            Rectangle().fill(Color.primary.opacity(0.08)).frame(height: 1)
        }
    }
}

struct RunFinishedRow: View {
    let outcome: RunFinished.Outcome
    var body: some View {
        Label(label, systemImage: icon).font(.caption).foregroundStyle(color)
    }
    private var label: String { "Run \(outcome.rawValue)" }
    private var icon: String {
        switch outcome {
        case .completed: "flag.checkered"
        case .interrupted: "stop.circle"
        case .errored: "xmark.octagon"
        case .maxTurns: "flag.checkered"
        }
    }
    private var color: Color {
        switch outcome {
        case .completed, .maxTurns: Palette.success
        case .interrupted: Palette.paused
        case .errored: Palette.danger
        }
    }
}

extension JSONValue {
    /// Human-readable tool-result text: a plain string, the joined text of an MCP content array,
    /// or the canonical JSON as a last resort. Mirrors the Mac's `TranscriptRow.resultText`, so an
    /// answered-question card and a git-commit card read the result the same way on both platforms.
    var resultText: String {
        if let string = stringValue { return string }
        if let array = arrayValue {
            let texts = array.compactMap { $0["text"]?.stringValue }
            if !texts.isEmpty { return texts.joined(separator: "\n") }
        }
        let canonical = canonicalString()
        return canonical == "null" ? "" : canonical
    }

    // (`compactSummary` — the one-line gist — lives in TranscriptProjection.swift, which builds
    // standalone as the host-testable NucleicRemoteProjection SwiftPM target and needs it there.)

    /// The full, **untruncated** content of a tool input for the approval card —
    /// the user must see exactly what they are granting before allowing. Unlike
    /// `compactSummary` (a one-line gist that drops values for complex objects) this
    /// returns the primary field verbatim, falling back to the pretty-printed input
    /// so nothing is hidden. The caller scrolls; it never clips.
    var approvalDetail: String {
        switch self {
        case .string(let s): return s
        case .object(let o):
            if let primary = o["command"]?.stringValue
                ?? o["file_path"]?.stringValue
                ?? o["path"]?.stringValue
                ?? o["notebook_path"]?.stringValue
                ?? o["url"]?.stringValue
                ?? o["query"]?.stringValue {
                return primary
            }
            return o.isEmpty ? "" : prettyString()
        case .null: return ""
        default: return prettyString()
        }
    }
}
