import SwiftUI

// Shared design pattern for the three swappable sidebar panes — Recents, Intelligence (AI),
// and Control — so they read and render identically. Each pane opens with a
// one-line summary header (an accent glyph, a terse status line, and an optional trailing
// control) over a divider, then a body of cards.
//
// Recents flows its session rows directly into the sidebar List's own scroll (so they keep
// their swipe actions and selection) and is hard-capped at eight rows in the data layer
// (`recentSummaries(limit:)`); it never scrolls on its own, since every session is also listed
// in the project tree just below. It therefore uses only `SidebarPaneHeader`.
//
// The queue panes are each a single List row built from `SidebarPane`: a header over a body
// that shrinks to fit its content and scrolls within an eight-card cap when there's more (their
// contents aren't shown anywhere else). The cap is eight session-card heights, matching Recents.

/// Carries the body's natural (unclipped) height up so `SidebarPane` can size to it.
private struct SidebarPaneBodyHeightKey: PreferenceKey {
    static let defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
    }
}

/// The summary header shared by every pane: accent glyph, status line, optional trailing
/// control, and a divider beneath. Recents uses this on its own; the queue panes get it via
/// `SidebarPane`.
struct SidebarPaneHeader<Trailing: View>: View {
    @Environment(\.appPalette) private var palette
    let icon: String
    let summary: String
    @ViewBuilder var trailing: () -> Trailing

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 8) {
                Image(systemName: icon)
                    .font(.caption).foregroundStyle(palette.accent)
                Text(summary)
                    .font(.caption).foregroundStyle(.secondary)
                    .lineLimit(2).fixedSize(horizontal: false, vertical: true)
                Spacer(minLength: 8)
                trailing()
            }
            // Align with the session rows' content inset (8) so the line sits over the rows,
            // not in a narrow stripe.
            .padding(.horizontal, 8).padding(.vertical, 10)
            Divider()
                // Match the 10pt above the divider (the header's bottom inset) so every pane
                // has the same padding on both sides of its divider. The bodies below add no
                // top inset of their own, so this is the single source of the divider gap.
                .padding(.bottom, 10)
        }
    }
}

extension SidebarPaneHeader where Trailing == EmptyView {
    init(icon: String, summary: String) {
        self.init(icon: icon, summary: summary, trailing: { EmptyView() })
    }
}

/// A queue pane (Intelligence, Control): the shared header over a body that **shrinks to fit**
/// its content and scrolls within `maxBodyHeight` (eight session-card heights, matching Recents)
/// once the content is taller. Idle/few-card panes stay compact; busy ones cap and scroll, with
/// a bottom fade.
///
/// Height is driven by an explicit `.frame(height:)` set from the body's measured natural height
/// — NOT the row's intrinsic size. That matters inside the macOS sidebar List, which caches a
/// row's intrinsic height and won't re-measure it (a plain self-sizing row froze at its
/// loading/low-row height — the "opens at one entry" bug). The List does follow an explicit
/// frame, so we measure the body inside the ScrollView (its true height, no feedback) and feed
/// that back. `measured` starts at 0 → the first frame is the full cap, so the pane can only
/// ever *shrink* to the real height, never open clipped to a single row.
struct SidebarPane<Trailing: View, Content: View>: View {
    let icon: String
    let summary: String
    /// Cap — eight session-card heights, matching the Recents section.
    let maxBodyHeight: CGFloat
    @ViewBuilder var trailing: () -> Trailing
    @ViewBuilder var content: () -> Content

    /// Measured natural height of the body; 0 until the first measurement (renders at full cap).
    @State private var measured: CGFloat = 0

    private var bodyHeight: CGFloat {
        measured > 0 ? min(measured, maxBodyHeight) : maxBodyHeight
    }
    private var scrollable: Bool { measured > maxBodyHeight + 0.5 }

    var body: some View {
        VStack(spacing: 0) {
            SidebarPaneHeader(icon: icon, summary: summary, trailing: trailing)
            ScrollView {
                content()
                    .background(GeometryReader { geo in
                        Color.clear.preference(key: SidebarPaneBodyHeightKey.self,
                                               value: geo.size.height)
                    })
            }
            .frame(height: bodyHeight)
            .scrollBounceBehavior(.basedOnSize)
            // Fade the bottom edge only while the list actually overflows the cap.
            .mask(BottomFadeMask(clipped: scrollable))
            .onPreferenceChange(SidebarPaneBodyHeightKey.self) { measured = $0 }
        }
    }
}

/// An idle/loading placeholder for the queue panes — sized to its content (so the pane shrinks
/// to fit it), centered horizontally with a little breathing room.
struct SidebarPanePlaceholder<Content: View>: View {
    @ViewBuilder var content: () -> Content

    var body: some View {
        content()
            .frame(maxWidth: .infinity)
            .padding(.vertical, 20).padding(.horizontal, 16)
    }
}

/// Bottom fade for the queue panes' scrollable body: opaque over the list, fading to clear
/// across the last `fade` points so a row that doesn't fully fit trails off instead of shearing
/// at the frame edge. `clipped` is false to stay fully opaque when nothing can scroll.
struct BottomFadeMask: View {
    var clipped: Bool
    var fade: CGFloat = 22

    var body: some View {
        VStack(spacing: 0) {
            Color.black
            if clipped {
                LinearGradient(colors: [.black, .clear], startPoint: .top, endPoint: .bottom)
                    .frame(height: fade)
            }
        }
    }
}
