import BackgroundTasks
import ForgejoKit
import SwiftData
import UIKit
import UserNotifications

actor NotificationPollingService {
    static let shared = NotificationPollingService()
    static let backgroundTaskIdentifier = "de.hausotte.Forji.notificationPoll"

    private let seenStore = SeenNotificationStore()
    private var foregroundTask: Task<Void, Never>?

    private init() {}

    // MARK: - Background Task Registration

    nonisolated func registerBackgroundTask() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: Self.backgroundTaskIdentifier,
            using: nil,
        ) { task in
            guard let refreshTask = task as? BGAppRefreshTask else { return }
            Task { await self.handleBackgroundTask(refreshTask) }
        }
    }

    nonisolated func scheduleBackgroundPoll() {
        let request = BGAppRefreshTaskRequest(identifier: Self.backgroundTaskIdentifier)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            #if DEBUG
                print("Failed to schedule background poll: \(error)")
            #endif
        }
    }

    private func handleBackgroundTask(_ task: BGAppRefreshTask) async {
        scheduleBackgroundPoll()
        await BackgroundDownloadManager.shared.startDownloads()
        task.setTaskCompleted(success: true)
    }

    // MARK: - Foreground Polling

    func startForegroundPolling(modelContainer: ModelContainer) {
        stopForegroundPolling()
        foregroundTask = Task { [weak self] in
            guard let self else { return }
            while !Task.isCancelled {
                await pollAllInstances(
                    modelContainer: modelContainer, postNotifications: false,
                )
                try? await Task.sleep(for: .seconds(60))
            }
        }
    }

    func stopForegroundPolling() {
        foregroundTask?.cancel()
        foregroundTask = nil
    }

    // MARK: - Core Polling

    func pollAllInstances(
        modelContainer: ModelContainer, postNotifications: Bool,
    ) async {
        let instances: [PollingInstanceSnapshot]
        do {
            instances = try await MainActor.run {
                let context = ModelContext(modelContainer)
                let descriptor = FetchDescriptor<ForgejoInstance>()
                let fetched = try context.fetch(descriptor)
                return fetched.map {
                    PollingInstanceSnapshot(
                        serverURL: $0.serverURL,
                        username: $0.username,
                        name: $0.name,
                        allowSelfSignedCertificates: $0.allowSelfSignedCertificates,
                    )
                }
            }
        } catch {
            return
        }

        var totalUnread = 0
        for instance in instances {
            totalUnread += await pollInstance(
                instance, postNotifications: postNotifications,
            )
        }
        await updateBadge(count: totalUnread)
    }

    private func pollInstance(
        _ instance: PollingInstanceSnapshot, postNotifications: Bool,
    ) async -> Int {
        let normalizedURL = ForgejoClient.normalizeServerURL(instance.serverURL)
        guard let token = try? await KeychainManager.shared.getToken(
            for: normalizedURL, username: instance.username,
        ) else {
            return 0
        }

        let client = ForgejoClient(
            serverURL: normalizedURL,
            username: instance.username,
            token: token,
            allowSelfSignedCertificates: instance.allowSelfSignedCertificates,
        )
        let notificationService = NotificationService(client: client)

        do {
            let unread = try await notificationService.fetchNotifications(
                statusTypes: ["unread"], page: 1, limit: 50,
            )
            let currentIDs = Set(unread.map(\.id))

            if !seenStore.isSeeded(for: normalizedURL, username: instance.username) {
                seenStore.seed(ids: currentIDs, for: normalizedURL, username: instance.username)
                return currentIDs.count
            }

            let seenIDs = seenStore.seenIDs(for: normalizedURL, username: instance.username)
            let newIDs = currentIDs.subtracting(seenIDs)

            if !newIDs.isEmpty {
                if postNotifications {
                    let newNotifications = unread.filter { newIDs.contains($0.id) }
                    await postLocalNotifications(
                        newNotifications,
                        instanceName: instance.name.isEmpty ? instance.serverURL : instance.name,
                        serverURL: normalizedURL,
                        username: instance.username,
                    )
                }
                seenStore.markSeen(newIDs, for: normalizedURL, username: instance.username)
            }

            seenStore.prune(keeping: currentIDs, for: normalizedURL, username: instance.username)
            return currentIDs.count
        } catch {
            return 0
        }
    }

    // MARK: - Local Notifications

    private func postLocalNotifications(
        _ notifications: [NotificationThread],
        instanceName: String,
        serverURL: String,
        username: String,
    ) async {
        let center = UNUserNotificationCenter.current()

        for notification in notifications {
            let content = UNMutableNotificationContent()
            content.title = instanceName
            content.subtitle = notification.repository.fullName
            content.body = notification.subject.title
            content.sound = .default
            content.threadIdentifier = "\(serverURL)_\(username)"
            content.categoryIdentifier = "FORGEJO_NOTIFICATION"
            content.userInfo = [
                "serverURL": serverURL,
                "username": username,
                "notificationID": notification.id,
                "subjectType": notification.subject.type,
            ]

            let requestID = "forji_\(serverURL)_\(notification.id)"
            let request = UNNotificationRequest(
                identifier: requestID,
                content: content,
                trigger: nil,
            )

            try? await center.add(request)
        }
    }

    private func updateBadge(count: Int) async {
        await MainActor.run {
            UNUserNotificationCenter.current().setBadgeCount(count)
        }
    }
}

private nonisolated struct PollingInstanceSnapshot {
    let serverURL: String
    let username: String
    let name: String
    let allowSelfSignedCertificates: Bool
}
