import APIClient
import AsyncAlgorithms
import Combine
import ComponentLibrary
import ComposableArchitecture
import CoreMedia.CMTime
import Foundation
import Localization
import Waveform

@DependencyClient
public struct LyricsClientV2 {
    public enum Event {
        case didStartPollingForAlignedLyrics(_ clip: Clip)
        case didPollClipForAlignedLyrics(_ clip: Clip)
        case didSucceedPollingLyrics(_ clip: Clip, lyricsData: LyricsDataV2, waveformData: WaveformData)
        case didFailPolling(_ clip: Clip, error: Error)
    }

    public var stream: () -> AsyncStream<LyricsClientV2.Event> = { .never }
    public var streamForClip: (_ clip: Clip) -> AsyncStream<LyricsClientV2.Event> = { _ in .never }

    public var getLyricsForClip: (_ clip: Clip) -> Void = { _ in }
}

extension LyricsClientV2: DependencyKey {
    public static var liveValue: LyricsClientV2 {
        @Dependency(APIClient.self) var apiClient
        @Dependency(APIClientV2.self) var apiClientV2
        let subject = PassthroughSubject<LyricsClientV2.Event, Never>()

        let lyricsCache = LyricsCache()

        func getLyricsForClipHelper(
            _ clip: Clip,
            continuation: AsyncStream<Event>.Continuation? = nil
        ) async {
            let cachedData = await lyricsCache.getLyricsAndWaveform(for: clip)
            if let lyrics = cachedData.lyrics, let waveform = cachedData.waveform {
                let event: Event = .didSucceedPollingLyrics(clip, lyricsData: lyrics, waveformData: waveform)
                subject.send(event)
                continuation?.yield(event)
            } else {
                await fetchLyricsAndWaveform(
                    clip: clip,
                    lyricsCache: lyricsCache,
                    subject: subject,
                    apiClient: apiClient,
                    apiClientV2: apiClientV2,
                    continuation: continuation
                )
            }
        }

        return Self(
            stream: {
                UncheckedSendable(subject.values).eraseToStream()
            },
            streamForClip: { clip in
                AsyncStream { continuation in
                    Task {
                        await getLyricsForClipHelper(clip, continuation: continuation)
                        continuation.finish()
                    }
                }
            },
            getLyricsForClip: { clip in
                Task {
                    await getLyricsForClipHelper(clip)
                }
            }
        )
    }
}

private extension LyricsClientV2 {
    static func fetchLyricsAndWaveform(
        clip: Clip,
        lyricsCache: LyricsCache,
        subject: PassthroughSubject<Event, Never>,
        apiClient: APIClient,
        apiClientV2: APIClientV2,
        continuation: AsyncStream<Event>.Continuation? = nil
    ) async {
        let pollingInterval: TimeInterval = 5.0

        func processAndSendLyrics(_ statusResponse: AlignedLyrics, freshClipData: Clip) async {
            let lyricsLines = statusResponse.alignedLyrics.mappedToLyricsLines
            let lyricsData = LyricsDataV2(lines: lyricsLines)

            let waveformData = WaveformData(
                normalizedArray: statusResponse.waveformData,
                totalDuration: freshClipData.duration,
                emptyWaveformStandInStyle: .noiseySinwave
            )

            await lyricsCache.setLyricsAndWaveform(lyricsData, waveformData, for: freshClipData)

            let event: Event = .didSucceedPollingLyrics(
                freshClipData,
                lyricsData: lyricsData,
                waveformData: waveformData
            )
            subject.send(event)
            continuation?.yield(event)
        }

        var shouldContinuePolling = true
        let startEvent: Event = .didStartPollingForAlignedLyrics(clip)
        subject.send(startEvent)
        continuation?.yield(startEvent)

        do {
            let result = try await apiClient.getAlignedLyrics(clip)

            let lyricsReady: Bool = !result.isStreamed
            if lyricsReady {
                let latestClipData = try await apiClientV2.getClip(clip.id.remoteId)
                await processAndSendLyrics(result, freshClipData: latestClipData)
                return
            }

            while shouldContinuePolling {
                guard !Task.isCancelled else { break }

                let pollEvent: Event = .didPollClipForAlignedLyrics(clip)
                subject.send(pollEvent)
                continuation?.yield(pollEvent)
                try await Task.sleep(for: .seconds(pollingInterval))

                let statusResponse = try await apiClient.getAlignedLyrics(clip)
                if statusResponse.isStreamed {
                    // Still streaming - continue polling
                    continue
                } else {
                    // Lyrics are ready
                    shouldContinuePolling = false
                    let latestClipData = try await apiClientV2.getClip(clip.id.remoteId)
                    await processAndSendLyrics(statusResponse, freshClipData: latestClipData)
                }
            }
        } catch {
            let failEvent: Event = .didFailPolling(clip, error: error)
            subject.send(failEvent)
            continuation?.yield(failEvent)
        }
    }
}

private extension [AlignedLyricLine] {
    var mappedToLyricsLines: [LyricsLine] {
        guard !self.isEmpty else { return [] }

        return self.map { lyricLine in
            let wordTokens = lyricLine.words.map { word in
                WordToken(
                    text: word.text,
                    startTime: word.startS,
                    endTime: word.endS
                )
            }
            return .init(
                text: lyricLine.text,
                startTime: lyricLine.startS,
                endTime: lyricLine.endS,
                section: lyricLine.section,
                words: wordTokens
            )
        }
    }
}

public extension DependencyValues {
    var lyricsClientV2: LyricsClientV2 {
        get { self[LyricsClientV2.self] }
        set { self[LyricsClientV2.self] = newValue }
    }
}
