import APIClient
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import CoreMedia
import FeatureCaptions
import FeatureComments
import Localization
import OmniPlayerClient
import Utilities

// MARK: - Comments Sheet

extension ExpandedPlayerReducer {
    func openCommentsSheet(state: inout State, for _: Clip, didOpenFromDeeplink: Bool = false) -> Effect<Action> {
        @Dependency(\.commentsClient) var commentsClient
        @Dependency(OmniPlayerClient.self) var omniplayerClient
        omniplayerClient.setPlaybackConfigurationOverride(.init(repeatMode: .one, shuffle: .off))
        let user = state.player.me.user
        let clipID = state.clip.id
        let authorUserHandle = state.clip.handle
        let isAuthorOfClip = state.clip.userId == state.player.me.user.id
        let comments = state.clipCommentThread?.allSortedUnparentedComments ?? []
        let currentTime = state.player.elapsedTime
        let authorCaption = state.clip.caption.map { caption in
            AuthorCaption(
                userID: state.clip.userId,
                userDisplayName: state.clip.displayName,
                userAvatarURL: state.clip.avatarImageUrl,
                content: caption,
                captionMentions: state.clip.captionMentions
            )
        }

        state.didOpenCommentsSheetForDifferentClip = didOpenFromDeeplink

        commentsClient.setCommentsCount(state.clip.commentCount, clipID)
        commentsClient.setCommentsActivationState(.activeOnClip(clipID))

        state.destination = .comments(.init(
            authorCaption: authorCaption,
            authorUserHandle: authorUserHandle,
            currentUser: user,
            clipID: clipID,
            isClipAuthorCurrentUser: isAuthorOfClip,
            comments: comments,
            elapsedTrackTime: currentTime
        ))

        // Handle routing into a comment
        if let commentID = state.deepLinkCommentID {
            state.deepLinkCommentID = nil
            return .run { send in
                try await Task.sleep(for: .milliseconds(100)) // delay makes sure keyboard opens correctly
                await send(.destination(.presented(.comments(.timeSyncedCommentTapped(commentID)))))
            }
        } else {
            return .none
        }
    }

    func dismissCommentsSheet(state: inout State) -> Effect<Action> {
        @Dependency(\.commentsClient) var commentsClient
        @Dependency(OmniPlayerClient.self) var omniplayerClient
        omniplayerClient.clearPlaybackConfigurationOverrides()
        commentsClient.setCommentsActivationState(.notActive)
        state.destination = nil
        return .none
    }
}

// MARK: - Playback

extension ExpandedPlayerReducer {
    func handlePlayTapped(state: inout State) -> Effect<Action> {
        switch state.source {
        case .hooksFeed:
            return .run { _ in
                @Dependency(OmniPlayerClient.self) var omniplayerClient
                omniplayerClient.playCurrentClip()
            }

        default:
            return .run { send in
                await send(.delegate(.playTapped))
            }
        }
    }

    func handlePauseTapped(state: inout State) -> Effect<Action> {
        switch state.source {
        case .hooksFeed:
            return .run { _ in
                @Dependency(OmniPlayerClient.self) var omniplayerClient
                omniplayerClient.pauseCurrentClip()
            }

        default:
            return .run { send in
                await send(.delegate(.pauseTapped))
            }
        }
    }

    func handleDidStartScrubbing(state: inout State, time: CMTime) -> Effect<Action> {
        switch state.source {
        case .hooksFeed:
            return .send(.playback(.binding(.set(\.scrub, .scrubbing))))
        default:
            return .run { send in
                await send(.delegate(.didStartScrubbing(time)))
            }
        }
    }

    func handleDidEndScrubbing(state: inout State, time: CMTime) -> Effect<Action> {
        switch state.source {
        case .hooksFeed:
            return .send(.playback(.binding(.set(\.scrub, .complete(time)))))
        default:
            return .run { send in
                await send(.delegate(.didEndScrubbing(time)))
            }
        }
    }

    func handlePrevTapped(state: inout State) -> Effect<Action> {
        guard state.player.displayTime.seconds < state.clipRestartTimeThreshold else {
            return .send(.delegate(.prevTapped))
        }
        let newIndex = max(0, state.selectedItemIndex - 1)
        state.selectedItemIndex = newIndex
        if state.didOpenCommentsSheetForDifferentClip {
            // Clear any queue override that may have been set
            omniplayerClient.clearQueueOverride(autoPlay: false, cause: .skipBackward)
            state.didOpenCommentsSheetForDifferentClip = false
        }
        if case .ready = state.generationState {
            state.generationState = .idle
        }
        return .send(.delegate(.prevTapped))
    }
}

