import APIClient
import ComponentLibrary
import ComposableArchitecture
import DeeplinkIntents
import EventBusClient
import FeatureHooksCreate
import FeatureHooksPost
import FeatureToasts
import Foundation
import HooksPlayerClient
import Localization
import NavigationRouterClient
import OmniPlayerClient
import StatsigClient
import SwiftUI
import Utilities

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

// swiftlint:disable file_length

@Reducer
public struct HooksFeedScreenReducer {
    @ObservableState
    public struct State: Equatable {
        @ObservationStateIgnored @ObservedBox public var hooksFeed: HooksFeedReducer.State
        @Shared public var me: Me
        @Shared(.inMemory(.isOnHooksFeed)) public var isOnHooksFeed: Bool = false
        @Shared(.inMemory(.isHooksCreateVisible)) var isHooksCreateVisible: Bool = false
        @Shared(.inMemory(.isHooksFeedFocused)) public var isHooksFeedFocused: Bool = false
        @Shared(.inMemory(.isCompactPlayerVisible)) public var isCompactPlayerVisible: Bool = false
        @Shared(.inMemory(.isPlayingKey)) public var isOmniPlayerPlaying: Bool = false

        public var showHooksOnboarding: Bool = false

        public var hooksPostToast: HooksPostHookToast.ToastState?
        @ObservationStateIgnored public var storedThumbnailImage: UIImage?
        public var lastCreatedHook: Hook?
        public var isMuted: Bool = true
        public var shortcuts: [Shortcut] = []
        public var showCarousel: Bool
        public var launchDeeplinkIntent: DeeplinkIntent?

        // Tracks whether user chose to focus the feed during the
        // current Hooks tab session. If the user taps on Hooks to play,
        // we don't show the OmniPlayer when they come back out or drag
        // to show the carousel.
        public var didPlayHooksInHooksTabSession: Bool = false

        // Helper that lets us expose the current feed focus/fullscreen state
        public var isCarouselVisible: Bool {
            return showCarousel && isHooksFeedFocused == false
        }

        public var isCarouselLoading: Bool {
            return shortcuts.isEmpty
        }

        public var isHooksFeedLoading: Bool {
            return hooksFeed.hooks.isEmpty && hooksFeed.loadingState != .loaded
        }

        public init(
            me: Shared<Me>,
            showCarousel: Bool,
            skipOnboarding: Bool = false,
            hasSeenHooksOnboarding: Bool = false,
            launchDeeplinkIntent: DeeplinkIntent? = nil
        ) {
            self._me = me

            let showOnboarding = !hasSeenHooksOnboarding && !skipOnboarding
            self.showHooksOnboarding = showOnboarding

            self.showCarousel = showCarousel
            self.launchDeeplinkIntent = launchDeeplinkIntent

            self.hooksFeed = HooksFeedReducer.State(
                me: me,
                alwaysFocused: showCarousel == false,
                showOnboarding: showOnboarding
            )
        }
    }

    public enum Action {
        public enum Internal {
            case launchDeeplinkHookResponse(Result<Hook, Error>)
        }

        case task
        case showCompactPlayer
        case hooksFeed(HooksFeedReducer.Action)
        case createHookFromLongPress
        case createHookTapped
        case setFeedInFocus(Bool)
        case startSwipeToMinimize
        case finishSwipeToMinimize
        case refreshFeedTapped
        case hooksPostClientEvent(HooksPostClient.HooksPostClientEvent)
        case hookEvent(EventBusClient.HookEvent)
        case hideHooksPostToast
        case userDismissedToast
        case cancelHooksPost
        case shareHook
        case toggleMute
        case setMuted(Bool)
        case loadShortcuts
        case shortcutsResponse(Result<[Shortcut], Error>)
        case shortcutTapped(Shortcut)
        case notificationsTapped
        case searchTapped
        case carouselSwiped(SwipeDirection)
        case dismissHooksOnboarding
        case `internal`(Internal)
    }

    public enum SwipeDirection: String {
        case left
        case right
    }

