//
//  AutocompleteMentionsController.swift
//  ComposeUI
//
//  Created by Shadowfacts on 3/25/23.
//

import SwiftUI
import Combine
import Pachyderm
import TuskerComponents

class AutocompleteMentionsController: ViewController {
    
    unowned let composeController: ComposeController
    var mastodonController: ComposeMastodonContext { composeController.mastodonController }
    
    private var stateCancellable: AnyCancellable?
    
    @Published private var accounts: [AnyAccount] = []
    private var searchTask: Task<Void, Never>?
    
    init(composeController: ComposeController) {
        self.composeController = composeController
        
        stateCancellable = composeController.$currentInput
            .compactMap { $0 }
            .flatMap { $0.autocompleteStatePublisher }
            .compactMap {
                if case .mention(let s) = $0 {
                    return s
                } else {
                    return nil
                }
            }
            .debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
            .sink { [unowned self] query in
                self.searchTask?.cancel()
                // weak in case the autocomplete controller is dealloc'd racing with the task starting
                self.searchTask = Task { [weak self] in
                    await self?.queryChanged(query)
                }
            }
    }
    
    @MainActor
    private func queryChanged(_ query: String) async {
        guard !query.isEmpty else {
            accounts = []
            return
        }
        
        let localSearchTask = Task {
            // we only want to search locally if the search API call takes more than .25sec or it fails
            try await Task.sleep(nanoseconds: 250 * NSEC_PER_MSEC)
            
            let results = self.mastodonController.searchCachedAccounts(query: query)
            try Task.checkCancellation()
            
            if !results.isEmpty {
                self.loadAccounts(results.map { .init(value: $0) }, query: query)
            }
        }
        
        let accounts = try? await mastodonController.run(Client.searchForAccount(query: query)).0
        guard let accounts,
              !Task.isCancelled else {
            return
        }
        localSearchTask.cancel()
        
       loadAccounts(accounts.map { .init(value: $0) }, query: query)
    }
    
    @MainActor
    private func loadAccounts(_ accounts: [AnyAccount], query: String) {
        guard case .mention(query) = composeController.currentInput?.autocompleteState else {
            return
        }
        
        // when sorting account suggestions, ignore the domain component of the acct unless the user is typing it themself
        let ignoreDomain = !query.contains("@")
        
        self.accounts =
            accounts.map { (account) -> (AnyAccount, (matched: Bool, score: Int)) in
                let fuzzyStr = ignoreDomain ? String(account.value.acct.split(separator: "@").first!) : account.value.acct
                let res = (account, FuzzyMatcher.match(pattern: query, str: fuzzyStr))
                return res
            }
            .filter(\.1.matched)
            .map { (account, res) -> (AnyAccount, Int) in
                // give higher weight to accounts that the user follows or is followed by
                var score = res.score
                if let relationship = mastodonController.cachedRelationship(for: account.value.id) {
                    if relationship.following {
                        score += 3
                    }
                    if relationship.followedBy {
                        score += 2
                    }
                }
                return (account, score)
            }
            .sorted { $0.1 > $1.1 }
            .map(\.0)
    }
    
    private func autocomplete(with account: AnyAccount) {
        guard let input = composeController.currentInput else {
            return
        }
        input.autocomplete(with: "@\(account.value.acct)")
    }
    
    var view: some View {
        AutocompleteMentionsView()
    }
    
    struct AutocompleteMentionsView: View {
        @EnvironmentObject private var controller: AutocompleteMentionsController
        
        var body: some View {
            ScrollView(.horizontal) {
                HStack(spacing: 8) {
                    ForEach(controller.accounts) { account in
                        AutocompleteMentionButton(account: account)
                    }
                    
                    Spacer()
                }
                .padding(.horizontal, 8)
                .animation(.linear(duration: 0.2), value: controller.accounts)
            }
            .onDisappear {
                controller.searchTask?.cancel()
            }
        }
    }
    
    private struct AutocompleteMentionButton: View {
        @EnvironmentObject private var composeController: ComposeController
        @EnvironmentObject private var controller: AutocompleteMentionsController
        let account: AnyAccount
        
        var body: some View {
            Button(action: { controller.autocomplete(with: account) }) {
                HStack(spacing: 4) {
                    AvatarImageView(
                        url: account.value.avatar,
                        size: 30,
                        style: composeController.config.avatarStyle,
                        fetchAvatar: composeController.fetchAvatar
                    )
                    
                    VStack(alignment: .leading) {
                        controller.composeController.displayNameLabel(account.value, .subheadline, 14)
                            .foregroundColor(.primary)
                        
                        Text(verbatim: "@\(account.value.acct)")
                            .font(.caption)
                            .foregroundColor(.primary)
                    }
                }
            }
            .frame(height: 30)
            .padding(.vertical, 8)
        }
    }
}

fileprivate struct AnyAccount: Equatable, Identifiable {
    let value: any AccountProtocol
    
    var id: String { value.id }
    
    static func ==(lhs: AnyAccount, rhs: AnyAccount) -> Bool {
        return lhs.value.id == rhs.value.id
    }
}