// MARK: - Omniplayer Event Handling

extension ExpandedPlayerReducer {
    func handleOmniplayerEvent(state: inout State, event: OmniPlayerEvent) -> Effect<Action> {
        switch event {
        case .clipChanged(let clip, let index):
            state.lyricsData = nil

            state.$player.withLock {
                $0.clip = clip
            }

            if let providedIndex = index, state.player.queue.indices.contains(providedIndex) {
                state.selectedItemIndex = providedIndex
            }

            // Set the playing clip here since we don't have OmniPlayerPlaybackReducer for hooksFeed
            if state.source == .hooksFeed {
                @Shared(.inMemory(.playingClipKey)) var playingClip: Clip?
                $playingClip.withLock { $0 = clip }
                hydrateRelationshipsForClip(clip.id)
            }

            return .none

        case .lyricsLoaded(let clip, let lyricsData):
            guard clip.id == state.clip.id else { return .none }
            state.lyricsData = lyricsData
            return .none

        case .lyricsFailed(let clip, let error):
            guard clip.id == state.clip.id else { return .none }
            state.lyricsData = nil
            return .none

        case .timeSyncedCommentShouldShow(let comment):
            state.currentTimeSyncedComment = comment
            state.isShowingTimeSyncedComment = true
            return .none

        case .timeSyncedCommentShouldHide:
            state.isShowingTimeSyncedComment = false
            return .none

        case .playbackTimeUpdated(let time):
            // Only update from here if we're not coming from the HooksFeed,
            // since OmniPlayer manages playback state for ExpandedPlayer
            // when available
            guard state.source == .hooksFeed else { return .none }

            // Clear the last scrub time if we're close
            if let lastScrubTime = state.playbackState?.scrub?.time {
                let timeDifference = abs(time.seconds - lastScrubTime.seconds)
                if timeDifference < 0.5 {
                    state.playbackState?.scrub = nil
                }
            }

            // Don't update the elapsed time if we're scrubbing
            guard state.playbackState?.scrub == nil else { return .none }

            let normalizedTime = CMTimeConvertScale(time, timescale: 1000, method: .default)
            state.$player.withLock { $0.elapsedTime = normalizedTime }
            return .none

        case .playbackStateChanged(let status):
            // Only update from here if we're not coming from the HooksFeed,
            // since OmniPlayer manages playback state for ExpandedPlayer
            // when available
            guard state.source == .hooksFeed else { return .none }
            state.$player.withLock { $0.timeControlStatus = status }
            return .none

        case .queueUpdated(let queue):
            guard state.source == .hooksFeed else { return .none }
            state.$player.withLock { player in
                player.queue = IdentifiedArrayOf(uniqueElements: queue.enumerated().map { index, clip in
                    QueueClip(clip: clip, position: index)
                })
            }
            return .none

        default:
            return .none
        }
    }
}

// MARK: - Remix Deeplink Handling

extension ExpandedPlayerReducer {
    func handleRemixDeeplink(state: inout State, remixType: RemixType?, style: String?, lyrics: String?) -> Effect<Action> {
        // If the current clip can't be remixed, show an error
        guard state.player.hasRemixableClip else {
            return .send(.setToast(.warning(L10n.FeatureOmniPlayer.remixErrorToast)))
        }
        if let remixType {
            let generationType: GenerationType = .text
            // Build a prompt and pipe deeplink metadata if needed
            let prompt = Prompt(
                title: state.clip.title,
                lyrics: lyrics ?? state.clip.prompt, // Pull the lyrics from the deeplink if available
                styles: style ?? state.clip.tags, // Pull the styles from the deeplink if available
                text: state.clip.gptDescriptionPrompt,
                generationType: generationType
            )

            switch remixType {
            case .cover:
                getCreateChannel().queue(.coverClip(state.clip, prompt: prompt))
            case .extend:
                getCreateChannel().queue(.extendClip(state.clip, prompt: prompt))
            }
        } else {
            // Open remix sheet
            state.destination = .remix(.init(
                me: state.$player.me,
                clip: state.clip
            ))
        }
        return .none
    }
}

// MARK: - Constants

extension ExpandedPlayerReducer {
    enum Constants {
        /// Time before restarting vs going to previous
        static let clipRestartTimeThreshold: TimeInterval = 3.0
    }
}
