//
//  ProfileViewModel.swift
//  Mastodon
//
//  Created by MainasuK Cirno on 2021-3-29.
//

import os.log
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonMeta
import MastodonAsset
import MastodonLocalization
import MastodonUI

// please override this base class
class ProfileViewModel: NSObject {
        
    let logger = Logger(subsystem: "ProfileViewModel", category: "ViewModel")
    
    typealias UserID = String
    
    var disposeBag = Set<AnyCancellable>()
    var observations = Set<NSKeyValueObservation>()
    private var mastodonUserObserver: AnyCancellable?
    private var currentMastodonUserObserver: AnyCancellable?
    
    let postsUserTimelineViewModel: UserTimelineViewModel
    let repliesUserTimelineViewModel: UserTimelineViewModel
    let mediaUserTimelineViewModel: UserTimelineViewModel
    let profileAboutViewModel: ProfileAboutViewModel
    
    // input
    let context: AppContext
    @Published var me: MastodonUser?
    @Published var user: MastodonUser?
    
    let viewDidAppear = PassthroughSubject<Void, Never>()
    
    @Published var isEditing = false
    @Published var isUpdating = false
    @Published var accountForEdit: Mastodon.Entity.Account?
        
    // output
    let relationshipViewModel = RelationshipViewModel()
    
    @Published var userIdentifier: UserIdentifier? = nil
    
    @Published var isRelationshipActionButtonHidden: Bool = true
    @Published var isReplyBarButtonItemHidden: Bool = true
    @Published var isMoreMenuBarButtonItemHidden: Bool = true
    @Published var isMeBarButtonItemsHidden: Bool = true
    @Published var isPagingEnabled = true

    // @Published var protected: Bool? = nil
    // let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
    
    init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
        self.context = context
        self.user = mastodonUser
        self.postsUserTimelineViewModel = UserTimelineViewModel(
            context: context,
            title: L10n.Scene.Profile.SegmentedControl.posts,
            queryFilter: .init(excludeReplies: true)
        )
        self.repliesUserTimelineViewModel = UserTimelineViewModel(
            context: context,
            title: L10n.Scene.Profile.SegmentedControl.postsAndReplies,
            queryFilter: .init(excludeReplies: false)
        )
        self.mediaUserTimelineViewModel = UserTimelineViewModel(
            context: context,
            title: L10n.Scene.Profile.SegmentedControl.media,
            queryFilter: .init(onlyMedia: true)
        )
        self.profileAboutViewModel = ProfileAboutViewModel(context: context)
        super.init()
        
        // bind me
        context.authenticationService.activeMastodonAuthenticationBox
            .receive(on: DispatchQueue.main)
            .sink { [weak self] authenticationBox in
                guard let self = self else { return }
                self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
            }
            .store(in: &disposeBag)
        $me
            .assign(to: \.me, on: relationshipViewModel)
            .store(in: &disposeBag)

        // bind user
        $user
            .map { user -> UserIdentifier? in
                guard let user = user else { return nil }
                return MastodonUserIdentifier(domain: user.domain, userID: user.id)
            }
            .assign(to: &$userIdentifier)
        $user
            .assign(to: \.user, on: relationshipViewModel)
            .store(in: &disposeBag)
        
        // bind userIdentifier
        $userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
        $userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
        $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
        
        // bind bar button items
        relationshipViewModel.$optionSet
            .sink { [weak self] optionSet in
                guard let self = self else { return }
                guard let optionSet = optionSet, !optionSet.contains(.none) else {
                    self.isReplyBarButtonItemHidden = true
                    self.isMoreMenuBarButtonItemHidden = true
                    self.isMeBarButtonItemsHidden = true
                    return
                }
                
                let isMyself = optionSet.contains(.isMyself)
                self.isReplyBarButtonItemHidden = isMyself
                self.isMoreMenuBarButtonItemHidden = isMyself
                self.isMeBarButtonItemsHidden = !isMyself
            }
            .store(in: &disposeBag)

        // query relationship
        let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
            user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
        }
        let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)

        // observe friendship
        Publishers.CombineLatest3(
            userRecord,
            context.authenticationService.activeMastodonAuthenticationBox,
            pendingRetryPublisher
        )
        .sink { [weak self] userRecord, authenticationBox, _ in
            guard let self = self else { return }
            guard let userRecord = userRecord,
                  let authenticationBox = authenticationBox
            else { return }
            Task {
                do {
                    let response = try await self.updateRelationship(
                        record: userRecord,
                        authenticationBox: authenticationBox
                    )
                    // there are seconds delay after request follow before requested -> following. Query again when needs
                    guard let relationship = response.value.first else { return }
                    if relationship.requested == true {
                        let delay = pendingRetryPublisher.value
                        DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
                            guard let _ = self else { return }
                            pendingRetryPublisher.value = min(2 * delay, 60)
                            os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
                        }
                    }
                } catch {
                    self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
                }
            }   // end Task
        }
        .store(in: &disposeBag)

        let isBlockingOrBlocked = Publishers.CombineLatest(
            relationshipViewModel.$isBlocking,
            relationshipViewModel.$isBlockingBy
        )
        .map { $0 || $1 }
        .share()
        
        Publishers.CombineLatest(
            isBlockingOrBlocked,
            $isEditing
        )
        .map { !$0 && !$1 }
        .assign(to: &$isPagingEnabled)
    }
    
}

extension ProfileViewModel {

    // fetch profile info before edit
    func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
        guard let me = me,
              let mastodonAuthentication = me.mastodonAuthentication
        else {
            return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
        }

        let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
        return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization)
    }
    
    private func updateRelationship(
        record: ManagedObjectRecord<MastodonUser>,
        authenticationBox: MastodonAuthenticationBox
    ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
        logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship...")
        let response = try await context.apiService.relationship(
            records: [record],
            authenticationBox: authenticationBox
        )
        logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] did update MastodonUser relationship")
        return response
    }

}

extension ProfileViewModel {
    func updateProfileInfo(
        headerProfileInfo: ProfileHeaderViewModel.ProfileInfo,
        aboutProfileInfo: ProfileAboutViewModel.ProfileInfo
    ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Account> {
        guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
            throw APIService.APIError.implicit(.badRequest)
        }
        
        let domain = authenticationBox.domain
        let authorization = authenticationBox.userAuthorization
        
        let _image: UIImage? = {
            guard let image = headerProfileInfo.avatar else { return nil }
            guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
                return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
            }
            return image
        }()
        
        let fieldsAttributes = aboutProfileInfo.fields.map { field in
            Mastodon.Entity.Field(name: field.name.value, value: field.value.value)
        }
        
        let query = Mastodon.API.Account.UpdateCredentialQuery(
            discoverable: nil,
            bot: nil,
            displayName: headerProfileInfo.name,
            note: headerProfileInfo.note,
            avatar: _image.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
            header: nil,
            locked: nil,
            source: nil,
            fieldsAttributes: fieldsAttributes
        )
        return try await context.apiService.accountUpdateCredentials(
            domain: domain,
            query: query,
            authorization: authorization
        )
    }
}
