import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import FeatureAnnouncements
import FeatureBrandedAlert
import FeatureToasts
import Foundation
import HCaptchaClient
import Localization
import NavigationRouterClient
import OpenAPIRuntime
import SnippetPlayerClient
import StatsigClient
import SunoModelClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

/*
 This is the Custom Create sheet that you pull up from Chat mode.
 
 It adapts to whatever prompt you give it and opens either
 default, cover or extend mode based on the `Prompt` object passed in.
*/
@Reducer
public struct OrpheusCustomCreate {
    public enum Mode: Equatable {
        case `default`
        case remix(Clip)
    }
    
    @Reducer(state: .equatable)
    public enum Destination {
        case alert(AlertState<Alert>)
        case bluejayAnnouncement(BluejayAnnouncement)
        case v5Announcement(V5Announcement)

        public enum Alert {
            case upgrade
        }
    }

    @ObservableState
    public struct State: Equatable {
        public enum Field {
            case lyrics
            case styles
            case title
            case excludeStyles
        }
        
        public enum SectionType: Equatable {
            case modular
            case styles
            case lyrics
            case advancedOptions
            case title
        }
        
        public enum ScrollTarget: String, Equatable, Hashable {
            case styles
            case lyrics
            case title
            case advancedOptions
        }
        
        public var sectionOrder: [SectionType] {
            switch mode {
            case .remix:
                if remixMode == .extend {
                    return [.modular, .lyrics, .styles, .advancedOptions, .title]
                } else {
                    return [.modular, .styles, .lyrics, .advancedOptions, .title]
                }
            case .default:
                return [.modular, .styles, .lyrics, .advancedOptions, .title]
            }
        }
        
        public var firstFocusableField: Field? {
            for section in sectionOrder {
                switch section {
                case .lyrics:
                    return .lyrics
                case .styles:
                    return .styles
                case .title:
                    return .title
                case .modular, .advancedOptions:
                    continue
                }
            }
            return nil
        }

        @Presents public var destination: Destination.State?
        @Shared public var me: Me
        
        public var mode: Mode
        public var sourceClip: Clip?
        public var remixMode: RemixMode = .cover
        public let prompt: Prompt?
        
        public enum RemixMode: Equatable {
            case cover
            case extend
        }
        
        public var waveformData: [Float] = []
        public var lyrics: String = ""
        public var styles: String = ""
        public var title: String = ""
        public var text: String = ""
        public var focusedField: Field?
        public var instrumental: Bool = false
        public var isSubmitting: Bool = false
        public var extendTimestamp: Double = 0
        public var scrollTarget: ScrollTarget?

        public var recommendedStyles: [String] = []
        public var selectedStyles: [String] = []
        
        public var useAdvancedOptions: Bool = false
        public var excludeStyles: String = ""
        public var weirdnessConstraint: Double = 0.5
        public var styleWeight: Double = 0.5
        public var audioWeight: Double = 0.5
        
        public var slidersHaveChanges: Bool {
            weirdnessConstraint != 0.5 || styleWeight != 0.5 || audioWeight != 0.5
        }
        
        public var showSliders: Bool {
            billingInfo?.plan != nil
        }

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

        @Shared(.inMemory(.billingInfo)) public var billingInfo: SubscriptionInfoResponse?
        @Shared(.inMemory(.selectedSunoModel)) public var selectedSunoModel: SunoModelMetaData = .modelDefault
        @Shared(.inMemory(.availableModels)) public var availableModels: [SunoModelMetaData] = []
        
        @ObservationStateIgnored @ObservedBox public var toast = ToastReducer.State()
        @ObservationStateIgnored @ObservedBox public var audioPlayerSection: CustomCreatePlayer.State?

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

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

        public var hasChanges: Bool {
            switch mode {
            case .remix:
                if remixMode == .extend {
                    return extendTimestamp != 0
                } else {
                    return !styles.isEmpty || !lyrics.isEmpty
                }
            case .default:
                return !styles.isEmpty || !lyrics.isEmpty
            }
        }

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

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

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

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

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

