import AppKit

/// **The shared Colors panel, as one takeover every surface can borrow** — `NSColorPanel.shared`
/// seeded, targeted, and translated back into the app's stored-value vocabulary.
///
/// This used to be `ColorComboView.Coordinator`'s panel handling, lifted out of it when a second
/// surface needed the identical takeover: `NSColorPanel` is a **singleton with a settable target and
/// no getter for it**, so "is the panel still mine to detach from" has nowhere to live but in a
/// static, and a second surface taking the panel over has to leave the first one's teardown harmless.
/// `ColorComboView` is retired now (2026-08-10, `ColorSwatchPicker.swift`'s own header), and every
/// caller left — the Style… popover's Background section, the symbol popover's tint section, and the
/// card sidebar's own colour rectangle — reaches this class through the one shared instance
/// `SharedColorPanelSession` holds, below.
///
/// ### What it hands back
///
/// Not an `NSColor` — the **stored string**: the palette name when the picked colour lands exactly
/// on one of the caller's own palette entries, `#RRGGBB[AA]` otherwise (`changeColor(_:)` below). That
/// is what makes a colour survive a round trip through the panel without drifting from
/// `light-cayenne` into an anonymous `#B6071E`.
///
/// ### It fires continuously
///
/// `NSColorPanel`'s action runs on every tick of a drag, not on release, and that is deliberate: a
/// live preview is the point of opening the panel at all. **Every caller therefore owes a debounce**
/// before the value reaches disk — `SharedColorPanelSession.present`'s own ~400ms trailing debounce is
/// that owed debounce for every caller today, and a drag that passes through a palette-exact colour
/// mid-gesture must not be recorded in `StyleRecents` the way a deliberate pick is.
@MainActor
final class SystemColorPanel: NSObject {

    /// Whichever instance most recently took the shared panel over. `weak`, so an owner that never
    /// got around to detaching — a window closed from under it — does not keep the next one from
    /// being collected either.
    private static weak var currentOwner: SystemColorPanel?

    private var onChange: ((String) -> Void)?
    /// The palette a picked colour is matched against before it is written — the caller's own role
    /// table, so a background pick checks the backgrounds and a tint pick checks the tints.
    private var palette: [PaletteColor] = []

    /// Seeds the panel with `seed` (black when the field carries no resolvable colour), takes it
    /// over, and asks for continuous updates.
    ///
    /// Opening it again from the same owner re-seeds rather than stacking — the panel is one window
    /// and this is how a second click on the same face behaves.
    func present(
        seed: NSColor?,
        matching palette: [PaletteColor],
        showsAlpha: Bool = true,
        onChange: @escaping (String) -> Void
    ) {
        self.onChange = onChange
        self.palette = palette
        let panel = NSColorPanel.shared
        panel.showsAlpha = showsAlpha
        panel.color = seed ?? .black
        panel.setTarget(self)
        panel.setAction(#selector(changeColor(_:)))
        Self.currentOwner = self
        panel.makeKeyAndOrderFront(nil)
    }

    /// Releases the panel **only if this instance still holds it** — last-writer-wins, so a view
    /// torn down after another surface took the panel over does not silently unhook that surface.
    func detach() {
        guard Self.currentOwner === self else { return }
        let panel = NSColorPanel.shared
        panel.setTarget(nil)
        panel.setAction(nil)
        Self.currentOwner = nil
        onChange = nil
    }

    /// The panel's own action. A colour no sRGB conversion can express is dropped rather than
    /// guessed at — `paletteHexString`'s `nil`, which no picker swatch actually produces.
    @objc private func changeColor(_ sender: NSColorPanel) {
        guard let hex = sender.color.paletteHexString else { return }
        onChange?(Palette.name(forHex: hex, in: palette) ?? hex)
    }
}

// MARK: - The session a transient surface borrows

/// **The Colors panel for surfaces that do not outlive it, debounce included.**
///
/// The obvious shape is a `SystemColorPanel` in the opening view's own state — and it does **not**
/// work for a surface whose door onto the panel sits *inside a transient popover*: the Style…
/// popover's Background section, the symbol popover's tint section, and, since 2026-08-10, the card
/// sidebar's own colour rectangle's popover (`ColorSwatchPicker`'s **Other…** row) are all like this.
/// Opening the Colors panel takes key status away, the popover dismisses, the view's state goes with
/// it — and the panel the user is now dragging in has nothing left to talk to. The gesture would
/// break itself. (`CardStyleSection` used to be the one exception — a card window's sidebar outlives
/// any panel opened from it, so it held its own `SystemColorPanel` and its own debounce `Task`
/// directly. It reaches this shared session too now that its own colour control opens the panel from
/// *inside* a popover rather than from a permanently-mounted face.)
///
/// So the session lives above every view: one panel, one pending write, replaced by whichever
/// surface opened it last. Callers capture their store **weakly**, so a board closed while its
/// Colors panel is still up drops the write instead of resurrecting a dead store.
///
/// The debounce lives here because the state it needs has to live somewhere a dismissed popover
/// cannot take with it: the panel's action is continuous, and only the value the user is still on
/// ~400ms after the last tick reaches disk. A drag is therefore one write, and every caller keeps
/// that one settled value **out of** `StyleRecents` — that row remembers deliberate palette picks,
/// not colours a drag swept through, so every `write` closure above writes raw rather than through
/// `StyleCommand.apply`.
@MainActor
enum SharedColorPanelSession {

    private static let panel = SystemColorPanel()
    private static var commit: Task<Void, Never>?

    static func present(
        seed: NSColor?,
        matching palette: [PaletteColor],
        write: @escaping (String) -> Void
    ) {
        panel.present(seed: seed, matching: palette) { value in
            commit?.cancel()
            commit = Task { @MainActor in
                try? await Task.sleep(for: .milliseconds(400))
                guard !Task.isCancelled else { return }
                write(value)
            }
        }
    }
}
