import Foundation
import Capacitor
import FirebaseCore
import FirebaseFirestore

private actor ListenerRegistrationMap {
    private var listenerRegistrationMap: [String: ListenerRegistration] = [:]

    func addRegistration(_ listenerRegistration: ListenerRegistration, listenerId: String) async {
        listenerRegistrationMap[listenerId] = listenerRegistration
    }

    func removeRegistration(listenerId: String) async {
        if let listenerRegistration = listenerRegistrationMap[listenerId] {
            listenerRegistration.remove()
        }
        listenerRegistrationMap.removeValue(forKey: listenerId)
    }

    func removeAll() async {
        listenerRegistrationMap.forEach { _, value in
            value.remove()
        }
        listenerRegistrationMap.removeAll()
    }
}

@objc public class FirebaseFirestore: NSObject {
    private let plugin: FirebaseFirestorePlugin
    private let config: FirebaseFirestoreConfig
    private var listenerRegistrationMap = ListenerRegistrationMap()

    init(plugin: FirebaseFirestorePlugin, config: FirebaseFirestoreConfig) {
        self.plugin = plugin
        self.config = config
        super.init()
        if FirebaseApp.app() == nil {
            guard FirebaseOptions.defaultOptions() != nil else {
                CAPLog.print("[FirebaseFirestore] Firebase was not configured: GoogleService-Info.plist is missing from the app bundle.")
                return
            }
            FirebaseApp.configure()
        }
    }

    @objc public func addDocument(_ options: AddDocumentOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let data = options.getData()

        var documentReference: DocumentReference?
        documentReference = getFirestoreInstance().collection(reference).addDocument(data: data) { error in
            if let error = error {
                completion(nil, error)
            } else {
                let result = AddDocumentResult(documentReference!)
                completion(result, nil)
            }
        }
    }

    @objc public func setDocument(_ options: SetDocumentOptions, completion: @escaping (Error?) -> Void) {
        let reference = options.getReference()
        let data = options.getData()
        let merge = options.getMerge()

        getFirestoreInstance().document(reference).setData(data, merge: merge) { error in
            if let error = error {
                completion(error)
            } else {
                completion(nil)
            }
        }
    }

    @objc public func getDocument(_ options: GetDocumentOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()

        getFirestoreInstance().document(reference).getDocument { documentSnapshot, error in
            if let error = error {
                completion(nil, error)
            } else {
                let result = GetDocumentResult(documentSnapshot!)
                completion(result, nil)
            }
        }
    }

    @objc public func updateDocument(_ options: UpdateDocumentOptions, completion: @escaping (Error?) -> Void) {
        let reference = options.getReference()
        let data = options.getData()

        getFirestoreInstance().document(reference).updateData(data) { error in
            if let error = error {
                completion(error)
            } else {
                completion(nil)
            }
        }
    }

    @objc public func deleteDocument(_ options: DeleteDocumentOptions, completion: @escaping (Error?) -> Void) {
        let reference = options.getReference()

        getFirestoreInstance().document(reference).delete { error in
            if let error = error {
                completion(error)
            } else {
                completion(nil)
            }
        }
    }

    @objc public func writeBatch(_ options: WriteBatchOptions, completion: @escaping (Error?) -> Void) {
        let operations = options.getOperations()

        let batch = getFirestoreInstance().batch()
        for operation in operations {
            let type = operation.getType()
            let reference = operation.getReference()
            let data = operation.getData()

            let documentReference = getFirestoreInstance().document(reference)
            switch type {
            case "set":
                if let setOpts = operation.getOptions(), setOpts.isMerge() {
                    batch.setData(
                        data,
                        forDocument: documentReference,
                        merge: true
                    )
                } else {
                    batch.setData(data, forDocument: documentReference)
                }
            case "update":
                batch.updateData(data, forDocument: documentReference)
            case "delete":
                batch.deleteDocument(documentReference)
            default:
                break
            }
        }

        batch.commit { error in
            if let error = error {
                completion(error)
            } else {
                completion(nil)
            }
        }
    }

