import APIClient
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import FeatureClipDetail
import Localization
import NavigationRouterClient
import StatsigClient
import SunoModelClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

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

    @ObservableState
    public struct State: Identifiable, Equatable {
        public var id: Clip.ID { clip.id }
        @Presents public var destination: Destination.State?
        public var clip: Clip

        var showPin: Bool {
            return FeatureFlag.clips.iosProfilePinning && shouldShowPinIfAvailable && clip.isPinned
        }

        public var tooltipToShow: Tooltip?
        public var shouldShowPinIfAvailable: Bool = true

        // If we're in the user's Library, we want to show
        // a loading indicator on new clips before they're
        // ready to play.
        public var isLibraryScreen: Bool

        @Shared var me: Me
        var playlist: Playlist?
        var position: Int

        @Shared(.inMemory(.playingClipKey)) var playingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var isPlaying: Bool = false
        @Shared(.inMemory(.hasSeenOmniPlayerTooltipThisSession)) var hasSeenTooltipThisSession: Bool = false
        @Shared(.appStorage(.hasSeenRemasterTooltip)) var hasSeenRemasterTooltip: Bool = false
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?

        var areCommentsEnabled: Bool {
            FeatureFlag.legacy.clipComments
        }

        var isReadyToPlay: Bool {
            return clip.status == .complete || clip.status == .streaming
        }

        public mutating func setPinGating(_ showPinIfAvailable: Bool) {
            self.shouldShowPinIfAvailable = showPinIfAvailable
        }

        public init(
            clip: Clip,
            me: Shared<Me>,
            position: Int,
            playlist: Playlist? = nil,
            isLibraryScreen: Bool = false
        ) {
            self.clip = clip
            self._me = me
            self.position = position
            self.playlist = playlist
            self.isLibraryScreen = isLibraryScreen
        }
    }

    public enum Action {
        case destination(PresentationAction<Destination.Action>)
        case moreTapped
        case authorTapped(String, String, String?)
        case delegate(Delegate)
        case delegateClient(DelegateClient)

        case showTooltip(Tooltip)
        case didDismissTooltip

        case upgradeTapped

        public enum Delegate {
            case deleteClip(Clip)
            case removeFromPlaylist(Clip, Playlist)
            case updateClip(Clip)
            case resumeUpload(Clip)
            case shouldResortPlaylist
            case shouldReloadPinnedList(_ pinnedItems: IdentifiedArray<ClipID, Clip>)
        }

        public enum DelegateClient {
            case authorTapped(String, String, String?)
        }
    }

    public init() {}

    @Dependency(ClipListItemDelegateClient.self) private var delegate
    @Dependency(NavigationRouterClient.self) private var navigationRouter
    @Dependency(CommentsClient.self) private var commentsClient
    @Dependency(\.eventBus.sendClipEvent) private var sendClipEvent

    public var body: some ReducerOf<Self> {
        Reduce<State, Action> { state, action in
            func sendDelegate(_ action: ClipListItem.Action.DelegateClient) -> Effect<Action> {
                delegate.send(action)
                return .send(.delegateClient(action))
            }
            switch action {
            case .moreTapped:
                if state.areCommentsEnabled {
                    commentsClient.enqueueGetCommentsForClip(clipID: state.clip.id)
                }
                sendClipEvent(.showSongActions(clip: state.clip, playlist: state.playlist))
                return .none

            case .authorTapped(let handle, let displayName, let avatarImageUrl):
                navigationRouter.send(.profile(handle, displayName: displayName, avatarImageUrl: avatarImageUrl))
                return sendDelegate(.authorTapped(handle, displayName, avatarImageUrl))
                // FIXME: Remove after Nav V2

            case .destination(.dismiss):
                state.destination = nil
                return .none

            case let .showTooltip(tooltip):
                state.tooltipToShow = tooltip
                return .none

            case .didDismissTooltip:
                state.tooltipToShow = nil
                return .none

            case .upgradeTapped:
                navigationRouter.send(.subscriptions)
                return .none

            case .destination, .delegate, .delegateClient:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        Analytics()
    }
}

public struct ClipListItemView: View {
    @Environment(\.colorScheme) private var colorScheme
    @Bindable var store: StoreOf<ClipListItem>
    @State var isAnimatingWaveform = false

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

    public var body: some View {
        clipView
            .listRowBackground(Color.clear)
            .alignmentGuide(.listRowSeparatorLeading) { d in
                d[.leading] + 24
            }
            .swipeActions(allowsFullSwipe: true) {
                if let playlist = store.playlist, playlist.isOwned {
                    deleteButton(for: .removeFromPlaylist(store.clip, playlist))
                } else if store.me.user.id == store.clip.userId, store.playlist == nil {
                    deleteButton(for: .deleteClip(store.clip))
                }
            }
            .sheet(item: $store.scope(state: \.destination?.songActions, action: \.destination.songActions)) { store in
                SongActionsMenu(store: store)
            }
    }

    private var clipView: some View {
        ClipListItemContent(
            me: store.$me,
            clip: store.clip,
            showPin: store.showPin,
            playingClipId: store.playingClip?.id,
            isPlaying: store.isPlaying,
            moreTapped: { store.send(.moreTapped) },
            authorTapped: { handle, displayName, avatarImageUrl in store.send(.authorTapped(handle, displayName, avatarImageUrl)) },
            upgradeTapped: { store.send(.upgradeTapped) },
            tooltipToShow: store.tooltipToShow,
            dismissTooltip: { store.send(.didDismissTooltip) },
            isLibraryScreen: store.isLibraryScreen,
            isReadyToPlay: store.isReadyToPlay,
            isPaidUser: store.billingInfo?.plan != nil
        )
    }

    private func deleteButton(for action: ClipListItem.Action.Delegate) -> some View {
        Button(role: .destructive) {
            UIImpactFeedbackGenerator(style: .medium).impactOccurred()
            store.send(.delegate(action))
        } label: {
            Label(
                title: { Text(L10n.FeatureClipList.delete) },
                icon: {
                    if colorScheme == .light {
                        Image.Icon.trashV1
                    } else {
                        Image.Icon.trashV1.renderingMode(.original)
                    }
                }
            )
            .typographyV1(.button1)
            .foregroundStyle(Color.SemanticV1.textInvert)
            .labelStyle(.titleAndIcon)
        }
        .tint(Color.SemanticV1.backgroundInvert)
    }
}

public struct ClipListItemContent: View {
    @State var isAnimatingWaveform = false
    @State var isAnimatingSpinner = false

    @Shared var me: Me
    var clip: Clip
    var playingClipId: Clip.ID?
    var isPlaying: Bool
    var showPin: Bool
    var moreTapped: (() -> Void)?
    var authorTapped: ((String, String, String?) -> Void)?
    var upgradeTapped: (() -> Void)?
    var tooltipToShow: Tooltip?
    var dismissTooltip: (() -> Void)?
    var isReadyToPlay: Bool
    var isLibraryScreen: Bool
    var isPaidUser: Bool

    var showEditLabel: Bool {
        clip.isFullSongFromEdits && clip.userId == me.user.id
    }

    var showExtensionLabel: Bool {
        // Extra check to make sure this an extension
        guard let history = clip.history, !history.clips.isEmpty else { return false }
        // Only show the label for the user's own clips
        return clip.task == .extend && clip.userId == me.user.id
    }

    var showCoverLabel: Bool {
        // Show the "Cover" label if the clip has a cover clip ID.
        return clip.coverClipId != nil
    }

    public init(
        me: Shared<Me>,
        clip: Clip,
        showPin: Bool,
        playingClipId: Clip.ID? = nil,
        isPlaying: Bool = false,
        moreTapped: (() -> Void)? = nil,
        authorTapped: ((String, String, String?) -> Void)? = nil,
        upgradeTapped: (() -> Void)? = nil,
        tooltipToShow: Tooltip? = nil,
        dismissTooltip: (() -> Void)? = nil,
        isLibraryScreen: Bool = false,
        isReadyToPlay: Bool = false,
        isPaidUser: Bool = false
    ) {
        self._me = me
        self.clip = clip
        self.showPin = showPin
        self.playingClipId = playingClipId
        self.isPlaying = isPlaying
        self.moreTapped = moreTapped
        self.authorTapped = authorTapped
        self.upgradeTapped = upgradeTapped
        self.tooltipToShow = tooltipToShow
        self.dismissTooltip = dismissTooltip
        self.isLibraryScreen = isLibraryScreen
        self.isReadyToPlay = !isLibraryScreen || isReadyToPlay
        self.isPaidUser = isPaidUser
    }

    private var showNewClipPinkDotIndicator: Bool {
        // Show the pink dot indicator only if the clip is new (playCount == 0)
        // and the user is the owner of the clip.
        return me.user.id == clip.userId && clip.playCount == 0 && isReadyToPlay
    }

    private var isCurrentlySelectedClip: Bool {
        playingClipId == clip.id
    }

    public var body: some View {
        HStack(spacing: 12) {
            image
                .overlay(alignment: .bottomTrailing) {
                    // Clip duration pill in the bottom right corner over the thumbnail
                    clipDurationOverlayPill
                }

            VStack(alignment: .leading, spacing: 2) {
                HStack(spacing: 8) {
                    if isCurrentlySelectedClip {
                        waveform
                    }

                    if clip.type == .preview {
                        Image.Icon.lockNew
                            .resizable()
                            .frame(width: 16, height: 16)
                            .foregroundColor(isCurrentlySelectedClip ? .SemanticV2.accentPink : .SemanticV1.textPrimary)
                            .opacity(0.5)
                            .padding(.trailing, -4)
                    }

                    Text(clip.title)
                        .typographyV1(.clipRowTitle)
                        .foregroundColor(isCurrentlySelectedClip ? .SemanticV2.accentPink : .SemanticV1.textPrimary)
                        .lineLimit(1)

                    if showPin {
                        Image.Icon.thumbtack
                            .resizable()
                            .renderingMode(.template)
                            .frame(width: 16.0, height: 16.0)
                            .foregroundColor(.SemanticV1.textPrimary)
                    }

                    // If this is a "Full Song" that the user made, show the "Edit" label
                    // on the clip list item row.
                    if showEditLabel {
                        Pill(type: .custom(label: L10n.FeatureClipList.editLabel))
                    } else if showExtensionLabel {
                        Pill(type: .custom(label: L10n.FeatureClipList.extensionLabel))
                    } else if showCoverLabel {
                        Pill(type: .custom(label: L10n.FeatureClipDetail.cover))
                    } else if clip.isAudioUpload {
                        Pill(type: .custom(label: L10n.FeatureClipDetail.upload))
                    }

                    if clip.type == .preview {
                        previewLabel
                    } else {
                        versionLabel
                    }
                }
                .animation(.snappy(duration: 0.25), value: isCurrentlySelectedClip)

                if let displayTags = clip.displayTags, !displayTags.isEmpty, isLibraryScreen == false, clip.highLevelModel == .v4_5 || clip.highLevelModel == .v5 {
                    Text(displayTags)
                        .typographyV1(.clipRowStyles)
                        .foregroundColor(.SemanticV2.foregroundTertiary)
                        .lineLimit(1)
                        .frame(maxWidth: .infinity, alignment: .leading)
                } else if !clip.tags.isEmpty {
                    TagText(clip.tags, font: TypographyV1.clipRowStyles)
                }

                if clip.type == .gen {
                    HStack(spacing: 8) {
                        if me.user.handle != clip.handle {
                            authorView
                        }
                        infoView(
                            icon: Image.Icon.playFilled,
                            text: String(clip.playCount.formatted(.number.notation(.compactName)))
                        )
                        infoView(
                            icon: Image.Icon.thumbsUpV2,
                            text: String(clip.localUpvoteCount.formatted(.number.notation(.compactName)))
                        )
                        infoView(
                            icon: Image.Icon.comment,
                            text: String(clip.commentCount.formatted(.number.notation(.compactName)))
                        )

                        if clip.isPublic, me.user.handle == clip.handle {
                            infoView(icon: Image.Icon.globe, text: "")
                        }
                    }
                    .padding(.top, 4)
                } else if let upgradeTapped, !clip.isUnlockingPreview(isPaidUser: isPaidUser) {
                    ModelPreviewUpgradeButton(action: upgradeTapped)
                        .padding(.vertical, 4)
                }
            }
            .multilineTextAlignment(.leading)
            .frame(maxWidth: .infinity, alignment: .leading)

            if clip.isLiked {
                Image.Icon.thumbsUpV2
                    .foregroundColor(.SemanticV1.iconPrimary)
            }

            if let moreTapped {
                Image.Icon.moreVertical
                    .resizable()
                    .renderingMode(.template)
                    .frame(width: 24, height: 24)
                    .foregroundStyle(Color.SemanticV2.foregroundInactive)
                    .onTapGesture {
                        UIImpactFeedbackGenerator(style: .light).impactOccurred()
                        moreTapped()
                    }
                    .modifier(
                        ClipListItemMoreButtonTooltipModifier(
                            showTooltip: Binding(
                                get: { tooltipToShow != nil },
                                set: { _ in }
                            ),
                            tooltip: tooltipToShow,
                            accentColor: Color.SemanticV1.auraPink,
                            buttonWidth: 24,
                            onDismiss: {}
                        )
                    )
            }
        }
        .contentShape(.rect)
        .padding(.vertical, 8)
        .disabled(!isReadyToPlay)
        .overlay(alignment: .leading) {
            if showNewClipPinkDotIndicator {
                Circle()
                    .fill(Color.SemanticV2.accentPink)
                    .frame(width: 6, height: 6)
                    .offset(x: -9)
                    .transition(.scale(scale: 0.8).combined(with: .opacity))
            }
        }
        .animation(.easeInOut(duration: 0.3), value: showNewClipPinkDotIndicator)
    }

    @ViewBuilder
    private var previewLabel: some View {
        if clip.isUnlockingPreview(isPaidUser: isPaidUser) {
            Pill(type: .modelPreview(version: .loading))
        } else {
            Pill(type: .modelPreview(version: .init(from: clip.highLevelModel)))
        }
    }

    @ViewBuilder
    private var versionLabel: some View {
        // Use ModelBadgeStyle if feature flag is enabled and style is available
        if FeatureFlag.legacy.v5LaunchModelStyles,
           let modelBadgeStyle = clip.modelBadgeStyle
        {
            Pill(type: .modelBadge(style: modelBadgeStyle))
        } else {
            // Fallback to default versioning pills
            switch clip.highLevelModel {
            case .previousToV4:
                EmptyView()

            case .v5:
                Pill(type: .versioning(version: .v5))

            case .v4:
                Pill(type: .versioning(version: .v4))

            case .v4_5:
                Pill(type: .versioning(version: .v4_5))

            case .v4_5Plus:
                Pill(type: .versioning(version: .v4_5Plus))
            }
        }
    }

    private var showDurationPill: Bool {
        isLibraryScreen && clip.duration > 0 // Only show when valid & in Library
    }

    @ViewBuilder
    private var clipDurationOverlayPill: some View {
        Text(clip.duration.format_m_ss)
            .typographyV1(.clipDuration)
            .padding(4)
            .foregroundStyle(.white)
            .background(RoundedRectangle(cornerRadius: 100).fill(.thinMaterial))
            .padding([.bottom, .trailing], 4)
            .environment(\.colorScheme, .dark)
            .opacity(showDurationPill ? 1 : 0) // Only show when valid & in Library
            .animation(.easeInOut(duration: 0.3), value: showDurationPill)
    }

    @ViewBuilder
    private var waveform: some View {
        Waveform(
            isAnimating: isAnimatingWaveform,
            barCount: 3,
            color: Color.SemanticV2.accentPink,
            barHeight: 12,
            barWidth: 1.5,
            barSpacing: 2
        )
        .frame(width: 9, height: 12)
        .padding(.trailing, -2)
        .transition(.opacity)
        .onDisappear { isAnimatingWaveform = false }
        .onAppear { isAnimatingWaveform = isPlaying }
        .onChange(of: isPlaying) { _, isPlaying in isAnimatingWaveform = isPlaying }
        .onChange(of: isCurrentlySelectedClip) { _, isSelected in
            if !isSelected {
                isAnimatingWaveform = false
            } else {
                isAnimatingWaveform = isPlaying
            }
        }
    }

    @ViewBuilder
    private var image: some View {
        let size: CGSize = .init(width: 52, height: 70)

        ZStack {
            RoundedRectangle(cornerRadius: 12)
                .foregroundColor(Color.SemanticV1.backgroundTertiary)
                .frame(width: size.width, height: size.height)
            if isReadyToPlay {
                RemoteImage(url: clip.imageUrl, fallbackId: clip.id.remoteId, requestedSize: size)
                    .clipShape(.rect(cornerRadius: 12))
                    .frame(width: size.width, height: size.height)
            } else {
                GradientSpinner(size: .extraLarge,
                                startColor: Color.SemanticV1.auraPink,
                                endColor: Color.SemanticV1.v4Blue)
            }
        }
        .frame(width: size.width, height: size.height)
        .overlay {
            // White border
            RoundedRectangle(cornerRadius: 12)
                .stroke(Color.SemanticV1.borderPrimary.opacity(0.10), lineWidth: 0.5)
        }
    }

    private var authorView: some View {
        HStack(spacing: 6) {
            RemoteImage(url: clip.avatarImageUrl, fallbackId: clip.userId)
                .frame(width: 16, height: 16)
                .clipShape(Circle())
            Text(clip.displayName)
                .typographyV1(.caption2.size { _ in 12.0 })
                .foregroundStyle(Color.SemanticV2.foregroundTertiary)
                .lineLimit(1)
        }
        .contentShape(.rect)
        .allowsHitTesting(authorTapped != nil)
        .onTapGesture {
            authorTapped?(clip.handle, clip.displayName, clip.avatarImageUrl)
        }
        .frame(height: 20)
    }

    private func infoView(icon: Image, text: String) -> some View {
        HStack(spacing: 2) {
            icon
                .resizable()
                .renderingMode(.template)
                .frame(width: 16, height: 16, alignment: .center)
                .foregroundStyle(Color.SemanticV2.foregroundTertiary)

            if text.isEmpty {
                EmptyView()
            } else {
                Text(text)
                    .lineLimit(1)
                    .typographyV1(.caption2.size { _ in 12.0 })
                    .foregroundStyle(Color.SemanticV2.foregroundTertiary)
                    .contentTransition(.numericText())
                    .animation(.easeInOut(duration: 0.3), value: text)
            }
        }
        .frame(height: 20)
    }

    @ViewBuilder
    private func plainLabel(text: String) -> some View {
        Text(text)
            .typographyV1(.pillText)
            .foregroundColor(.SemanticV2.foregroundTertiary)
            .padding(.horizontal, 6)
            .padding(.vertical, 2)
            .lineLimit(1)
            .minimumScaleFactor(0.8)
            .background(
                RoundedRectangle(cornerRadius: 34)
                    .fill(Color.SemanticV2.backgroundGlassThick)
            )
    }
}

private extension TypographyV1 {
    static let clipRowTitle: TypographyV1 = .init(
        name: "Clip Row Styles",
        size: 14,
        style: .body,
        weight: .ppNeueMontrealMedium,
        kerning: 0.14,
        lineHeight: 24
    )

    static let clipRowStyles: TypographyV1 = .init(
        name: "Clip Row Styles",
        size: 14,
        style: .body,
        weight: .ppNeueMontrealRegular,
        kerning: 0.24,
        lineHeight: 14
    )

    static let clipDuration: TypographyV1 = .init(
        name: "Clip Duration",
        size: 10,
        style: .body,
        weight: .inputSans,
        lineHeight: 16
    )

    static let pillText: TypographyV1 = .init(
        name: "Pill Text",
        size: 12,
        style: .body,
        weight: .ppNeueMontrealRegular,
        kerning: 0.84,
        lineHeight: 16
    )
}
