// LoopFollow
// OnboardingViewModel.swift

import Combine
import SwiftUI
import UserNotifications

/// Drives the onboarding wizard: tracks the current phase, the chosen data
/// source, the seeded alarms, and persists everything when the user finishes.
///
/// The child connection view models are the same ones used by the regular
/// settings screens, so URL/token/credential entry and validation behave
/// identically here and there.
@MainActor
final class OnboardingViewModel: ObservableObject {
    enum DataSource: Hashable {
        case nightscout
        case dexcom
        case copyFromPhone
    }

    /// A single recommended alarm offered on the alarms phase. Display fields are
    /// carried explicitly (rather than derived from `AlarmType`) so two alarms of
    /// the same type — a warning Low and an Urgent Low — can read differently.
    struct SeedAlarm: Identifiable {
        let id = UUID()
        var alarm: Alarm
        var isEnabled: Bool
        var title: String
        var detail: String
        /// Needs Nightscout loop/uploader data, so it's hidden for Dexcom-only.
        var requiresNightscout: Bool

        var type: AlarmType { alarm.type }

        /// Use each alarm type's own icon rather than a bespoke one.
        var icon: String { alarm.type.icon }
    }

    /// Position within a multi-page phase, reported by that phase's view so the
    /// overall progress bar and label can reflect it.
    struct PhaseProgress: Equatable {
        var page: Int
        var count: Int
    }

    @Published var step: OnboardingStep = .welcome
    @Published var dataSource: DataSource?
    @Published var seedAlarms: [SeedAlarm]

    /// Within-phase position for phases that own internal pages (connect, alarms).
    /// Reset to `nil` whenever the phase changes.
    @Published var phaseProgress: PhaseProgress?

    /// Set once a QR settings import on the "copy from another phone" path
    /// succeeds, so the connect phase can be considered complete.
    @Published var didImportSettings = false

    /// Whether the notification permission is still undecided. The notifications
    /// phase is only shown when it is — there's no point prompting someone who has
    /// already granted or denied it (iOS won't show the prompt again anyway).
    /// Defaults to `true`; resolved asynchronously at launch, well before the user
    /// reaches that phase.
    @Published private var notificationsUndecided = true

    let nightscoutViewModel = NightscoutSettingsViewModel()
    let dexcomViewModel = DexcomSettingsViewModel()

    /// Called to dismiss the onboarding cover.
    private let onClose: () -> Void
    private var cancellables = Set<AnyCancellable>()

    /// Whether to show the in-flow telemetry consent phase. Captured once at
    /// launch so the decision the phase records doesn't remove it from under the
    /// navigation while the user is still on it.
    private let includeTelemetryStep: Bool

    /// Alarm types the user already has, captured at launch. Onboarding only
    /// offers recommended alarms whose type the user doesn't already own, so a
    /// returning user is helped to add new ones without touching their existing
    /// (possibly custom-named) alarms.
    private let existingAlarmTypes: Set<AlarmType>

    init(onClose: @escaping () -> Void) {
        self.onClose = onClose
        includeTelemetryStep = !Storage.shared.telemetryConsentDecisionMade.value
        existingAlarmTypes = Set(Storage.shared.alarms.value.map(\.type))
        let hasNightscout = !Storage.shared.url.value.isEmpty
        let hasDexcom = !Storage.shared.shareUserName.value.isEmpty
            && !Storage.shared.sharePassword.value.isEmpty
        isAlreadyConfigured = hasNightscout || hasDexcom
        seedAlarms = OnboardingViewModel.defaultSeedAlarms()

        // Re-publish child changes so the footer's `canProceed` stays in sync
        // with live connection validation.
        nightscoutViewModel.objectWillChange
            .sink { [weak self] in self?.objectWillChange.send() }
            .store(in: &cancellables)
        dexcomViewModel.objectWillChange
            .sink { [weak self] in self?.objectWillChange.send() }
            .store(in: &cancellables)

        UNUserNotificationCenter.current().getNotificationSettings { settings in
            let undecided = settings.authorizationStatus == .notDetermined
            Task { @MainActor [weak self] in
                self?.notificationsUndecided = undecided
            }
        }
    }

    // MARK: - Derived state

    /// True when the user already had a working data source at launch — used to
    /// make skipping prominent for returning users and to decide whether the
    /// data-source/connect phases are shown. Captured once in `init`, not read
    /// live: the connect step persists the URL/credentials as the user types, so
    /// a live read would flip this to `true` mid-`.connect`, drop those phases
    /// from `activeSteps`, and make `advance()` fall through to `finish()` —
    /// silently skipping units, alarms, and the rest of setup.
    let isAlreadyConfigured: Bool

