import SwiftUI
import CoreImage.CIFilterBuiltins
import NucleicCore
import NucleicProtocol
import NucleicTailnet

/// Settings ▸ Orbital ▸ "Mesh" section (host side, UX_IOS §7): runs the sync server and pairs
/// devices. "Enable meshing service" starts the listener; "Add device…" shows the pairing QR a
/// phone scans or a Mac copies; the meshed sibling Macs (join/presence/leave) are folded in via
/// `MeshMacsView`. This is the one section now — "Remote access" and "Mesh" were merged.
struct RemoteAccessSection: View {
    @Environment(AppStore.self) private var store
    @State private var showPairing = false

    /// Paired phones (Macs get their own section below).
    private var pairedPhones: [PairedDevice] {
        store.pairedDevices.filter { $0.kind != .mac }
    }

    var body: some View {
        Section("Mesh") {
            // `syncStartInFlight` keeps the toggle ON through a slow tailnet start (a
            // browser login can take minutes) instead of snapping back; toggling off
            // mid-start is honored at the start's commit point.
            Toggle("Enable meshing service", isOn: Binding(
                get: { store.syncRunning || store.syncStartInFlight },
                set: { on in
                    // Remember the choice so a relaunch (notably a Sparkle auto-update) restores it —
                    // `syncRunning` is in-memory only. Recorded here, from the user's action, rather
                    // than inside start/stopSyncServer, so the `restartIfRunning` bounces a transport
                    // or relay change triggers never clear the intent.
                    UserDefaults.standard.set(on, forKey: AppStore.remoteEnabledDefaultsKey)
                    Task { on ? await store.startSyncServer() : await store.stopSyncServer() }
                }
            ))

            // Shown while remote access is on *or* mid-(re)start — a connection-method
            // change bounces the server, and gating on `syncRunning` alone made the whole
            // pairing UI blink out during the restart. `beginPairing` needs the live host,
            // so the button stays put but disables until the restart commits.
            if store.syncRunning || store.syncStartInFlight {
                Button {
                    showPairing = true
                    Task { await store.beginPairing() }
                } label: {
                    Label("Add device…", systemImage: "qrcode")
                }
                .disabled(!store.syncRunning)

                if store.syncStartInFlight && !store.syncRunning {
                    Text("Reconnecting…")
                        .font(.caption).foregroundStyle(.secondary)
                }

                if pairedPhones.isEmpty {
                    Text("No paired iPhones yet.")
                        .font(.caption).foregroundStyle(.secondary)
                } else {
                    ForEach(pairedPhones, id: \.deviceID) { device in
                        // A live inbound connection means this phone is reachable right now
                        // (mirrors the Paired Macs dot) — otherwise it's paired but offline.
                        let online = store.inboundConnectedDeviceIDs.contains(device.deviceID)
                        HStack {
                            Label(device.label, systemImage: "iphone")
                            Circle()
                                .fill(online ? Color.green : Color.secondary.opacity(0.4))
                                .frame(width: 7, height: 7)
                            Spacer()
                            Button("Revoke") { Task { await store.revokeDevice(device.deviceID) } }
                                .buttonStyle(.borderless)
                                .foregroundStyle(.red)
                        }
                        .font(.callout)
                    }
                }

                // The sibling Macs meshed with this one (mesh P4): join, live presence,
                // and leaving — folded into this section now that "Remote access" and
                // "Mesh" are one. Only meaningful while the meshing service runs.
                MeshMacsView()
            }
        }
        .sheet(isPresented: $showPairing, onDismiss: { Task { await store.endPairing() } }) {
            PairingQRSheet()
        }

        // A chat stranded mid-transfer by a relaunch — surfaced regardless of remote-access state so
        // the user can always recover their work.
        if !store.pendingArrivedTransfers.isEmpty {
            InterruptedArrivalsSection()
        }

        ConnectionTransportSection()

        PushRelaySection()
    }
}

/// Settings ▸ Remote ▸ Interrupted arrivals (mesh P5): chats that arrived from a peer Mac and were
/// staged + verified here, but this Mac relaunched before the transfer committed. Each can be
/// activated anyway (the work is on disk) or discarded.
private struct InterruptedArrivalsSection: View {
    @Environment(AppStore.self) private var store

    private func sourceName(_ deviceID: String) -> String {
        store.pairedDevices.first { $0.deviceID == deviceID }?.label ?? "another Mac"
    }

