import APIClient
import ComponentLibrary
import ComposableArchitecture
import FeatureBanner
import FeatureClipDetail
import FeatureToasts
import Foundation
import Localization
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct ClipEdits {
    @Reducer(state: .equatable)
    public enum Destination {
        case songActions(SongActions)
    }

    @ObservableState
    public struct State: Equatable {
        public struct RemovedClipItem: Identifiable, Equatable {
            public var id: Clip.ID { clipItem.clip.id }
            public let index: Int
            public let state: ClipEditsListItem.State
            public let clipItem: ChildClip
        }

        public var rootClip: Clip?
        public let rootClipId: Clip.ID

        @Shared var me: Me

        public var clipEdits: IdentifiedArrayOf<ClipEditsListItem.State> = []
        public var currentlyPollingClipId: Clip.ID?

        public var unreadClips: [Clip.ID] = []
        public var currentlyEditingClipId: Clip.ID?

        @ObservationStateIgnored @ObservedBox var bannerState = BannerReducer.State()
        @ObservationStateIgnored @ObservedBox var toast = ToastReducer.State()
        @Shared(.inMemory(.playingClipKey)) var playingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var isPlaying: Bool = false
        @Shared(.appStorage(.hasSeenTapToMakeMoreExtensionsTooltip)) var hasSeenTapToMakeMoreExtensionsTooltip: Bool = false
        @Presents public var destination: Destination.State?

        public var recentAction: Action.Undoable?
        public var recentlyRemovedClips: IdentifiedArrayOf<RemovedClipItem> = []
        public init(clip: Clip, me: Shared<Me>) {
            if let rootClipItem = clip.history?.clips.first {
                self.rootClipId = rootClipItem.id
                self.currentlyEditingClipId = clip.id // Editing the child clip
            } else {
                self.rootClipId = clip.id
                self.rootClip = clip
                self.currentlyEditingClipId = clip.id // Editing the root clip
            }
            self._me = me
        }

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

        public var rootClipCreatedAt: String? {
            guard let createdAt = rootClip?.createdAt else { return nil }
            // November 26, 2024, at 12:48 PM
            let formatter = DateFormatter()
            formatter.dateStyle = .long
            formatter.timeStyle = .short
            return formatter.string(from: createdAt)
        }

        public var isPlayingRootClip: Bool {
            guard let playingClip = playingClip else { return false }
            return rootClip == playingClip && isPlaying
        }

        // Show the "Tap to make more extensions" tooltip
        // once we collapse the editor for the first time
        public var showTapToMakeMoreExtensionsTooltip: Bool = false

        // If a root clip is available, meaning this clip was made
        // from at least one extension, we show the "Show Original" button
        // that reloads the screen with the original clip
        public var showShowOriginalButton: Bool {
            guard let rootClip = rootClip else { return false }
            return rootClip.concatHistory?.rootClipId != nil
        }

        // If the user taps Show Original, we need to reload the editor
        // and player with that clip
        public var shouldReloadEditor: Bool = false
    }

    public init() {}

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

    public enum Action {
        case task
        case clipEdits(IdentifiedActionOf<ClipEditsListItem>)
        case didSelectEdit(Clip)
        case addClips([Clip])
        case `internal`(Internal)
        case toast(ToastReducer.Action)
        case bannerAction(BannerReducer.Action)
        case closeTapped
        case undo(Undoable)
        case togglePlayPause(Clip)
        case markClipAsPlayed(Clip)
        case updateCurrentlyEditingClip(Clip)
        case resetCurrentlyEditingClip
        case delegate(Delegate)
        case moreTappedOnRootClip
        case showTapToMakeMoreExtensionsTooltip
        case resetTapToSeeMoreExtensionsTooltip
        case didTapShowOriginal
        case destination(PresentationAction<Destination.Action>)

        public enum Delegate {
            case closeTapped
            case didGetFullClip(Clip)
            case setupPlayer([ChildClip])
            case addClipToPlayer(ChildClip)
            case togglePlayPause(Clip)
            case reloadEditorWithClips([ChildClip])
            case removeClipFromPlayer(ChildClip)
        }

        public enum Internal {
            case fetchRootClip(ClipID)
            case fetchRootClipResponse(Result<Clip, Error>)
            case fetchClipChildren(Clip)
            case fetchClipChildrenResponse(Result<[ChildClip], Error>)
            case checkCompletion
            case checkCompletionResponse(Result<[Clip], Error>)
            case getFullClip(Clip)
            case getFullClipResponse(Clip.ID, Result<Clip, Error>)
            case reopenClipEditsWithOriginalClip
            case deleteClipResponse(Result<Void, Error>, Clip)
            case removeClip(Clip.ID)
            case restoreClip(Clip.ID)
            case undoDeleteClip(Clip)
        }

        public enum Undoable: Equatable {
            case deleteClip(Clip)
        }
    }

    public var body: some ReducerOf<Self> {
        Scope(state: \.toast, action: \.toast) {
            ToastReducer()
        }
        Scope(state: \.bannerState, action: \.bannerAction) {
            BannerReducer()
        }
        Reduce<State, Action> { state, action in
            struct CheckCompletionCancellableId: Hashable {}
            switch action {
            case .task:
                guard let rootClip = state.rootClip else {
                    state.shouldReloadEditor = true
                    return .send(.internal(.fetchRootClip(state.rootClipId)))
                }
                return .send(.internal(.fetchClipChildren(rootClip)))

            case .internal(.fetchRootClip(let clipId)):
                return .run { send in
                    await send(.internal(.fetchRootClipResponse(.init(catching: {
                        try await api.getClip(clipId.remoteId)
                    }))))
                }

            case .internal(.fetchRootClipResponse(.success(let rootClip))):
                // If we're just fetching the root clip, fetch clip children after
                guard state.shouldReloadEditor else { return .none }
                state.rootClip = rootClip
                return .send(.internal(.fetchClipChildren(rootClip)))

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

            case .internal(.fetchClipChildren(let clip)):
                return .run { send in
                    do {
                        // We need to call getFeedByIds on the clip edits in order to make sure
                        // we have the latest status, since getClipEdits doesn't guarantee that.
                        // This makes sure a streaming or complete clip shows up correctly after
                        // the user closes and re-opens the screen.
                        let clipChildren = try await api.getClipEdits(clipId: clip.id)
                        let clips = try await api.getFeedByIds(clipChildren.map { $0.clip.id.remoteId })
                        // Update the clip children with the latest details
                        let updatedClipChildren = clipChildren.enumerated().map { index, childClip in
                            var updated = childClip
                            updated.clip = clips[index]
                            return updated
                        }
                        await send(.internal(.fetchClipChildrenResponse(.success(updatedClipChildren))))
                    } catch {
                        await send(.internal(.fetchClipChildrenResponse(.failure(error))))
                    }
                }

            case .internal(.fetchClipChildrenResponse(let result)):
                switch result {
                case .success(let childClips):
                    state.unreadClips.removeAll()

                    // Filter to only show extensions
                    let clipEdits = childClips.filter { $0.clip.task == .extend }

                    // We reverse the clips so the first clip in the list is the latest edit
                    var orderedClipEdits = clipEdits
                    orderedClipEdits.reverse()

                    state.clipEdits.append(contentsOf: orderedClipEdits.compactMap { childClip in
                        let clip = childClip.clip
                        let extendOffset = childClip.extendOffset
                        let totalDuration = childClip.totalDuration
                        let editNumber = childClip.index

                        // If playcount is zero, we consider the clip "unread"
                        var hasPlayedForFirstTime = false
                        if clip.playCount == 0 {
                            state.unreadClips.append(clip.id)
                        } else {
                            hasPlayedForFirstTime = true
                        }

                        return .init(
                            clip: clip,
                            me: state.$me,
                            editNumber: editNumber,
                            hasPlayedForFirstTime: hasPlayedForFirstTime,
                            rootClipDuration: state.rootClip?.duration ?? 0,
                            isCurrentlyEditing: clip.id == state.currentlyEditingClipId,
                            extendOffset: extendOffset,
                            totalDuration: totalDuration
                        )
                    })

                    var effects: [Effect<Action>] = []

                    // If there's an incomplete clip, we start polling on it
                    if let firstIncompleteClip = orderedClipEdits.first(where: { $0.clip.status != .streaming && $0.clip.status != .complete }) {
                        state.currentlyPollingClipId = firstIncompleteClip.clip.id
                        effects.append(.run { send in
                            await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                                for await _ in clock.timer(interval: .seconds(3)) {
                                    await send(.internal(.checkCompletion))
                                }
                            }
                        })
                    }

                    // Setup the player with the base clip and the latest edits
                    guard let rootClip = state.rootClip else { return .none }

                    let rootClipItem = ChildClip(clip: rootClip)

                    let clipItemsToPlay = [rootClipItem] + orderedClipEdits

                    if state.shouldReloadEditor {
                        state.shouldReloadEditor = false
                        effects.append(.send(.delegate(.reloadEditorWithClips(clipItemsToPlay))))
                    } else {
                        effects.append(.send(.delegate(.setupPlayer(clipItemsToPlay))))
                    }

                    return .merge(effects)

                case .failure(let error):
                    log.telemetry.error(error)

                    // TEMP PATCH:
                    // If we fail to fetch clip children but have a root clip, we can try to fail gracefully
                    // This happens consistently when we try to extend a song that doesn't belong to the user.
                    guard let rootClip = state.rootClip else { return .none }
                    let rootClipItem = ChildClip(clip: rootClip)
                    return .send(.delegate(.setupPlayer([rootClipItem])))
                }

            case .addClips(let clips):
                // Hide any tooltips
                state.showTapToMakeMoreExtensionsTooltip = false

                for clip in clips {
                    guard !state.clipEdits.contains(where: { $0.clip.id == clip.id })
                    else { continue }

                    // When adding new clips, we look at the last clip's edit number
                    // and increment it by 1
                    let editNumber = (state.clipEdits.first?.editNumber ?? 0) + 1

                    // Add the newly generated clip to the list and mark it as unread
                    let newClipState = ClipEditsListItem.State(
                        clip: clip,
                        me: state.$me,
                        editNumber: editNumber,
                        hasPlayedForFirstTime: false,
                        rootClipDuration: state.rootClip?.duration ?? 0,
                        isCurrentlyEditing: false
                    )

                    state.clipEdits.insert(newClipState, at: 0)
                    state.unreadClips.append(clip.id)
                }

                // Check if we're already polling on another clip first
                guard let firstIncompleteClip = state.clipEdits.last(where: { $0.clip.status != .streaming && $0.clip.status != .complete })?.clip,
                      state.currentlyPollingClipId == nil
                else {
                    return .none
                }

                // Start polling on the first incomplete clip
                state.currentlyPollingClipId = firstIncompleteClip.id

                return .run { send in
                    await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                        for await _ in clock.timer(interval: .seconds(3)) {
                            await send(.internal(.checkCompletion))
                        }
                    }
                }

            case let .clipEdits(.element(_, action: .delegate(.didSelectClip(clip)))):
                return .send(.didSelectEdit(clip))

            case let .clipEdits(.element(_, action: .delegate(.togglePlayPause(clip)))):
                return .send(.togglePlayPause(clip))

            case .internal(.checkCompletion):
                guard let clipId = state.currentlyPollingClipId else { return .none }
                return .run { send in
                    await send(.internal(.checkCompletionResponse(.init(catching: { try await api.getFeedByIds([clipId.remoteId]) }))))
                }

            case .internal(.checkCompletionResponse(.success(let clips))):
                // We only poll on one clip at a time, so we get one clip back
                guard let clip = clips.first, state.currentlyPollingClipId == clip.id else { return .none }

                // If the clip is in error, we show a banner and stop polling
                guard clip.status != .error else {
                    return .concatenate(
                        .cancel(id: CheckCompletionCancellableId()),
                        .send(.bannerAction(.show(type: .warning(.string(L10n.FeatureCreateClip.errorTitle)), autoDismiss: true)))
                    )
                }

                guard (clip.status == .complete || clip.status == .streaming) && !clip.largeImageUrl.isEmpty else {
                    return .none
                }

                if let url = URL(string: clip.largeImageUrl) {
                    // Cache the large image so it's ready to display on the OmniPlayer
                    RemoteImagePrefetcher.shared.loadImages(urls: [url])
                }

                var effects: [Effect<Action>] = []

                // Update the clip in the list and poll on the next clip
                if let clipIndex = state.clipEdits.firstIndex(where: { $0.clip.id == clip.id }) {
                    state.clipEdits[clipIndex].clip = clip

                    // Add the clip to the player only if we successfully updated the state
                    let clipEdit = state.clipEdits[clipIndex]
                    let clipItem = ChildClip(clip: clip, index: clipEdit.editNumber)
                    effects.append(.send(.delegate(.addClipToPlayer(clipItem))))
                }

                // If there's another clip to poll on, start polling on it
                if let nextClip = state.clipEdits.last(where: { $0.clip.status != .streaming && $0.clip.status != .complete })?.clip {
                    state.currentlyPollingClipId = nextClip.id
                } else {
                    state.currentlyPollingClipId = nil
                    effects.append(.cancel(id: CheckCompletionCancellableId()))
                }

                return .merge(effects)

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

            case .didSelectEdit(let clip):
                return .merge(
                    .send(.internal(.getFullClip(clip))),
                    .send(.clipEdits(.element(id: clip.id, action: .setLoadingFullClip(true))))
                )

            case .internal(.getFullClip(let clip)):
                return .run { send in
                    await send(.internal(.getFullClipResponse(clip.id, .init(catching: { try await api.getFullClip(clip.id) }))))
                }

            case .internal(.getFullClipResponse(let clipId, .success(let clip))):
                return .merge(
                    .send(.clipEdits(.element(id: clipId, action: .setLoadingFullClip(false)))),
                    .send(.delegate(.didGetFullClip(clip)))
                )

            case .internal(.getFullClipResponse(let clipId, .failure(let error))):
                log.telemetry.error(error)
                return .merge(
                    .send(.clipEdits(.element(id: clipId, action: .setLoadingFullClip(false)))),
                    .send(.toast(.show(.warning(L10n.FeatureCreateClip.errorTitle, .string(L10n.FeatureCreateClip.error)))))
                )

            case .closeTapped:
                return .send(.delegate(.closeTapped))

            case .togglePlayPause(let clip):
                // Only toggle play/pause if the clip is ready to play
                let isReadyToPlay = clip.status == .complete || clip.status == .streaming
                guard isReadyToPlay else { return .none }
                return .send(.delegate(.togglePlayPause(clip)))

            case .markClipAsPlayed(let clip):
                state.unreadClips.removeAll(where: { $0 == clip.id })
                if var clipEditState = state.clipEdits.first(where: { $0.clip.id == clip.id }) {
                    clipEditState.hasPlayedForFirstTime = true
                    state.clipEdits[id: clip.id] = clipEditState
                }
                return .none

            case .resetCurrentlyEditingClip:
                guard let currentlyEditingClipId = state.currentlyEditingClipId else { return .none }
                state.currentlyEditingClipId = nil
                return .send(.clipEdits(.element(id: currentlyEditingClipId, action: .setCurrentlyEditing(false))))

            case .updateCurrentlyEditingClip(let clip):
                var effects: [Effect<Action>] = []
                if let previousClipId = state.currentlyEditingClipId {
                    effects.append(.send(.clipEdits(.element(id: previousClipId, action: .setCurrentlyEditing(false)))))
                }
                state.currentlyEditingClipId = clip.id
                effects.append(.send(.clipEdits(.element(id: clip.id, action: .setCurrentlyEditing(true)))))
                return .merge(effects)

            case .moreTappedOnRootClip:
                guard let rootClip = state.rootClip else { return .none }
                state.destination = .songActions(SongActions.State(clip: rootClip, playlist: nil, me: state.$me))
                return .none

            case .showTapToMakeMoreExtensionsTooltip:
                state.showTapToMakeMoreExtensionsTooltip = true
                state.$hasSeenTapToMakeMoreExtensionsTooltip.withLock { $0 = true }
                return .none

            case .resetTapToSeeMoreExtensionsTooltip:
                state.showTapToMakeMoreExtensionsTooltip = false
                return .none

            case .didTapShowOriginal:
                return .send(.internal(.reopenClipEditsWithOriginalClip))

            case .internal(.reopenClipEditsWithOriginalClip):
                // Uses the concat history to find the last full-clip in the chain
                guard let rootClipId = state.rootClip?.concatHistory?.rootClipId else { return .none }
                state.shouldReloadEditor = true
                state.clipEdits.removeAll()
                return .send(.internal(.fetchRootClip(rootClipId)))

            case .clipEdits(.element(_, action: .destination(.presented(.songActions(.delegate(.deleteClip(let clip))))))):
                // Explicitly set destination to nil first
                state.destination = nil
                return .run { send in
                    await send(.internal(.deleteClipResponse(Result(catching: { try await apiClient.trashClip(clip, true) }), clip)))
                }
                .concatenate(with: .send(.clipEdits(.element(id: clip.id, action: .destination(.dismiss)))))
                .concatenate(with: .send(.internal(.removeClip(clip.id)), animation: .default))

            case .internal(.deleteClipResponse(.success, let clip)):
                state.recentAction = .deleteClip(clip)
                return .send(.toast(.show(.success(L10n.FeatureCatalog.songDeleted, position: .bottom, trailingView: .undo))))

            case let .internal(.deleteClipResponse(.failure(error), _)):
                log.telemetry.error(error)
                return .send(.toast(.show(.warning(L10n.FeatureCatalog.actionFailed, position: .bottom))))

            case .internal(.removeClip(let clipId)):
                guard let index = state.clipEdits.firstIndex(where: { $0.clip.id == clipId }) else { return .none }
                let clipState = state.clipEdits.remove(at: index)
                let clipItem = ChildClip(clip: clipState.clip, index: clipState.editNumber)
                state.recentlyRemovedClips.append(.init(index: index, state: clipState, clipItem: clipItem))
                return .send(.delegate(.removeClipFromPlayer(clipItem)))

            case .internal(.restoreClip(let clipId)):
                guard let removedClipItem = state.recentlyRemovedClips.removeClip(id: clipId) else { return .none }
                state.clipEdits.insert(removedClipItem.state, at: removedClipItem.index)
                return .send(.delegate(.addClipToPlayer(removedClipItem.clipItem)))

            case let .undo(action):
                switch action {
                case let .deleteClip(clip):
                    return .send(.internal(.undoDeleteClip(clip)))
                }

            case .internal(.undoDeleteClip(let clip)):
                // Make sure destination is nil to prevent sheet from reappearing
                state.destination = nil
                return .merge(
                    .run { _ in try await apiClient.trashClip(clip, false) },
                    .run { send in
                        await send(.internal(.restoreClip(clip.id)), animation: .default)
                    }
                )

            case .toast,
                 .clipEdits,
                 .bannerAction,
                 .delegate,
                 .destination:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .forEach(\.clipEdits, action: \.clipEdits) {
            ClipEditsListItem()
        }

        Analytics()
    }
}

