import Foundation
import Security
import NucleicProtocol

/// Sends the macOS host's anonymous DAI (daily active installs) heartbeat at most once per day
/// (`docs/CLOUD_INFRA.md` §4).
/// Opt-out (default on) via `HeartbeatSettings`; local development builds never report.
///
/// The install ID is a random UUID kept only in the Keychain, deliberately **separate** from the
/// Noise device identity (`HostIdentityStore`) so telemetry can't be correlated with the crypto
/// identity. Mirrors that store's Keychain pattern.
public enum HeartbeatReporter {
    /// Fire-and-forget; safe to call on every launch. Self-gates on the opt-out switch, the build
    /// channel, and the once-per-day schedule, and swallows network errors (retried next launch).
    public static func reportIfDue(
        session: URLSession = .shared, defaults: UserDefaults = .standard
    ) async {
        guard HeartbeatSettings.isEnabled(defaults) else { return }
        let channel = Self.channel
        guard channel != "dev" else { return }            // never count local development builds
        guard HeartbeatSchedule.isDue(defaults) else { return }

        let info = Bundle.main.infoDictionary
        let os = ProcessInfo.processInfo.operatingSystemVersion
        let beat = Heartbeat(
            installId: installID(),
            platform: "macos",
            osVersion: "\(os.majorVersion).\(os.minorVersion)",
            channel: channel,
            appVersion: info?["CFBundleShortVersionString"] as? String ?? "unknown",
            build: info?["CFBundleVersion"] as? String ?? "unknown",
            arch: Self.arch,
            locale: Locale.current.language.languageCode?.identifier ?? "und")

        do {
            try await post(beat, session: session)
            HeartbeatSchedule.markSent(defaults)
        } catch {
            // Best-effort telemetry: drop it and try again next launch.
        }
    }

    // MARK: - Build identity

    /// The same compile-time channel define the build banner uses — `Package.swift` sets it on
    /// this target too. `dev` short-circuits before any send.
    private static var channel: String {
        #if NUCLEIC_STABLE
        return "stable"
        #elseif NUCLEIC_RC
        return "rc"
        #elseif NUCLEIC_BETA
        return "beta"
        #else
        return "dev"
        #endif
    }

    private static var arch: String {
        #if arch(arm64)
        return "arm64"
        #elseif arch(x86_64)
        return "x86_64"
        #else
        return "unknown"
        #endif
    }

    // MARK: - Transport

    private static func post(_ beat: Heartbeat, session: URLSession) async throws {
        var req = URLRequest(url: HeartbeatSchedule.endpoint)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONEncoder().encode(beat)
        let (_, resp) = try await session.data(for: req)
        guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }
    }

    // MARK: - Anonymous install id (Keychain; separate from the Noise identity)

    private static let installAccount = "com.nucleic.host.installid"

    private static func installID() -> String {
        if let data = keychainRead(installAccount), let id = String(data: data, encoding: .utf8) {
            return id
        }
        let id = UUID().uuidString
        keychainWrite(Data(id.utf8), account: installAccount)
        return id
    }

    private static func keychainRead(_ account: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne,
        ]
        var item: CFTypeRef?
        guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess else { return nil }
        return item as? Data
    }

    private static func keychainWrite(_ data: Data, account: String) {
        SecItemDelete([
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
        ] as CFDictionary)
        SecItemAdd([
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
        ] as CFDictionary, nil)
    }
}
