import AdamantiumClient
import APIClient
import ClipDataClient
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureBrandedAlert
import FeatureEditSongArt
import FeatureHooksModels
import FeatureManageClip
import FeatureShare
import FeatureShareSheet
import FeatureToasts
import Foundation
import HookActionsClient
import Localization
import NavigationRouterClient
import OmniPlayerClient
import SongActionsClient
import StatsigClient
import SunoModelClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct SongActions {
    public enum DownloadState: Equatable {
        case downloaded(URL)
        case downloading(progress: Double)
        case notDownloaded
        case unknown
        case pending
    }

    @Reducer(state: .equatable)
    public enum Destination {
        @Reducer public struct DownloadShare {
            @ObservableState
            public struct State: Equatable {
                let url: URL
            }
        }

        case shareSheet(DownloadShare)
        case editSongDetails(EditSongDetailsReducer)
        case selectPlaylist(SelectPlaylist)
        case replaceSongArt(ReplaceSongArt)
        case moreInfo(MoreInfo)
        case publish(PublishSongReducer)
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State?
        @Shared(.inMemory(.sunoModelUserAccessCategory)) var sunoModelUserAccessCategory: SunoModelUserAccess = .defaultLimitedAccess
        @Shared(.inMemory(.selectedSunoRemasterModel)) var selectedSunoRemasterModel: RemasterModelMetaData?
        @Shared(.inMemory(.clipParentMap)) var clipParentMap: [ClipID: ParentClip] = [:]
        @Shared(.inMemory(.commentsAccessMap)) var commentsAccessMap: ClipCommentAccessMap = .defaultValue
        @Shared(.appStorage(.hasUsedVideoCoversFeature)) var hasUsedVideoCoversFeature = false
        @Shared(.fileStorage(.savedPrompts)) var savedPrompts:
            [Clip.ID: Prompt] = [:]
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
        @Shared var me: Me

        var error: String?
        var lastKnownAlertType: BrandedAlertStyle = .noAlert

        var isTogglingPrivacy = false

        var commentsToggleEnabled: Bool?

        var isPinned: Bool = false
        var isLoadingPinning = false
        var isTogglingRemixability = false
        var areCommentsEnabledForClip: Bool = false

        var clip: Clip
        let playlist: Playlist?
        var pinnedClipsList: IdentifiedArray<ClipID, Clip> = []
        var hasConfirmedPinLimitReplace = false
        var hasConfirmedUnpublish = false
        @ObservationStateIgnored @ObservedBox var brandedAlert:
            BrandedAlert.State

        var toast: ToastType?

        var isLoadingRootClip = false
        var isDeletingClip = false
        var isTogglingDislike = false

        var videoDownloadState: DownloadState = .unknown

        public var songArtRefreshId: String

        var pendingActionOnClipCompletion: Action.ClipCompletionCallback?
        var audioDownloadState: DownloadState = .unknown

        var isMe: Bool {
            clip.userId == me.user.id
        }

        var isPublic: Bool {
            return clip.isPublic
        }

        var isPreview: Bool {
            return clip.type == .preview
        }

        public var showPublishSong: Bool {
            return
            isMe && (!clip.hasVocal || (clip.hasVocal && clip.canPublishWithVocal)) && !isPreview // Approved audio recordings only
        }

        var shouldShowCommentBubble: Bool {
            return
                clip.isPublic && !isTogglingPrivacy
        }

        var shouldShowPinning: Bool {
            return isMe
        }

        public var shouldShowRemaster: Bool {
            isMe
        }

        var areCommentsEnabledOnClip: Bool {
            return commentsToggleEnabled ?? commentsAccessMap.areCommentsEnabledOnClip(clip.id.remoteId)
        }

        public var canRemix: Bool {
            (isMe || clip.canRemix)
                && FeatureFlag.create.remixAndAttribution && !isPreview
        }

        public var downloadDisabled: Bool {
            (clip.downloadDisabledReason != nil &&
                FeatureFlag.clips.remixContestDisableDownloads) || isPreview
        }

        var shouldShowRemixDownloadWarning: Bool {
            guard let parent = clipParentMap[clip.id] else { return false }
            return parent.userHandle != me.user.handle
        }

        public var shouldShowReplaceSongArt: Bool {
            FeatureFlag.legacy.videoSongCover && isMe
        }

        public var showVideoCover: Bool {
            FeatureFlag.legacy.videoSongCover && clip.videoCoverUrl != nil
        }

        var showDownloadVideoOption: Bool {
            clip.playableSceneUrl != nil
        }

        public var showEditSongDetails: Bool {
            return isMe
        }

        var isDownloadingVideo: Bool {
            guard case .downloading = videoDownloadState else {
                return false
            }
            return true
        }

        var hasAccessToV5: Bool {
            FeatureFlag.legacy.v5Launch == true
        }

        public init(clip: Clip, playlist: Playlist?, me: Shared<Me>) {
            self.clip = clip
            self.playlist = playlist
            self._me = me
            self.brandedAlert = .init(style: .noAlert)
            self.songArtRefreshId =
                clip.id.remoteId + "_" + (clip.videoCoverUrl ?? clip.imageUrl)
        }
    }

    public enum Action: BindableAction {
        case destination(PresentationAction<Destination.Action>)
        case binding(BindingAction<State>)
        case clipCompletionCallback(ClipCompletionCallback)
        case dismiss
        case `internal`(Internal)
        case remixExtendTapped
        case remixReuseTapped
        case onAppear
        case publishTapped
        case reportInappropriateTapped
        case downloadTapped
        case addToPlaylistTapped
        case deleteClipTapped
        case remasterTapped
        case selectRemasterModel(String)
        case delegate(Delegate)
        case toastAction(ToastReducer.Action)
        case brandedAlert(BrandedAlert.Action)
        case remixabilityToggleTapped
        case toggleDislike(Bool)
        case toggleLike(Bool)
        case editSongDetailsTapped
        case replaceSongArtTapped
        case createHookTapped
        case viewHookVideoTapped
        case setToast(ToastType?)
        case setCommentsAccess(Bool)
        case remixCoverTapped
        case downloadVideoTapped
        case getPinnedClipList
        case moreInfoTapped
        case shareTapped
        case togglePrivacyTapped
        case clipEvents(EventBusClient.ClipEvent)
        case clipPinningToggled
        case removeFromPlaylistTapped(Playlist)
        case toggleShowVideoCoverInHooksFeed
        case goToProfile(String)

        public enum ClipCompletionCallback {
            case download
            case extend
        }

        public enum Internal {
            case waitForClipCompletion
            case checkCompletionResponse(Result<Clip, Error>)
            case downloadAudioResponse(Result<APIClient.DownloadEvent, Error>)
            case extendClip(Clip)
            case fetchRootClip(Clip)
            case fetchRootClipResponse(Result<Clip, Error>)
            case incrementActionCountForEventResponse(Result<Void, Error>)
            case togglePrivacyResponse(Result<Clip, Error>)
            case downloadVideoResponse(Result<URL, Error>)
            case saveAudioResponse(Result<URL, Error>)
            case downloadAudio
            case toggledClipPinning(Result<PinnedClipsContainer, Error>, _ originalValue: Bool)
            case getPinnedClipsResponse(Result<PinnedClipsContainer, Error>)

            case toggleRemixabilityResponse(Result<Clip, Error>)
            case dislikeResponse((Bool, Bool), Result<Void, Error>)
        }

        public enum Delegate {
            case extendClip(Clip, Prompt)
            case toastAfter(ToastReducer.State.ToastType)
            case deleteClip(Clip)
            case clipUpdate(Clip)
            case remasterClip(Clip)
            case didUpdatePinning(_ pinnedClips: IdentifiedArray<ClipID, Clip>)
            case queueDestination(clip: Clip, destination: QueueDestinationType)
        }
    }

    @Dependency(AdamantiumClient.self) var adamantiumClient
    @Dependency(APIClient.self) var apiClient
    @Dependency(\.dismiss) var dismiss
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.toastClient.show) var showToast
    @Dependency(\.apiClientV2) var api
    @Dependency(ClipDataClient.self) var clipDataClient
    @Dependency(\.continuousClock) var clock
    @Dependency(\.eventBus.getCreateChannel) var getCreateChannel
    @Dependency(\.clipLineageClient.hydrateRelationshipsForClip) private var hydrateRelationshipsForClip
    @Dependency(\.commentsClient) var commentsClient
    @Dependency(\.eventBus.sendClipEvent) private var sendClipEvent
    @Dependency(\.eventBus.sendHookEvent) private var sendHookEvent
    @Dependency(HookActionsClient.self) var hookActionsClient
    @Dependency(SongActionsClient.self) var songActionsClient
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(OmniPlayerClient.self) var omniplayerClient
    @Dependency(SunoModelClient.self) var sunoModelClient

    public init() {}

    struct CheckCompletionCancellableId: Hashable {}

    public var body: some ReducerOf<Self> {
        Scope(state: \.brandedAlert, action: \.brandedAlert) {
            BrandedAlert()
        }

        Reduce<State, Action> { state, action in
            func reusablePrompt() -> Prompt {
                if var prompt = state.savedPrompts[state.clip.id] {
                    prompt.lyrics = state.clip.prompt
                    prompt.styles = state.clip.tags
                    return prompt
                } else {
                    // Audio not supported without a saved prompt
                    let generationType: GenerationType = .text
                    return Prompt(
                        title: state.clip.title,
                        lyrics: state.clip.prompt,
                        styles: state.clip.tags,
                        text: state.clip.gptDescriptionPrompt,
                        generationType: generationType
                    )
                }
            }

            switch action {
            case .remixReuseTapped:
                let prompt = reusablePrompt()
                getCreateChannel().queue(.reusePrompt(prompt: prompt))
                return .send(.dismiss)

            case .goToProfile(let handle):
                return .run { send in
                    await send(.dismiss)
                    songActionsClient.goToProfile(handle)
                }

            case .moreInfoTapped:
                state.destination = .moreInfo(.init(clip: state.clip, me: state.$me))
                return .none

            case .remixCoverTapped:
                let prompt = state.reusablePrompt()
                getCreateChannel().queue(.coverClip(state.clip, prompt: prompt))
                return .send(.dismiss)

            case .remixExtendTapped:
                guard state.clip.status == .complete else {
                    state.pendingActionOnClipCompletion = .extend
                    return .send(.internal(.waitForClipCompletion))
                }
                // Get root clip if needed
                if let history = state.clip.history, !history.isRoot {
                    return .send(.internal(.fetchRootClip(state.clip)))
                } else {
                    return .send(.internal(.extendClip(state.clip)))
                }

            case .deleteClipTapped:
                if state.clip.hasHook {
                    state.lastKnownAlertType = .multiButtonAlert(.v2(.preset(.areYouSureYouWantToDeleteClipWithHook)))
                    return .send(.brandedAlert(.setStyle(.multiButtonAlert(.v2(.preset(.areYouSureYouWantToDeleteClipWithHook))))))
                }

                state.error = nil
                state.isDeletingClip = true
                return .send(.delegate(.deleteClip(state.clip)))

            case let .internal(.fetchRootClip(clip)):
                guard let rootClipId = clip.history?.rootClip?.remoteId else {
                    return .none
                }
                state.isLoadingRootClip = true
                return .run { send in
                    await send(
                        .internal(
                            .fetchRootClipResponse(
                                .init(catching: {
                                    try await api.getClip(rootClipId)
                                })
                            )
                        )
                    )
                }

            case .internal(.waitForClipCompletion):
                return checkClipCompletion(
                    state: &state,
                    for: state.clip.id,
                    shouldPoll: true
                )

            case .internal(.checkCompletionResponse(.success(let clip))):
                guard clip.status == .complete else {
                    return .none
                }

                state.clip = clip
                guard let pendingAction = state.pendingActionOnClipCompletion
                else {
                    return .cancel(id: CheckCompletionCancellableId())
                }
                state.pendingActionOnClipCompletion = nil
                return .merge(
                    .send(.clipCompletionCallback(pendingAction)),
                    .cancel(id: CheckCompletionCancellableId())
                )

            case let .internal(.fetchRootClipResponse(.success(clip))):
                state.isLoadingRootClip = false
                return .send(.internal(.extendClip(clip)))

            case .internal(.fetchRootClipResponse(.failure(let error))):
                state.isLoadingRootClip = false
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .clipPinningToggled:
                if state.pinnedClipsList.count >= 5 && !state.hasConfirmedPinLimitReplace && !state.isPinned {
                    state.lastKnownAlertType = .multiButtonAlert(.preset(.pinningWillClearOldestPin))
                    return .send(.brandedAlert(.setStyle(.multiButtonAlert(.preset(.pinningWillClearOldestPin)))))
                } else {
                    let originalValue = state.isPinned
                    let newPinningValue = !state.isPinned // Optimisitic
                    state.isPinned = newPinningValue
                    return .run { [clipID = state.clip.id] send in
                        await send(.internal(.toggledClipPinning(
                            Result(catching: {
                                try await api.togglePinClip(clipID)
                            }),
                            originalValue
                        )))
                    }
                }

            case .internal(.toggledClipPinning(.success(let itemContainer), _)):
                let pinnedClips = itemContainer.pinnedClips
                if let pinnedClip = pinnedClips.first(where: { $0.id == state.clip.id }) {
                    state.clip.isPublic = pinnedClip.isPublic
                    state.isPinned = true
                } else {
                    state.isPinned = false
                }
                state.pinnedClipsList = pinnedClips
                sendClipEvent(.updateClip(state.clip))
                return .send(.delegate(.didUpdatePinning(pinnedClips)))

            case .internal(.toggledClipPinning(.failure(let error), let originalValue)):
                log.telemetry.error(error)
                state.isPinned = originalValue
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.couldNotUpdatePinning,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .remixabilityToggleTapped:
                state.error = nil
                state.isTogglingRemixability = true
                state.clip.canRemix.toggle()
                return .run { [clip = state.clip] send in
                    let apiResult = await Result { try await api.toggleRemixability(clip.id, enabled: clip.canRemix) }
                        .map { clip }
                    await send(.internal(.toggleRemixabilityResponse(apiResult)))
                }

            case .internal(.toggleRemixabilityResponse(.success(let clip))):
                state.clip = clip
                sendClipEvent(.updateClip(clip))
                state.isTogglingRemixability = false
                return .none

            case .internal(.toggleRemixabilityResponse(.failure(let error))):
                state.error = error.underlyingError
                state.isTogglingRemixability = false
                state.clip.canRemix.toggle()
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .setToast(let toast):
                state.toast = toast
                return .none

            case let .internal(.extendClip(clip)):
                let prompt = reusablePrompt()
                getCreateChannel().queue(.extendClip(clip, prompt: prompt))
                return .send(.dismiss)

            case .shareTapped:
                state.error = nil
                return .send(.delegate(.queueDestination(clip: state.clip, destination: .shareSheet)))

            case .addToPlaylistTapped:
                return .send(.delegate(.queueDestination(clip: state.clip, destination: .addToPlaylist)))

            case .clipCompletionCallback(let callback):
                switch callback {
                case .download:
                    return .send(.internal(.downloadAudio))
                case .extend:
                    return .send(.internal(.extendClip(state.clip)))
                }

            case .internal(.incrementActionCountForEventResponse(.success)):
                return .none

            case .internal(
                .incrementActionCountForEventResponse(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .reportInappropriateTapped:
                return .run { [clip = state.clip] _ in
                    songActionsClient.reportInappropriate(clip: clip)
                    await self.dismiss()
                }

            case .downloadVideoTapped:
                state.error = nil
                state.videoDownloadState = .downloading(progress: 0)
                return .merge(
                    .run(priority: .background) { [clip = state.clip, isImageToSong = state.clip.isImageToSong] send in
                        do {
                            var videoUrl = try await clip.downloadableShortUrl()
                            if let watermarkedURL = try await adamantiumClient
                                .watermarkVideo(
                                    videoUrl,
                                    Image.RenderingAsset.watermarkStillV1,
                                    isImageToSong
                                )
                            {
                                videoUrl = watermarkedURL
                            }
                            _ = try await VideoDownloader().saveVideo(from: videoUrl)
                            await send(.internal(.downloadVideoResponse(.success(videoUrl))))
                        } catch {
                            await send(.internal(.downloadVideoResponse(.failure(error))))
                        }
                    },
                    .run { [clip = state.clip] send in
                        await send(.internal(.incrementActionCountForEventResponse(.init(catching: { try await apiClient.incrementAction(clip, .downloadVideo) }))))
                    }
                )

            case let .internal(.downloadVideoResponse(.success(url))):
                state.videoDownloadState = .downloaded(url)
                state.error = nil
                let successToast = ToastReducer.State.ToastType.success(
                    L10n.FeatureShare.downloadComplete, position: .top
                )
                showToast(successToast)
                return .none

            case let .internal(.downloadVideoResponse(.failure(error))):
                state.error = error.underlyingError
                state.videoDownloadState = .notDownloaded
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .downloadTapped:
                /// Prevent downloading if the clip is for a remix competition
                guard !state.downloadDisabled else {
                    return .none
                }
                guard state.clip.status == .complete else {
                    state.audioDownloadState = .pending // still streaming
                    state.pendingActionOnClipCompletion = .download
                    return .send(.internal(.waitForClipCompletion))
                }
                return .send(.internal(.downloadAudio))

            case .internal(.downloadAudio):
                state.error = nil
                state.audioDownloadState = .downloading(progress: 0)
                guard let audioURL = URL(string: state.clip.audioUrl) else { return .none }
                return .merge(
                    .run(priority: .background) { [audioURL] send in
                        for try await event in apiClient.downloadStream(audioURL) {
                            await send(.internal(.downloadAudioResponse(.success(event))))
                        }
                    } catch: { error, send in
                        await send(.internal(.downloadAudioResponse(.failure(error))))
                    },
                    .run { [clip = state.clip] send in
                        await send(.internal(.incrementActionCountForEventResponse(.init(catching: { try await apiClient.incrementAction(clip, .downloadAudio) }))))
                    }
                )

            case let .internal(.downloadAudioResponse(.success(.response(data)))):
                state.error = nil

                return .run { [clip = state.clip] send in
                    await send(.internal(.saveAudioResponse(Result(catching: { try clipDataClient.save(clip, data) }))))
                }

            case let .internal(.downloadAudioResponse(.success(.updateProgress(progress)))):
                state.audioDownloadState = .downloading(progress: progress)
                return .none

            case .internal(.downloadAudioResponse(.failure(let error))):
                state.error = error.underlyingError
                state.audioDownloadState = .notDownloaded
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case let .internal(.saveAudioResponse(.success(url))):
                state.audioDownloadState = .downloaded(url)
                state.destination = .shareSheet(.init(url: url))
                return .none

            case .internal(.saveAudioResponse(.failure(let error))):
                state.error = error.underlyingError
                state.audioDownloadState = .notDownloaded
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .getPinnedClipList:
                state.isLoadingPinning = true
                return .run { send in
                    await send(.internal(.getPinnedClipsResponse(
                        Result(catching: {
                            try await api.getPinnedClips()
                        })
                    )))
                }

            case .internal(.getPinnedClipsResponse(let action)):
                switch action {
                case .success(let clipResponse):
                    let pinnedClips = clipResponse.pinnedClips
                    state.pinnedClipsList = pinnedClips
                    let isPinned = pinnedClips.contains { $0.id == state.clip.id }
                    state.isPinned = isPinned

                case .failure(let error):
                    log.telemetry.error(error)
                }
                state.isLoadingPinning = false
                return .none

            case .viewHookVideoTapped:
                // Pause the current clip
                omniplayerClient.pauseCurrentClip()
                // Dismiss the sheet first and then navigate to HooksGrid
                let route = NavigationRouterClient.Route.clipHooks(
                    config: HooksGridConfig.clipHooks(
                        me: state.$me,
                        initialState: .none,
                        clipId: state.clip.id.remoteId
                    )
                )
                return .send(.dismiss)
                    .concatenate(with: .run { _ in navigationRouter.send(route: route) })

            case .createHookTapped:
                // Pause the current clip
                omniplayerClient.pauseCurrentClip()
                // Dismiss the sheet first and then navigate to Hook Create flow
                sendHookEvent(.showCreateHook(with: state.clip))
                return .send(.dismiss)

            case .editSongDetailsTapped:
                state.destination = .editSongDetails(EditSongDetailsState(clip: state.clip, me: state.$me))
                return .none

            case .publishTapped:
                state.destination = .publish(PublishSongState(clip: state.clip, me: state.$me))
                return .none

            case .destination(.presented(.publish(.delegate(.publishSongSuccess(let updatedClip))))):
                state.clip = updatedClip
                state.destination = nil
                state.isPinned = updatedClip.isPinned // This is the only place where updatedClip.isPinned is valid/set
                sendClipEvent(.updateClip(updatedClip))
                let successToast = ToastType.success(
                    L10n.FeatureManageClip.publishSongSuccessMessage,
                    position: .bottom
                )
                return .send(.setToast(successToast))

            case .destination(.presented(.replaceSongArt(.delegate(.songArtUpdateSuccess(let clip))))):
                state.clip = clip
                state.destination = nil
                return .run { send in
                    try await Task.sleep(for: .seconds(0.3))
                    let successToast = ToastType.success(
                        L10n.FeatureClipDetail.songArtUploadSuccess,
                        position: .top
                    )
                    await send(.setToast(successToast))
                }

            case .replaceSongArtTapped:
                state.$hasUsedVideoCoversFeature.withLock { $0 = true }
                state.destination = .replaceSongArt(.init(clip: state.clip, showSheetNavigationBar: true))
                return .none

            case .onAppear:
                hydrateRelationshipsForClip(state.clip.id)
                return .merge(
                    .send(.getPinnedClipList),
                    checkClipCompletion(state: &state, for: state.clip.id),
                    .subscribe(getClipPublisher(), send: Action.clipEvents)
                )

            case .clipEvents(.updateClip(let clip)):
                guard clip.id == state.clip.id else { return .none }
                state.clip = clip
                return .none

            case .clipEvents(.toggledLike(let clip)):
                state.clip = clip
                return .none

            case .remasterTapped:
                getCreateChannel().queue(.remasterClip(state.clip))
                return .send(.dismiss)

            case .selectRemasterModel(let externalKey):
                return .run { _ in
                    await sunoModelClient.setRemasterModel(externalKey)
                }

            case .toggleDislike(let disliked):
                state.isTogglingDislike = true
                state.clip.isDisliked = disliked
                return .run { [clip = state.clip] send in
                    await send(.internal(.dislikeResponse(
                        (clip.isLiked, clip.isDisliked),
                        .init(catching: {
                            try await api.setReaction(clip, clip.isLiked, clip.isDisliked, nil)
                        })
                    )))
                }

            case .toggleLike(let liked):
                return .run { [clip = state.clip] _ in
                    songActionsClient.setLiked(clip: clip, liked: liked, showToast: true, hook: nil, source: nil)
                    await self.dismiss()
                }

            case .toggleShowVideoCoverInHooksFeed:
                songActionsClient.toggleVideoCoverInHooksFeed(clip: state.clip, showVideoCover: state.clip.optOutVideoCoverHook)
                return .none

            case .internal(.dislikeResponse(_, .success)):
                state.isTogglingDislike = false
                sendClipEvent(.toggledLike(state.clip))
                return .none

            case let .internal(.dislikeResponse(originals, .failure(error))):
                state.isTogglingDislike = false
                state.clip.isLiked = originals.0
                state.clip.isDisliked = originals.1
                log.telemetry.error(error)
                sendClipEvent(.toggledLike(state.clip))
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.somethingWentWrongTryAgain,
                    position: .top
                )
                return .send(.setToast(warningToast))

            case .togglePrivacyTapped:
                state.error = nil
                if state.clip.isPublic, state.hasConfirmedUnpublish == false {
                    if state.clip.hasHook {
                        state.lastKnownAlertType = .multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublishWithHook)))
                        return .send(.brandedAlert(.setStyle(.multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublishWithHook))))))
                    } else {
                        state.lastKnownAlertType = .multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublish)))
                        return .send(.brandedAlert(.setStyle(.multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublish))))))
                    }
                }
                state.isTogglingPrivacy = true
                let isPublic = state.clip.isPublic
                state.clip.isPublic.toggle()

                return .run { [clip = state.clip] send in
                    let apiResult = await Result { try await api.setVisibility(clip, !isPublic) }
                        .map { clip }
                    await send(.internal(.togglePrivacyResponse(apiResult)))
                }

            case let .internal(.togglePrivacyResponse(.success(clip))):
                state.clip = clip
                state.isTogglingPrivacy = false
                sendClipEvent(.updateClip(clip))
                // If the song is now private, update the clip pinning state
                guard !state.clip.isPublic, state.isPinned else { return .none }
                return .send(.clipPinningToggled)

            case .internal(.togglePrivacyResponse(.failure(let error))):
                state.error = error.underlyingError
                state.clip.isPublic.toggle()
                state.isTogglingPrivacy = false
                log.telemetry.error(error)
                let warningToast = ToastType.warning(
                    L10n.FeatureClipDetail.actionFailed,
                    position: .bottom
                )
                return .send(.setToast(warningToast))

            case .brandedAlert(.destination(.presented(.multiButtonAlert(.delegate(.tappedPrimaryButton))))):
                var effects: [Effect<Action>] = []
                let lastKnownAlertType = state.lastKnownAlertType

                if lastKnownAlertType == .multiButtonAlert(.preset(.pinningWillClearOldestPin)) {
                    state.hasConfirmedPinLimitReplace = true
                    effects.append(.send(.clipPinningToggled))
                } else if lastKnownAlertType == .multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublish)))
                    || lastKnownAlertType == .multiButtonAlert(.v2(.preset(.areYouSureYouWantToUnpublishWithHook)))
                {
                    state.hasConfirmedUnpublish = true
                    effects.append(.send(.togglePrivacyTapped))
                } else if lastKnownAlertType == .multiButtonAlert(.v2(.preset(.areYouSureYouWantToDeleteClipWithHook))) {
                    state.error = nil
                    state.isDeletingClip = true
                    state.lastKnownAlertType = .noAlert
                    effects.append(.send(.delegate(.deleteClip(state.clip))))
                }

                state.lastKnownAlertType = .noAlert
                effects.append(.send(.brandedAlert(.setStyle(.noAlert))))
                return .concatenate(effects)

            case .brandedAlert(.destination(.presented(.multiButtonAlert(.delegate(.tappedSecondaryButton))))):
                if state.brandedAlert.alertStyle == .multiButtonAlert(.preset(.pinningWillClearOldestPin)) {
                    state.hasConfirmedPinLimitReplace = false
                } else {
                    state.hasConfirmedUnpublish = false
                }
                state.lastKnownAlertType = .noAlert
                return .send(.brandedAlert(.setStyle(.noAlert)))

            case .setCommentsAccess(let canAccess):
                state.commentsToggleEnabled = canAccess
                commentsClient.enqueueSetCommentsAccessOnClip(state.clip.id, canAccess)
                return .none

            case .removeFromPlaylistTapped(let playlist):
                state.error = nil
                if !playlist.isLikedPlaylist {
                    songActionsClient.removeClipFromPlaylist(clip: state.clip, playlist: playlist)
                } else {
                    songActionsClient.setLiked(clip: state.clip, liked: false, showToast: true, hook: nil, source: nil)
                    sendClipEvent(.removeClipFromPlaylist(state.clip, playlist: playlist))
                }
                return .send(.dismiss)

            case .dismiss:
                return .run { _ in await self.dismiss() }

            case .internal(.checkCompletionResponse(.failure)):
                return .none

            case .toastAction,
                 .delegate,
                 .binding,
                 .destination,
                 .brandedAlert,
                 .clipEvents:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        Analytics()
    }

    private func checkClipCompletion(
        state _: inout State,
        for clipId: ClipID,
        shouldPoll: Bool = false
    ) -> Effect<Action> {
        return .run { send in
            await withTaskCancellation(
                id: CheckCompletionCancellableId(),
                cancelInFlight: true
            ) {
                for await _ in clock.timer(interval: .seconds(3)) {
                    await send(
                        .internal(
                            .checkCompletionResponse(
                                .init(catching: {
                                    try await api.getClip(clipId.remoteId)
                                })
                            )
                        )
                    )
                    guard shouldPoll else { return }
                }
            }
        }
    }
}