        public init(mode: Mode, me: Shared<Me>, prompt: Prompt? = nil) {
            self.mode = mode
            self._me = me
            self.prompt = prompt
            
            switch mode {
            case .default:
                self.sourceClip = nil
                self.lyrics = prompt?.lyrics ?? ""
                self.styles = prompt?.styles ?? ""
                self.title = ""
                self.instrumental = prompt?.instrumental ?? false
                self.audioPlayerSection = nil
                self.remixMode = .cover
                
            case .remix(let clip):
                self.sourceClip = clip
                if let continueAt = prompt?.continueAt, continueAt > 0 {
                    self.remixMode = .extend
                    self.extendTimestamp = continueAt
                    self.lyrics = ""
                } else {
                    self.remixMode = .cover
                    self.extendTimestamp = 30.0
                    self.lyrics = prompt?.lyrics ?? clip.prompt
                }
                self.title = clip.title
                self.styles = prompt?.styles ?? clip.tags
                self.instrumental = prompt?.instrumental ?? false
                self.audioPlayerSection = .init(clip: clip, canSwitchModes: true)
            }
        }
    }

    public enum Action: BindableAction {
        case task
        case dismiss
        case teardown
        case clearAll
        case destination(PresentationAction<Destination.Action>)
        case `internal`(Internal)
        case binding(BindingAction<State>)
        case toast(ToastReducer.Action)
        case audioPlayerSection(CustomCreatePlayer.Action)
        case delegate(Delegate)
        case getRecommendedStyles
        case getBillingInfo
        case recommendedStylesResponse(Result<Styles, Error>)

        case selectStyle(String)
        case didSelectModel(SunoModelMetaData)
        case showUpgradeAlert(SunoModelMetaData.MarketingLevelUnderstanding)
        case showSubscriptions
        case createTapped
        case switchMode

        public enum Delegate {
            case createTapped(mode: Mode, sourceClip: Clip?, prompt: Prompt)
        }

