import SwiftUI
import Utilities

// MARK: - Detent Definition
public enum CustomSheetDetent: Equatable, Hashable {
    case custom(CGFloat)
    case half
    case full
    case flex // Height based on content

    func height(screenHeight: CGFloat, contentHeight: CGFloat? = nil) -> CGFloat {
        switch self {
        case .custom(let height):
            return height
        case .half:
            return screenHeight * 0.5
        case .full:
            return screenHeight
        case .flex:
            // Use content height if available, otherwise use half screen as fallback
            guard let contentHeight = contentHeight, contentHeight > 0 else {
                return screenHeight * 0.5
            }
            // Clamp between min (100) and max (screen height - 50)
            return min(max(contentHeight, 100), screenHeight - 50)
        }
    }
}

// MARK: - Sheet Style
enum CustomSheetStyle {
    case edgeToEdge  // Ignore safe area, full width
    case floating    // 16px padding around sheet like a card
}

// MARK: - Sheet Background
enum CustomSheetBackground {
    case solid(Color)
    case glass
}

// MARK: - Custom Sheet View
struct CustomSheet<Content: View, AboveSheetContent: View>: View {
    @Binding var isPresented: Bool
    @Binding var currentDetent: CustomSheetDetent
    let detents: [CustomSheetDetent]
    let style: CustomSheetStyle
    let background: CustomSheetBackground
    let allowsFullDismissal: Bool
    let showDimmedBackground: Bool
    let showGrabber: Bool
    var sheetHeight: Binding<CGFloat>? // Optional binding to report sheet height
    let content: Content
    let aboveSheetContent: AboveSheetContent?

    // Drag state
    @State private var dragOffset: CGFloat = 0
    @State private var lastDragValue: CGFloat = 0
    @GestureState private var dragState: DragState = .inactive
    @State private var isDragging: Bool = false
    @State private var dragStartHeight: CGFloat = 0
    @State private var dragStartDetent: CustomSheetDetent = .flex
    @State private var dragMaxHeight: CGFloat = 0
    @State private var dragStartedAtTop: Bool = false

    // Keyboard tracking
    @State private var keyboardHeight: CGFloat = 0

    // Animation state
    @State private var animatedOffset: CGFloat = 0

    // Content height tracking
    @State private var contentHeight: CGFloat = 0

    // Scroll tracking for ScrollView support
    @State private var scrollOffset: CGFloat = 0
    @State private var isScrollViewAtTop: Bool = true
    @State private var isSheetDraggingDown: Bool = false

