import APIClient
import ClipDataClient
import ComponentLibrary
import ComposableArchitecture
import FeatureClipList
import Localization
import PlayerClient
import SwiftUI
import Utilities

@Reducer
public struct AudioLibraryPicker {
    public enum DownloadState: Equatable {
        case downloaded(URL)
        case downloading(progress: Double)
        case notDownloaded
        case unknown
    }

    @ObservableState
    public struct State: Equatable {
        @Shared var me: Me
        @ObservationStateIgnored @ObservedBox var clipList: ClipList.State
        var loadState: LoadState { clipList.loadState }
        var downloadState: DownloadState = .unknown

        var isPlayingDisabled = true
        var playingClip: Clip?
        var downloadingClip: Clip?

        public init(me: Shared<Me>) {
            self._me = me
            self.clipList = .init(me: me, firstPageIndex: 0, context: SessionContext(source: .audioLibraryPicker))
        }
    }

    public enum Action {
        public enum Delegate {
            case downloadedClip(URL, Clip)
        }

        public enum Internal {
            case audioSetupCompletion(Result<Void, Error>)
            case downloadAudioResponse(Result<APIClient.DownloadEvent, Error>, Clip)
            case saveAudioResponse(Result<URL, Error>, Clip)
        }

        case clipList(ClipList.Action)
        case delegate(Delegate)
        case `internal`(Internal)
        case onAppear
        case loadClips
        case playTapped(Clip)
        case dismiss
        case setPlayingClip(Clip?)
        case downloadTapped(Clip)
    }

    @Dependency(\.dismiss) private var dismiss
    @Dependency(\.apiClientV2) private var api
    @Dependency(PlayerClient.self) private var playerClient
    @Dependency(APIClient.self) private var apiClient
    @Dependency(ClipDataClient.self) var clipDataClient

    public init() {}

    public var body: some ReducerOf<Self> {
        Scope(state: \.clipList, action: \.clipList) {
            ClipList()
        }
        .withClipListClient { _ in
            .init(getClips: { page in
                // Params: page, isPublic, isLiked, isVideoToSong, isScene, isUploadedAudio
                try await api.getFeedV2(page, nil, nil, nil, nil, true, nil).clips
            })
        }

        Reduce<State, Action> { state, action in
            switch action {
            case .onAppear:
                return .run { send in
                    // Pause currently playing OmniPlayer clip when we start recording
                    @Dependency(\.omniplayerClient.pauseCurrentClip) var pauseCurrentClip
                    pauseCurrentClip()
                    await send(.loadClips)
                }

            case .loadClips:
                return .run { send in
                    await send(.clipList(.loadClips))
                    await send(.internal(.audioSetupCompletion(Result(catching: { try playerClient.setup() }))))
                }

            case .internal(.audioSetupCompletion(.success)):
                state.isPlayingDisabled = false
                return .none

            case .internal(.audioSetupCompletion(.failure(let error))):
                state.isPlayingDisabled = true
                log.telemetry.error(error)
                return .none

            case .dismiss:
                return .merge(
                    .send(.setPlayingClip(nil)),
                    .run { _ in await self.dismiss() }
                )

            case .playTapped(let clip):
                if state.playingClip == clip {
                    return .send(.setPlayingClip(nil))
                } else {
                    return .merge(
                        .run { [url = clip.audioUrl] _ in
                            _ = await playerClient.replaceCurrentItem(url)
                            playerClient.play()
                        },
                        .send(.setPlayingClip(clip))
                    )
                }

            case .setPlayingClip(let clip):
                state.playingClip = clip
                if clip == nil {
                    playerClient.pause()
                }
                return .none

            case .downloadTapped(let clip):
                guard let audioURL = URL(string: clip.audioUrl) else { return .none }

                // Stop playing current song
                playerClient.pause()
                state.playingClip = nil

                // Check for already downloaded clips
                if let savedUrl = clipDataClient.audioURL(clip) {
                    return .send(.internal(.saveAudioResponse(.success(savedUrl), clip)))
                }

                state.downloadState = .downloading(progress: 0)
                state.downloadingClip = clip

                return .run(priority: .background) { send in
                    for try await event in apiClient.downloadStream(audioURL) {
                        await send(.internal(.downloadAudioResponse(.success(event), clip)))
                    }
                } catch: { error, send in
                    await send(.internal(.downloadAudioResponse(.failure(error), clip)))
                }

            case .internal(.downloadAudioResponse(.success(.response(let data)), let clip)):
                state.downloadingClip = nil
                return .run { send in
                    // Delays calling saved audio so user can see completed progress
                    try await Task.sleep(for: .seconds(0.5))
                    await send(.internal(.saveAudioResponse(Result(catching: { try clipDataClient.save(clip, data) }), clip)))
                }

            case .internal(.downloadAudioResponse(.success(.updateProgress(let progress)), _)):
                state.downloadState = .downloading(progress: progress)
                return .none

            case .internal(.downloadAudioResponse(.failure(let error), _)):
                log.telemetry.error(error)
                state.downloadState = .notDownloaded
                state.downloadingClip = nil
                return .none

            case .internal(.saveAudioResponse(.success(let url), let clip)):
                state.downloadState = .downloaded(url)
                return .send(.delegate(.downloadedClip(url, clip)))

            case .internal(.saveAudioResponse(.failure(let error), _)):
                log.telemetry.error(error)
                state.downloadState = .notDownloaded
                return .none

            case .delegate, .internal, .clipList:
                // Catch-all
                return .none
            }
        }
    }
}

