import BitFoundation
import Foundation
import Combine
import CoreBluetooth

/// Abstract transport interface used by ChatViewModel and services.
/// BLEService implements this protocol; a future Nostr transport can too.
struct TransportPeerSnapshot: Equatable, Hashable {
    let peerID: PeerID
    let nickname: String
    let isConnected: Bool
    let noisePublicKey: Data?
    let lastSeen: Date
    /// Whether the peer's announce was signature-verified (courier tier gate).
    let isVerified: Bool

    init(
        peerID: PeerID,
        nickname: String,
        isConnected: Bool,
        noisePublicKey: Data?,
        lastSeen: Date,
        isVerified: Bool = false
    ) {
        self.peerID = peerID
        self.nickname = nickname
        self.isConnected = isConnected
        self.noisePublicKey = noisePublicKey
        self.lastSeen = lastSeen
        self.isVerified = isVerified
    }
}

/// Outcome of a `/ping` probe over the mesh.
struct MeshPingResult: Equatable {
    /// Round-trip time in milliseconds.
    let rttMs: Int
    /// Total hops to the peer (1 = directly connected), derived from the
    /// pong's TTL decrements; nil when the reply carried inconsistent TTLs.
    let hops: Int?
}

/// Undirected mesh link between two peers, normalized so `(a, b)` and
/// `(b, a)` collapse to one edge.
struct MeshTopologyEdge: Hashable {
    let a: PeerID
    let b: PeerID

    init(_ first: PeerID, _ second: PeerID) {
        if first < second {
            a = first
            b = second
        } else {
            a = second
            b = first
        }
    }
}

/// Point-in-time view of the mesh graph learned from gossiped announces
/// (each announce carries up to 10 `directNeighbors`).
struct MeshTopologySnapshot: Equatable {
    let localPeerID: PeerID
    let nodes: [PeerID]
    let edges: [MeshTopologyEdge]
}

enum TransportEvent: @unchecked Sendable {
    case messageReceived(BitchatMessage)
    case publicMessageReceived(peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?)
    case noisePayloadReceived(peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date)
    /// Encrypted group broadcast (MessageType 0x25). Opaque here — the group
    /// coordinator decrypts and authenticates against the roster.
    case groupMessageReceived(payload: Data, timestamp: Date)
    /// Public live-voice burst packet (MessageType 0x29), already
    /// signature-verified against the claimed sender.
    case publicVoiceFrameReceived(peerID: PeerID, nickname: String, payload: Data, timestamp: Date)
    case peerConnected(PeerID)
    case peerDisconnected(PeerID)
    case peerListUpdated([PeerID])
    case peerSnapshotsUpdated([TransportPeerSnapshot])
    case messageDeliveryStatusUpdated(messageID: String, status: DeliveryStatus)
    case bluetoothStateUpdated(CBManagerState)
}

/// Downgrade-safe decision for a private-media recipient. Callers ask before
/// prompting, and BLEService checks again when it consumes any one-shot
/// legacy consent.
enum PrivateMediaSendPolicy: Equatable {
    case encrypted
    /// A public announce hinted at encrypted media (or a prior authenticated
    /// pin exists), but this exact Noise session has not yet supplied its
    /// authenticated peer-state proof. Callers wait boundedly; they must not
    /// pre-queue encrypted bytes or silently select the legacy path.
    case awaitingCapabilityProof
    case legacyRequiresConsent
    case blockedDowngrade
}

/// Receiver-only persistence surface for explicit private-media deletion.
/// Kept separate from `Transport` so sender retry branches can rebase without
/// inheriting or implementing receiver storage concerns.
protocol PrivateMediaDeletionPersisting: AnyObject {
    @MainActor
    func persistDeletedPrivateMedia(
        messageIDs: [String],
        payloadRelativePaths: [String: String],
        protectedPayloadRelativePaths: Set<String>,
        completion: @escaping @MainActor (Bool) -> Void
    )

    /// Gated unlink for a LEGACY (non-stable-ID) incoming payload whose
    /// bubble was explicitly removed. Implementations delete the file only
    /// when its path is not pending delivery and not reserved by any receipt
    /// or in-flight deletion transaction; otherwise the file stays for
    /// bounded quota cleanup.
    @MainActor
    func removeLegacyPrivateMediaPayload(relativePath: String)
}

