import Observation
import SwiftUI

extension RecordingView {

    /// The observable model that drives ``RecordingView``.
    ///
    /// The model implements the recording flow as a ``State`` machine: it exposes the visibility, icons, accessibility labels, and alert
    /// message of the view's controls for the current state, counts the elapsed recording time, and processes the recorded input once
    /// it is sent. The audio capture and its transcription are delegated to the ``Capturing`` and ``Transcribing`` attached at
    /// initialization; when either of them fails, the model falls back to the not-recording state and publishes the failure in ``error``.
    ///
    /// A recording paused by a system interruption of the capture resumes by itself once the interruption ends with the system's
    /// resume hint, and a recording already being processed can still be discarded, cancelling its in-flight transcription.
    @MainActor
    @Observable
    final class Model {

        // MARK: Properties

        /// The current state of the recording flow.
        private(set) var state: State = .notRecording

        /// The number of seconds spent recording, excluding any time spent paused.
        private(set) var elapsedSeconds: Int = 0

        /// The error to surface to the user, or `nil` when none occurred. Cleared when it is dismissed or a new recording starts.
        private(set) var error: RecordingError?

        /// The transcription of the last processed recording, or `nil` when none has been processed yet.
        private(set) var transcription: Transcription?

        /// The recording time accumulated by earlier recording stretches, up to the last pause.
        @ObservationIgnored
        private var accumulated: Duration = .zero

        /// The instant the current recording stretch started, or `nil` while not recording.
        @ObservationIgnored
        private var anchor: ContinuousClock.Instant?

        /// The service that captures the audio from a microphone.
        @ObservationIgnored
        private let capturer: any Capturing

        /// The clock that measures the recording time.
        @ObservationIgnored
        private let clock = ContinuousClock()

        /// Whether the recording was paused by a system interruption of the audio capture, as opposed to by the user.
        @ObservationIgnored
        private var isPausedByInterruption = false

        /// The binding to the locale of the spoken language to transcribe.
        @ObservationIgnored
        private let locale: Binding<Locale>

        /// The task that serializes the calls to the capturing service, so rapid state changes can never reach the service out of order.
        @ObservationIgnored
        private var taskCapturer: Task<Void, Never>?

        /// The task that listens to the events emitted by the capturing service, or `nil` until the first recording starts.
        @ObservationIgnored
        private var taskEvents: Task<Void, Never>?

        /// The task that updates ``elapsedSeconds`` from the measured recording time once per second while recording.
        @ObservationIgnored
        private var taskTimer: Task<Void, Never>?

        /// The callable service that transcribes the recorded audio into text.
        @ObservationIgnored
        private let transcribe: any Transcribing

        // MARK: Initializers

        /// Creates a model attached to the given recording and transcribing services.
        ///
        /// - Parameters:
        ///   - capturer: The service that captures the audio from a microphone. Defaults to ``DummyCapturing``.
        ///   - transcribe: The callable service that transcribes the recorded audio into text. Defaults to ``DummyTranscribing``.
        ///   - locale: The binding to the locale of the spoken language to transcribe. Defaults to a constant binding to the user's current locale.
        init(
            capturer: any Capturing = DummyCapturing(),
            transcribe: any Transcribing = DummyTranscribing(),
            locale: Binding<Locale> = .constant(.current)
        ) {
            self.capturer = capturer
            self.transcribe = transcribe
            self.locale = locale
        }

        // MARK: Computed

        /// The image resource for the main button: a pause icon while recording, a record icon otherwise.
        var iconMain: ImageResource {
            switch state {
            case .notRecording, .paused, .processing: .Icon.record
            case .recording: .Icon.pause
            }
        }

        /// The image resource for the send button while paused, or `nil` when the button should show a progress indicator instead.
        var iconSend: ImageResource? {
            switch state {
            case .paused: .Icon.send
            default: nil
            }
        }

        /// Whether the recorded input is currently being processed.
        var isProcessing: Bool {
            state == .processing
        }

        /// The accessibility label for the main button, matching its action in the current state.
        var labelMain: LocalizedStringResource {
            switch state {
            case .notRecording, .processing: .viewRecordingButtonMainLabelRecord
            case .recording: .viewRecordingButtonMainLabelPause
            case .paused: .viewRecordingButtonMainLabelResume
            }
        }

        /// The accessibility label for the send button: the sending action while paused, the ongoing processing otherwise.
        var labelSend: LocalizedStringResource {
            isProcessing
                ? .viewRecordingButtonSendLabelProcessing
                : .viewRecordingButtonSendLabelSend
        }

