import SwiftUI
import AppKit
import NucleicCore

/// Wraps the chat (`SessionDetailView`) in a paneling system: the chat is always
/// centered, with an optional left column, right column, and a row below it. Panels
/// (file explorer, terminal) are dragged from the toolbar palette onto an edge, or
/// added via the Panels menu; the arrangement is the global working layout held by
/// `PanelLayoutStore` and can be saved as a named layout with a default.
///
/// Design: layouts are app-wide rather than per-session so a user settles on one
/// cockpit and every chat opens into it. Each panel is keyed on session + instance
/// id, so switching chats re-roots the file tree / shell at the new worktree while
/// keeping a panel's state stable within a chat.
struct PaneledChatView: View {
    @Environment(AppStore.self) private var store
    @Environment(PanelLayoutStore.self) private var panels
    @Environment(FileSelectionStore.self) private var fileSelection

    @State private var savingLayout = false
    @State private var layoutNameDraft = ""
    @State private var managingLayouts = false

    /// Thickness of an empty region's drop rail, and of a resize handle.
    private let railThickness: CGFloat = 12
    private let handleThickness: CGFloat = 7

    private var session: Session? { store.openSession }
    private var layout: PanelLayout { panels.working }

    /// The worktree's folder name — used as the subtitle for the Files and Terminal
    /// headers (both are rooted there), echoing the chat header's status line so the
    /// pane headers are the same height.
    private var worktreeName: String {
        guard let path = session?.worktreePath else { return "No worktree" }
        return (path as NSString).lastPathComponent
    }

    /// Per-kind subtitle shown under the title in each pane's chat-matching header.
    private func subtitle(for kind: PanelKind) -> String {
        switch kind {
        case .fileExplorer, .terminal:
            worktreeName
        case .editor:
            fileSelection.selected.map { ($0.path as NSString).lastPathComponent } ?? "No file open"
        }
    }

    /// Pane-specific header controls: the editor surfaces its Save/Preview icons in
    /// the header; other panes have none.
    private func headerAccessory(for kind: PanelKind) -> AnyView {
        switch kind {
        case .editor: AnyView(EditorHeaderControls())
        case .fileExplorer, .terminal: AnyView(EmptyView())
        }
    }

    var body: some View {
        GeometryReader { geo in
            HStack(spacing: 0) {
                leftRegion(container: geo.size)
                centerStack(container: geo.size)
                rightRegion(container: geo.size)
            }
        }
        .toolbar { ToolbarItem { panelsMenu } }
        // A file path is worktree-relative to the open chat; clear the shared
        // selection when the chat changes so the Editor never shows another
        // worktree's file.
        .onChange(of: session?.id) { _, _ in fileSelection.clear() }
        .alert("Save Layout", isPresented: $savingLayout) {
            TextField("Layout name", text: $layoutNameDraft)
            Button("Cancel", role: .cancel) {}
            Button("Save") { panels.saveWorking(as: layoutNameDraft) }
        } message: {
            Text("Saves the current panel arrangement. Reusing a name overwrites that layout.")
        }
        .sheet(isPresented: $managingLayouts) { ManageLayoutsSheet() }
    }

    // MARK: - Regions

    @ViewBuilder
    private func leftRegion(container: CGSize) -> some View {
        if layout.left.isEmpty {
            edgeRail(.left)
        } else {
            panelStack(.left, axis: .vertical)
                .frame(width: clampWidth(layout.leftWidth, container: container))
            ResizeHandle(axis: .horizontal, sign: 1, value: layout.leftWidth,
                         range: widthRange(container),
                         onChange: { panels.setWidth($0, for: .left) },
                         onCommit: { panels.commitSizes() })
                .frame(width: handleThickness)
        }
    }

    @ViewBuilder
    private func rightRegion(container: CGSize) -> some View {
        if layout.right.isEmpty {
            edgeRail(.right)
        } else {
            ResizeHandle(axis: .horizontal, sign: -1, value: layout.rightWidth,
                         range: widthRange(container),
                         onChange: { panels.setWidth($0, for: .right) },
                         onCommit: { panels.commitSizes() })
                .frame(width: handleThickness)
            panelStack(.right, axis: .vertical)
                .frame(width: clampWidth(layout.rightWidth, container: container))
        }
    }

