import OpenClawKit
import Network
import Observation
import os
import SwiftUI
import UIKit

struct SettingsTab: View {
    private struct FeatureHelp: Identifiable {
        let id = UUID()
        let title: String
        let message: String
    }

    @Environment(NodeAppModel.self) private var appModel: NodeAppModel
    @Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager
    @Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
    @Environment(\.dismiss) private var dismiss
    @AppStorage("node.displayName") private var displayName: String = "iOS Node"
    @AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
    @AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
    @AppStorage("talk.enabled") private var talkEnabled: Bool = false
    @AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
    @AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
    @AppStorage("talk.voiceDirectiveHint.enabled") private var talkVoiceDirectiveHintEnabled: Bool = true
    @AppStorage("camera.enabled") private var cameraEnabled: Bool = true
    @AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = OpenClawLocationMode.off.rawValue
    @AppStorage("screen.preventSleep") private var preventSleep: Bool = true
    @AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
    @AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = ""
    @AppStorage("gateway.autoconnect") private var gatewayAutoConnect: Bool = false
    @AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false
    @AppStorage("gateway.manual.host") private var manualGatewayHost: String = ""
    @AppStorage("gateway.manual.port") private var manualGatewayPort: Int = 18789
    @AppStorage("gateway.manual.tls") private var manualGatewayTLS: Bool = true
    @AppStorage("gateway.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false
    @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false

    // Onboarding control (RootCanvas listens to onboarding.requestID and force-opens the wizard).
    @AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0
    @AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false
    @AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false

    @State private var connectingGatewayID: String?
    @State private var lastLocationModeRaw: String = OpenClawLocationMode.off.rawValue
    @State private var gatewayToken: String = ""
    @State private var gatewayPassword: String = ""
    @State private var defaultShareInstruction: String = ""
    @AppStorage("gateway.setupCode") private var setupCode: String = ""
    @State private var setupStatusText: String?
    @State private var manualGatewayPortText: String = ""
    @State private var gatewayExpanded: Bool = true
    @State private var selectedAgentPickerId: String = ""

    @State private var showResetOnboardingAlert: Bool = false
    @State private var activeFeatureHelp: FeatureHelp?
    @State private var suppressCredentialPersist: Bool = false

    private let gatewayLogger = Logger(subsystem: "ai.openclaw.ios", category: "GatewaySettings")

    var body: some View {
        NavigationStack {
            Form {
                Section {
                    DisclosureGroup(isExpanded: self.$gatewayExpanded) {
                        if !self.isGatewayConnected {
                            Text(
                                "1. Open Telegram and message your bot: /pair\n"
                                    + "2. Copy the setup code it returns\n"
                                    + "3. Paste here and tap Connect\n"
                                    + "4. Back in Telegram, run /pair approve")
                                .font(.footnote)
                                .foregroundStyle(.secondary)

                            if let warning = self.tailnetWarningText {
                                Text(warning)
                                    .font(.footnote.weight(.semibold))
                                    .foregroundStyle(.orange)
                            }

                            TextField("Paste setup code", text: self.$setupCode)
                                .textInputAutocapitalization(.never)
                                .autocorrectionDisabled()

                            Button {
                                Task { await self.applySetupCodeAndConnect() }
                            } label: {
                                if self.connectingGatewayID == "manual" {
                                    HStack(spacing: 8) {
                                        ProgressView()
                                            .progressViewStyle(.circular)
                                        Text("Connecting…")
                                    }
                                } else {
                                    Text("Connect with setup code")
                                }
                            }
                            .disabled(self.connectingGatewayID != nil
                                || self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)

                            if let status = self.setupStatusLine {
                                Text(status)
                                    .font(.footnote)
                                    .foregroundStyle(.secondary)
                            }
                        }

                        if self.isGatewayConnected {
                            Picker("Bot", selection: self.$selectedAgentPickerId) {
                                Text("Default").tag("")
                                let defaultId = (self.appModel.gatewayDefaultAgentId ?? "")
                                    .trimmingCharacters(in: .whitespacesAndNewlines)
                                ForEach(self.appModel.gatewayAgents.filter { $0.id != defaultId }, id: \.id) { agent in
                                    let name = (agent.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
                                    Text(name.isEmpty ? agent.id : name).tag(agent.id)
                                }
                            }
                            Text("Controls which bot Chat and Talk speak to.")
                                .font(.footnote)
                                .foregroundStyle(.secondary)
                        }

                        if self.appModel.gatewayServerName == nil {
                            LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
                        }
                        LabeledContent("Status", value: self.appModel.gatewayStatusText)
                        Toggle("Auto-connect on launch", isOn: self.$gatewayAutoConnect)

                        if let serverName = self.appModel.gatewayServerName {
                            LabeledContent("Server", value: serverName)
                            if let addr = self.appModel.gatewayRemoteAddress {
                                let parts = Self.parseHostPort(from: addr)
                                let urlString = Self.httpURLString(host: parts?.host, port: parts?.port, fallback: addr)
                                LabeledContent("Address") {
                                    Text(urlString)
                                }
                                .contextMenu {
                                    Button {
                                        UIPasteboard.general.string = urlString
                                    } label: {
                                        Label("Copy URL", systemImage: "doc.on.doc")
                                    }

                                    if let parts {
                                        Button {
                                            UIPasteboard.general.string = parts.host
                                        } label: {
                                            Label("Copy Host", systemImage: "doc.on.doc")
                                        }

                                        Button {
                                            UIPasteboard.general.string = "\(parts.port)"
                                        } label: {
                                            Label("Copy Port", systemImage: "doc.on.doc")
                                        }
                                    }
                                }
                            }

                            Button("Disconnect", role: .destructive) {
                                self.appModel.disconnectGateway()
                            }
                        } else {
                            self.gatewayList(showing: .all)
                        }

                        DisclosureGroup("Advanced") {
                            Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled)

                            TextField("Host", text: self.$manualGatewayHost)
                                .textInputAutocapitalization(.never)
                                .autocorrectionDisabled()

                            TextField("Port (optional)", text: self.manualPortBinding)
                                .keyboardType(.numberPad)

                            Toggle("Use TLS", isOn: self.$manualGatewayTLS)

                            Button {
                                Task { await self.connectManual() }
                            } label: {
                                if self.connectingGatewayID == "manual" {
                                    HStack(spacing: 8) {
                                        ProgressView()
                                            .progressViewStyle(.circular)
                                        Text("Connecting…")
                                    }
                                } else {
                                    Text("Connect (Manual)")
                                }
                            }
                            .disabled(self.connectingGatewayID != nil || self.manualGatewayHost
                                .trimmingCharacters(in: .whitespacesAndNewlines)
                                .isEmpty || !self.manualPortIsValid)

                            Text(
                                "Use this when mDNS/Bonjour discovery is blocked. "
                                    + "Leave port empty for 443 on tailnet DNS (TLS) or 18789 otherwise.")
                                .font(.footnote)
                                .foregroundStyle(.secondary)

                            Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled)
                                .onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in
                                    self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue)
                                }

                            NavigationLink("Discovery Logs") {
                                GatewayDiscoveryDebugLogView()
                            }

                            Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled)

                            TextField("Gateway Auth Token", text: self.$gatewayToken)
                                .textInputAutocapitalization(.never)
                                .autocorrectionDisabled()

                            SecureField("Gateway Password", text: self.$gatewayPassword)

                            Button("Reset Onboarding", role: .destructive) {
                                self.showResetOnboardingAlert = true
                            }

                            VStack(alignment: .leading, spacing: 6) {
                                Text("Debug")
                                    .font(.footnote.weight(.semibold))
                                    .foregroundStyle(.secondary)
                                Text(self.gatewayDebugText())
                                    .font(.system(size: 12, weight: .regular, design: .monospaced))
                                    .foregroundStyle(.secondary)
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                    .padding(10)
                                    .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
                            }
                        }
                    } label: {
                        HStack(spacing: 10) {
                            Circle()
                                .fill(self.isGatewayConnected ? Color.green : Color.secondary.opacity(0.35))
                                .frame(width: 10, height: 10)
                            Text("Gateway")
                            Spacer()
                            Text(self.gatewaySummaryText)
                                .font(.footnote)
                                .foregroundStyle(.secondary)
                        }
                    }
                }

