import APIClient
import ComponentLibrary
import ComposableArchitecture
import FeatureBrandedAlert
import Localization
import StatsigClient
import SunoModelClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct ExtendClip {
    @ObservableState
    public struct State: Equatable {
        public enum Field {
            case lyrics
            case styles
        }

        let clip: Clip
        let prompt: Prompt
        var waveformData: [Float] = []
        var lyrics: String = ""
        var styles: String = ""
        var text: String = ""
        var focusedField: Field?
        var instrumental: Bool = false
        var isSubmitting: Bool = false
        var isLyricsEditorShown: Bool = false
        var extendTimestamp: Double
        var hasShownBrandedAlert: Bool = false

        var recommendedStyles: [String] = []
        var selectedStyles: [String] = []

        @ObservationStateIgnored @ObservedBox public var waveform: WaveformEditor.State
        @ObservationStateIgnored @ObservedBox public var lyricsSelection: LyricsSelection.State

        func appendingStyle(_ style: String, to keyPath: WritableKeyPath<State, String>) -> String {
            var value = self[keyPath: keyPath]
            value.append(value.isEmpty ? style : ", \(style)")
            return value
        }

        var hasChanges: Bool {
            lyrics != prompt.lyrics ||
                styles != prompt.styles ||
                instrumental != prompt.instrumental ||
                extendTimestamp != clip.duration
        }

        // MARK: - Character Limits

        @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault

        let defaultCharCountLimit: Int = 200
        let defaultLyricsCharCountLimit: Int = 3000

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

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

        public init(clip: Clip, me: Shared<Me>, prompt: Prompt) {
            self.clip = clip
            self.prompt = prompt
            self.lyricsSelection = LyricsSelection.State(
                lyrics: prompt.lyrics,
                lyricsSelectionRange: nil,
                highlightingStyle: .onlyCaptureStart
            )

            self.styles = prompt.styles
            self.waveform = .init(
                clip: clip,
                me: me,
                mode: .rightHandleFixed,
                clipDuration: clip.duration,
                startPointCaptionOverride: L10n.FeatureEditClip.extendFrom
            )
            self.extendTimestamp = clip.duration
        }
    }

    public enum Action: BindableAction {
        case task
        case clearAll
        case delegate(Delegate)
        case `internal`(Internal)
        case binding(BindingAction<State>)

        case getRecommendedStyles
        case recommendedStylesResponse(Result<Styles, Error>)

        case selectRandomStyle
        case selectStyle(String)
        case generate(_ token: String?)
        case generationResponse(Prompt, Result<[Clip], Error>)
        case hCaptchaTokenGenerationFailed
        case setStartPoint(Double)
        case dismissKeyboard
        case waveform(WaveformEditor.Action)
        case lyricsSelection(LyricsSelection.Action)
        case refreshHighlighting(TimeInterval)
        case showEditsWarningAboutLyrics

        public enum Delegate {
            case generationResponse(Prompt, Result<[Clip], Error>)
            case showBrandedAlertOverlay(BrandedAlertStyle)
        }

        public enum Internal {
            case loadAndPollForWaveform
            case loadWaveform
            case waveformLoadResponse(Result<[Float], Error>)
        }
    }

    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(\.continuousClock) private var clock

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Scope(state: \.waveform, action: \.waveform) {
            WaveformEditor()
        }
        Scope(state: \.lyricsSelection, action: \.lyricsSelection) {
            LyricsSelection()
        }
        Reduce { state, action in
            struct CheckCompletionCancellableId: Hashable {}
            switch action {
            case .task:
                return .merge(
                    .send(.getRecommendedStyles),
                    .send(.internal(.loadAndPollForWaveform))
                )

            case .clearAll:
                state.lyricsSelection.lyrics = state.prompt.lyrics
                state.styles = state.prompt.styles
                state.instrumental = state.prompt.instrumental
                return .send(.setStartPoint(state.clip.duration))

            case .getRecommendedStyles:
                return .run { [exclude = state.selectedStyles] send in
                    await send(.recommendedStylesResponse(Result(catching: { try await apiClient.getRecommendedStyles(exclude)
                    })))
                }

            case .recommendedStylesResponse(let result):
                switch result {
                case .success(let styles):
                    state.recommendedStyles = styles.recommendedStyles
                case .failure(let error):
                    log.telemetry.error(error)
                }
                return .none

            case .selectRandomStyle:
                guard let style = state.recommendedStyles.randomElement() else { return .none }
                return .send(.selectStyle(style))

            case .selectStyle(let style):
                let new = state.appendingStyle(style, to: \.styles)
                guard new.count <= state.stylesCharCountLimit else { return .none }
                state.styles = new
                return .send(.getRecommendedStyles)

            case .generate(let token):
                var lyricsForPrompt: String {
                    if state.instrumental {
                        return ""
                    } else if let replace = state.lyricsSelection.replaceText {
                        return replace
                    } else {
                        return state.lyricsSelection.getTextValueForSelection()
                    }
                }

                let prompt = Prompt(
                    title: state.clip.title,
                    lyrics: lyricsForPrompt,
                    instrumental: state.instrumental,
                    styles: state.styles,
                    text: state.text,
                    continueClipId: state.clip.id.remoteId,
                    continueAt: state.extendTimestamp,
                    includeHistory: state.prompt.includeHistory,
                    generationType: state.prompt.generationType,
                    token: token,
                    task: .extend,
                    isRemix: FeatureFlag.create.remixAndAttribution ? true : nil
                )

                return .run { send in
                    await send(.generationResponse(prompt, Result(catching: {
                        try await api.generateV2(prompt)
                    })))
                }

            case .waveform(.delegate(.didChangeStartPoint(let time))):
                state.extendTimestamp = time
                return .send(.refreshHighlighting(time))

            case .lyricsSelection(.delegate(.didUpdateLyrics(let lyricsText, let highlightTags))):
                return .concatenate(
                    .send(.lyricsSelection(.updateLyricsAndTimedTags(lyricsText, highlightTags))),
                    .send(.refreshHighlighting(state.extendTimestamp))
                )

            case .lyricsSelection(.delegate(.didTapReplaceLyrics)):
                state.isLyricsEditorShown = true
                return .none

            case .lyricsSelection(.delegate(.clearIsEditing)):
                state.isLyricsEditorShown = false
                return .none

            case .lyricsSelection(.delegate(.didTapClearReplaceLyrics)):
                state.hasShownBrandedAlert = false
                return .none

            case .lyricsSelection(.delegate(.didToggleInsturmental)):
                state.instrumental = !state.instrumental
                return .none

            case .waveform(.delegate(.isDraggingLeftHandle(let time))):
                if state.hasShownBrandedAlert {
                    state.hasShownBrandedAlert = false
                    return .merge(
                        .send(.lyricsSelection(.clearReplaceValue)),
                        .send(.refreshHighlighting(time))
                    )
                } else {
                    return .send(.refreshHighlighting(time))
                }

            case .waveform(.delegate(.didResetLeftHandle)):
                if state.hasShownBrandedAlert {
                    state.hasShownBrandedAlert = false
                    return .merge(
                        .send(.lyricsSelection(.clearReplaceValue)),
                        .send(.lyricsSelection(.clearToDefaults))
                    )
                } else {
                    return .send(.lyricsSelection(.clearToDefaults))
                }

            case let .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)))

            case .hCaptchaTokenGenerationFailed:
                state.isSubmitting = false
                return .none

            case .setStartPoint(let time):
                // Replace with delegate method from LyricsSelection, if needed
                state.extendTimestamp = time
                return .send(.waveform(.didSetStartPoint(time)))

            case .internal(.loadAndPollForWaveform):
                // Send to a separate action since the .task that calls this
                // merges the effect with another action
                return .send(.internal(.loadWaveform))

            case .internal(.loadWaveform):
                return .run { [clip = state.clip] send in
                    do {
                        let alignedLyrics = try await apiClient.getAlignedLyrics(clip)
                        let waveformData = alignedLyrics.waveformData
                        guard !waveformData.isEmpty else {
                            return await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                                for await _ in clock.timer(interval: .seconds(3)) {
                                    await send(.internal(.loadWaveform))
                                }
                            }
                        }
                        await send(.internal(.waveformLoadResponse(.success(waveformData))))
                        await send(.lyricsSelection(.processLyrics(alignedLyrics.alignedWords)))
                    } catch {
                        await send(.internal(.waveformLoadResponse(.failure(error))))
                    }
                }

            case .internal(.waveformLoadResponse(let result)):
                switch result {
                case .success(let waveform):
                    state.waveformData = waveform
                    return .send(.waveform(.setWaveformData(waveform)))

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

            case .dismissKeyboard:
                state.focusedField = nil
                return .none

            case .lyricsSelection:
                return .none

            case .refreshHighlighting(let time):
                state.focusedField = nil
                return .send(.lyricsSelection(.updateHighlightingStart(time)))

            case .showEditsWarningAboutLyrics:
                state.hasShownBrandedAlert = true
                return .send(.delegate(.showBrandedAlertOverlay(
                    .singleButtonAlert(.preset(.editingSelectionWillClearLyrics))
                )))

            case .binding(\.focusedField):
                // If we tapped outside the keyboard to dismiss it,
                // tell the waveform editor to dismiss the keyboard
                // if it's up
                let saveResults = state.focusedField == nil
                return .send(.waveform(.resetFocusedField(saveResults)))

            case .binding,
                 .delegate,
                 .waveform,
                 .internal:
                return .none
            }
        }
    }
}