    @ViewBuilder
    private func centerStack(container: CGSize) -> some View {
        VStack(spacing: 0) {
            SessionDetailView()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            if layout.bottom.isEmpty {
                edgeRail(.bottom)
            } else {
                ResizeHandle(axis: .vertical, sign: -1, value: layout.bottomHeight,
                             range: heightRange(container),
                             onChange: { panels.setBottomHeight($0) },
                             onCommit: { panels.commitSizes() })
                    .frame(height: handleThickness)
                panelStack(.bottom, axis: .horizontal)
                    .frame(height: clampHeight(layout.bottomHeight, container: container))
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    /// The panels in a slot, stacked flush along `axis` and separated by hairline
    /// dividers (no insets/cards), with the whole region acting as a drop target.
    @ViewBuilder
    private func panelStack(_ slot: PanelSlot, axis: Axis) -> some View {
        let items = layout.instances(in: slot)
        Group {
            if axis == .vertical {
                VStack(spacing: 0) { panelViews(items) }
            } else {
                HStack(spacing: 0) { panelViews(items) }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(AppTheme.background)
        .dropDestination(for: PanelDrag.self) { drags, _ in
            guard let drag = drags.first else { return false }
            panels.drop(drag, into: slot)
            return true
        }
    }

    @ViewBuilder
    private func panelViews(_ items: [PanelInstance]) -> some View {
        ForEach(Array(items.enumerated()), id: \.element.id) { index, instance in
            if index > 0 { Divider() }
            PanelContainer(
                instance: instance, session: session,
                headerStyle: .prominent,
                headerSubtitle: subtitle(for: instance.kind),
                headerAccessory: headerAccessory(for: instance.kind)
            ) {
                panels.remove(instance.id)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }

    /// A thin strip along an empty region's edge: a drop target that highlights when
    /// a panel is dragged over it. Clicking it offers the same add menu as the
    /// toolbar, so panels are reachable without a drag too.
    private func edgeRail(_ slot: PanelSlot) -> some View {
        EdgeRail(slot: slot, thickness: railThickness,
                 onAdd: { panels.add($0, to: slot) },
                 onDrop: { panels.drop($0, into: slot) })
    }

    // MARK: - Sizing

    private func widthRange(_ container: CGSize) -> ClosedRange<CGFloat> {
        160 ... max(200, container.width * 0.5)
    }
    private func heightRange(_ container: CGSize) -> ClosedRange<CGFloat> {
        120 ... max(160, container.height * 0.6)
    }
    private func clampWidth(_ value: CGFloat, container: CGSize) -> CGFloat {
        value.clamped(to: widthRange(container))
    }
    private func clampHeight(_ value: CGFloat, container: CGSize) -> CGFloat {
        value.clamped(to: heightRange(container))
    }

    // MARK: - Toolbar

    private var panelsMenu: some View {
        Menu {
            Section("Add panel") {
                ForEach(PanelKind.allCases) { kind in
                    Menu {
                        ForEach(PanelSlot.allCases, id: \.self) { slot in
                            Button { panels.add(kind, to: slot) } label: {
                                Label(slot.label, systemImage: slot.systemImage)
                            }
                        }
                    } label: {
                        Label(kind.title, systemImage: kind.systemImage)
                    }
                }
            }
            if !panels.saved.isEmpty {
                Section("Layouts") {
                    ForEach(panels.saved) { saved in
                        Button { panels.apply(saved) } label: {
                            layoutMenuLabel(saved)
                        }
                    }
                }
            }
            Section {
                Button("Save Current Layout…", systemImage: "square.and.arrow.down") {
                    layoutNameDraft = ""
                    savingLayout = true
                }
                Button("Manage Layouts…", systemImage: "slider.horizontal.3") {
                    managingLayouts = true
                }
                if !layout.left.isEmpty || !layout.right.isEmpty || !layout.bottom.isEmpty {
                    Button("Clear Panels", systemImage: "xmark.rectangle", role: .destructive) {
                        panels.clearAll()
                    }
                }
            }
        } label: {
            Label("Panels", systemImage: "rectangle.split.3x1")
        }
        .tint(.white)
        .help("Add panels around the chat, or apply a saved layout")
    }

    @ViewBuilder
    private func layoutMenuLabel(_ saved: PanelLayout) -> some View {
        let isDefault = panels.defaultLayoutID == saved.id
        let isActive = panels.matchesWorking(saved)
        if isActive {
            Label(saved.name, systemImage: "checkmark")
        } else if isDefault {
            Label("\(saved.name)  (default)", systemImage: "house")
        } else {
            Text(saved.name)
        }
    }
}

// MARK: - Edge rail

/// The slim strip shown along an empty region: it looks like an ordinary split
/// divider (a hairline against the chat) until you hover or drag a panel over it,
/// when it reveals a short line (vertical on the side columns, horizontal on the
/// bottom row) and tints — so the chat reads as flush to the window edge
/// rather than boxed in by gutters. Clicking offers the same add menu as the
/// toolbar; it's also a drop target.
private struct EdgeRail: View {
    let slot: PanelSlot
    let thickness: CGFloat
    let onAdd: (PanelKind) -> Void
    let onDrop: (PanelDrag) -> Void

    @State private var targeted = false
    @State private var hovering = false
    @Environment(\.appPalette) private var palette

    private var isVerticalStrip: Bool { slot != .bottom }
    private var active: Bool { targeted || hovering }

    /// The edge of the strip that faces the chat, where the hairline divider sits.
    private var hairlineAlignment: Alignment {
        switch slot {
        case .left: .trailing
        case .right: .leading
        case .bottom: .top
        }
    }

    var body: some View {
        Menu {
            ForEach(PanelKind.allCases) { kind in
                Button { onAdd(kind) } label: { Label(kind.title, systemImage: kind.systemImage) }
            }
        } label: {
            ZStack {
                (targeted ? palette.accent.opacity(0.25) : Color.clear)
                Capsule()
                    .fill(active ? palette.accent : Color.secondary)
                    .frame(width: isVerticalStrip ? 2 : 18,
                           height: isVerticalStrip ? 18 : 2)
                    .opacity(active ? 1 : 0.35)
            }
            .overlay(alignment: hairlineAlignment) {
                Rectangle().fill(AppTheme.hairline)
                    .frame(width: isVerticalStrip ? 1 : nil, height: isVerticalStrip ? nil : 1)
            }
            .contentShape(Rectangle())
        }
        .menuStyle(.borderlessButton)
        .menuIndicator(.hidden)
        .frame(width: isVerticalStrip ? thickness : nil,
               height: isVerticalStrip ? nil : thickness)
        .onHover { hovering = $0 }
        .help("Drop a panel here, or click to add one")
        .dropDestination(for: PanelDrag.self) { drags, _ in
            guard let drag = drags.first else { return false }
            onDrop(drag)
            return true
        } isTargeted: { targeted = $0 }
    }
}

// MARK: - Resize handle

/// A draggable divider that adjusts a region's width or height. `sign` flips the
/// drag direction so the same control works whether the region is leading
/// (handle trails it) or trailing (handle leads it).
///
/// The gesture is measured in *global* coordinates on purpose: the handle moves as
/// the region resizes, so a local-space `translation` would be measured against a
/// frame that's shifting under it — a feedback loop that makes the drag jitter. A
/// global translation is independent of the handle's own movement, so the drag
/// tracks the cursor smoothly.
private struct ResizeHandle: View {
    enum Axis { case horizontal, vertical }
    let axis: Axis
    let sign: CGFloat
    let value: CGFloat
    let range: ClosedRange<CGFloat>
    let onChange: (CGFloat) -> Void
    let onCommit: () -> Void

    @State private var base: CGFloat?

    var body: some View {
        Rectangle()
            .fill(Color.clear)
            .overlay(
                Rectangle().fill(AppTheme.hairline)
                    .frame(width: axis == .horizontal ? 1 : nil,
                           height: axis == .vertical ? 1 : nil))
            .contentShape(Rectangle())
            .onHover { inside in
                if inside {
                    (axis == .horizontal ? NSCursor.resizeLeftRight : NSCursor.resizeUpDown).set()
                } else {
                    NSCursor.arrow.set()
                }
            }
            .gesture(
                DragGesture(minimumDistance: 1, coordinateSpace: .global)
                    .onChanged { g in
                        let start = base ?? value
                        if base == nil { base = value }
                        let raw = axis == .horizontal ? g.translation.width : g.translation.height
                        onChange((start + sign * raw).clamped(to: range))
                    }
                    .onEnded { _ in base = nil; onCommit() }
            )
    }
}

extension Comparable {
    func clamped(to range: ClosedRange<Self>) -> Self {
        min(max(self, range.lowerBound), range.upperBound)
    }
}
