import AppKit
import SwiftUI

// MARK: - Appearance

/// The sidebar's **Appearance** section (renamed from **Style**, owner's ruling 2026-08-10, Pipeline
/// card 5004c540 — the sidebar-only rename; the board context menu's **Style…** item and
/// `StyleEditorView`'s own naming are unaffected, per that ruling's own scope): the **`SymbolPicker`**
/// rectangle beside the **`ColorSwatchPicker`** rectangle, side by side — Symbol left, Background
/// right, each captioned *above* rather than leading (05-card-window.md ▸ Style; 03-board-ui.md ▸
/// Styling ▸ Controls, its 2026-08-06 anchor-ownership rule for the background half, extended to the
/// symbol half on 2026-08-08). Card styling is discoverable here without a context menu — both
/// rectangles are the same reusable controls the board popover's glyph picker beside the rename field
/// already uses (`BoardInfoPopover`) and the Style… popover's own grid reuses (`ColorSwatchPicker`'s
/// own header).
///
/// ### Why the curated grid left (history, unaffected by this rename)
///
/// `StyleEditorView`'s curated grid used to draw **whole, uncollapsed** in this column
/// (`StyleEditorLayout.sidebar`'s `symbolGridMaximumHeight: nil` — "the sidebar is already a scroll
/// view, and a scroll view inside a scroll view is a scroll view that fights") — a five-to-seven-row
/// grid, permanently open, ahead of every other section the sidebar carries. `SymbolPicker` is the
/// general-purpose control built for precisely this shape of problem (its own header: "a control any
/// caller can aim at one symbol field without wiring up a `BoardStore`, a `StyleTarget`, or the
/// two-dimension batch machinery `StyleEditorView` carries") — one small rectangle at rest, the grid
/// only while its popover is open. This section is the first caller to aim it at a store-backed field
/// rather than a plain binding, which is why it does the store plumbing below that `SymbolPicker`
/// itself deliberately knows nothing about.
///
/// **`StyleEditorView` is no longer instantiated here at all.** It was already `showsBackground:
/// false` — the background rectangle has owned that half since 2026-08-06 (as `ColorComboView`, now
/// `ColorSwatchPicker`) — so the curated symbol grid was its only remaining job in this section, and
/// that job moved out to `SymbolPicker`. The shared component's `.sidebar` layout variant and
/// `showsBackground` toggle stay in `StyleEditor.swift` regardless: they are still correct and still
/// tested (`StyleEditorLayoutTests`), and that file's own stance is to be anchor-agnostic about
/// callers it does not currently have.
///
/// ### The curated set does not shrink
///
/// `SymbolPicker`'s own default (`SymbolPickerCatalog.defaultSet`, 36 general "boards and projects"
/// glyphs) is deliberately smaller than `CuratedSymbols.cards` (~40, work-item/content-flavored — one
/// of the three level-specific sets `CuratedSymbols` split into, 2026-08-09) — `SymbolPicker.swift`'s
/// own doc comment states why the lists are free to diverge. A card is exactly the level
/// `CuratedSymbols.cards` is grouped for, so this section passes it explicitly (`symbols:
/// CuratedSymbols.availableCards`) rather than taking the picker's domain-agnostic default: the
/// Style… popover's card-level grid and this section now offer the identical curated vocabulary,
/// through two different controls.
///
/// ### Writes still ride the one funnel, on this window's own stack
///
/// `onSelect`/`onSelectColor`/`onChange` below call `StyleCommand.apply(icon:iconColor:to:in:store:
/// recents:on:)` — the same funnel `StyleEditorView`'s wells rode, `on: undo` passed through exactly
/// as before (13-native-undo.md ▸ Rules ▸ two levels: a colour or symbol chosen here is a gesture
/// *issued in this window*, so its step joins the window's session and reaches board history only
/// inside the coarse close step). Nothing about `BoardStore.applyStyle`'s batch semantics, its no-op
/// skipping, or its `HistoryAnchor` choice moved; only the views that call it did —
/// `CardSessionUndoTests.sidebarStylingIsASessionGesture` calls that same funnel directly and needed
/// no change.
///
/// **The background rectangle's Colors-panel door no longer needs this section's own debounce.**
/// `ColorComboView`'s old panel handoff was a per-view-owned `SystemColorPanel`, undebounced at the
/// source, so this section carried its own trailing-debounce `Task` (`backgroundPanelCommit`) around
/// it. `ColorSwatchPicker`'s **Other…** row opens `SharedColorPanelSession` instead — the same
/// pre-debounced (~400ms) shared session the Style… popover's own background section and the symbol
/// popover's tint row already use — so the settled value arrives already debounced, and this section
/// only has to write it (`applyPanelBackground(_:)`), raw, never through the recents funnel, exactly
/// as `StyleEditorView.openBackgroundPanel` writes its own.
///
/// ### The target is fixed, and that is the whole difference in behavior
///
/// The Style… popover *tracks*: its target is the selection at the moment the gesture named it,
/// re-resolved against every snapshot, dismissing when it empties (`StyleEditorSession`). This
/// section tracks nothing. Its target is this window's card, always — "the two embedded anchors need
/// none of this and get none: the card sidebar dismisses with its card's window, and the board
/// popover's target is the board itself" (`StyleEditorSession`'s own note). `SymbolPicker`'s and
/// `ColorSwatchPicker`'s own popover presentations are plain view-local `@State`, needing no session
/// for the same reason: the window's card cannot vanish out from under its own window, and when it
/// does the window goes with it (`CardWindowFate`).
struct CardStyleSection: View {

