import Algorithms
import AnalyticsClient
import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureClipDetail
import FeatureShare
import LyricsClient
import MediaPlayer
import OmniPlayerClient
import PlayerUtilities
import StatsigClient
import Utilities

private let log = Logger(category: "OmniPlayerPlaybackReducer")

@Reducer
public struct OmniPlayerPlaybackReducer {
    public typealias State = OmniPlayerPlaybackState

    @CasePathable
    public enum Action: BindableAction {
        case setup
        case replaceCurrentItemResponse(Clip)
        case binding(BindingAction<State>)
        case `internal`(Internal)
        case omniplayerEvent(OmniPlayerEvent)

        public enum Internal {
            case setupCompletion(Result<Void, Error>)
            case timeControlStatusResponse(Result<AVPlayer.TimeControlStatus, Error>)
            case periodicTimeResponse(CMTime)
        }
    }

    @Dependency(APIClient.self) var apiClient
    @Dependency(\.mainQueue) var mainQueue
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.omniplayerClient) var omniplayerClient
    @Dependency(\.omniplayerClient.stream) var omniplayerStream

    public init() {}

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            struct OmniplayerEventsSubscription: Hashable {}
            switch action {
            case .setup:
                return .stream(omniplayerStream(), send: Action.omniplayerEvent, cancellableId: OmniplayerEventsSubscription())

            case .omniplayerEvent(let event):
                switch event {
                case .playbackTimeUpdated(currentTime: let time):
                    return .send(.internal(.periodicTimeResponse(time)))

                case .playbackStateChanged(let status):
                    return .send(.internal(.timeControlStatusResponse(.success(status))))

                case .clipChanged(let clip, _):
                    return .send(.replaceCurrentItemResponse(clip))

                case .queueUpdated(let queue):
                    state.queue = IdentifiedArrayOf(uniqueElements: queue.enumerated().map { index, clip in
                        QueueClip(clip: clip, position: index)
                    })
                    return .none

                case .playbackScrubbingTimeout:
                    state.scrub = nil
                    return .none

                default:
                    return .none
                }

            case .internal(.timeControlStatusResponse(.success(let status))):
                state.timeControlStatus = status
                return .none

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

            case .internal(.periodicTimeResponse(let time)):
                // Clear the last scrub time if we're close
                if let lastScrubTime = state.scrub?.time {
                    let timeDifference = abs(time.seconds - lastScrubTime.seconds)
                    if timeDifference < 0.5 {
                        state.scrub = nil
                    } else if state.isPlayingNewGeneration && timeDifference < 2.0 {
                        // Lower tolerance when streaming,
                        // otherwise we rely on `.playbackScrubbingTimeout` above
                        // to clear the scrub state
                        state.scrub = nil
                    }
                }

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

                let normalizedTime = CMTimeConvertScale(time, timescale: 1000, method: .default)
                state.elapsedTime = normalizedTime
                return .none

            case .internal(.setupCompletion(.success)):
                return .none

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

            case let .replaceCurrentItemResponse(clip):
                guard state.playingClip?.id != clip.id else { return .none }
                state.$playingClip.withLock { $0 = clip }
                state.clip = clip
                state.scrub = nil
                return .none

            case .binding(\.scrub):
                guard case .complete(let time) = state.scrub else { return .none }
                omniplayerClient.seekTo(time)
                return .none

            case .binding:
                // Catch-all
                return .none
            }
        }
    }
}
