import Adamantium
import APIClient
import BlendedCreateClient
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureAnnouncements
import FeatureBrandedAlert
import FeaturePaywall
import FeatureToasts
import HCaptchaClient
import Localization
import OpenAPIRuntime
import StatsigClient
import SunoModelClient
import SwiftUI
import UserDefaultsClient
import Utilities

// swiftlint:disable file_length

@Reducer
public struct BlendedCreate {
    public enum Tab: Codable {
        case promptBuilder
        case camera
        case audio

        /// Helper init for `EventBusClient`'s `CreateClipEvent.TabID`
        public init?(eventBusTabId: EventBusClient.CreateEvent.TabId?) {
            guard let eventBusTabId = eventBusTabId else { return nil }
            self = switch eventBusTabId {
            case .text:
                .promptBuilder
            case .camera:
                .camera
            case .audio:
                .audio
            }
        }
    }

    @Reducer(state: .equatable)
    public enum Destination {
        case subscriptions(PaywallV1)
        case alert(AlertState<Alert>)
        case brandedAlert(BrandedAlert)
        case bluejayAnnouncement(BluejayAnnouncement)
        case v5Announcement(V5Announcement)

        public enum Alert {
            case upgrade
        }
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State?
        @Presents public var settings: CreateClipSettings.State?
        @ObservationStateIgnored @ObservedBox public var toastState = ToastReducer.State()
        public var hCaptchaToken: String?
        @ObservationStateIgnored @ObservedBox public var brandedAlertState = BrandedAlert.State()

        @ObservationStateIgnored @ObservedBox public var promptBuilder: BlendedCreatePromptBuilder.State
        @ObservationStateIgnored @ObservedBox public var camera: CreateClipCamera.State
        @ObservationStateIgnored @ObservedBox public var audio: Audio.State

        @Shared public var me: Me
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?

        public var didUseAppShortcut: Bool = false

        // Store prompt on success for lookup later in the omniplayer
        @Shared(.fileStorage(.savedPrompts)) var savedPrompts: [Clip.ID: Prompt] = [:]
        @Shared(.appStorage(.hasSeenHowToUseAudio)) var hasSeenHowToUseAudio: Bool = false
        @Shared(.fileStorage(FilePathKeys.ApplicationSupport.lastUsedBlendedCreatePrompt.url())) var lastUsedBlendedCreatePrompt: BlendedCreatePrompt?
        @Shared(.fileStorage(FilePathKeys.ApplicationSupport.lastUsedCreateTab.url())) var lastUsedCreateTab: Tab = .promptBuilder

        public var requiresTokenToGenerate: Bool {
            FeatureFlag.legacy.requiresTokenToGenerate
        }

        public var hCaptchaRetryCount = 0
        public var tokenValidationFailureCount = 0
        public let maxTokenValidationFailures = 3

        @Shared var prompt: BlendedCreatePrompt

        public var tabs: [Tab] = [.audio, .promptBuilder]
        public var currentTab: Tab = .promptBuilder
        public var showTabControls: Bool {
            !prompt.canCreate &&
                audio.isPending && prompt.audioRecording == nil
        }

        var showControls: Bool {
            switch currentTab {
            case .promptBuilder:
                return true
            case .camera:
                return camera.media == nil // show controls
            case .audio:
                return true
            }
        }

        var showCreateButton: Bool {
            prompt.canCreate || prompt.isAudioCreate
        }

        var showUseLastPromptBanner: Bool {
            !prompt.canCreate &&
                lastUsedBlendedCreatePrompt != nil &&
                prompt.isEmpty &&
                audio.isPending &&
                currentTab != .camera // not supported right now
        }

        // Not ideal to have this here, but for now we're hiding "Simple" mode while uploading audio
        var showCreateModePicker: Bool {
            return prompt.audioRecording == nil
        }

        var isCreateButtonLoading: Bool = false

        var tooltip: Tooltip?

        // Temp
        @Shared(.fileStorage(FilePathKeys.ApplicationSupport.selectedLyricsModel.url())) var selectedLyricsModel: LyricsModelMetadata = .remi

