import Foundation
import FirebaseFirestore
import FirebaseAuth
import FirebaseFunctions

// MARK: - Firestore Service

final class FirestoreService: @unchecked Sendable {
    static let shared = FirestoreService()
    
    let db = Firestore.firestore()
    let functions = Functions.functions()
    
    private init() {}
    
    // MARK: - Collection References
    
    func usersCollection() -> CollectionReference {
        db.collection("users")
    }
    
    func userDocument(_ userId: String) -> DocumentReference {
        usersCollection().document(userId)
    }
    
    func couplesCollection() -> CollectionReference {
        db.collection("couples")
    }
    
    func coupleDocument(_ coupleId: String) -> DocumentReference {
        couplesCollection().document(coupleId)
    }

    // TODO(iOS-E2EE): The iOS MVP skips E2EE, so any couple created directly
    // from iOS must be written with encryptionVersion = 0 (PLAINTEXT).
    // Cross-platform couples must be created via acceptInviteCallable (Android
    // path) which writes encryptionVersion = 2. Do NOT create a mixed v0/v2
    // couple from iOS until iOS implements Tink-compatible E2EE parity.
    // See Android: app/src/main/java/app/closer/crypto/EncryptionVersion.kt
    
    func invitesCollection() -> CollectionReference {
        db.collection("invites")
    }
    
    func inviteDocument(_ code: String) -> DocumentReference {
        invitesCollection().document(code)
    }
    
    // MARK: - Subcollections
    
    func dailyQuestionRef(coupleId: String, date: String) -> DocumentReference {
        coupleDocument(coupleId)
            .collection("daily_question")
            .document(date)
    }
    
    func dailyAnswerRef(coupleId: String, date: String, userId: String) -> DocumentReference {
        dailyQuestionRef(coupleId: coupleId, date: date)
            .collection("answers")
            .document(userId)
    }
    
    func questionThreadsRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("question_threads")
    }
    
    func dateSwipesRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("date_swipes")
    }
    
    func dateMatchesRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("date_matches")
    }
    
    func bucketListRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("bucket_list")
    }
    
    func capsulesRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("capsules")
    }
    
    func sessionsRef(coupleId: String) -> CollectionReference {
        coupleDocument(coupleId).collection("sessions")
    }
    
    func entitlementDocument(_ userId: String) -> DocumentReference {
        userDocument(userId)
            .collection("entitlements")
            .document("premium")
    }
    
    func fcmTokensRef(_ userId: String) -> CollectionReference {
        userDocument(userId).collection("fcmTokens")
    }
    
    // MARK: - Helpers
    
    func userId() throws -> String {
        guard let uid = Auth.auth().currentUser?.uid else {
            throw FirestoreError.notAuthenticated
        }
        return uid
    }
    
    func setDocument<T: Encodable>(_ value: T, at document: DocumentReference, merge: Bool = true) async throws {
        if merge {
            try await document.setData(value.asDictionary(), merge: true)
        } else {
            try await document.setData(value.asDictionary())
        }
    }
    
    func getDocument<T: Decodable>(at document: DocumentReference) async throws -> T? {
        let snapshot = try await document.getDocument()
        guard snapshot.exists else { return nil }
        return try snapshot.data(as: T.self)
    }
    
    func getDocuments<T: Decodable>(in collection: CollectionReference) async throws -> [T] {
        let snapshot = try await collection.getDocuments()
        return try snapshot.documents.compactMap { try $0.data(as: T.self) }
    }
    
    func queryDocuments<T: Decodable>(
        in collection: CollectionReference,
        where field: String,
        isEqualTo value: Any
    ) async throws -> [T] {
        let snapshot = try await collection.whereField(field, isEqualTo: value).getDocuments()
        return try snapshot.documents.compactMap { try $0.data(as: T.self) }
    }
}

// MARK: - Callable Functions

extension FirestoreService {
    // TODO(iOS-E2EE): iOS does not yet generate E2EE keys or encrypt the recovery phrase,
    // so iOS-originated invites create plaintext couples (encryptionVersion=0). Cross-platform
    // couples where the Android user invites must go through acceptInviteCallable on Android.
    // When iOS implements E2EE parity (CryptoKit keyset + Argon2id phrase cipher), update
    // createInviteCallable to supply wrappedCoupleKey, kdfSalt, kdfParams, and
    // encryptedRecoveryPhrase, and update acceptInviteCallable to decrypt the phrase.

    func acceptInviteCallable(code: String) async throws -> String {
        let result = try await functions.httpsCallable("acceptInviteCallable").call(["code": code])
        guard let coupleId = (result.data as? [String: Any])?["coupleId"] as? String else {
            throw FirestoreError.invalidResponse
        }
        return coupleId
    }

    func createInviteCallable() async throws -> (code: String, expiresAt: Date) {
        // iOS MVP omits all E2EE fields; server writes nulls and sets encryptionVersion=0.
        let data: [String: Any] = [:]
        let result = try await functions.httpsCallable("createInviteCallable").call(data)
        guard let payload = result.data as? [String: Any],
              let code = payload["code"] as? String,
              let expiresAtTimestamp = payload["expiresAt"] as? Timestamp else {
            throw FirestoreError.invalidResponse
        }
        return (code, expiresAtTimestamp.dateValue())
    }
    
    func leaveCoupleCallable() async throws {
        let result = try await functions.httpsCallable("leaveCoupleCallable").call()
        guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
            throw FirestoreError.invalidResponse
        }
    }
    
    func syncEntitlementCallable() async throws -> Entitlement {
        let result = try await functions.httpsCallable("syncEntitlement").call()
        guard let data = result.data as? [String: Any] else {
            throw FirestoreError.invalidResponse
        }
        return Entitlement(
            id: UUID().uuidString,
            userId: try userId(),
            source: "sync",
            productId: "closer_premium",
            isActive: data["premium"] as? Bool ?? false,
            expiresAt: (data["expiresAt"] as? Timestamp)?.dateValue(),
            updatedAt: Date()
        )
    }
    
    func sendGentleReminderCallable() async throws {
        try await functions.httpsCallable("sendGentleReminderCallable").call()
    }
    
    func deleteUserCallable() async throws {
        let result = try await functions.httpsCallable("deleteUserCallable").call()
        guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
            throw FirestoreError.invalidResponse
        }
    }
    
    func updateUserCallable(displayName: String, bio: String?) async throws {
        var data: [String: Any] = ["displayName": displayName]
        if let bio = bio {
            data["bio"] = bio
        }
        let result = try await functions.httpsCallable("updateUserCallable").call(data)
        guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
            throw FirestoreError.invalidResponse
        }
    }
    
    func exportDataCallable() async throws {
        let result = try await functions.httpsCallable("exportDataCallable").call()
        guard let success = (result.data as? [String: Any])?["success"] as? Bool, success else {
            throw FirestoreError.invalidResponse
        }
    }
}

// MARK: - Errors

enum FirestoreError: LocalizedError {
    case notAuthenticated
    case invalidResponse
    case documentNotFound
    case permissionDenied
    
    var errorDescription: String? {
        switch self {
        case .notAuthenticated: return "User is not signed in."
        case .invalidResponse: return "Invalid server response."
        case .documentNotFound: return "Document not found."
        case .permissionDenied: return "Permission denied."
        }
    }
}

// MARK: - Encodable Helpers

extension Encodable {
    func asDictionary() throws -> [String: Any] {
        let data = try JSONEncoder().encode(self)
        guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw EncodingError.invalidValue(self, .init(codingPath: [], debugDescription: "Not a dictionary"))
        }
        return dict
    }
}