                Section("Device") {
                    DisclosureGroup("Features") {
                        self.featureToggle(
                            "Voice Wake",
                            isOn: self.$voiceWakeEnabled,
                            help: "Enables wake-word activation to start a hands-free session.") { newValue in
                                self.appModel.setVoiceWakeEnabled(newValue)
                            }
                        self.featureToggle(
                            "Talk Mode",
                            isOn: self.$talkEnabled,
                            help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in
                                self.appModel.setTalkEnabled(newValue)
                            }
                        self.featureToggle(
                            "Background Listening",
                            isOn: self.$talkBackgroundEnabled,
                            help: "Keeps listening while the app is backgrounded. Uses more battery.")

                        NavigationLink {
                            VoiceWakeWordsSettingsView()
                        } label: {
                            LabeledContent(
                                "Wake Words",
                                value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))
                        }

                        self.featureToggle(
                            "Allow Camera",
                            isOn: self.$cameraEnabled,
                            help: "Allows the gateway to request photos or short video clips while OpenClaw is foregrounded.")

                        HStack(spacing: 8) {
                            Text("Location Access")
                            Spacer()
                            Button {
                                self.activeFeatureHelp = FeatureHelp(
                                    title: "Location Access",
                                    message: "Controls location permissions for OpenClaw. Off disables location tools, While Using enables foreground location, and Always enables background location.")
                            } label: {
                                Image(systemName: "info.circle")
                                    .foregroundStyle(.secondary)
                            }
                            .buttonStyle(.plain)
                            .accessibilityLabel("Location Access info")
                        }
                        Picker("Location Access", selection: self.$locationEnabledModeRaw) {
                            Text("Off").tag(OpenClawLocationMode.off.rawValue)
                            Text("While Using").tag(OpenClawLocationMode.whileUsing.rawValue)
                            Text("Always").tag(OpenClawLocationMode.always.rawValue)
                        }
                        .labelsHidden()
                        .pickerStyle(.segmented)

                        self.featureToggle(
                            "Prevent Sleep",
                            isOn: self.$preventSleep,
                            help: "Keeps the screen awake while OpenClaw is open.")

                        DisclosureGroup("Advanced") {
                            VStack(alignment: .leading, spacing: 8) {
                                Text("Talk Voice (Gateway)")
                                    .font(.footnote.weight(.semibold))
                                    .foregroundStyle(.secondary)
                                LabeledContent("Provider", value: "ElevenLabs")
                                LabeledContent(
                                    "API Key",
                                    value: self.appModel.talkMode.gatewayTalkConfigLoaded
                                        ? (self.appModel.talkMode.gatewayTalkApiKeyConfigured ? "Configured" : "Not configured")
                                        : "Not loaded")
                                LabeledContent(
                                    "Default Model",
                                    value: self.appModel.talkMode.gatewayTalkDefaultModelId ?? "eleven_v3 (fallback)")
                                LabeledContent(
                                    "Default Voice",
                                    value: self.appModel.talkMode.gatewayTalkDefaultVoiceId ?? "auto (first available)")
                                Text("Configured on gateway via talk.apiKey, talk.modelId, and talk.voiceId.")
                                    .font(.footnote)
                                    .foregroundStyle(.secondary)
                            }
                            self.featureToggle(
                                "Voice Directive Hint",
                                isOn: self.$talkVoiceDirectiveHintEnabled,
                                help: "Adds voice-switching instructions to Talk prompts. Disable to reduce prompt size.")
                            self.featureToggle(
                                "Show Talk Button",
                                isOn: self.$talkButtonEnabled,
                                help: "Shows the floating Talk button in the main interface.")
                            TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
                                .lineLimit(2 ... 6)
                                .textInputAutocapitalization(.sentences)
                            HStack(spacing: 8) {
                                Text("Default Share Instruction")
                                    .font(.footnote)
                                    .foregroundStyle(.secondary)
                                Spacer()
                                Button {
                                    self.activeFeatureHelp = FeatureHelp(
                                        title: "Default Share Instruction",
                                        message: "Appends this instruction when sharing content into OpenClaw from iOS.")
                                } label: {
                                    Image(systemName: "info.circle")
                                        .foregroundStyle(.secondary)
                                }
                                .buttonStyle(.plain)
                                .accessibilityLabel("Default Share Instruction info")
                            }

                            VStack(alignment: .leading, spacing: 8) {
                                Button {
                                    Task { await self.appModel.runSharePipelineSelfTest() }
                                } label: {
                                    Label("Run Share Self-Test", systemImage: "checkmark.seal")
                                }
                                Text(self.appModel.lastShareEventText)
                                    .font(.footnote)
                                    .foregroundStyle(.secondary)
                            }
                        }
                    }

