import Foundation

/// **The one funnel every label gesture goes through** — the write plus the recents record, so no
/// anchor can do one without the other (`StyleCommand`'s shape and its reason exactly:
/// `StyleEditor.swift`).
///
/// Two anchors exist today and they are in different windows — the card window's sidebar section and
/// the board card menu's `labels` submenu, plus that submenu's More… dialog. Every one of them ends
/// up here, which is what keeps "the MRU is updated on every label apply" a fact rather than three
/// call sites' good intentions.
///
/// ### Recording the add half only
///
/// A **removal records nothing**. The MRU exists to answer "what is this user reaching for", and
/// taking `bug` off a card is evidence of the opposite — recording it would float a label to the top
/// of the very menu the user is trying to get away from. `StyleRecents`' own "the None well is not a
/// colour" carve-out, one field over (`LabelRecents`).
///
/// ### Spelling is the board's, not the typist's
///
/// A name typed into the sidebar's field or the dialog's create box is resolved against the board's
/// own universe first (`LabelIndex.canonicalSpelling(of:)`), so typing `BUG` onto a board that already
/// says `bug` tags the card `bug` rather than minting a second variant nothing can tell apart. Only a
/// genuinely new name keeps the typist's capitalisation — which is exactly right, because for a new
/// label the typist *is* the board.
@MainActor
enum LabelCommand {

    /// Adds or removes `name` on one card, whichever the card's current list calls for — the context
    /// menu's rows and the dialog's checkboxes.
    ///
    /// - Parameter undo: the issuing window's own stack, for the one anchor that has one — the card
    ///   window's sidebar (13-native-undo.md ▸ Rules ▸ two levels). `nil`, which every board-side
    ///   anchor passes, is the board's stack.
    /// - Returns: whether bytes reached disk.
    @discardableResult
    static func toggle(
        _ name: String,
        onCard cardID: ItemID,
        in store: BoardStore,
        recents: LabelRecents,
        on undo: CardWindowUndo? = nil
    ) -> Bool {
        let current = store.labels(ofCard: cardID)
        guard CardLabels.contains(name, in: current) else {
            return add(name, onCard: cardID, in: store, recents: recents, on: undo)
        }
        return store.setLabels(CardLabels.removing(name, from: current), onCard: cardID, on: undo)
    }

    /// Adds `name` to one card — the sidebar's add field and the dialog's create box, which both mean
    /// "put this on the card" rather than "flip whatever it is now".
    ///
    /// A name the card already carries is a no-op that **still records**: the user reached for it, and
    /// the MRU's whole subject is what they reach for.
    @discardableResult
    static func add(
        _ name: String,
        onCard cardID: ItemID,
        in store: BoardStore,
        recents: LabelRecents,
        on undo: CardWindowUndo? = nil
    ) -> Bool {
        guard let name = resolved(name, in: store) else { return false }
        recents.record(name)
        let current = store.labels(ofCard: cardID)
        return store.setLabels(CardLabels.adding(name, to: current), onCard: cardID, on: undo)
    }

    /// Removes `name` from one card — the chip's ⊗. Records nothing (see the type comment).
    @discardableResult
    static func remove(
        _ name: String,
        fromCard cardID: ItemID,
        in store: BoardStore,
        recents _: LabelRecents,
        on undo: CardWindowUndo? = nil
    ) -> Bool {
        let current = store.labels(ofCard: cardID)
        return store.setLabels(CardLabels.removing(name, from: current), onCard: cardID, on: undo)
    }

    /// A typed name in the board's own spelling — see the type comment. `nil` for a name that trims to
    /// nothing, which is not a label anybody meant.
    static func resolved(_ name: String, in store: BoardStore) -> String? {
        guard let name = CardLabels.normalized(name) else { return nil }
        return store.labelIndex.canonicalSpelling(of: name) ?? name
    }
}
