import APIClient
import Charts
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import Foundation
import Localization
import OrderedCollections
import PaywallClient
import StatsigClient
import SunoModelClient
import SwiftUI
import Utilities

@Reducer
public struct PaywallV2 {
    @Reducer(state: .equatable)
    public enum Destination {
        @Reducer public struct LoadingState {}
        @Reducer public struct ErrorState {
            @ObservableState
            public struct State: Equatable {
                public enum ActionType {
                    case retry
                    case contactSupport
                }

                let message: String
                let buttonTitle: String
                let action: ActionType
            }
        }

        case loading(LoadingState)
        case loaded(PaywallLoadedV2)
        case error(ErrorState)
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State? = .loading(.init())
        var isRestoring = false
        var isRefreshingAfterPurchase = false
        var subscriptions: [Subscription] = []
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
        var purchases: [Entitlement]?
        var appStoreCurrencyCode: String?
        var subscriptionPage: SubscriptionPageResponse?
        
        var isSubscribed: Bool {
            return purchases?.isEmpty == false || billingInfo?.plan != nil
        }

        public init() {}
    }

    public enum Action {
        case destination(PresentationAction<Destination.Action>)

        case dismiss
        case loadSubscriptions
        case loadSubscriptionPage
        case topUpTapped
        case contactSupport
        case subscriptionsResult(Result<SubscriptionsConfiguration, Error>)
        case billingInfoResult(Result<SubscriptionInfoResponse, Error>)
        case purchasesResult(Result<[Entitlement], Error>)
        case subscriptionPageResult(Result<SubscriptionPageResponse, Error>)
    }

    @Dependency(APIClient.self) private var apiClient
    @Dependency(APIClientV2.self) private var api
    @Dependency(PaywallClient.self) private var paywall
    @Dependency(\.openURL) var openURL
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.dismiss) var dismiss
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(\.eventBus.getBillingChannel) var getBillingChannel

    public init() {}

    private func tryPresentLoadedView(state: inout State) {
        guard !state.isRefreshingAfterPurchase,
              let subscriptionPage = state.subscriptionPage,
              let billingInfo = state.billingInfo,
              let purchases = state.purchases,
              !state.subscriptions.isEmpty else {
            return
        }

        if case .loaded = state.destination {
            return
        }

        state.destination = .loaded(.init(
            subscriptions: state.subscriptions,
            appStoreCurrencyCode: state.appStoreCurrencyCode,
            purchases: purchases,
            billingInfo: billingInfo,
            subscriptionPageContent: subscriptionPage.content
        ))
    }

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .loadSubscriptionPage:
                return .run { send in
                    await send(
                        .subscriptionPageResult(
                            Result {
                                guard let data = try await paywall.loadSubscriptionPage() else {
                                    throw PaywallError.subscriptionPageNotLoaded
                                }
                                return data
                            }
                        )
                    )
                }

            case .subscriptionPageResult(.success(let subscriptionPage)):
                state.subscriptionPage = subscriptionPage
                tryPresentLoadedView(state: &state)
                return .none

            case .subscriptionPageResult(.failure(let error)):
                log.telemetry.error(error)
                // If CMS data is required and failed, show error
                // Allow retry which will reload CMS if it hasn't succeeded yet
                if state.subscriptionPage == nil {
                    state.destination = .error(.init(
                        message: L10n.FeaturePaywall.errorSubscriptions,
                        buttonTitle: L10n.FeaturePaywall.retry,
                        action: .retry
                    ))
                }
                return .none

            case .loadSubscriptions:
                return .run { [billingInfo = state.billingInfo] send in
                    await send(.subscriptionsResult(Result(catching: { try await paywall.subscriptions(billingInfo) })))
                }

            case .subscriptionsResult(.success(let subscriptionsConfiguration)):
                state.subscriptions = subscriptionsConfiguration.subscriptions
                state.appStoreCurrencyCode = subscriptionsConfiguration.currencyCode
                return .run { send in
                    await send(.purchasesResult(Result(catching: { try await paywall.syncPurchases() })))
                }

            case .subscriptionsResult(.failure(let error)):
                state.destination = .error(.init(
                    message: L10n.FeaturePaywall.errorSubscriptions,
                    buttonTitle: L10n.FeaturePaywall.retry,
                    action: .retry
                ))
                log.telemetry.error(error, message: "Failed to load subscriptions")
                return .none

            case let .purchasesResult(.success(purchases)):
                state.isRestoring = false
                state.purchases = purchases
                return .run { send in
                    await send(.billingInfoResult(Result(catching: { try await api.getBillingInfo() })))
                }

            case .purchasesResult(.failure(let error)):
                state.isRestoring = false
                state.destination = .error(.init(
                    message: L10n.FeaturePaywall.errorPurchases,
                    buttonTitle: L10n.FeaturePaywall.retry,
                    action: .retry
                ))
                log.telemetry.error(error, message: "Failed to sync purchases")
                return .none

            case let .billingInfoResult(.success(billingInfo)):
                state.$billingInfo.withLock { $0 = billingInfo }
                if state.isRefreshingAfterPurchase {
                    return .run { send in
                        await send(.destination(.presented(.loaded(.destination(.presented(.topUp(.creditsRefreshResult(.success(billingInfo)))))))))
                    }
                }

                state.isRestoring = false
                tryPresentLoadedView(state: &state)
                getBillingChannel().queue(.billingInfoUpdated(billingInfo))
                return .none

            case .billingInfoResult(.failure(let error)):
                if state.isRefreshingAfterPurchase {
                    return .run { send in
                        await send(.destination(.presented(.loaded(.destination(.presented(.topUp(.creditsRefreshResult(.failure(error)))))))))
                    }
                }
                state.isRestoring = false
                state.destination = .error(.init(
                    message: L10n.FeaturePaywall.errorPurchases,
                    buttonTitle: L10n.FeaturePaywall.retry,
                    action: .retry
                ))
                log.telemetry.error(error, message: "Failed to fetch billing info")
                return .none

            case .topUpTapped:
                return .run { send in
                    await send(.destination(.presented(.loaded(.topUpTapped))))
                }

            case .contactSupport:
                if let url = URL(string: "mailto:support@suno.com") {
                    return .run { _ in
                        await openURL(url)
                    }
                } else {
                    return .none
                }

            case .destination(.presented(.loaded(.purchaseResult(.success((_, let purchases)))))):
                return .send(.purchasesResult(.success(purchases)))

            case .destination(.presented(.loaded(.purchaseResult(.failure(PaywallError.cancelled))))):
                return .none

            case .destination(.presented(.loaded(.purchaseResult(.failure)))):
                state.destination = .error(.init(
                    message: L10n.FeaturePaywall.errorPurchase,
                    buttonTitle: L10n.FeaturePaywall.contactSupport,
                    action: .contactSupport
                ))
                return .none

            case .destination(.presented(.loaded(.delegate(.refreshCreditsAfter)))):
                state.isRefreshingAfterPurchase = true
                return .run { send in
                    await send(.billingInfoResult(Result(catching: { try await api.getBillingInfo() })))
                }

            case .destination(.presented(.loaded(.delegate(.restore)))):
                return .run { send in
                    await send(.purchasesResult(Result(catching: { try await paywall.restore() })))
                }

            case .destination:
                return .none

            case .dismiss:
                if let billingInfo = state.billingInfo {
                    sunoModelClient.setUserAccess(userAccess: billingInfo.sunoModelUserAccess)
                }
                return .run { _ in await self.dismiss() }
            }
        }
        .ifLet(\.$destination, action: \.destination)
    }
}

