import AVFAudio
import OpenClawChatUI
import OpenClawProtocol
import SwiftUI

private struct ChatScrollEdgeTreatment: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 26.0, *) {
            // The shared canvas supplies color for the native blur. Automatic
            // edge effects harden to black when the host inserts an opaque fill.
            content.scrollEdgeEffectStyle(.soft, for: .vertical)
        } else {
            content
        }
    }
}

struct ChatProTab: View {
    enum GatewayStatusTone: Equatable {
        case success
        case warning
        case error
    }

    private struct TranscriptShareItem: Identifiable {
        let id = UUID()
        let fileURL: URL
    }

    private struct SessionDashboardPresentation: Identifiable {
        let id = UUID()
        let sessionKey: String
    }

    @Environment(NodeAppModel.self) private var appModel
    @AppStorage("openclaw.webchat.showAssistantTrace")
    private var showsAssistantTrace = true
    @State private var viewModel: OpenClawChatViewModel?
    @State private var viewModelOwnerID = ""
    @State private var transcriptShareItem: TranscriptShareItem?
    @State private var showsTranscriptExportError = false
    @State private var showsBackgroundTasks = false
    @State private var showsSessions = false
    @State private var showsNewSessionOptions = false
    @State private var sessionDashboardPresentation: SessionDashboardPresentation?
    // Transport can start unscoped while the UI uses its "main" fallback.
    // Track the real agent so gateway metadata replaces the captured transport.
    @State private var viewModelTransportAgentID = ""
    @State private var viewModelRoutingContract = ""
    @State private var viewModelPresentationAgentID = "main"
    @State private var viewModelPresentationAgentName = "Main"
    @State private var viewModelPresentationAgentBadge = "M"
    @State private var viewModelHasVerifiedOfflineRoutingIdentity = false
    @State private var speech: OpenClawChatSpeechController?
    @State private var isGatewayStatusManuallyExpanded = false
    let headerSidebarAction: OpenClawSidebarHeaderAction?
    let headerTitle: String?
    let showsAgentBadge: Bool
    let ownsNavigationStack: Bool
    let openSettings: (() -> Void)?

    init(
        headerSidebarAction: OpenClawSidebarHeaderAction? = nil,
        headerTitle: String? = nil,
        showsAgentBadge: Bool = true,
        ownsNavigationStack: Bool = true,
        openSettings: (() -> Void)? = nil)
    {
        self.headerSidebarAction = headerSidebarAction
        self.headerTitle = headerTitle
        self.showsAgentBadge = showsAgentBadge
        self.ownsNavigationStack = ownsNavigationStack
        self.openSettings = openSettings
    }

    var body: some View {
        Group {
            if self.ownsNavigationStack {
                NavigationStack {
                    self.content
                }
            } else {
                // Phone and iPad hosts already provide a NavigationStack. Keep
                // one native bar so embedded Chat never grows duplicate chrome.
                self.content
            }
        }
        .task {
            await self.appModel.restoreChatSessionRoutingIdentityIfNeeded()
            self.syncChatViewModel()
            await self.handleNewChatRequest(self.appModel.newChatRequestID)
            if self.speech == nil {
                let gateway = self.appModel.operatorSession
                self.speech = OpenClawChatSpeechController { text in
                    try await ChatMessageSpeechClient.synthesize(text: text, gateway: gateway)
                }
            }
        }
        .onChange(of: self.appModel.chatSessionKey) { _, _ in
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.chatViewModelOwnerID) { _, _ in
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.chatAgentId) { _, _ in
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.gatewayDefaultAgentId) { _, _ in
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.chatSessionRoutingContract) { _, _ in
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.voiceNoteRecorder.ownsPendingChatAttachment) { _, _ in
            self.viewModel?.attachmentOwnerActivityChanged()
            self.syncChatViewModel()
        }
        .onChange(of: self.viewModel?.isAttachmentOwnerPinned) { _, pinned in
            guard pinned == false else { return }
            self.syncChatViewModel()
        }
        .onChange(of: self.appModel.isAppleReviewDemoModeEnabled) { _, _ in
            self.syncChatViewModel()
            self.viewModel?.refresh()
        }
        .onChange(of: self.appModel.isScreenshotFixtureModeEnabled) { _, _ in
            self.syncChatViewModel()
            self.viewModel?.refresh()
        }
        .onChange(of: self.appModel.isOperatorGatewayConnected) { _, connected in
            guard connected else { return }
            self.syncChatViewModel()
            self.viewModel?.refresh()
        }
        .onChange(of: self.appModel.newChatRequestID) { _, requestID in
            Task { await self.handleNewChatRequest(requestID) }
        }
    }