public struct SongActionsMenu: View {
    @State private var isShowingRemixDownloadAlert = false
    @Bindable var store: StoreOf<SongActions>
    @State private var detentHeight: CGFloat = 0
    @Environment(\.safeAreaInsets) var safeAreaInsets
    var bottomInsets: CGFloat {
        safeAreaInsets.bottom
    }

    let coverSize: CGSize = .init(width: 60, height: 80)

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

    public var body: some View {
        rootWithBasicSheets
    }

    @ViewBuilder
    private var rootWithBasicSheets: some View {
        // Larger view for isMe menu, auto resizing for others (doesn't contain as many options as "me" songs especially with remix disabled).
        let customMediumDetent: PresentationDetent = store.isMe ? .fraction(0.8) : .height(detentHeight)

        rootWithToast
            .sheet(item: $store.scope(state: \.destination?.selectPlaylist, action: \.destination.selectPlaylist)) { store in
                SelectPlaylistScreen(store: store)
                    .presentationDetents([.large])
            }
            .sheet(item: $store.scope(state: \.destination?.shareSheet, action: \.destination.shareSheet)) { downloadShare in
                DownloadShareSheetView(fileURL: downloadShare.state.url)
                    .presentationDetents([.medium, .large])
                    .presentationDragIndicator(.hidden)
            }
            .sheet(item: $store.scope(state: \.destination?.replaceSongArt, action: \.destination.replaceSongArt)) { store in
                ReplaceSongArtScreen(store: store)
            }
            .sheet(item: $store.scope(state: \.destination?.moreInfo, action: \.destination.moreInfo)) { store in
                MoreInfoView(store: store)
                    .presentationDetents([.large])
            }
            .fullScreenCover(item: $store.scope(state: \.destination?.publish, action: \.destination.publish)) { store in
                PublishSongView(store: store)
            }
            .presentationCornerRadius(40, conditional: true)
            .presentationDetents([customMediumDetent, .large])
    }

