import Foundation
import Combine
import UIKit

class UserService: ObservableObject {
    private var cancellables = Set<AnyCancellable>()
    private var authRepository: AuthRepository
    private var commentRepository: CommentRepository
    private var notificationRepository: NotificationRepository
    private var preferencesRepository: PreferencesRepository
    private var bookmarksRepository: BookmarksRepository
    
    @Published private (set) var signedInState: SignInState = .unknown
    @Published private (set) var userComments: Comments? = nil
    @Published private (set) var userBookmarks: [Int] = []
    @Published private (set) var token: String? = nil
    @Published private (set) var notificationSettings: UNNotificationSettings? = nil
    @Published private (set) var notificationSubscriptions: [NotificationType] = []
    @Published private (set) var hasSeenOnboardingPublisher: Bool = false
    @Published private (set) var openDeeplink: Deeplink? = nil
    
    init(authRepository: AuthRepository, commentRepository: CommentRepository, notificationRepository: NotificationRepository, preferencesRepository: PreferencesRepository, bookmarksRepository: BookmarksRepository) {
        self.authRepository = authRepository
        self.commentRepository = commentRepository
        self.notificationRepository = notificationRepository
        self.preferencesRepository = preferencesRepository
        self.bookmarksRepository = bookmarksRepository
        
        self.setupObservers()
        self.refreshContent()
    }
    
    private func determineIfBookmarksTipNeeded() {
        if userBookmarks.count == 0 {
            shouldShowBookmarksTip = false
            return
        }
        
        if UserDefaults.standard.hasSeenBookmarkTip {
            shouldShowBookmarksTip = false
            return
        }
        
        shouldShowBookmarksTip = true
    }
    
    private func setupObservers() {
        UserDefaults.standard.publisher(for: \.hasSeenBookmarkTip)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newValue in
                if let sself = self {
                    sself.determineIfBookmarksTipNeeded()
                }
            }
            .store(in: &cancellables)
        
        NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                if let sself = self {
                    Task {
                        await sself.updateNotificationSettings()
                    }
                }
            }
            .store(in: &cancellables)
        
        authRepository.signedInState
            .receive(on: DispatchQueue.main)
            .sink { [weak self] signedInState in
                self?.signedInState = signedInState
                self?.refreshContent()
            }
            .store(in: &cancellables)
        
        notificationRepository.token
            .receive(on: DispatchQueue.main)
            .sink { [weak self] token in
                self?.token = token
            }.store(in: &cancellables)
        
        notificationRepository.notificationSettings
            .receive(on: DispatchQueue.main)
            .sink { [weak self] settings in
                self?.notificationSettings = settings
            }.store(in: &cancellables)
        
        notificationRepository.notificationTypeNames
            .receive(on: DispatchQueue.main)
            .sink { [weak self] notificationTypeNames in
                if let notificationTypeNames = notificationTypeNames {
                    self?.notificationSubscriptions = notificationTypeNames.compactMap { NotificationType(string: $0) }
                }
            }.store(in: &cancellables)
        
        notificationRepository.deeplinkToOpen
            .receive(on: DispatchQueue.main)
            .sink { [weak self] deeplink in
                self?.openDeeplink = deeplink
            }.store(in: &cancellables)
        
        preferencesRepository.hasSeenOnboardingPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] hasSeenOnboarding in
                self?.hasSeenOnboardingPublisher = hasSeenOnboarding
            }.store(in: &cancellables)
        
        self.setupNotificationReplyObserver()
    }
    
    func refreshContent() {
        Task {
            await self.retrieveUserComments()
        }
        Task {
            await self.retrieveUserBookmarks()
            DispatchQueue.main.async { [weak self] in
                self?.determineIfBookmarksTipNeeded()
            }
        }
    }
    
    @MainActor
    func signInWithViewController() {
        self.authRepository.signIn(presentationContextProvider: ShimViewController())
    }
    
    func signOut() {
        self.authRepository.signOut()
    }
    
    func signInWithStoredToken() async {
        await self.authRepository.signInWithStoredToken()
    }
    
    func updateAvatar(_ image: UIImage) async throws {
        guard case .signedIn(let authResponse) = self.signedInState else {
            return
        }
        
        guard let squareImage = image.cropToSquare() else {
            throw(RiotError.dataError)
        }
        
        try await self.authRepository.updateUserAvatar(token: authResponse.token, image: squareImage)
    }
    
    func requestDeletion() async throws -> DeleteUserResponse {
        guard case .signedIn(let authResponse) = self.signedInState else {
            throw(RiotError.authError)
        }
        
        return try await self.authRepository.requestDeletion(token: authResponse.token)
    }
    
    func resendDeletionRequest() async throws -> DeleteUserResponse {
        guard case .signedIn(let authResponse) = self.signedInState else {
            throw(RiotError.authError)
        }
        
        return try await self.authRepository.resendDeletionRequest(token: authResponse.token)
    }
    
    func cancelDeletionRequest() async throws -> DeleteUserResponse {
        guard case .signedIn(let authResponse) = self.signedInState else {
            throw(RiotError.authError)
        }
        
        return try await self.authRepository.cancelDeletionRequest(token: authResponse.token)
    }
    
    @Published private (set) var shouldShowBookmarksTip: Bool = false
    func didSeeBookmarksTip() {
        bookmarksRepository.didSeeBookmarksTip()
    }
    
    @MainActor
    func retrieveUserBookmarks() async {
        do {
            let intVals = try await self.bookmarksRepository.getBookmarks()
            
            userBookmarks = intVals.compactMap { Int($0) }
        } catch {
            // TODO: handle error
            print(error)
        }
    }
    
    func updateBookmarks(postIds: [Int]) async throws {
        
        try await self.bookmarksRepository.updateBookmarks(postIds: postIds.compactMap { "\($0)"})
        
        await MainActor.run { [weak self] in
            self?.userBookmarks = postIds
            self?.determineIfBookmarksTipNeeded()
        }
    }
    
    func updateNotificationSettings() async {
        await self.notificationRepository.updateNotificationSettings()
    }
    
    func requestNotificationPermission() async {
        do {
            try await self.notificationRepository.requestNotificationPermission()
        } catch {
            // TODO: handle error
            print(error)
        }
    }
    
    func subscribeToNotificationTypes(_ notificationTypes: [NotificationType]) {
        notificationRepository.subscribeToNotificationTypes(notificationTypes)
    }
    
    func unsubscribeFromNotificationTypes(_ notificationTypes: [NotificationType]) {
        notificationRepository.unsubscribeFromNotificationTypes(notificationTypes)
    }
    
    // Observe when new notificationReply values are emitted by the
    // notification repository.
    //
    // The app might be in various states when this happens:
    // - recently launched from scratch (launched in response to the user
    //   interacting with the notification),
    // - resumed from having been running recently,
    // - resumed from having been run a while ago.
    //
    // Irrespective of how the app was launched, we will need a fresh auth
    // token before we can post the comment to the API.
    //
    // So when we observe a new notificationReply we will also need to obtain
    // a fresh token (unless we already have one).
    func setupNotificationReplyObserver() {
        
        print("Setting up notififcation reply observer")
        
        notificationRepository.notificationReply
            .receive(on: DispatchQueue.main)
            .sink { reply in
                
                print("Observed reply: \(reply.content)")
                _ = self.authRepository.signedInState.sink { signInState in
                    if case let SignInState.signedIn(response) = signInState {
                        
                        print("Signed in")
                        Task {
                            do {
                                let result = try await self.commentRepository.postReplyToComment(token: response.token, reply: reply)
                                print(result)
                            } catch {
                                print("error")
                            }
                        }
                    }
                }
                Task {
                    await self.authRepository.signInWithStoredToken()
                }
            }.store(in: &cancellables)
    }

    @MainActor
    func retrieveUserComments() async {
        guard case .signedIn(let authResponse) = self.signedInState else {
            return
        }
        do {
            userComments = try await self.commentRepository.fetchUserComments(token: authResponse.token)
        } catch {
            // TODO: handle error
            print(error)
        }
    }
    
    func addCommentToPost(_ comment: String, postId: Int, inReplyToComment commentId: Int? = nil) async throws -> CommentResponse {
        guard case .signedIn(let authResponse) = self.signedInState else {
            throw(RiotError.authError)
        }
        
        let reply = Reply(content: comment.trimmingCharacters(in: .whitespacesAndNewlines), post: postId, parent: commentId)
        
        // TODO: More thoroughly sanitize comment?
        if reply.content.isEmpty {
            throw(RiotError.dataError)
        }
        
        return try await self.commentRepository.postReplyToComment(token: authResponse.token, reply: reply)
    }
    
    func postFeedback(_ feedback: Feedback) async throws {
        try await commentRepository.postFeedback(feedback: feedback)
    }
    
    var ignoreExternalWebAssets: Bool {
        get {
            preferencesRepository.ignoreExternalWebAssets
        }
        set {
            preferencesRepository.ignoreExternalWebAssets = newValue
        }
    }
    
    var ignoreAppCSS: Bool {
        get {
            preferencesRepository.ignoreAppCSS
        }
        set {
            preferencesRepository.ignoreAppCSS = newValue
        }
    }
    
    var hasSeenOnboarding: Bool {
        get {
            preferencesRepository.hasSeenOnboarding
        }
        set {
            preferencesRepository.hasSeenOnboarding = newValue
        }
    }
    
    var userPostcode: Int {
        get {
            preferencesRepository.userPostcode > 0 ? preferencesRepository.userPostcode : 2600
        }
        set {
            preferencesRepository.userPostcode = newValue
            notificationRepository.savePostcode("\(newValue)")
        }
    }
}
