// The gamepad UI's answer to a system alert / confirmation dialog (iOS/iPadOS/macOS).
//
// `.alert` and `.confirmationDialog` are UIKit/AppKit surfaces. A game controller cannot move
// through their buttons or press one — so on iOS/macOS every prompt in the connect path was a dead
// end for a pad-only user, and they are not incidental prompts:
//
//   - "Pairing required" (Request Access / Pair with PIN…) is the FIRST thing an unpaired host
//     shows. Pairing was unreachable before it even got to the PIN.
//   - "Connection failed" strands the console UI behind a modal only a finger can dismiss.
//   - "Waiting for approval" owns the only Cancel for a connect that may never complete.
//
// tvOS keeps the system alerts: the focus engine drives them natively there, which is the whole
// reason this gap was tvOS-invisible.
//
// Deliberately NOT built on GamepadMenuList: that is a ScrollView (right for a settings screen of
// unknown length, wrong for two buttons in a card, where it would need an invented height and
// could clip). A prompt has two or three actions, so it owns a plain VStack and a cursor.

import PunktfunkKit
import SwiftUI
#if os(iOS) || os(macOS)

/// One choice in a console prompt.
struct GamepadPromptAction: Identifiable {
    let id: String
    let title: String
    /// This is the action B (and Esc) performs, and the one the cursor opens on. Exactly one
    /// action should carry it — `GamepadPrompt` falls back to the LAST action when none does,
    /// which matches how a system alert treats its cancel role.
    var isCancel = false
    /// Drawn as the primary, accent-tinted row. At most one.
    var isPrimary = false
    let run: () -> Void
}

/// A prompt to show over the console UI: what happened, and what can be done about it.
struct GamepadPrompt: Identifiable {
    let id: String
    let title: String
    let message: String
    let actions: [GamepadPromptAction]
    /// A wait with no outcome yet (the delegated-approval hold) shows a spinner beside the title —
    /// the prompt is the UI for something still in flight, not a report that it finished.
    var busy = false
}

/// The prompt, worn as the console's own modal: a dimmed field, a glass card, a focus list of
/// actions, and the same legend every other gamepad screen carries.
struct GamepadPromptView: View {
    @Environment(\.gamepadInk) private var ink
    @Environment(\.gamepadMetrics) private var metrics
    let prompt: GamepadPrompt

    @State private var cursor = 0
    @State private var input = GamepadMenuInput(manager: .shared)
    @State private var haptics = MenuHaptics(manager: .shared)
    /// `.sensoryFeedback` counters — device ticks for confirm and for a refused move at an end.
    @State private var activateTick = 0
    @State private var boundaryTick = 0

    #if os(iOS)
    @Environment(\.verticalSizeClass) private var vSizeClass

    private var compact: Bool { vSizeClass == .compact }
    #else
    private let compact = false
    #endif

    var body: some View {
        ZStack {
            // Swallows touch to the launcher behind it, which is also gated out of the controller
            // poll for as long as this is up (ContentView's `promptActive`).
            Rectangle()
                .fill(.black.opacity(0.55))
                .ignoresSafeArea()
                .contentShape(Rectangle())
                .onTapGesture {}
            card
        }
        .sensoryFeedback(.selection, trigger: cursor)
        .sensoryFeedback(.impact(weight: .medium), trigger: activateTick)
        .sensoryFeedback(.impact(flexibility: .rigid, intensity: 0.7), trigger: boundaryTick)
        // A prompt is exactly where a keyboard user gets stuck, so it takes arrows/Return/Esc too.
        .gamepadKeyNavigation(
            onMove: { direction in
                switch direction {
                case .up: step(by: -1)
                case .down: step(by: 1)
                case .left, .right: break
                }
            },
            onConfirm: { activate() },
            onBack: { back() })
        .onAppear {
            cursor = prompt.actions.firstIndex(where: \.isCancel) ?? max(prompt.actions.count - 1, 0)
            wire()
            input.start()
        }
        // The prompt's identity is stable across a message change (same `id`), so re-wire rather
        // than rely on a remount: the stored closures captured the OLD actions array.
        .onChange(of: prompt.actions.map(\.id)) { _, _ in
            cursor = min(cursor, max(prompt.actions.count - 1, 0))
            wire()
        }
        .onDisappear {
            input.stop()
            haptics.stop()
        }
    }

