Gitfed
bastien-mrq/bt-message / btmessage / ViewModels / AppState.swift
// AppState.swift

import Foundation
import Combine
import CryptoKit

@MainActor
public class AppState: ObservableObject {
    // MARK: - Published

    @Published public var peers: [PeerInfo] = []
    @Published public var groups: [GroupModel] = []
    @Published public var conversations: [String: [ChatMessage]] = [:]  // peerID ou groupID → messages
    @Published public var selectedConversationID: String? = nil
    @Published public var errorMessage: String? = nil

    // MARK: - Dependencies

    public let mesh: MeshManager
    private let identity = IdentityManager.shared
    private var cancellables = Set<AnyCancellable>()

    // MARK: - Init

    public init() {
        let displayName = IdentityManager.shared.localPeerID
        self.mesh = MeshManager(displayName: displayName)

        // Charger les groupes persistés
        self.groups = GroupKeyManager.shared.allGroups

        setupBindings()
        mesh.start()
    }

    // MARK: - DM

    public func sendMessage(text: String, to peerID: String) {
        appendOutgoing(text: text, conversationID: peerID, senderID: identity.localPeerID)
        do {
            let messageID = try mesh.sendMessage(text: text, to: peerID)
            // Mettre à jour le message avec l'ID pour tracking ACK
            if let lastIndex = conversations[peerID]?.indices.last {
                conversations[peerID]?[lastIndex].id = messageID
            }
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    // MARK: - Groupe

    public func createGroup(name: String, memberIDs: [String]) {
        let groupID = UUID().uuidString
        let groupKey = SymmetricKey(size: .bits256)
        let groupKeyData = groupKey.withUnsafeBytes { Data($0) }
        let allMembers = Array(Set(memberIDs + [identity.localPeerID]))
        let group = GroupModel(id: groupID, name: name, memberIDs: allMembers, creatorID: identity.localPeerID)

        GroupKeyManager.shared.store(group: group, key: groupKey)
        groups.append(group)

        // Distribuer la clé chiffrée à chaque membre (sauf soi-même)
        for memberID in memberIDs where memberID != identity.localPeerID {
            do {
                try mesh.sendGroupKeyDistrib(group: group, groupKeyData: groupKeyData, to: memberID)
            } catch {
                errorMessage = "Erreur distribution clé à \(memberID.prefix(8)): \(error.localizedDescription)"
            }
        }
    }

    public func sendGroupMessage(text: String, to groupID: String) {
        appendOutgoing(text: text, conversationID: groupID, senderID: identity.localPeerID)
        do {
            try mesh.sendGroupMessage(text: text, to: groupID)
        } catch {
            errorMessage = error.localizedDescription
        }
    }

    /// Point d'entrée unifié — route vers DM ou groupe automatiquement
    public func send(text: String, to conversationID: String) {
        if isGroup(conversationID) {
            sendGroupMessage(text: text, to: conversationID)
        } else {
            sendMessage(text: text, to: conversationID)
        }
    }

    public func messages(for conversationID: String) -> [ChatMessage] {
        conversations[conversationID] ?? []
    }

    public func isGroup(_ id: String) -> Bool {
        groups.contains(where: { $0.id == id })
    }

    // MARK: - Private

    private func appendOutgoing(text: String, conversationID: String, senderID: String) {
        let msg = ChatMessage(
            id: UUID().uuidString,
            conversationID: conversationID,
            senderID: senderID,
            text: text,
            timestamp: Date(),
            delivered: false
        )
        conversations[conversationID, default: []].append(msg)
    }

    private func setupBindings() {
        mesh.$connectedPeers
            .sink { [weak self] mcPeers in
                Task { @MainActor [weak self] in
                    guard let self else { return }
                    for mcPeer in mcPeers {
                        if !self.peers.contains(where: { $0.id == mcPeer.displayName }) {
                            self.peers.append(PeerInfo(
                                id: mcPeer.displayName,
                                displayName: mcPeer.displayName,
                                publicKey: nil,
                                lastSeen: Date()
                            ))
                        }
                    }
                }
            }
            .store(in: &cancellables)

        mesh.$knownPublicKeys
            .sink { [weak self] keyMap in
                Task { @MainActor [weak self] in
                    guard let self else { return }
                    for (peerID, pubKey) in keyMap {
                        if let idx = self.peers.firstIndex(where: { $0.id == peerID }) {
                            self.peers[idx].publicKey = pubKey
                            self.peers[idx].lastSeen  = Date()
                        } else {
                            self.peers.append(PeerInfo(
                                id: peerID,
                                displayName: peerID,
                                publicKey: pubKey,
                                lastSeen: Date()
                            ))
                        }
                    }
                }
            }
            .store(in: &cancellables)

        mesh.$incomingMessages
            .sink { [weak self] received in
                Task { @MainActor [weak self] in
                    guard let self, let last = received.last else { return }
                    let msg = ChatMessage(
                        id: last.messageID,
                        conversationID: last.conversationID,
                        senderID: last.senderID,
                        text: last.text,
                        timestamp: last.timestamp,
                        delivered: true
                    )
                    self.conversations[last.conversationID, default: []].append(msg)
                }
            }
            .store(in: &cancellables)

        mesh.onGroupKeyReceived = { [weak self] group in
            Task { @MainActor [weak self] in
                guard let self else { return }
                if !self.groups.contains(where: { $0.id == group.id }) {
                    self.groups.append(group)
                }
            }
        }

        mesh.onMessageDelivered = { [weak self] messageID in
            Task { @MainActor [weak self] in
                self?.markMessageAsDelivered(messageID: messageID)
            }
        }
    }

    private func markMessageAsDelivered(messageID: String) {
        for (conversationID, var messages) in conversations {
            if let index = messages.firstIndex(where: { $0.id == messageID }) {
                messages[index].delivered = true
                conversations[conversationID] = messages
                break
            }
        }
    }
}