import SwiftUI
import AppKit
import ObjectiveC

/// Suppresses `NSSplitView`'s divider resize-cursor rects on flagged instances, so hovering the
/// sidebar edge shows the normal arrow rather than the "move divider" cursor. Our sidebars are a
/// fixed width (see `SidebarColumnController`), so the resize affordance is misleading — the
/// divider can't move.
///
/// This must NOT be done by reclassing the live split view (`object_setClass` to a subclass with
/// an empty `resetCursorRects`): AppKit's window sidebar tracking KVO-observes the split view
/// (`NSThemeFrame` observes `splitView._peeking` via `_NSSplitViewPartitionAdapter`), and KVO
/// works by isa-swizzling. Stomping the `NSKVONotifying_` class corrupts the observation
/// bookkeeping, and the deferred `removeObserver:forKeyPath:` during `-[NSWindow dealloc]` then
/// throws mid-dealloc and aborts the app — seconds after the window closed, when the autorelease
/// pool finally drained. Instead, `-[NSSplitView resetCursorRects]` is replaced once, class-wide,
/// with a version that no-ops only for instances carrying our associated-object flag. The object's
/// class chain is never touched, so KVO is safe in either registration order.
@MainActor
private enum FixedDividerCursorSuppressor {
    private nonisolated(unsafe) static var flagKey: UInt8 = 0

    private static let installSwizzle: Void = {
        let selector = #selector(NSView.resetCursorRects)
        guard let method = class_getInstanceMethod(NSSplitView.self, selector) else { return }
        typealias ResetCursorRects = @convention(c) (NSSplitView, Selector) -> Void
        let original = unsafeBitCast(method_getImplementation(method), to: ResetCursorRects.self)
        let replacement: @convention(block) (NSSplitView) -> Void = { splitView in
            guard objc_getAssociatedObject(splitView, &flagKey) == nil else { return }
            original(splitView, selector)
        }
        method_setImplementation(method, imp_implementationWithBlock(replacement))
    }()

    static func suppressDividerCursor(of splitView: NSSplitView) {
        _ = installSwizzle
        guard objc_getAssociatedObject(splitView, &flagKey) == nil else { return }
        objc_setAssociatedObject(splitView, &flagKey, true, .OBJC_ASSOCIATION_RETAIN)
        splitView.window?.invalidateCursorRects(for: splitView)
    }
}

/// Owns the AppKit sidebar split-view item behind `NavigationSplitView` so the
/// column only opens or closes on an explicit toggle — never on its own when the
/// window resizes.
///
/// On macOS, `NavigationSplitView` auto-collapses its sidebar whenever the window
/// narrows past the point where the sidebar and detail minimum widths both fit,
/// then re-expands it on widening. With our wide detail (chat / project views)
/// that threshold sits near the 900pt window minimum, so ordinary resizes flip
/// the column open and shut. Setting `canCollapse = false` removes AppKit's
/// automatic (and divider-drag) collapsing; we still collapse/expand on demand by
/// driving `isCollapsed` directly, which is honoured regardless of `canCollapse`.
@MainActor
final class SidebarColumnController {
    /// Fixed sidebar width. Pinned as both the AppKit item's minimum and maximum thickness so
    /// the divider can't be dragged to resize the column.
    static let defaultFixedWidth: CGFloat = 306

    /// The width for this particular split view. Settings uses a narrower column than the main
    /// application sidebar while retaining the same non-resizable behavior.
    let fixedWidth: CGFloat

    private weak var item: NSSplitViewItem?
    private var resizeObserver: NSObjectProtocol?

    init(fixedWidth: CGFloat = SidebarColumnController.defaultFixedWidth) {
        self.fixedWidth = fixedWidth
    }

    /// Adopt the sidebar item once SwiftUI has built the split view, and pin it
    /// open against window-driven collapsing. Re-asserted on every window resize
    /// in case SwiftUI re-enables collapsing during a layout pass.
    func adopt(_ item: NSSplitViewItem, in window: NSWindow?) {
        self.item = item
        lockCollapse()
        guard resizeObserver == nil, let window else { return }
        resizeObserver = NotificationCenter.default.addObserver(
            forName: NSWindow.didResizeNotification, object: window, queue: .main
        ) { [weak self] _ in
            MainActor.assumeIsolated { self?.lockCollapse() }
        }
    }

    private func lockCollapse() {
        guard let item else { return }
        // Remove AppKit's automatic/divider collapsing…
        if item.canCollapse { item.canCollapse = false }
        // …and undo any collapse that already slipped through (e.g. a layout pass that
        // narrowed the window before we pinned the item), so the column can never be stuck shut.
        if item.isCollapsed { item.isCollapsed = false }
        // Pin the column to a single width. Equal minimum/maximum thickness makes AppKit refuse
        // to move the divider, so the sidebar can't be resized by dragging. A high holding
        // priority keeps the fixed width even as the window (and detail pane) resize.
        if item.minimumThickness != fixedWidth { item.minimumThickness = fixedWidth }
        if item.maximumThickness != fixedWidth { item.maximumThickness = fixedWidth }
        item.holdingPriority = .defaultHigh + 1
    }

    var hasItem: Bool { item != nil }

    /// Collapse or expand the sidebar in response to the toolbar button. Driving
    /// `isCollapsed` directly works even though `canCollapse` is false — that flag
    /// only governs automatic and user-divider collapsing, not programmatic intent.
    func toggle() {
        guard let item else { return }
        item.animator().isCollapsed.toggle()
    }
}

/// Locates the `NSSplitView` backing the enclosing `NavigationSplitView` and hands
/// its sidebar item to `controller`. Rendered invisibly in the sidebar background.
struct SidebarColumnConfigurator: NSViewRepresentable {
    let controller: SidebarColumnController

    func makeNSView(context: Context) -> NSView {
        let view = NSView(frame: .zero)
        Task { @MainActor in configure(from: view) }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
        Task { @MainActor in configure(from: nsView) }
    }

    @MainActor
    private func configure(from view: NSView) {
        guard let splitView = enclosingSplitView(of: view),
              let splitController = splitView.delegate as? NSSplitViewController,
              let sidebar = splitController.splitViewItems.first
        else { return }
        FixedDividerCursorSuppressor.suppressDividerCursor(of: splitView)
        controller.adopt(sidebar, in: view.window)
    }

    private func enclosingSplitView(of view: NSView) -> NSSplitView? {
        var current: NSView? = view.superview
        while let candidate = current {
            if let split = candidate as? NSSplitView { return split }
            current = candidate.superview
        }
        return nil
    }
}
