import APIClient
import ComposableArchitecture
import EventBusClient
import Foundation
import Utilities
import UIKit

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

/*
 Actor to manage play count batching for new plays:
 - Only counts plays after 1 second of listening
 - Max batch size is 10
 - Batch flush interval is 10 seconds
 - Batch is flushed when the max batch size is reached or the flush interval is reached
 - Batch is flushed when the app enters background (didEnterBackground)
 - Batch is flushed when the app is terminated (willTerminate)
 */
public actor PlayCountManager {
    private var playCountBatch: Set<Clip> = []
    private var batchFlushTimer: Task<Void, Error>?
    private let maxBatchSize = 10
    private let batchFlushInterval: TimeInterval = 10.0

    private var pendingPlayCounts: [Clip: Task<Void, Never>] = [:]
    private let playCountDelay: TimeInterval = 1.0

    private var lifecycleObservers: [NSObjectProtocol] = []

    public init() {
        Task { await setupLifecycleObservers() }
    }

    deinit {
        Task { @MainActor [lifecycleObservers] in
            for observer in lifecycleObservers {
                NotificationCenter.default.removeObserver(observer)
            }
        }
    }

    /// Start counting a play for the given clip - will only count after 1 second of listening
    public func startPlayCountTimer(for clip: Clip) {
        // Cancel any existing timer for this clip
        cancelPlayCountTimer(for: clip)

        // Start a new timer for this clip
        let timer = Task {
            do {
                try await Task.sleep(for: .seconds(playCountDelay))
                // If we reach here, the clip has been playing for 1 second
                addToPlayCountBatch(clip)
            } catch {
                // Timer was cancelled, don't count the play
            }
        }

        pendingPlayCounts[clip] = timer
    }

    /// Cancel the play count timer for the given clip
    public func cancelPlayCountTimer(for clip: Clip) {
        if let timer = pendingPlayCounts.removeValue(forKey: clip) {
            timer.cancel()
        }
    }

    private func addToPlayCountBatch(_ clip: Clip) {
        // Remove from pending timers since we're now counting it
        pendingPlayCounts.removeValue(forKey: clip)?.cancel()

        // Add to batch (Set automatically handles duplicates)
        playCountBatch.insert(clip)

        // Start flush timer if this is the first item
        if playCountBatch.count == 1 {
            startBatchFlushTimer()
        }

        // Flush immediately if we hit max batch size
        if playCountBatch.count >= maxBatchSize {
            Task {
                await flushPlayCountBatch()
            }
        }

        // Send clip event to update play count
        updateLocalPlayCountForClip(clip)
    }

    private func startBatchFlushTimer() {
        // Cancel any existing timer
        batchFlushTimer?.cancel()

        // Start new timer
        batchFlushTimer = Task {
            do {
                try await Task.sleep(for: .seconds(batchFlushInterval))
                await flushPlayCountBatch()
            } catch {
                // Task was cancelled, do nothing
            }
        }
    }

    private func flushPlayCountBatch() async {
        guard !playCountBatch.isEmpty else { return }

        let batchToFlush = Array(playCountBatch)
        playCountBatch.removeAll()

        // Cancel the timer since we're flushing
        batchFlushTimer?.cancel()
        batchFlushTimer = nil

        // Use detached task to prevent cancellation propagation
        Task.detached { [weak self] in
            await self?.sendPlayCountBatch(batchToFlush)
        }
    }

    private func sendPlayCountBatch(_ clips: [Clip]) async {
        @Dependency(\.apiClientV2) var apiClient
        do {
            try await apiClient.incrementPlayCounts(clips.map { $0.id })
        } catch {
            revertLocalPlayCount(for: clips)
            log.telemetry.error(error, message: "Increment play counts request failed.")
        }
    }

    public func forceFlushPlayCountBatch() async {
        await flushPlayCountBatch()
    }

    public func cancelAllPendingTimers() {
        for (_, timer) in pendingPlayCounts {
            timer.cancel()
        }
        pendingPlayCounts.removeAll()
    }

    private func setupLifecycleObservers() async {
        let willTerminateObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.willTerminateNotification,
            object: nil,
            queue: nil
        ) { [weak self] _ in
            Task { [weak self] in
                await self?.cancelAllPendingTimers()
                await self?.flushPlayCountBatch()
            }
        }

        let didEnterBackgroundObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.didEnterBackgroundNotification,
            object: nil,
            queue: nil
        ) { [weak self] _ in
            Task { [weak self] in
                await self?.cancelAllPendingTimers()
                await self?.flushPlayCountBatch()
            }
        }

        lifecycleObservers = [willTerminateObserver, didEnterBackgroundObserver]
    }

    // Increment play count for the clip in different surface areas in the app
    private func updateLocalPlayCountForClip(_ clip: Clip) {
        @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
        var updatedClip = clip
        updatedClip.playCount += 1
        sendClipEvent(.updateClip(updatedClip))
    }

    // Gets called if the API request fails and we need to revert the optimistically
    // incremented play count
    private func revertLocalPlayCount(for clips: [Clip]) {
        @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
        for clip in clips {
            var updatedClip = clip
            updatedClip.playCount -= 1
            sendClipEvent(.updateClip(updatedClip))
        }
    }
}

extension PlayCountManager: DependencyKey {
    public static let liveValue = PlayCountManager()
}

public extension DependencyValues {
    var playCountManager: PlayCountManager {
        get { self[PlayCountManager.self] }
        set { self[PlayCountManager.self] = newValue }
    }
}