    @Dependency(\.hooksPlayerClient) var hooksPlayerClient
    @Dependency(EventBusClient.self) var eventBusClient
    @Dependency(\.eventBus.getHookPublisher) var getHookPublisher
    @Dependency(\.eventBus.sendHookEvent) var sendHookEvent
    @Dependency(\.toastClient.show) var showToast
    @Dependency(\.apiClientV2) var apiClient
    @Dependency(NavigationRouterClient.self) var navigationRouter

    public init() {}

    @Dependency(\.hooksPostClient) var hooksPostClient

    public var body: some ReducerOf<Self> {
        Scope(state: \.hooksFeed, action: \.hooksFeed) {
            HooksFeedReducer()
        }
        Reduce<State, Action> { state, action in
            struct HooksPostClientEventObserver: Hashable {}
            struct ToastDismissalTimerID: Hashable {}

            switch action {
            case .task:
                if !state.showCarousel {
                    // If we're not in the Carousel treatment, and only show
                    // the feed on Hooks tab, set this flag on launch
                    state.$isHooksFeedFocused.withLock { $0 = true }
                }
                state.$isOnHooksFeed.withLock { $0 = true }
                var effects: [Effect<Action>] = [
                    .run { send in
                        for await event in hooksPostClient.events() {
                            await send(.hooksPostClientEvent(event))
                        }
                    }
                    .cancellable(id: HooksPostClientEventObserver()),
                    .send(.loadShortcuts),
                    .subscribe(getHookPublisher(), send: Action.hookEvent),
                ]

                // If we have a launch deeplink, check if it's for a Hook
                // and wait for that response before initializing the feed
                if let launchDeeplinkIntent = state.launchDeeplinkIntent {
                    switch launchDeeplinkIntent {
                    case .hook(let hookId), .profileHook(_, let hookId):
                        effects.append(.run { send in
                            await send(.internal(.launchDeeplinkHookResponse(Result(catching: {
                                try await apiClient.getHookById(hookId)
                            }))))
                        })

                    case .contentShortlink(let shortcode):
                        effects.append(.run { send in
                            do {
                                let shareCodeResponse = try await apiClient.getShareShortlinkInfo(shortcode)
                                if shareCodeResponse.contentType == .hook {
                                    await send(.internal(.launchDeeplinkHookResponse(Result(catching: {
                                        try await apiClient.getHookById(shareCodeResponse.contentId)
                                    }))))
                                } else {
                                    await send(.internal(.launchDeeplinkHookResponse(.failure(NSError(domain: "NotAHook", code: 0)))))
                                }
                            } catch {
                                await send(.internal(.launchDeeplinkHookResponse(.failure(error))))
                            }
                        })

                    default:
                        effects.append(.send(.hooksFeed(.task)))
                    }
                } else {
                    effects.append(.send(.hooksFeed(.task)))
                }

                hooksPlayerClient.setup(state.me.user.id)

                return .merge(effects)

            case .showCompactPlayer:
                state.didPlayHooksInHooksTabSession = false
                return .none

            case .createHookTapped, .createHookFromLongPress:
                state.$isHooksCreateVisible.withLock { $0 = true }
                hooksPlayerClient.pauseCurrentHook(.navigation(.createHook))
                eventBusClient.sendHookEvent(.showCreateHook())
                return .none

            case .cancelHooksPost:
                // Cancel all running operations and hide the toast
                // User cancellation should not show error
                state.hooksPostToast = nil

                return .merge(
                    .cancel(id: ToastDismissalTimerID()),
                    .run { _ in
                        try await hooksPostClient.cancelHookPost()
                    }
                )

            case .shareHook:
                if let hook = state.lastCreatedHook {
                    eventBusClient.sendHookEvent(.shareHook(hook, source: .hooksFeed))
                } else {
                    let toast = ToastReducer.State.ToastType.warning(
                        L10n.FeatureHooks.failedToLoadHook,
                        position: .bottom
                    )
                    showToast(toast)
                }
                return .send(.hideHooksPostToast)

            case .refreshFeedTapped:
                // If we're in the Carousel treatment, we want to set the correct mute state
                // after reloading the feed (should be muted since the feed isn't active).
                let shouldMuteAfterReload: Bool = state.showCarousel == true
                return .merge(
                    .send(.setFeedInFocus(false)),
                    .send(.hooksFeed(.reloadFeed(shouldMute: shouldMuteAfterReload)))
                )

            case .setFeedInFocus(let isInFocus):
                // If we're trying to unfocus the feed, only allow it in carousel mode
                guard isInFocus || state.showCarousel == true else { return .none }

                let wasCarouselVisible = state.isCarouselVisible
                state.$isHooksFeedFocused.withLock { $0 = isInFocus }
                state.hooksFeed.isFocused = isInFocus
                state.hooksFeed.hideTopNavBar = isInFocus
                hooksPlayerClient.setMuted(!isInFocus)
                state.isMuted = !isInFocus
                state.hooksFeed.didTapToUnmute = isInFocus

                // Only set the flag when user focuses (enters fullscreen) from carousel view
                if isInFocus {
                    if wasCarouselVisible {
                        // User tapped from carousel to enter feed - hide omniplayer for this session
                        state.didPlayHooksInHooksTabSession = true
                    }
                    state.hooksFeed.lastFocusedIndex = state.hooksFeed.currentIndex
                    // If we're currently playing something in the (compact) player,
                    // let's also pause it before focusing the feed
                    @Dependency(OmniPlayerClient.self) var omniPlayerClient
                    omniPlayerClient.pauseCurrentClip()
                }
                return .none

            case .startSwipeToMinimize:
                state.hooksFeed.disableScroll = true
                return .none

            case .finishSwipeToMinimize:
                state.hooksFeed.disableScroll = false
                return .none

            case .toggleMute:
                return .send(.setFeedInFocus(true))

            case .setMuted(let muted):
                state.isMuted = muted
                hooksPlayerClient.setMuted(muted)
                return .none

            case .loadShortcuts:
                if state.shortcuts.isEmpty == false {
                    return .none
                } else {
                    return .run { send in
                        await send(.shortcutsResponse(Result(catching: { try await apiClient.getHooksTabShortcuts() })))
                    }
                }

            case .shortcutTapped(let shortcut):
                switch shortcut.destination {
                case .library:
                    navigationRouter.send(.library(tooltipToShow: nil, showNewClips: false))
                case .likedSongs:
                    navigationRouter.send(.likedSongs)
                case .playlistWithId(let playlistId):
                    navigationRouter.send(.playlistWithId(playlistId, title: shortcut.name, imageUrl: shortcut.imageUrl))
                case .followingPlaylist:
                    navigationRouter.send(.followingPlaylist)
                case .continueListeningPlaylist:
                    navigationRouter.send(.listenHistory)
                case .weeklyHitsPlaylist:
                    navigationRouter.send(.weeklyHitsPlaylist)
                case .discoverTab:
                    navigationRouter.send(.explore)
                }
                return .none

            case .shortcutsResponse(.success(let shortcuts)):
                state.shortcuts = shortcuts
                return .none

            case .shortcutsResponse(.failure(let error)):
                log.telemetry.error(error, message: "Failed to load shortcuts.")
                return .none

            case .notificationsTapped:
                navigationRouter.send(route: .notifications)
                return .none

            case .searchTapped:
                navigationRouter.send(route: .search(.publicSong))
                return .none

            case .hooksPostClientEvent(let event):
                switch event {
                case .uploadComplete, .cancelled:
                    // no need to update thumbnail
                    break

                case .statusChanged(_, let status, let thumbnailImage, let videoUploadId):
                    if let thumbnailImage = thumbnailImage {
                        state.storedThumbnailImage = thumbnailImage
                    }

                    // Always use the stored thumbnail
                    let localThumbnail = state.storedThumbnailImage

                    switch status {
                    case .creatingHook, .awaitingHookReadiness:
                        if var toast = state.hooksPostToast {
                            toast.status = .loading
                            toast.thumbnailImage = localThumbnail ?? toast.thumbnailImage
                            toast.isVisible = true
                            state.hooksPostToast = toast
                        } else {
                            state.hooksPostToast = HooksPostHookToast.ToastState(
                                status: .loading,
                                thumbnailImage: localThumbnail,
                                isVisible: true
                            )
                        }

                    case .hookReady:
                        state.hooksPostToast = HooksPostHookToast.ToastState(
                            status: .success,
                            thumbnailImage: localThumbnail
                        )
                        return .run { send in
                            try await Task.sleep(for: .seconds(5))
                            await send(.hideHooksPostToast)
                        }
                        .cancellable(id: ToastDismissalTimerID())

                    case .uploadVideoFailed:
                        state.hooksPostToast = HooksPostHookToast.ToastState(
                            status: .uploadFailed,
                            thumbnailImage: localThumbnail
                        )
                        return .run { send in
                            try await Task.sleep(for: .seconds(5))
                            await send(.hideHooksPostToast)
                        }
                        .cancellable(id: ToastDismissalTimerID())

                    case .createHookFailed:
                        state.hooksPostToast = HooksPostHookToast.ToastState(
                            status: .failure,
                            thumbnailImage: localThumbnail
                        )
                        return .run { send in
                            try await Task.sleep(for: .seconds(5))
                            await send(.hideHooksPostToast)
                        }
                        .cancellable(id: ToastDismissalTimerID())

                    case .hookProcessingFailed:
                        state.hooksPostToast = HooksPostHookToast.ToastState(
                            status: .hookProcessingFailed,
                            thumbnailImage: localThumbnail
                        )
                        return .run { send in
                            try await Task.sleep(for: .seconds(5))
                            await send(.hideHooksPostToast)
                        }
                        .cancellable(id: ToastDismissalTimerID())

                    case .hookFailedModeration:
                        state.hooksPostToast = HooksPostHookToast.ToastState(
                            status: .hookFailedModeration,
                            thumbnailImage: localThumbnail
                        )
                        return .run { send in
                            try await Task.sleep(for: .seconds(5))
                            await send(.hideHooksPostToast)
                        }
                        .cancellable(id: ToastDismissalTimerID())

                    default:
                        break
                    }
                }
                return .none

            case .hookEvent(let event):
                switch event {
                case .hookCreated(let hook):
                    state.lastCreatedHook = hook
                    hooksPlayerClient.playHookInFeed(hook, state.hooksFeed.source)
                    return .none

                default:
                    return .none
                }

            case .hideHooksPostToast:
                state.hooksPostToast = nil
                return .cancel(id: ToastDismissalTimerID())

            case .userDismissedToast:
                if var currentToast = state.hooksPostToast {
                    currentToast.isVisible = false
                    state.hooksPostToast = currentToast
                }
                return .cancel(id: ToastDismissalTimerID())

            case .hooksFeed(.delegate(.hideOnboardingAfterHookDeeplink)):
                state.showHooksOnboarding = false
                return .none

            case .hooksFeed:
                return .none

            case .carouselSwiped:
                // just used for analytics
                return .none

            case .dismissHooksOnboarding:
                state.showHooksOnboarding = false
                let shouldMute = state.showCarousel
                state.isMuted = shouldMute
                state.hooksFeed.didTapToUnmute = shouldMute == false
                // Makes sure we don't unmute if we dismiss the onboarding modal to show a minimized feed
                // that should play unmuted
                return .run { _ in
                    @Shared(.appStorage(.hasSeenHooksOnboarding)) var hasSeenHooksOnboarding: Bool = false
                    $hasSeenHooksOnboarding.withLock { $0 = true }
                    hooksPlayerClient.setOnboardingState(false)
                    hooksPlayerClient.setMuted(shouldMute)
                }

            case .internal(.launchDeeplinkHookResponse(let result)):
                /* Even if we weren't able to determine which Hook to play
                 from the deeplink, we should still unmute the feed and play
                 the first batch of Hooks */

                // Clear stored intent
                state.launchDeeplinkIntent = nil
                // Remove the Mute overlay on the main Hooks feed
                // and put the feed in focus if we're in Carousel mode
                state.isMuted = false
                state.$isHooksFeedFocused.withLock { $0 = true }
                state.hooksFeed.isFocused = true
                // Make sure we unmute the actual feed content
                state.hooksFeed.didTapToUnmute = true
                switch result {
                case .success(let hook):
                    // Include the Hook from a app launch deeplink
                    state.hooksFeed.initialHooks = [hook]
                    state.hooksFeed.source = .deeplink(sourceUrl: "", hookId: hook.id) // No need to pass `sourceUrl` yet
                case .failure(let error):
                    log.telemetry.error(error, message: "Failed to fetch hook from launch deeplink")
                    showToast(.warning(L10n.FeatureHooks.somethingWentWrong, .string(L10n.FeatureHooks.pleaseTryAgain), position: .bottom))
                }

                return .send(.hooksFeed(.task))
            }
        }
        Analytics()
    }
}