        /// Whether the main button should be disabled.
        var shouldDisableMain: Bool {
            state == .processing
        }

        /// Whether the action buttons (send and discard) should be visible.
        var shouldShowActions: Bool {
            state != .notRecording
                && state != .recording
        }

        /// Whether the timer label should be visible.
        var shouldShowTimer: Bool {
            state != .notRecording
        }

        /// The localized message describing ``error`` for the alert, or `nil` when no error occurred.
        var textAlertMessage: LocalizedStringResource? {
            switch error {
            case .assetsUnavailable: .viewRecordingAlertErrorMessageAssets
            case .captureFailed: .viewRecordingAlertErrorMessageCapture
            case .permissionDenied: .viewRecordingAlertErrorMessagePermission
            case .transcriptionFailed: .viewRecordingAlertErrorMessageTranscription
            default: nil
            }
        }

        /// The elapsed recording time, formatted as `mm:ss` for the timer label.
        var textTimer: String {
            Duration
                .seconds(elapsedSeconds)
                .formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2)))
        }

        /// The elapsed recording time, spelled out in full units for assistive technologies.
        var textTimerAccessible: String {
            Duration
                .seconds(elapsedSeconds)
                .formatted(.units(
                    allowed: [.minutes, .seconds],
                    width: .wide
                ))
        }

        // MARK: Methods

        /// Handles a press of the discard button.
        ///
        /// Throws away a paused recording — or one already being processed, cancelling its in-flight transcription: it resets the
        /// timer, returns to the not-recording state, and stops the audio capture, deleting the captured audio file. Does nothing in
        /// any other state.
        func pressedDiscard() {
            isPausedByInterruption = false

            switch state {
            case .paused:
                state = .notRecording

                stopTimer()

                accumulated = .zero
                elapsedSeconds = 0

                enqueueCapturer {
                    guard let audio = try? await self.capturer.stop() else {
                        return
                    }

                    try? FileManager.default.removeItem(at: audio)
                }
            case .processing:
                taskCapturer?.cancel()

                state = .notRecording

                accumulated = .zero
                elapsedSeconds = 0

                enqueueCapturer {
                    guard let audio = try? await self.capturer.stop() else {
                        return
                    }

                    try? FileManager.default.removeItem(at: audio)
                }
            default:
                break
            }
        }

        /// Handles a press of the main button.
        ///
        /// Starts a recording when idle, pauses an ongoing recording, or resumes a paused one — starting and stopping the timer
        /// and the audio capture accordingly. Does nothing while the input is being processed.
        func pressedMain() {
            isPausedByInterruption = false

            switch state {
            case .notRecording:
                state = .recording

                listenToEvents()
                startTimer(true)
                startCapturer(true)
            case .recording:
                pauseRecording()
            case .paused:
                resumeRecording()
            case .processing:
                break
            }
        }

        /// Handles the dismissal of the surfaced ``error``, clearing it.
        func dismissedError() {
            error = nil
        }

        /// Handles a press of the send button.
        ///
        /// Moves a paused recording into processing, stopping the timer and kicking off ``processInput()``. Does nothing in any other state.
        func pressedSend() {
            isPausedByInterruption = false

            switch state {
            case .paused:
                state = .processing

                stopTimer()

                enqueueCapturer {
                    await self.processInput()
                }
            default:
                break
            }
        }

    }
}

// MARK: - Helpers

private extension RecordingView.Model {

    // MARK: Computed

    /// The time spent recording so far, excluding any time spent paused, as measured by the clock.
    var elapsed: Duration {
        guard let anchor else {
            return accumulated
        }

        return accumulated + anchor.duration(to: clock.now)
    }

    // MARK: Methods

    /// Processes the recorded input: it stops the audio capture, transcribes the captured audio, and publishes the result in ``transcription``;
    /// when finished — or when either service fails, in which case the failure is published in ``error`` — it resets ``elapsedSeconds``
    /// and returns the model to the not-recording state. A processing cancelled through the discard button publishes nothing:
    /// the discard already reset the model.
    func processInput() async {
        guard state == .processing else {
            return
        }

        do {
            let audio = try await capturer.stop()

            do {
                let transcription = try await transcribe(
                    audio,
                    locale: locale.wrappedValue
                )

                guard !Task.isCancelled else {
                    return
                }

                self.transcription = transcription
            }
            catch is CancellationError {
                return
            }
            catch {
                guard !Task.isCancelled else {
                    return
                }

                transcription = nil
                self.error =
                    error as? AudioTranscribingError == .assetsNotInstalled
                    ? .assetsUnavailable
                    : .transcriptionFailed
            }
        } catch is CancellationError {
            return
        } catch {
            guard !Task.isCancelled else {
                return
            }

            transcription = nil
            self.error = .captureFailed
        }

        guard !Task.isCancelled else {
            return
        }

        accumulated = .zero
        elapsedSeconds = 0
        state = .notRecording
    }