    private var card: some View {
        VStack(alignment: .leading, spacing: 14) {
            HStack(spacing: 10) {
                if prompt.busy {
                    ProgressView().controlSize(.small).tint(ink.fg(0.8))
                }
                Text(prompt.title)
                    .font(.geist(compact ? 19 : 22, .bold, relativeTo: .title3))
                    .foregroundStyle(ink.fg)
            }
            Text(prompt.message)
                .font(.geist(metrics.detailFont, relativeTo: .callout))
                .foregroundStyle(ink.fg(0.62))
                .fixedSize(horizontal: false, vertical: true)
            VStack(spacing: 6) {
                ForEach(Array(prompt.actions.enumerated()), id: \.element.id) { idx, action in
                    actionRow(action, focused: idx == cursor)
                        .contentShape(Rectangle())
                        .onTapGesture { tap(idx) }
                }
            }
            .padding(.top, 2)
            GamepadHintBar(hints: hints)
        }
        .padding(compact ? 20 : 26)
        .frame(maxWidth: 460)
        .consoleGlass(RoundedRectangle(cornerRadius: 24, style: .continuous))
        .overlay {
            RoundedRectangle(cornerRadius: 24, style: .continuous)
                .strokeBorder(ink.fg(0.12), lineWidth: 1)
        }
        .padding(24)
    }

    private func actionRow(_ action: GamepadPromptAction, focused: Bool) -> some View {
        let m = metrics
        return Text(action.title)
            .font(.geist(m.labelFont, .semibold, relativeTo: .body))
            .foregroundStyle(action.isPrimary ? ink.accent : ink.fg)
            .frame(maxWidth: .infinity)
            .padding(.horizontal, m.rowHPad)
            .padding(.vertical, m.rowVPad)
            .consoleGlass(
                RoundedRectangle(cornerRadius: m.rowCorner, style: .continuous),
                tint: focused ? ink.accent(0.30) : nil,
                interactive: focused)
            .overlay {
                RoundedRectangle(cornerRadius: m.rowCorner, style: .continuous)
                    .strokeBorder(ink.fg(focused ? 0.28 : 0.06), lineWidth: 1)
            }
            .scaleEffect(focused ? 1.0 : 0.98)
            .animation(.smooth(duration: 0.18), value: focused)
    }

    private var hints: [GamepadHint] {
        var hints: [GamepadHint] = [.init(
            glyph: buttonGlyph(\.buttonA, fallback: "a.circle"), text: "Select",
            action: { activate() })]
        // Only where B has somewhere to go: a one-action prompt ("OK") is dismissed by that
        // action, and B does it too — naming it twice would just be noise.
        if prompt.actions.count > 1, let cancel = prompt.actions.first(where: \.isCancel) {
            hints.append(.init(
                glyph: buttonGlyph(\.buttonB, fallback: "b.circle"), text: cancel.title,
                action: { back() }))
        }
        return hints
    }

    // MARK: - Input

    private func wire() {
        input.onMove = { direction in
            switch direction {
            case .up: step(by: -1)
            case .down: step(by: 1)
            // A prompt's actions are a vertical list; left/right have nothing to mean here, and
            // silently treating them as up/down would make a nudged stick pick a different button.
            case .left, .right: break
            }
        }
        input.onConfirm = { activate() }
        input.onBack = { back() }
    }

    private func step(by delta: Int) {
        let target = cursor + delta
        guard target >= 0, target < prompt.actions.count else {
            boundaryTick &+= 1
            haptics.boundary()
            return
        }
        cursor = target
        haptics.move()
    }

    private func activate() {
        guard cursor >= 0, cursor < prompt.actions.count else { return }
        activateTick &+= 1
        haptics.confirm()
        prompt.actions[cursor].run()
    }

    /// B: the cancel action, else the last one — the same fallback a system alert applies when
    /// nothing carries the cancel role, so B always has a way out rather than doing nothing.
    private func back() {
        guard let action = prompt.actions.first(where: \.isCancel) ?? prompt.actions.last
        else { return }
        activateTick &+= 1
        haptics.confirm()
        action.run()
    }

    /// Touch fallback matching the rest of the gamepad UI: a tap focuses AND activates.
    private func tap(_ idx: Int) {
        guard idx >= 0, idx < prompt.actions.count else { return }
        cursor = idx
        activate()
    }
}
#endif