public struct HooksFeedScreen: View {
    @Bindable var store: StoreOf<HooksFeedScreenReducer>

    @State private var dragOffset: CGFloat = 0
    @State private var maxDragDistance: CGFloat = 0
    @Environment(\.safeAreaInsets) private var safeAreaInsets

    private let idleOffset: CGFloat = 232
    private let focusThreshold: CGFloat = 100
    private let carouselPlaceholderCount = 3

    public init(store: StoreOf<HooksFeedScreenReducer>) {
        self.store = store
    }

    public var body: some View {
        ZStack {
            shortcutsCarousel
            feedView
        }
        .safeAreaPadding(.bottom, CustomBottomBarConstants.tabBarHeight)
        .background {
            Color.SemanticV1.backgroundPrimary
                .ignoresSafeArea()
        }
        .overlay(alignment: .top) {
            if !store.hooksFeed.isShowingOmniPlayer {
                topNavBar
            }
        }
        .animation(.smooth(duration: 0.25), value: store.isHooksFeedFocused)
        .animation(.smooth(duration: 0.25), value: store.hooksFeed.isShowingOmniPlayer)
        .animation(.smooth(duration: 0.25), value: dragOffset == 0)
        .overlay(alignment: .top) {
            hooksPostToastView()
        }
        .animation(.spring(response: 0.5, dampingFraction: 0.6), value: store.hooksPostToast?.isVisible ?? false)
        .overlay {
            hooksOnboardingOverlay
        }
        .environment(\.colorScheme, .dark)
    }
}

