import APIClient
import AVFoundation
import BlendedCreateClient
import ComponentLibrary
import ComposableArchitecture
import Foundation
import Localization
import PlayerClient
import StatsigClient
import SunoModelClient
import SwiftUI
import UserDefaultsClient
import Utilities

// swiftlint:disable file_length

@Reducer
public struct BlendedCreatePromptBuilder {
    public enum FocusedField: Hashable {
        case simple(Simple)
        case custom(Custom)

        public enum Simple: Hashable {
            case description, lyrics
        }

        public enum Custom: Hashable {
            case lyrics, styles, excludeStyles, advancedOptions
        }
    }

    // This is used for audio
    // Could probably use some renaming
    @Reducer(state: .equatable)
    public enum Destination {
        case deletionAlert(AlertState<DeletionAlert>)
        case createStyle(AudioCreateStyleSelection)

        public enum DeletionAlert {
            case confirm
        }
    }

    @ObservableState
    public struct State: Equatable {
        @Shared var prompt: BlendedCreatePrompt
        @Presents public var destination: Destination.State?
        @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?

        public var focusedField: FocusedField?
        var isGeneratingLyrics: Bool = false

        public var selectedDescriptionTags: [String] = []
        public var recommendedDescriptionTags: [String] = []
        public var selectedStyles: [String] = []
        public var recommendedStyles: [String] = []

        var generateLyricsButtonText: String {
            if isGeneratingLyrics {
                return L10n.FeatureCreateClip.writingLyrics
            } else if prompt.lyrics.isEmpty {
                return L10n.FeatureCreateClip.generateLyrics
            } else {
                return L10n.FeatureCreateClip.generateNewLyrics
            }
        }

        // MARK: - Character Limits

        let defaultCharCountLimit: Int = 200
        let defaultLyricsCharCountLimit: Int = 3000
        let charCountDisplayThreshold: Float = 0.75

        var descriptionCharCountLimit: Int {
            selectedSunoModel.maxLengths?.gptDescriptionPrompt ?? defaultCharCountLimit
        }

        var lyricsCharCountLimit: Int {
            selectedSunoModel.maxLengths?.prompt ?? defaultLyricsCharCountLimit
        }

        var stylesCharCountLimit: Int {
            selectedSunoModel.maxLengths?.tags ?? defaultCharCountLimit
        }

        var excludeStylesCharCountLimit: Int {
            selectedSunoModel.maxLengths?.negativeTags ?? defaultCharCountLimit
        }

        var titleCharCountLimit: Int {
            selectedSunoModel.maxLengths?.title ?? defaultCharCountLimit
        }

        var showSliders: Bool {
            billingInfo?.plan != nil
        }

        // MARK: - Audio

