//
//  AuthSessionStore.swift
//  nirvana-ios
//
//  Created by Arjun Patel on 12/14/21.
//

import Foundation
import Firebase
import Combine
import FirebaseAuth
import GoogleSignIn
import SwiftUI

enum SessionState {
    case notCheckedYet
    case isAuthenticated
    case isLoggedOut
}

protocol SessionStore {
    var sessionState:SessionState { get }
    var user:User? { get }
    
    func signInOrCreateUser()
    func logOut()
    func unbind()
    func setupAuthListen()
}

final class AuthSessionStore: ObservableObject, SessionStore {
    @Published var user : User?
    @Published var sessionState: SessionState = SessionState.notCheckedYet
    
    // TODO: figure out which ones to publish
    var messagesArr: [Message] = []
    var userFriendsDict: [String: UserFriends] = [:] // all active and inactive relationships
    
    // transformed data for the views
    @Published var friendsArr: [String] = []
    @Published var inboxUsersArr: [String] = []
    @Published var relevantUsersDict: [String: User] = [:] // all cached/snapshotted users from db for app use
    @Published var relevantMessagesByUserDict: [String: [Message]] = [:] // note, this is all messages related to me
    
    private var friendsListeners: [ListenerRegistration] = []
    
    private var dataListeners: [ListenerRegistration] = []
    private var listenersActive = false // false on app/processes killed
    
    // TODO: temporary...should not have this here
    private var db = Firestore.firestore()
        
    private var GIDconfig:GIDConfiguration
    
    private var handler: AuthStateDidChangeListenerHandle?
    
    private var firestoreService: FirestoreService = FirestoreService()
    
    init(isPreview: Bool) {
        let fakeUser = User(id: UUID().uuidString, nickname: "arjunya", phoneNumber: "+19302919293")
        self.user = fakeUser
        
        let clientID = FirebaseApp.app()?.options.clientID
        self.GIDconfig = GIDConfiguration(clientID: clientID!)
    }
    
    init() {
        // create google sign in configuration object
        let clientID = FirebaseApp.app()?.options.clientID
        self.GIDconfig = GIDConfiguration(clientID: clientID!)
        
        print("the google client id: prolly shouldn't be printing this: \(clientID)")
        
        self.setupAuthListen()
    }
    
    // MARK: OLD for google sign in with link
    func signInOrCreateUser() {
        if GIDSignIn.sharedInstance.currentUser == nil {
            guard
                let viewController = UIApplication.shared.windows.first!.rootViewController
            else {
                return
            }
            
            GIDSignIn.sharedInstance.signIn(with: self.GIDconfig, presenting: viewController)
                {[self] user, err in
                    
                    if err != nil {
                        print(err!.localizedDescription)
                        return
                    }
                    
                    guard
                      let authentication = user?.authentication,
                      let idToken = authentication.idToken
                    else {
                      return
                    }

                    let credential = GoogleAuthProvider.credential(withIDToken: idToken,
                                                                   accessToken: authentication.accessToken)
                    
                    // sign in with firebase
                    self.firebaseAuth(withCredentials: credential)
                }
        }
    }
    
    // MARK: OLD for google sign in with link
    func firebaseAuth(withCredentials authCred: AuthCredential) {
        Auth.auth().signIn(with: authCred) { result, error in
            if error != nil {
                print(error!.localizedDescription)
            }
            
            guard let user = result?.user else {
                return
            }
            
            // User is signed in
            print(user.displayName ?? "Success!")
            print(user)
        }
    }
    
    func logOut() {
         do {
           try Auth.auth().signOut()
             
           self.deinitAllDataListeners()
         } catch let signOutError as NSError {
           print("Error signing out: %@", signOutError)
         }
    }
    
    func unbind () {
        if let handle = self.handler {
            Auth.auth().removeStateDidChangeListener(handle)
        }
    }
    
    static func getCurrentUserId() -> String?  {
        return Auth.auth().currentUser?.uid
    }
    
    func setupAuthListen() {
        // monitor authentication changes using firebase
        self.handler = Auth.auth().addStateDidChangeListener { [weak self] res, authUser in
            print("auth listener activated")
            
            guard let self = self else { return }
         
            self.sessionState = authUser == nil ? SessionState.isLoggedOut : SessionState.isAuthenticated
            print("set the session state to \(self.sessionState)")
            
            // get the user from the auth table in firebase auth
            if let uid = authUser?.uid {
                // if we have a user, create a new user model
                print("auth listener: Got user: \(uid)")
                print("auth listener: phone number: \(authUser?.phoneNumber)")
                
            } else {
                // if we don't have a user, set our session to nil
                self.user = nil
                self.sessionState = .isLoggedOut
            }
        }
    }
}

extension UIApplication {
  func getRootViewController() -> UIViewController {
      guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return .init() }
      
      guard let root = screen.windows.first?.rootViewController else {
          return .init()
      }
      
      return root
  }

}


// activate data listeners for the main data throughout the app
extension AuthSessionStore {
    // get all my friends
    // traverse all my friends, and put them into the friendsDict
    
