// Sumly for iOS · https://sumly.dev
//
// Counts screens and events in your app next to your website, and ties a
// payment to the person who made it. One file, no dependencies: add it to
// your target.
//
//   Sumly.configure(siteKey: "sl_…")                    // in App.init or didFinishLaunching
//   Sumly.screen("Home")                                // or .sumlyScreen("Home") on a SwiftUI view
//   Sumly.track("signup", props: ["plan": "pro"])
//   Sumly.identify(id: user.id, email: user.email, name: user.name)
//
// Payments: pass Sumly.visitorID to Stripe metadata as sumly_visitor_id, or
// to RevenueCat as the subscriber attribute sumly_visitor_id
// (Purchases.shared.attribution.setAttributes(["sumly_visitor_id": Sumly.visitorID])),
// and Sumly traces the money to the visit.
//
// Privacy: no IDFA and no tracking across apps, so no App Tracking
// Transparency prompt. The install id is kept in UserDefaults, which Apple
// lists as a required-reason API: declare NSPrivacyAccessedAPICategoryUserDefaults
// with reason CA92.1 in your app's PrivacyInfo.xcprivacy (most apps already do).
// In App Privacy, declare Product Interaction and Coarse Location (country and
// city from the network), and User ID, Email or Name if you call identify;
// none of them used for tracking.

import Foundation
#if canImport(UIKit)
import UIKit
#endif
#if canImport(SwiftUI)
import SwiftUI
#endif

public final class Sumly {
    public static let shared = Sumly()

    private let idKey = "sumly_vid"
    private let queueKey = "sumly_queue"
    private let maxQueue = 300
    private let lock = NSLock()
    private var siteKey: String?
    private var endpoint = URL(string: "https://sumly.dev/api/e")!
    private var queue: [[String: Any]] = []
    private var current = "App"
    private var sending = false
    private var timer: Timer?
    private var debug = false

    /// The install id, passed as sumly_visitor_id in Stripe metadata or as a RevenueCat attribute.
    public static var visitorID: String { shared.visitor }

    private lazy var visitor: String = {
        if let saved = UserDefaults.standard.string(forKey: idKey) { return saved }
        let made = UUID().uuidString.lowercased()
        UserDefaults.standard.set(made, forKey: idKey)
        return made
    }()

    // MARK: - Public

    public static func configure(siteKey: String, endpoint: URL? = nil, debug: Bool = false) {
        shared.configure(siteKey: siteKey, endpoint: endpoint, debug: debug)
    }

    /// A screen the person opened; counts like a page on the web.
    public static func screen(_ name: String, props: [String: Any]? = nil) {
        shared.current = name.trimmingCharacters(in: CharacterSet(charactersIn: "/")).isEmpty ? "App" : name
        var item: [String: Any] = ["t": "pageview", "n": shared.current]
        if let props = plain(props) { item["p"] = props }
        shared.push(item)
    }

    /// Something the person did: signup, upgrade, share.
    public static func track(_ name: String, props: [String: Any]? = nil) {
        var item: [String: Any] = ["t": "event", "n": name]
        if let props = plain(props) { item["p"] = props }
        shared.push(item)
    }

    /// Who the person is once they sign in.
    public static func identify(id: String? = nil, email: String? = nil, name: String? = nil) {
        var who: [String: Any] = [:]
        if let id { who["id"] = id }
        if let email { who["email"] = email }
        if let name { who["name"] = name }
        shared.push(["t": "identify", "p": who])
        shared.flush()
    }

    public static func flush() { shared.flush() }

    /// Only text, numbers and true/false travel: anything else (a Date, a URL) would stop the queue from saving.
    private static func plain(_ props: [String: Any]?) -> [String: Any]? {
        guard let props else { return nil }
        let kept = props.filter { $0.value is String || $0.value is NSNumber || $0.value is Bool }
        return kept.isEmpty ? nil : kept
    }

    // MARK: - Inside

    private func configure(siteKey: String, endpoint: URL?, debug: Bool) {
        self.siteKey = siteKey
        self.debug = debug
        if let endpoint { self.endpoint = endpoint }
        if let data = UserDefaults.standard.data(forKey: queueKey),
           let saved = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
            queue = Array((saved + queue).suffix(maxQueue))
        }
        _ = visitor
        DispatchQueue.main.async {
            self.timer?.invalidate()
            self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in self.flush() }
        }
        #if canImport(UIKit) && !os(watchOS)
        NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { _ in self.flush() }
        #endif
    }

    private func push(_ item: [String: Any]) {
        guard siteKey != nil else { return }
        var item = item
        item["u"] = "app://\(Bundle.main.bundleIdentifier ?? "app")/\(current)"
        item["ts"] = Int(Date().timeIntervalSince1970 * 1000)
        // k tells this item apart, so a sent batch is taken off the queue by what was sent, not by count.
        item["k"] = String(UUID().uuidString.prefix(12))
        lock.lock()
        queue.append(item)
        if queue.count > maxQueue { queue.removeFirst(queue.count - maxQueue) }
        let count = queue.count
        lock.unlock()
        save()
        if count >= 20 { flush() }
    }

    private func save() {
        lock.lock()
        let copy = queue
        lock.unlock()
        if JSONSerialization.isValidJSONObject(copy), let data = try? JSONSerialization.data(withJSONObject: copy) {
            UserDefaults.standard.set(data, forKey: queueKey)
        }
    }

    private func flush() {
        lock.lock()
        guard let siteKey, !sending, !queue.isEmpty else { lock.unlock(); return }
        sending = true
        let batch = Array(queue.prefix(50))
        lock.unlock()

        var body: [String: Any] = [
            "s": siteKey, "v": visitor, "pl": "ios",
            "os": "\(osName) \(osVersion)", "dt": isTablet ? "tablet" : "phone",
            "batch": batch,
        ]
        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { body["av"] = version }

        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
        request.setValue("Sumly-SDK/1.0 (ios)", forHTTPHeaderField: "User-Agent")
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { _, response, error in
            let status = (response as? HTTPURLResponse)?.statusCode ?? 0
            self.lock.lock()
            // A refused batch (4xx) would be refused again, so it goes; "too many" (429) and server trouble wait for the next try.
            if error == nil, (200..<300).contains(status) || ((400..<500).contains(status) && status != 429) {
                let sent = Set(batch.compactMap { $0["k"] as? String })
                self.queue.removeAll { sent.contains($0["k"] as? String ?? "") }
            }
            self.sending = false
            self.lock.unlock()
            self.save()
            if self.debug { print("[Sumly] sent \(batch.count), status \(status)") }
        }.resume()
    }

    private var osName: String {
        #if os(iOS)
        return UIDevice.current.userInterfaceIdiom == .pad ? "iPadOS" : "iOS"
        #else
        return "iOS"
        #endif
    }

    private var osVersion: String {
        let v = ProcessInfo.processInfo.operatingSystemVersion
        return "\(v.majorVersion).\(v.minorVersion)"
    }

    private var isTablet: Bool {
        #if os(iOS)
        return UIDevice.current.userInterfaceIdiom == .pad
        #else
        return false
        #endif
    }
}

#if canImport(SwiftUI)
public extension View {
    /// Counts this view as a screen each time it appears.
    func sumlyScreen(_ name: String) -> some View {
        onAppear { Sumly.screen(name) }
    }
}
#endif