        public init(
            me: Shared<Me>,
            reusePrompt: Prompt?,
            billingInfo: Shared<SubscriptionInfoResponse?>,
            didUseAppShortcut: Bool = false,
            isRemix: Bool? = nil
        ) {
            self._me = me
            self._billingInfo = billingInfo
            self.didUseAppShortcut = didUseAppShortcut // Used for metrics

            let sharedPrompt: Shared<BlendedCreatePrompt>

            var blendedCreatePrompt: BlendedCreatePrompt
            var reusePromptTab: Tab?
            // This isn't the cleanest experience right now—look into fixing this https://linear.app/sunomusic/issue/CRE8-310/fix-reuse-prompt-for-custom-and-simple-mode-distinction
            if let reusePrompt = reusePrompt {
                blendedCreatePrompt = BlendedCreatePrompt(from: reusePrompt)
                // If we're reusing a video or image, set the tab to scenes
                switch reusePrompt.generationType {
                case .video, .image:
                    reusePromptTab = .camera
                default:
                    reusePromptTab = .promptBuilder
                }
            } else {
                blendedCreatePrompt = BlendedCreatePrompt()
            }

            if let isRemix {
                blendedCreatePrompt.isRemix = isRemix
            }

            // Open the advanced options menu if reusing weirdness or style influence constraints
            if blendedCreatePrompt.slidersHaveChanges {
                blendedCreatePrompt.useAdvancedOptions = true
            }

            sharedPrompt = Shared(value: blendedCreatePrompt)
            self._prompt = sharedPrompt
            self.promptBuilder = .init(prompt: sharedPrompt)
            self.audio = .init(me: me)
            self.camera = .init(reusePrompt: reusePrompt, isRemix: isRemix)
            self.currentTab = reusePromptTab ?? lastUsedCreateTab
        }
    }

    @CasePathable
    public enum Action: BindableAction {
        @CasePathable
        public enum Internal {
            case getBillingInfo
            case billingInfoResponse(Result<SubscriptionInfoResponse, Error>)
            case generationResponse(BlendedCreatePrompt, Result<[Clip], Error>)
            case initializeClipResponse(Result<String, Error>)
            case storePromptForReuse(BlendedCreatePrompt, BlendedCreateClient.CreateType)
        }

        @CasePathable
        public enum View {
            case task
            case onAppear
            case showSubscriptions
            case showTooltips
            case showUsingAudioTooltip
            case showAudioUploadTermsTooltip
            case showAudioUploadTermsTooltipDelayed
            case showBrandedAlert(BrandedAlertStyle)

            case didTapCloseButton

            case reuseLastPromptTapped

            case didSelectTextCreateMode(BlendedCreatePrompt.CreateMode)

            public enum SwipeDirection {
                case left, right
            }

            case didSwipeOnView(SwipeDirection)
            case didTapSettingsButton
        }

        @CasePathable
        public enum Controls {
            case didTapCreateButton
            case didTapClearButton
            case didSelectTab(Tab)
        }

        // TODO: Move this into a pub-sub
        @CasePathable
        public enum Delegate {
            // TODO: Rename to something more apt
            // It's a duplicate with the same name in `.internal`
            case generationResponse(BlendedCreatePrompt, Result<[Clip], Error>)
            case startedGenerating
            case cancelOngoingVideoUploads
            case dismiss
        }

        @CasePathable
        public enum HCaptcha {
            case configure
            case fetchToken
            case setTokenIfNeeded(String?)
            case tokenGenerationFailed
        }

        /// Reducer Categories
        case `internal`(Internal)
        case view(View)
        case delegate(Delegate)
        case hCaptcha(HCaptcha)
        case controls(Controls)

        /// Child Reducers
        case destination(PresentationAction<Destination.Action>)
        case settings(PresentationAction<CreateClipSettings.Action>)
        case toastAction(ToastReducer.Action)
        case brandedAlertAction(BrandedAlert.Action)
        case promptBuilder(BlendedCreatePromptBuilder.Action)
        case camera(CreateClipCamera.Action)
        case audio(Audio.Action)
        case binding(BindingAction<State>)
    }

    @Dependency(\.dismiss) private var dismiss
    @Dependency(UserDefaultsClient.self) private var userDefaults
    @Dependency(APIClient.self) var apiClient
    @Dependency(APIClientV2.self) var apiClientV2
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(HCaptchaClient.self) private var hCaptchaClient
    @Dependency(BlendedCreateClient.self) var blendedCreateClient

    public init() {}