    // get all messages where I am the receiver or sender in the past 5 days let's say
    //          this way, we won't have enormous lists
    // traverse all messages and put them in the relevantMessagesByUserDict
    //      [sarth: [arjun's message, sarth's message, arjun's message...], liam: [liam's message, liam's message]]
    // note: that list of messages should be sorted
    
    // can easily now traverse friends and find any messages to do with them
    
    // example:
    // I send a new message to sarth -> goes to messages database -> notification to sarth
    // sarth's data looks like this: [arjun: [...arjun's message, sarth's reply...]]
    
    func activateMainDataListeners() {
        // do nothing if the snapshots are already alive
        if self.listenersActive {
            print("listeners already alive...no point reinitiating")
            return
        }
        
        // extra validation to make sure that we are authenticated although should be if this is called
        if self.sessionState != SessionState.isAuthenticated {
            print("can't initiate data listeners...user not authenticated")
            return
        }
        
        // can't get proper listeners working if don't have current user's id
        let currUserId = AuthSessionStore.getCurrentUserId()
        if currUserId == nil {
            print("no user id available to initiate data listeners")
            return
        }
        
        // MARK: keeping the @Published user object updated
        // TODO: THIS WON'T UPDATE VIEW since user is a reference type...can manually publish...research and learn more
        self.firestoreService.getUserRealtime(userId: currUserId!) {[weak self] realtimeUpdatedUser in
            if realtimeUpdatedUser != nil {
                print("up to date user")
                self?.user = realtimeUpdatedUser
                self?.objectWillChange.send()
                
                self?.relevantUsersDict[currUserId!] = realtimeUpdatedUser
            }
        }
        
        // MARK: keeping the friends list updated
        // TODO: break into firestoreService metadata of each change...later tho since this listener will barely get changes
        let friendsListener = db.collection("user_friends").whereField("userId", isEqualTo: currUserId).limit(to: 100)
            .addSnapshotListener { querySnapshot, error in
                print("friends listener activated")
                
                guard let documents = querySnapshot?.documents else {
                    print("Error fetching user's friends: \(error!)")
                    return
                }
                
                // resetting array to reset friends
                self.userFriendsDict.removeAll()
                self.friendsArr.removeAll() // updating to keep the friends in order
                
                // clear the listeners for the the previous set of users
                self.deinitFriendListeners()
                
                for document in querySnapshot!.documents {
                    let userFriend:UserFriends? = try? document.data(as: UserFriends.self)
                    
                    if userFriend == nil {
                        continue
                    }
                    
                    self.userFriendsDict[userFriend!.friendId] = userFriend!
                    
                    // making sure that I clear the inbox if this user came from the inbox...
                    // could be an accepted or rejected user
                    if self.inboxUsersArr.contains(userFriend!.friendId) {
                        self.inboxUsersArr = self.inboxUsersArr.filter {inboxUserId in
                            return inboxUserId != userFriend!.friendId
                        }
                    }
                    
                    // only get friend's info if is an active friend
                    if !userFriend!.isActive {
                        continue
                    }
                    
                    // create a listener for this specific user
                    let currFriendListener = self.db.collection("users").document(userFriend!.friendId)
                        .addSnapshotListener {documentSnapshot, error in
                            guard let document = documentSnapshot else {
                                print("Error fetching friend's data: \(error!)")
                                return
                            }
                            
                            guard let data = document.data() else {
                                print("friend realtime data was empty.")
                                return
                            }
                            
                            let updatedReturnedUser = try? document.data(as: User.self)
                            
                            if updatedReturnedUser != nil {
                                DispatchQueue.main.async {
                                    self.relevantUsersDict[userFriend!.friendId] = updatedReturnedUser!
                                    
                                    // update current friendsArr if the friend is already there
                                    if let indexFriend = self.friendsArr.firstIndex(of: userFriend!.friendId) {
                                        print("friend already in the friends arr, just needed to update the relevantusersdict")
                                    } else { // new friend not in array
                                        self.friendsArr.append(userFriend!.friendId)
                                    }
                                    
                                    self.objectWillChange.send()
                                    
                                    print("added this user to the array of users for user's circle\(updatedReturnedUser?.nickname)")
                                }
                            }
                        }
                    
                    // adding to list of current friend listeners
                    self.friendsListeners.append(currFriendListener)
                }
            }
        
        // listener for people who have me as an active friend might make inbox easier
        // where friendId = me, isActive = true...this way it's a published property that can update the ui
        // as inbox users is only being called on load
        
        let inboxUsersListener = db.collection("user_friends").whereField("friendId", isEqualTo: currUserId).whereField("isActive", isEqualTo: true)
            .addSnapshotListener { querySnapshot, error in
                print("inbox users listener")
                guard let documents = querySnapshot?.documents else {
                    print("Error fetching friends who have me as a friend: \(error!)")
                    return
                }
                
                self.inboxUsersArr.removeAll()
                
                for document in querySnapshot!.documents {
                    let userFriend:UserFriends? = try? document.data(as: UserFriends.self)
                    
                    if userFriend == nil {
                        continue
                    }
                                        
                    // if this user is not already a friend, active or inactive/rejected, then get their user data and add to inbox
                    if self.userFriendsDict.keys.contains(userFriend!.userId) {
                        print("already have this user in my circle or I rejected them \(userFriend?.userId)")
                        continue
                    }
                    else {
                        self.firestoreService.getUser(userId: userFriend!.userId) {[weak self] returnedUser in
                            if returnedUser == nil {
                                print("couldn't get new user info")
                                return
                            }
                            print("new user for inbox fetched \(returnedUser!.nickname)")
                            
                            DispatchQueue.main.async {
                                // add data to our app cache of users for their info down the road
                                self?.relevantUsersDict[userFriend!.userId] = returnedUser!
                                self?.inboxUsersArr.append(userFriend!.userId)
                            }
                        }
                    }
                }
            }
        
        // MARK: listener for messages
        
        // TODO: don't know if we need a listener for sent messages as we can alter our model from local and add to the dictionary
        // senderIdReceiverIdComposite: composite array of strings which contains the senderId and receiverId as elements
        // order: sent time should make it easy to add to dict
        // limit: because each user should have most 12 friends and so would at most need 24 messages to show turns and all that...save myself from hackers here...unless a user gets 100 messages from someone, and that too at least they will be ordered
        
        // SOLUTION: composite with array
        let messagesListener = db.collection("messages").whereField("senderIdReceiverIdComposite", arrayContains: currUserId).order(by: "sentTimestamp", descending: true).limit(to: 100)
            .addSnapshotListener { querySnapshot, error in
                    print("messages listener activated")
                
                    guard let documents = querySnapshot?.documents else {
                        print("error in fetching messages: \(error!)")
                        return
                    }
                    print("going through all messages now that the query found changes")
                
                    // clearing dict to allow clean list of messages to be put forth
                    //optimize this? but also saving on memory and same db reads
                    self.relevantMessagesByUserDict.removeAll()
                
                    self.messagesArr = documents.compactMap { (queryDocumentSnapshot) -> Message? in
                        do {
                            let currMessage = try queryDocumentSnapshot.data(as: Message.self)
                            print("new message received! \(currMessage!.sentTimestamp)")
                            
                            DispatchQueue.main.async {
                                if currMessage != nil { // not really possible but just check
                                    // if the user doesn't exist for the dictionary, then add it
                                    // this means it's most likely someone new (never had user_friend relationship before) messaging for the user's inbox
                                    // TODO: prolly want to make a call to get this sender user details for the inbox, but they should either be in the friendsDict or their are not a friend so won't be there
                                    // also add in any messages where I am the sender
                                    if currMessage!.senderId == currUserId { // if I am the sender
                                        if self.relevantMessagesByUserDict[currMessage!.receiverId] == nil {
                                            self.relevantMessagesByUserDict[currMessage!.receiverId] = [currMessage!]
                                        } else {
                                            self.relevantMessagesByUserDict[currMessage!.receiverId]?.append(currMessage!)
                                        }
                                    }
                                    else  { // if I am receiving
                                        if self.relevantMessagesByUserDict[currMessage!.senderId] == nil {
                                            self.relevantMessagesByUserDict[currMessage!.senderId] = [currMessage!]
                                        } else {
                                            self.relevantMessagesByUserDict[currMessage!.senderId]?.append(currMessage!)
                                        }
                                    }
                                }
                                
                                self.objectWillChange.send()
                            }
                            
                            return currMessage
                        } catch {
                            print(error)
                        }
                        return nil
                    }                
                }
        
        self.listenersActive = true
        
        // adding listeners to be able to deinit later
        // TODO: make sure to have this listener avaible available to deinit as well
        // self.dataListeners.append(currUserListener)
        self.dataListeners.append(messagesListener)
        self.dataListeners.append(friendsListener)
        self.dataListeners.append(inboxUsersListener)
        
    }
    
    func deinitFriendListeners() {
        for listener in self.friendsListeners {
            listener.remove()
        }
    }
    
    func deinitAllDataListeners() {
        // get rid of the main listeners
        for listener in self.dataListeners {
            listener.remove()
        }
        
        // get rid of the inner listeners
        self.deinitFriendListeners()
        
        print("deactivated listeners!")
        
        self.listenersActive = false
    }
}


// manage whether user is online or not
extension AuthSessionStore {
    func updateUserStatus(userStatus: UserStatus) {
        // if user is authenticated
        if self.sessionState == .isAuthenticated {
            // if user already has the status that we want to update to, no need to update
            // remember that the user is updated realtime so we have the newest user data
            if self.user?.userStatus == userStatus {
                // do nothing...already have this status
                print("user already has status: \(userStatus)")
            }
            else {
                if let uid = AuthSessionStore.getCurrentUserId() {
                    self.firestoreService.updateUserStatus(userId: uid, userStatus: userStatus) {res in
                       print(res)
                    }
                }
            }
        }
        
        print("not authenticated can't change user status")
    }
}