        public enum Internal {
            case billingInfoResponse(Result<SubscriptionInfoResponse, Error>)
        }
    }

    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(HCaptchaClient.self) private var hCaptchaClient
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.dismiss) var dismiss
    @Dependency(\.snippetPlayerClient) private var snippetPlayerClient

    public init() {}

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Scope(state: \.toast, action: \.toast) {
            ToastReducer()
        }
        Reduce { state, action in
            switch action {
            case .audioPlayerSection(.delegate(.modeSwitchRequested)):
                return .send(.switchMode)
                
            case .audioPlayerSection(.delegate(.resetToDefaultCustomMode)):
                guard case .remix = state.mode else { return .none }
                state.mode = .default
                state.sourceClip = nil
                state.audioPlayerSection = nil
                state.remixMode = .cover
                state.extendTimestamp = 0
                state.lyrics = state.prompt?.lyrics ?? ""
                state.styles = state.prompt?.styles ?? ""
                state.title = ""
                state.instrumental = state.prompt?.instrumental ?? false
                return .run { _ in
                    snippetPlayerClient.teardown()
                }
                
            case .audioPlayerSection:
                return .none

            case .task:
                // Set initial focus
                if let firstField = state.firstFocusableField {
                    state.focusedField = firstField
                }
                return .merge(
                    .send(.getBillingInfo),
                    .send(.getRecommendedStyles)
                )

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

            case .teardown:
                return .run { _ in
                    snippetPlayerClient.teardown()
                }

            case .getBillingInfo:
                return .run { send in
                    await send(.internal(.billingInfoResponse(Result(catching: { try await api.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
                        await sunoModelClient.configureWithSubscriptionInfoResponse(userId: userId, response: billingInfo)
                    }
                case .failure:
                    break
                }
                return .none

            case .clearAll:
                state.lyrics = state.prompt?.lyrics ?? ""
                state.styles = state.prompt?.styles ?? ""
                state.instrumental = state.prompt?.instrumental ?? false
                return .none

            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 .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 .createTapped:
                let prompt: Prompt
                switch state.mode {
                case .default:
                    prompt = Prompt(
                        title: state.title,
                        lyrics: state.instrumental ? "" : state.lyrics,
                        instrumental: state.instrumental,
                        styles: state.styles,
                        excludeStyles: state.excludeStyles,
                        text: state.text,
                        generationType: .text,
                        audioWeight: state.audioWeight,
                        styleWeight: state.styleWeight,
                        weirdnessConstraint: state.weirdnessConstraint
                    )
                    
                case .remix(let clip):
                    if state.remixMode == .cover {
                        prompt = Prompt(
                            title: state.title.isEmpty ? clip.title : state.title,
                            lyrics: state.instrumental ? "" : state.lyrics,
                            instrumental: state.instrumental,
                            styles: state.styles,
                            excludeStyles: state.excludeStyles,
                            text: state.text,
                            coverClipId: clip.id.remoteId,
                            generationType: .text,
                            task: .cover,
                            audioWeight: state.audioWeight,
                            styleWeight: state.styleWeight,
                            weirdnessConstraint: state.weirdnessConstraint
                        )
                    } else {
                        prompt = Prompt(
                            title: state.title.isEmpty ? clip.title : state.title,
                            lyrics: state.instrumental ? "" : state.lyrics,
                            instrumental: state.instrumental,
                            styles: state.styles,
                            excludeStyles: state.excludeStyles,
                            text: state.text,
                            continueClipId: clip.id.remoteId,
                            continueAt: state.extendTimestamp,
                            generationType: .text,
                            task: .extend,
                            audioWeight: state.audioWeight,
                            styleWeight: state.styleWeight,
                            weirdnessConstraint: state.weirdnessConstraint
                        )
                    }
                }
                
                return .send(.delegate(.createTapped(mode: state.mode, sourceClip: state.sourceClip, prompt: prompt)))

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

            case .showSubscriptions:
                navigationRouter.send(route: .subscriptions)
                return .none

            case .didSelectModel(let model):
                return .run { _ in
                    await sunoModelClient.setModel(model.id)
                }

            case .showUpgradeAlert(let marketingLevelUnderstanding):
                switch marketingLevelUnderstanding {
                case .previousToV4, .v3Dot5:
                    assertionFailure("Should not `showUpgradeAlert` for free models.")
                case .v4, .auk:
                    navigationRouter.send(route: .subscriptions)
                case .bluejay:
                    state.destination = .bluejayAnnouncement(.init(style: .free))
                case .v5:
                    state.destination = .v5Announcement(.init(style: .free))
                }
                return .none

            case .destination(.presented(.bluejayAnnouncement(.delegate(.openSubscriptions)))):
                navigationRouter.send(route: .subscriptions)
                return .none

            case .destination(.presented(.v5Announcement(.delegate(.openSubscriptions)))):
                navigationRouter.send(route: .subscriptions)
                return .none
                
            case .switchMode:
                guard case .remix(_) = state.mode else { return .none }
                switch state.remixMode {
                case .cover:
                    state.remixMode = .extend
                    if state.extendTimestamp == 0 {
                        state.extendTimestamp = 30.0
                    }
                case .extend:
                    state.remixMode = .cover
                }
                return .none
                
            case .binding(\.useAdvancedOptions):
                if state.useAdvancedOptions {
                    state.scrollTarget = .advancedOptions
                }
                return .none
                
            case .binding, .destination, .toast, .internal, .delegate:
                return .none
            }
        }
       .ifLet(\.$destination, action: \.destination)
       .ifLet(\.audioPlayerSection, action: \.audioPlayerSection) {
           CustomCreatePlayer()
       }
    }
}