    @ViewBuilder
    private var rootWithToast: some View {
        root
            .toast($store.toast, position: .bottom, colorScheme: .dark)
    }

    @ViewBuilder
    private var root: some View {
        let publishButtonShowingPadding = 74.0
        let publishButtonHiddenPadding = 18.0
        NavigationStack {
            ScrollView {
                content
                    .onGeometryChange(for: CGSize.self) { proxy in proxy.size } action: {
                        detentHeight = $0.height
                    }
            }
            .scrollBounceBehavior(.basedOnSize)
            .scrollIndicators(.never)
            .padding(.horizontal, 16)
            .padding(.top, 30)
            .presentationDragIndicator(.visible)
            .overlay {
                if let _ = store.brandedAlert.destination {
                    Color.SemanticV1.backgroundQuaternary.opacity(0.4)
                        .edgesIgnoringSafeArea(.all)
                    BrandedAlertView(store.scope(state: \.brandedAlert, action: \.brandedAlert))
                }
            }
            .overlay(alignment: .bottom) {
                if store.showPublishSong {
                    publishSongFloatingButton
                }
            }
            .background(Color.SemanticV1.backgroundPrimary)
        }
        .brandedStackAlert(
            isOn: $isShowingRemixDownloadAlert,
            config: .remixDownloadWarning(
                primaryAction: {
                    isShowingRemixDownloadAlert = false
                    store.send(.downloadTapped)
                },
                secondaryAction: { isShowingRemixDownloadAlert = false }
            )
        )
    }