private extension HooksFeedScreen {
    @ViewBuilder
    func hooksPostToastView() -> some View {
        if let toastState = store.hooksPostToast, toastState.isVisible {
            HooksPostHookToast(toastState: toastState) {
                switch toastState.status {
                case .loading:
                    store.send(.cancelHooksPost)
                case .success:
                    store.send(.shareHook)
                case .failure, .hookProcessingFailed, .hookFailedModeration, .uploadFailed:
                    store.send(.userDismissedToast)
                }
            }
            .padding(.horizontal)
            .transition(.move(edge: .top).combined(with: .opacity))
            .gesture(toastDismissalGesture)
        }
    }

    private var currentOffset: CGFloat {
        let baseOffset = store.isHooksFeedFocused ? 0 : idleOffset
        let safeAreaOffset = store.isHooksFeedFocused ? 0 : safeAreaInsets.top
        let combinedOffset = baseOffset + dragOffset + safeAreaOffset

        // Don't allow dragging below the idle position
        return combinedOffset
    }

    private var currentScale: CGFloat {
        let progress = max(0, min(1, (idleOffset - currentOffset) / idleOffset))
        return 0.95 + (progress * 0.05) // Scale from 0.95 to 1.0
    }

    private var idleHooksFeedNavBarOpacity: CGFloat {
        if store.isHooksFeedFocused {
            return 0
        }

        // Fade out based on drag progress - start fading immediately when dragging up
        let dragProgress = max(0, min(1, abs(dragOffset) / (idleOffset * 0.3)))
        return 1.0 - dragProgress
    }