    var canProceed: Bool {
        switch step {
        case .welcome, .overview, .units, .generalAlarms, .alarms,
             .tabOrder, .notifications, .telemetry, .completion:
            return true
        case .dataSource:
            return dataSource != nil
        case .connect:
            switch dataSource {
            case .nightscout: return nightscoutViewModel.isConnected || nightscoutViewModel.provisionedTokenPending
            case .dexcom: return dexcomViewModel.canVerifyProceed
            case .copyFromPhone: return didImportSettings
            case .none: return false
            }
        }
    }

    /// Whether Nightscout loop/uploader data is (or will be) available — used to
    /// gate alarms that depend on it. True for the Nightscout source, or any path
    /// that ends with a Nightscout URL configured (including a QR import).
    private var hasNightscoutData: Bool {
        dataSource == .nightscout || !Storage.shared.url.value.isEmpty
    }

    /// Whether a seeded alarm should be offered: its data source must be available
    /// and the user must not already have an alarm of that type.
    func isOffered(_ seed: SeedAlarm) -> Bool {
        guard !seed.requiresNightscout || hasNightscoutData else { return false }
        return !existingAlarmTypes.contains(seed.type)
    }

    var offeredSeedAlarms: [SeedAlarm] {
        seedAlarms.filter { isOffered($0) }
    }

    private var alarmsOffered: Bool { !offeredSeedAlarms.isEmpty }

    /// True when a returning user already has a working data source and there are
    /// no recommended alarms left to add — nothing essential to configure, so the
    /// overview becomes a short "you're all set" confirmation instead of a plan.
    var hasNothingToSetUp: Bool {
        isAlreadyConfigured && !alarmsOffered
    }

    /// The phases actually shown for the current configuration, in order. Optional
    /// phases are included only when relevant; a returning user skips the phases
    /// they've already completed.
    var activeSteps: [OnboardingStep] {
        var steps: [OnboardingStep] = [.welcome, .overview]

        // Already set up and nothing to add — the overview is the last stop, with
        // a "Done" that finishes. Notification/telemetry consent is still handled
        // after dismissal, the same as when skipping.
        if hasNothingToSetUp {
            return steps
        }

        // A returning user with a working data source skips source selection and
        // the connection screen; everyone else sets them up.
        if !isAlreadyConfigured {
            steps.append(contentsOf: [.dataSource, .connect])
        }

        steps.append(.units)
        if alarmsOffered {
            steps.append(contentsOf: [.generalAlarms, .alarms])
        }
        steps.append(.tabOrder)
        if notificationsUndecided {
            steps.append(.notifications)
        }
        if includeTelemetryStep {
            steps.append(.telemetry)
        }
        steps.append(.completion)
        return steps
    }

    /// Phases that show the progress header — the unit over which the bar fills.
    private var chromePhases: [OnboardingStep] {
        activeSteps.filter { $0.showsProgressHeader }
    }

    /// Progress fraction (0...1). Each phase is one slot; a multi-page phase fills
    /// its slot proportionally via `phaseProgress`, so the bar stays smooth no
    /// matter how many pages a phase turns out to have.
    var progress: Double {
        guard !chromePhases.isEmpty else { return 0 }
        guard let index = chromePhases.firstIndex(of: step) else {
            return step == .completion ? 1 : 0
        }
        let within: Double
        if let progress = phaseProgress, progress.count > 1 {
            within = Double(progress.page) / Double(progress.count)
        } else {
            within = 0
        }
        return (Double(index) + within) / Double(chromePhases.count)
    }

    /// The phase name shown in the header, with a local page count for multi-page
    /// phases (e.g. "Alarms · 3 of 13").
    var progressLabel: String {
        let title = step.phaseTitle
        if let progress = phaseProgress, progress.count > 1 {
            return "\(title) · \(min(progress.page + 1, progress.count)) of \(progress.count)"
        }
        return title
    }

    // MARK: - Navigation

    var canGoBack: Bool {
        guard let index = activeSteps.firstIndex(of: step) else { return false }
        return index > 0
    }