    private var content: some View {
        self.chatSurface
            .modifier(ChatScrollEdgeTreatment())
            .navigationTitle(self.showsAgentBadge ? "" : self.headerDisplayTitle)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                if let headerSidebarAction {
                    OpenClawSidebarToolbarItem(
                        action: headerSidebarAction,
                        placement: .topBarLeading)
                }
                if self.showsAgentBadge {
                    if #available(iOS 26.0, *) {
                        ToolbarItem(placement: .topBarLeading) {
                            self.headerAgentIdentity
                        }
                        .sharedBackgroundVisibility(.hidden)
                    } else {
                        ToolbarItem(placement: .topBarLeading) {
                            self.headerAgentIdentity
                        }
                    }
                } else {
                    ToolbarItem(placement: .topBarTrailing) {
                        self.headerGatewayStatus
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    self.chatActionsMenu
                }
            }
            .sheet(item: self.$transcriptShareItem) { item in
                ChatTranscriptShareSheet(fileURL: item.fileURL)
            }
            .sheet(isPresented: self.$showsBackgroundTasks) {
                BackgroundTasksScreen(agentID: self.currentAgentID)
            }
            .sheet(isPresented: self.$showsSessions) {
                if let viewModel {
                    ChatSessionsSheet(viewModel: viewModel)
                }
            }
            .sheet(isPresented: self.$showsNewSessionOptions) {
                if let viewModel {
                    ChatNewSessionOptionsPopover(viewModel: viewModel) {
                        self.showsNewSessionOptions = false
                    }
                    .presentationDetents([.medium])
                    .presentationDragIndicator(.visible)
                }
            }
            .sheet(item: self.$sessionDashboardPresentation) { presentation in
                NavigationStack {
                    SessionDashboardScreen(sessionKey: presentation.sessionKey)
                }
            }
            .alert(
                String(localized: "Unable to Export Transcript"),
                isPresented: self.$showsTranscriptExportError)
            {
                Button(role: .cancel) {} label: {
                    Text("OK")
                        .font(OpenClawType.body)
                }
            } message: {
                Text("OpenClaw could not prepare the Markdown file.")
                    .font(OpenClawType.body)
            }
    }

    @ViewBuilder
    private var chatSurface: some View {
        if let viewModel {
            OpenClawChatView(
                viewModel: viewModel,
                drawsBackground: true,
                showsSessionSwitcher: false,
                userAccent: self.chatUserAccent,
                showsAssistantTrace: self.showsAssistantTrace,
                assistantName: self.agentDisplayName,
                assistantAvatarText: self.agentBadge,
                assistantAvatarTint: OpenClawBrand.accent,
                showsAssistantAvatars: false,
                composerChrome: .clean,
                isComposerEnabled: self.gatewayConnected || self.canQueueOffline,
                isAttachmentInputEnabled: self.gatewayConnected || self.canQueueOffline,
                messagePlaceholder: self.messagePlaceholder,
                emptyAssistantIntro: String(localized: "What would you like to work on?"),
                emptyAssistantPrompts: Self.emptyAssistantPrompts,
                talkControl: Self.shouldExposeCaptureControl(
                    isAttachmentOwnerPinned: viewModel.isAttachmentOwnerPinned,
                    isCaptureInFlight: self.appModel.talkMode.isEnabled) ? self.talkControl : nil,
                dictationControl: Self.shouldExposeCaptureControl(
                    isAttachmentOwnerPinned: viewModel.isAttachmentOwnerPinned,
                    isCaptureInFlight: self.appModel.isChatDictationPending || self.appModel.isChatDictationActive)
                    ? self.dictationControl
                    : nil,
                voiceNoteControl: self.voiceNoteControl,
                speech: self.speech)
                // iMessage-style grey bubbles for agent replies in the clean chrome.
                    .environment(\.openClawAssistantBubblesInCleanChrome, true)
                    .id(ObjectIdentifier(viewModel))
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        } else {
            ContentUnavailableView(
                "Preparing Chat",
                systemImage: "bubble.left.and.bubble.right",
                description: Text("The session attaches once the gateway is ready.")
                    .font(OpenClawType.body))
        }
    }

    /// Voice activity owns the contour while gateway state stays a compact,
    /// stable dot on the avatar instead of competing with it in the toolbar.
    private var headerIdentityBadge: some View {
        TalkAvatarWaveformView(
            phase: self.voiceAvatarPhase,
            palette: .openClawBrand,
            diameter: 38,
            avatarDiameter: 28)
        {
            Text(self.agentBadge)
                .font(OpenClawType.avatar(size: self.agentBadge.count > 2 ? 12 : 16))
                .foregroundStyle(.white)
                .minimumScaleFactor(0.6)
                .lineLimit(1)
                .frame(width: 28, height: 28)
                .background(Circle().fill(OpenClawBrand.carapaceElevated))
        }
        .overlay(alignment: .topTrailing) {
            self.gatewayAvatarStatusDot
        }
        .accessibilityElement(children: .ignore)
    }

    private var gatewayAvatarStatusDot: some View {
        ZStack {
            Circle()
                .fill(Color(uiColor: .systemBackground))
            Circle()
                .fill(self.gatewayStatusColor)
                .padding(2)
        }
        .frame(width: 13, height: 13)
        .accessibilityHidden(true)
    }

    private var headerAgentIdentity: some View {
        HStack {
            self.headerAgentIdentityControl
        }
        .frame(minHeight: 44)
        .accessibilityIdentifier("chat-agent-identity")
        .animation(.snappy(duration: 0.24), value: self.showsExpandedGatewayStatus)
    }

    @ViewBuilder
    private var headerGatewayStatus: some View {
        if self.gatewayStatusIsHealthy || self.openSettings != nil {
            Button(action: self.handleHeaderAgentIdentityTap) {
                self.headerGatewayStatusLabel
            }
            .buttonStyle(.plain)
            .accessibilityLabel(Text(verbatim: self.gatewayAccessibilityLabel))
            .accessibilityHint(self.gatewayStatusAccessibilityHint)
            .accessibilityIdentifier("chat-gateway-status")
        } else {
            self.headerGatewayStatusLabel
                .accessibilityElement(children: .ignore)
                .accessibilityLabel(Text(verbatim: self.gatewayAccessibilityLabel))
                .accessibilityIdentifier("chat-gateway-status")
        }
    }

    private var headerGatewayStatusLabel: some View {
        HStack(spacing: 6) {
            Circle()
                .fill(self.gatewayStatusColor)
                .frame(width: 10, height: 10)
            if self.showsExpandedGatewayStatus {
                self.expandedGatewayStatusLabel
            }
        }
        .frame(minHeight: 44)
        .animation(.snappy(duration: 0.24), value: self.showsExpandedGatewayStatus)
    }

    @ViewBuilder
    private var headerAgentIdentityControl: some View {
        if self.gatewayStatusIsHealthy || self.openSettings != nil {
            Button(action: self.handleHeaderAgentIdentityTap) {
                self.headerAgentIdentityLabel
            }
            .buttonStyle(.plain)
            .accessibilityLabel(Text(verbatim: self.headerAgentAccessibilityLabel))
            .accessibilityValue(self.showsExpandedGatewayStatus ? "Expanded" : "Collapsed")
            .accessibilityHint(self.gatewayStatusAccessibilityHint)
            .accessibilityIdentifier("chat-gateway-status")
        } else {
            self.headerAgentIdentityLabel
                .accessibilityElement(children: .ignore)
                .accessibilityLabel(Text(verbatim: self.headerAgentAccessibilityLabel))
                .accessibilityIdentifier("chat-gateway-status")
        }
    }

    private var headerAgentIdentityLabel: some View {
        HStack(spacing: 7) {
            self.headerIdentityBadge
            if self.showsExpandedGatewayStatus {
                self.expandedGatewayStatusLabel
                    .transition(.opacity.combined(with: .move(edge: .leading)))
            } else {
                Text(self.agentDisplayName)
                    .font(OpenClawType.headline)
                    .lineLimit(1)
                    .transition(.opacity)
            }
        }
    }

    private var gatewayStatusIsHealthy: Bool {
        Self.gatewayStatusTone(
            state: self.gatewayDisplayState,
            isGatewayUsable: self.gatewayConnected) == .success
    }

    private var headerAgentAccessibilityLabel: String {
        "\(self.voiceAvatarAccessibilityLabel). \(self.gatewayAccessibilityLabel)"
    }

    private func handleHeaderAgentIdentityTap() {
        if self.gatewayStatusIsHealthy {
            withAnimation(.snappy(duration: 0.24)) {
                self.isGatewayStatusManuallyExpanded.toggle()
            }
        } else {
            self.openSettings?()
        }
    }

    private var expandedGatewayStatusLabel: some View {
        Text(Self.gatewayStatusTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))
            .font(OpenClawType.subheadMedium)
            .foregroundStyle(.primary)
            .lineLimit(1)
            .fixedSize(horizontal: true, vertical: false)
            .padding(.horizontal, 9)
            .padding(.vertical, 5)
            .background(self.gatewayStatusColor.opacity(0.12), in: Capsule())
            .overlay {
                Capsule()
                    .stroke(self.gatewayStatusColor.opacity(0.28), lineWidth: 1)
            }
    }

    private var showsExpandedGatewayStatus: Bool {
        Self.gatewayStatusShouldExpand(
            state: self.gatewayDisplayState,
            isGatewayUsable: self.gatewayConnected,
            isManuallyExpanded: self.isGatewayStatusManuallyExpanded)
    }

    private var gatewayStatusAccessibilityHint: String {
        if !self.gatewayStatusIsHealthy {
            return String(localized: "Opens Settings / Gateway")
        }
        return self.isGatewayStatusManuallyExpanded
            ? String(localized: "Hides the gateway status label")
            : String(localized: "Shows the full gateway status label")
    }

    private var voiceAvatarPhase: TalkWaveformPhase {
        guard self.appModel.talkMode.isEnabled else { return .idle }
        if self.appModel.talkMode.isSpeaking {
            return .speaking(level: self.appModel.talkMode.playbackLevel)
        }
        if self.appModel.talkMode.isListening {
            return .listening(
                level: self.appModel.talkMode.micLevel,
                speechActive: self.appModel.talkMode.isUserSpeechDetected)
        }
        return .idle
    }

    private var voiceAvatarAccessibilityLabel: String {
        let state = self.appModel.talkMode.isEnabled
            ? self.appModel.talkMode.statusText
            : String(localized: "Voice off")
        return "\(self.agentDisplayName), \(state)"
    }

    private func syncChatViewModel() {
        let sessionKey = self.appModel.chatSessionKey
        // Includes the cache gateway identity so switching paired gateways
        // rebuilds the view model even while the transport mode stays the same.
        let ownerID = self.appModel.chatViewModelOwnerID
        let deliveryAgentID = self.appModel.chatDeliveryAgentId
        let transportAgentID = Self.transportAgentID(deliveryAgentID)
        let routingContract = self.appModel.chatSessionRoutingContract ?? ""
        guard let viewModel else {
            self.viewModelOwnerID = ownerID
            self.viewModelTransportAgentID = transportAgentID
            self.viewModelRoutingContract = routingContract
            self.captureCurrentPresentationIdentity()
            self.viewModel = self.makeChatViewModel(sessionKey: sessionKey)
            return
        }
        if Self.requiresViewModelRebuild(
            currentOwnerID: self.viewModelOwnerID,
            nextOwnerID: ownerID,
            currentTransportAgentID: self.viewModelTransportAgentID,
            nextTransportAgentID: transportAgentID)
        {
            // Keep recording, staging, and delivery on their captured route.
            // The pin-change observer replays this rebuild with latest state.
            guard !viewModel.isAttachmentOwnerPinned else { return }
            viewModel.endPendingToolActivities()
            self.viewModelOwnerID = ownerID
            self.viewModelTransportAgentID = transportAgentID
            self.viewModelRoutingContract = routingContract
            self.captureCurrentPresentationIdentity()
            self.viewModel = self.makeChatViewModel(sessionKey: sessionKey)
            return
        }
        if self.viewModelRoutingContract != routingContract {
            self.viewModelRoutingContract = routingContract
            viewModel.syncSessionRoutingContract(self.appModel.chatSessionRoutingContract)
        }
        viewModel.syncSession(to: sessionKey)
        if !viewModel.isAttachmentOwnerPinned {
            self.captureCurrentPresentationIdentity()
        }
    }

    private func handleNewChatRequest(_ requestID: Int) async {
        guard let viewModel,
              self.appModel.consumeNewChatRequest(requestID)
        else { return }
        _ = await viewModel.startNewSession()
    }

    private func captureCurrentPresentationIdentity() {
        self.viewModelPresentationAgentID = self.currentAgentID
        self.viewModelPresentationAgentName = self.currentAgentDisplayName
        self.viewModelPresentationAgentBadge = self.currentAgentBadge
        self.viewModelHasVerifiedOfflineRoutingIdentity = self.appModel.hasVerifiedChatOfflineRoutingIdentity
    }

    private func makeChatViewModel(sessionKey: String) -> OpenClawChatViewModel {
        // One gateway facade backs both seams while routing cache and outbox
        // operations to their separate installation-wide databases.
        let offlineStore = self.appModel.makeChatOfflineStore()
        let voiceNoteRecorder = self.appModel.voiceNoteRecorder
        return OpenClawChatViewModel(
            sessionKey: sessionKey,
            // Bind durable rows and their transport lease to the exact same
            // gateway owner even if app state switches between these calls.
            transport: self.appModel.makeChatTransport(outboxGatewayID: offlineStore?.gatewayID),
            activeAgentId: self.appModel.chatDeliveryAgentId,
            sessionRoutingContract: self.appModel.chatSessionRoutingContract,
            attachmentOwnerIsActive: { voiceNoteRecorder.ownsPendingChatAttachment },
            transcriptCache: offlineStore,
            outbox: offlineStore,
            onSessionChanged: { sessionKey in
                self.appModel.focusChatSession(sessionKey)
            },
            onToolActivity: { id, name, isActive, toolSessionKey in
                if isActive {
                    LiveActivityManager.shared.showTool(
                        id: id,
                        name: name,
                        agentName: self.agentDisplayName,
                        agentBadge: self.agentBadge,
                        sessionKey: toolSessionKey)
                } else {
                    LiveActivityManager.shared.endTool(id: id, sessionKey: toolSessionKey)
                }
            },
            diagnosticsLog: { message in
                GatewayDiagnostics.log(message)
            })
    }

    private var talkControl: OpenClawChatTalkControl {
        OpenClawChatTalkControl(
            isEnabled: self.appModel.talkMode.isEnabled,
            isListening: self.appModel.talkMode.isListening,
            isSpeaking: self.appModel.talkMode.isSpeaking,
            isGatewayConnected: self.appModel.talkMode.isGatewayConnected,
            statusText: self.appModel.talkMode.statusText,
            providerLabel: self.appModel.talkMode.gatewayTalkProviderLabel,
            level: self.talkLevel,
            inputDevices: self.talkInputDevices,
            selectedInputDeviceID: self.selectedTalkInputDeviceID,
            selectInputDevice: { deviceID in
                self.appModel.talkMode.selectInputDevice(deviceID)
            },
            cameraFacing: self.appModel.preferredCameraFacing == .front ? .front : .back,
            flipCamera: {
                self.appModel.flipPreferredCameraFacing()
            },
            toggle: { sessionKey in
                self.appModel.focusChatSession(sessionKey)
                self.appModel.setTalkEnabled(!self.appModel.talkMode.isEnabled)
            })
    }

    private var talkInputDevices: [OpenClawChatAudioInputDevice] {
        (AVAudioSession.sharedInstance().availableInputs ?? []).map { input in
            OpenClawChatAudioInputDevice(id: input.uid, name: input.portName)
        }
    }

    private var selectedTalkInputDeviceID: String? {
        guard let preferredID = self.appModel.talkMode.preferredInputDeviceID,
              self.talkInputDevices.contains(where: { $0.id == preferredID })
        else { return nil }
        return preferredID
    }

    private var dictationControl: OpenClawChatDictationControl {
        OpenClawChatDictationControl(
            isActive: self.appModel.isChatDictationActive,
            isAvailable: !self.appModel.isTalkCaptureActive || self.appModel.isChatDictationActive,
            start: {
                try await self.appModel.transcribeChatDraft()
            },
            finish: {
                self.appModel.finishChatDictation()
            },
            cancel: {
                self.appModel.cancelChatDictation()
            })
    }

    private var talkLevel: Double {
        if self.appModel.talkMode.isSpeaking {
            return self.appModel.talkMode.playbackLevel ?? 0
        }
        if self.appModel.talkMode.isListening {
            return self.appModel.talkMode.micLevel
        }
        return 0
    }

    private var voiceNoteControl: OpenClawChatVoiceNoteControl {
        OpenClawChatVoiceNoteControl(
            recorder: self.appModel.voiceNoteRecorder,
            isTalkActive: self.appModel.isTalkCaptureActive)
    }

    private var chatActionsMenu: some View {
        Menu {
            Button {
                Task { await self.viewModel?.startNewSession() }
            } label: {
                Label {
                    Text("New Chat")
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "plus.bubble")
                }
            }
            .disabled(self.viewModel == nil || !self.gatewayConnected || self.isAttachmentOwnerPinned)

            if self.activeAgent?.workspacegit == true {
                Button {
                    Task { await self.viewModel?.startNewSession(worktree: true) }
                } label: {
                    Label {
                        Text("New Chat in Worktree")
                            .font(OpenClawType.body)
                    } icon: {
                        Image(systemName: "arrow.triangle.branch")
                    }
                }
                .disabled(self.viewModel == nil || !self.gatewayConnected || self.isAttachmentOwnerPinned)
            }

            Button {
                self.showsNewSessionOptions = true
            } label: {
                Label {
                    Text("New Session Options…")
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "slider.horizontal.3")
                }
            }
            .disabled(self.viewModel == nil || !self.gatewayConnected || self.isAttachmentOwnerPinned)

            Button {
                self.showsSessions = true
            } label: {
                Label {
                    Text(String(localized: "Sessions…"))
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "rectangle.stack")
                }
            }

            Button {
                guard let sessionKey = self.viewModel?.sessionKey else { return }
                self.sessionDashboardPresentation = SessionDashboardPresentation(sessionKey: sessionKey)
            } label: {
                Label {
                    Text("Dashboard")
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "rectangle.grid.2x2")
                }
            }
            .disabled(self.viewModel == nil)

            Divider()

            if let viewModel {
                ChatModelControlsMenuItems(viewModel: viewModel)
                Divider()
            }

            Button {
                self.showsBackgroundTasks = true
            } label: {
                Label {
                    Text("Background Tasks")
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "clock.arrow.circlepath")
                }
            }
            .disabled(!self.appModel.isOperatorGatewayConnected)

            Button {
                self.exportTranscript()
            } label: {
                Label {
                    Text("Export Transcript")
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "square.and.arrow.up")
                }
            }
            .disabled(self.viewModel == nil)

            if let openSettings {
                Divider()

                Button(action: openSettings) {
                    Label {
                        Text("Gateway Settings")
                            .font(OpenClawType.body)
                    } icon: {
                        Image(systemName: "network")
                    }
                }
                .accessibilityIdentifier("chat-gateway-settings")
            }

            Toggle(isOn: self.$showsAssistantTrace) {
                Label {
                    Text(String(localized: "Show reasoning & tool activity"))
                        .font(OpenClawType.body)
                } icon: {
                    Image(systemName: "brain.head.profile")
                }
            }
        } label: {
            Image(systemName: "ellipsis.circle")
        }
        .accessibilityLabel("Chat actions")
    }

    private func exportTranscript() {
        guard let viewModel else { return }
        let title = viewModel.sessions.first { $0.key == viewModel.sessionKey }?.displayName
        let filename = ChatTranscriptExporter.filename(
            sessionTitle: title,
            sessionKey: viewModel.sessionKey)
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent("OpenClawTranscripts", isDirectory: true)
        let fileURL = directory.appendingPathComponent(filename, isDirectory: false)

        do {
            try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
            try viewModel.exportTranscriptMarkdown().write(to: fileURL, atomically: true, encoding: .utf8)
            self.transcriptShareItem = TranscriptShareItem(fileURL: fileURL)
        } catch {
            self.showsTranscriptExportError = true
        }
    }

    private var gatewayConnected: Bool {
        guard self.gatewayDisplayState == .connected else {
            return false
        }
        return self.appModel.isLocalChatFixtureEnabled || self.appModel.isOperatorGatewayConnected
    }

    private var gatewayDisplayState: GatewayDisplayState {
        Self.presentationGatewayState(
            current: GatewayStatusBuilder.build(appModel: self.appModel),
            isAttachmentOwnerPinned: self.isAttachmentOwnerPinned,
            capturedOwnerID: self.viewModelOwnerID,
            currentOwnerID: self.appModel.chatViewModelOwnerID)
    }

    nonisolated static func presentationGatewayState(
        current: GatewayDisplayState,
        isAttachmentOwnerPinned: Bool,
        capturedOwnerID: String,
        currentOwnerID: String) -> GatewayDisplayState
    {
        if isAttachmentOwnerPinned, capturedOwnerID != currentOwnerID {
            return .disconnected
        }
        return current
    }

    /// Attachment pinning blocks new capture, but starting or active capture must keep its stop control.
    nonisolated static func shouldExposeCaptureControl(
        isAttachmentOwnerPinned: Bool,
        isCaptureInFlight: Bool) -> Bool
    {
        !isAttachmentOwnerPinned || isCaptureInFlight
    }

    private var gatewayAccessibilityLabel: String {
        "Gateway: \(Self.gatewayStatusTitle(state: self.gatewayDisplayState, isGatewayUsable: self.gatewayConnected))"
    }

    private var gatewayStatusColor: Color {
        switch Self.gatewayStatusTone(
            state: self.gatewayDisplayState,
            isGatewayUsable: self.gatewayConnected)
        {
        case .success:
            OpenClawBrand.statusSuccess
        case .warning:
            OpenClawBrand.statusWarning
        case .error:
            OpenClawBrand.statusError
        }
    }

    nonisolated static func gatewayStatusTone(
        state: GatewayDisplayState,
        isGatewayUsable: Bool) -> GatewayStatusTone
    {
        switch state {
        case .connected:
            isGatewayUsable ? .success : .warning
        case .connecting, .error:
            .warning
        case .disconnected:
            .error
        }
    }

    nonisolated static func gatewayStatusShouldExpand(
        state: GatewayDisplayState,
        isGatewayUsable: Bool,
        isManuallyExpanded: Bool) -> Bool
    {
        isManuallyExpanded || self.gatewayStatusTone(
            state: state,
            isGatewayUsable: isGatewayUsable) != .success
    }

    nonisolated static func gatewayStatusTitle(state: GatewayDisplayState, isGatewayUsable: Bool) -> String {
        switch state {
        case .connected:
            isGatewayUsable ? "Connected" : "Unavailable"
        case .connecting:
            "Connecting"
        case .error:
            "Attention"
        case .disconnected:
            "Offline"
        }
    }

    private var messagePlaceholder: String {
        if self.gatewayConnected {
            return String(
                format: String(localized: "Message %@..."),
                self.agentDisplayName)
        }
        if self.canQueueOffline {
            return String(
                format: String(localized: "Message %@; sends when connected"),
                self.agentDisplayName)
        }
        return String(localized: "Connect to a gateway")
    }

    private var canQueueOffline: Bool {
        self.viewModel?.supportsOfflineTextOutbox == true &&
            (self.isAttachmentOwnerPinned
                ? self.viewModelHasVerifiedOfflineRoutingIdentity
                : self.appModel.hasVerifiedChatOfflineRoutingIdentity)
    }

    private var headerDisplayTitle: String {
        self.normalized(self.headerTitle)
            ?? Self.defaultHeaderTitle(showsAgentBadge: self.showsAgentBadge, agentDisplayName: self.agentDisplayName)
    }

    nonisolated static func defaultHeaderTitle(showsAgentBadge: Bool, agentDisplayName: String) -> String {
        showsAgentBadge ? agentDisplayName : "Chat"
    }

    private var chatUserAccent: Color {
        OpenClawBrand.accent
    }

    private var isAttachmentOwnerPinned: Bool {
        self.viewModel?.isAttachmentOwnerPinned == true
    }

    private var currentAgentID: String {
        self.normalized(self.appModel.chatAgentId) ?? "main"
    }

    private var currentActiveAgent: AgentSummary? {
        self.appModel.gatewayAgents.first { $0.id == self.currentAgentID }
    }

    private var activeAgentID: String {
        self.isAttachmentOwnerPinned ? self.viewModelPresentationAgentID : self.currentAgentID
    }

    private var activeAgent: AgentSummary? {
        self.appModel.gatewayAgents.first { $0.id == self.activeAgentID }
    }

    private var currentAgentDisplayName: String {
        self.normalized(self.currentActiveAgent?.name) ?? self.appModel.chatAgentName
    }

    private var agentDisplayName: String {
        self.isAttachmentOwnerPinned ? self.viewModelPresentationAgentName : self.currentAgentDisplayName
    }

    private var currentAgentBadge: String {
        if let identity = currentActiveAgent?.identity,
           let emoji = identity["emoji"]?.value as? String,
           let normalizedEmoji = Self.normalizedBadgeEmoji(emoji)
        {
            return normalizedEmoji
        }
        return Self.initialsBadge(for: self.currentAgentDisplayName)
    }

    private var agentBadge: String {
        self.isAttachmentOwnerPinned ? self.viewModelPresentationAgentBadge : self.currentAgentBadge
    }

    nonisolated static func initialsBadge(for displayName: String) -> String {
        AgentIdentityPresentation.initialsBadge(for: displayName)
    }

    nonisolated static func normalizedBadgeEmoji(_ value: String?) -> String? {
        AgentIdentityPresentation.normalizedBadgeEmoji(value)
    }

    nonisolated static func transportAgentID(_ value: String?) -> String {
        value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
    }

    nonisolated static func requiresViewModelRebuild(
        currentOwnerID: String,
        nextOwnerID: String,
        currentTransportAgentID: String,
        nextTransportAgentID: String) -> Bool
    {
        currentOwnerID != nextOwnerID || currentTransportAgentID != nextTransportAgentID
    }

    nonisolated static let emptyAssistantPrompts: [OpenClawChatView.StarterPrompt] = [
        OpenClawChatView.StarterPrompt(
            id: "summarize-status",
            title: String(localized: "Check OpenClaw status"),
            prompt: String(localized: "Summarize the current OpenClaw status and tell me what needs attention.")),
        OpenClawChatView.StarterPrompt(
            id: "show-controls",
            title: String(localized: "What can I control here?"),
            prompt: String(localized: "Show me which phone controls and device capabilities are available right now.")),
        OpenClawChatView.StarterPrompt(
            id: "start-voice",
            title: String(localized: "Help me start voice chat"),
            prompt: String(localized: "Help me start a realtime voice session from this phone.")),
    ]

    private func normalized(_ value: String?) -> String? {
        guard let value else { return nil }
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }
}