    private var allowDragging: Bool {
        store.showCarousel && store.hooksFeed.isHooksOmniPlayerVisible == false
    }

    private var allowDragToMinimize: Bool {
        guard allowDragging,
              let lastFocusedIndex = store.hooksFeed.lastFocusedIndex,
              lastFocusedIndex == store.hooksFeed.currentIndex
        else {
            return false
        }
        return true
    }

    private var feedView: some View {
        Color.clear
            .overlay(alignment: .top) {
                feedContent
            }
            .allowsHitTesting(store.isHooksFeedFocused)
            .task {
                store.send(.task)
            }
            .onAppear {
                // If we're coming back to the Hooks tab,
                // whether through navigating back from the carousel or
                // when switching tabs, we rely on `.onAppear` to check
                // if we need to bring up the compact player again.
                let shouldShowCompactPlayer = store.isCompactPlayerVisible && // Player is on (only set once per app session, since you can't close the OmniPlayer)
                    store.didPlayHooksInHooksTabSession && // User has played hooks in this tab session
                    !store.isHooksFeedFocused // Feed is not in fullscreen mode

                if shouldShowCompactPlayer {
                    store.send(.showCompactPlayer)
                }
            }
            .overlay {
                idleHooksFeedOverlay
                    .transition(.blurReplace)
                    .opacity(store.isHooksFeedFocused ? 0 : 1)
            }
            .glassBorder(shape: .rect(cornerRadius: 30), enabled: !store.isHooksFeedFocused)
            .clipShape(.rect(cornerRadius: 30))
            .padding(.top, currentOffset)
            .scaleEffect(currentScale, anchor: .bottom)
            .simultaneousGesture(
                // Only add gesture when not focused
                allowDragging && !store.isHooksFeedFocused ? swipeToExpandGesture : nil
            )
            .simultaneousGesture(
                // Add dismissal gesture when focused and showing first hook since last refresh
                allowDragToMinimize ? swipeToMinimizeGesture : nil
            )
            .ignoresSafeArea(edges: store.isHooksFeedFocused && dragOffset == 0 ? .all : .top)
    }

