import Foundation
import AVFoundation
import SwiftData
import os

/// Handles the full post-recording pipeline:
/// transcribe → filter → format → word-replace → prompt-detect → AI enhance → start paste + dismiss → save
@MainActor
class TranscriptionPipeline {
    private let modelContext: ModelContext
    private let serviceRegistry: TranscriptionServiceRegistry
    private let enhancementService: AIEnhancementService?
    private let promptDetectionService = PromptDetectionService()
    private let logger = Logger(subsystem: "com.noaber.proat", category: "TranscriptionPipeline")

    var licenseViewModel: LicenseViewModel

    init(
        modelContext: ModelContext,
        serviceRegistry: TranscriptionServiceRegistry,
        enhancementService: AIEnhancementService?
    ) {
        self.modelContext = modelContext
        self.serviceRegistry = serviceRegistry
        self.enhancementService = enhancementService
        self.licenseViewModel = LicenseViewModel()
    }

    /// Run the full pipeline for a given transcription record.
    /// - Parameters:
    ///   - transcription: The pending Transcription SwiftData object to populate and save.
    ///   - audioURL: The recorded audio file.
    ///   - model: The transcription model to use.
    ///   - session: An active streaming session if one was prepared, otherwise nil.
    ///   - onStateChange: Called when the pipeline moves to a new recording state (e.g. `.enhancing`).
    ///   - shouldCancel: Returns true if the user requested cancellation.
    ///   - onCleanup: Called when cancellation is detected to release model resources.
    ///   - onDismiss: Called as soon as paste is initiated to dismiss the recorder panel.
    func run(
        transcription: Transcription,
        audioURL: URL,
        model: any TranscriptionModel,
        session: TranscriptionSession?,
        onStateChange: @escaping (RecordingState) -> Void,
        shouldCancel: () -> Bool,
        onCleanup: @escaping () async -> Void,
        onDismiss: @escaping () async -> Void
    ) async {
        if shouldCancel() {
            await onCleanup()
            return
        }

        var finalPastedText: String?
        var promptDetectionResult: PromptDetectionService.PromptDetectionResult?
        var didInsertSessionMetric = false

        do {
            let transcriptionStart = Date()
            var text: String
            if let session {
                text = try await session.transcribe(audioURL: audioURL)
            } else {
                text = try await serviceRegistry.transcribe(audioURL: audioURL, model: model)
            }
            let rawTranscription = text
            text = TranscriptionOutputFilter.filter(text)
            // Laag 1 taal-gate: filter() geeft "" terug als de output overwegend
            // niet-Latijns schrift was (bv. Voxtral koos abusievelijk Russisch).
            // Speel het "afgebroken"-geluid zodat de gebruiker weet dat er bewust
            // niets geplakt wordt en de zin opnieuw ingesproken moet worden.
            // Respecteert de bestaande geluid-feedback-toggle.
            if text.isEmpty, TranscriptionOutputFilter.isPredominantlyNonLatinScript(rawTranscription) {
                SoundManager.shared.playEscSound()
            }
            let transcriptionDuration = Date().timeIntervalSince(transcriptionStart)

            let powerModeManager = PowerModeManager.shared
            let activePowerModeConfig = powerModeManager.currentActiveConfiguration
            let powerModeName = (activePowerModeConfig?.isEnabled == true) ? activePowerModeConfig?.name : nil
            let powerModeEmoji = (activePowerModeConfig?.isEnabled == true) ? activePowerModeConfig?.emoji : nil

            if shouldCancel() { await onCleanup(); return }

            text = text.trimmingCharacters(in: .whitespacesAndNewlines)

            if UserDefaults.standard.bool(forKey: "IsTextFormattingEnabled") {
                text = WhisperTextFormatter.format(text)
            }

            text = WordReplacementService.shared.applyReplacements(to: text, using: modelContext)
            let cleanedText = TranscriptionOutputFilter.applyUserCleanupPreferences(text)

            let audioAsset = AVURLAsset(url: audioURL)
            let actualDuration = (try? CMTimeGetSeconds(await audioAsset.load(.duration))) ?? 0.0

            transcription.text = cleanedText
            transcription.duration = actualDuration
            transcription.transcriptionModelName = model.displayName
            transcription.transcriptionDuration = transcriptionDuration
            transcription.powerModeName = powerModeName
            transcription.powerModeEmoji = powerModeEmoji
            finalPastedText = cleanedText

            if let enhancementService, enhancementService.isConfigured {
                let detectionResult = await promptDetectionService.analyzeText(text, with: enhancementService)
                promptDetectionResult = detectionResult
                await promptDetectionService.applyDetectionResult(detectionResult, to: enhancementService)
            }

            let isSkipShortEnhancementEnabled = UserDefaults.standard.bool(forKey: "SkipShortEnhancement")
            let savedThreshold = UserDefaults.standard.integer(forKey: "ShortEnhancementWordThreshold")
            let shortEnhancementWordThreshold = savedThreshold > 0 ? savedThreshold : 3
            // Geen merk/domein-uitzondering meer: de regex-woordvervanging draait
            // hierboven (regel ~84) en heeft de merknaam al perfect gemaakt vóór
            // deze stap. Een kaal "noaber.ai" door Gemini halen kost 3-14s en
            // levert aantoonbaar niks op (retention 1.00) — dus korte uitingen
            // mogen gewoon skippen (instant, al correct). Echte zinnen mét een
            // merknaam hebben wordCount > drempel en worden sowieso verfijnd.
            let isShortSkip = isSkipShortEnhancementEnabled && WordCounter.count(in: text) <= shortEnhancementWordThreshold
            // Slimme skip-laag: tekst die al schoon is (hoofdletter-start,
            // afsluitend leesteken, geen STT-artefacten) heeft Gemini niet
            // nodig — dat scheelt timeout-gevoelige calls zonder kwaliteitsverlies.
            let cleanSkipEnabled = UserDefaults.standard.bool(forKey: "SkipCleanEnhancement")
            let isCleanSkip = cleanSkipEnabled && Self.looksAlreadyClean(text)
            let shouldSkipEnhancement = (isShortSkip || isCleanSkip) && !(promptDetectionResult?.shouldEnableAI == true)
            if isCleanSkip { ProatDebugLog.write("SKIP clean | \(String(text.prefix(80)))") }

            if let enhancementService,
               enhancementService.isEnhancementEnabled,
               enhancementService.isConfigured,
               !shouldSkipEnhancement {
                if shouldCancel() { await onCleanup(); return }

                onStateChange(.enhancing)
                let textForAI = promptDetectionResult?.processedText ?? text

                do {
                    let (enhancedText, enhancementDuration, promptName) = try await enhancementService.enhance(textForAI)
                    transcription.enhancedText = enhancedText
                    transcription.aiEnhancementModelName = enhancementService.getAIService()?.currentModel
                    transcription.promptName = promptName
                    transcription.enhancementDuration = enhancementDuration
                    transcription.aiRequestSystemMessage = enhancementService.lastSystemMessageSent
                    transcription.aiRequestUserMessage = enhancementService.lastUserMessageSent
                    finalPastedText = enhancedText
                } catch {
                    let errorDescription = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
                    transcription.enhancedText = "Enhancement failed: \(errorDescription)"
                    let shortReason = String(errorDescription.prefix(80))
                    await MainActor.run {
                        NotificationManager.shared.showNotification(
                            title: "Enhancement failed: \(shortReason)",
                            type: .warning
                        )
                    }
                    if shouldCancel() { await onCleanup(); return }
                }
            }

            transcription.transcriptionStatus = TranscriptionStatus.completed.rawValue
        } catch {
            let errorDescription = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription

            if let nativeAppleError = error as? NativeAppleTranscriptionService.ServiceError,
               case .assetDownloadRequired = nativeAppleError {
                await MainActor.run {
                    NotificationManager.shared.showNotification(
                        title: errorDescription,
                        type: .error,
                        duration: 5.0
                    )
                }
            }

            transcription.text = "Transcription Failed: \(errorDescription)"
            transcription.transcriptionStatus = TranscriptionStatus.failed.rawValue
        }

        func saveTranscriptionAndPostCompletion() {
            if transcription.transcriptionStatus == TranscriptionStatus.completed.rawValue {
                do {
                    didInsertSessionMetric = try SessionMetricRecorder.recordRecorderSession(
                        transcription: transcription,
                        model: model,
                        in: modelContext
                    )
                } catch {
                    logger.error("Failed to record session metric: \(error.localizedDescription, privacy: .public)")
                }
            }

            do {
                try modelContext.save()
                if didInsertSessionMetric {
                    NotificationCenter.default.post(name: .sessionMetricsDidChange, object: nil)
                }
                NotificationCenter.default.post(name: .transcriptionCompleted, object: transcription)
            } catch {
                logger.error("Failed to save transcription: \(error.localizedDescription, privacy: .public)")
            }
        }

        func restorePromptDetectionSettingsIfNeeded() async {
            if let result = promptDetectionResult,
               let enhancementService,
               result.shouldEnableAI {
                await promptDetectionService.restoreOriginalSettings(result, to: enhancementService)
            }
        }

        if shouldCancel() {
            await onCleanup()
            saveTranscriptionAndPostCompletion()
            return
        }

        let dismissTask: Task<Void, Never>?
        if var textToPaste = finalPastedText,
           transcription.transcriptionStatus == TranscriptionStatus.completed.rawValue {
            let appendSpace = UserDefaults.standard.bool(forKey: "AppendTrailingSpace")
            let pastedText = textToPaste + (appendSpace ? " " : "")
            let pastePostTask = CursorPaster.startPasteAtCursor(pastedText)
            SoundManager.shared.playStopSound()

            // Verborgen noaberschap-knipoog: precies één keer per dag, alleen bij
            // de exacte zin "goeie noaber". Verandert NOOIT de geplakte tekst —
            // toont alleen een feestelijke bevestiging in plaats van stilte.
            maybeShowNoaberEasterEgg(for: textToPaste)
            let autoSendKey = PowerModeManager.shared.currentActiveConfiguration?.autoSendKey
            await restorePromptDetectionSettingsIfNeeded()
            dismissTask = Task { @MainActor in
                await onDismiss()
            }
            await pastePostTask.value

            if let autoSendKey, autoSendKey.isEnabled {
                Task { @MainActor in
                    try? await Task.sleep(nanoseconds: 500_000_000)
                    CursorPaster.performAutoSend(autoSendKey)
                }
            }
        } else {
            await restorePromptDetectionSettingsIfNeeded()
            await onDismiss()
            dismissTask = nil
        }

        saveTranscriptionAndPostCompletion()

        await dismissTask?.value
    }