    var body: some View {
        Section("Interrupted arrivals") {
            Text("These chats arrived from another Mac but weren't finished before this Mac "
                 + "restarted. Their work is here — activate to keep it, or discard it.")
                .font(.caption).foregroundStyle(.secondary)
                .fixedSize(horizontal: false, vertical: true)
            ForEach(store.pendingArrivedTransfers) { prompt in
                HStack {
                    VStack(alignment: .leading, spacing: 2) {
                        Text(prompt.title.isEmpty ? "Untitled chat" : prompt.title)
                            .font(.callout).lineLimit(1)
                        Text("from \(sourceName(prompt.sourceDeviceID))")
                            .font(.caption).foregroundStyle(.secondary)
                    }
                    Spacer()
                    Button("Activate") {
                        Task { await store.activateArrivedSession(prompt.transferID) }
                    }
                    .buttonStyle(.borderless)
                    Button("Discard", role: .destructive) {
                        Task { await store.discardArrivedSession(prompt.transferID) }
                    }
                    .buttonStyle(.borderless)
                    .foregroundStyle(.red)
                }
            }
        }
    }
}

/// The sibling Macs this one is meshed with (mesh P4): live presence and the connection
/// method in use, plus joining and leaving. Rendered inside the combined "Mesh" section
/// (it has no Section of its own). "Join mesh…" is the joining side of the ceremony — the
/// *other* Mac opens its pairing window (Add device…), the link is copied from its QR
/// sheet, and pasted here; it's hidden once this Mac is already a mesh member. Bulk hand-off
/// lives elsewhere now, so there's no per-peer "Hand off…" here.
private struct MeshMacsView: View {
    @Environment(AppStore.self) private var store
    @State private var showPairMac = false
    @State private var confirmLeave = false

    private var pairedMacs: [PairedDevice] {
        store.pairedDevices.filter { $0.kind == .mac }
    }

