import Foundation
import Combine

/// Per-dialog observation isolation for ChatDetailView.
///
/// Instead of `@ObservedObject messageRepository` (which re-renders on ANY dialog change),
/// this ViewModel subscribes only to the specific dialog's messages via Combine pipeline
/// with `removeDuplicates()`. The view re-renders ONLY when its own dialog's data changes.
@MainActor
final class ChatDetailViewModel: ObservableObject {
    let dialogKey: String

    @Published private(set) var messages: [ChatMessage] = []
    @Published private(set) var isTyping: Bool = false
    @Published private(set) var typingSenderNames: [String] = []
    /// Android parity: true while loading messages from DB. Shows skeleton placeholder.
    @Published private(set) var isLoading: Bool = true
    /// Pagination: true while older messages are available in SQLite.
    @Published private(set) var hasMoreMessages: Bool = true
    /// Pagination: guard against concurrent loads.
    @Published private(set) var isLoadingMore: Bool = false

    private var cancellables = Set<AnyCancellable>()

    init(dialogKey: String) {
        self.dialogKey = dialogKey

        let repo = MessageRepository.shared

        // Seed with current values
        let initial = repo.messages(for: dialogKey)
        messages = initial
        isTyping = repo.isTyping(dialogKey: dialogKey)
        // Android parity: if we already have messages, skip skeleton.
        // Otherwise keep isLoading=true until first Combine emission or timeout.
        isLoading = initial.isEmpty
        // If initial load returned fewer than pageSize, no more to load.
        if initial.count < MessageRepository.pageSize {
            hasMoreMessages = false
        }

        // Subscribe to messagesByDialog changes, filtered to our dialog only.
        // Broken into steps to help the Swift type-checker.
        let key = dialogKey
        // Android parity: debounce(50ms) batches rapid message mutations
        // (delivery status, read marks, sync bursts) into fewer UI updates.
        let messagesPublisher = repo.$messagesByDialog
            .map { (dict: [String: [ChatMessage]]) -> [ChatMessage] in
                dict[key] ?? []
            }
            .debounce(for: .milliseconds(50), scheduler: DispatchQueue.main)
            .removeDuplicates { (lhs: [ChatMessage], rhs: [ChatMessage]) -> Bool in
                guard lhs.count == rhs.count else { return false }
                for i in lhs.indices {
                    if lhs[i].id != rhs[i].id ||
                       lhs[i].deliveryStatus != rhs[i].deliveryStatus ||
                       lhs[i].isRead != rhs[i].isRead {
                        return false
                    }
                }
                return true
            }

        messagesPublisher
            .sink { [weak self] newMessages in
                PerformanceLogger.shared.track("chatDetail.messagesEmit")
                self?.messages = newMessages
                if self?.isLoading == true {
                    self?.isLoading = false
                }
            }
            .store(in: &cancellables)

        // Fast path: immediately update messages when a sent message is inserted,
        // bypassing the 50ms Combine debounce for instant bubble appearance.
        // No .receive(on:) — notification is always posted from @MainActor
        // (SessionManager → upsertFromMessagePacket), so subscriber fires
        // synchronously on main thread. This is critical: the message must be
        // in ViewModel.messages BEFORE sendCurrentMessage() clears replyingToMessage,
        // so SwiftUI batches both changes into one render pass.
        NotificationCenter.default.publisher(for: .sentMessageInserted)
            .compactMap { $0.userInfo?["opponentKey"] as? String }
            .filter { $0 == key }
            .sink { [weak self] _ in
                let fresh = repo.messages(for: key)
                self?.messages = fresh
                if self?.isLoading == true {
                    self?.isLoading = false
                }
            }
            .store(in: &cancellables)

        // Subscribe to typing state changes, filtered to our dialog
        repo.$typingDialogs
            .map { (dialogs: [String: Set<String>]) -> [String] in
                guard let senderKeys = dialogs[key], !senderKeys.isEmpty else { return [] }
                return senderKeys.map { sk in
                    DialogRepository.shared.dialogs[sk]?.opponentTitle
                        ?? String(sk.prefix(8))
                }
            }
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] names in
                self?.isTyping = !names.isEmpty
                self?.typingSenderNames = names
            }
            .store(in: &cancellables)
    }

    /// Pagination: load older messages from SQLite when user scrolls to top.
    func loadMore() async {
        guard !isLoadingMore, hasMoreMessages else { return }
        guard let earliest = messages.first else { return }
        isLoadingMore = true

        let older = MessageRepository.shared.loadOlderMessages(
            for: dialogKey,
            beforeTimestamp: earliest.timestamp,
            beforeMessageId: earliest.id,
            limit: MessageRepository.pageSize
        )

        if older.count < MessageRepository.pageSize {
            hasMoreMessages = false
        }
        // messages will update via Combine pipeline (repo already prepends to cache).
        isLoadingMore = false
    }

    /// Ensures a target message is present in current dialog cache before scroll-to-message.
    /// Returns true when the message is available to the UI list.
    func ensureMessageLoaded(messageId: String) async -> Bool {
        guard !messageId.isEmpty else { return false }
        if messages.contains(where: { $0.id == messageId }) {
            return true
        }

        let repo = MessageRepository.shared
        guard repo.ensureMessageLoaded(for: dialogKey, messageId: messageId) else {
            return false
        }

        // Wait briefly for Combine debounce (50ms) to propagate to this view model.
        for _ in 0..<8 {
            if messages.contains(where: { $0.id == messageId }) {
                return true
            }
            try? await Task.sleep(for: .milliseconds(16))
        }

        // Fallback: force a direct snapshot refresh from repository.
        let refreshed = repo.messages(for: dialogKey)
        if refreshed.contains(where: { $0.id == messageId }) {
            messages = refreshed
            return true
        }

        return false
    }
}