    private var promptBuilderReducer: some ReducerOf<Self> {
        Scope(state: \.promptBuilder, action: \.promptBuilder) {
            BlendedCreatePromptBuilder()
        }
    }

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Scope(state: \.toastState, action: \.toastAction) {
            ToastReducer()
        }
        Scope(state: \.brandedAlertState, action: \.brandedAlertAction) {
            BrandedAlert()
        }
        promptBuilderReducer
        Scope(state: \.camera, action: \.camera) {
            CreateClipCamera()
        }
        Scope(state: \.audio, action: \.audio) {
            Audio()
        }
        Reduce<State, Action> { state, action in
            // MARK: Helpers

            // Generation Helper function
            func _generate(createType: BlendedCreateClient.CreateType) -> Effect<Action> {
                return .run { [prompt = state.prompt, lyricsModel = state.selectedLyricsModel] send in
                    // `BlendedCreateClient` transforms the prompt after creation, so we take it and store it for reuse
                    let promptUsedForGeneration = blendedCreateClient.generate(prompt, createType: createType, lyricsModel: lyricsModel.externalKey)
                    await send(.internal(.storePromptForReuse(promptUsedForGeneration, createType)))
                    await send(.delegate(.startedGenerating))
                    await send(.delegate(.dismiss))
                }
            }
            // Audio clip initialization function
            func _initializeClipForAudio(uploadRequestId: String) -> Effect<Action> {
                return .run { send in
                    await send(.internal(.initializeClipResponse(.init(catching: {
                        try await blendedCreateClient.initializeClip(uploadRequestId: uploadRequestId)
                    }))))
                }
            }
            // Set and save tab
            // The persisted state is decoupled from the view state in order to prevent unwanted screen transitions when trying to persist tabs, such as while dismissing the create sheet.
            func _setTabCached(_ tab: Tab) {
                state.currentTab = tab
                state.$lastUsedCreateTab.withLock { $0 = tab }
            }

            switch action {
            // MARK: View

            case .view(.task):
                // Focus keyboard if on prompt builder
                if state.currentTab == .promptBuilder {
                    switch state.prompt.createMode {
                    case .simple:
                        state.promptBuilder.focusedField = .simple(.description)
                    case .custom:
                        state.promptBuilder.focusedField = .custom(.lyrics)
                    }
                }
                return .concatenate(
                    .merge(
                        .send(.hCaptcha(.configure)),
                        .send(.view(.showTooltips))
                    ),
                    .send(.internal(.getBillingInfo))
                )

            case .view(.onAppear):
                return .none

            case .view(.didTapCloseButton):
                return .send(.delegate(.dismiss))

            case .view(.showBrandedAlert(let style)):
                state.destination = .brandedAlert(.init(style: style))
                return .none

            case .view(.showSubscriptions):
                state.destination = .subscriptions(.init())
                return .none

            case .view(.showTooltips):
                guard state.currentTab == .audio else { return .none }
                if !state.hasSeenHowToUseAudio {
                    return .send(.view(.showUsingAudioTooltip))
                } else if !userDefaults.hasAcceptedAudioUploadTOS {
                    return .send(.view(.showAudioUploadTermsTooltip))
                }
                return .none

            case .view(.showUsingAudioTooltip):
                state.destination = .brandedAlert(.init(style: .listAlert(.preset(.useAudio))))
                state.$hasSeenHowToUseAudio.withLock { $0 = true }
                return .none

            case .view(.didSelectTextCreateMode(let mode)):
                state.$prompt.withLock {
                    $0.createMode = mode
                    // Determine the lyrics mode from the text entry states
                    switch mode {
                    case .simple:
                        // Simple mode should change to "Auto" if we're in "Write" and have no lyrics
                        if $0.lyricsMode == .write && $0.lyrics.isEmpty {
                            $0.lyricsMode = .auto
                        }

                    case .custom:
                        // Custom mode should change to "Write" if we're in "Auto" and have no description
                        if $0.lyricsMode == .auto && $0.description.isEmpty {
                            $0.lyricsMode = .write
                        }
                    }
                }
                state.promptBuilder.focusedField = nil
                return .none

            case .view(.showAudioUploadTermsTooltip):
                state.destination = .brandedAlert(.init(style: .singleButtonAlert(.preset(.audioUploadTerms))))
                return .none

            case .view(.showAudioUploadTermsTooltipDelayed):
                guard !userDefaults.hasAcceptedAudioUploadTOS else { return .none }
                return .run { send in
                    try await Task.sleep(for: .seconds(0.5))
                    await send(.view(.showAudioUploadTermsTooltip), animation: .default)
                }

            case .view(.reuseLastPromptTapped):
                guard let lastPrompt = state.lastUsedBlendedCreatePrompt else {
                    return .none
                }
                state.$prompt.withLock { $0 = lastPrompt }
                // Show the main tab
                _setTabCached(.promptBuilder)
                // If it's an audio create, show the correct uploaded state
                if state.prompt.isAudioCreate {
                    guard let clipId = state.prompt.clipId else {
                        assertionFailure("Clip ID missing while trying to reuse last audio create prompt.")
                        return .none
                    }
                    // TODO: This logic should probably be cleaned up. If we hit "Create" successfully, we shouldn't have to upload the prompt again...
                    state.promptBuilder.audioUploadState = .completed(.complete(id: clipId))
                }
                return .none

            case .view(.didSwipeOnView(let direction)):
                // Don't handle swipes if tab controls are hidden
                guard state.showTabControls else { return .none }
                guard let currentTabIndex = state.tabs.firstIndex(of: state.currentTab) else { return .none }
                switch direction {
                case .left:
                    guard currentTabIndex < state.tabs.count - 1 else { return .none }
                    _setTabCached(state.tabs[currentTabIndex + 1])

                case .right:
                    guard currentTabIndex > 0 else { return .none }
                    _setTabCached(state.tabs[currentTabIndex - 1])
                }
                // Dismiss keyboard
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                return .none

            case .view(.didTapSettingsButton):
                // Need to dismiss keyboard here or else SwiftUI will try to set the focus back to its previous value after the sheet dismisses.
                state.promptBuilder.focusedField = nil
                state.settings = .init()
                return .none

            // MARK: Internal

            case .internal(.getBillingInfo):
                return .run { send in
                    await send(.internal(.billingInfoResponse(Result(catching: { try await apiClientV2.getBillingInfo() }))))
                }

            case .internal(.billingInfoResponse(let result)):
                switch result {
                case .success(let billingInfo):
                    state.$billingInfo.withLock { $0 = billingInfo }
                    sunoModelClient.setUserAccess(billingInfo.sunoModelUserAccess)
                    return .run { [userId = state.me.user.id] _ in
                        // Sets available models
                        await sunoModelClient.configureWithSubscriptionInfoResponse(userId: userId, response: billingInfo)
                    }

                case .failure:
                    break
                }
                return .none

            case let .internal(.generationResponse(prompt, result)):
                // Let parent handle loading state and manage
                // hCaptcha token errors
                if case .failure(let error) = result,
                   let apiError = error.underlyingApiError,
                   apiError == .invalidHCaptchaToken
                {
                    //                    state.isSubmitting = true
                } else {
                    //                    state.isSubmitting = false
                }
                return .send(.delegate(.generationResponse(prompt, result)))
                    .merge(with: .send(.delegate(.dismiss)))

            case .internal(.initializeClipResponse(let result)):
                switch result {
                case .success(let clipId):
                    state.$prompt.withLock {
                        $0.clipId = clipId
                    }
                    return _generate(createType: .audio(clipId: clipId))

                case .failure(let error):
                    return .send(.toastAction(.show(.warning(L10n.FeatureCreateClip.error, .string(error.underlyingError)))))
                }

            case .internal(.storePromptForReuse(let prompt, let createType)):
                state.$lastUsedBlendedCreatePrompt.withLock { $0 = prompt }
                // Also store the last used create tab
                // Not using helper function in order to prevent unwanted screen transitions
                state.$lastUsedCreateTab.withLock {
                    switch createType {
                    case .textOnly:
                        $0 = .promptBuilder
                    case .audio:
                        $0 = .audio
                    }
                }
                return .none

            // MARK: Camera

            // !! NOTE: Most of these camera actions are legacy. We barely touch the camera creation pipeline

            case .camera(.delegate(.submitVideo)):
                return .run { _ in await dismiss() }

            case .camera(.delegate(.showAlert(let style))),
                 .camera(.video(.delegate(.showAlert(let style)))):
                state.destination = .brandedAlert(.init(style: style))
                return .none

            case let .camera(.delegate(.generationResponse(prompt, .success(clips)))):
                // Reset hCaptcha retry count if successful
                state.hCaptchaRetryCount = 0
                for clip in clips {
                    state.$savedPrompts.withLock { $0[clip.id] = prompt }
                }
                // Remove our last used blended create prompt to avoid confusion
                state.$lastUsedBlendedCreatePrompt.withLock {
                    $0 = nil
                }
                // Set the last used tab to scenes
                _setTabCached(.camera)
                UINotificationFeedbackGenerator().notificationOccurred(.success)
                return .concatenate(
                    .send(.delegate(.generationResponse(state.prompt, .success(clips)))),
                    .run { _ in await dismiss() }
                )

            case .camera(.delegate(.generationResponse(_, .failure(let error)))):
                if let error = error as? OpenAPIRuntime.ClientError, let apiError = error.underlyingError as? APIError {
                    switch apiError {
                    case .insufficientCredits:
                        let message = apiError.insufficientCreditsMessage(billingInfo: state.billingInfo)
                        return .send(.toastAction(.show(.warning(nil, .string(message), destination: .paywall))))

                    case .tooManyRunningJobs:
                        state.destination = .alert(.init(
                            title: { TextState(L10n.FeatureCreateClip.capacityTitle) },
                            actions: {
                                ButtonState(action: .upgrade) { TextState(L10n.FeatureCreateClip.upgrade) }
                                ButtonState(role: .cancel) { TextState(L10n.FeatureCreateClip.cancel) }
                            },
                            message: { TextState(L10n.FeatureCreateClip.capacityMessage) }
                        ))
                        return .none

                    case .clientError, .serverError:
                        // logger.log(error)
                        if case .camera = action {
                            return .none
                        } else {
                            return .send(.toastAction(.show(.warning(L10n.FeatureCreateClip.error, .string(error.underlyingError)))))
                        }

                    case .invalidHCaptchaToken:
                        let isRetry = state.hCaptchaRetryCount != 0
                        let tokenValidationFailureCount = state.tokenValidationFailureCount
                        let userId = state.me.user.id
                        let error = HCaptchaError.invalidToken("isRetry: \(isRetry), errorWithRetryCount: \(tokenValidationFailureCount), userId: \(userId)")
                        log.telemetry.error(error)
                        if state.hCaptchaRetryCount < 1 {
                            state.hCaptchaRetryCount += 1
                            return .send(.hCaptcha(.fetchToken))
                        } else {
                            state.hCaptchaRetryCount = 0
                            state.tokenValidationFailureCount += 1
                            if state.tokenValidationFailureCount >= state.maxTokenValidationFailures {
                                return .merge(
                                    .send(.hCaptcha(.tokenGenerationFailed)),
                                    .send(.toastAction(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.contactUsMessage)))))
                                )
                            } else {
                                return .merge(
                                    .send(.hCaptcha(.tokenGenerationFailed)),
                                    .send(.toastAction(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.tryAgain)))))
                                )
                            }
                        }

                    case .errorMessage, .forbidden:
                        return .none
                    }

                } else if error is AudioRecorderError {
                    state.destination = .alert(.init(
                        title: { TextState(L10n.FeatureCreateClip.recordingFailed) }
                    ))
                    return .none

                } else {
                    log.telemetry.error(error)
                    return .send(.toastAction(.show(.warning(L10n.FeatureCreateClip.error, .string(error.underlyingError)))))
                }

            // MARK: Audio

            case .audio(.delegate(.showHowToUseAudio)):
                state.destination = .brandedAlert(.init(style: .listAlert(.preset(.useAudio))))
                return .none

            case .audio(.delegate(.showAudioUploadTermsTooltip)):
                return .send(.view(.showAudioUploadTermsTooltip))

            case .audio(.delegate(.handleAudioRecording(let audioRecording))):
                // Make sure to reset the Audio Reducer
                state.audio.destination = nil
                state.$prompt.withLock {
                    $0.audioRecording = audioRecording
                }
                _setTabCached(.promptBuilder)
                // Pass the audio recording to the promptBuilder to handle upload
                return promptBuilderReducer.reduce(into: &state, action: .promptBuilder(.audio(.uploadAudioRecording(audioRecording))))

            case .audio(.delegate(.handleUseExistingClip(let clip, let downloadedUrl))):
                // Make sure to reset the Audio Reducer
                state.audio.destination = nil
                // Set up prompt and audio recording
                state.$prompt.withLock {
                    // Extract clip medatadata
                    $0.title = clip.title
                    $0.lyrics = clip.prompt
                    $0.styleText = clip.tags
                    // TODO: Add exclude styles to `Clip` and transfer that metadata here
                    $0.description = clip.gptDescriptionPrompt
                    // Create an `AudioRecording` from clip
                    $0.audioRecording = .init(date: clip.createdAt ?? Date(), duration: clip.duration, title: clip.title, url: downloadedUrl)
                    $0.clipId = clip.id.remoteId
                }
                _setTabCached(.promptBuilder)
                // Make sure to set the upload state to completed
                state.promptBuilder.audioUploadState = .completed(.complete(id: clip.id.remoteId, imageUrl: clip.imageUrl))
                return .none

            case .audio(.delegate(.didPressAudioLengthUpsell)):
                return .send(.view(.showSubscriptions))

            // MARK: Destination

            case .destination(.presented(.alert(.upgrade))):
                return .send(.view(.showSubscriptions))

            case .destination(.presented(.subscriptions(.billingInfoResult(let result)))):
                return .send(.internal(.billingInfoResponse(result)))

            case .destination(.presented(.brandedAlert(.delegate(.didTriggerWithIntentionToUpgrade)))):
                UIImpactFeedbackGenerator(style: .medium).impactOccurred()
                state.destination = nil
                return .send(.view(.showSubscriptions))

            case .destination(.presented(.brandedAlert(.destination(.presented(.listAlert(.delegate(.tappedButton))))))):
                return .send(.view(.showAudioUploadTermsTooltipDelayed))

            case .destination(.presented(.brandedAlert(.dismissAlert(let style)))):
                guard case .listAlert = style else { return .none }
                return .send(.view(.showAudioUploadTermsTooltipDelayed))

            case .destination(.presented(.brandedAlert(.destination(.presented(.singleButtonAlert(.delegate(.tappedButton))))))):
                return .run { _ in await userDefaults.setHasAcceptedAudioUploadTOS(true) }

            // MARK: HCaptcha

            case .hCaptcha(.configure):
                guard state.requiresTokenToGenerate else { return .none }
                hCaptchaClient.prepareToken()
                return .none

            case .hCaptcha(.fetchToken):
                return .run { [requiresTokenToGenerate = state.requiresTokenToGenerate] send in
                    do {
                        if requiresTokenToGenerate {
                            let token = try await hCaptchaClient.getToken()
                            await send(.hCaptcha(.setTokenIfNeeded(token)))
                        } else {
                            await send(.hCaptcha(.setTokenIfNeeded(nil)))
                        }
                    } catch {
                        // logger.log(error)
                        await send(.hCaptcha(.tokenGenerationFailed))
                        await send(.toastAction(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.tryAgain)))))
                    }
                }

            case .hCaptcha(.tokenGenerationFailed):
                switch state.currentTab {
                case .audio:
                    return .none

                case .camera:
                    switch state.camera.media {
                    case .image:
                        return .send(.camera(.image(.hCaptchaTokenGenerationFailed)))
                    case .video:
                        return .send(.camera(.video(.hCaptchaTokenGenerationFailed)))
                    default:
                        return .none
                    }

                case .promptBuilder:
                    return .none
                }

            case .hCaptcha(.setTokenIfNeeded(let hCaptchaToken)):
                switch state.currentTab {
                case .promptBuilder:
                    var prompt = state.prompt
                    prompt.token = hCaptchaToken
                    // If it's audio create, we need `Prompt` to have a clip ID. If we're missing that, we need to generate it with our uploadRequestId
                    if prompt.isAudioCreate {
                        // If the prompt already has a clip ID, generate with that
                        if let clipId = prompt.clipId {
                            return _generate(createType: .audio(clipId: clipId))
                        }
                        // Otherwise, we need to generate one w/ audioUploadRequestId
                        guard let uploadRequestId = state.prompt.audioUploadRequestId else {
                            assertionFailure("Cannot generate with audio while missing an upload request ID.")
                            return .none
                        }
                        return _initializeClipForAudio(uploadRequestId: uploadRequestId)
                    } else {
                        return _generate(createType: .textOnly)
                    }

                case .audio:
                    return .none

                case .camera:
                    switch state.camera.media {
                    case .image:
                        return .send(.camera(.submitImage(hCaptchaToken)))
                    case .video:
                        return .send(.camera(.submitVideo(hCaptchaToken)))
                    default:
                        return .none
                    }
                }

            // MARK: Controls

            case .controls(.didTapCreateButton),
                 .camera(.image(.didTapCreate)),
                 .camera(.video(.didTapCreate)):
                return .send(.hCaptcha(.fetchToken))

            case .controls(.didTapClearButton),
                 .promptBuilder(.delegate(.reset)):
                // Decide on which tab we're going to and handle any outstanding cleanup
                if state.prompt.isAudioCreate {
                    _setTabCached(.audio)
                    // Reset `AudioUploadState`
                    state.promptBuilder.audioUploadState = .uploading(progress: 0)
                } else {
                    _setTabCached(.promptBuilder)
                }
                state.$prompt.withLock {
                    // Delete any audio recording if it exists
                    $0.deleteAudioRecording()
                    var emptyPrompt = BlendedCreatePrompt() // Reset to a new prompt
                    emptyPrompt.createMode = state.prompt.createMode // Retain create mode
                    $0 = emptyPrompt // Replace the current prompt with an empty one
                }
                state.promptBuilder.focusedField = nil
                return .none

            case .controls(.didSelectTab(let tab)):
                _setTabCached(tab)
                if tab != .promptBuilder {
                    // Dismiss keyboard
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                }
                return .none

            // MARK: Promptbuilder

            case .promptBuilder(.delegate(.didSelectLyricsMode(let mode))):
                state.$prompt.withLock { $0.lyricsMode = mode }
                return .none

            case .settings(.presented(.delegate(.showUpgradeAlert(let marketingLevelUnderstanding)))):
                // Dismiss settings
                state.settings = nil
                switch marketingLevelUnderstanding {
                case .previousToV4, .v3Dot5:
                    assertionFailure("Should not `showUpgradeAlert` for free models.")
                case .v4:
                    state.destination = .brandedAlert(.init(style: .versioningAlert(.preset(.v4FreeUserUpgradeInfoPush))))
                case .auk:
                    state.destination = .brandedAlert(.init(style: .versioningAlert(.preset(.v4_5FreeUserUpgradeInfoPush))))
                case .bluejay:
                    state.destination = .bluejayAnnouncement(.init(style: .free))
                case .v5:
                    state.destination = .v5Announcement(.init(style: .free))
                }
                return .none

            case .destination(.presented(.bluejayAnnouncement(.delegate(.openSubscriptions)))):
                state.destination = .subscriptions(.init())
                return .none

            case .destination(.presented(.v5Announcement(.delegate(.openSubscriptions)))):
                state.destination = .subscriptions(.init())
                return .none

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

            case .promptBuilder,
                 .camera,
                 .audio,
                 .destination,
                 .settings,
                 .toastAction,
                 .brandedAlertAction,
                 .delegate,
                 .binding,
                 .controls:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .ifLet(\.$settings, action: \.settings) {
            CreateClipSettings()
        }
        Analytics()
    }
}