    var body: some View {
        Group {
            // Only offer joining when this Mac isn't already in a mesh — a member grows the
            // group by having *new* Macs join it (scan/paste its code), not by joining outward
            // again, which would be joining a group it's already in.
            if pairedMacs.isEmpty {
                Button {
                    showPairMac = true
                } label: {
                    Label("Join mesh…", systemImage: "link")
                }
            }

            ForEach(pairedMacs, id: \.deviceID) { mac in
                let presence = store.meshPeers.first { $0.deviceID == mac.deviceID }
                let inbound = store.inboundConnectedDeviceIDs.contains(mac.deviceID)
                let online = (presence?.connected ?? false) || inbound
                VStack(alignment: .leading, spacing: 2) {
                    HStack {
                        Label {
                            Text(mac.label)
                        } icon: {
                            Image(systemName: "macbook")
                        }
                        Circle()
                            .fill(online ? Color.green : Color.secondary.opacity(0.4))
                            .frame(width: 7, height: 7)
                        if let transport = presence?.transport, presence?.connected == true {
                            Text("via \(transport.label)")
                                .font(.caption).foregroundStyle(.secondary)
                        }
                        Spacer()
                        Button("Revoke") { Task { await store.revokeDevice(mac.deviceID) } }
                            .buttonStyle(.borderless)
                            .foregroundStyle(.red)
                    }
                    .font(.callout)
                    if let error = presence?.lastError, !online {
                        Text(error)
                            .font(.caption).foregroundStyle(.orange)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
            }

            if !pairedMacs.isEmpty {
                Button(role: .destructive) {
                    confirmLeave = true
                } label: {
                    Label("Leave mesh", systemImage: "rectangle.portrait.and.arrow.right")
                }
            }
        }
        .confirmationDialog(
            "Leave the mesh?", isPresented: $confirmLeave, titleVisibility: .visible
        ) {
            Button("Leave mesh", role: .destructive) { Task { await store.leaveMesh() } }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("This leaves the group, forgetting all \(pairedMacs.count) mesh Mac\(pairedMacs.count == 1 ? "" : "s") and dropping every mesh connection. Your sessions stop appearing on those Macs, and theirs stop appearing here. The other members drop this Mac too. You can re-join anytime.")
        }
        .sheet(isPresented: $showPairMac) {
            PairMacSheet()
        }
    }
}

/// Bulk hand-off (mesh P5): pick which live chats to move to one connected peer Mac at once —
/// the "take the laptop home, hand its sessions to the Mac that stays" flow. Each selected chat
/// rides the same single-session transfer path, run in sequence.
///
/// Currently unwired from Settings — the per-peer "Hand off…" button was removed from the Mesh
/// section; this sheet is retained for the relocated hand-off entry point.
private struct HandoffSheet: View {
    @Environment(AppStore.self) private var store
    @Environment(\.dismiss) private var dismiss
    let deviceID: String
    let label: String

    @State private var sessions: [NucleicCore.SessionSummary] = []
    @State private var selected: Set<SessionID> = []
    @State private var loaded = false
    @State private var moving = false

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Hand off to \(label)").font(.title2).bold()

            if !loaded {
                ProgressView().frame(maxWidth: .infinity, minHeight: 120)
            } else if sessions.isEmpty {
                Text("No chats are ready to move. A chat must be idle, standalone (not part of a "
                     + "stack), and have its own worktree.")
                    .font(.callout).foregroundStyle(.secondary)
                    .frame(maxWidth: .infinity, alignment: .leading)
            } else {
                Text("Each chat is quiesced, bundled, and reopened on \(label). It leaves this Mac "
                     + "as a “Moved to \(label)” tombstone.")
                    .font(.caption).foregroundStyle(.secondary)
                HStack {
                    Button(selected.count == sessions.count ? "Deselect all" : "Select all") {
                        selected = selected.count == sessions.count ? [] : Set(sessions.map(\.id))
                    }
                    .buttonStyle(.borderless).font(.caption)
                    Spacer()
                }
                ScrollView {
                    VStack(alignment: .leading, spacing: 2) {
                        ForEach(sessions) { summary in
                            Toggle(isOn: Binding(
                                get: { selected.contains(summary.id) },
                                set: { on in
                                    if on { selected.insert(summary.id) } else { selected.remove(summary.id) }
                                }
                            )) {
                                VStack(alignment: .leading, spacing: 1) {
                                    Text(summary.title).lineLimit(1)
                                    Text(store.project(summary.projectID)?.name ?? "")
                                        .font(.caption).foregroundStyle(.secondary).lineLimit(1)
                                }
                            }
                        }
                    }
                    .padding(.vertical, 4)
                }
                .frame(minHeight: 160)
            }

            HStack {
                Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction)
                Spacer()
                if !sessions.isEmpty {
                    Button(moving ? "Moving…" : moveButtonTitle) {
                        moving = true
                        Task {
                            await store.moveSessionsToPeer(Array(selected), to: deviceID, label: label)
                            dismiss()
                        }
                    }
                    .keyboardShortcut(.defaultAction)
                    .disabled(selected.isEmpty || moving)
                }
            }
        }
        .padding(20)
        .frame(width: 440, height: 480)
        .task {
            sessions = await store.transferableSessions()
            selected = Set(sessions.map(\.id))   // default: hand off everything
            loaded = true
        }
    }

    private var moveButtonTitle: String {
        selected.count == 1 ? "Move 1 chat" : "Move \(selected.count) chats"
    }
}

/// The joining Mac's paste-a-link pairing flow (mesh P4). There is no URL-scheme handler —
/// `nucleic://pair?d=…` is just text; `PairingPayload(qrString:)` decodes whatever is pasted.
private struct PairMacSheet: View {
    @Environment(AppStore.self) private var store
    @Environment(\.dismiss) private var dismiss
    @State private var code = ""
    @State private var error: String?
    @State private var pairing = false
    @State private var pairTask: Task<Void, Never>?

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Join mesh").font(.title2).bold()
            Text("On a Mac already in the mesh, enable the meshing service, click “Add device…”, "
                 + "copy the join code, and paste it here. Its user confirms over there — and this "
                 + "joins you to the whole group, not just that one Mac.")
                .font(.caption).foregroundStyle(.secondary)
                .fixedSize(horizontal: false, vertical: true)
            TextField("Join link", text: $code, prompt: Text("nucleic://pair?d=…"))
                .autocorrectionDisabled()
                .disabled(pairing)
            if pairing {
                Text("Connecting — waiting for the other Mac to let you in…")
                    .font(.caption).foregroundStyle(.secondary)
            }
            if let error {
                Text(error)
                    .font(.caption).foregroundStyle(.orange)
                    .fixedSize(horizontal: false, vertical: true)
            }
            HStack {
                if pairing { ProgressView().controlSize(.small) }
                Spacer()
                Button("Cancel") { dismiss() }
                Button("Join") { pairTask = Task { await pair() } }
                    .keyboardShortcut(.defaultAction)
                    .buttonStyle(.borderedProminent)
                    .disabled(pairing || code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
            }
        }
        .padding(20)
        .frame(width: 460)
        // Dismissing (Cancel, Esc, click-away) aborts an in-flight attempt — otherwise pair()
        // keeps running headless and could pin the peer after the user backed out.
        .onDisappear { pairTask?.cancel() }
    }

    private func pair() async {
        pairing = true
        error = nil
        defer { pairing = false }
        do {
            try await store.joinMesh(with: code)
            dismiss()
        } catch let pairError as PeerPairError {
            error = pairError.errorDescription
        } catch is CancellationError {
            // Sheet dismissed mid-attempt; nothing to report.
        } catch {
            self.error = "Pairing failed: \(error.localizedDescription)"
        }
    }
}

