import AppKit
import SwiftUI

/// **The user-draggable divider between the body column and the comments pane**, in the beside mount
/// only (05-card-window.md ▸ The comments column, extended 2026-08-09: "the divider … is
/// user-resizable, with sensible min/max clamps").
///
/// A real `HStack` element rather than an overlay hung off a neighbour — `LaneResizeHandle`'s own
/// alternative, and the wrong one here: that handle overlaps *into* the inter-lane gap it already
/// has a right to because it is drawn as part of the lane it resizes, and the lane strip is drawn
/// after it in one continuous row. This divider sits between two independent panes with zero spacing
/// between them (`HStack(spacing: 0)`), so an overlay wide enough to grab would paint under one
/// neighbour and over the other depending on which sibling SwiftUI happens to draw second — a
/// z-order bug waiting to be found by a user's cursor. Giving the divider its own modest width
/// instead (`CardWindowMetrics.commentsDividerHitWidth`) is the same interactive width with none of
/// that hazard: `Divider()` draws its hairline centered in the wider box, and the box itself is what
/// both hovers and drags.
///
/// **Pure arithmetic, impure host**: what a drag computes is `CardWindowMetrics
/// .clampedCommentsColumnWidth`, called fresh on every tick; what this view owns is only the touch
/// bookkeeping the arithmetic needs and cannot itself hold — the drag's anchor width, the last tick's
/// clamped result (so release has something to commit), and the cursor push/pop balance
/// (`LaneResizeHandle`'s own idiom, `NSCursor.resizeLeftRight`).
struct CommentsColumnDivider: View {

    /// The body font's point size — every measurement here derives from it, same as the rest of the
    /// window.
    let bodyPointSize: CGFloat

    /// The beside `HStack`'s own width this render — the window's content area minus the fixed
    /// sidebar, and the figure the drag's upper clamp is measured against
    /// (`CardWindowMetrics.clampedCommentsColumnWidth`).
    let containerWidth: CGFloat

    /// The comments column's width **as it is drawn right now**, already clamped. Read once, at the
    /// first tick of a new drag, as the anchor translation is measured from — not a `@State` default,
    /// because the true current width lives one level up (`CardWindowView.commentsColumnWidth`, live
    /// mid-drag and persisted otherwise) and a stale copy captured at this view's own init would drift
    /// the moment a sibling window's drag changed the app-wide figure out from under it.
    let currentWidth: CGFloat

    /// Reports a new clamped width on every drag tick — the caller's job is only to hold it as a live
    /// override for this render (`CardWindowView.commentsColumnWidthOverride`), never to persist it:
    /// writing `UserDefaults` on every pixel of a drag would be `LaneResizeSession`'s mistake to make,
    /// and it already made a different choice — track live, write once.
    let onChange: (CGFloat) -> Void

    /// Fires exactly once per gesture, at release, with the width the drag ended on — the one write
    /// that reaches `storedCommentsColumnWidth`, plain callback for `onChange`'s reason: the source of
    /// truth is app-wide state this view does not own.
    let onCommit: (CGFloat) -> Void

    /// The drag's anchor — the column's width when the current gesture began, `nil` between drags.
    /// Captured once per gesture rather than measured incrementally, so a fast flick that fires
    /// several `.onChanged` events before this view re-renders still resolves against one fixed
    /// starting point instead of compounding rounding from tick to tick.
    @State private var anchor: CGFloat?

    /// The most recent clamped width this gesture computed — read back at `.onEnded` and handed to
    /// `onCommit`, since the gesture's own `DragGesture.Value` at release carries the final
    /// *translation*, not the clamped width that translation produced.
    @State private var liveWidth: CGFloat?

    /// Guards the cursor push/pop balance — `LaneResizeHandle`'s own reason: a fast cursor can leave
    /// the strip mid-drag, so the two calls must be idempotent to never leave a stuck resize cursor.
    @State private var cursorPushed = false

    private var hitWidth: CGFloat { CardWindowMetrics.commentsDividerHitWidth(bodyPointSize: bodyPointSize) }

    var body: some View {
        Divider()
            .frame(width: hitWidth)
            .contentShape(Rectangle())
            .onHover { inside in
                if inside { pushCursor() } else { popCursor() }
            }
            // `.global`, not `.local`: dragging the divider moves the divider itself (the body
            // column shrinks or grows underneath it), so a `.local` translation would be measured
            // against a coordinate space that is sliding out from under the gesture — the same
            // reasoning `LaneResizeHandle` states for its own strip.
            .gesture(
                DragGesture(minimumDistance: 2, coordinateSpace: .global)
                    .onChanged { value in
                        if anchor == nil {
                            pushCursor()
                            anchor = currentWidth
                        }
                        // Dragging left (negative translation) widens the comments pane — the
                        // divider is trailing the body, so ceding ground to the left is the pane
                        // gaining it.
                        let proposed = (anchor ?? currentWidth) - value.translation.width
                        let clamped = CardWindowMetrics.clampedCommentsColumnWidth(
                            proposed,
                            bodyPointSize: bodyPointSize,
                            containerWidth: containerWidth
                        )
                        liveWidth = clamped
                        onChange(clamped)
                    }
                    .onEnded { _ in
                        if let liveWidth { onCommit(liveWidth) }
                        anchor = nil
                        liveWidth = nil
                        popCursor()
                    }
            )
            // Pointer-only, `LaneResizeHandle`'s own carve-out: the accessible path to the same
            // outcome is **View ▸ Comments Beside Body** unchecked, which drops the divider
            // entirely rather than asking a keyboard to drive a continuous width.
            .accessibilityHidden(true)
    }

    private func pushCursor() {
        guard !cursorPushed else { return }
        NSCursor.resizeLeftRight.push()
        cursorPushed = true
    }

    private func popCursor() {
        guard cursorPushed else { return }
        NSCursor.pop()
        cursorPushed = false
    }
}