    /// Enqueues an operation behind any previously enqueued ones, so the calls to the capturing service always reach it in the order
    /// the states changed, no matter how quickly the user presses the view's controls.
    ///
    /// - Parameter operation: The operation on the capturing service to enqueue.
    func enqueueCapturer(
        _ operation: @escaping @MainActor () async -> Void
    ) {
        taskCapturer = Task { [taskCapturer] in
            await taskCapturer?.value
            await operation()
        }
    }

    /// Starts listening to the events emitted by the capturing service, unless already listening: an interruption of the capture pauses
    /// an ongoing recording, exactly like a press of the main button would, and the end of an interruption resumes the recording it
    /// paused — but only when the system hints the capture may resume, and never one the user paused themselves.
    func listenToEvents() {
        guard taskEvents == nil else {
            return
        }

        taskEvents = Task { [weak self, events = capturer.events] in
            for await event in events {
                guard let self else {
                    return
                }

                switch event {
                case .interrupted:
                    if state == .recording {
                        isPausedByInterruption = true

                        pauseRecording()
                    }
                case .interruptionEnded(let shouldResume):
                    if shouldResume, state == .paused, isPausedByInterruption {
                        resumeRecording()
                    }

                    isPausedByInterruption = false
                }
            }
        }
    }

    /// Pauses the ongoing recording: it stops the timer and pauses the audio capture.
    func pauseRecording() {
        state = .paused

        stopTimer()

        enqueueCapturer {
            try? await self.capturer.pause()
        }
    }

    /// Resumes the paused recording: it restarts the timer, without resetting it, and resumes the audio capture.
    func resumeRecording() {
        state = .recording

        startTimer(false)
        startCapturer(false)
    }

    /// Starts the audio capture through the attached ``Capturing``, falling back to the not-recording state and publishing the failure
    /// in ``error`` when the service fails.
    ///
    /// - Parameter isNewRecording: Whether a new recording should be started, as opposed to a paused one being resumed.
    func startCapturer(
        _ isNewRecording: Bool
    ) {
        enqueueCapturer {
            do {
                isNewRecording
                    ? try await self.capturer.start()
                    : try await self.capturer.resume()
            } catch {
                self.stopTimer()

                self.state = .notRecording
                self.error =
                    error as? AudioCapturingError == .permissionNotGranted
                    ? .permissionDenied
                    : .captureFailed
            }
        }
    }

    /// Starts the timer task, which updates ``elapsedSeconds`` from the time measured by the clock on every whole second until it is
    /// cancelled, so scheduling latency never accumulates as drift. Any previously running timer task is cancelled first.
    ///
    /// - Parameter shouldRestartTimer: Whether the measured recording time, ``elapsedSeconds``, ``error``, and ``transcription``
    /// should be cleared before the timer starts, which is the case for a new recording as opposed to one resuming from a pause.
    func startTimer(
        _ shouldRestartTimer: Bool
    ) {
        if shouldRestartTimer {
            accumulated = .zero
            elapsedSeconds = 0
            error = nil
            transcription = nil
        }

        anchor = clock.now

        taskTimer?.cancel()
        taskTimer = Task { [weak self] in
            while !Task.isCancelled {
                guard let self, !Task.isCancelled else {
                    return
                }

                let elapsed = self.elapsed
                let seconds = elapsed.components.seconds

                self.elapsedSeconds = Int(seconds)

                try? await Task.sleep(for: .seconds(seconds + 1) - elapsed)
            }
        }
    }

    /// Stops the timer task, if any, folding the current recording stretch into the accumulated recording time so ``elapsedSeconds``
    /// keeps its value.
    func stopTimer() {
        if let anchor {
            accumulated += anchor.duration(to: clock.now)

            self.anchor = nil
        }

        taskTimer?.cancel()
        taskTimer = nil
    }

}

// MARK: - States

extension RecordingView.Model {
    /// The states of the recording flow.
    enum State {
        /// No recording is in progress.
        case notRecording
        /// A recording is in progress and the timer is running.
        case recording
        /// The recording is paused, and can be either resumed or sent.
        case paused
        /// The recorded input is being processed.
        case processing
    }
}