    private var feedContent: some View {
        ZStack {
            if store.isHooksFeedLoading {
                feedPlaceholderView
            } else {
                HooksFeedView(store: store.scope(state: \.hooksFeed, action: \.hooksFeed))
            }
        }
        .animation(.snappy, value: store.isHooksFeedLoading)
    }

    private var feedPlaceholderView: some View {
        RoundedRectangle(cornerRadius: 30)
            .fill(Color.SemanticV2.backgroundSecondary)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .placeholderShimmering(isVisible: store.isHooksFeedLoading, cornerRadius: 30)
    }

    func rubberBand(_ offset: CGFloat, startAt: CGFloat = 0, maxValue: CGFloat) -> CGFloat {
        let calculatedOffset = max(offset - startAt, 0)

        let constant: CGFloat = 0.55
        let result = (constant * abs(calculatedOffset) * maxValue) / (maxValue + constant * abs(calculatedOffset))
        return offset < 0 ? -result : result + min(startAt, offset)
    }

    @ViewBuilder
    var idleHooksFeedOverlay: some View {
        ZStack {
            idleHooksFeedGradientOverlay
            idleHooksFeedNavBar
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        }
    }

    private var hideSunoLogo: Bool {
        // Makes sure we don't show the top "Suno" logo when the OmniPlayer is opened from Hooks feed
        // or when the user is scrolling in the feed
        store.hooksFeed.hideTopNavBar == true || store.hooksFeed.expandedState != nil
    }

