import ForgejoKit
import Foundation
import SwiftData
import UIKit
import UserNotifications

final class BackgroundDownloadManager: NSObject, Sendable {
    static let shared = BackgroundDownloadManager()

    private static let sessionIdentifier = "de.hausotte.Forji.bgDownload"

    private let seenStore = SeenNotificationStore()
    private let queue = DispatchQueue(label: "de.hausotte.Forji.bgDownloadManager")

    private nonisolated(unsafe) var backgroundSession: URLSession!
    private nonisolated(unsafe) var completionHandler: (() -> Void)?
    private nonisolated(unsafe) var pendingDownloads = 0
    private nonisolated(unsafe) var totalUnread = 0

    override private init() {
        super.init()
        backgroundSession = createSession()
    }

    private func createSession() -> URLSession {
        let config = URLSessionConfiguration.background(withIdentifier: Self.sessionIdentifier)
        config.sessionSendsLaunchEvents = true
        config.isDiscretionary = false
        config.timeoutIntervalForResource = 60
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.from(queue))
    }

    // MARK: - Public API

    func reconnectIfNeeded() {
        // Accessing backgroundSession triggers reconnection to any in-flight downloads
        _ = backgroundSession
    }

    func setCompletionHandler(_ handler: @escaping () -> Void) {
        queue.sync { completionHandler = handler }
    }

    func startDownloads() {
        Task { @MainActor in
            let instances = self.fetchInstances()
            guard !instances.isEmpty else { return }

            self.queue.sync {
                self.pendingDownloads = 0
                self.totalUnread = 0
            }

            for instance in instances {
                guard let request = await self.buildRequest(for: instance) else { continue }
                self.queue.sync { self.pendingDownloads += 1 }
                self.backgroundSession.downloadTask(with: request).resume()
            }
        }
    }

    // MARK: - Instance Fetching

    @MainActor
    private func fetchInstances() -> [InstanceMetadata] {
        do {
            let schema = Schema([ForgejoInstance.self])
            let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
            let container = try ModelContainer(for: schema, configurations: [config])
            let context = ModelContext(container)
            let descriptor = FetchDescriptor<ForgejoInstance>()
            let fetched = try context.fetch(descriptor)
            return fetched.map {
                InstanceMetadata(
                    serverURL: ForgejoClient.normalizeServerURL($0.serverURL),
                    username: $0.username,
                    name: $0.name,
                )
            }
        } catch {
            return []
        }
    }

    private func buildRequest(for instance: InstanceMetadata) async -> URLRequest? {
        guard let token = try? await KeychainManager.shared.getToken(
            for: instance.serverURL, username: instance.username,
        ) else { return nil }

        guard var components = URLComponents(string: "\(instance.serverURL)/api/v1/notifications") else {
            return nil
        }
        components.queryItems = [
            URLQueryItem(name: "status-types", value: "unread"),
            URLQueryItem(name: "page", value: "1"),
            URLQueryItem(name: "limit", value: "50"),
        ]

        guard let url = components.url else { return nil }

        var request = URLRequest(url: url)
        request.setValue("token \(token)", forHTTPHeaderField: "Authorization")

        storeMetadata(instance, for: url)

        return request
    }

    // MARK: - Metadata Storage

    private func storeMetadata(_ metadata: InstanceMetadata, for url: URL) {
        let key = url.absoluteString
        var map = loadMetadataMap()
        map[key] = metadata
        if let data = try? JSONEncoder().encode(map) {
            try? data.write(to: metadataFileURL())
        }
    }

    private func loadMetadata(for url: URL) -> InstanceMetadata? {
        let map = loadMetadataMap()
        return map[url.absoluteString]
    }

    private func removeMetadata(for url: URL) {
        var map = loadMetadataMap()
        map.removeValue(forKey: url.absoluteString)
        if let data = try? JSONEncoder().encode(map) {
            try? data.write(to: metadataFileURL())
        }
    }

    private func loadMetadataMap() -> [String: InstanceMetadata] {
        guard let data = try? Data(contentsOf: metadataFileURL()),
              let map = try? JSONDecoder().decode([String: InstanceMetadata].self, from: data)
        else { return [:] }
        return map
    }

    private func metadataFileURL() -> URL {
        FileManager.default.temporaryDirectory.appendingPathComponent("forji_bg_metadata.json")
    }
}

// MARK: - URLSessionDownloadDelegate

extension BackgroundDownloadManager: URLSessionDownloadDelegate {
    func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        guard let originalURL = downloadTask.originalRequest?.url,
              let metadata = loadMetadata(for: originalURL)
        else { return }

        defer { removeMetadata(for: originalURL) }

        guard let data = try? Data(contentsOf: location) else { return }

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = forgejoDateDecodingStrategy

        guard let notifications = try? decoder.decode([NotificationThread].self, from: data) else { return }

        let currentIDs = Set(notifications.map(\.id))
        totalUnread += currentIDs.count

        if !seenStore.isSeeded(for: metadata.serverURL, username: metadata.username) {
            seenStore.seed(ids: currentIDs, for: metadata.serverURL, username: metadata.username)
        } else {
            let seenIDs = seenStore.seenIDs(for: metadata.serverURL, username: metadata.username)
            let newIDs = currentIDs.subtracting(seenIDs)

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

            seenStore.prune(keeping: currentIDs, for: metadata.serverURL, username: metadata.username)
        }
    }

    func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
        if error != nil {
            if let url = task.originalRequest?.url {
                removeMetadata(for: url)
            }
        }

        pendingDownloads -= 1
        if pendingDownloads <= 0 {
            pendingDownloads = 0
            updateBadge(count: totalUnread)
        }
    }

    func urlSessionDidFinishEvents(forBackgroundURLSession _: URLSession) {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            let handler = queue.sync { () -> (() -> Void)? in
                let pending = self.completionHandler
                self.completionHandler = nil
                return pending
            }
            handler?()
        }
    }

    // MARK: - Local Notifications

    private func postLocalNotifications(
        _ notifications: [NotificationThread],
        instanceName: String,
        serverURL: String,
        username: String,
    ) {
        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,
            )

            center.add(request)
        }
    }

    private func updateBadge(count: Int) {
        DispatchQueue.main.async {
            UNUserNotificationCenter.current().setBadgeCount(count)
        }
    }
}

// MARK: - OperationQueue Helper

private extension OperationQueue {
    static func from(_ queue: DispatchQueue) -> OperationQueue {
        let opQueue = OperationQueue()
        opQueue.underlyingQueue = queue
        opQueue.maxConcurrentOperationCount = 1
        return opQueue
    }
}

// MARK: - Instance Metadata

nonisolated struct InstanceMetadata: Codable {
    let serverURL: String
    let username: String
    let name: String
}
