import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureAnnouncements
import FeatureBrandedAlert
import FeatureCreateClip
import FeaturePaywall
import FeatureToasts
import HCaptchaClient
import Localization
import OpenAPIRuntime
import PlayerClient
import StatsigClient
import SunoModelClient
import SwiftUI
import UIKit
import Utilities

// swiftlint:disable file_length

@Reducer
public struct CoverClip {
    @Reducer(state: .equatable)
    public enum Destination {
        case subscriptions(Paywall)
        case alert(AlertState<Alert>)
        case brandedAlert(BrandedAlert)
        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
        }

        @Presents public var destination: Destination.State?
        @Presents var settings: CreateClipSettings.State?

        let clip: Clip
        let hook: Hook?
        @Shared var me: Me
        let prompt: Prompt
        var title: String
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
        var lyrics: String
        var styles: String = ""
        var text: String = ""
        var focusedField: Field?
        var instrumental: Bool = false
        var isSubmitting: Bool = false
        var isCollapsed = false
        var extendTimestamp: Double

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

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

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

        @ObservationStateIgnored @ObservedBox public var toast = ToastReducer.State()
        @ObservationStateIgnored @ObservedBox public var audioPlayerSection: CustomCreatePlayer.State

        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 requiresTokenToGenerate: Bool {
            FeatureFlag.legacy.requiresTokenToGenerate
        }

        let formatter: DateComponentsFormatter = {
            var formatter = DateComponentsFormatter()
            formatter.allowedUnits = [.minute, .second]
            formatter.unitsStyle = .positional
            formatter.zeroFormattingBehavior = .pad
            return formatter
        }()

        // MARK: - Character Limits

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

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

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

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