    private var hideCreateHookButton: Bool {
        // Makes sure we don't show this over the OmniPlayer when opened from Hooks feed
        store.hooksFeed.expandedState != nil
    }

    @ViewBuilder
    var sunoLogo: some View {
        Image.Icon.logoV1
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(height: 22.0)
            .foregroundStyle(.white)
            .shadow(color: .black.opacity(0.25), radius: 3, x: 0, y: 0)
            .opacity(hideSunoLogo ? 0 : 1)
            .animation(.snappy(duration: 0.15), value: hideSunoLogo)
    }

    @ViewBuilder
    var topNavBar: some View {
        HStack {
            sunoLogo
                .onTapGesture {
                    guard store.showCarousel else { return }
                    dragOffset = 0
                    store.send(.setFeedInFocus(false))
                }
            Spacer()

            if store.isHooksFeedFocused {
                createHookButton
                    .transition(.blurReplace)
            } else {
                HStack(spacing: 12) {
                    ToolbarButton(.notifications, background: Material.ultraThinMaterial, glass: true) {
                        store.send(.notificationsTapped)
                    }
                    .notificationToolbarBadge(isToolbarItem: false)

                    ToolbarButton(.search, background: Material.ultraThinMaterial, glass: true) {
                        store.send(.searchTapped)
                    }
                }
                .transition(.blurReplace)
            }
        }
        .padding(.top, 4)
        .padding(.horizontal, 12)
    }

    @ViewBuilder
    var createHookButton: some View {
        if #available(iOS 26.0, *) {
            Button {
                store.send(.createHookTapped)
            } label: {
                Text(L10n.FeatureClipDetail.createHook)
                    .foregroundStyle(.white)
                    .typographyV1(.createHookButton)
            }
            .buttonStyle(.glass)
            .contentShape(.capsule)
            .environment(\.colorScheme, .dark)
            .opacity(hideCreateHookButton ? 0 : 1)
            .animation(.snappy(duration: 0.15), value: hideCreateHookButton)
        } else {
            Button {
                store.send(.createHookTapped)
            } label: {
                Text(L10n.FeatureClipDetail.createHook)
                    .foregroundStyle(.white)
                    .typographyV1(.createHookButton)
                    .padding(.vertical, 7)
                    .padding(.horizontal, 14)
                    .background {
                        Capsule()
                            .fill(.ultraThinMaterial)
                            .strokeBorder(Color.SemanticV2.backgroundGlassDense, lineWidth: 0.25)
                    }
                    .clipShape(.capsule)
            }
            .buttonStyle(ScaleButtonStyle(scaleAmount: 0.95))
            .environment(\.colorScheme, .dark)
            .opacity(hideCreateHookButton ? 0 : 1)
            .animation(.snappy(duration: 0.15), value: hideCreateHookButton)
        }
    }

    @ViewBuilder
    var idleHooksFeedGradientOverlay: some View {
        LinearGradient(
            stops: [
                Gradient.Stop(color: .black.opacity(0), location: 0.00),
                Gradient.Stop(color: .black.opacity(0.2), location: 1.00),
            ],
            startPoint: UnitPoint(x: 0.5, y: 0.16),
            endPoint: UnitPoint(x: 0.5, y: 0)
        )
        .contentShape(Rectangle())
        .onTapGesture {
            guard !store.isHooksFeedFocused else { return }
            store.send(.setFeedInFocus(true))
        }
    }

    // "Hooks" title and unmute button
    @ViewBuilder
    var idleHooksFeedNavBar: some View {
        HStack {
            if store.isHooksFeedLoading {
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.white.opacity(0.1))
                    .frame(width: 71, height: 20)
            } else {
                Text(L10n.FeatureCatalog.hooks)
                    .typographyV1(.hooksFeedTitle)
                    .foregroundStyle(.white)
            }

            Spacer()

            if !store.isHooksFeedLoading {
                Button {
                    store.send(.toggleMute)
                } label: {
                    if store.isMuted {
                        Image.Icon.volumeOff
                            .foregroundStyle(.white)
                            .font(.system(size: 24))
                    } else {
                        Image.Icon.volumeOn
                            .foregroundStyle(.white)
                            .font(.system(size: 24))
                    }
                }
                .clipShape(.rect)
                .buttonStyle(ScaleButtonStyle(scaleAmount: 0.9))
            }
        }
        .padding(.horizontal, 22)
        .padding(.top, 24)
        .opacity(idleHooksFeedNavBarOpacity)
    }

    var swipeToExpandGesture: some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                // Only allow dragging up when not focused, or any direction when focused
                if !store.isHooksFeedFocused {
                    if value.translation.height < 0 {
                        dragOffset = value.translation.height
                    } else {
                        dragOffset = rubberBand(value.translation.height, maxValue: 200)
                    }
                } else if store.isHooksFeedFocused {
                    dragOffset = value.translation.height
                }
            }
            .onEnded { value in
                let offset = value.predictedEndTranslation.height
                if offset < -focusThreshold {
                    store.send(.setFeedInFocus(true))
                    UIImpactFeedbackGenerator(style: .light).impactOccurred()
                }
                dragOffset = 0
            }
    }

    var swipeToMinimizeGesture: some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                if value.translation.height > 0 {
                    store.send(.startSwipeToMinimize)
                    dragOffset = rubberBand(value.translation.height, startAt: 250, maxValue: 200)
                }
            }
            .onEnded { value in
                let offset = value.predictedEndTranslation.height
                if offset > focusThreshold {
                    store.send(.setFeedInFocus(false))
                    UIImpactFeedbackGenerator(style: .light).impactOccurred()
                }
                store.send(.finishSwipeToMinimize)
                dragOffset = 0
            }
    }

    var toastDismissalGesture: some Gesture {
        DragGesture()
            .onEnded { value in
                if value.translation.height < -50 {
                    store.send(.userDismissedToast)
                }
            }
    }
}