public struct ClipEditsScreen: View {
    @Bindable var store: StoreOf<ClipEdits>
    @State var animateTapToMakeMoreExtensions: Bool = false
    public init(store: StoreOf<ClipEdits>) {
        self.store = store
    }

    public var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                HStack {
                    Text(L10n.FeatureEditClip.clipEditsTitle)
                        .typographyV1(.headline3)
                    Spacer()
                    ToolbarButton(.close, background: Color.SemanticV1.backgroundQuaternary) {
                        store.send(.closeTapped)
                    }
                }
                .padding(.horizontal, 16)
                .padding(.top, 10)
                if let rootClip = store.rootClip {
                    loadedView(rootClip)
                } else {
                    Spacer()
                }
            }
            .padding(.top, 50)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.SemanticV1.backgroundPrimary)
            .clipShape(RoundedRectangle(cornerRadius: 40))
            .edgesIgnoringSafeArea(.vertical)
            .task { store.send(.task) }
            .sheet(item: $store.scope(state: \.destination?.songActions, action: \.destination.songActions)) { store in
                // Only for showing the menu on the Root Clip
                // Child clips call this directly in `ClipEditsListItem`
                SongActionsMenu(store: store)
            }
            .overlay(alignment: .bottom) {
                VStack(spacing: .zero) {
                    // TODO: Fix animation to only show this once
                    Text(L10n.FeatureEditClip.openCollapsedEditorTooltip)
                        .typographyV1(.caption3)
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                        .opacity(animateTapToMakeMoreExtensions ? 0.9 : 0.5)
                    Image.Icon.chevronDown
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                        .opacity(animateTapToMakeMoreExtensions ? 0.9 : 0.5)
                        .offset(y: animateTapToMakeMoreExtensions ? -2 : -8.0)
                        .animation(
                            .linear(duration: 1).repeatForever(autoreverses: true),
                            value: animateTapToMakeMoreExtensions
                        )
                }
                .offset(y: -150)
                .opacity(store.showTapToMakeMoreExtensionsTooltip ? 1.0 : 0.0)
            }
            .overlay(alignment: .top) {
                ToastView(store: store.scope(state: \.toast, action: \.toast), undoAction: {
                    guard let action = store.recentAction else { return }
                    store.send(.undo(action))
                })
            }
            .onChange(of: store.showTapToMakeMoreExtensionsTooltip) { _, shouldShowTooltip in
                guard shouldShowTooltip else { return }
                animateTapToMakeMoreExtensions = true
            }
        }
    }

    @ViewBuilder
    func loadedView(_ rootClip: Clip) -> some View {
        VStack(spacing: 20) {
            clipDetails(rootClip)
            editsListHeader
            if !store.state.clipEdits.isEmpty {
                editsList
            } else {
                Spacer()
            }
        }
    }

    @ViewBuilder
    var editsListHeader: some View {
        HStack {
            HStack(spacing: 4) {
                Image.Icon.editSparkle
                    .renderingMode(.template)
                    .foregroundColor(Color.SemanticV1.iconBrand)
                    .frame(width: 16)
                Text(L10n.FeatureEditClip.numberOfEdits(store.clipEdits.count).uppercased())
                    .typographyV1(.monospace)
                    .foregroundColor(Color.SemanticV1.textBrand)
            }
            .padding(.horizontal, 10)
            .padding(.vertical, 4)
            .background(RoundedRectangle(cornerRadius: 16).foregroundColor(Color.SemanticV1.backgroundSecondary))
            Spacer()
            Button {
                store.send(.didTapShowOriginal)
            } label: {
                Text(L10n.FeatureEditClip.showOriginal.uppercased())
                    .typographyV1(.monospace)
                    .foregroundColor(Color.SemanticV1.textBrand)
                    .padding(.leading, 8)
                    .padding(.trailing, 10)
                    .padding(.vertical, 4)
                    .background(RoundedRectangle(cornerRadius: 16).foregroundColor(Color.SemanticV1.backgroundSecondary))
            }
            .opacity(store.showShowOriginalButton ? 1.0 : 0.0)
        }
        .padding(.horizontal, 10)
    }

    @ViewBuilder
    var editsList: some View {
        ScrollView {
            VStack(spacing: 10) {
                ForEach(store.scope(state: \.clipEdits, action: \.clipEdits)) { clipEditStore in
                    ClipEditsListItemView(store: clipEditStore)
                }
            }
            .padding(.horizontal, 10)
            .padding(.bottom, 200)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    @ViewBuilder
    func clipDetails(_ rootClip: Clip) -> some View {
        HStack(spacing: 10) {
            Group {
                RemoteImage(url: rootClip.imageUrl, fallbackId: rootClip.id.remoteId)
                    .frame(width: 54, height: 72)
                    .clipShape(.rect(cornerRadius: 4))
                    .overlay(alignment: .center) {
                        playPauseIcon(isPlaying: store.isPlayingRootClip)
                    }

                VStack(alignment: .leading, spacing: 0) {
                    Text(rootClip.title)
                        .typographyV1(.body2thin.lineHeight(24).neueMontrealMedium())
                        .tracking(0.6)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .lineLimit(1)
                        .minimumScaleFactor(0.75)
                    if let createdAt = store.rootClipCreatedAt {
                        Text(createdAt)
                            .typographyV1(.body2.lineHeight(28).neueMontrealMedium())
                            .tracking(0.6)
                            .foregroundStyle(Color.SemanticV1.textSecondary)
                    }
                    Spacer().frame(height: 8)
                    HStack(spacing: 8) {
                        playCountLabel(rootClip)
                        upvoteCountLabel(rootClip)
                    }
                }
            }
            .onTapGesture {
                store.send(.togglePlayPause(rootClip))
            }
            Spacer()
            Button {
                store.send(.moreTappedOnRootClip)
            } label: {
                Image.Icon.moreVertical
                    .foregroundColor(Color.SemanticV1.iconBrand)
                    .frame(width: 24, height: 24)
            }
        }
        .padding(5)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .foregroundColor(store.currentlyEditingClipId == rootClip.id ? Color.SemanticV1.backgroundQuaternary : Color.SemanticV1.backgroundSecondary)
        )
        .padding(.horizontal, 10)
    }

    @ViewBuilder
    private func playPauseIcon(isPlaying: Bool) -> some View {
        if isPlaying {
            Image.Icon.pause
                .foregroundColor(Color.SemanticV1.textInvert)
                .frame(width: 40, height: 40)
        } else {
            Image.Icon.playFilled
                .foregroundColor(Color.SemanticV1.textInvert)
                .frame(width: 40, height: 40)
        }
    }

    private func playCountLabel(_ rootClip: Clip) -> some View {
        HStack(spacing: 6) {
            Image.Icon.playFilled
                .resizable()
                .foregroundColor(.SemanticV1.iconBrand)
                .frame(width: 12, height: 12)

            Text(rootClip.playCount.formatted(.number.notation(.compactName)))
                .typographyV1(.monospace)
                .foregroundColor(.SemanticV1.iconBrand)
        }
    }

    private func upvoteCountLabel(_ rootClip: Clip) -> some View {
        HStack(spacing: 4) {
            Image.Icon.thumbsUpFilled15V1
                .resizable()
                .foregroundColor(.SemanticV1.iconBrand)
                .frame(width: 16, height: 16)
            Text(rootClip.upvoteCount.formatted(.number.notation(.compactName)))
                .typographyV1(.monospace)
                .foregroundColor(.SemanticV1.iconBrand)
        }
    }
}