public struct ExtendClipScreen: View {
    @Bindable var store: StoreOf<ExtendClip>
    @FocusState var focusedField: ExtendClip.State.Field?
    @Namespace private var animation

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

    public var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                waveform
                lyrics
                style
                clearAllButton
                Spacer()
            }
            .padding(.bottom, 200)
            .padding(12)
        }
        .bind($store.focusedField, to: $focusedField)
        .onTapGesture { store.focusedField = nil }
        .task {
            store.send(.task)
        }
        .sheet(isPresented: $store.isLyricsEditorShown, content: {
            LyricsEditorView(store: store.scope(state: \.lyricsSelection, action: \.lyricsSelection))
                .presentationDetents([.large])
                .presentationCornerRadius(40.0)
        })
    }

    @ViewBuilder
    var waveform: some View {
        ZStack {
            WaveformEditorView(store: store.scope(state: \.waveform, action: \.waveform))

            if store.lyricsSelection.isWaveformBlockedByWarning {
                Color.clear
                    .contentShape(.rect)
                    .onTapGesture {
                        store.send(.showEditsWarningAboutLyrics)
                        store.send(.lyricsSelection(.clearBlockWarning))
                    }
                    .gesture(DragGesture(minimumDistance: 0)
                        .onEnded { _ in
                            store.send(.showEditsWarningAboutLyrics)
                            store.send(.lyricsSelection(.clearBlockWarning))
                        }
                    )
            }
        }
    }

    @ViewBuilder
    var lyrics: some View {
        LyricsSelectionView(
            store: store.scope(state: \.lyricsSelection, action: \.lyricsSelection),
            isInstrumental: $store.instrumental,
            lyricsCharCountLimit: store.lyricsCharCountLimit
        )
    }

    @ViewBuilder
    var style: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(L10n.FeatureCreateClip.style)
                .typographyV1(.headline4)
                .foregroundStyle(Color.SemanticV1.textPrimary)

            TextField(L10n.FeatureCreateClip.stylePlaceholder, text: $store.styles.limit(store.stylesCharCountLimit), axis: .vertical)
                .focused($focusedField, equals: .styles)
                .typographyV1(.body2.neueMontrealMedium())
                .foregroundStyle(Color.SemanticV1.textPrimary)
                .padding(EdgeInsets(top: 16, leading: 16, bottom: 80, trailing: 16))
                .lineLimit(4, reservesSpace: true)
                .background(Color.SemanticV1.backgroundSecondary)
                .cornerRadius(8)
                .overlay(alignment: .bottomLeading) {
                    if !store.recommendedStyles.isEmpty {
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack(spacing: 8) {
                                ClipSuggestionsView(
                                    suggestions: store.recommendedStyles,
                                    random: { store.send(.selectRandomStyle) },
                                    selected: { store.send(.selectStyle($0)) }
                                )
                            }
                            .padding(.horizontal, 8)
                        }
                        .scrollClipDisabled()
                        .scrollBounceBehavior(.basedOnSize)
                        .padding(.bottom, 8)
                    }
                }
                .clipped()
        }
    }

    @ViewBuilder
    var clearAllButton: some View {
        PrimaryButtonV1(
            title: L10n.FeatureEditClip.clearAllChanges,
            colorCombination: .secondary,
            preferredSize: .smallAdaptive
        ) {
            store.send(.clearAll)
        }
        .disabled(!store.hasChanges)
        .padding(.bottom, 64)
    }
}

extension PillButtonSizeV1 {
    public static let extendButtonSize = PillButtonSizeV1(
        typography: .button1,
        width: nil,
        maxWidth: .infinity,
        minHeight: 60,
        iconWidth: 24,
        borderRadius: 22,
        padding: .init(top: 0, leading: 24, bottom: 0, trailing: 24)
    )

    static let extendAdaptiveButtonSize = PillButtonSizeV1(
        typography: .button1,
        maxWidth: .infinity,
        minHeight: 44,
        iconWidth: 24,
        borderRadius: 22,
        padding: .init(top: 0, leading: 24, bottom: 0, trailing: 24),
        alignment: .center,
        loadingAlignment: .center
    )

    static let extendCircleButtonSize = PillButtonSizeV1(
        typography: .button1,
        maxWidth: 44,
        minHeight: 44,
        iconWidth: 24,
        borderRadius: 22,
        padding: .init(),
        alignment: .center,
        loadingAlignment: .center
    )
}
