import AppKit
import NucleicCore
import SwiftUI

/// Renders the concrete content for a panel instance, keyed so each panel keeps its
/// own state (terminal process, file selection) across redraws.
struct PanelHost: View {
    let instance: PanelInstance
    let session: Session?

    var body: some View {
        content
            // Re-root (and, for the terminal, restart the shell) when the open chat
            // changes: a new session id forces SwiftUI to rebuild this subtree at the
            // new worktree, rather than reusing the previous chat's process/tree.
            .id(PanelIdentity(instance: instance.id, session: session?.id))
    }

    @ViewBuilder
    private var content: some View {
        switch instance.kind {
        case .fileExplorer:
            FileExplorerPanel(root: session?.worktreePath, instanceID: instance.id)
        case .editor:
            EditorPanel()
        case .terminal:
            TerminalPanel(workingDirectory: session?.worktreePath)
        case .vmMonitor:
            VMMonitorPanel(session: session)
        case .subagents:
            SubagentsPanel(session: session)
        case .nashViewer:
            NashViewerPanel(session: session)
        }
    }

    private struct PanelIdentity: Hashable {
        let instance: UUID
        let session: SessionID?
    }
}

/// How a pane draws its title bar.
enum PanelHeaderStyle {
    /// A slim flat strip in the sidebar tone (editor, terminal).
    case compact
    /// A taller header that mirrors the chat's own header band — `.headline` title
    /// over a caption subtitle, on the chat background with a divider beneath — so
    /// the pane's content starts on the same line as the chat transcript.
    case prominent
}

/// A framed panel: a title bar (name, drag source, close button) over its content.
/// The whole title bar is the drag source, so a panel can be dragged to any position
/// in any region; right-clicking it offers the same moves as a menu, and the ×
/// removes it.
struct PanelContainer: View {
    let instance: PanelInstance
    let session: Session?
    var headerStyle: PanelHeaderStyle = .compact
    /// Secondary line under the title in the prominent header (mirrors the chat
    /// header's status line); keeps the header the same height as the chat's.
    var headerSubtitle: String? = nil
    /// Pane-specific controls placed in the prominent header, left of the close
    /// button (e.g. the editor's Save/Preview icons).
    var headerAccessory: AnyView = AnyView(EmptyView())
    /// Asks the paneling root where a cursor position would drop this pane. The root owns
    /// every region's geometry, so it — not the pane — decides what a drag is aimed at.
    var resolveTarget: (CGPoint) -> PanelDropTarget? = { _ in nil }
    let onClose: () -> Void

    @Environment(PanelDragState.self) private var drag
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    /// This pane's own size, handed to the drag so the floating preview can keep its
    /// proportions instead of guessing at them.
    @State private var paneSize: CGSize = .zero

    /// True while *this* pane is the one being dragged. The body fades back and settles
    /// a hair smaller, so the preview under the cursor reads as this pane lifted out of
    /// the column — and the hole it is about to leave is legible before it lands.
    private var lifted: Bool { drag.isDragging(instance.id) }

    var body: some View {
        VStack(spacing: 0) {
            switch headerStyle {
            case .compact: compactHeader
            case .prominent: prominentHeader
            }
            Divider()
            content
        }
        .onGeometryChange(for: CGSize.self) { $0.size } action: { paneSize = $0 }
        .opacity(lifted ? 0.35 : 1)
        .scaleEffect(lifted ? 0.985 : 1)
        .animation(PanelMotion.lift(reduceMotion), value: lifted)
    }

    private var chrome: PanelHeaderChrome {
        PanelHeaderChrome(
            instance: instance, subtitle: headerSubtitle, paneSize: paneSize,
            resolveTarget: resolveTarget, onClose: onClose)
    }

    private var content: some View {
        PanelHost(instance: instance, session: session)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(DetailSurface(layer: .inner))
    }

    /// A flat title strip, flush to the pane edges (no rounded card), mirroring the
    /// app's real sidebar tone so the pane reads as window chrome around the chat.
    private var compactHeader: some View {
        HStack(spacing: 6) {
            Image(systemName: instance.kind.systemImage)
                .font(.caption).foregroundStyle(.secondary)
            Text(instance.kind.title)
                .font(.system(size: 11, weight: .semibold))
                .foregroundStyle(.secondary)
                .textCase(.uppercase)
            Spacer()
            closeButton.font(.system(size: 9, weight: .semibold))
        }
        .padding(.horizontal, 10).padding(.vertical, 6)
        .frame(maxWidth: .infinity)
        .background(AppTheme.sidebar)
        .modifier(chrome)
    }

    /// Matches `SessionDetailView`'s header: a headline title over a caption
    /// subtitle, padded 12 on the chat background — so the Files header lines up
    /// with the chat header and the tree starts at the transcript's top.
    private var prominentHeader: some View {
        HStack(alignment: .top) {
            VStack(alignment: .leading, spacing: 2) {
                Text(instance.kind.title).font(.headline)
                Text(headerSubtitle ?? " ")
                    .font(.caption).foregroundStyle(.secondary).lineLimit(1)
            }
            Spacer(minLength: 12)
            headerAccessory
            closeButton.font(.body)
        }
        .padding(12)
        .frame(maxWidth: .infinity)
        .background(DetailSurface(layer: .inner))
        .modifier(chrome)
    }

    private var closeButton: some View {
        PanelCloseButton(action: onClose)
    }
}

// MARK: - Header chrome