    init(
        isPresented: Binding<Bool>,
        currentDetent: Binding<CustomSheetDetent>,
        detents: [CustomSheetDetent],
        style: CustomSheetStyle = .edgeToEdge,
        background: CustomSheetBackground = .solid(Color(red: 0x1C/255.0, green: 0x1C/255.0, blue: 0x1F/255.0).opacity(0.95)),
        allowsFullDismissal: Bool = false,
        showDimmedBackground: Bool = false,
        showGrabber: Bool = true,
        sheetHeight: Binding<CGFloat>? = nil,
        @ViewBuilder content: () -> Content,
        @ViewBuilder aboveSheetContent: () -> AboveSheetContent
    ) {
        self._isPresented = isPresented
        self._currentDetent = currentDetent
        self.detents = detents
        self.style = style
        self.background = background
        self.allowsFullDismissal = allowsFullDismissal
        self.showDimmedBackground = showDimmedBackground
        self.showGrabber = showGrabber
        self.sheetHeight = sheetHeight
        self.content = content()
        self.aboveSheetContent = aboveSheetContent()
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .bottom) {
                dimmedBackgroundView
                if isPresented {
                    sheetContentView(geometry: geometry)
                        .transition(.move(edge: .bottom))
                }
            }
        }
        .ignoresSafeArea(edges: .bottom)
        .animation(isDragging ? nil : .spring(response: 0.4, dampingFraction: 0.8), value: isPresented)
        .animation(isDragging ? nil : .spring(response: 0.4, dampingFraction: 0.8), value: animatedOffset)
        .animation(isDragging ? nil : .spring(response: 0.4, dampingFraction: 0.8), value: currentDetent)
        .onChange(of: currentDetent) { oldDetent, newDetent in
            scrollOffset = 0
            isScrollViewAtTop = true
        }
        .background(heightTrackingBackground)
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
            handleKeyboardShow(notification)
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            handleKeyboardHide()
        }
        .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("CustomSheetScrollOffset"))) { notification in
            if let offset = notification.userInfo?["offset"] as? CGFloat {
                scrollOffset = offset
                isScrollViewAtTop = offset <= 0
            }
        }
    }
    
    // MARK: - Subviews
    
    private var dimmedBackgroundView: some View {
        Group {
            if showDimmedBackground {
                if isPresented {
                    Color.black.opacity(0.4)
                        .ignoresSafeArea()
                        .onTapGesture {
                            if allowsFullDismissal {
                                dismissSheet()
                            } else {
                                withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                                    currentDetent = .flex
                                }
                            }
                        }
                        .transition(.opacity)
                }
            } else {
                Color.clear
                    .ignoresSafeArea()
            }
        }
    }
    
    private func sheetContentView(geometry: GeometryProxy) -> some View {
        VStack(spacing: 0) {
            if currentDetent == .flex, let aboveContent = aboveSheetContent {
                aboveContent
                    .transition(.move(edge: .bottom).combined(with: .opacity))
                    .offset(y: totalOffset(in: geometry))
            }
            
            mainSheetContent(geometry: geometry)
        }
    }
    
    private func mainSheetContent(geometry: GeometryProxy) -> some View {
        VStack(spacing: 0) {
            if showGrabber {
                grabberView
            }
            
            content
                .environment(\.customSheetScrollAtTop, isScrollViewAtTop)
                .environment(\.customSheetDraggingDown, isSheetDraggingDown)
                .id(currentDetent)
        }
        .fixedSize(horizontal: false, vertical: currentDetent == .flex)
        .ignoresSafeArea(edges: currentDetent == .full ? .bottom : [])
        .background(contentHeightTrackingBackground)
        .onPreferenceChange(ContentHeightKey.self) { height in
            if height > 0 && currentDetent == .flex && !isDragging {
                contentHeight = height
            }
        }
        .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
            scrollOffset = offset
            isScrollViewAtTop = offset <= 0
        }
        .frame(height: currentDetent == .flex ? nil : (isDragging ? dragStartHeight : sheetHeight(in: geometry)), alignment: .top)
        .frame(maxWidth: .infinity)
        .background {
            Group {
                switch background {
                case .solid(let color):
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(color)
                case .glass:
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(ChatConstants.Colors.Background.Smoke.thick)
                        .glassBackground(
                            shape: .rect(cornerRadius: cornerRadius),
                            type: .regular,
                            fallbackStyle: ChatConstants.Colors.Background.Smoke.thick,
                            interactive: true
                        )
                }
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
        .padding(sheetPadding)
        .offset(y: totalOffset(in: geometry))
        .simultaneousGesture(dragGesture(geometry: geometry))
    }
    
    private var grabberView: some View {
        VStack(spacing: 0) {
            RoundedRectangle(cornerRadius: 100)
                .fill(Color.white.opacity(0.3))
                .frame(width: 36, height: 4)
                .padding(.vertical, 8)
        }
        .frame(maxWidth: .infinity)
        .opacity(keyboardHeight > 0 ? 0 : 1)
    }
    
    private var contentHeightTrackingBackground: some View {
        GeometryReader { contentGeometry in
            Color.clear
                .preference(key: ContentHeightKey.self, value: contentGeometry.size.height)
        }
    }
    
    
    private var sheetPadding: EdgeInsets {
        if style == .floating && (isDragging ? dragStartDetent : currentDetent) != .full {
            return EdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)
        } else {
            return EdgeInsets()
        }
    }
    
    private func dragGesture(geometry: GeometryProxy) -> some Gesture {
        DragGesture(minimumDistance: 10)
            .onChanged { value in
                handleDragChanged(value, in: geometry)
            }
            .onEnded { value in
                handleDragEnded(value, in: geometry)
            }
    }
    
    private var heightTrackingBackground: some View {
        GeometryReader { geometry in
            Color.clear
                .onAppear {
                    updateSheetHeight(geometry: geometry)
                }
                .onChange(of: currentDetent) { _, _ in
                    updateSheetHeight(geometry: geometry)
                }
                .onChange(of: contentHeight) { _, _ in
                    updateSheetHeight(geometry: geometry)
                }
        }
    }

    // MARK: - Helper Functions

    private var cornerRadius: CGFloat {
        36
    }

    private func sheetHeight(in geometry: GeometryProxy) -> CGFloat {
        let screenHeight = geometry.size.height
        let safeAreaTop = geometry.safeAreaInsets.top
        let safeAreaBottom = geometry.safeAreaInsets.bottom

        if currentDetent == .full {
            // Full detent: fill from safe area top to physical bottom
            // Since we ignore bottom safe area, add it back to reach true bottom
            return screenHeight
        } else {
            // Other detents use calculated height without adjustment
            // The .ignoresSafeArea(edges: .bottom) handles bottom positioning
            return currentDetent.height(screenHeight: screenHeight, contentHeight: contentHeight)
        }
    }

    private func totalOffset(in geometry: GeometryProxy) -> CGFloat {
        var baseOffset = dragOffset + animatedOffset

        // When keyboard is visible and sheet is in flex mode, offset UP to sit on top of keyboard
        let keyboardOffset: CGFloat
        if keyboardHeight > 0 && currentDetent == .flex && !isDragging {
            keyboardOffset = -keyboardHeight
        } else {
            keyboardOffset = 0
        }

        // Apply resistance when dragging beyond bounds (but not for keyboard offset)
        if baseOffset < 0 {
            // Dragging up beyond full height - apply resistance
            return (baseOffset * 0.3) + keyboardOffset
        }

        return baseOffset + keyboardOffset
    }

    private func handleDragChanged(_ value: DragGesture.Value, in geometry: GeometryProxy) {
        // Disable dragging when keyboard is visible
        if keyboardHeight > 0 {
            return
        }

        let translation = value.translation.height
        let horizontalTranslation = abs(value.translation.width)
        let verticalTranslation = abs(value.translation.height)

        // Ignore gesture if it's primarily horizontal
        if horizontalTranslation > verticalTranslation {
            return
        }

        // Capture if drag started at top on first movement
        if !isDragging {
            dragStartedAtTop = isScrollViewAtTop
        }

        // Only process if drag started when ScrollView was at top
        if !dragStartedAtTop {
            isSheetDraggingDown = false
            return
        }

        // When at top of ScrollView, only handle downward drags
        // Let upward drags pass through to ScrollView
        if isScrollViewAtTop && translation < 0 {
            // Dragging up when at top - only handle if expanding sheet (not at full yet)
            if currentDetent == .full {
                isSheetDraggingDown = false
                return
            }
        }

        // Use transaction to disable implicit animations during drag
        var transaction = Transaction()
        transaction.disablesAnimations = true

        withTransaction(transaction) {
            // Capture the starting height and detent on first drag movement
            if !isDragging {
                isDragging = true
                dragStartHeight = sheetHeight(in: geometry)
                dragStartDetent = currentDetent
                // Cache max height to avoid recalculation during drag
                dragMaxHeight = detents.map { $0.height(screenHeight: geometry.size.height, contentHeight: contentHeight) }.max() ?? dragStartHeight
            }

            if translation < 0 {
                // Dragging up - expand sheet to full
                isSheetDraggingDown = false
                dragOffset = translation
            } else {
                // Dragging down - collapse sheet or dismiss, block ScrollView
                isSheetDraggingDown = true
                dragOffset = translation
            }
        }
    }

    private func handleDragEnded(_ value: DragGesture.Value, in geometry: GeometryProxy) {
        // Disable dragging when keyboard is visible
        if keyboardHeight > 0 {
            return
        }

        let translation = value.translation.height
        let velocity = value.predictedEndTranslation.height - translation

        // Reset drag state
        isDragging = false
        isSheetDraggingDown = false

        // Only process if drag started at top
        if !dragStartedAtTop {
            dragStartedAtTop = false
            return
        }

        dragStartedAtTop = false

        // Check for dismiss gesture - drag down more than 100pt or fast downward velocity
        if translation > 100 || velocity > 500 {
            if allowsFullDismissal {
                // Only fully dismiss if explicitly allowed
                dismissSheet()
                dragOffset = 0
                return
            } else {
                // Otherwise, return to flex detent
                withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                    currentDetent = .flex
                    dragOffset = 0
                }
                return
            }
        }

        // Find the closest detent based on drag direction and velocity
        let currentHeight = currentDetent.height(screenHeight: geometry.size.height, contentHeight: contentHeight)
        let sortedDetents = detents.sorted {
            $0.height(screenHeight: geometry.size.height, contentHeight: contentHeight) <
            $1.height(screenHeight: geometry.size.height, contentHeight: contentHeight)
        }

        var targetDetent = currentDetent
        if translation > 30 || velocity > 200 {
            // Dragging down - go to smaller detent
            if let nextDetent = sortedDetents.last(where: {
                $0.height(screenHeight: geometry.size.height, contentHeight: contentHeight) < currentHeight
            }) {
                targetDetent = nextDetent
            }
        } else if translation < -30 || velocity < -200 {
            // Dragging up - go to larger detent
            if let nextDetent = sortedDetents.first(where: {
                $0.height(screenHeight: geometry.size.height, contentHeight: contentHeight) > currentHeight
            }) {
                targetDetent = nextDetent
            }
        }

        // Apply detent change and reset drag offset together with animation
        withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
            currentDetent = targetDetent
            dragOffset = 0
        }
    }

    private func dismissSheet() {
        withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
            isPresented = false
        }
    }

    private func handleKeyboardShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
            return
        }

        withAnimation(.easeOut(duration: 0.3)) {
            keyboardHeight = keyboardFrame.height
        }
    }

    private func handleKeyboardHide() {
        withAnimation(.easeOut(duration: 0.3)) {
            keyboardHeight = 0
        }
    }

    private func updateSheetHeight(geometry: GeometryProxy) {
        let calculatedHeight = currentDetent.height(screenHeight: geometry.size.height, contentHeight: contentHeight)
        // Add bottom padding if floating style and not full detent
        let bottomPadding: CGFloat = (style == .floating && currentDetent != .full) ? 16 : 0
        let totalHeight = calculatedHeight + bottomPadding
        sheetHeight?.wrappedValue = totalHeight
    }
}