    @objc public func getCollection(_ options: GetCollectionOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let compositeFilter = options.getCompositeFilter()
        let queryConstraints = options.getQueryConstraints()

        Task {
            do {
                let collectionReference = getFirestoreInstance().collection(reference)
                var query = collectionReference as Query
                if let compositeFilter = compositeFilter {
                    if let filter = compositeFilter.toFilter() {
                        query = query.whereFilter(filter)
                    }
                }
                if !queryConstraints.isEmpty {
                    for queryConstraint in queryConstraints {
                        query = try await queryConstraint.toQuery(query: query, firestore: getFirestoreInstance())
                    }
                }

                let querySnapshot = try await query.getDocuments()
                let result = GetCollectionResult(querySnapshot)
                completion(result, nil)
            } catch {
                completion(nil, error)
            }
        }
    }

    @objc public func getCollectionGroup(_ options: GetCollectionGroupOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let compositeFilter = options.getCompositeFilter()
        let queryConstraints = options.getQueryConstraints()

        Task {
            do {
                let collectionReference = getFirestoreInstance().collectionGroup(reference)
                var query = collectionReference as Query
                if let compositeFilter = compositeFilter {
                    if let filter = compositeFilter.toFilter() {
                        query = query.whereFilter(filter)
                    }
                }
                if !queryConstraints.isEmpty {
                    for queryConstraint in queryConstraints {
                        query = try await queryConstraint.toQuery(query: query, firestore: getFirestoreInstance())
                    }
                }

                let querySnapshot = try await query.getDocuments()
                let result = GetCollectionResult(querySnapshot)
                completion(result, nil)
            } catch {
                completion(nil, error)
            }
        }
    }

    @objc public func disablePersistence() {
        let firestore = getFirestoreInstance()
        let settings = firestore.settings
        settings.cacheSettings = MemoryCacheSettings()
        firestore.settings = settings
    }

    @objc public func enablePersistence(_ cacheSizeBytes: NSNumber?) {
        let firestore = getFirestoreInstance()
        let settings = firestore.settings
        if let cacheSizeBytes = cacheSizeBytes {
            settings.cacheSettings = PersistentCacheSettings(sizeBytes: cacheSizeBytes)
        } else {
            settings.cacheSettings = PersistentCacheSettings()
        }
        firestore.settings = settings
    }

    @objc public func clearPersistence(completion: @escaping (Error?) -> Void) {
        getFirestoreInstance().clearPersistence { error in
            completion(error)
        }
    }

    @objc public func enableNetwork(completion: @escaping (Error?) -> Void) {
        getFirestoreInstance().enableNetwork { error in
            completion(error)
        }
    }

    @objc public func disableNetwork(completion: @escaping (Error?) -> Void) {
        getFirestoreInstance().disableNetwork { error in
            completion(error)
        }
    }

    @objc func useEmulator(_ host: String, _ port: Int) {
        let firestore = getFirestoreInstance()
        let settings = firestore.settings
        settings.host = "\(host):\(port)"
        settings.isSSLEnabled = false
        firestore.settings = settings
    }

    @objc public func getCountFromServer(_ options: GetCountFromServerOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let compositeFilter = options.getCompositeFilter()
        let queryConstraints = options.getQueryConstraints()

        Task {
            do {
                let collectionReference = getFirestoreInstance().collection(reference)
                var query = collectionReference as Query
                if let compositeFilter = compositeFilter {
                    if let filter = compositeFilter.toFilter() {
                        query = query.whereFilter(filter)
                    }
                }
                if !queryConstraints.isEmpty {
                    for queryConstraint in queryConstraints {
                        query = try await queryConstraint.toQuery(query: query, firestore: getFirestoreInstance())
                    }
                }

                let snapshot = try await query.count.getAggregation(source: .server)
                let result = GetCountFromServerResult(snapshot.count.intValue)
                completion(result, nil)
            } catch {
                completion(nil, error)
            }
        }
    }