protocol TransportEventDelegate: AnyObject {
    @MainActor func didReceiveTransportEvent(_ event: TransportEvent)
}

/// Optional typed-event contract for sinks that can synchronously decide
/// whether an inbound message was accepted.
protocol SynchronousMessageTransportEventDelegate: TransportEventDelegate {
    @MainActor
    func didReceiveTransportMessageSynchronously(_ message: BitchatMessage) -> Bool
}

protocol Transport: AnyObject {
    // Event sink
    var delegate: BitchatDelegate? { get set }
    // Typed event sink for transport-domain events. Prefer this over BitchatDelegate for new code.
    var eventDelegate: TransportEventDelegate? { get set }
    // Peer events (preferred over publishers for UI)
    var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
    
    // Peer snapshots (for non-UI services)
    func currentPeerSnapshots() -> [TransportPeerSnapshot]

    // Identity
    var myPeerID: PeerID { get }
    var myNickname: String { get }
    func setNickname(_ nickname: String)

    // Lifecycle
    func startServices()
    func stopServices()
    func emergencyDisconnectAll()

    // Connectivity and peers
    func isPeerConnected(_ peerID: PeerID) -> Bool
    func isPeerReachable(_ peerID: PeerID) -> Bool
    /// Whether a send to this peer is likely to leave the device promptly.
    /// Distinct from reachability: Nostr claims any favorite with a known
    /// npub as reachable even with no relay connection, where a send only
    /// joins a queue waiting for internet that may never come.
    func canDeliverPromptly(to peerID: PeerID) -> Bool
    /// Whether a send to this peer can complete an end-to-end encrypted
    /// delivery right now (e.g. an established Noise session). Distinct from
    /// connectivity: a "connected" link binding alone is forgeable — link
    /// bindings heal on signature-verified "direct" announces, but directness
    /// rides on the unsigned TTL, so a replayed announce can wear an absent
    /// peer's ID on the replayer's link. Routers must not trust a connected
    /// link outright without this.
    func canDeliverSecurely(to peerID: PeerID) -> Bool
    func peerNickname(peerID: PeerID) -> String?
    func getPeerNicknames() -> [PeerID: String]

    // Protocol utilities
    func getFingerprint(for peerID: PeerID) -> String?
    func getNoiseSessionState(for peerID: PeerID) -> LazyHandshakeState
    func triggerHandshake(with peerID: PeerID)

    // Noise identity/session access. Narrow, purpose-named wrappers so the
    // underlying NoiseEncryptionService (and its peer-binding/session
    // orchestration) is never exposed outside the transport.
    /// The remote static public key of the Noise session with `peerID`, if established.
    func noiseSessionPublicKeyData(for peerID: PeerID) -> Data?
    /// Fingerprint of our own Noise static identity key.
    func noiseIdentityFingerprint() -> String
    /// Our Noise static public key (Curve25519 key agreement).
    func noiseStaticPublicKeyData() -> Data
    /// Our Noise signing public key (Ed25519).
    func noiseSigningPublicKeyData() -> Data
    /// Signs `data` with our Noise signing key.
    func noiseSignData(_ data: Data) -> Data?
    /// Verifies an Ed25519 `signature` over `data` against `publicKey`.
    func noiseVerifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool
    /// Registers session-lifecycle callbacks (peer authenticated / handshake required).
    func installNoiseSessionCallbacks(
        onPeerAuthenticated: @escaping (PeerID, String) -> Void,
        onHandshakeRequired: @escaping (PeerID) -> Void
    )

    // Messaging
    func sendMessage(_ content: String, mentions: [String])
    func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date)
    func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
    func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
    func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
    func sendBroadcastAnnounce()
    func sendDeliveryAck(for messageID: String, to peerID: PeerID)
    /// Capabilities the peer advertised in its last verified announce;
    /// empty for peers that predate the capabilities TLV.
    func peerCapabilities(_ peerID: PeerID) -> PeerCapabilities
    /// Appends a peer-authenticated observer. Unlike
    /// `installNoiseSessionCallbacks` this never touches the (single-slot)
    /// handshake-required callback, so secondary features can observe
    /// session establishment without disturbing the primary registration.
    func addPeerAuthenticatedObserver(_ handler: @escaping (PeerID, String) -> Void)
}

