import APIClient
import AsyncAlgorithms
import AVFoundation
import BlendedCreateClient
import Combine
import ComposableArchitecture
import EventBusClient
import FeatureHooksModels
import Foundation
import Localization
import MusicPlayerClient
import OpenAPIRuntime
import PlayerUtilities
import Utilities

// swiftlint:disable file_length

/*
 SnippetPlayerClient can play any `ClipSnippet` - which is a portion of a clip or song.
 Snippets are used in Hooks. They indicate the portion of a clip that the user wants to
 play in their Hook. Each `ClipSnippet` has a `Clip`, `startTime`, and `endTime`.

 This can also be used in any future features that require a preview of a clip.

 This can be configured to repeat the snippet or simply stop at the end.
 */

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

public enum SnippetPlayerClientEvent {
    case snippetChanged(ClipSnippet)
    case playbackStateChanged(AVPlayer.TimeControlStatus)
    case playbackTimeUpdated(currentTime: CMTime)
}

@DependencyClient
public struct SnippetPlayerClient {
    public enum ContentType: Equatable {
        case videoCover
        case scene
        case hook
        case custom(caption: String, maximumDuration: Double)

        public var maximumDuration: Double {
            switch self {
            case .videoCover:
                return 10.0
            case .scene:
                return 30.0
            case .hook:
                return HooksConstants.maxHookMediaDuration
            case .custom(_, let maximumDuration):
                return maximumDuration
            }
        }
    }

    // Stream for events
    public var stream: @Sendable () -> AsyncStream<SnippetPlayerClientEvent> = { .never }

    // Setup and cleanup
    public var setup: @Sendable (_ repeats: Bool) -> Void = { _ in }
    public var teardown: @Sendable () -> Void = {}

    // Player controls
    public var playCurrentClip: @Sendable () -> Void = {}
    public var pauseCurrentClip: @Sendable () -> Void = {}
    public var togglePlayPause: @Sendable () -> Void = {}
    public var loadSnippet: @Sendable (ClipSnippet) async -> Void = { _ in }
    public var loadAndPlaySnippet: @Sendable (ClipSnippet) -> Void = { _ in }
    public var updateSnippet: @Sendable (ClipSnippet, Double, Double, Bool) -> Void = { _, _, _, _ in } // clip, startTime, endTime, autoPlay
    public var seekTo: @Sendable (CMTime) -> Void = { _ in }
    public var restartCurrentClip: @Sendable () -> Void = {}

    // Volume controls
    public var setVolume: @Sendable (Float) -> Void = { _ in }
    public var getVolume: @Sendable () -> Float = { 1.0 }

    // Player attachment
    public var detachFromPlayer: @Sendable () async -> Void = {}
    public var attachToPlayer: @Sendable () async -> Void = {}
    public var isAttachedToPlayer: @Sendable () async -> Bool = { true }

    // Misc
    public var toggleLike: @Sendable (Clip) -> Void = { _ in }
    public var toggleDislike: @Sendable (Clip) -> Void = { _ in }

    // Content Type
    public var getContentType: @Sendable () async -> ContentType = { .hook }
    public var setContentType: @Sendable (ContentType) async -> Void = { _ in }
}

// MARK: - Default Implementations

public extension SnippetPlayerClient {
    /// Default empty implementation with all methods doing minimal work
    static var empty: Self {
        Self()
    }
}

