import Foundation
import Observation
import XPCWrappers

/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
@Observable public final class Updater: UpdaterProtocol, Sendable {

    private let state = State()
    @MainActor @Observable public final class State {
        var update: Release? = nil
        nonisolated init() {}
    }
    public var update: Release? {
        state.update
    }

    /// The current version of the app that is running.
    public let currentVersion: SemVer

    /// The current OS version.
    private let osVersion: SemVer

    /// Initializes an Updater.
    /// - Parameters:
    ///   - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch.
    ///   - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour.
    ///   - osVersion: The current OS version.
    ///   - currentVersion: The current version of the app that is running.
    public init(
        checkOnLaunch: Bool,
        checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value,
        osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion),
        currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")
    ) {
        self.osVersion = osVersion
        self.currentVersion = currentVersion
        Task {
            if checkOnLaunch {
                try await checkForUpdates()
            }
            while !Task.isCancelled {
                try? await Task.sleep(for: .seconds(Int(checkFrequency)))
                try await checkForUpdates()
            }
        }
    }

    /// Manually trigger an update check.
    public func checkForUpdates() async throws {
        let session = try await XPCTypedSession<[Release], Never>(serviceName: "com.maxgoedjen.Secretive.SecretiveUpdater")
        await evaluate(releases: try await session.send())
        session.complete()
    }


    /// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
    /// - Parameter release: The release to ignore.
    public func ignore(release: Release) async {
        guard !release.critical else { return }
        defaults.set(true, forKey: release.name)
        await MainActor.run {
            state.update = nil
        }
    }

}

extension Updater {

    /// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run.
    /// - Parameter releases: An array of ``Release`` objects.
    func evaluate(releases: [Release]) async {
        guard let release = releases
                .sorted()
                .reversed()
                .filter({ !$0.prerelease })
                .first(where: { $0.minimumOSVersion <= osVersion }) else { return }
        guard !userIgnored(release: release) else { return }
        guard !release.prerelease else { return }
        let latestVersion = SemVer(release.name)
        if latestVersion > currentVersion {
            await MainActor.run {
                state.update = release
            }
        }
    }

    /// Checks whether the user has ignored a release.
    /// - Parameter release: The release to check.
    /// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical.
    func userIgnored(release: Release) -> Bool {
        guard !release.critical else { return false }
        return defaults.bool(forKey: release.name)
    }

    /// The user defaults used to store user ignore state.
    var defaults: UserDefaults {
        UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
    }

}