// MARK: - Shortcuts Carousel

private extension HooksFeedScreen {
    var shortcutsCarousel: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            carouselContent
                .padding(.horizontal, 16)
                .scrollTargetLayout()
                .animation(.snappy, value: store.isCarouselLoading)
        }
        .disablePressDelay()
        .scrollTargetBehavior(.viewAligned)
        .padding(.top, 64)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .simultaneousGesture(carouselSwipeGesture)
    }

    var carouselContent: some View {
        HStack(spacing: 16) {
            if store.isCarouselLoading {
                ForEach(0..<carouselPlaceholderCount, id: \.self) { _ in
                    ShortcutCard.Placeholder()
                        .transition(.blurReplace)
                }
            } else {
                ForEach(store.shortcuts) { shortcut in
                    ShortcutCard(shortcut: shortcut) {
                        store.send(.shortcutTapped(shortcut))
                    }
                    .transition(.blurReplace)
                }
            }
        }
    }

    var carouselSwipeGesture: some Gesture {
        DragGesture()
            .onEnded { value in
                let horizontalAmount = value.translation.width
                if abs(horizontalAmount) > 100 {
                    let direction: HooksFeedScreenReducer.SwipeDirection = horizontalAmount < 0 ? .left : .right
                    store.send(.carouselSwiped(direction))
                }
            }
    }
}

private extension HooksFeedScreen {
    @ViewBuilder
    var hooksOnboardingOverlay: some View {
        if store.showHooksOnboarding {
            Color.black.opacity(0.3)
                .ignoresSafeArea()

            HooksFeedOnboardingView(
                onDismiss: { store.send(.dismissHooksOnboarding) }
            )
            .transition(.move(edge: .bottom).combined(with: .opacity))
            .animation(.spring(response: 0.5, dampingFraction: 0.8), value: store.showHooksOnboarding)
        }
    }
}

private extension TypographyV1 {
    static let hooksFeedTitle: TypographyV1 = .init(
        name: "Hooks Feed Title",
        size: 20,
        style: .title,
        weight: .ppNeueMontrealSemiBold,
        lineHeight: 20
    )
}
