import Foundation
import Notifying
import Observation
import Recording

extension ContentView {

    /// The observable model that drives ``ContentView``.
    ///
    /// The model owns the recording and transcribing services attached to the recording feature, along with the ``locale`` the feature
    /// transcribes in, picked in the view's toolbar from the supported ``locales`` that ``load()`` fetches. It also holds the transcription
    /// currently presented in the view's modal sheet: ``received(_:)`` presents the transcription of a processed recording, and
    /// ``dismissed()`` clears it when the sheet closes.
    ///
    /// Every picked locale kicks off ``preinstall()``, a cancellable task that preinstalls the locale's speech model assets through the
    /// preinstalling service — cancelling any download still in flight for a previously picked locale. The service's started, cancelled, and
    /// failed download events are mapped to the transient, self-dismissing notifications of the ``notifier``.
    ///
    /// Every collaborator is injected at initialization behind its protocol, defaulting to the on-device services: the app runs the model
    /// against the real backends, while its unit tests and previews attach mocks and dummies without touching this type.
    @MainActor
    @Observable
    final class Model {

        // MARK: Properties

        /// The locale of the spoken language to transcribe.
        var locale: Locale

        /// The transcription currently presented in the modal sheet, or `nil` when none is shown.
        var transcription: Transcription?

        /// The locales the transcriber supports, sorted by their localized names, or empty while ``load()`` has not finished yet.
        private(set) var locales: [Locale]

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

        /// The locale whose speech model assets are preinstalled or being preinstalled, or `nil` when none are.
        @ObservationIgnored
        private var localePreinstalling: Locale?

        /// The notifier that owns the transient in-app notifications reporting the download events.
        @ObservationIgnored
        let notifier: Notifier

        /// The service that preinstalls the speech model assets of a picked locale.
        @ObservationIgnored
        private let preinstaller: any Preinstalling

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

        /// The task that preinstalls the speech model assets of the last picked locale, or `nil` until the first locale is picked.
        @ObservationIgnored
        private var taskPreinstall: Task<Void, Never>?

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

        // MARK: Initializers

        /// Creates a model set to the user's current locale, with no supported locales loaded yet, attached to the given services
        /// and notifier.
        ///
        /// - Parameters:
        ///   - capturer: The service that captures the audio from a microphone. Defaults to the on-device ``AudioCapturing``.
        ///   - preinstaller: The service that preinstalls the speech model assets of a picked locale. Defaults to ``AssetPreinstalling``.
        ///   - transcriber: The service that transcribes the recorded audio into text. Defaults to the on-device ``AudioTranscribing``.
        ///   - notifier: The notifier that owns the transient in-app notifications. Defaults to a notifier with its standard dismissal delay.
        init(
            capturer: any Capturing = AudioCapturing(),
            preinstaller: any Preinstalling = AssetPreinstalling(),
            transcriber: any Transcribing = AudioTranscribing(),
            notifier: Notifier = .init(),
        ) {
            self.locale = .current
            self.locales = []
            self.capturer = capturer
            self.notifier = notifier
            self.preinstaller = preinstaller
            self.transcriber = transcriber
        }

        // MARK: Methods

        /// Loads the locales the transcriber supports — resolved through the preinstalling service — into ``locales``, sorted by their
        /// localized names, and aligns ``locale`` with the supported equivalent of its current value — falling back to the supported
        /// equivalent of a default locale when none exists — so the locale picker starts with a valid selection. It then kicks off the
        /// preinstallation of the aligned locale's speech model assets.
        func load() async {
            locales =
                await preinstaller
                .supportedLocales()
                .sorted {
                    name(for: $0).localizedStandardCompare(name(for: $1)) == .orderedAscending
                }

            locale =
                if let equivalent = await preinstaller.supportedLocale(equivalentTo: locale) {
                    equivalent
                }
                else if let fallback = await preinstaller.supportedLocale(equivalentTo: .byDefault) {
                    fallback
                }
                else {
                    .current
                }

            preinstall()
        }

        /// Preinstalls the speech model assets for the current ``locale`` in a cancellable task, cancelling the preinstallation of any
        /// previously picked locale still in flight — which the ``notifier`` reports as a cancelled download. Does nothing when the
        /// current locale is the one already preinstalled or being preinstalled.
        func preinstall() {
            guard localePreinstalling != locale else {
                return
            }

            taskPreinstall?.cancel()

            localePreinstalling = locale

            listenToEvents()

            taskPreinstall = Task { [locale] in
                await preinstaller.preinstall(for: locale)
            }
        }

        /// Returns the localized name of a locale for the locale picker.
        ///
        /// - Parameter locale: The locale to name.
        /// - Returns: The name of the locale in the user's current locale, or its identifier when no name is available.
        func name(
            for locale: Locale
        ) -> String {
            Locale
                .current
                .localizedString(
                    forIdentifier: locale.identifier
                )?.localizedCapitalized
                ?? locale.identifier
        }

        /// Handles the transcription of a processed recording, presenting it in the modal sheet.
        ///
        /// - Parameter transcription: The transcription of the processed recording.
        func received(
            _ transcription: Transcription
        ) {
            self.transcription = transcription
        }

        /// Handles the dismissal of the modal sheet.
        func dismissed() {
            transcription = nil
        }

    }

}

// MARK: - Helpers

private extension ContentView.Model {

    // MARK: Methods

    /// Starts listening to the events emitted by the preinstalling service, unless already listening: every started, cancelled, and
    /// failed download is posted to the ``ContentView/Model/notifier``, and a cancellation or failure clears the preinstalled locale
    /// — only when it is still the affected one, so a newer preinstallation is never forgotten — letting a re-pick retry.
    func listenToEvents() {
        guard taskEvents == nil else {
            return
        }

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

                switch event {
                case .cancelled(let locale):
                    if localePreinstalling == locale {
                        localePreinstalling = nil
                    }

                    notifier.post(
                        .warning,
                        message: String(localized: .viewRecordingNotificationDownloadCancelled(name(for: locale))),
                        symbol: Constant.Symbol.cancelled
                    )
                case .failed(let locale):
                    if localePreinstalling == locale {
                        localePreinstalling = nil
                    }

                    notifier.post(
                        .error,
                        message: String(localized: .viewRecordingNotificationDownloadFailed(name(for: locale))),
                        symbol: Constant.Symbol.failed
                    )
                case .started(let locale):
                    notifier.post(
                        .info,
                        message: String(localized: .viewRecordingNotificationDownloadStarted(name(for: locale))),
                        symbol: Constant.Symbol.started
                    )
                }
            }
        }
    }

}

// MARK: - Constants

/// The constant values used across the model.
private enum Constant {
    /// The symbol constants.
    enum Symbol {
        /// The system symbol of a cancelled download.
        static let cancelled = "xmark.circle"
        /// The system symbol of a failed download.
        static let failed = "exclamationmark.triangle"
        /// The system symbol of a started download.
        static let started = "arrow.down.circle"
    }
}

private extension Locale {
    /// The locale to fall back to when the transcriber supports no equivalent of the user's current locale.
    static let byDefault: Locale = .init(identifier: "en_US")
}