        public var audioUploadState: AudioUploadState = .uploading(progress: 0)
        public var isPlayButtonDisabled = true
        public var isPlaying = false
        public var elapsedTime: CMTime = .zero
        public var clipDuration: String {
            Duration.seconds(elapsedTime != .zero ? elapsedTime.seconds : prompt.audioRecording?.duration ?? 0)
                .formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2, roundFractionalSeconds: .down)))
        }

        public var imageUrl: String? {
            guard case .completed(let status) = audioUploadState else { return nil }
            return status.imageUrl
        }

        public init(prompt: Shared<BlendedCreatePrompt>) {
            self._prompt = prompt
        }
    }

    public enum Action: BindableAction {
        @CasePathable
        public enum Delegate {
            case didTapCustomLyricsSegment
            case didSelectLyricsMode(BlendedCreatePrompt.LyricsMode)
            case reset
        }

        @CasePathable
        public enum Internal {
            // probably move these to a function :/
            case generateLyricsResponse(Result<String, Error>)
            case updateLyrics(GeneratedLyrics)
        }

        @CasePathable
        public enum Simple {
            case task

            case didToggleWriteLyricsExpanded
            case didTapGenerateLyrics
            case updateLyrics(GeneratedLyrics)
            case recommendedDescriptionTagsResponse(Result<Styles, Error>)
            case didSelectDescriptionTag(String)
            case didTapRefreshRecommendedDescriptionTags
            case didTapGetRandomDescription
        }

        @CasePathable
        public enum Custom {
            case task

            // Lyrics
            case didTapGenerateLyrics
            case updateLyrics(GeneratedLyrics)
            case didTapLyricsBox

            // Styles
            case recommendedStylesResponse(Result<Styles, Error>)
            case didSelectStyle(String)
            case didTapExpandButton
            case didTapRefreshRecommendedStyleTags

            // Advanced Options
            case didTapAdvancedOptionsExpandButton
            case didTapResetSliders
            case didDoubleTapWeirdnessSlider
            case didDoubleTapStyleWeightSlider
            case didDoubleTapAudioWeightSlider
        }

        @CasePathable
        public enum Audio {
            case initializeClipResponse(Result<String?, Error>)
            case audioSetupCompletion(AudioRecording, Result<Void, Error>)
            case audioReachedEndTime
            case periodicTimeResponse(CMTime)

            case audioUploadEventStream(BlendedCreateClient.AudioUploadStatus)
            case audioUploadError(Error)

            case didTapTogglePlay
            case didTapDeleteClip

            case uploadAudioRecording(AudioRecording)
            case didTapCreateStyleButton
        }

        case binding(BindingAction<State>)
        case delegate(Delegate)
        case `internal`(Internal)

        case simple(Simple)
        case custom(Custom)
        case audio(Audio)

        case destination(PresentationAction<Destination.Action>)
    }

    public init() {}

    @Dependency(APIClient.self) private var apiClient
    @Dependency(\.continuousClock) private var clock
    @Dependency(PlayerClient.self) private var playerClient
    @Dependency(\.mainQueue) private var mainQueue
    @Dependency(BlendedCreateClient.self) private var blendedCreateClient

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce<State, Action> { state, action in
            struct CheckCompletionCancellableId: Hashable {}
            struct AudioUploadCancellable: Hashable {}
            struct DidPlayToEndTimeCancellableId: Hashable {}
            switch action {
            // MARK: - Internal

            case .internal(.generateLyricsResponse(.success(let requestId))):
                return .run { send in
                    await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                        for await _ in clock.timer(interval: .seconds(3)) {
                            do {
                                let generated = try await apiClient.getGeneratedLyrics(requestId)
                                guard generated.status == "complete" else { continue }
                                await send(.internal(.updateLyrics(generated)))

                            } catch {
                                await send(.internal(.generateLyricsResponse(.failure(error))))
                            }
                        }
                    }
                }

            case .internal(.generateLyricsResponse(.failure(let error))):
                log.telemetry.error(error)
                state.isGeneratingLyrics = false
                UINotificationFeedbackGenerator().notificationOccurred(.error)
                return .cancel(id: CheckCompletionCancellableId())

            case .internal(.updateLyrics(let generated)):
                state.$prompt.withLock { $0.lyrics = generated.text }
                state.isGeneratingLyrics = false
                UINotificationFeedbackGenerator().notificationOccurred(.success)
                return .cancel(id: CheckCompletionCancellableId())

            // MARK: - Simple Mode

            case .simple(.task):
                return self.getRecommendedDescriptionTags(state: &state)

            case .simple(.didToggleWriteLyricsExpanded):
                state.$prompt.withLock {
                    $0.lyricsMode = $0.lyricsMode == .write ? .auto : .write
                }
                if state.prompt.lyricsMode == .write {
                    state.focusedField = .simple(.lyrics)
                }
                return .none

            case .simple(.didTapGenerateLyrics):
                state.isGeneratingLyrics = true
                return .run { [promptLyrics = state.prompt.lyrics] send in
                    await send(.internal(.generateLyricsResponse(Result(catching: { try await apiClient.generateLyrics(promptLyrics) }))))
                }

            case .simple(.recommendedDescriptionTagsResponse(.success(let styles))):
                state.recommendedDescriptionTags = styles.recommendedStyles
                return .none

            case .simple(.recommendedDescriptionTagsResponse(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .simple(.didSelectDescriptionTag(let style)):
                state.$prompt.withLock {
                    $0.description.append($0.description.isEmpty ? style : ", " + style)
                }
                state.selectedDescriptionTags.append(style)
                return self.getRecommendedDescriptionTags(state: &state, exclude: state.selectedDescriptionTags)

            case .simple(.didTapRefreshRecommendedDescriptionTags):
                return self.getRecommendedDescriptionTags(state: &state, exclude: state.recommendedDescriptionTags)

            case .simple(.didTapGetRandomDescription):
                state.$prompt.withLock {
                    $0.description = Prompt().getRandomPrompt()
                }
                return .none

            // MARK: - Custom Mode

            case .custom(.task):
                return self.getRecommendedStyles(state: &state)

            case .custom(.didTapGenerateLyrics):
                state.isGeneratingLyrics = true
                return .run { [promptLyrics = state.prompt.lyrics] send in
                    await send(.internal(.generateLyricsResponse(Result(catching: { try await apiClient.generateLyrics(promptLyrics) }))))
                }

            case .custom(.didTapLyricsBox):
                if state.prompt.lyricsMode == .auto {
                    state.$prompt.withLock {
                        // Switch to write mode when tapping the lyrics box in auto mode
                        $0.lyricsMode = .write
                    }
                }
                return .none

            case .custom(.recommendedStylesResponse(.success(let styles))):
                state.recommendedStyles = styles.recommendedStyles
                return .none

            case .custom(.recommendedStylesResponse(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .custom(.didSelectStyle(let style)):
                state.$prompt.withLock { $0.styleText.append($0.styleText.isEmpty ? style : ", " + style) }
                state.selectedStyles.append(style)
                return .none

            case .custom(.didTapExpandButton):
                state.$prompt.withLock {
                    $0.useNegativeTags.toggle()
                }
                return .none

            case .custom(.didTapRefreshRecommendedStyleTags):
                return getRecommendedStyles(state: &state, exclude: state.recommendedStyles)

            case .custom(.didTapAdvancedOptionsExpandButton):
                state.$prompt.withLock {
                    $0.useAdvancedOptions.toggle()
                    $0.useNegativeTags.toggle()
                }
                return .none

            case .custom(.didTapResetSliders):
                state.$prompt.withLock {
                    $0.weirdnessConstraint = BlendedCreatePrompt.defaultSliderValue
                    $0.styleWeight = BlendedCreatePrompt.defaultSliderValue
                    $0.audioWeight = BlendedCreatePrompt.defaultSliderValue
                }
                return .none

            case .custom(.didDoubleTapWeirdnessSlider):
                state.$prompt.withLock {
                    $0.weirdnessConstraint = BlendedCreatePrompt.defaultSliderValue
                }
                return .none

            case .custom(.didDoubleTapStyleWeightSlider):
                state.$prompt.withLock {
                    $0.styleWeight = BlendedCreatePrompt.defaultSliderValue
                }
                return .none

            case .custom(.didDoubleTapAudioWeightSlider):
                state.$prompt.withLock {
                    $0.audioWeight = BlendedCreatePrompt.defaultSliderValue
                }
                return .none

            // MARK: - Audio

            case .audio(.uploadAudioRecording(let audioRecording)):
                return .merge(
                    .run { send in
                        await withTaskCancellation(id: AudioUploadCancellable()) {
                            do {
                                for try await event in blendedCreateClient.uploadAudio(audioRecording) {
                                    await send(.audio(.audioUploadEventStream(event)))
                                }
                            } catch {
                                await send(.audio(.audioUploadError(error)))
                            }
                        }
                    }
                    .cancellable(id: AudioUploadCancellable(), cancelInFlight: true),
                    .run { send in
                        await send(.audio(.audioSetupCompletion(audioRecording, Result(catching: { try playerClient.setup() }))))
                    }
                )

            case .audio(.audioUploadEventStream(let event)):
                switch event {
                case .uploadEvent(let event):
                    switch event {
                    case .updateProgress(let progress):
                        state.audioUploadState = .uploading(progress: progress)

                    case .success, .failure:
                        // Success: final state determined by .status events
                        // Failure: yielded as event then stream throws, handled via .audioUploadError action
                        return .none
                    }

                case .status(let uploadRequestStatus):
                    if let errorMessage = uploadRequestStatus.errorMessage {
                        state.audioUploadState = .error(errorMessage)
                        return .cancel(id: AudioUploadCancellable())
                    }
                    switch uploadRequestStatus.status {
                    case .complete:
                        state.audioUploadState = .completed(uploadRequestStatus)
                        // Set the prompt's upload ID, which is necessary for generate
                        state.$prompt.withLock {
                            $0.audioUploadRequestId = uploadRequestStatus.id
                        }
                        return .cancel(id: AudioUploadCancellable())

                    default:
                        state.audioUploadState = .checkingStatus(uploadRequestStatus)
                    }
                }
                return .none

            case .audio(.audioUploadError(let error)):
                log.telemetry.error(error)
                if let audioUploadError = error as? BlendedCreateClient.AudioUploadError {
                    switch audioUploadError {
                    case .fileCreationFailed:
                        state.audioUploadState = .error(L10n.FeatureCreateClip.createFileFailed)

                    case .statusCheckFailed:
                        state.audioUploadState = .error(L10n.FeatureCreateClip.statusCheckFailed)

                    case .verificationFailed:
                        state.audioUploadState = .error(L10n.FeatureCreateClip.verifyFailed)
                    }
                } else {
                    state.audioUploadState = .error(L10n.FeatureCreateClip.audioUploadFailed)
                }
                return .cancel(id: AudioUploadCancellable())

            case .audio(.audioSetupCompletion(let audioRecording, .success)):
                state.isPlayButtonDisabled = false
                return .concatenate(
                    .run { _ in
                        _ = await playerClient.replaceCurrentItem(audioRecording.url.absoluteString)
                    },
                    .publisher {
                        NotificationCenter.default.publisher(for: AVPlayerItem.didPlayToEndTimeNotification, object: nil)
                            .receive(on: mainQueue)
                            .map { _ in .audio(.audioReachedEndTime) }
                    }
                    .cancellable(id: DidPlayToEndTimeCancellableId(), cancelInFlight: true)
                )

            case .audio(.audioSetupCompletion(_, .failure(let error))):
                state.isPlayButtonDisabled = true
                log.telemetry.error(error)
                return .none

            case .audio(.audioReachedEndTime):
                state.isPlaying = false
                playerClient.pause()
                playerClient.seek(.zero)
                return .none

            case .audio(.periodicTimeResponse(let time)):
                state.elapsedTime = time
                return .none

            case .audio(.didTapDeleteClip):
                state.destination = .deletionAlert(.confirmDelete)
                return .none

            case .audio(.didTapCreateStyleButton):
                state.destination = .createStyle(.init(selectedStyle: state.prompt.audioCreateStyle, imageUrl: state.imageUrl))
                return .none

            case .audio(.didTapTogglePlay):
                if state.isPlaying {
                    playerClient.pause()
                } else {
                    playerClient.play()
                }
                state.isPlaying.toggle()
                return .publisher {
                    playerClient.periodicTime().map { .audio(.periodicTimeResponse($0)) }
                }

            // MARK: - Destination

            case .destination(.presented(.deletionAlert(.confirm))):
                state.destination = nil
                return .send(.delegate(.reset))

            case .destination(.presented(.createStyle(.selectStyle(let style)))):
                state.$prompt.audioCreateStyle.withLock { $0 = style }
                return .none

            case .delegate, .internal, .binding, .simple, .custom, .destination, .audio:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
    }

    private func getRecommendedDescriptionTags(state _: inout State, exclude: [String] = []) -> Effect<Action> {
        return .run { send in
            await send(.simple(.recommendedDescriptionTagsResponse(Result(catching: { try await apiClient.getRecommendedStyles(exclude) }))))
        }
    }

    private func getRecommendedStyles(state _: inout State, exclude: [String] = []) -> Effect<Action> {
        return .run { send in
            await send(.custom(.recommendedStylesResponse(Result(catching: { try await apiClient.getRecommendedStyles(exclude) }))))
        }
    }
}

public struct BlendedCreatePromptBuilderView: View {
    @Bindable var store: StoreOf<BlendedCreatePromptBuilder>

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

    public var body: some View {
        ZStack {
            SimpleModeView(store: store, getRandomPromptAction: {})
                .opacity(store.prompt.createMode == .simple ? 1 : 0)
                .transition(.opacity)

            CustomModeView(store: store)
                .opacity(store.prompt.createMode == .custom ? 1 : 0)
                .transition(.opacity)
        }
        .onDisappear { store.focusedField = nil }
        .alert($store.scope(state: \.destination?.deletionAlert, action: \.destination.deletionAlert))
        .sheet(item: $store.scope(state: \.destination?.createStyle, action: \.destination.createStyle)) { store in
            AudioCreateStyleSelectionView(store: store)
        }
    }
}

extension AlertState where Action == BlendedCreatePromptBuilder.Destination.DeletionAlert {
    static var confirmDelete: Self {
        Self {
            TextState(L10n.FeatureCreateClip.discard)
        } actions: {
            ButtonState(role: .cancel) {
                TextState(L10n.FeatureCreateClip.cancel)
            }
            ButtonState(role: .destructive, action: .confirm) {
                TextState(L10n.FeatureCreateClip.discard)
            }
        } message: {
            TextState(L10n.FeatureCreateClip.discardMessage)
        }
    }
}