extension SnippetPlayerClient: DependencyKey {
    public static let liveValue: Self = {
        @Dependency(\.apiClientV2) var apiClientV2
        @Dependency(MusicPlayerClient.self) var playerClient

        // Create subject for events
        let subject = PassthroughSubject<SnippetPlayerClientEvent, Never>()

        // Create state actor to manage shared state
        let state = SnippetPlayerState()

        // Get maximum snippet duration from content type
        @Sendable
        func getMaxSnippetDuration() async -> Double {
            let contentType = await state.contentType
            return contentType.maximumDuration
        }

        // Maximum snippet length - initialized with default .hook value
        var maxSnippetDuration: Double = ContentType.hook.maximumDuration

        let performSeek: @Sendable (CMTime) -> Void = { time in
            Task {
                await playerClient.cancelPreload()
            }
            Task {
                try? await playerClient.seek(time)
            }
        }

        @Sendable
        func resetPlayerIfPastEndTime(_ elapsedTime: CMTime, repeats: Bool) async {
            // Get the current clip
            let currentClipSnippet = await state.currentClipSnippet
            guard let currentClipSnippet else { return }

            let maxSnippetDuration = await getMaxSnippetDuration()

            // Apply maximum limit based on content type
            let effectiveEndTime = min(
                currentClipSnippet.endTime,
                currentClipSnippet.startTime + maxSnippetDuration
            )

            // If we're past the snippet end time (with 30s limit), reset the player and continue playing
            if elapsedTime.seconds > effectiveEndTime, repeats {
                let startTime = CMTime(seconds: currentClipSnippet.startTime, preferredTimescale: 1000)
                // Seek back to start and continue playing (like ShareAsset)
                try? await playerClient.seek(startTime)
                playerClient.play()
            }
        }

        @Sendable
        func removeCurrentlyPlayingClip() async {
            // First mark that we're replacing the item to prevent other operations
            await state.setIsReplacingCurrentItem(true)

            // Then clear the current item
            _ = try? await playerClient.replaceCurrentItem(nil)

            // Signal that the playback time has been reset
            subject.send(.playbackTimeUpdated(currentTime: .zero))
        }

        @Sendable
        func prefetchClipAssets(for clips: [Clip]) {
            let preloadTasks = [
                Task.detached { [clips] in
                    let urls = clips.compactMap { URL(string: $0.audioUrl) }
                    await playerClient.preloadAssets(urls)
                },
                Task.detached { [clips] in
                    RemoteImagePrefetcher.shared.loadImages(urls: clips.compactMap { URL(string: $0.largeImageUrl) })
                },
            ]

            // Register the tasks with the state actor
            Task {
                for task in preloadTasks {
                    await state.addPreloadTask(task)
                }
            }
        }

        @Sendable
        func loadClip(_ hookSnippet: ClipSnippet, autoplay: Bool) async {
            // Make sure we're ready to load this clip
            do {
                // Check for cancellation at the very start - BEFORE any expensive operations
                try Task.checkCancellation()

                // Load audio - immediately replace current item for quick switching
                let audioUrl = hookSnippet.clip.playableSceneUrl?.absoluteString ?? hookSnippet.clip.audioUrl

                _ = try await playerClient.replaceCurrentItem(audioUrl)

                try Task.checkCancellation()

                // Make sure to update the state
                await state.setIsReplacingCurrentItem(false)

                // Check if user manually paused - if so, don't auto-play the new clip
                let isCurrentlyPlaying = await state.isPlaying
                let isManuallyPaused = await state.manuallyPaused
                let wasUserPaused = !isCurrentlyPlaying && isManuallyPaused

                if !wasUserPaused, autoplay {
                    // Only play if user didn't manually pause
                    let startTime = CMTime(seconds: hookSnippet.startTime, preferredTimescale: 1000)
                    try? await playerClient.seek(startTime)
                    playerClient.play()
                }
            } catch is CancellationError {
                log.debug("🚫 loadAndPlayClip: Cancelled for '\(hookSnippet.clip.title)'")
            } catch {
                log.telemetry.error(error, message: "❌ loadAndPlayClip: Error loading and playing clip '\(hookSnippet.clip.title)'")
            }
        }

        @Sendable
        func prepareClipChange(for hookSnippet: ClipSnippet, autoplay: Bool) async {
            let maxSnippetDuration = await getMaxSnippetDuration()

            // Apply maximum limit based on content type
            let effectiveEndTime = min(hookSnippet.endTime, hookSnippet.startTime + maxSnippetDuration)
            let limitedSnippet = ClipSnippet(
                clip: hookSnippet.clip,
                startTime: hookSnippet.startTime,
                endTime: effectiveEndTime
            )

            // Wait for the current clip to be removed
            await removeCurrentlyPlayingClip()

            do {
                await state.cancelPendingClipChangeTask()
                await playerClient.cancelPreload()

                // Store the snippet with 30s limit applied
                await state.setCurrentClipSnippet(hookSnippet)

                let startTime = CMTime(seconds: hookSnippet.startTime, preferredTimescale: 1000)
                // Reset time first to avoid progress bar positioning issues
                subject.send(.playbackTimeUpdated(currentTime: startTime))

                try Task.checkCancellation()

                // Create a task for playing the clip - use regular Task so it inherits cancellation from parent
                let task = Task {
                    do {
                        // Final cancellation check before starting expensive operation
                        try Task.checkCancellation()
                        await loadClip(hookSnippet, autoplay: autoplay)
                    } catch is CancellationError {
                        log.debug("🚫 loadAndPlayClipAtIndex: Cancelled during playback for '\(hookSnippet.clip.title)'")
                    } catch {
                        // No-op
                    }
                }

                // Store the task IMMEDIATELY so it can be cancelled
                await state.setPendingClipChangeTask(task)
            } catch is CancellationError {
                // No-op
            } catch {
                // No-op
            }
        }

        // Break up complex expressions to help Swift compiler
        let streamImpl: @Sendable () -> AsyncStream<SnippetPlayerClientEvent> = {
            // Convert PassthroughSubject to AsyncStream like in ShareAssetClient
            AsyncStream { continuation in
                let cancellable = subject.sink { event in
                    continuation.yield(event)
                }
                continuation.onTermination = { _ in
                    cancellable.cancel()
                }
            }
        }

        @Sendable
        func setupTasks() async {
            await state.setPlaybackStatusTask(
                Task {
                    for await status in playerClient.timeControlStatus() {
                        guard !Task.isCancelled else { break }
                        let isReplacingCurrentItem = await state.isReplacingCurrentItem
                        let manualPauseState = await state.manuallyPaused
                        if !isReplacingCurrentItem {
                            subject.send(.playbackStateChanged(status))
                            await state.setIsPlaying(status != .paused)
                        } else if manualPauseState {
                            subject.send(.playbackStateChanged(.paused))
                        }
                    }
                }
            )

            await state.setPlaybackTimeTask(
                Task {
                    for await time in playerClient.periodicTime() {
                        guard !Task.isCancelled else { break }
                        // Time updates are throttled in MusicPlayerClient
                        let isReplacingCurrentItem = await state.isReplacingCurrentItem
                        guard !isReplacingCurrentItem else { continue }
                        let isManuallyPaused = await state.manuallyPaused
                        guard !isManuallyPaused else { continue }
                        let repeats = await state.repeats

                        // Reset the player if we're past the snippet end time
                        await resetPlayerIfPastEndTime(time, repeats: repeats)

                        subject.send(.playbackTimeUpdated(currentTime: time))
                    }
                }
            )

            await state.setEndOfTrackTask(
                Task {
                    for await playerItem in playerClient.didPlayToEndTime() {
                        guard !Task.isCancelled else { break }

                        let currentClipSnippet = await state.currentClipSnippet
                        let isAttached = await state.isAttached
                        let shouldRepeat = await state.repeats

                        if !isAttached || currentClipSnippet == nil || !shouldRepeat {
                            playerClient.pause()
                            subject.send(.playbackStateChanged(.paused))
                            continue
                        }

                        // Check if the current snippet ends at the end of the clip
                        guard let currentClipSnippet else { continue }

                        let currentClipDuration = currentClipSnippet.clip.duration
                        let maxSnippetDuration = await getMaxSnippetDuration()

                        // Apply maximum limit based on content type
                        let effectiveEndTime = min(
                            currentClipSnippet.endTime,
                            currentClipSnippet.startTime + maxSnippetDuration
                        )

                        let shouldRepeatAtSnippetEnd = abs(effectiveEndTime - currentClipDuration) < 0.05
                        guard shouldRepeatAtSnippetEnd, shouldRepeat else { continue }

                        let startTime = CMTime(seconds: currentClipSnippet.startTime, preferredTimescale: 1000)
                        try? await playerClient.seek(startTime)
                        playerClient.play()
                    }
                }
            )
        }

        @Sendable
        func cancelTasks() async {
            await state.cancelPlaybackTimeTask()
            await state.cancelPlaybackStatusTask()
            await state.cancelEndOfTrackTask()
        }

        @Sendable
        func setupImpl(repeats: Bool) {
            Task {
                // Set finer time updates when editing Hooks and playing snippets
                await playerClient.setOwner(.snippetPlayer)
                await setupTasks()
                do {
                    try await playerClient.setup()
                    await state.setRepeat(repeats)
                } catch {
                    log.telemetry.error(error)
                }
            }
        }

        let pauseCurrentClipImpl: @Sendable () -> Void = {
            Task {
                // Mark as manually paused
                await state.setManuallyPaused(true)

                // Pause using player client
                playerClient.pause()
            }
        }

        let playCurrentClipImpl: @Sendable () -> Void = {
            Task {
                await state.setManuallyPaused(false)

                // Play using player client
                playerClient.play()
            }
        }

        return .init(
            stream: streamImpl,
            setup: { repeats in
                setupImpl(repeats: repeats)
            },
            teardown: {
                Task {
                    // Stop audio playback and remove the clip
                    playerClient.pause()
                    await removeCurrentlyPlayingClip()
                    // Clean up subscribers
                    await cancelTasks()
                    await state.clearCurrentClipSnippet()
                    // Always hand off to OmniPlayer when tearing down
                    // TODO: (BA): This can also be managed by a separate layer,
                    // and once we do, we can have something more graceful than
                    // defaulting to OmniPlayer
                    await playerClient.setOwner(.omniPlayer)
                }
            },
            // Player controls
            playCurrentClip: playCurrentClipImpl,
            pauseCurrentClip: pauseCurrentClipImpl,
            togglePlayPause: { @Sendable in
                Task {
                    // Toggle playing state
                    let isCurrentlyPlaying = await state.isPlaying

                    if !isCurrentlyPlaying {
                        await state.setManuallyPaused(false)
                        playerClient.play()
                    } else {
                        await state.setManuallyPaused(true)
                        playerClient.pause()
                    }
                }
            },
            loadSnippet: { snippet in
                await prepareClipChange(for: snippet, autoplay: false)
            },
            loadAndPlaySnippet: { snippet in
                // Cancel any pending clip change task first
                Task {
                    // Clear manual pause state to ensure playback starts
                    await state.setManuallyPaused(false)

                    await prepareClipChange(for: snippet, autoplay: true)
                }
            },
            updateSnippet: { snippet, startTime, endTime, autoPlay in
                Task {
                    await state.setIsReplacingCurrentItem(true)
                    let maxSnippetDuration = await getMaxSnippetDuration()
                    // Apply maximum limit based on content type
                    let effectiveEndTime = min(endTime, startTime + maxSnippetDuration)
                    await state.setCurrentClipSnippet(ClipSnippet(clip: snippet.clip, startTime: startTime, endTime: effectiveEndTime))
                    performSeek(CMTime(seconds: startTime, preferredTimescale: 1000))
                    guard autoPlay else { return }
                    await state.setManuallyPaused(false)
                    playerClient.play()
                    await state.setIsReplacingCurrentItem(false)
                }
            },
            seekTo: performSeek,
            restartCurrentClip: {
                Task {
                    try? await playerClient.seek(.zero)
                    subject.send(.playbackTimeUpdated(currentTime: .zero))
                }
            },
            // Volume controls
            setVolume: { volume in
                playerClient.setVolume(volume)
            },
            getVolume: {
                playerClient.getVolume()
            },
            // Player attachment
            detachFromPlayer: { @Sendable in
                Task {
                    await state.setIsAttached(false)
                    pauseCurrentClipImpl()
                }
            },
            attachToPlayer: { @Sendable in
                Task {
                    await state.setIsAttached(true)
                    playCurrentClipImpl()
                }
            },
            isAttachedToPlayer: {
                // Properly awaiting access to actor state
                await state.isAttached
            },
            toggleLike: { clip in
                Task {
                    @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
                    @Dependency(\.apiClientV2) var apiClientV2

                    // Send event to notify other parts of the app
                    sendClipEvent(.toggledLike(clip))

                    do {
                        try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil)
                    } catch {
                        var revertedClip = clip
                        revertedClip.isLiked.toggle()
                        sendClipEvent(.toggledLike(revertedClip))
                    }
                }
            },
            toggleDislike: { clip in
                Task {
                    @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
                    @Dependency(\.apiClientV2) var apiClientV2

                    // Send event to notify other parts of the app
                    sendClipEvent(.toggledLike(clip))

                    do {
                        try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil)
                    } catch {
                        var revertedClip = clip
                        revertedClip.isDisliked.toggle()
                        sendClipEvent(.toggledLike(revertedClip))
                    }
                }
            },
            getContentType: {
                await state.contentType
            },
            setContentType: { contentType in
                await state.setContentType(contentType)
            }
        )
    }()
}

public extension DependencyValues {
    var snippetPlayerClient: SnippetPlayerClient {
        get { self[SnippetPlayerClient.self] }
        set { self[SnippetPlayerClient.self] = newValue }
    }
}