    func advance() {
        phaseProgress = nil
        let steps = activeSteps
        guard let index = steps.firstIndex(of: step) else {
            // Defensive: the current step should always be in `activeSteps`.
            // If it ever isn't (a state change removed it while we were on it),
            // continue to the next still-active step in canonical order rather
            // than silently ending setup via `finish()`.
            let canonical = OnboardingStep.allCases
            let currentRank = canonical.firstIndex(of: step) ?? canonical.count
            if let next = steps.first(where: { (canonical.firstIndex(of: $0) ?? 0) > currentRank }) {
                step = next
            } else {
                finish()
            }
            return
        }
        guard index + 1 < steps.count else {
            finish()
            return
        }
        step = steps[index + 1]
    }

    func goBack() {
        phaseProgress = nil
        guard let index = activeSteps.firstIndex(of: step), index > 0 else { return }
        step = activeSteps[index - 1]
    }

    /// Skip the rest of setup. Marks onboarding complete without seeding alarms
    /// or touching units, leaving any existing configuration untouched.
    func skip() {
        Storage.shared.hasCompletedOnboarding.value = true
        onClose()
    }

    /// Finish setup: seed selected alarms, mark units/onboarding complete, and
    /// kick a refresh so the home screen loads data immediately.
    func finish() {
        persistSeededAlarms()
        Storage.shared.hasConfiguredUnits.value = true
        Storage.shared.hasCompletedOnboarding.value = true
        onClose()
        NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
    }

    // MARK: - Alarm seeding

    private func persistSeededAlarms() {
        var alarms = Storage.shared.alarms.value
        let existingTypes = Set(alarms.map(\.type))

        for seed in offeredSeedAlarms where seed.isEnabled {
            // Don't re-add a type the user already configured. The two seeded Low
            // alarms (warning + urgent) are both added on a fresh install because
            // `existingTypes` is sampled once, before any are appended.
            guard !existingTypes.contains(seed.type) else { continue }
            alarms.append(seed.alarm)
        }

        Storage.shared.alarms.value = alarms
    }

    // MARK: - Default seed set

    /// The recommended alarms, in page order. Glucose and phone alarms are offered
    /// to everyone; loop/pump/insulin alarms require Nightscout data.
    private static func defaultSeedAlarms() -> [SeedAlarm] {
        func seed(
            _ type: AlarmType,
            title: String,
            detail: String,
            requiresNightscout: Bool,
            configure: (inout Alarm) -> Void = { _ in }
        ) -> SeedAlarm {
            var alarm = Alarm(type: type)
            configure(&alarm)
            return SeedAlarm(
                alarm: alarm,
                isEnabled: true,
                title: title,
                detail: detail,
                requiresNightscout: requiresNightscout
            )
        }

        return [
            seed(.low, title: "Low glucose",
                 detail: "Warns when glucose is low, now or soon.",
                 requiresNightscout: false)
            {
                $0.name = "Low glucose"
                $0.belowBG = 70
                $0.predictiveMinutes = 20
            },
            seed(.low, title: "Urgent low",
                 detail: "A separate warning for when glucose is very low.",
                 requiresNightscout: false)
            {
                $0.name = "Urgent low"
                $0.belowBG = 54
                $0.predictiveMinutes = 0
                $0.persistentMinutes = 0
            },
            seed(.high, title: "High glucose",
                 detail: "Warns when glucose is high.",
                 requiresNightscout: false)
            {
                $0.aboveBG = 180
            },
            seed(.fastDrop, title: "Fast drop",
                 detail: "Warns when glucose is falling quickly.",
                 requiresNightscout: false),
            seed(.missedReading, title: "Missed readings",
                 detail: "Warns when glucose stops updating.",
                 requiresNightscout: false),
            seed(.notLooping, title: "Not looping",
                 detail: "Warns when the loop stops running.",
                 requiresNightscout: true),
            seed(.battery, title: "Looping phone battery",
                 detail: "Warns when the battery of the phone running Loop or Trio is low.",
                 requiresNightscout: true),
            seed(.iob, title: "IOB",
                 detail: "Warns when insulin on board is high.",
                 requiresNightscout: true),
            seed(.cob, title: "COB",
                 detail: "Warns when carbs on board are high.",
                 requiresNightscout: true),
            seed(.sensorChange, title: "Sensor change",
                 detail: "Reminds you when the CGM sensor is due.",
                 requiresNightscout: true)
            {
                $0.threshold = 10
                $0.activeOption = .day
            },
            seed(.pumpChange, title: "Pump change",
                 detail: "Reminds you when the pump site is due.",
                 requiresNightscout: true)
            {
                $0.threshold = 3
                $0.activeOption = .day
            },
            seed(.pump, title: "Pump insulin",
                 detail: "Warns when pump reservoir insulin is low.",
                 requiresNightscout: true),
        ]
    }
}