public struct AudioLibraryPickerScreen: View {
    private static let top = "top"

    @Bindable var store: StoreOf<AudioLibraryPicker>

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

    public var body: some View {
        ZStack {
            switch store.loadState {
            case .loading:
                LoadingView()
                    .onAppear { store.send(.onAppear) }

            case .loaded:
                loadedView

            case .failed(let message):
                FailedView(
                    title: L10n.FeatureCreateClip.errorTitle,
                    message: message,
                    buttonTitle: L10n.FeatureCreateClip.retry,
                    action: { store.send(.loadClips) }
                )
            }
        }
        .navigationBarBackButtonHidden()
        .navigationBarTitleDisplayMode(.inline)
    }

    private var loadedView: some View {
        List {
            if store.clipList.clips.isEmpty {
                emptyView
            } else {
                Section {
                    ForEach(store.clipList.clips, id: \.id) { clipState in
                        VStack(spacing: 0) {
                            itemView(clipState.clip)
                                .padding(.horizontal, 12)

                            Divider()
                                .overlay(Color.SemanticV1.borderPrimary)
                        }
                        .listRowSeparator(.hidden)
                    }

                    if store.clipList.pages.hasMore, !store.clipList.clips.isEmpty {
                        loadingNextPageProgress
                            .onAppear {
                                guard !store.clipList.pages.loadingNext else { return }
                                store.send(.clipList(.getNextPage))
                            }
                    }
                }
                .listRowBackground(Color.clear)
                .listSectionSeparator(.hidden, edges: .top)
                .listSectionSpacing(.zero)
                .listRowInsets(.init())
            }
        }
        .background(Color.SemanticV1.backgroundPrimary)
        .listStyle(.plain)
        .stableRefreshable { await store.send(.loadClips).finish() }
        .contentMargins(.bottom, 90, for: .scrollContent)
        .toolbar {
            ToolbarItem(placement: .principal) {
                ToolbarTitle(L10n.FeatureCreateClip.useAudioClip)
            }
        }
    }

    public func itemView(_ clip: Clip) -> some View {
        Button {
            store.send(.downloadTapped(clip))
        } label: {
            HStack(spacing: 16) {
                RemoteImage(url: clip.imageUrl, fallbackId: clip.id.remoteId)
                    .clipShape(.rect(cornerRadius: 8))
                    .frame(width: 48, height: 64)

                VStack(alignment: .leading, spacing: 0) {
                    Text(clip.title)
                        .typographyV1(.body3)
                        .foregroundColor(.SemanticV1.textPrimary)
                        .lineLimit(1)

                    HStack(spacing: 8) {
                        if let createdAt = clip.createdAt {
                            infoView(
                                icon: nil,
                                text: createdAt.formatted(.dateTime.day().month()).uppercased(),
                                isNumberStyle: true
                            )
                        }

                        infoView(
                            icon: Image.Icon.watch,
                            text: {
                                let formatter = DateComponentsFormatter()
                                formatter.allowedUnits = [.minute, .second]
                                formatter.unitsStyle = .positional
                                formatter.zeroFormattingBehavior = .pad
                                return formatter.string(from: clip.duration) ?? "--:--"
                            }(),
                            isNumberStyle: true
                        )
                    }
                    .padding(.top, 4)
                }
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)

                let isPlaying = store.playingClip?.id == clip.id
                ButtonView(
                    image: isPlaying ? Image.Icon.pause : Image.Icon.playFilled,
                    padding: isPlaying ? 6 : 8,
                    color: .SemanticV1.iconBrand
                ) {
                    store.send(.playTapped(clip))
                }
                .disabled(store.isPlayingDisabled)
                .animation(.default, value: isPlaying)

                if store.downloadingClip == clip {
                    switch store.downloadState {
                    case let .downloading(progress: progress):
                        CircularProgressView(progress: progress)
                            .frame(width: 24, height: 24)

                    case let .downloading(progress: progress) where progress == 0:
                        ProgressView()
                            .progressViewStyle(.circular)
                            .frame(width: 24, height: 24)

                    case .unknown, .notDownloaded, .downloaded:
                        EmptyView()
                    }
                }
            }
            .contentShape(.rect)
            .padding(.vertical, 8)
        }
    }

    private var loadingNextPageProgress: some View {
        ProgressView()
            .progressViewStyle(.circular)
            .padding()
            .frame(maxWidth: .infinity)
            .listRowSeparator(.hidden)
            .background(Color.clear)
            .listRowBackground(Color.clear)
            .id(UUID())
    }

    private func infoView(icon: Image?, text: String, isNumberStyle: Bool = false) -> some View {
        HStack(spacing: 4) {
            if let icon {
                icon
                    .resizable()
                    .frame(width: 15, height: 15, alignment: .center)
                    .foregroundStyle(Color.SemanticV1.textBrand)
            }

            Text(text)
                .lineLimit(1)
                .typographyV1(isNumberStyle ? .monospace : .caption2)
                .foregroundStyle(Color.SemanticV1.textBrand)
        }
    }

    private var emptyView: some View {
        Text(L10n.FeatureCreateClip.audioLibraryEmpty)
            .typographyV1(.body1)
            .opacity(0.7)
            .foregroundColor(Color.SemanticV1.textPrimary)
            .frame(maxWidth: .infinity)
            .frame(height: 100)
            .listRowSeparator(.hidden)
            .listRowBackground(Color.clear)
    }
}