/// Everything a pane's title bar does beyond showing its title: it is the drag source,
/// it grows a grabber and a tint under the pointer, it swaps in the open-hand cursor,
/// and it carries the right-click menu that moves the pane without a drag.
///
/// Shared by both header styles so the compact and prominent panes behave identically —
/// the only difference between them is how the title itself is drawn.
private struct PanelHeaderChrome: ViewModifier {
    let instance: PanelInstance
    let subtitle: String?
    let paneSize: CGSize
    let resolveTarget: (CGPoint) -> PanelDropTarget?
    let onClose: () -> Void

    @Environment(PanelLayoutStore.self) private var panels
    @Environment(PanelDragState.self) private var drag
    @Environment(\.appPalette) private var palette
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var hovering = false

    private var payload: PanelDrag {
        PanelDrag(instanceID: instance.id, kind: instance.kind)
    }

    func body(content: Content) -> some View {
        content
            // Layered *over* the header rather than behind it: both header styles paint
            // an opaque background of their own, which a tint underneath would never
            // show through. At this weight it reads as the strip warming up, and the
            // title stays legible straight through it.
            .overlay {
                palette.accent.opacity(hovering ? 0.07 : 0)
                    .allowsHitTesting(false)
            }
            .overlay(alignment: .top) { grabber }
            .contentShape(Rectangle())
            .onHover { inside in
                hovering = inside
                // Never while a drag is live: the pointer leaving this header mid-drag
                // would otherwise reset the closed hand the drag put up. `.set()` rather
                // than push/pop, matching the resize handles — an unbalanced pop from a
                // missed exit event leaves the whole window with the wrong cursor.
                guard drag.lift == nil else { return }
                (inside ? NSCursor.openHand : NSCursor.arrow).set()
            }
            .animation(PanelMotion.hover(reduceMotion), value: hovering)
            // A plain gesture rather than `.draggable` / `.onDrag`: AppKit drag-and-drop
            // needs a declared uniform type identifier, and this app has no bundle to
            // declare one in — see the note on `PanelDragState`. Measured in the paneling
            // root's space so the location can be hit-tested against the regions directly.
            .gesture(
                DragGesture(minimumDistance: 4, coordinateSpace: .named(PanelDragState.rootSpace))
                    .onChanged { value in
                        if drag.lift == nil {
                            drag.begin(payload, subtitle: subtitle, paneSize: paneSize,
                                       at: value.location)
                        }
                        drag.move(to: value.location, target: resolveTarget(value.location))
                    }
                    .onEnded { value in
                        // Resolved from the gesture, not from `drag.target`, so the landing
                        // never depends on the order the state happens to be torn down in.
                        let landing = resolveTarget(value.location)
                        drag.end()
                        if let landing {
                            panels.drop(payload, into: landing.slot, at: landing.index)
                        }
                    })
            .contextMenu { moveMenu }
            .help("Drag to move this panel, or right-click to send it to another region")
    }

    /// The sheet-style grabber that fades in on hover — the only hint that the strip is
    /// a handle. Deliberately an overlay rather than part of the layout: the prominent
    /// header's title has to stay aligned with the chat's, so nothing may take width or
    /// height from it.
    private var grabber: some View {
        Capsule()
            .fill(Color.secondary)
            .frame(width: 26, height: 3)
            .opacity(hovering ? 0.45 : 0)
            .padding(.top, 3)
            .allowsHitTesting(false)
    }

    /// Every move a drag can make, as menu commands: the drag is the fast path, this is
    /// the discoverable and keyboard-reachable one.
    @ViewBuilder
    private var moveMenu: some View {
        let current = panels.slot(of: instance.id)
        let siblings = current.map { panels.working.instances(in: $0) } ?? []
        let index = siblings.firstIndex { $0.id == instance.id }
        // The bottom row lays out across, so "up/down" would name the wrong direction.
        let acrossRow = current == .bottom

        Section("Move to") {
            ForEach(PanelSlot.allCases, id: \.self) { slot in
                Button {
                    panels.move(instance.id, to: slot)
                } label: {
                    Label(slot.label, systemImage: slot.systemImage)
                }
                // Sending the only pane in a region back to that region is a no-op.
                .disabled(slot == current && siblings.count <= 1)
            }
        }
        if let index, siblings.count > 1 {
            Section {
                Button(acrossRow ? "Move Left" : "Move Up",
                       systemImage: acrossRow ? "arrow.left" : "arrow.up") {
                    panels.shift(instance.id, by: -1)
                }
                .disabled(index == 0)
                Button(acrossRow ? "Move Right" : "Move Down",
                       systemImage: acrossRow ? "arrow.right" : "arrow.down") {
                    panels.shift(instance.id, by: 1)
                }
                .disabled(index == siblings.count - 1)
            }
        }
        Section {
            Button("Remove Panel", systemImage: "xmark", role: .destructive, action: onClose)
        }
    }
}

/// The pane's ×. Brightens and swells slightly under the pointer so it reads as live
/// against the header's own hover tint rather than disappearing into it.
private struct PanelCloseButton: View {
    let action: () -> Void

    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var hovering = false

    var body: some View {
        Button(action: action) {
            Image(systemName: "xmark")
        }
        .buttonStyle(.plain)
        .foregroundStyle(hovering ? AnyShapeStyle(.primary) : AnyShapeStyle(.secondary))
        .scaleEffect(hovering ? 1.18 : 1)
        .onHover { hovering = $0 }
        .animation(PanelMotion.hover(reduceMotion), value: hovering)
        .help("Remove panel")
    }
}
