import ComposableArchitecture
import Foundation
import SwiftUI
import Utilities

private let log = Logger(category: "OmniplayerReducer")

public struct OmniPlayerView: View {
    private enum Constants {
        static let dragStartThreshold: CGFloat = 5
        static let compactPlayerHeight: CGFloat = 64
        static let customTabBarHeight: CGFloat = 52
        static let compactScreenVisualOffset: CGFloat = 42
        static let quickFlickVelocityThreshold: CGFloat = 50
        static let velocityThreshold: CGFloat = 10

        static let transitionDuration: Double = 0.2
        static let transitionCurve = Animation.easeInOut(duration: transitionDuration)
        static let springAnimation = Animation.interpolatingSpring(stiffness: 260, damping: 28)
    }

    @Environment(\.safeAreaInsets) private var safeAreaInsets
    @Environment(\.scenePhase) private var scenePhase

    @Bindable private var store: StoreOf<OmniPlayerReducer>
    private let namespace: Namespace.ID
    @Shared(.inMemory(.omniPlayerDragOffset)) var dragOffset: CGFloat = 0
    @State private var isExpanded: Bool = false
    @State private var isDragging: Bool = false
    @State private var isDraggingToExpand: Bool = false
    @State private var isDraggingToCollapse: Bool = false
    @State private var didFinishDraggingToExpand: Bool = false
    @State private var didFinishDraggingToCollapse: Bool = false
    @State private var didTapToExpand: Bool = false
    @State private var didFinishExpandingFromTap: Bool = false
    @State private var didTapToCollapse: Bool = false
    @State private var didFinishCollapsingFromTap: Bool = false

    // Dynamic color scheme that forces dark mode when we're on the Hooks tab
    private var compactPlayerColorScheme: ColorScheme {
        if store.isOnHooksFeed {
            return .dark
        } else {
            let defaultColorScheme = UIScreen.main.traitCollection.userInterfaceStyle == .dark ? ColorScheme.dark : ColorScheme.light
            guard let window = UIWindow.current,
                  window.overrideUserInterfaceStyle != .unspecified
            else {
                return defaultColorScheme
            }
            return window.overrideUserInterfaceStyle == .dark ? .dark : defaultColorScheme
        }
    }

    private var maxDragOffset: CGFloat {
        return UIScreen.height - compactPlayerOffset
    }

    private var expandedFadeStartThresholdWhenSwipingDown: CGFloat {
        return maxDragOffset * 0.8
    }

    private var expandedFadeEndThresholdWhenSwipingDown: CGFloat {
        return maxDragOffset
    }

    private var compactFadeStartThresholdWhenSwipingDown: CGFloat {
        return maxDragOffset * 0.8
    }

    private var compactFadeEndThresholdWhenSwipingDown: CGFloat {
        return maxDragOffset
    }

    private var expandedFadeStartThresholdWhenSwipingUp: CGFloat {
        return -maxDragOffset * 0.05
    }

    private var expandedFadeEndThresholdWhenSwipingUp: CGFloat {
        return -maxDragOffset * 0.2
    }

    private var compactFadeStartThresholdWhenSwipingUp: CGFloat {
        return -maxDragOffset * 0.01
    }

    private var compactFadeEndThresholdWhenSwipingUp: CGFloat {
        return -maxDragOffset * 0.4
    }

    private var expandedOpacity: Double {
        if isDraggingToCollapse && dragOffset > expandedFadeStartThresholdWhenSwipingDown {
            let progress = CGFloat(dragOffset - expandedFadeStartThresholdWhenSwipingDown) / CGFloat(expandedFadeEndThresholdWhenSwipingDown - expandedFadeStartThresholdWhenSwipingDown)
            return 1.0 - abs(progress)
        } else if isDraggingToExpand && dragOffset < expandedFadeStartThresholdWhenSwipingUp {
            let progress = CGFloat(dragOffset - expandedFadeStartThresholdWhenSwipingUp) / CGFloat(expandedFadeEndThresholdWhenSwipingUp - expandedFadeStartThresholdWhenSwipingUp)
            return abs(progress)
        } else if isExpanded {
            return 1.0
        } else {
            return 0.0
        }
    }

    private var collapsedOpacity: Double {
        if isDraggingToCollapse && dragOffset > compactFadeStartThresholdWhenSwipingDown {
            let progress = CGFloat(dragOffset - compactFadeStartThresholdWhenSwipingDown) / CGFloat(compactFadeEndThresholdWhenSwipingDown - compactFadeStartThresholdWhenSwipingDown)
            return abs(progress)
        } else if isDraggingToExpand && dragOffset < compactFadeStartThresholdWhenSwipingUp {
            let progress = CGFloat(dragOffset - compactFadeStartThresholdWhenSwipingUp) / CGFloat(compactFadeEndThresholdWhenSwipingUp - compactFadeStartThresholdWhenSwipingUp)
            return 1.0 - abs(progress)
        } else if !isExpanded {
            return 1.0
        } else {
            return 0.0
        }
    }