// MARK: - Drag State
enum DragState {
    case inactive
    case dragging(translation: CGFloat, velocity: CGFloat)

    var translation: CGFloat {
        switch self {
        case .inactive:
            return 0
        case .dragging(let translation, _):
            return translation
        }
    }

    var velocity: CGFloat {
        switch self {
        case .inactive:
            return 0
        case .dragging(_, let velocity):
            return velocity
        }
    }
}

// MARK: - Preference Key for Content Height
struct ContentHeightKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

// MARK: - Preference Key for Scroll Offset
struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

// MARK: - Environment Key for Scroll Position
private struct CustomSheetScrollAtTopKey: EnvironmentKey {
    static let defaultValue: Bool = true
}

extension EnvironmentValues {
    var customSheetScrollAtTop: Bool {
        get { self[CustomSheetScrollAtTopKey.self] }
        set { self[CustomSheetScrollAtTopKey.self] = newValue }
    }
}

// MARK: - Environment Key for Sheet Dragging Down
private struct CustomSheetDraggingDownKey: EnvironmentKey {
    static let defaultValue: Bool = false
}

extension EnvironmentValues {
    var customSheetDraggingDown: Bool {
        get { self[CustomSheetDraggingDownKey.self] }
        set { self[CustomSheetDraggingDownKey.self] = newValue }
    }
}

