import AVFoundation
import Speech

/// The transcribing service that transcribes recorded audio into text on device.
///
/// The service writes the recorded audio into a temporary `.m4a` file and runs it through a `SpeechAnalyzer` with a `SpeechTranscriber`
/// module, joining the finalized results into the returned transcription. The transcription happens in the locale given at each call — or rather in the
/// closest equivalent the transcriber supports. The speech model assets for that locale are downloaded and installed on first use; every
/// transcription after that happens entirely offline.
public struct AudioTranscribing: Transcribing {

    // MARK: Initializers

    /// Creates an audio transcribing service.
    public init() {}

    // MARK: Methods

    /// Transcribes the given recorded audio into text, deleting the temporary audio file when the transcription finishes.
    ///
    /// - Parameters:
    ///   - audio: The recorded audio to transcribe.
    ///   - locale: The locale of the spoken language to transcribe.
    /// - Returns: The transcription of the recorded audio.
    /// - Throws: ``AudioTranscribingError/localeNotSupported`` when the transcriber supports no equivalent of the given locale,
    /// or any error thrown while installing the speech model assets, reading the audio file, or analyzing its contents.
    public func callAsFunction(
        _ audio: Data,
        locale: Locale
    ) async throws -> Transcription {
        try audio.write(to: Constant.File.url)

        defer {
            try? FileManager.default.removeItem(at: Constant.File.url)
        }

        guard let locale = await SpeechTranscriber.supportedLocale(
            equivalentTo: locale
        ) else {
            throw AudioTranscribingError.localeNotSupported
        }

        let transcriber = SpeechTranscriber(
            locale: locale,
            preset: .transcription
        )

        if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
            try await request.downloadAndInstall()
        }

        let analyzer = SpeechAnalyzer(modules: [transcriber])

        async let text = transcriber.results.reduce(into: "") { text, result in
            text += String(result.text.characters)
        }

        let file = try AVAudioFile(forReading: Constant.File.url)

        if let lastSampleTime = try await analyzer.analyzeSequence(from: file) {
            try await analyzer.finalizeAndFinish(through: lastSampleTime)
        } else {
            await analyzer.cancelAndFinishNow()
        }

        return try await .init(text: text)
    }

}

// MARK: - Errors

/// The errors thrown by ``AudioTranscribing``.
public enum AudioTranscribingError: Error {
    /// The transcriber supports no equivalent of the locale the transcription was requested with.
    case localeNotSupported
}

// MARK: - Constants

/// The constant values used across the audio transcribing service.
private enum Constant {
    /// The file constants.
    enum File {
        /// The location of the temporary file the recorded audio is written into for the analysis.
        static let url = FileManager.default.temporaryDirectory.appending(path: "transcription.m4a")
    }
}