    private var expandedOffset: CGFloat {
        if !isExpanded {
            return maxDragOffset
        } else {
            return 0
        }
    }

    private var collapsedOffset: CGFloat {
        if !isExpanded {
            return maxDragOffset
        } else {
            return 0
        }
    }

    private var tabBarHeight: CGFloat {
        return Constants.customTabBarHeight
    }

    private var compactPlayerOffset: CGFloat {
        tabBarHeight + safeAreaInsets.bottom + Constants.compactPlayerHeight + 1
    }

    public init(store: StoreOf<OmniPlayerReducer>, namespace: Namespace.ID) {
        self.store = store
        self.namespace = namespace
    }

    public var body: some View {
        content
    }

    @ViewBuilder
    private var content: some View {
        ZStack {
            ExpandedPlayerView(
                store: store.scope(state: \.expandedState, action: \.expanded),
                isVisible: isExpanded,
                dragOffsetWhileAtTop: { _ in
                }
            )
            .opacity(expandedOpacity)
            .offset(y: expandedOffset)
            .animation(Constants.springAnimation, value: expandedOffset)
            .animation(Constants.transitionCurve, value: expandedOpacity)

            Group {
                if #available(iOS 26.0, *) {
                    VStack {
                        CompactPlayerV3View(store: store.scope(state: \.compactState, action: \.compact))
                            .contentShape(Rectangle())
                            .onTapGesture(perform: handleTapToExpand)
                            .frame(height: Constants.compactPlayerHeight)
                        Spacer()
                    }
                    .clipShape(UnevenRoundedRectangle(
                        topLeadingRadius: 12,
                        bottomLeadingRadius: 0,
                        bottomTrailingRadius: 0,
                        topTrailingRadius: 12
                    ))
                    .glassEffect(.regular, in: UnevenRoundedRectangle(
                        topLeadingRadius: 12,
                        bottomLeadingRadius: 0,
                        bottomTrailingRadius: 0,
                        topTrailingRadius: 12
                    ))
                } else {
                    VStack {
                        CompactPlayerV3View(store: store.scope(state: \.compactState, action: \.compact))
                            .contentShape(Rectangle())
                            .onTapGesture(perform: handleTapToExpand)
                            .frame(height: Constants.compactPlayerHeight)
                        Spacer()
                    }
                    .clipShape(UnevenRoundedRectangle(
                        topLeadingRadius: 12,
                        bottomLeadingRadius: 0,
                        bottomTrailingRadius: 0,
                        topTrailingRadius: 12
                    ))
                    .background(
                        UnevenRoundedRectangle(
                            topLeadingRadius: 12,
                            bottomLeadingRadius: 0,
                            bottomTrailingRadius: 0,
                            topTrailingRadius: 12
                        )
                        .fill(Material.thick)
                    )
                }
            }
            .opacity(collapsedOpacity)
            .offset(y: collapsedOffset)
            .animation(Constants.springAnimation, value: collapsedOffset)
            .animation(Constants.transitionCurve, value: collapsedOpacity)
            .environment(\.colorScheme, compactPlayerColorScheme)
        }
        .offset(y: dragOffset)
        .gesture(
            DragGesture()
                .onChanged { value in
                    handleDragChanged(value.translation, velocity: value.velocity)
                }
                .onEnded { value in
                    handleDragEnded(value.translation, velocity: value.velocity)
                }
        )
        .ignoresSafeArea()
        .task {
            store.send(.task)
            handleViewTask()
        }
        .onChange(of: store.isCollapsed) { _, shouldCollapse in
            handleProgrammaticTransition(shouldCollapse)
        }