        public init(clip: Clip, me: Shared<Me>, prompt: Prompt, hook: Hook? = nil) {
            self.clip = clip
            self._me = me
            self.prompt = prompt
            self.title = clip.title
            self.lyrics = prompt.lyrics
            self.styles = prompt.styles
            self.instrumental = prompt.instrumental
            self.extendTimestamp = clip.duration
            self.audioPlayerSection = .init(clip: clip, canSwitchModes: false)
            self.hook = hook
        }
    }

    public enum Action: BindableAction {
        case destination(PresentationAction<Destination.Action>)
        case settings(PresentationAction<CreateClipSettings.Action>)

        case task
        case dismiss
        case `internal`(Internal)
        case delegate(Delegate)
        case binding(BindingAction<State>)
        case toast(ToastReducer.Action)
        case audioPlayerSection(CustomCreatePlayer.Action)

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

        case generateTapped
        case selectRandomStyle
        case selectStyle(String)
        case generate(_ token: String?)
        case generationResponse(Prompt, Result<[Clip], Error>)
        case hCaptcha(HCaptcha)
        case showSubscriptions
        case settingsTapped
        case backTapped
        case didSelectModel(SunoModelMetaData)
        case showUpgradeAlert(_ marketingLevelUnderstanding: SunoModelMetaData.MarketingLevelUnderstanding)

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

        public enum Internal {
            case billingInfoResponse(Result<SubscriptionInfoResponse, Error>)
            case generationResponse(Prompt, Result<[Clip], Error>)
        }

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

    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(HCaptchaClient.self) private var hCaptchaClient
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(\.dismiss) var dismiss
    @Dependency(\.eventBus.getCreateChannel) private var getCreateChannel
    @Dependency(\.eventBus.sendHookEvent) private var sendHookEvent

    public init() {}

    public var body: some ReducerOf<Self> {
        Scope(state: \.toast, action: \.toast) {
            ToastReducer()
        }
        Scope(state: \.audioPlayerSection, action: \.audioPlayerSection) {
            CustomCreatePlayer()
        }
        BindingReducer()
        Reduce { state, action in
            struct SnippetPlayerEventStreamCancellable: Hashable {}
            switch action {
            case .task:
                // Reverse to list the highest version last
                // TODO: (JY) This selector dataflow should be revisited and better encapsulated.
                return .merge(
                    .send(.getBillingInfo),
                    .send(.hCaptcha(.configure)),
                    .send(.getRecommendedStyles)
                )

            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
                        // Sets available models
                        await sunoModelClient.configureWithSubscriptionInfoResponse(userId: userId, response: billingInfo)
                    }

                case .failure:
                    break
                }
                return .none

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

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

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

            case .recommendedStylesResponse(.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 .selectRandomStyle:
                guard let style = state.recommendedStyles.randomElement() else { return .none }
                return .send(.selectStyle(style))

            case .generateTapped:
                state.isSubmitting = true
                return .send(.hCaptcha(.fetchToken))

            case .generate(let token):
                let prompt = Prompt(
                    title: state.title,
                    lyrics: state.lyrics,
                    instrumental: state.instrumental,
                    styles: state.styles,
                    text: state.text,
                    coverClipId: state.clip.id.remoteId,
                    generationType: state.prompt.generationType,
                    token: token,
                    task: .cover,
                    isRemix: FeatureFlag.create.remixAndAttribution ? true : nil
                )

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

            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(.internal(.generationResponse(prompt, result)))
                    .merge(with: .send(.delegate(.generationResponse(prompt, result))))

            case .internal(.generationResponse(_, .success)):
                return .send(.dismiss)

            case .internal(.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(.toast(.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, .forbidden, .serverError, .errorMessage:
                        log.telemetry.error(error)
                        return .send(.toast(.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
                            state.isSubmitting = false
                            if state.tokenValidationFailureCount >= state.maxTokenValidationFailures {
                                return .merge(
                                    .send(.hCaptcha(.tokenGenerationFailed)),
                                    .send(.toast(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.contactUsMessage)))))
                                )
                            } else {
                                return .merge(
                                    .send(.hCaptcha(.tokenGenerationFailed)),
                                    .send(.toast(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.tryAgain)))))
                                )
                            }
                        }
                    }

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

            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 {
                        log.telemetry.error(error)
                        await send(.hCaptcha(.tokenGenerationFailed))
                        await send(.toast(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.tryAgain)))))
                    }
                }

            case .hCaptcha(.tokenGenerationFailed):
                state.isSubmitting = false
                return .none

            case .hCaptcha(.setTokenIfNeeded(let hCaptchaToken)):
                return .send(.generate(hCaptchaToken))

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

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

            case .settingsTapped:
                state.settings = .init()
                return .none

            case .backTapped:
                return .send(.dismiss)

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

            case .showUpgradeAlert(let marketingLevelUnderstanding),
                 .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(.brandedAlert(.delegate(.didTriggerWithIntentionToUpgrade)))):
                UIImpactFeedbackGenerator(style: .medium).impactOccurred()
                state.destination = nil
                return .send(.showSubscriptions)

            case .binding, .toast, .destination, .settings, .delegate, .audioPlayerSection:
                // Catch-all
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .ifLet(\.$settings, action: \.settings) {
            CreateClipSettings()
        }

        Analytics()
    }
}

// MARK: New

public struct CoverClipSheet: View {
    @Bindable var store: StoreOf<CoverClip>
    @FocusState var focusedField: CoverClip.State.Field?