public struct PaywallScreenV2: View {
    @Bindable private var store: StoreOf<PaywallV2>

    @Shared(.inMemory(.overrideColorScheme)) var overrideColorScheme: ColorScheme? = nil

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

    public var body: some View {
        ZStack {
            if let destination = store.destination {
                switch destination {
                case .loading:
                    GradientSpinner(size: .large)
                        .task {
                            store.send(.loadSubscriptionPage)
                            store.send(.loadSubscriptions)
                        }

                case .loaded:
                    if let loadedStore = store.scope(state: \.destination?.loaded, action: \.destination.loaded) {
                        PaywallLoadedViewV2(store: loadedStore)
                    }

                case .error:
                    if let errorStore = store.scope(state: \.destination?.error, action: \.destination.error) {
                        FailedView(title: L10n.FeaturePaywall.errorTitle, message: errorStore.message, buttonTitle: errorStore.buttonTitle) {
                            switch errorStore.action {
                            case .retry:
                                // Retry subscriptions and CMS data (if not already loaded)
                                store.send(.loadSubscriptionPage)
                                store.send(.loadSubscriptions)
                            case .contactSupport: store.send(.contactSupport)
                            }
                        }
                    }
                }
            }
        }
        .animation(.snappy, value: store.destination)
        .navigationBarBackground(Color.SemanticV1.backgroundBrown)
        .navigationBarTitleDisplayMode(.inline)
        .onDisappear {
            $overrideColorScheme.withLock {
                $0 = nil
            }
        }
        .onAppear {
            $overrideColorScheme.withLock {
                $0 = .dark
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.SemanticV1.backgroundPrimary.ignoresSafeArea())
        .environment(\.colorScheme, .dark)
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                if store.isSubscribed {
                    Button {
                        store.send(.topUpTapped)
                    } label: {
                        Text(L10n.FeaturePaywall.topUp)
                            .typographyV1(.body1)
                            .foregroundStyle(Color.SemanticV1.textOnDark)
                    }
                    .buttonStyle(.borderedProminent)
                    .tint(Color.SemanticV1.textLink)
                }
            }
        }
    }

    private func errorView(_ message: String) -> some View {
        VStack {
            Text(L10n.FeaturePaywall.errorTitle)
                .typographyV1(.headline4)

            Text(message)
                .typographyV1(.body1)
                .foregroundStyle(Color.SemanticV1.textSecondary)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}