        .onChange(of: scenePhase) {
            if scenePhase == .active {
                store.send(.didBecomeActive)
            } else if scenePhase == .inactive {
                store.send(.didBecomeInactive)
            }
        }
    }

    private func handleViewTask() {
        guard !store.isCollapsed else {
            isExpanded = false
            return
        }

        isDragging = true
        isDraggingToExpand = true
        isDraggingToCollapse = false
        didFinishDraggingToCollapse = false
        didFinishDraggingToExpand = false
        isExpanded = true
    }

    private func handleTapToExpand() {
        isExpanded = true
        store.send(.setExpanded(true))
    }

    private func handleProgrammaticTransition(_ shouldCollapse: Bool) {
        if shouldCollapse, isDragging {
            isDragging = false
            isDraggingToCollapse = false
            isDraggingToExpand = false
            didFinishDraggingToCollapse = false
            didFinishDraggingToExpand = false
        }

        guard isDragging == false else { return }

        if shouldCollapse, isExpanded {
            isExpanded = false
        } else if !shouldCollapse, !isExpanded {
            isExpanded = true
        }
    }

    private func handleDragChanged(_ translation: CGSize, velocity: CGSize) {
        if store.expandedState.player.isScrubbing {
            $dragOffset.withLock { $0 = 0 }
            return
        }

        let velocityIsVertical = abs(velocity.height) > abs(velocity.width) * 1.2
        let translationIsVertical = abs(translation.height) > abs(translation.width) * 1.2
        let isVertical = velocityIsVertical || (abs(velocity.height) < Constants.velocityThreshold && abs(velocity.width) < Constants.velocityThreshold && translationIsVertical)

        if !isVertical {
            return
        }

        let offset = translation.height
        isDragging = true

        if isExpanded, offset < 0 {
            $dragOffset.withLock { $0 = 0 }
            return
        } else if !isExpanded, offset > 0 {
            $dragOffset.withLock { $0 = 0 }
            return
        }

        let clampedOffset: CGFloat
        if !isExpanded, offset < 0 {
            clampedOffset = max(offset, -maxDragOffset)
        } else if isExpanded, offset > 0 {
            clampedOffset = min(offset, maxDragOffset)
        } else {
            clampedOffset = offset
        }

        $dragOffset.withLock { $0 = clampedOffset }
        if isExpanded, clampedOffset > 0 {
            isDraggingToCollapse = true
            isDraggingToExpand = false
            didFinishDraggingToCollapse = false
            didFinishDraggingToExpand = false
        } else if !isExpanded, offset < 0 {
            isDraggingToExpand = true
            isDraggingToCollapse = false
            didFinishDraggingToCollapse = false
            didFinishDraggingToExpand = false
        } else if !isExpanded, offset > 0 {
        } else if isExpanded, offset < 0 {
        } else {
            $dragOffset.withLock { $0 = 0 }
            isDraggingToCollapse = false
            isDraggingToExpand = false
            didFinishDraggingToCollapse = false
            didFinishDraggingToExpand = false
        }
    }

    private func handleDragEnded(_ translation: CGSize, velocity: CGSize) {
        defer {
            isDragging = false
            isDraggingToCollapse = false
            isDraggingToExpand = false
            $dragOffset.withLock { $0 = 0 }
        }

        if store.expandedState.player.isScrubbing {
            return
        }

        let velocityIsVertical = abs(velocity.height) > abs(velocity.width) * 1.2
        let translationIsVertical = abs(translation.height) > abs(translation.width) * 1.2
        let isVertical = velocityIsVertical || (abs(velocity.height) < Constants.velocityThreshold && abs(velocity.width) < Constants.velocityThreshold && translationIsVertical)

        if !isVertical {
            return
        }

        let offset = translation.height
        let verticalVelocity = velocity.height

        if !isDragging {
            return
        }

        $dragOffset.withLock { $0 = offset }

        let isQuickFlick = abs(verticalVelocity) > Constants.quickFlickVelocityThreshold
        let shouldTransition: Bool

        if isExpanded {
            shouldTransition = offset > Constants.dragStartThreshold || (isQuickFlick && verticalVelocity > 0)
        } else {
            shouldTransition = offset < -Constants.dragStartThreshold || (isQuickFlick && verticalVelocity < 0)
        }

        if shouldTransition, isDraggingToExpand {
            isExpanded = true
            store.send(.setExpanded(true))
        } else if shouldTransition, isDraggingToCollapse {
            isExpanded = false
            store.send(.setExpanded(false))
        }
    }
}

#if DEBUG
import APIClient

#Preview {
    @Previewable @Namespace var namespace
    let mockClip = Clip.mock(id: "1")
    let queue: [Clip] = [mockClip, .mock(id: "2"), .mock(id: "3")]

    Rectangle()
        .foregroundStyle(.green)
        .overlay(alignment: .bottom) {
            Rectangle()
                .foregroundStyle(.red)
                .frame(height: 52)
                .opacity(0.5)
        }
        .overlay {
            OmniPlayerView(
                store: Store(
                    initialState: .init(
                        clip: mockClip,
                        queue: queue,
                        me: Shared(value: Me(models: [], roles: [:], flags: [:], user: User.mock())),
                        isExpanded: false,
                        prompt: .none,
                        autoStart: false,
                        context: SessionContext(source: .topToast)
                    ),
                    reducer: { OmniPlayerReducer() }
                ),
                namespace: namespace
            )
        }
}
#endif
