// PIN pairing sheet. The host shows the pairing PIN in its web console (port 3000 →
// Pairing; also printed in the host's log when armed via --allow-pairing); the user
// types it here. The ceremony is SPAKE2, so a wrong PIN buys an
// attacker exactly one online guess — for the user a typo just means "try again" (the
// host rate-limits ceremonies to one per 2 s). Success returns the host's now-VERIFIED
// fingerprint: the caller pins it, no manual comparison needed, and the host stores this
// client's identity in return.

import Foundation
import PunktfunkKit
import SwiftUI

/// Dismissing the sheet must abandon an in-flight ceremony: the blocking pair() call
/// can't be interrupted, so its completion checks this flag and self-discards — a late
/// success must NOT pin and auto-connect to a host the user cancelled out of. Only
/// touched on the main actor.
private final class CeremonyToken: @unchecked Sendable {
    var cancelled = false
}

struct PairSheet: View {
    @Environment(\.dismiss) private var dismiss
    let host: StoredHost
    /// Called with the verified host fingerprint after a successful ceremony.
    let onPaired: (Data) -> Void

    @State private var pin = ""
    #if os(macOS)
    @State private var clientName = Host.current().localizedName ?? "Mac"
    #else
    @State private var clientName = UIDevice.current.name
    #endif
    @State private var busy = false
    @State private var errorText: String?
    @State private var token = CeremonyToken()
    #if os(tvOS)
    private enum EditField: String, Identifiable {
        case pin, clientName
        var id: String { rawValue }
    }
    @State private var editing: EditField?
    #endif

    var body: some View {
        #if os(tvOS)
        VStack(spacing: 24) {
            Text("The PIN is shown in the host's web console "
                + "(http://<host>:3000 → Pairing). "
                + "Pairing verifies both sides at once — no fingerprint comparison "
                + "needed.")
                .font(.geist(16, relativeTo: .callout))
                .foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
            TVFieldRow(
                label: "PIN", value: pin, placeholder: "Shown in the host's web console"
            ) { editing = .pin }
            TVFieldRow(
                label: "Device name", value: clientName, placeholder: "Apple TV"
            ) { editing = .clientName }
            if let errorText {
                Text(errorText)
                    .font(.geist(16, relativeTo: .callout))
                    .foregroundStyle(.red)
            }
            HStack(spacing: 32) {
                Button("Cancel", role: .cancel) {
                    token.cancelled = true
                    dismiss()
                }
                if busy {
                    ProgressView()
                }
                Button("Pair & Connect") { runCeremony() }
                    .disabled(busy || pin.trimmingCharacters(in: .whitespaces).isEmpty)
            }
            .padding(.top, 12)
        }
        .frame(maxWidth: 1000)
        .padding(60)
        .navigationTitle("Pair with \(host.displayName)")
        .onDisappear { token.cancelled = true }
        .fullScreenCover(item: $editing) { field in
            switch field {
            case .pin:
                TVTextEntry(
                    title: "PIN (shown in the host's web console)", text: pin,
                    keyboardType: .numberPad
                ) {
                    pin = $0.trimmingCharacters(in: .whitespaces)
                    editing = nil
                }
            case .clientName:
                TVTextEntry(title: "Device name", text: clientName) {
                    clientName = $0
                    editing = nil
                }
            }
        }
        #else
        VStack(spacing: 0) {
            Form {
                Section {
                    TextField(
                        "PIN", text: $pin,
                        prompt: Text("Shown in the host's web console"))
                        .font(.system(.title3, design: .monospaced))
                        #if os(iOS)
                        .keyboardType(.numberPad)
                        #endif
                    TextField(
                        "Client name", text: $clientName,
                        prompt: Text("How the host lists this Mac"))
                        #if os(tvOS)
                        .labelsHidden() // prefilled → tvOS floats the label off-center
                        #endif
                } header: {
                    Label("Pair with \(host.displayName)", systemImage: "lock.shield")
                        .foregroundStyle(.tint)
                } footer: {
                    Text("The PIN is shown in the host's web console "
                        + "(http://<host>:3000 → Pairing). "
                        + "Pairing verifies both sides at once — no fingerprint "
                        + "comparison needed.")
                        .font(.geist(12, relativeTo: .caption))
                        .foregroundStyle(.secondary)
                }
                if let errorText {
                    Section {
                        Text(errorText)
                            .font(.geist(16, relativeTo: .callout))
                            .foregroundStyle(.red)
                    }
                }
            }
            #if !os(tvOS)
        .formStyle(.grouped)
        #endif
            HStack {
                Button("Cancel", role: .cancel) {
                    token.cancelled = true
                    dismiss()
                }
                #if !os(tvOS)
                .keyboardShortcut(.cancelAction)
                #endif
                Spacer()
                if busy {
                    ProgressView()
                        .controlSize(.small)
                        .padding(.trailing, 8)
                }
                Button("Pair & Connect") { runCeremony() }
                    .glassProminentButtonStyle()
                    #if !os(tvOS)
                    .keyboardShortcut(.defaultAction)
                    #endif
                    .disabled(busy || pin.trimmingCharacters(in: .whitespaces).isEmpty)
            }
            #if os(iOS)
            .controlSize(.large)
            #endif
            .padding(16)
        }
        #if os(macOS)
        .frame(width: 400)
        .fixedSize(horizontal: false, vertical: true)
        #endif
        #if os(iOS)
        // Bottom sheet instead of a full-screen modal (Liquid Glass background on iOS 26).
        // .medium rests; .large is included so the sheet grows to keep the Pair/Cancel row
        // above the keyboard when the PIN field is focused. Hide the grabber while the ceremony
        // is in flight — dismissal is disabled then (interactiveDismissDisabled), so a drag
        // would only rubber-band; the always-enabled Cancel button is the exit.
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(busy ? .hidden : .visible)
        #endif
        .interactiveDismissDisabled(busy)
        .onDisappear { token.cancelled = true } // any other dismissal path
        #endif
    }

    private func runCeremony() {
        busy = true
        errorText = nil
        let pin = pin.trimmingCharacters(in: .whitespaces)
        let name = clientName.trimmingCharacters(in: .whitespaces)
        let address = host.address
        let port = host.port
        let token = token
        Task.detached(priority: .userInitiated) {
            // Identity load + the ceremony both block — keep them off the main actor.
            // loadForPairing is the strict variant: the host durably trusts this
            // identity, so it must have made it into the Keychain.
            let result = Result {
                let identity = try ClientIdentityStore.shared.loadForPairing()
                return try PunktfunkKit.pair(
                    host: address, port: port, identity: identity,
                    pin: pin, name: name.isEmpty ? "Mac" : name)
            }
            await MainActor.run {
                guard !token.cancelled else { return } // sheet dismissed mid-ceremony
                busy = false
                switch result {
                case .success(let fingerprint):
                    onPaired(fingerprint)
                    dismiss()
                case .failure(PunktfunkClientError.wrongPIN):
                    errorText = "Wrong PIN — check the host's web console (port 3000) "
                        + "and try again."
                case .failure(is ClientIdentityStore.IdentityError):
                    errorText = "Can't store this Mac's identity in the Keychain, so the "
                        + "pairing would not survive a relaunch. Unlock the login "
                        + "keychain and try again."
                case .failure:
                    errorText = "Pairing failed. Is the host reachable, pairing armed "
                        + "(web console → Pairing), and not mid-session? Retries are "
                        + "rate-limited to one per 2 seconds."
                }
            }
        }
    }
}
