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 and icon 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.
    @MainActor
    @Observable
    final class Model {

        // MARK: Properties

        /// The current state of the recording flow.
        var state: State = .notRecording

        /// The number of seconds spent recording, excluding any time spent paused.
        private(set) var elapsedSeconds: Int = 0
        
        /// 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 clock that measures the recording time.
        @ObservationIgnored
        private let clock = ContinuousClock()

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

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

        /// 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
        }

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

        /// Whether the send button should be visible.
        var shouldShowSend: Bool {
            state != .notRecording
                && state != .recording
        }

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

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

        // MARK: Methods

        /// Handles a press of the main button.
        ///
        /// Starts a recording when idle, pauses an ongoing recording, or resumes
        /// a paused one. Does nothing while the input is being processed.
        func pressedMain() {
            guard state != .processing else {
                return
            }

            switch state {
            case .notRecording:
                state = .recording
            case .recording:
                state = .paused
            case .paused:
                state = .recording
            case .processing:
                break
            }
        }

        /// Handles a press of the send button.
        ///
        /// Moves a paused recording into processing. Does nothing in any other state.
        func pressedSend() {
            guard state == .paused else {
                return
            }

            switch state {
            case .paused:
                state = .processing
            default:
                break
            }
        }

        /// Reacts to a change of ``state``, expected to be called from the view whenever it observes one.
        ///
        /// Starts the timer and the audio capture when a recording begins or resumes, stops the timer in every other state — pausing the capture
        /// while paused — and kicks off ``processInput()`` when the input is sent.
        ///
        /// - Parameter shouldRestartTimer: Whether ``elapsedSeconds`` should be reset to zero before the timer starts, which is the case for
        /// a new recording as opposed to one resuming from a pause.
        func updatedState(
            shouldRestartTimer: Bool
        ) {
            switch state {
            case .recording:
                startTimer(shouldRestartTimer)
                startCapturer(shouldRestartTimer)
            case .paused:
                stopTimer()

                Task {
                    try? await capturer.pause()
                }
            case .processing:
                stopTimer()

                Task {
                    await processInput()
                }
            case .notRecording:
                stopTimer()
            }
        }

    }
}

// 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 — it resets ``elapsedSeconds`` and returns the model to the not-recording state.
    func processInput() async {
        guard state == .processing else {
            return
        }

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

            transcription = try await transcribe(
                audio,
                locale: locale.wrappedValue
            )
        } catch {
            transcription = nil
        }

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

    /// Starts the audio capture through the attached ``Capturing``, falling back to the not-recording state 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
    ) {
        Task {
            do {
                isNewRecording
                    ? try await capturer.start()
                    : try await capturer.resume()
            } catch {
                stopTimer()

                state = .notRecording
            }
        }
    }

    /// 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``, 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
            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
    }
}