/// Settings ▸ Remote ▸ Connection methods: which methods paired devices reach this Mac
/// over — a multi-select, not a picker; the sync server listens on every enabled method
/// at once and clients dial whichever they can reach. Local network is the zero-config
/// default. Tailnet runs an embedded Tailscale node (TailscaleKit) so devices connect
/// from anywhere on the user's tailnet — no relay in the path, same Noise E2EE. Nucleic
/// Private Relay (SYNC_PROTOCOL §3.2) is the zero-config fallback: both sides dial out to
/// a Cloudflare room that only ever sees ciphertext. When only Local network is on, a
/// banner strongly recommends adding Tailnet or Relay — devices lose each other
/// off-network otherwise.
private struct ConnectionTransportSection: View {
    @Environment(AppStore.self) private var store
    @AppStorage(SyncTransportSetting.setDefaultsKey) private var transportsRaw = ""

    /// The enabled method set; an empty stored string defers to `resolveEnabled`, which
    /// migrates the legacy single-choice picker value.
    private var enabled: Set<SyncTransportHint> {
        let parsed = SyncTransportSetting.parseEnabled(transportsRaw)
        return parsed.isEmpty ? SyncTransportSetting.resolveEnabled() : parsed
    }

    var body: some View {
        Section("Connection methods") {
            methodToggle(.lan, title: "Local network")

            methodToggle(.tailnet, title: "Tailnet")
            if enabled.contains(.tailnet) {
                tailnetDetails
            }

            methodToggle(.relay, title: "Nucleic Private Relay")

            if enabled == [.lan] {
                lanOnlyWarning
            }
        }
        // Turning Tailnet on registers the node, which mints a browser-login URL — take the
        // user straight there once (independent of remote access); the button below re-opens.
        .onChange(of: store.tailnetLoginURL) {
            if let url = store.tailnetLoginURL { NSWorkspace.shared.open(url) }
        }
    }

