import Foundation
import os

/// Fout die de deadline-race gooit als de primaire (cloud) transcriptie niet
/// binnen de drempel antwoordt.
struct TranscriptionFailoverTimeout: Error {}

/// Racet een lopende taak tegen een tijdslimiet.
/// - Returns: het resultaat van `task` als die binnen `seconds` klaar is.
/// - Throws: de fout van `task` als die eerder faalt, of `TranscriptionFailoverTimeout`
///   als de drempel verstrijkt. Spiegelt het bestaande patroon in
///   `NativeAppleTranscriptionService.waitForResultStream`.
func raceAgainstDeadline<T: Sendable>(_ task: Task<T, Error>, seconds: TimeInterval) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask { try await task.value }
        group.addTask {
            try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
            throw TranscriptionFailoverTimeout()
        }
        guard let result = try await group.next() else {
            throw TranscriptionFailoverTimeout()
        }
        group.cancelAll()
        return result
    }
}

/// Decorator rond een cloud-transcriptieservice die bij een stall of netwerkfout
/// automatisch terugvalt op lokale Whisper-transcriptie, zodat een dictaat nooit
/// verloren gaat. Wordt door `TranscriptionServiceRegistry` voor alle cloud-providers
/// teruggegeven, zodat zowel het opname-pad (`FileTranscriptionSession`) als het
/// bestandsimport-pad (`registry.transcribe`) de failover automatisch krijgen.
final class FailoverTranscriptionService: TranscriptionService {
    /// Enige knop om aan te draaien: hoeveel seconden we op de cloud wachten
    /// voor we lokaal gaan. Voxtral antwoordt normaal in ~1s.
    static let defaultDeadline: TimeInterval = 2.0

    private let primary: TranscriptionService
    private let fallback: TranscriptionService
    private weak var modelProvider: (any WhisperModelProvider)?
    private let deadline: TimeInterval
    private let logger = Logger(subsystem: "com.noaber.proat", category: "FailoverTranscriptionService")

    init(
        primary: TranscriptionService,
        fallback: TranscriptionService,
        modelProvider: (any WhisperModelProvider)?,
        deadline: TimeInterval = FailoverTranscriptionService.defaultDeadline
    ) {
        self.primary = primary
        self.fallback = fallback
        self.modelProvider = modelProvider
        self.deadline = deadline
    }

    func transcribe(audioURL: URL, model: any TranscriptionModel) async throws -> String {
        // Geen lokaal model gedownload → failover is onmogelijk. Doe de cloud-call
        // dan ongelimiteerd (geen drempel die een trage-maar-werkende call zou killen).
        guard let fallbackModel = await resolveFallbackModel() else {
            return try await primary.transcribe(audioURL: audioURL, model: model)
        }

        let cloudTask = Task { [primary] in
            try await primary.transcribe(audioURL: audioURL, model: model)
        }

        do {
            return try await raceAgainstDeadline(cloudTask, seconds: deadline)
        } catch {
            // Drempel verstreken óf cloud-fout → cloud-taak stoppen en lokaal verder.
            cloudTask.cancel()
            let reason = (error is TranscriptionFailoverTimeout)
                ? "geen antwoord binnen \(deadline)s"
                : error.localizedDescription
            ProatDebugLog.write("FAILOVER cloud→lokaal | reden=\(reason) | model=\(fallbackModel.name)")
            logger.notice("Cloud-transcriptie viel terug op lokaal Whisper: \(reason, privacy: .public)")
            await notifyFailover()
            let text = try await fallback.transcribe(audioURL: audioURL, model: fallbackModel)
            ProatDebugLog.write("FAILOVER lokaal resultaat | chars=\(text.count)")
            return text
        }
    }

    /// Kiest het lokale Whisper-model voor de failover: bij voorkeur het al
    /// geladen model, anders het eerste gedownloade. Nil als er niets is.
    private func resolveFallbackModel() async -> (any TranscriptionModel)? {
        guard let provider = modelProvider else { return nil }
        let available = await provider.availableModels
        let loadedName = await provider.loadedWhisperModel?.name
        let chosen = available.first(where: { $0.name == loadedName }) ?? available.first
        guard let file = chosen else { return nil }
        return FailoverWhisperModel(name: file.name)
    }

    private func notifyFailover() async {
        await MainActor.run {
            NotificationManager.shared.showNotification(
                title: "Mistral reageerde niet — lokaal getranscribeerd",
                type: .warning,
                duration: 4.0
            )
        }
    }
}

/// Minimale `TranscriptionModel` voor de lokale failover. `WhisperTranscriptionService`
/// gebruikt enkel `provider` (.whisper), `name` (om het modelbestand te matchen) en
/// `displayName` (logging), dus dit volstaat zonder afhankelijk te zijn van de
/// vooraf-gedefinieerde modellenlijst.
private struct FailoverWhisperModel: TranscriptionModel {
    let id = UUID()
    let name: String
    var displayName: String { "Lokaal (\(name))" }
    var description: String { "Lokale failover toen de cloud-transcriptie niet reageerde." }
    let provider: ModelProvider = .whisper
    var isMultilingualModel: Bool { true }
    var supportedLanguages: [String: String] { [:] }
    var supportsStreaming: Bool { false }
}