    @objc public func addDocumentSnapshotListener(_ options: AddDocumentSnapshotListenerOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let includeMetadataChanges = options.getIncludeMetadataChanges()
        let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior())
        let callbackId = options.getCallbackId()

        let listenerRegistration = getFirestoreInstance().document(reference).addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { documentSnapshot, error in
            if let error = error {
                completion(nil, error)
            } else {
                let result = GetDocumentResult(documentSnapshot!, serverTimestampBehavior)
                completion(result, nil)
            }
        }
        Task {
            await self.listenerRegistrationMap.addRegistration(listenerRegistration, listenerId: callbackId)
        }
    }

    @objc public func addCollectionSnapshotListener(_ options: AddCollectionSnapshotListenerOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let compositeFilter = options.getCompositeFilter()
        let queryConstraints = options.getQueryConstraints()
        let includeMetadataChanges = options.getIncludeMetadataChanges()
        let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior())
        let callbackId = options.getCallbackId()

        Task {
            do {
                let collectionReference = getFirestoreInstance().collection(reference)
                var query = collectionReference as Query
                if let compositeFilter = compositeFilter {
                    if let filter = compositeFilter.toFilter() {
                        query = query.whereFilter(filter)
                    }
                }
                if !queryConstraints.isEmpty {
                    for queryConstraint in queryConstraints {
                        query = try await queryConstraint.toQuery(query: query, firestore: getFirestoreInstance())
                    }
                }

                let listenerRegistration = query.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { querySnapshot, error in
                    if let error = error {
                        completion(nil, error)
                    } else {
                        let result = GetCollectionResult(querySnapshot!, serverTimestampBehavior)
                        completion(result, nil)
                    }
                }
                await listenerRegistrationMap.addRegistration(listenerRegistration, listenerId: callbackId)
            } catch {
                completion(nil, error)
            }
        }
    }

    @objc public func addCollectionGroupSnapshotListener(_ options: AddCollectionGroupSnapshotListenerOptions, completion: @escaping (Result?, Error?) -> Void) {
        let reference = options.getReference()
        let compositeFilter = options.getCompositeFilter()
        let queryConstraints = options.getQueryConstraints()
        let includeMetadataChanges = options.getIncludeMetadataChanges()
        let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior())
        let callbackId = options.getCallbackId()

        Task {
            do {
                let collectionReference = getFirestoreInstance().collectionGroup(reference)
                var query = collectionReference as Query
                if let compositeFilter = compositeFilter {
                    if let filter = compositeFilter.toFilter() {
                        query = query.whereFilter(filter)
                    }
                }
                if !queryConstraints.isEmpty {
                    for queryConstraint in queryConstraints {
                        query = try await queryConstraint.toQuery(query: query, firestore: getFirestoreInstance())
                    }
                }

                let listenerRegistration = query.addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { querySnapshot, error in
                    if let error = error {
                        completion(nil, error)
                    } else {
                        let result = GetCollectionGroupResult(querySnapshot!, serverTimestampBehavior)
                        completion(result, nil)
                    }
                }
                await listenerRegistrationMap.addRegistration(listenerRegistration, listenerId: callbackId)
            } catch {
                completion(nil, error)
            }
        }
    }

    @objc public func removeSnapshotListener(_ options: RemoveSnapshotListenerOptions, completion: @escaping () -> Void) {
        let callbackId = options.getCallbackId()
        Task {
            await listenerRegistrationMap.removeRegistration(listenerId: callbackId)
            completion()
        }
    }

    @objc public func removeAllListeners(completion: @escaping () -> Void) {
        Task {
            await listenerRegistrationMap.removeAll()
            completion()
        }
    }

    public func getFirestoreInstance() -> Firestore {
        if let databaseId = config.databaseId {
            return Firestore.firestore(database: databaseId)
        }
        return Firestore.firestore()
    }
}