    private var hasStyles: Bool {
        !store.styles.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    private var lyricsTextBinding: Binding<String> {
        Binding<String>(
            get: {
                store.instrumental ? L10n.FeatureEditClip.lyricsPlaceholderInstrumental : store.lyrics
            },
            set: { newValue in
                store.lyrics = newValue
            }
        )
    }

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

    public var body: some View {
        VStack(spacing: 0) {
            // Grabber
            grabber

            // Header - Custom remix-style header
            header

            ScrollView {
                VStack(spacing: 16) {
                    // Audio player component (replaces clipDetails)
                    CustomCreatePlayerView(
                        store: store.scope(state: \.audioPlayerSection, action: \.audioPlayerSection),
                        sectionType: .cover
                    )

                    // Style description component
                    styleSection

                    // Lyrics description component
                    lyricsSection

                    // Extra padding at bottom for floating button
                    // Also helps center the last form box.
                    Spacer()
                        .frame(height: 80)
                }
                .padding(16)
            }
            .scrollDismissesKeyboard(.interactively)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .safeAreaInset(edge: .bottom) {
            createFooter
        }
        .bind($store.focusedField, to: $focusedField)
        .task {
            store.send(.task)
        }
        .presentationDragIndicator(.hidden)
        .presentationContentInteraction(.scrolls)
        .modify {
            /// Use default styling on iOS 26+
            if #available(iOS 26.0, *) {
                $0
            } else {
                $0.presentationBackground {
                    Color.FigmaMCP.Semantic.smokeDense
                        .background(.ultraThinMaterial)
                }
            }
        }
        .preferredColorScheme(.dark)
        .onTapGesture { self.dismissKeyboard() }
        .overlay(alignment: .top) {
            ToastView(store: store.scope(state: \.toast, action: \.toast)) { destination in
                if destination == .paywall, store.me.flags[FlagKey.iosSubscriptions] == true {
                    store.send(.showSubscriptions)
                }
            }
        }
        .fullScreenCover(item: $store.scope(state: \.destination?.subscriptions, action: \.destination.subscriptions)) { paywallStore in
            NavigationStack {
                PaywallScreen(store: paywallStore)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading) {
                            ToolbarButton(.close, background: Material.ultraThin) {
                                paywallStore.send(.dismiss)
                            }
                        }
                    }
            }
        }
        .alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
        .overlay {
            if let store = store.scope(state: \.destination?.brandedAlert, action: \.destination.brandedAlert) {
                BrandedAlertView(store)
            }
        }
        .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)
            }
        }
    }

    // MARK: - Components

    private var grabber: some View {
        VStack {
            RoundedRectangle(cornerRadius: 100)
                .fill(Color.FigmaMCP.Semantic.fogDense)
                .frame(width: 32, height: 4)
        }
        .padding(16)
    }

    private var header: some View {
        HStack {
            // Left Section - Back button, title, and credits
            leftSection

            Spacer()

            // Right Section - Settings button (replacing model selector)
            rightSection
        }
        .padding(.horizontal, 16)
        .padding(.bottom, 16)
        .overlay(
            // Bottom border
            Rectangle()
                .fill(Color.white.opacity(0.1))
                .frame(height: 1),
            alignment: .bottom
        )
    }

    private var leftSection: some View {
        HStack(spacing: 12) {
            // Back Button (using toolbar button style)
            Button(action: {
                store.send(.backTapped)
            }) {
                Image.FigmaMCP.chevronLeft
                    .figmaMCPIconStyle(size: 16, semanticColor: Color.FigmaMCP.Semantic.foregroundPrimary)
                    .frame(width: 40, height: 40)
                    .background(Color.FigmaMCP.Semantic.fogThin)
                    .clipShape(Circle())
            }
            .buttonStyle(PlainButtonStyle())

            // Title and Credits
            VStack(alignment: .leading, spacing: 2) {
                // Remix Type Title
                Text(L10n.FeatureEditClip.cover)
                    .figmaMCPTypography(TypographyV1.FigmaMCP.largeRegular)
                    .foregroundColor(Color.FigmaMCP.Semantic.foregroundPrimary)
                    .tracking(0.36)

                // Credits Display
                if let billingInfo = store.billingInfo {
                    Text(L10n.FeatureEditClip.credits(billingInfo.totalCreditsLeft))
                        .figmaMCPTypography(TypographyV1.FigmaMCP.timecode)
                        .foregroundColor(billingInfo.totalCreditsLeft <= 0 ? Color.FigmaMCP.Semantic.accentError : Color.FigmaMCP.Semantic.fogDense)
                        .tracking(0.2)
                } else {
                    // TODO: (JY) Add a useful error state + styling here.
                    Text(L10n.FeatureEditClip.creditsPlaceholder)
                        .figmaMCPTypography(TypographyV1.FigmaMCP.timecode)
                        .foregroundColor(Color.FigmaMCP.Semantic.fogDense)
                        .tracking(0.2)
                }
            }
        }
    }

    private var rightSection: some View {
        ModelSelectorV3(
            selectedModel: store.selectedSunoModel,
            models: store.availableModels,
            setSelectedModel: { model in
                if model.canUse == false {
                    store.send(.showUpgradeAlert(model.marketingLevelUnderstanding))
                } else {
                    store.send(.didSelectModel(model))
                }
            }
        )
        .environment(\.colorScheme, .dark)
    }

    private var styleSection: some View {
        ExpandableSection(
            title: L10n.FeatureEditClip.style,
            text: $store.styles,
            limit: store.stylesCharCountLimit,
            placeholder: L10n.FeatureEditClip.stylePlaceholder,
            focusBinding: $focusedField,
            focusValue: .styles
        ) {
            // Style suggestions footer
            if !store.recommendedStyles.isEmpty {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 8) {
                        // Quick style additions using MediumButton styling
                        ForEach(store.recommendedStyles, id: \.self) { style in
                            Button(action: {
                                store.send(.selectStyle(style))
                            }) {
                                HStack(spacing: 4) {
                                    Image.FigmaMCP.plus
                                        .figmaMCPIconStyle(size: 16, semanticColor: Color.FigmaMCP.Semantic.foregroundPrimary)

                                    Text(style)
                                        .figmaMCPTypography(TypographyV1.FigmaMCP.xSmallTitle)
                                        .foregroundColor(Color.FigmaMCP.Semantic.foregroundPrimary)
                                        .tracking(0.24)
                                        .lineLimit(1)
                                }
                                .padding(.horizontal, 16)
                            }
                            .buttonStyle(PlainButtonStyle())
                            .frame(height: 40)
                            .background(Color.FigmaMCP.Semantic.fogThin)
                            .clipShape(RoundedRectangle(cornerRadius: 100))
                        }
                    }
                    .padding(.horizontal, 16)
                }
                .scrollClipDisabled()
                .scrollBounceBehavior(.basedOnSize)
                .padding(.bottom, 16)
            }
        }
    }

    private var lyricsSection: some View {
        ExpandableSection(
            title: L10n.FeatureEditClip.lyrics,
            text: lyricsTextBinding,
            limit: store.lyricsCharCountLimit,
            placeholder: L10n.FeatureEditClip.lyricsPlaceholder, // Placeholder will be replaced once we have the magic wand button
            focusBinding: $focusedField,
            focusValue: .lyrics,
            isDisabled: store.instrumental
        ) {
            // Custom instrumental toggle button matching prototype
            Button(action: {
                store.send(.binding(.set(\.instrumental, !store.instrumental)), animation: .easeInOut(duration: 0.2))
            }) {
                HStack(spacing: 4) {
                    // Checkmark icon
                    Image.FigmaMCP.success
                        .figmaMCPIconStyle(size: 16, semanticColor: store.instrumental ? Color.FigmaMCP.Semantic.accentBrand : Color.FigmaMCP.Semantic.fogDense)

                    Text(L10n.FeatureCreateClip.instrumental)
                        .figmaMCPTypography(TypographyV1.FigmaMCP.xSmallTitle)
                        .foregroundColor(store.instrumental ? Color.FigmaMCP.Semantic.backgroundPrimary : Color.FigmaMCP.Semantic.foregroundPrimary)
                        .tracking(0.24)
                }
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .frame(height: 40)
                .background(
                    RoundedRectangle(cornerRadius: 100)
                        .fill(store.instrumental ? Color.FigmaMCP.Semantic.foregroundPrimary : Color.clear)
                        .overlay(
                            RoundedRectangle(cornerRadius: 100)
                                .stroke(Color.FigmaMCP.Semantic.borderPrimary, lineWidth: 1)
                        )
                )
            }
            .buttonStyle(PlainButtonStyle())
            .padding(.horizontal, 16)
            .padding(.bottom, 16)
        }
    }

    private var createFooter: some View {
        // Button area
        AuraButton(
            title: L10n.FeatureEditClip.create,
            isLoading: store.isSubmitting,
            auraStyle: .orangeAura,
            withGradientOverlay: false,
            leadingView: {
                Image.FigmaMCP.create
                    .figmaMCPIconStyle(size: 16, semanticColor: Color.FigmaMCP.Semantic.foregroundPrimary)
            },
            action: { store.send(.generateTapped) }
        )
        .pillButtonSizeV1(.extendButtonSize)
        .padding(.horizontal, 16)
        .padding(.bottom, 8)
        .opacity(hasStyles ? 1 : 0.6)
        .disabled(!hasStyles)
    }
}