    let store: BoardStore
    let recents: StyleRecents
    let cardID: ItemID
    /// **This window's undo stack** (13-native-undo.md ▸ Rules ▸ two levels): a colour or symbol
    /// chosen here is a gesture *issued in this window*, so its step joins the window's session and
    /// reaches board history only inside the coarse close step. The background rectangle below and
    /// the symbol rectangle both take it as a parameter, for the same reason.
    let undo: CardWindowUndo

    /// The live body metric, read here rather than passed in — `CardAttachmentsSection`'s pattern,
    /// so every section in this sidebar derives its geometry the same way.
    private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }

    /// **What this section styles: this card, and nothing else.**
    ///
    /// A one-line seam because it is a claim worth pinning rather than a computation worth reading:
    /// every other anchor derives its target from something that moves (the selection, the board),
    /// and the mistake this window could make is inheriting one of those. `.items` of exactly one id
    /// also means `applyStyle`'s own batch machinery is a no-op here — one subject, so the display is
    /// never mixed and the write bracket holds one file.
    nonisolated static func target(forCard id: ItemID) -> StyleTarget {
        .items([id])
    }

    var body: some View {
        VStack(alignment: .leading, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
            CardSidebarSectionHeader(title: "Appearance")
            // Side by side, Symbol left of Background — the owner's sketch (2026-08-10 ruling):
            // "Symbol     Background" over the two rectangles. `.top` alignment keeps the two
            // captions, and the two rectangles below them, on one shared baseline even though
            // neither column reserves a fixed width for the other.
            HStack(alignment: .top, spacing: CardWindowMetrics.sidebarRowSpacing(bodyPointSize: pointSize)) {
                symbolColumn
                backgroundColumn
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    // MARK: - Symbol

    /// The **Symbol** column: its caption *above* the rectangle rather than leading it — the owner's
    /// sketch, and `CardDetailsSection.row`'s own caption-over-value shape (`CardDetailsSection.
    /// swift`) applied here instead of `CardSidebarSectionHeader`'s title-and-divider shape, which is
    /// a section-level header rather than a per-control caption. `4` is `CardSidebarSectionHeader`'s
    /// own literal spacing between its title and its divider, restated for the same small gap here.
    private var symbolColumn: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Symbol")
                .font(.caption)
                .foregroundStyle(.secondary)
            SymbolPicker(
                current: currentSymbol,
                fallback: ItemSymbol.card,
                symbols: CuratedSymbols.availableCards,
                onSelect: { applySymbol($0) },
                currentColor: currentIconColor,
                onSelectColor: { applyIconColor($0) }
            )
            // The same lock `StyleEditorView`'s whole body disabled under
            // (`.disabled(!store.acceptsBoardMutations)`, `StyleEditor.swift`) — the read-only lock
            // and the board's inline-editing rule alike, unchanged across the layout reshape and the
            // control's own two-zone-to-one-zone change.
            .disabled(!store.acceptsBoardMutations)
        }
    }

    /// The card's `icon` field, exactly as written — `currentBackground`'s rule, restated for the
    /// field `SymbolPicker.current` reads. Resolution (an unresolvable name falling back to the level
    /// default) is `SymbolPicker`'s own job, not this property's — `ItemSymbol.name(_:fallback:)`'s
    /// lenient rule, which the picker's `resolvedName` already applies.
    private var currentSymbol: String? {
        StyleFieldState.written(currentSubject?.icon ?? .missing)
    }

    /// The card's `iconColor` field, exactly as written — one dimension over from `currentSymbol`,
    /// feeding `SymbolPicker.currentColor`.
    private var currentIconColor: String? {
        StyleFieldState.written(currentSubject?.iconColor ?? .missing)
    }

    /// `nil` clears back to the card default (`icon` removed); a name sets it — `StyleChange`'s
    /// `set`/`remove` split, `SymbolPicker.onSelect`'s own contract turned into the vocabulary
    /// `StyleCommand.apply` speaks. Routed through the funnel rather than `store.applyStyle` directly
    /// so a symbol pick joins `StyleRecents` exactly as a background pick does — moot today (only
    /// `.set(background:)` records, `StyleCommand.apply`'s own rule), but future-proof against that
    /// rule ever widening to symbols.
    private func applySymbol(_ name: String?) {
        StyleCommand.apply(
            icon: name.map { StyleChange.set($0) } ?? .remove,
            to: Self.target(forCard: cardID),
            in: store,
            recents: recents,
            on: undo
        )
    }

    /// `nil` clears the tint (`iconColor` removed); a palette name sets it — `applySymbol`'s sibling,
    /// one dimension over, exactly as `BoardInfoPopover`'s own `onSelectColor` writes the board's.
    private func applyIconColor(_ name: String?) {
        StyleCommand.apply(
            iconColor: name.map { StyleChange.set($0) } ?? .remove,
            to: Self.target(forCard: cardID),
            in: store,
            recents: recents,
            on: undo
        )
    }

    // MARK: - Background

    /// The **Background** column, `symbolColumn`'s own caption-above shape mirrored — both
    /// rectangles read their geometry off the identical `PickerRectMetrics`
    /// (`PickerRect.swift`), so the two columns line up at the same height without either one
    /// naming a size of its own.
    private var backgroundColumn: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Background")
                .font(.caption)
                .foregroundStyle(.secondary)
            ColorSwatchPicker(
                role: .background,
                value: currentBackground,
                isEnabled: !store.isReadOnly,
                onChange: { commitBackground($0) },
                onPanelChange: { applyPanelBackground($0) }
            )
        }
    }

    /// The card's `background` field, exactly as written — malformed reads as its raw text, missing
    /// reads `nil`, both `StyleFieldState.written`'s own rule (`StyleModel.swift`). The **raw**
    /// string, never a resolved colour: the grid's selection matching needs the bytes, not what they
    /// render as (`ColorSwatchPicker`'s own header on why that matching is now literal-name only).
    private var currentBackground: String? {
        StyleFieldState.written(currentSubject?.background ?? .missing)
    }

    /// A discrete pick — commits immediately. `nil` removes; a name from `Palette.backgrounds` goes
    /// through `StyleCommand.apply` so it feeds `StyleRecents` exactly like a well click would
    /// ("updated on every background application from any anchor", `StyleEditor.swift`); anything
    /// else writes directly, since it is not the "palette pick" recents was ever meant to remember.
    private func commitBackground(_ newValue: String?) {
        let target = Self.target(forCard: cardID)
        guard let newValue else {
            store.applyStyle(to: target, background: .remove, icon: .keep, on: undo)
            return
        }
        if Palette.backgrounds.contains(where: { $0.name == newValue }) {
            StyleCommand.apply(background: .set(newValue), to: target, in: store, recents: recents, on: undo)
        } else {
            store.applyStyle(to: target, background: .set(newValue), icon: .keep, on: undo)
        }
    }

    /// The Colors panel's settled value, from `ColorSwatchPicker`'s **Other…** door
    /// (`SharedColorPanelSession`, already debounced ~400ms at the source — this section carries no
    /// debounce of its own any more, `CardStyleSection`'s own header). Written raw, **never** through
    /// `StyleCommand.apply` — a panel pick must not enter `StyleRecents`, the same rule
    /// `StyleEditorView.openBackgroundPanel` applies to its own **Other…** row.
    private func applyPanelBackground(_ newValue: String?) {
        let target = Self.target(forCard: cardID)
        if let newValue {
            store.applyStyle(to: target, background: .set(newValue), icon: .keep, on: undo)
        } else {
            store.applyStyle(to: target, background: .remove, icon: .keep, on: undo)
        }
    }

    /// One `styleSubjects` lookup behind `currentBackground`, `currentSymbol` and `currentIconColor`
    /// above — a card window names exactly one target, so there is exactly one subject to read for
    /// all three fields.
    private var currentSubject: BoardStore.StyleSubject? {
        store.styleSubjects(of: Self.target(forCard: cardID)).first
    }
}
