import SwiftUI

// MARK: - Presentation

/// Whether *this* card face is the one showing the open **Labels ▸ More…** dialog.
///
/// `styleEditorPresentation`'s shape and its reasoning, one session over and simpler for it: a label
/// session names exactly one card, so the anchor test is an identity comparison rather than a walk for
/// the first live member of a set. The setter is narrowed to this anchor's own dismissal for that
/// function's reason exactly — a session that has moved on must not be discarded by a surface it is no
/// longer about.
@MainActor
func labelEditorPresentation(_ store: BoardStore, anchor: ItemID) -> Binding<Bool> {
    Binding(
        get: { store.transient.labelEditor?.cardID == anchor },
        set: { presented in
            guard !presented, store.transient.labelEditor?.cardID == anchor else { return }
            store.transient.discardLabelEditor()
        }
    )
}

// MARK: - The dialog

/// **Labels ▸ More…** — every label the board uses, each with a toggle, plus a field that creates a
/// new one (the owner's card-menu spec: "More… (show a dialog with a list of all used labels +
/// ability to create new)").
///
/// ### Why it exists beside the twelve
///
/// The submenu's rows are a *shortcut* — the labels this user is most likely to want, ranked
/// (`LabelRanking`). This is the **inventory**: every label in the board's universe, however rarely
/// used, in an order built for looking one up rather than for reaching the common ones fast
/// (`LabelIndex.alphabetical`). It is also the only surface on the board side that can mint a label
/// the board has never used — the twelve can only ever offer what already exists.
///
/// ### A popover, not a sheet
///
/// "Dialog" is the owner's word for the shape, not necessarily for the presentation. A sheet would
/// block the board window for a gesture whose whole character is quick tagging, and it would have
/// nothing to anchor to — the user right-clicked a specific card and the answer belongs beside it.
/// Style… is the precedent in this exact position: same anchor, same session-backed lifecycle, same
/// dismissal on click-away. Flagged for owner review if a real modal was meant.
///
/// ### One card
///
/// Every row is a checkmark, and a checkmark states a fact about one card — see
/// `CardFaceView.labelsMenu` for the argument and why it is also what keeps the menu render-safe.
struct LabelPickerPopover: View {

    let store: BoardStore
    let recents: LabelRecents

    /// What is typed in the create field.
    @State private var draft = ""

    @FocusState private var fieldFocused: Bool

    var body: some View {
        // Empty for the frame between a session ending and the popover's own dismissal landing —
        // `StyleEditorPopover`'s own formality, and for its reason.
        if let session = store.transient.labelEditor {
            content(for: session.cardID)
        }
    }

    private func content(for cardID: ItemID) -> some View {
        let current = store.labels(ofCard: cardID)
        let universe = store.labelIndex.alphabetical
        return VStack(alignment: .leading, spacing: 8) {
            Text("Labels")
                .font(.caption.weight(.semibold))
                .textCase(.uppercase)
                .foregroundStyle(.secondary)

            if universe.isEmpty {
                Text("This board has no labels yet.")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            } else {
                // A scroll view rather than a bare stack: the universe has no bound, and a popover
                // that grows past the screen is a popover that cannot be dismissed by its own bottom
                // edge. The height is a cap, not a size — a board with three labels draws three rows.
                ScrollView(.vertical) {
                    VStack(alignment: .leading, spacing: 2) {
                        ForEach(universe, id: \.self) { name in
                            Toggle(name, isOn: binding(for: name, onCard: cardID, current: current))
                                .toggleStyle(.checkbox)
                                .lineLimit(1)
                                .truncationMode(.middle)
                        }
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
                .frame(maxHeight: 220)
            }

            Divider()

            HStack(spacing: 6) {
                TextField("New label", text: $draft)
                    .textFieldStyle(.roundedBorder)
                    .focused($fieldFocused)
                    .onSubmit { create(onCard: cardID) }
                    .accessibilityLabel("New Label")
                Button("Add") { create(onCard: cardID) }
                    .disabled(CardLabels.normalized(draft) == nil)
            }
            .disabled(!store.acceptsBoardMutations)
        }
        .padding(12)
        .frame(width: 240)
        .onAppear { fieldFocused = true }
    }

    /// One row's checkmark: reads the card's current list, writes through the one funnel.
    ///
    /// `current` is passed in rather than re-read per row — one snapshot lookup for the whole dialog,
    /// so twenty rows cost one walk instead of twenty.
    private func binding(for name: String, onCard cardID: ItemID, current: [String]) -> Binding<Bool> {
        Binding(
            get: { CardLabels.contains(name, in: current) },
            set: { _ in LabelCommand.toggle(name, onCard: cardID, in: store, recents: recents) }
        )
    }

    /// The create field's landing. It **applies** rather than toggles — the user typed a name into a
    /// box labelled "New label", which is a request to put it on the card, never to take it off
    /// (`LabelCommand.add`, which also resolves the board's own spelling for a name that merely looks
    /// new).
    ///
    /// The field clears and keeps focus: adding two labels in a row is the common case, and the
    /// popover's own dismissal is click-away or Escape.
    private func create(onCard cardID: ItemID) {
        guard store.acceptsBoardMutations, CardLabels.normalized(draft) != nil else { return }
        LabelCommand.add(draft, onCard: cardID, in: store, recents: recents)
        draft = ""
        fieldFocused = true
    }
}