public struct BlendedCreateViewV4: View {
    @Bindable var store: StoreOf<BlendedCreate>
    @Namespace private var namespace

    let promptBuilderStore: StoreOf<BlendedCreatePromptBuilder>
    let cameraStore: StoreOf<CreateClipCamera>
    let audioStore: StoreOf<Audio>

    public init(store: StoreOf<BlendedCreate>) {
        self.store = store
        self.promptBuilderStore = store.scope(state: \.promptBuilder, action: \.promptBuilder)
        self.cameraStore = store.scope(state: \.camera, action: \.camera)
        self.audioStore = store.scope(state: \.audio, action: \.audio)
    }

    public var body: some View {
        NavigationStack {
            blendedCreateViewStack
                .safeAreaInset(edge: .bottom) {
                    VStack {
                        if store.showControls {
                            BlendedCreateControls(
                                store: store
                            )
                            .transition(.move(edge: .bottom).combined(with: .opacity))
                            .animation(.snappy, value: store.showControls)
                        }

                        if store.showUseLastPromptBanner {
                            reusePromptBanner
                                .transition(.move(edge: .bottom).combined(with: .opacity))
                                .animation(.bouncy(duration: 0.25), value: store.showUseLastPromptBanner)
                        }
                    }
                }
        }
        .scrollDismissesKeyboard(.immediately)
        .task { store.send(.view(.task)) }
        .onAppear { store.send(.view(.onAppear)) }
        .sheet(item: $store.scope(state: \.settings, action: \.settings)) { store in
            CreateClipSettingsSheet(store: store)
        }
        .fullScreenCover(item: $store.scope(state: \.destination?.subscriptions, action: \.destination.subscriptions)) { paywallStore in
            NavigationStack {
                PaywallScreenV1(store: paywallStore)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading) {
                            ToolbarButton(.close, background: Material.ultraThin) {
                                paywallStore.send(.dismiss)
                            }
                        }
                    }
            }
        }
        .overlay {
            if let store = store.scope(state: \.destination?.brandedAlert, action: \.destination.brandedAlert) {
                BrandedAlertView(store)
                    .transaction { transaction in
                        // Don't animate branded alerts for BlendedCreate
                        // They're really sluggish
                        transaction.animation = nil
                    }
            }
        }
        .overlay {
            if let store = store.scope(state: \.destination?.bluejayAnnouncement, action: \.destination.bluejayAnnouncement) {
                BluejayAnnouncementView(store: store)
            }
        }
        .overlay {
            if let store = store.scope(state: \.destination?.v5Announcement, action: \.destination.v5Announcement) {
                V5AnnouncementView(store: store)
            }
        }
    }

    @ViewBuilder
    private var backgroundView: some View {
        GeometryReader { geometry in
            ZStack {
                Color.SemanticV2.backgroundPrimary

                // Having two large Metal renders seems to impact camera recording FPS, so we disable the shader view on the camera tab
                if store.currentTab != .camera {
                    ZStack {
                        #if targetEnvironment(simulator)
                            Color.orange
                        #else
                            BlendedCreateAuraView()
                        #endif
                    }
                    // Custom inverted elliptical vignette mask
                    .mask {
                        let width = geometry.size.width
                        let height = geometry.size.height
                        Capsule()
                            .foregroundStyle(.black)
                            .blur(radius: 65)
                            .frame(width: max(0, width), height: height * 0.5)
                            .position(x: width * 0.5, y: 0 - (height * 0.1))
                    }
                    .ignoresSafeArea(.container)
                }
            }
        }
        .ignoresSafeArea()
    }

    @ViewBuilder
    private var blendedCreateViewStack: some View {
        ZStack {
            backgroundView

            tabContentView
                .animation(.linear, value: store.currentTab)
                .navigationBarTitleDisplayMode(.inline)
                .padding(.horizontal, 10)
                .toolbar {
                    if store.currentTab != .camera {
                        ToolbarItem(placement: .topBarLeading) {
                            ToolbarButton(.settings, image: Image.Icon.cog, color: Color.SemanticV1.iconPrimary) {
                                store.send(.view(.didTapSettingsButton))
                            }
                        }
                    }

                    if store.currentTab == .promptBuilder {
                        if store.prompt.audioRecording == nil {
                            ToolbarItem(placement: .principal) {
                                createModePicker
                                    .padding(.top, 5)
                            }
                        } else {
                            ToolbarItem(placement: .principal) {
                                Text("Audio")
                                    .typographyV1(.subtitleMedium)
                                    .foregroundStyle(Color.SemanticV2.foregroundPrimary)
                            }
                        }
                    }

                    ToolbarItem(placement: .topBarTrailing) {
                        ToolbarButton(.close, image: Image.Icon.close, color: Color.SemanticV1.iconPrimary) {
                            store.send(.view(.didTapCloseButton))
                        }
                    }
                }
                .toolbarBackground(.hidden, for: .navigationBar)
        }
    }

    @ViewBuilder
    private var tabContentView: some View {
        // Custom implementation rather than TabView because our TabView overrides prevent transparent backgrounds
        GeometryReader { geometry in
            ZStack {
                // We have trouble keeping all three of these in memory without a significant performance hit—especially the camera view. Keep this switch statement around
                switch store.currentTab {
                case .promptBuilder:
                    BlendedCreatePromptBuilderView(store: promptBuilderStore)
                        .opacity(store.currentTab == .promptBuilder ? 1 : 0)
                        .transition(.opacity)

                case .camera:
                    CreateClipCameraView(store: cameraStore)
                        .opacity(store.currentTab == .camera ? 1 : 0)
                        .transition(.opacity)

                case .audio:
                    AudioView(store: audioStore)
                        .opacity(store.currentTab == .audio ? 1 : 0)
                        .transition(.opacity)
                }
            }
            .gesture(
                DragGesture()
                    .onEnded { value in
                        let threshold = geometry.size.width * 0.333
                        if value.translation.width > threshold {
                            store.send(.view(.didSwipeOnView(.right)), animation: .default)
                        } else if value.translation.width < -threshold {
                            store.send(.view(.didSwipeOnView(.left)), animation: .default)
                        }
                    }
            )
        }
    }

    let createModes: [BlendedCreatePrompt.CreateMode] = [.simple, .custom]

    private var createModePicker: some View {
        HStack(spacing: 10) {
            ForEach(createModes) { mode in
                Button {
                    UISelectionFeedbackGenerator().selectionChanged()
                    store.send(.view(.didSelectTextCreateMode(mode)))
                } label: {
                    let title: String = switch mode {
                    case .simple:
                        L10n.FeatureCreateClip.simple
                    case .custom:
                        L10n.FeatureCreateClip.custom
                    }

                    Text(title)
                        .typographyV1(.subtitleLarge)
                        .foregroundStyle(store.prompt.createMode == mode ? Color.SemanticV2.foregroundPrimary : Color.SemanticV2.foregroundSecondary)
                        .blendMode(.luminosity)
                        .contentShape(.rect)
                }
                .padding(.bottom, 4)
                .buttonStyle(.plain)
                .matchedGeometryEffect(id: mode, in: namespace)
            }
        }
        .overlay {
            Color.SemanticV2.foregroundPrimary
                .frame(height: 1)
                .frame(maxHeight: .infinity, alignment: .bottom)
                .matchedGeometryEffect(id: store.prompt.createMode, in: namespace, isSource: false)
        }
        .buttonStyle(.plain)
        .animation(.default.speed(1.5), value: store.prompt.createMode)
    }

    public var reusePromptBanner: some View {
        Button {
            store.send(.view(.reuseLastPromptTapped), animation: .default)
            UIImpactFeedbackGenerator(style: .medium).impactOccurred()
        } label: {
            HStack(alignment: .center) {
                Image.Icon.promptStar
                    .foregroundStyle(Color.SemanticV2.backgroundPrimary)

                Text(L10n.FeatureCreateClip.useLastPrompt)
                    .typographyV1(.caption)
                    .foregroundStyle(Color.SemanticV2.backgroundPrimary)
            }
            .frame(height: 50)
            .frame(maxWidth: .infinity)
            .background {
                Color.SemanticV2.foregroundPrimary
                    .clipShape(UnevenRoundedRectangle(topLeadingRadius: 16.0, topTrailingRadius: 16.0))
                    .ignoresSafeArea(.all, edges: .bottom)
            }
        }
    }
}