    @ViewBuilder
    private func methodToggle(_ hint: SyncTransportHint, title: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Toggle(isOn: methodBinding(hint)) {
                HStack(spacing: 6) {
                    Text(title)
                    statusDot(hint)
                }
            }
            .disabled(enabled == [hint])  // the last method can't be unchecked
            if let failure = store.syncTransportHealth[hint], enabled.contains(hint) {
                Text(failure)
                    .font(.caption).foregroundStyle(.orange)
                    .fixedSize(horizontal: false, vertical: true)
            }
        }
    }

    /// A labeled connection state for the method, so "is the relay actually up?" is legible
    /// rather than inferred from a bare dot: green "Connected" = listener up (for the relay,
    /// dialed into its room and reachable), orange "Not reachable" = enabled but the start
    /// failed, "Connecting…" while a (re)start is in flight. Nothing when the sync server is
    /// off or the method is unchecked.
    @ViewBuilder
    private func statusDot(_ hint: SyncTransportHint) -> some View {
        if store.syncStartInFlight, enabled.contains(hint) {
            Text("Connecting…")
                .font(.caption).foregroundStyle(.secondary)
        } else if store.syncRunning, enabled.contains(hint) {
            let active = store.syncActiveTransports.contains(hint)
            Circle()
                .fill(active ? Color.green : Color.orange)
                .frame(width: 7, height: 7)
            Text(active ? "Connected" : "Not reachable")
                .font(.caption)
                .foregroundStyle(active ? Color.green : Color.orange)
        }
    }

    private var tailnetDetails: some View {
        Group {
            if TailnetSupport.isBuiltIn {
                if let status = store.tailnetStatus {
                    LabeledContent("Tailscale node", value: status)
                }
                if let loginURL = store.tailnetLoginURL {
                    Button {
                        NSWorkspace.shared.open(loginURL)
                    } label: {
                        Label("Open Tailscale login page", systemImage: "person.crop.circle.badge.checkmark")
                    }
                }
            } else {
                Text("This build doesn't include Tailscale support — run scripts/build-tailscalekit.sh and rebuild.")
                    .font(.caption).foregroundStyle(.orange)
                    .fixedSize(horizontal: false, vertical: true)
            }
        }
    }

    /// The strong recommendation (mesh P1): LAN-only means devices lose each other the
    /// moment they leave the network. Persistent while the condition holds.
    private var lanOnlyWarning: some View {
        VStack(alignment: .leading, spacing: 4) {
            Label("Local network only", systemImage: "exclamationmark.triangle.fill")
                .font(.callout.weight(.semibold))
                .foregroundStyle(.orange)
            Text("Your devices will lose each other the moment they leave this network. "
                 + "Add Tailnet or Nucleic Private Relay so they can always reconnect.")
                .font(.caption)
                .fixedSize(horizontal: false, vertical: true)
            HStack {
                Button("Use Nucleic Private Relay") { methodBinding(.relay).wrappedValue = true }
                Button("Use my Tailnet") { methodBinding(.tailnet).wrappedValue = true }
            }
        }
        .padding(.vertical, 4)
    }

    private func methodBinding(_ hint: SyncTransportHint) -> Binding<Bool> {
        Binding(
            get: { enabled.contains(hint) },
            set: { on in
                var set = enabled
                if on { set.insert(hint) } else { set.remove(hint) }
                guard !set.isEmpty else { return }  // at least one method stays enabled
                transportsRaw = SyncTransportSetting.serializeEnabled(set)
                // Tailnet registers its node the moment it's enabled — independent of remote
                // access — so the browser login opens right away. When remote access *is*
                // running, the restart binds/unbinds the listener (and owns the node) instead.
                if hint == .tailnet, !store.syncRunning {
                    Task { on ? await store.startTailnetNode() : await store.stopTailnetNode() }
                } else {
                    restartIfRunning()
                }
            })
    }

    /// Settings are read when the sync server starts; make a method change take effect
    /// immediately by bouncing remote access when it's running (same as the Relay toggle).
    private func restartIfRunning() {
        guard store.syncRunning else { return }
        Task {
            await store.stopSyncServer()
            await store.startSyncServer()
        }
    }
}

/// The Relay section (docs/PUSH_SETUP.md §4): one toggle. Turning it on lets the Mac ask
/// the relay to wake paired iPhones for approvals when they aren't connected. There is
/// nothing to configure — the host self-enrolls with the relay for its own scoped
/// credential (Keychain-held, invisible here), the relay holds the APNS key in its Worker
/// secrets, and the push is a content-free tickle. The relay URL and APNS-environment
/// overrides surface only in local development builds; distributed builds use the
/// production relay and production APNS.
private struct PushRelaySection: View {
    @Environment(AppStore.self) private var store
    @AppStorage("nucleic.relay.enabled") private var relayEnabled = false
    // Dev-build-only overrides (env vars of the same names win, for scripted runs).
    @AppStorage("NUCLEIC_RELAY_URL") private var relayURL = ""
    @AppStorage("NUCLEIC_APNS_ENV") private var apnsEnv = "production"

    private var isDevBuild: Bool { BuildInfo.current.channel == .local }

    var body: some View {
        Section("Relay") {
            Toggle("Push notifications", isOn: $relayEnabled)
                .onChange(of: relayEnabled) { restartIfRunning() }

            if isDevBuild {
                TextField("Relay URL (dev)", text: $relayURL,
                          prompt: Text(PushRelayConfig.defaultBaseURL.absoluteString))
                    .autocorrectionDisabled()
                    .onSubmit { restartIfRunning() }
                Picker("APNS environment (dev)", selection: $apnsEnv) {
                    Text("Production (TestFlight / App Store)").tag("production")
                    Text("Sandbox (Xcode installs)").tag("sandbox")
                }
                .onChange(of: apnsEnv) { restartIfRunning() }
            }
        }
    }

    /// Settings are read when the sync server starts; make the toggle take effect
    /// immediately by bouncing remote access when it's on.
    private func restartIfRunning() {
        guard store.syncRunning else { return }
        Task {
            await store.stopSyncServer()
            await store.startSyncServer()
        }
    }
}