                    DisclosureGroup("Device Info") {
                        TextField("Name", text: self.$displayName)
                        Text(self.instanceId)
                            .font(.footnote)
                            .foregroundStyle(.secondary)
                            .lineLimit(1)
                            .truncationMode(.middle)
                        LabeledContent("Device", value: self.deviceFamily())
                        LabeledContent("Platform", value: self.platformString())
                        LabeledContent("OpenClaw", value: self.openClawVersionString())
                    }
                }
            }
            .navigationTitle("Settings")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        self.dismiss()
                    } label: {
                        Image(systemName: "xmark")
                    }
                    .accessibilityLabel("Close")
                }
            }
            .alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) {
                Button("Reset", role: .destructive) {
                    self.resetOnboarding()
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text(
                    "This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.")
            }
            .alert(item: self.$activeFeatureHelp) { help in
                Alert(
                    title: Text(help.title),
                    message: Text(help.message),
                    dismissButton: .default(Text("OK")))
            }
            .onAppear {
                self.lastLocationModeRaw = self.locationEnabledModeRaw
                self.syncManualPortText()
                let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
                if !trimmedInstanceId.isEmpty {
                    self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
                    self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
                }
                self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
                self.appModel.refreshLastShareEventFromRelay()
                // Keep setup front-and-center when disconnected; keep things compact once connected.
                self.gatewayExpanded = !self.isGatewayConnected
                self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
                if self.isGatewayConnected {
                    self.appModel.reloadTalkConfig()
                }
            }
            .onChange(of: self.selectedAgentPickerId) { _, newValue in
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
                self.appModel.setSelectedAgentId(trimmed.isEmpty ? nil : trimmed)
            }
            .onChange(of: self.appModel.selectedAgentId ?? "") { _, newValue in
                if newValue != self.selectedAgentPickerId {
                    self.selectedAgentPickerId = newValue
                }
            }
            .onChange(of: self.preferredGatewayStableID) { _, newValue in
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !trimmed.isEmpty else { return }
                GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
            }
            .onChange(of: self.gatewayToken) { _, newValue in
                guard !self.suppressCredentialPersist else { return }
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
                let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !instanceId.isEmpty else { return }
                GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
            }
            .onChange(of: self.gatewayPassword) { _, newValue in
                guard !self.suppressCredentialPersist else { return }
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
                let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !instanceId.isEmpty else { return }
                GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
            }
            .onChange(of: self.defaultShareInstruction) { _, newValue in
                ShareToAgentSettings.saveDefaultInstruction(newValue)
            }
            .onChange(of: self.manualGatewayPort) { _, _ in
                self.syncManualPortText()
            }
            .onChange(of: self.appModel.gatewayServerName) { _, newValue in
                if newValue != nil {
                    self.setupCode = ""
                    self.setupStatusText = nil
                    return
                }
                if self.manualGatewayEnabled {
                    self.setupStatusText = self.appModel.gatewayStatusText
                }
            }
            .onChange(of: self.appModel.gatewayStatusText) { _, newValue in
                guard self.manualGatewayEnabled || self.connectingGatewayID == "manual" else { return }
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !trimmed.isEmpty else { return }
                self.setupStatusText = trimmed
            }
            .onChange(of: self.locationEnabledModeRaw) { _, newValue in
                let previous = self.lastLocationModeRaw
                self.lastLocationModeRaw = newValue
                guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
                Task {
                    let granted = await self.appModel.requestLocationPermissions(mode: mode)
                    if !granted {
                        await MainActor.run {
                            self.locationEnabledModeRaw = previous
                            self.lastLocationModeRaw = previous
                        }
                        return
                    }
                    await MainActor.run {
                        self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
                    }
                }
            }
        }
        .gatewayTrustPromptAlert()
    }

    @ViewBuilder
    private func gatewayList(showing: GatewayListMode) -> some View {
        if self.gatewayController.gateways.isEmpty {
            VStack(alignment: .leading, spacing: 12) {
                Text("No gateways found yet.")
                    .foregroundStyle(.secondary)
                Text("If your gateway is on another network, connect it and ensure DNS is working.")
                    .font(.footnote)
                    .foregroundStyle(.secondary)

                if let lastKnown = GatewaySettingsStore.loadLastGatewayConnection(),
                   case let .manual(host, port, _, _) = lastKnown
                {
                    Button {
                        Task { await self.connectLastKnown() }
                    } label: {
                        self.lastKnownButtonLabel(host: host, port: port)
                    }
                    .disabled(self.connectingGatewayID != nil)
                    .buttonStyle(.borderedProminent)
                    .tint(self.appModel.seamColor)
                }
            }
        } else {
            let connectedID = self.appModel.connectedGatewayID
            let rows = self.gatewayController.gateways.filter { gateway in
                let isConnected = gateway.stableID == connectedID
                switch showing {
                case .all:
                    return true
                case .availableOnly:
                    return !isConnected
                }
            }

            if rows.isEmpty, showing == .availableOnly {
                Text("No other gateways found.")
                    .foregroundStyle(.secondary)
            } else {
                ForEach(rows) { gateway in
                    HStack {
                        VStack(alignment: .leading, spacing: 2) {
                            // Avoid localized-string formatting edge cases from Bonjour-advertised names.
                            Text(verbatim: gateway.name)
                            let detailLines = self.gatewayDetailLines(gateway)
                            ForEach(detailLines, id: \.self) { line in
                                Text(verbatim: line)
                                    .font(.footnote)
                                    .foregroundStyle(.secondary)
                            }
                        }
                        Spacer()

                        Button {
                            Task { await self.connect(gateway) }
                        } label: {
                            if self.connectingGatewayID == gateway.id {
                                ProgressView()
                                    .progressViewStyle(.circular)
                            } else {
                                Text("Connect")
                            }
                        }
                        .disabled(self.connectingGatewayID != nil)
                    }
                }
            }
        }
    }

    private enum GatewayListMode: Equatable {
        case all
        case availableOnly
    }

    private var isGatewayConnected: Bool {
        let status = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        if status.contains("connected") { return true }
        return self.appModel.gatewayServerName != nil && !status.contains("offline")
    }

    private var gatewaySummaryText: String {
        if let server = self.appModel.gatewayServerName, self.isGatewayConnected {
            return server
        }
        let trimmed = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? "Not connected" : trimmed
    }

    private func platformString() -> String {
        let v = ProcessInfo.processInfo.operatingSystemVersion
        return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
    }

    private func deviceFamily() -> String {
        switch UIDevice.current.userInterfaceIdiom {
        case .pad:
            "iPad"
        case .phone:
            "iPhone"
        default:
            "iOS"
        }
    }

    private func openClawVersionString() -> String {
        let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
        let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
        let trimmedBuild = build.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmedBuild.isEmpty || trimmedBuild == version {
            return version
        }
        return "\(version) (\(trimmedBuild))"
    }

    private func featureToggle(
        _ title: String,
        isOn: Binding<Bool>,
        help: String,
        onChange: ((Bool) -> Void)? = nil
    ) -> some View {
        HStack(spacing: 8) {
            Toggle(title, isOn: isOn)
            Button {
                self.activeFeatureHelp = FeatureHelp(title: title, message: help)
            } label: {
                Image(systemName: "info.circle")
                    .foregroundStyle(.secondary)
            }
            .buttonStyle(.plain)
            .accessibilityLabel("\(title) info")
        }
        .onChange(of: isOn.wrappedValue) { _, newValue in
            onChange?(newValue)
        }
    }

    private func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
        self.connectingGatewayID = gateway.id
        self.manualGatewayEnabled = false
        self.preferredGatewayStableID = gateway.stableID
        GatewaySettingsStore.savePreferredGatewayStableID(gateway.stableID)
        self.lastDiscoveredGatewayStableID = gateway.stableID
        GatewaySettingsStore.saveLastDiscoveredGatewayStableID(gateway.stableID)
        defer { self.connectingGatewayID = nil }

        let err = await self.gatewayController.connectWithDiagnostics(gateway)
        if let err {
            self.setupStatusText = err
        }
    }

    private func connectLastKnown() async {
        self.connectingGatewayID = "last-known"
        defer { self.connectingGatewayID = nil }
        await self.gatewayController.connectLastKnown()
    }

    private func gatewayDebugText() -> String {
        var lines: [String] = [
            "gateway: \(self.appModel.gatewayStatusText)",
            "discovery: \(self.gatewayController.discoveryStatusText)",
        ]
        lines.append("server: \(self.appModel.gatewayServerName ?? "—")")
        lines.append("address: \(self.appModel.gatewayRemoteAddress ?? "—")")
        if let last = self.gatewayController.discoveryDebugLog.last?.message {
            lines.append("discovery log: \(last)")
        }
        return lines.joined(separator: "\n")
    }

    @ViewBuilder
    private func lastKnownButtonLabel(host: String, port: Int) -> some View {
        if self.connectingGatewayID == "last-known" {
            HStack(spacing: 8) {
                ProgressView()
                    .progressViewStyle(.circular)
                Text("Connecting…")
            }
            .frame(maxWidth: .infinity)
        } else {
            HStack(spacing: 8) {
                Image(systemName: "bolt.horizontal.circle.fill")
                VStack(alignment: .leading, spacing: 2) {
                    Text("Connect last known")
                    Text("\(host):\(port)")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
                Spacer()
            }
            .frame(maxWidth: .infinity)
        }
    }

    private var manualPortBinding: Binding<String> {
        Binding(
            get: { self.manualGatewayPortText },
            set: { newValue in
                let filtered = newValue.filter(\.isNumber)
                if self.manualGatewayPortText != filtered {
                    self.manualGatewayPortText = filtered
                }
                if filtered.isEmpty {
                    if self.manualGatewayPort != 0 {
                        self.manualGatewayPort = 0
                    }
                } else if let port = Int(filtered), self.manualGatewayPort != port {
                    self.manualGatewayPort = port
                }
            })
    }

    private var manualPortIsValid: Bool {
        if self.manualGatewayPortText.isEmpty { return true }
        return self.manualGatewayPort >= 1 && self.manualGatewayPort <= 65535
    }

    private func syncManualPortText() {
        if self.manualGatewayPort > 0 {
            let next = String(self.manualGatewayPort)
            if self.manualGatewayPortText != next {
                self.manualGatewayPortText = next
            }
        } else if !self.manualGatewayPortText.isEmpty {
            self.manualGatewayPortText = ""
        }
    }

    private func applySetupCodeAndConnect() async {
        self.setupStatusText = nil
        guard self.applySetupCode() else { return }
        let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
        let resolvedPort = self.resolvedManualPort(host: host)
        let hasToken = !self.gatewayToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        let hasPassword = !self.gatewayPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        GatewayDiagnostics.log(
            "setup code applied host=\(host) port=\(resolvedPort ?? -1) tls=\(self.manualGatewayTLS) token=\(hasToken) password=\(hasPassword)")
        guard let port = resolvedPort else {
            self.setupStatusText = "Failed: invalid port"
            return
        }
        let ok = await self.preflightGateway(host: host, port: port, useTLS: self.manualGatewayTLS)
        guard ok else { return }
        self.setupStatusText = "Setup code applied. Connecting…"
        await self.connectManual()
    }

    @discardableResult
    private func applySetupCode() -> Bool {
        let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !raw.isEmpty else {
            self.setupStatusText = "Paste a setup code to continue."
            return false
        }

        guard let payload = GatewaySetupCode.decode(raw: raw) else {
            self.setupStatusText = "Setup code not recognized."
            return false
        }

        if let urlString = payload.url, let url = URL(string: urlString) {
            self.applySetupURL(url)
        } else if let host = payload.host, !host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            self.manualGatewayHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
            if let port = payload.port {
                self.manualGatewayPort = port
                self.manualGatewayPortText = String(port)
            } else {
                self.manualGatewayPort = 0
                self.manualGatewayPortText = ""
            }
            if let tls = payload.tls {
                self.manualGatewayTLS = tls
            }
        } else if let url = URL(string: raw), url.scheme != nil {
            self.applySetupURL(url)
        } else {
            self.setupStatusText = "Setup code missing URL or host."
            return false
        }

        let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
        if let token = payload.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
            self.gatewayToken = trimmedToken
            if !trimmedInstanceId.isEmpty {
                GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: trimmedInstanceId)
            }
        }
        if let password = payload.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
            self.gatewayPassword = trimmedPassword
            if !trimmedInstanceId.isEmpty {
                GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: trimmedInstanceId)
            }
        }

        return true
    }

    private func applySetupURL(_ url: URL) {
        guard let host = url.host, !host.isEmpty else { return }
        self.manualGatewayHost = host
        if let port = url.port {
            self.manualGatewayPort = port
            self.manualGatewayPortText = String(port)
        } else {
            self.manualGatewayPort = 0
            self.manualGatewayPortText = ""
        }
        let scheme = (url.scheme ?? "").lowercased()
        if scheme == "wss" || scheme == "https" {
            self.manualGatewayTLS = true
        } else if scheme == "ws" || scheme == "http" {
            self.manualGatewayTLS = false
        }
    }

    private func resolvedManualPort(host: String) -> Int? {
        if self.manualGatewayPort > 0 {
            return self.manualGatewayPort <= 65535 ? self.manualGatewayPort : nil
        }
        let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }
        if self.manualGatewayTLS && trimmed.lowercased().hasSuffix(".ts.net") {
            return 443
        }
        return 18789
    }

    private func preflightGateway(host: String, port: Int, useTLS: Bool) async -> Bool {
        let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return false }

        if Self.isTailnetHostOrIP(trimmed) && !Self.hasTailnetIPv4() {
            let msg = "Tailscale is off on this iPhone. Turn it on, then try again."
            self.setupStatusText = msg
            GatewayDiagnostics.log("preflight fail: tailnet missing host=\(trimmed)")
            self.gatewayLogger.warning("\(msg, privacy: .public)")
            return false
        }

        self.setupStatusText = "Checking gateway reachability…"
        let ok = await Self.probeTCP(host: trimmed, port: port, timeoutSeconds: 3)
        if !ok {
            let msg = "Can't reach gateway at \(trimmed):\(port). Check Tailscale or LAN."
            self.setupStatusText = msg
            GatewayDiagnostics.log("preflight fail: unreachable host=\(trimmed) port=\(port)")
            self.gatewayLogger.warning("\(msg, privacy: .public)")
            return false
        }
        GatewayDiagnostics.log("preflight ok host=\(trimmed) port=\(port) tls=\(useTLS)")
        return true
    }

    private static func probeTCP(host: String, port: Int, timeoutSeconds: Double) async -> Bool {
        await TCPProbe.probe(
            host: host,
            port: port,
            timeoutSeconds: timeoutSeconds,
            queueLabel: "gateway.preflight")
    }

    // (GatewaySetupCode) decode raw setup codes.

    private func connectManual() async {
        let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !host.isEmpty else {
            self.setupStatusText = "Failed: host required"
            return
        }
        guard self.manualPortIsValid else {
            self.setupStatusText = "Failed: invalid port"
            return
        }

        self.connectingGatewayID = "manual"
        self.manualGatewayEnabled = true
        defer { self.connectingGatewayID = nil }

        GatewayDiagnostics.log(
            "connect manual host=\(host) port=\(self.manualGatewayPort) tls=\(self.manualGatewayTLS)")
        await self.gatewayController.connectManual(
            host: host,
            port: self.manualGatewayPort,
            useTLS: self.manualGatewayTLS)
    }

    private var setupStatusLine: String? {
        let trimmedSetup = self.setupStatusText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
        if let friendly = self.friendlyGatewayMessage(from: gatewayStatus) { return friendly }
        if let friendly = self.friendlyGatewayMessage(from: trimmedSetup) { return friendly }
        if !trimmedSetup.isEmpty { return trimmedSetup }
        if gatewayStatus.isEmpty || gatewayStatus == "Offline" { return nil }
        return gatewayStatus
    }

    private var tailnetWarningText: String? {
        let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !host.isEmpty else { return nil }
        guard Self.isTailnetHostOrIP(host) else { return nil }
        guard !Self.hasTailnetIPv4() else { return nil }
        return "This gateway is on your tailnet. Turn on Tailscale on this iPhone, then tap Connect."
    }

    private func friendlyGatewayMessage(from raw: String) -> String? {
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }
        let lower = trimmed.lowercased()
        if lower.contains("pairing required") {
            return "Pairing required. Go back to Telegram and run /pair approve, then tap Connect again."
        }
        if lower.contains("device nonce required") || lower.contains("device nonce mismatch") {
            return "Secure handshake failed. Make sure Tailscale is connected, then tap Connect again."
        }
        if lower.contains("device signature expired") || lower.contains("device signature invalid") {
            return "Secure handshake failed. Check that your iPhone time is correct, then tap Connect again."
        }
        if lower.contains("connect timed out") || lower.contains("timed out") {
            return "Connection timed out. Make sure Tailscale is connected, then try again."
        }
        if lower.contains("unauthorized role") {
            return "Connected, but some controls are restricted for nodes. This is expected."
        }
        return nil
    }

    private static func hasTailnetIPv4() -> Bool {
        var addrList: UnsafeMutablePointer<ifaddrs>?
        guard getifaddrs(&addrList) == 0, let first = addrList else { return false }
        defer { freeifaddrs(addrList) }

        for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
            let flags = Int32(ptr.pointee.ifa_flags)
            let isUp = (flags & IFF_UP) != 0
            let isLoopback = (flags & IFF_LOOPBACK) != 0
            let family = ptr.pointee.ifa_addr.pointee.sa_family
            if !isUp || isLoopback || family != UInt8(AF_INET) { continue }

            var addr = ptr.pointee.ifa_addr.pointee
            var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
            let result = getnameinfo(
                &addr,
                socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
                &buffer,
                socklen_t(buffer.count),
                nil,
                0,
                NI_NUMERICHOST)
            guard result == 0 else { continue }
            let len = buffer.prefix { $0 != 0 }
            let bytes = len.map { UInt8(bitPattern: $0) }
            guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
            if self.isTailnetIPv4(ip) { return true }
        }

        return false
    }

    private static func isTailnetHostOrIP(_ host: String) -> Bool {
        let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        if trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.") {
            return true
        }
        return self.isTailnetIPv4(trimmed)
    }

    private static func isTailnetIPv4(_ ip: String) -> Bool {
        let parts = ip.split(separator: ".")
        guard parts.count == 4 else { return false }
        let octets = parts.compactMap { Int($0) }
        guard octets.count == 4 else { return false }
        let a = octets[0]
        let b = octets[1]
        guard (0...255).contains(a), (0...255).contains(b) else { return false }
        return a == 100 && b >= 64 && b <= 127
    }

    private static func parseHostPort(from address: String) -> SettingsHostPort? {
        SettingsNetworkingHelpers.parseHostPort(from: address)
    }

    private static func httpURLString(host: String?, port: Int?, fallback: String) -> String {
        SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback)
    }

    private func resetOnboarding() {
        // Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
        self.appModel.disconnectGateway()
        self.connectingGatewayID = nil
        self.setupStatusText = nil
        self.setupCode = ""
        self.gatewayAutoConnect = false

        self.suppressCredentialPersist = true
        defer { self.suppressCredentialPersist = false }

        self.gatewayToken = ""
        self.gatewayPassword = ""

        let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmedInstanceId.isEmpty {
            GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
        }

        // Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks).
        GatewaySettingsStore.clearLastGatewayConnection()

        // RootCanvas also short-circuits onboarding when these are true.
        self.onboardingComplete = false
        self.hasConnectedOnce = false

        // Clear manual override so it doesn't count as an existing gateway config.
        self.manualGatewayEnabled = false
        self.manualGatewayHost = ""

        // Force re-present even without app restart.
        self.onboardingRequestID += 1

        // The onboarding wizard is presented from RootCanvas; dismiss Settings so it can show.
        self.dismiss()
    }

    private func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] {
        var lines: [String] = []
        if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") }
        if let tailnet = gateway.tailnetDns { lines.append("Tailnet: \(tailnet)") }

        let gatewayPort = gateway.gatewayPort
        let canvasPort = gateway.canvasPort
        if gatewayPort != nil || canvasPort != nil {
            let gw = gatewayPort.map(String.init) ?? "—"
            let canvas = canvasPort.map(String.init) ?? "—"
            lines.append("Ports: gateway \(gw) · canvas \(canvas)")
        }

        if lines.isEmpty {
            lines.append(gateway.debugID)
        }

        return lines
    }
}