/// A carried public mesh message from the store-and-forward window, decoded
/// for display. `packetIdHex` is stable across launches so echo rows keep a
/// deterministic message ID.
struct ArchivedPublicMessage {
    let packetIdHex: String
    let senderPeerID: PeerID
    let senderNickname: String
    let content: String
    let timestamp: Date
}

extension BitchatMessage {
    /// Echo rows are minted locally with this prefix (packet-id derived, so
    /// stable across launches); the timeline dims them.
    static let archivedEchoIDPrefix = "echo-"

    var isArchivedEcho: Bool {
        id.hasPrefix(Self.archivedEchoIDPrefix)
    }
}

extension Transport {
    // Reachability implies prompt delivery for transports that hand packets
    // straight to the radio; queue-backed transports override this.
    func canDeliverPromptly(to peerID: PeerID) -> Bool { isPeerReachable(peerID) }

    // Transports without a forgeable link-binding layer (everything but the
    // BLE mesh) have no stronger delivery signal than prompt delivery.
    func canDeliverSecurely(to peerID: PeerID) -> Bool { canDeliverPromptly(to: peerID) }

    // Noise identity hooks default to inert for transports that do not carry
    // Noise sessions (e.g. NostrTransport).
    func noiseSessionPublicKeyData(for peerID: PeerID) -> Data? { nil }
    func noiseIdentityFingerprint() -> String { "" }
    func noiseStaticPublicKeyData() -> Data { Data() }
    func noiseSigningPublicKeyData() -> Data { Data() }
    func noiseSignData(_ data: Data) -> Data? { nil }
    func noiseVerifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool { false }
    func installNoiseSessionCallbacks(
        onPeerAuthenticated: @escaping (PeerID, String) -> Void,
        onHandshakeRequired: @escaping (PeerID) -> Void
    ) {}

    func peerCapabilities(_ peerID: PeerID) -> PeerCapabilities { [] }
    func addPeerAuthenticatedObserver(_ handler: @escaping (PeerID, String) -> Void) {}

    func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
        sendMessage(content, mentions: mentions)
    }
}

protocol TransportPeerEventsDelegate: AnyObject {
    @MainActor func didUpdatePeerSnapshots(_: [TransportPeerSnapshot])
}

extension BitchatDelegate {
    @MainActor
    func receiveTransportEvent(_ event: TransportEvent) {
        switch event {
        case .messageReceived(let message):
            didReceiveMessage(message)
        case let .publicMessageReceived(peerID, nickname, content, timestamp, messageID):
            didReceivePublicMessage(
                from: peerID,
                nickname: nickname,
                content: content,
                timestamp: timestamp,
                messageID: messageID
            )
        case let .noisePayloadReceived(peerID, type, payload, timestamp):
            didReceiveNoisePayload(from: peerID, type: type, payload: payload, timestamp: timestamp)
        case let .groupMessageReceived(payload, timestamp):
            didReceiveGroupMessage(payload: payload, timestamp: timestamp)
        case let .publicVoiceFrameReceived(peerID, nickname, payload, timestamp):
            didReceivePublicVoiceFrame(from: peerID, nickname: nickname, payload: payload, timestamp: timestamp)
        case .peerConnected(let peerID):
            didConnectToPeer(peerID)
        case .peerDisconnected(let peerID):
            didDisconnectFromPeer(peerID)
        case .peerListUpdated(let peers):
            didUpdatePeerList(peers)
        case .peerSnapshotsUpdated:
            break
        case let .messageDeliveryStatusUpdated(messageID, status):
            didUpdateMessageDeliveryStatus(messageID, status: status)
        case .bluetoothStateUpdated(let state):
            didUpdateBluetoothState(state)
        }
    }
}

extension BLEService: Transport {}
extension BLEService: MeshFileTransferring {}
extension BLEService: MeshVoiceStreaming {}
extension BLEService: MeshCourierTransporting {}
extension BLEService: MeshGroupMessaging {}
extension BLEService: MeshBoardBroadcasting {}
extension BLEService: MeshDiagnosing {}
extension BLEService: MeshVerifying {}
extension BLEService: MeshPublicArchiving {}
extension BLEService: BluetoothStateReporting {}
extension BLEService: PanicResettingTransport {}
extension BLEService: MeshBridgingTransport {}