    private var scrollingTitleBar: some View {
        MarqueeText(
            text: store.clip.title,
            font: TypographyV1.heading4.uiFont ?? .systemFont(ofSize: 15),
            leftFade: 4,
            rightFade: 3,
            startDelay: 3
        )
    }

    @ViewBuilder
    private var content: some View {
        VStack(spacing: 16) {
            header
            menu
        }
        .padding(.bottom, 80)
        .fullScreenCover(item: $store.scope(state: \.destination?.editSongDetails, action: \.destination.editSongDetails)) { store in
            EditSongDetailsView(store: store)
        }
        .onAppear {
            store.send(.onAppear)
        }
    }

    @ViewBuilder
    private var header: some View {
        HStack(spacing: 0) {
            HStack(spacing: 12) {
                Button {
                    if store.shouldShowReplaceSongArt {
                        store.send(.replaceSongArtTapped)
                    }
                } label: {
                    videoAndImageCover
                        .overlay { editCoverIcon }
                }
                .buttonStyle(PlainButtonStyle())

                VStack(alignment: .leading) {
                    scrollingTitleBar
                        .lineLimit(1)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .frame(height: 19)

                    Button(action: {
                        store.send(.goToProfile(store.clip.handle))
                    }, label: {
                        Text(L10n.FeatureClipDetail.by(store.clip.displayName))
                            .lineLimit(1)
                            .foregroundStyle(Color.SemanticV1.foregroundInactive)
                            .typographyV1(TypographyV1.playerCaption)
                    })
                }
            }
            Spacer()

            Button(
                action: {
                    store.send(.moreInfoTapped)
                },
                label: {
                    Text(L10n.FeatureClipDetail.moreInfo)
                        .typographyV1(.monospaceSmall)
                        .foregroundStyle(Color.SemanticV1.foregroundInactive)
                        .padding(.vertical, 6)
                        .padding(.horizontal, 10)
                }
            )
            .clipShape(RoundedRectangle(cornerRadius: 30))
            .overlay(
                RoundedRectangle(cornerRadius: 30)
                    .stroke(Color.SemanticV1.foregroundInactive, lineWidth: 1)
            )
        }
        .padding(.horizontal, 8)
    }

