import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import MediaPlayer
import OmniPlayerClient
import PlayerClient
import Utilities

/*
 VideoSongArtFullscreenPreviewPlayer is a simplified player specifically designed
 for previewing video covers in the ReplaceVideo feature. It only handles
 a single clip and basic playback functionality.
 */
@Reducer
public struct VideoSongArtFullscreenPreviewPlayer {
    @ObservableState
    public struct State: Equatable, Identifiable {
        public var id: Clip.ID { clip.id }
        public var clip: Clip
        public var elapsedTime: CMTime = .zero
        public var totalTime: CMTime = .zero
        public var fallbackTotalTimeSeconds: Double { clip.isScene ? 30 : 120 }
        public var isScrubbing: Bool { scrub != nil }
        public var displayTime: CMTime { scrub?.time ?? elapsedTime }
        public var timeControlStatus: AVPlayer.TimeControlStatus = .waitingToPlayAtSpecifiedRate
        public var isPlaying: Bool = false
        public var scrub: ScrubState?

        @Shared(.inMemory(.playingClipKey)) var previousPlayingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var wasOmniPlayerPlaying: Bool = false

        @ObservableState
        public enum ScrubState: Equatable {
            case scrubbing(CMTime)
            case complete(CMTime)

            public var time: CMTime {
                switch self {
                case .scrubbing(let time), .complete(let time): return time
                }
            }
        }

        public init(clip: Clip) {
            self.clip = clip
        }
    }

    public enum Action: BindableAction {
        case binding(BindingAction<State>)
        case setup
        case play
        case pause
        case restart
        case seek(Double)
        case `internal`(Internal)
        case delegate(Delegate)
        case teardown

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

        public enum Delegate {
            case setupComplete
        }
    }

    @Dependency(PlayerClient.self) var playerClient
    @Dependency(VideoCoverClient.self) var videoCoverClient
    @Dependency(\.mainQueue) var mainQueue
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.eventBus.getOmniplayerChannel) private var omniplayerChannel
    @Dependency(\.omniplayerClient.pauseCurrentClip) private var pauseCurrentClip

    public var body: some ReducerOf<Self> {
        BindingReducer()
            .onChange(of: \.scrub) { oldValue, newValue in
                Reduce { _, _ in
                    switch (oldValue, newValue) {
                    case (.scrubbing, .complete(let time)):
                        return .run { _ in
                            playerClient.seek(time)
                        }

                    default:
                        return .none
                    }
                }
            }
        Reduce { state, action in
            struct TimeControlStatusPublisherCancellable: Hashable {}
            struct ElapsedTimePublisherCancellable: Hashable {}
            struct DidPlayToEndTimeNotificationCancellable: Hashable {}

            switch action {
            case .setup:
                pauseCurrentClip()
                videoCoverClient.pause()
                videoCoverClient.seek(.zero)
                return .merge(
                    .send(.internal(.setupCompletion(Result(catching: { try playerClient.setup() })))),
                    .publisher {
                        playerClient.timeControlStatus()
                            .map { .internal(.timeControlStatusResponse(.success($0))) }
                    }
                    .cancellable(id: TimeControlStatusPublisherCancellable(), cancelInFlight: true)
                )

            case .teardown:
                playerClient.pause()
                omniplayerChannel().queue(.refresh)
                return .none

            case .internal(.setupCompletion(.success)):
                guard !state.clip.audioUrl.isEmpty else { return .none }

                let url = state.clip.playableSceneUrl?.absoluteString ?? state.clip.audioUrl
                return .run { send in
                    let duration = await playerClient.replaceCurrentItemAndPlay(url)
                    await send(.internal(.replaceCurrentItemResponse(duration)))
                    await send(.delegate(.setupComplete))
                }

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

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

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

            case .internal(.replaceCurrentItemResponse(let duration)):
                state.totalTime = duration
                return .merge(
                    .publisher {
                        // Specify the notification object to ensure we only receive notifications for the current player item, not when the video loops
                        NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: playerClient.getCurrentItem())
                            .map { _ in .internal(.didPlayToEndTime) }
                    }
                    .cancellable(id: DidPlayToEndTimeNotificationCancellable()),
                    .publisher {
                        playerClient.periodicTime()
                            .map { .internal(.periodicTimeResponse($0)) }
                    }
                    .cancellable(id: ElapsedTimePublisherCancellable(), cancelInFlight: true)
                )

            case .internal(.periodicTimeResponse(let time)):
                state.elapsedTime = time

                switch state.scrub {
                case .complete(let time):
                    state.elapsedTime = time
                    state.scrub = nil
                    return .none

                case .scrubbing, nil:
                    return .none
                }

            case .internal(.didPlayToEndTime):
                // Restart the clip and video
                state.elapsedTime = .zero
                state.scrub = nil
                playerClient.seek(.zero)
                playerClient.play()
                return .none

            case .play:
                playerClient.play()
                return .none

            case .pause:
                playerClient.pause()
                return .none

            case .restart:
                playerClient.seek(.zero)
                return .none

            case .seek(let time):
                let cmTime = CMTime(seconds: time, preferredTimescale: 1000)
                playerClient.seek(cmTime)
                return .none

            case .delegate, .binding:
                return .none
            }
        }
    }
}

enum VideoCoverPreviewPlayerError: Error {
    case invalidURL
}