// MARK: - Convenience Initializer (No Above Sheet Content)
extension CustomSheet where AboveSheetContent == EmptyView {
    init(
        isPresented: Binding<Bool>,
        currentDetent: Binding<CustomSheetDetent>,
        detents: [CustomSheetDetent],
        style: CustomSheetStyle = .edgeToEdge,
        background: CustomSheetBackground = .solid(Color(red: 0x1C/255.0, green: 0x1C/255.0, blue: 0x1F/255.0).opacity(0.95)),
        allowsFullDismissal: Bool = false,
        showDimmedBackground: Bool = false,
        showGrabber: Bool = true,
        sheetHeight: Binding<CGFloat>? = nil,
        @ViewBuilder content: () -> Content
    ) {
        self.init(
            isPresented: isPresented,
            currentDetent: currentDetent,
            detents: detents,
            style: style,
            background: background,
            allowsFullDismissal: allowsFullDismissal,
            showDimmedBackground: showDimmedBackground,
            showGrabber: showGrabber,
            sheetHeight: sheetHeight,
            content: content,
            aboveSheetContent: { EmptyView() }
        )
    }
}

// MARK: - ScrollView Offset Tracking
extension View {
    public func trackScrollOffset() -> some View {
        self.background(
            GeometryReader { geometry in
                let offset = -geometry.frame(in: .named("scroll")).minY
                Color.clear
                    .preference(
                        key: ScrollOffsetPreferenceKey.self,
                        value: offset
                    )
                    .onAppear {
                        // Send initial offset via notification
                        NotificationCenter.default.post(
                            name: NSNotification.Name("CustomSheetScrollOffset"),
                            object: nil,
                            userInfo: ["offset": offset]
                        )
                    }
                    .onChange(of: offset) { oldValue, newValue in
                        // Send offset updates via notification for immediate updates
                        NotificationCenter.default.post(
                            name: NSNotification.Name("CustomSheetScrollOffset"),
                            object: nil,
                            userInfo: ["offset": newValue]
                        )
                    }
            }
        )
    }
}

// MARK: - ScrollView with Sheet Support
public struct ScrollViewWithSheetSupport<Content: View>: View {
    @Environment(\.customSheetScrollAtTop) private var isAtTop
    @Environment(\.customSheetDraggingDown) private var isDraggingDown
    let content: Content

    public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    public var body: some View {
        ScrollView {
            content
                .trackScrollOffset()
        }
        .coordinateSpace(name: "scroll")
        .scrollBounceBehavior(isAtTop ? .basedOnSize : .automatic)
        .scrollDisabled(isDraggingDown)
    }
}

