import SwiftUI
import AppKit

/// Works around a macOS 26 Liquid Glass quirk: the window's unified-toolbar material
/// stops compositing after the window is occluded (covered by another window) or the
/// app goes idle, leaving the toolbar a flat fill — showing the BuildBanner color flat
/// beneath it — until a manual window resize forces it to re-render. `.toolbarBackground`
/// guarantees the toolbar *has* a material, but can't keep that material from going
/// dormant.
///
/// We can't observe the dropout directly, but it's reliably cured by a relayout (the
/// resize the user reaches for). So on the events that trigger it — the window being
/// revealed, the app reactivating — we reproduce that relayout by nudging the window
/// frame a pixel and immediately back. The two `setFrame` calls coalesce within one
/// runloop pass, so there's no visible jump, but the toolbar re-composites its glass.
///
/// The nudge is debounced until the window is geometrically stable. That matters
/// because the non-animated `setFrame` would otherwise cancel a running zoom (or
/// drag-resize) and snap the window back to its pre-zoom size — a visible stutter
/// followed by a revert. A real resize is itself the relayout we're faking, so nudging
/// during one is redundant anyway.
///
/// We can't trust resize *notifications* to tell us when a zoom is in flight — the
/// animation of a double-click zoom doesn't reliably post `didResizeNotification` for
/// every intermediate frame, so a notification-only debounce can still fire mid-zoom.
/// Instead we watch the window's frame *value*: an in-flight zoom is, by definition, a
/// frame that's still moving. Before nudging we require two consecutive samples (one
/// debounce interval apart) to be identical; while the frame keeps changing we keep
/// deferring. This catches any animation regardless of whether it emits notifications.
struct ToolbarGlassKeeper: NSViewRepresentable {
    func makeNSView(context: Context) -> NSView {
        let view = NSView(frame: .zero)
        let coordinator = context.coordinator
        // The host view isn't in a window yet during `makeNSView`; the MainActor hop
        // lets `view.window` resolve first (same pattern as `SidebarColumnConfigurator`).
        Task { @MainActor in coordinator.attach(to: view.window) }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
        let coordinator = context.coordinator
        Task { @MainActor in coordinator.attach(to: nsView.window) }
    }

    func makeCoordinator() -> Coordinator { Coordinator() }

    @MainActor
    final class Coordinator {
        private weak var window: NSWindow?
        private var observers: [NSObjectProtocol] = []
        /// A reveal/reactivation asked for a nudge; cleared once it's applied. Kept as a
        /// flag (not applied inline) so a concurrent resize can defer it — see below.
        private var repairPending = false
        /// The debounced apply, cancelled/rescheduled while frames are still in flight.
        private var settleWorkItem: DispatchWorkItem?
        /// The window frame seen at the previous settle. The nudge only fires once two
        /// consecutive samples match — i.e. the frame has stopped moving — so an
        /// animating zoom (whose frame is still changing) keeps deferring instead of
        /// being cancelled by our `setFrame`.
        private var lastSampledFrame: CGRect?

        /// Idempotent: registers once for the window's reveal / app-reactivation events.
        func attach(to window: NSWindow?) {
            guard observers.isEmpty, let window else { return }
            self.window = window
            let center = NotificationCenter.default
            observers = [
                // Fully-occluded → visible again: the material commonly drops while
                // hidden and doesn't resume on reveal.
                center.addObserver(
                    forName: NSWindow.didChangeOcclusionStateNotification,
                    object: window, queue: .main
                ) { [weak self] _ in MainActor.assumeIsolated { self?.requestRepair() } },
                // Reactivating the app after it sat in the background is the other
                // common trigger.
                center.addObserver(
                    forName: NSApplication.didBecomeActiveNotification,
                    object: nil, queue: .main
                ) { [weak self] _ in MainActor.assumeIsolated { self?.requestRepair() } },
                // A resize — a drag-resize or a step of a double-click *zoom* — is itself
                // the relayout that cures the glass, so the nudge is redundant during one
                // and, worse, its non-animated `setFrame` would cancel an in-flight zoom
                // and snap the window back to its pre-zoom size. When one of these
                // notifications does fire it pushes an already-pending nudge past the
                // resize; it never starts one. (The frame-stability check in
                // `applyRepairIfPending` is the real guard — a zoom animation doesn't
                // reliably post this for every frame.)
                center.addObserver(
                    forName: NSWindow.didResizeNotification,
                    object: window, queue: .main
                ) { [weak self] _ in MainActor.assumeIsolated { self?.deferPendingRepair() } },
            ]
        }

        /// Reveal / reactivation: mark a nudge wanted and (re)arm the debounce. Seed the
        /// frame baseline with the current frame so a still window nudges on the first
        /// settle, while one caught mid-animation defers until its frame stops moving.
        private func requestRepair() {
            repairPending = true
            lastSampledFrame = window?.frame
            scheduleSettle()
        }

        /// Resize step: only push an *already-pending* nudge past the resize so it lands
        /// once the window is stable — never fire mid-zoom.
        private func deferPendingRepair() {
            guard repairPending else { return }
            scheduleSettle()
        }

        private func scheduleSettle() {
            settleWorkItem?.cancel()
            let work = DispatchWorkItem { [weak self] in
                MainActor.assumeIsolated { self?.applyRepairIfPending() }
            }
            settleWorkItem = work
            // Also the interval between frame-stability samples: long enough that a zoom
            // animation visibly moves the frame between two settles (so it keeps
            // deferring), short enough that a still window nudges promptly.
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: work)
        }

        private func applyRepairIfPending() {
            guard repairPending else { return }
            guard let window, window.occlusionState.contains(.visible) else {
                repairPending = false
                lastSampledFrame = nil
                return
            }
            // Still resizing (a drag hasn't been let go) — wait for stability.
            if window.inLiveResize { scheduleSettle(); return }
            let frame = window.frame
            // A frame that changed since the last settle is still animating (a zoom in
            // flight). Touching it now would cancel the zoom and revert the window, so
            // re-sample and wait for two matching frames before nudging.
            if lastSampledFrame != frame {
                lastSampledFrame = frame
                scheduleSettle()
                return
            }
            repairPending = false
            lastSampledFrame = nil
            guard frame.height > 2 else { return }
            // Shrink by a pixel (anchored at the top edge — AppKit's origin is
            // bottom-left, so raise the origin as height drops) then restore. Shrinking
            // rather than growing avoids fighting the screen's bottom/top bounds.
            var nudged = frame
            nudged.size.height -= 1
            nudged.origin.y += 1
            window.setFrame(nudged, display: false)
            window.setFrame(frame, display: true)
        }
        // No deinit-time observer removal: this coordinator lives for the window's
        // session (same as `SidebarColumnController`'s resize observer), and the blocks
        // capture `self` weakly, so a stale registration is harmless.
    }
}