    @ViewBuilder
    private var videoAndImageCover: some View {
        if store.showVideoCover, let videoCoverUrl = store.clip.videoCoverUrl,
           let playableUrl = URL(string: videoCoverUrl)
        {
            ZStack {
                RemoteImage(
                    url: store.clip.largeImageUrl,
                    fallbackId: store.clip.id.remoteId
                )
                .aspectRatio(contentMode: .fill)
                LoopingVideoPlayer(
                    playableUrl: playableUrl,
                    restartBeforePlaying: false,
                    autoPlay: true,
                    isPlaying: .constant(true),
                    overrideTime: nil,
                    fallbackView: {
                        coverImage
                    }
                )
            }
            .id(store.songArtRefreshId)
            .frame(width: coverSize.width, height: coverSize.height)
            .clipShape(RoundedRectangle(cornerRadius: 14))
        } else {
            coverImage
        }
    }

    @ViewBuilder
    private var menu: some View {
        if store.isMe {
            meMenu
        } else {
            othersMenu
        }
    }

    @ViewBuilder
    private var meMenu: some View {
        VStack(spacing: 16) {
            HStack(spacing: 12) {
                if !store.isPreview {
                    addToPlaylist
                }

                likeToggle

                shareSong
            }

            if !store.isPreview {
                VStack(spacing: 0.5) {
                    editSongDetails

                    viewHooks // gated and only shows if song has hooks

                    createHook // gated
                }
                .clipShape(RoundedRectangle(cornerRadius: 12))
            }

            if store.canRemix {
                remixSection
                    .clipShape(RoundedRectangle(cornerRadius: 12))
            }

            if store.isPublic {
                publicOptionsSection
            }

            VStack(spacing: 0.5) {
                if let playlist = store.playlist {
                    removeFromPlaylistRow(playlist)

                    sectionDivider
                }

                dislikeSong

                sectionDivider

                reportInappropriate
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))

            if !store.downloadDisabled {
                downloadSong
                    .clipShape(RoundedRectangle(cornerRadius: 12))
            }

            deleteSong
                .clipShape(RoundedRectangle(cornerRadius: 12))

            if store.downloadDisabled && !store.isPreview {
                disabledActionsWarning
            }
        }
    }

    @ViewBuilder
    private var disabledActionsWarning: some View {
        let disabledReason = switch store.clip.downloadDisabledReason {
        case .remixContest:
            L10n.FeatureClipDetail.remixAndDownloadDisabled
        default:
            L10n.FeatureClipDetail.unavailable
        }
        Text(disabledReason)
            .foregroundStyle(Color.SemanticV2.foregroundInactive)
            .typographyV1(.caption.kerning(0.28))
            .multilineTextAlignment(.center)
            .padding(.leading, 15)
            .padding(.trailing)
            .padding(.vertical, 15)
    }

    @ViewBuilder
    private var remixSection: some View {
        VStack(spacing: 0.5) {
            if #available(iOS 18, *) {
                remixExtend

                sectionDivider
            }

            remixCover

            sectionDivider

            remixReuseStyle

            if store.shouldShowRemaster {
                sectionDivider
                remaster
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }

    @ViewBuilder
    private var othersMenu: some View {
        VStack(spacing: 16) {
            HStack(spacing: 12) {
                addToPlaylist

                likeToggle

                shareSong
            }

            if store.canRemix {
                VStack(spacing: 0.5) {
                    if #available(iOS 18, *) {
                        remixExtend

                        sectionDivider
                    }

                    remixCover

                    sectionDivider

                    remixReuseStyle
                }
                .clipShape(RoundedRectangle(cornerRadius: 12))
            }

            VStack(spacing: 0.5) {
                dislikeSong

                sectionDivider

                reportInappropriate
            }
            .clipShape(RoundedRectangle(cornerRadius: 12))
        }
    }

    @ViewBuilder
    private var remixExtend: some View {
        MenuRowFullWidth(
            title:
            "\(L10n.FeatureClipDetail.remix): \(L10n.FeatureClipDetail.extend)"
        ) {
            store.send(.remixExtendTapped)
        } leadingView: {
            Image.Icon.extend
                .resizable()
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
        .disabled(store.clip.status == .streaming)
    }

    @ViewBuilder
    private var remixCover: some View {
        MenuRowFullWidth(
            title:
            "\(L10n.FeatureClipDetail.remix): \(L10n.FeatureClipDetail.cover)"
        ) {
            store.send(.remixCoverTapped)
        } leadingView: {
            Image.Icon.coverCreate
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
        .disabled(store.clip.status == .streaming)
    }

    private func removeFromPlaylistRow(_ playlist: Playlist) -> some View {
        MenuRowFullWidth(title: L10n.FeatureClipDetail.removeFromThisPlaylist) {
            store.send(.removeFromPlaylistTapped(playlist))
        } leadingView: {
            Image.Icon.minus
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
    }

    @ViewBuilder
    private var likeToggle: some View {
        var likeToggleTitle: String {
            store.clip.isLiked ? L10n.FeatureClipDetail.liked : L10n.FeatureClipDetail.likeSong
        }

        MenuRow(
            title: likeToggleTitle
        ) {
            store.send(.toggleLike(!store.clip.isLiked))
        } icon: {
            if store.clip.isLiked {
                ZStack {
                    Circle()
                        .frame(width: 24, height: 24)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)
                    Image.Icon.thumbsUpV2
                        .resizable()
                        .renderingMode(.template)
                        .frame(width: 17, height: 17)
                        .foregroundColor(Color.SemanticV1.backgroundSecondary)
                }
            } else {
                Image.Icon.thumbsUpV2
                    .resizable()
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }

    @ViewBuilder
    private var remixReuseStyle: some View {
        let remixReuseTitle =
            "\(L10n.FeatureClipDetail.remix): \(L10n.FeatureClipDetail.reuseStyle)"

        MenuRowFullWidth(title: remixReuseTitle) {
            store.send(.remixReuseTapped)
        } leadingView: {
            Image.Icon.reuse
                .resizable()
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
        .disabled(store.clip.status == .streaming)
    }

    @ViewBuilder
    private var remaster: some View {
        let hasRemaster = store.billingInfo?.accessibleFeatures.contains(FeatureSchema(name: .remaster)) == true
        let shouldShowUpsell = !hasRemaster
        let currentModel = store.selectedSunoRemasterModel
        let currentModelKey = currentModel?.externalKey ?? ""

        // Determine pill display based on current model
        let pillType: Pill.PillType.VersionDisplayState = {
            if shouldShowUpsell {
                if store.hasAccessToV5 {
                    return .upsellV5
                } else {
                    return .upsellV4_5Plus
                }
            } else {
                // Show different version based on the selected model
                if store.hasAccessToV5 && currentModel?.name == "v5" {
                    return .v5
                } else if currentModel?.name == "v4.5+" {
                    return .v4_5Plus
                } else {
                    return .v4
                }
            }
        }()

        MenuRowFullWidth(title: L10n.FeatureClipDetail.remaster) {
            store.send(.remasterTapped)
        } leadingView: {
            Image.Icon.star
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        } trailingView: {
            AnyView(
                HStack {
                    Pill(type: .versioning(version: pillType), action: {
                        store.send(.remasterTapped)
                    })
                    Spacer()
                }
            )
        }
        .disabled(store.clip.status == .streaming)
    }

    @ViewBuilder
    private var dislikeSong: some View {
        MenuRowFullWidth(title: store.clip.isDisliked ? L10n.FeatureClipDetail.removeDislike : L10n.FeatureClipDetail.dislikeSong, isLoading: store.isTogglingDislike) {
            store.send(.toggleDislike(!store.clip.isDisliked))
        } leadingView: {
            Image.Icon.thumbsDown
                .resizable()
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
    }

    @ViewBuilder
    private var reportInappropriate: some View {
        MenuRowFullWidth(
            title: L10n.FeatureClipDetail.reportInappropriate
        ) {
            store.send(.reportInappropriateTapped)
        } leadingView: {
            Image.Icon.flagV2
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
    }

    @ViewBuilder
    private var shareSong: some View {
        MenuRow(
            title: L10n.FeatureClipDetail.share)
        {
            store.send(.shareTapped)
        } icon: {
            Image.Omniplayer.share
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }

    @ViewBuilder
    private var downloadSong: some View {
        VStack(spacing: 0.5) {
            if store.downloadDisabled {
                let reason = switch store.clip.downloadDisabledReason {
                case .remixContest:
                    L10n.FeatureClipDetail.unavailableForRemixesFromThisContest
                default: // We can show this even if downloadDisabledReason came back as nil
                    L10n.FeatureClipDetail.unavailable
                }

                MenuRowFullWidth(
                    title: L10n.FeatureClipDetail.downloadSong,
                    subtitle: reason
                ) {} leadingView: {
                    Image(systemName: "icloud.and.arrow.down.fill")
                }
                .disabled(true)
                .opacity(0.5)
            } else if case let .downloaded(url) = store.state.audioDownloadState {
                ShareLink(item: url) {
                    MenuRowFullWidth(title: L10n.FeatureClipDetail.downloadSong) {} leadingView: {
                        Image(systemName: "icloud.and.arrow.down.fill")
                    }
                    .disabled(true)
                }
                .simultaneousGesture(TapGesture().onEnded {
                    UIImpactFeedbackGenerator(style: .light).impactOccurred()
                })
            } else {
                MenuRowFullWidth(title: L10n.FeatureClipDetail.downloadSong) {
                    if store.shouldShowRemixDownloadWarning {
                        isShowingRemixDownloadAlert = true
                    } else {
                        store.send(.downloadTapped)
                    }
                } leadingView: {
                    switch store.state.audioDownloadState {
                    case .pending:
                        InfiniteCircularProgressView(strokeColor: Color.SemanticV1.textPrimary, lineWidth: 2)
                            .frame(width: 24, height: 24)

                    case let .downloading(progress: progress):
                        CircularProgressView(progress: progress)
                            .frame(width: 24, height: 24)

                    case let .downloading(progress: progress) where progress == 0:
                        ProgressView()
                            .progressViewStyle(.circular)
                            .frame(width: 24, height: 24)

                    case .unknown, .notDownloaded, .downloaded:
                        Image(systemName: "icloud.and.arrow.down.fill")
                    }
                }
            }

            if store.showDownloadVideoOption {
                sectionDivider

                MenuRowFullWidth(title: L10n.FeatureClipDetail.downloadVideo, isLoading: store.isDownloadingVideo) {
                    store.send(.downloadVideoTapped)
                } leadingView: {
                    Image.Icon.downloadTray
                        .resizable()
                        .renderingMode(.template)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)
                        .frame(width: 24, height: 24)
                }
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }

    @ViewBuilder
    private var deleteSong: some View {
        MenuRowFullWidth(
            title: L10n.FeatureClipDetail.deleteSong,
            isLoading: store.isDeletingClip, flagWarning: true
        ) {
            store.send(.deleteClipTapped)
        } leadingView: {
            Image.Icon.trashV1
                .resizable()
                .renderingMode(.template)
                .frame(width: 24, height: 24)
        }
        .foregroundStyle(Color.SemanticV2.accentError)
    }

    @ViewBuilder
    private var addToPlaylist: some View {
        MenuRow(
            title: L10n.FeatureClipDetail.addToPlaylist
        ) {
            store.send(.addToPlaylistTapped)
        } icon: {
            Image.Icon.plus
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        }
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }

    @ViewBuilder
    private var editCoverIcon: some View {
        if store.shouldShowReplaceSongArt {
            ZStack {
                Circle()
                    .foregroundStyle(Color.SemanticV1.backgroundPrimary)
                    .opacity(0.3)
                    .frame(width: 24, height: 24)
                Image.Icon.editV3
                    .resizable()
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 12, height: 12)
            }
            .padding(.top, 5)
            .padding(.trailing, 4)
            .environment(\.colorScheme, .dark)
            .frame(
                maxWidth: .infinity, maxHeight: .infinity,
                alignment: .topTrailing
            )
            .frame(width: coverSize.width, height: coverSize.height)
        }
    }

    @ViewBuilder
    private var publicOptionsSection: some View {
        VStack(spacing: 0.5) {
            if FeatureFlag.legacy.clipComments && store.shouldShowCommentBubble {
                commentsToggleSection
            }

            if FeatureFlag.create.remixAndAttribution {
                sectionDivider

                if !store.downloadDisabled {
                    remixabilityToggleSection
                }
            }

            showVideoCoverInHooksFeedToggle // gated

            if store.shouldShowPinning {
                sectionDivider

                pinningToggleSection
            }
        }
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }

    @ViewBuilder
    private var commentsToggleSection: some View {
        ZStack {
            MenuRowFullWidth(
                title: L10n.FeatureClipDetail.allowComments,
                rowType: .toggle,
                isToggleOn: store.areCommentsEnabledOnClip,
                toggleAction: {
                    store.send(.setCommentsAccess(!store.areCommentsEnabledOnClip))
                }
            ) {
                store.send(.setCommentsAccess(!store.areCommentsEnabledOnClip))
            } leadingView: {
                Image.Icon.comment
                    .resizable()
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)
            }
        }
    }

    @ViewBuilder
    private var remixabilityToggleSection: some View {
        ZStack {
            MenuRowFullWidth(
                title: L10n.FeatureClipDetail.allowRemix,
                isLoading: store.isTogglingRemixability,
                rowType: .toggle,
                isToggleOn: store.clip.canRemix,
                toggleAction: {
                    store.send(.remixabilityToggleTapped)
                }
            ) {
                store.send(.remixabilityToggleTapped)
            } leadingView: {
                Image.Icon.remix
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)
            }
        }
    }

    @ViewBuilder
    private var pinningToggleSection: some View {
        ZStack {
            MenuRowFullWidth(
                title: L10n.FeatureClipDetail.pin,
                isLoading: store.isLoadingPinning,
                rowType: .toggle,
                isToggleOn: store.isPinned,
                toggleAction: {
                    store.send(.clipPinningToggled)
                }
            ) {
                store.send(.clipPinningToggled)
            } leadingView: {
                Image.Icon.thumbtack
                    .resizable()
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)
            }
        }
    }

    @ViewBuilder
    private var publishSongFloatingButton: some View {
        ZStack(alignment: .bottom) {
            // Linear gradient background
            LinearGradient(
                colors: Color.Gradient.clearToColorLinearGradient(baseColor: Color.SemanticV1.backgroundPrimary),
                startPoint: .top,
                endPoint: .bottom
            )
            .frame(height: 144 + bottomInsets + 50)
            .offset(y: bottomInsets + 25)
            .allowsHitTesting(false)

            Button {
                if store.clip.isPublic {
                    store.send(.togglePrivacyTapped)
                } else {
                    store.send(.publishTapped)
                }
            } label: {
                HStack {
                    if store.isPublic {
                        Image.Icon.globeSlash
                            .renderingMode(.template)
                            .foregroundStyle(Color.SemanticV1.iconPrimary)
                    } else {
                        Image.Icon.globe
                            .renderingMode(.template)
                            .foregroundStyle(Color.SemanticV1.backgroundPrimary)
                    }
                    Text(
                        store.isPublic
                            ? L10n.FeatureClipDetail.unpublishSong
                            : L10n.FeatureClipDetail.publishSong
                    )
                    .typographyV1(.caption2)
                    .foregroundStyle(
                        store.isPublic
                            ? Color.SemanticV1.textPrimary
                            : Color.SemanticV1.backgroundPrimary
                    )
                    .padding(.vertical, 16)
                }
                .frame(maxWidth: .infinity)
                .background(
                    RoundedRectangle(cornerRadius: 60)
                        .fill(
                            store.isPublic
                                ? Color.SemanticV2.backgroundTertiary
                                : Color.SemanticV2.foregroundPrimary)
                )
                .contentShape(.rect)
            }
            .buttonStyle(ScaleButtonStyle(scaleAmount: 0.98))
            .padding(.bottom, 10)
            .padding(.horizontal, 16)
            .frame(maxWidth: .infinity, alignment: .bottom)
        }
    }

    @ViewBuilder
    private var sectionDivider: some View {
        Spacer()
            .frame(height: 0.00001)
    }

    private var coverImage: some View {
        RemoteImage(
            url: store.clip.largeImageUrl, fallbackId: store.clip.id.remoteId
        )
        .aspectRatio(contentMode: .fill)
        .frame(width: coverSize.width, height: coverSize.height)
        .clipShape(RoundedRectangle(cornerRadius: 14))
    }

    private func hookThumbnail(urlString: String) -> some View {
        RemoteImage(url: urlString, fallbackId: store.clip.userId)
            .clipShape(.rect(cornerRadius: 4))
            .frame(width: 24, height: 34)
            .foregroundStyle(Color.SemanticV1.iconPrimary)
            .shadow(color: .black.opacity(0.25), radius: 4, x: 0, y: 0)
    }

    @ViewBuilder
    private var editSongDetails: some View {
        MenuRowFullWidth(title: L10n.FeatureClipDetail.editSongDetails) {
            store.send(.editSongDetailsTapped)
        } leadingView: {
            Image.Icon.information
                .resizable()
                .renderingMode(.template)
                .foregroundStyle(Color.SemanticV1.iconPrimary)
                .frame(width: 24, height: 24)
        } trailingView: {
            AnyView(
                Image.Icon.caretRight
                    .resizable()
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)
            )
        }
    }

    @ViewBuilder
    private var viewHooks: some View {
        if store.clip.hasHook,
           let thumbnailUrlString = store.clip.hookPreviewThumbnailUrl,
           FeatureFlag.hooks.isFeedEnabled
        {
            sectionDivider

            MenuRowFullWidth(title: L10n.FeatureClipDetail.viewHooks) {
                store.send(.viewHookVideoTapped)
            } leadingView: {
                hookThumbnail(urlString: thumbnailUrlString)
            } trailingView: {
                AnyView(
                    Image.Icon.caretRight
                        .resizable()
                        .renderingMode(.template)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)
                        .frame(width: 24, height: 24)
                )
            }
        }
    }

    @ViewBuilder
    private var createHook: some View {
        if FeatureFlag.hooks.isFeedEnabled, FeatureFlag.hooks.createHookFromClip {
            sectionDivider
            MenuRowFullWidth(title: L10n.FeatureClipDetail.createHook) {
                store.send(.createHookTapped)
            } leadingView: {
                Image.Icon.hooksTabIcon
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .padding(.vertical, 2)
                    .padding(.horizontal, 4)
                    .frame(width: 24, height: 24)
            } trailingView: {
                AnyView(
                    HStack {
                        BadgeLabel(L10n.FeatureClipDetail.new, style: .primary)
                        Spacer()
                    }
                )
            }
        }
    }

    @ViewBuilder
    private var showVideoCoverInHooksFeedToggle: some View {
        if FeatureFlag.hooks.isFeedEnabled && store.clip.isPublic && store.clip.videoCoverUrl != nil {
            sectionDivider
            MenuRowFullWidth(
                title: L10n.FeatureClipDetail.showVideoCoverInHooksFeed,
                rowType: .toggle,
                isToggleOn: !store.clip.optOutVideoCoverHook,
                toggleAction: {
                    store.send(.toggleShowVideoCoverInHooksFeed)
                }
            ) {
                store.send(.toggleShowVideoCoverInHooksFeed)
            } leadingView: {
                Image.Icon.hooksTabIcon
                    .renderingMode(.template)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .padding(.vertical, 2)
                    .padding(.horizontal, 4)
                    .frame(width: 24, height: 24)
            }
        }
    }
}

public enum QueueDestinationType {
    case addToPlaylist
    case shareSheet
}

struct DownloadShareSheetView: UIViewControllerRepresentable {
    let fileURL: URL

    func makeUIViewController(context _: UIViewControllerRepresentableContext<DownloadShareSheetView>) -> UIActivityViewController {
        UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
    }

    func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<DownloadShareSheetView>) {}
}