    // MARK: - Verborgen noaberschap-knipoog

    private static let easterEggDefaultsKey = "proatEasterEggLastShown"

    /// Toont — hooguit één keer per dag — een feestelijke bevestiging wanneer de
    /// gebruiker letterlijk "goeie noaber" dicteert. Wijzigt de geplakte tekst niet.
    private func maybeShowNoaberEasterEgg(for finalText: String) {
        // Normaliseer: lowercased, leestekens weg, witruimte trimmen.
        let lowered = finalText.lowercased()
        let stripped = lowered.unicodeScalars
            .map { CharacterSet.alphanumerics.contains($0) || $0 == " " ? Character($0) : " " }
            .reduce(into: "") { $0.append($1) }
        let normalized = stripped
            .split(whereSeparator: { $0 == " " })
            .joined(separator: " ")
            .trimmingCharacters(in: .whitespaces)

        // Exacte zin, of als losstaande zin binnen de tekst.
        let phrase = "goeie noaber"
        let isMatch = normalized == phrase
            || normalized.hasPrefix(phrase + " ")
            || normalized.hasSuffix(" " + phrase)
            || normalized.contains(" " + phrase + " ")
        guard isMatch else { return }

        // Gate: max één keer per kalenderdag.
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd"
        let today = formatter.string(from: Date())

        let lastShown = UserDefaults.standard.string(forKey: Self.easterEggDefaultsKey)
        guard lastShown != today else { return }

        UserDefaults.standard.set(today, forKey: Self.easterEggDefaultsKey)
        logger.notice("Noaberschap-knipoog getoond")

        Task { @MainActor in
            NotificationManager.shared.showNotification(
                title: "✓ Geplakt · noaberschap ✦",
                type: .success,
                duration: 3.0
            )
        }
    }

    /// Conservatieve "is al schoon"-check. Alleen true als de tekst er
    /// onmiskenbaar verzorgd uitziet: begint met hoofdletter/quote, eindigt
    /// op een afsluitend leesteken, en bevat geen typische STT-artefacten
    /// (dubbele spaties, hapering-woorden). Bij twijfel → false (wél verfijnen).
    static func looksAlreadyClean(_ text: String) -> Bool {
        let t = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard t.count >= 2 else { return false }
        guard let first = t.first, (first.isUppercase || first == "\"" || first == "'" || first.isNumber) else { return false }
        guard let last = t.last, ".!?…\"')".contains(last) else { return false }
        if t.contains("  ") || t.contains(" ,") || t.contains(" .") { return false }
        let artefacts = #"(?i)\b(eh+m?|uh+m?|ehm|hmm+|\.\.\.)\b"#
        if t.range(of: artefacts, options: .regularExpression) != nil { return false }
        return true
    }
}
