import AVFoundation

/// The recording service that captures real audio from the device's microphone.
///
/// The service records into a temporary `.m4a` file through an `AVAudioRecorder`, and returns the file's contents when the recording stops.
/// The user's permission to record is requested before a recording starts and, on the platforms that require it, the shared audio session is
/// configured for recording while the capture is in progress.
@MainActor
public final class AudioCapturing: Capturing {

    // MARK: Properties

    /// The recorder that captures the audio from the microphone into a temporary file.
    private var recorder: AVAudioRecorder?

    // MARK: Initializers

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

    // MARK: Methods

    /// Starts a new audio recording from the microphone.
    ///
    /// - Throws: ``AudioCapturingError/permissionNotGranted`` when the user denies the app access to the microphone,
    /// ``AudioCapturingError/captureNotStarted`` when the recorder fails to start, or any error thrown while configuring
    /// the audio session or creating the recorder.
    public func start() async throws {
        guard await AVAudioApplication.requestRecordPermission() else {
            throw AudioCapturingError.permissionNotGranted
        }

        #if os(iOS) || os(visionOS)
        let session = AVAudioSession.sharedInstance()

        try session.setCategory(.record, mode: .default)
        try session.setActive(true)
        #endif

        let recorder = try AVAudioRecorder(
            url: Constant.File.url,
            settings: Constant.Audio.settings
        )

        guard recorder.record() else {
            throw AudioCapturingError.captureNotStarted
        }

        self.recorder = recorder
    }

    /// Pauses the ongoing recording.
    ///
    /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress.
    public func pause() async throws {
        guard let recorder else {
            throw AudioCapturingError.noOngoingRecording
        }

        recorder.pause()
    }

    /// Resumes a paused recording.
    ///
    /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress, or
    /// ``AudioCapturingError/captureNotStarted`` when the recorder fails to resume.
    public func resume() async throws {
        guard let recorder else {
            throw AudioCapturingError.noOngoingRecording
        }
        guard recorder.record() else {
            throw AudioCapturingError.captureNotStarted
        }
    }

    /// Stops the recording, deleting the temporary audio file after its contents are read.
    ///
    /// - Returns: The audio captured since the recording started.
    /// - Throws: ``AudioCapturingError/noOngoingRecording`` when no recording is in progress, or any error thrown
    /// while reading the recorded audio file.
    public func stop() async throws -> Data {
        guard let recorder else {
            throw AudioCapturingError.noOngoingRecording
        }

        recorder.stop()

        self.recorder = nil

        #if os(iOS) || os(visionOS)
        try? AVAudioSession.sharedInstance().setActive(false)
        #endif

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

        return try Data(contentsOf: Constant.File.url)
    }

}

// MARK: - Errors

/// The errors thrown by ``AudioCapturing``.
public enum AudioCapturingError: Error {
    /// The recorder failed to start or resume the audio capture.
    case captureNotStarted
    /// No recording is in progress.
    case noOngoingRecording
    /// The user denied the app permission to record audio from the microphone.
    case permissionNotGranted
}

// MARK: - Constants

/// The constant values used across the audio recording service.
private enum Constant {
    /// The audio constants.
    enum Audio {
        /// The settings of the recorded audio: single-channel AAC at a 44.1 kHz sample rate, encoded in high quality.
        @MainActor
        static let settings: [String: Any] = [
            AVFormatIDKey: kAudioFormatMPEG4AAC,
            AVNumberOfChannelsKey: 1,
            AVSampleRateKey: 44_100.0,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
        ]
    }

    /// The file constants.
    enum File {
        /// The location of the temporary file the audio is captured into.
        static let url = FileManager.default.temporaryDirectory.appending(path: "recording.m4a")
    }
}