/// Renders the active pairing QR (`store.pairingQR`) for the phone to scan — or, for a
/// sibling Mac, the same payload as a copyable link (mesh P4). Also hosts the confirm
/// dialog a Mac pairing raises: the code came from this window, so the user is right here.
struct PairingQRSheet: View {
    @Environment(AppStore.self) private var store
    @Environment(\.dismiss) private var dismiss
    @State private var copiedCode = false
    @State private var copyResetTask: Task<Void, Never>?

    var body: some View {
        VStack(spacing: 16) {
            Text("Join this group").font(.headline)
            if let qr = store.pairingQR {
                if let image = Self.qrImage(qr) {
                    Image(nsImage: image)
                        .interpolation(.none)
                        .resizable()
                        .frame(width: 240, height: 240)
                        .background(.white)
                        .padding(8)
                        .background(.white, in: RoundedRectangle(cornerRadius: 12))
                } else {
                    ProgressView().frame(width: 240, height: 240)
                }
                Text("Scan from an iPhone or iPad (Nucleic Remote ▸ Join) to join it to the "
                     + "whole group — every Mac and iPhone here then sees each other.")
                    .font(.caption).foregroundStyle(.secondary)
                    .multilineTextAlignment(.center)

                // The copy path is a peer to the QR, not a fallback: a sibling Mac has no
                // camera, so pasting this code is the *only* way to join one. An "or" divider
                // and a full-width prominent button give it the same weight as the QR.
                HStack(spacing: 8) {
                    VStack { Divider() }
                    Text("or").font(.caption).foregroundStyle(.secondary)
                    VStack { Divider() }
                }

                VStack(spacing: 8) {
                    Text(qr)
                        .font(.system(.caption, design: .monospaced))
                        .lineLimit(1)
                        .truncationMode(.middle)
                        .textSelection(.enabled)
                        .padding(.horizontal, 10)
                        .padding(.vertical, 7)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(.quaternary, in: RoundedRectangle(cornerRadius: 8))

                    Button(action: copyCode) {
                        Label(copiedCode ? "Copied" : "Copy join code",
                              systemImage: copiedCode ? "checkmark" : "doc.on.doc")
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .tint(copiedCode ? .green : .accentColor)

                    Text("Paste into “Join mesh…” on another Mac, or “Enter code manually” "
                         + "on an iPhone or iPad.")
                        .font(.caption).foregroundStyle(.secondary)
                        .multilineTextAlignment(.center)
                }
            } else {
                ProgressView().frame(width: 240, height: 240)
            }
            Button("Done") { dismiss() }.keyboardShortcut(.defaultAction)
        }
        .padding(24)
        .frame(width: 320)
        .onDisappear { copyResetTask?.cancel() }
        .confirmationDialog(
            "Let “\(store.pendingMacPairRequest?.label ?? "another Mac")” join?",
            isPresented: Binding(
                get: { store.pendingMacPairRequest != nil },
                set: { if !$0 { store.resolveMacPairing(false) } }),
            titleVisibility: .visible
        ) {
            Button("Join") { store.resolveMacPairing(true) }
            Button("Cancel", role: .cancel) { store.resolveMacPairing(false) }
        } message: {
            Text("It joins the whole group — it gets the same control of this Mac's sessions an "
                 + "iPhone does, and every Mac here stays connected for hand-offs.")
        }
    }

    /// Copy the join code and briefly flip the button to a checkmark — same confirmation
    /// pattern as `CopyButton`, inlined here so the prominent labeled button owns its state.
    private func copyCode() {
        guard let qr = store.pairingQR else { return }
        Clipboard.copy(qr)
        withAnimation(.easeOut(duration: 0.12)) { copiedCode = true }
        copyResetTask?.cancel()
        copyResetTask = Task {
            try? await Task.sleep(for: .seconds(1.4))
            guard !Task.isCancelled else { return }
            withAnimation(.easeIn(duration: 0.2)) { copiedCode = false }
        }
    }

    private static func qrImage(_ string: String) -> NSImage? {
        let filter = CIFilter.qrCodeGenerator()
        filter.message = Data(string.utf8)
        filter.correctionLevel = "M"
        guard let output = filter.outputImage else { return nil }
        let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
        let context = CIContext()
        guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
        return NSImage(cgImage: cg, size: NSSize(width: scaled.extent.width, height: scaled.extent.height))
    }
}
