import BitFoundation
import BitLogger
import Foundation

/// Narrow environment for `BLEAnnounceHandler`.
///
/// All queue hops (collections barrier, BLE-queue link-state reads, main-actor
/// UI notification, delayed re-announce) live inside the closures supplied by
/// `BLEService`, keeping the handler queue-agnostic and synchronously testable.
struct BLEAnnounceHandlerEnvironment {
    /// Local peer identity at the time the announce is handled.
    let localPeerID: () -> PeerID
    /// TTL value used for direct (non-relayed) packets.
    let messageTTL: UInt8
    /// Current time source.
    let now: () -> Date
    /// Noise and signing public keys already recorded for the peer, if any
    /// (single registry read so both come from one consistent snapshot).
    let existingPeerKeys: (PeerID) -> (noisePublicKey: Data?, signingPublicKey: Data?)
    /// Signing key from the persisted cryptographic identity for the peer, if
    /// any. Registry pins do not survive app restarts or offline-peer
    /// eviction; this fallback keeps the TOFU signing-key pin effective for
    /// returning peers.
    let persistedSigningPublicKey: (PeerID) -> Data?
    /// Ed25519 key previously bound to this Noise identity by an authenticated
    /// peer-state payload, if any (persistent identity-state read).
    let authenticatedSigningPublicKey: (_ noisePublicKey: Data) -> Data?
    /// Verifies the packet signature against the announced signing key.
    let verifySignature: (_ packet: BitchatPacket, _ signingPublicKey: Data) -> Bool
    /// Direct link state for the peer (BLE-queue read).
    let linkState: (PeerID) -> (hasPeripheral: Bool, hasCentral: Bool)
    /// Whether the link this packet arrived on is already bound to a
    /// different peer ID (ingress-registry + BLE-queue read). Directness
    /// rides on the unsigned TTL, so a replayed announce can look "direct"
    /// on the replayer's link; that link must not shortcut an absent peer
    /// into "connected".
    let linkBoundToOtherPeer: (_ packet: BitchatPacket, _ peerID: PeerID) -> Bool
    /// Runs the registry mutation phase under the collections barrier.
    let withRegistryBarrier: (() -> Void) -> Void
    /// Upserts the verified announce into the peer registry.
    /// Returns `nil` when the registry refuses the announce because it carries
    /// a signing key different from the one already pinned for this peer.
    /// Must only be called from inside `withRegistryBarrier`.
    let upsertVerifiedAnnounce: (
        _ peerID: PeerID,
        _ announcement: AnnouncementPacket,
        _ isConnected: Bool,
        _ now: Date
    ) -> BLEPeerAnnounceUpdate?
    /// Debounced reconnect-log decision.
    /// Must only be called from inside `withRegistryBarrier`.
    let shouldEmitReconnectLog: (_ peerID: PeerID, _ now: Date) -> Bool
    /// Records verified direct-neighbor claims in the mesh topology.
    let updateTopology: (_ peerID: PeerID, _ neighbors: [Data]) -> Void
    /// Persists the announced cryptographic identity for offline verification.
    let persistIdentity: (AnnouncementPacket) -> Void
    /// Announce-back dedup check.
    let dedupContains: (String) -> Bool
    /// Announce-back dedup marking.
    let dedupMarkProcessed: (String) -> Void
    /// Delivers the announce UI events as one ordered main-actor hop:
    /// `.peerConnected` (if flagged) → initial gossip sync scheduling (if
    /// flagged) → peer-ID snapshot + data publish + `.peerListUpdated`.
    /// A single closure keeps the original in-order delivery guarantee that
    /// separate unstructured tasks would not provide.
    let deliverAnnounceUIEvents: (
        _ peerID: PeerID,
        _ notifyPeerConnected: Bool,
        _ scheduleInitialSync: Bool
    ) -> Void
    /// Tracks the announce packet for gossip sync.
    let trackPacketSeen: (BitchatPacket) -> Void
    /// Reciprocates the announce for bidirectional discovery.
    let sendAnnounceBack: () -> Void
    /// Schedules a delayed re-announce (afterglow) after the given delay.
    let scheduleAfterglow: (TimeInterval) -> Void
}

/// Outcome of an accepted announce, surfaced so the service can run
/// follow-up work (e.g. courier handover) that keys off the announce.
struct BLEAnnounceHandlingResult {
    let peerID: PeerID
    let announcement: AnnouncementPacket
    let isDirectAnnounce: Bool
    let isVerified: Bool
}

/// Orchestrates inbound announce packets: preflight validation, signature
/// trust, registry/topology updates, identity persistence, UI notification,
/// gossip tracking, and the reciprocal announce response.
final class BLEAnnounceHandler {
    private let environment: BLEAnnounceHandlerEnvironment

    init(environment: BLEAnnounceHandlerEnvironment) {
        self.environment = environment
    }

    @discardableResult
    func handle(_ packet: BitchatPacket, from peerID: PeerID) -> BLEAnnounceHandlingResult? {
        let env = environment
        let now = env.now()
        let preflight = BLEAnnouncePreflightPolicy.evaluate(
            packet: packet,
            from: peerID,
            localPeerID: env.localPeerID(),
            now: now
        )

        let announcement: AnnouncementPacket
        switch preflight {
        case .accept(let acceptance):
            announcement = acceptance.announcement
        case .reject(.malformed):
            SecureLogger.error("❌ Failed to decode announce packet from \(peerID.id.prefix(8))…", category: .session)
            return nil
        case .reject(.senderMismatch(let derivedFromKey)):
            SecureLogger.warning("⚠️ Announce sender mismatch: derived \(derivedFromKey.id.prefix(8))… vs packet \(peerID.id.prefix(8))…", category: .security)
            return nil
        case .reject(.selfAnnounce):
            return nil
        case .reject(.stale(let ageSeconds)):
            SecureLogger.debug("⏰ Ignoring stale announce from \(peerID.id.prefix(8))… (age: \(ageSeconds)s)", category: .session)
            return nil
        }

        // Suppress announce logs to reduce noise

        // Precompute signature verification outside barrier to reduce contention
        var existingPeerKeys = env.existingPeerKeys(peerID)
        if existingPeerKeys.signingPublicKey == nil {
            // The registry entry (and its signing-key pin) is dropped on app
            // restart and offline-peer eviction, but the persisted
            // cryptographic identity survives both. Fall back to it so a
            // returning peer is not treated as first contact — otherwise an
            // attacker could replay the peer's noiseKey/peerID with their own
            // signing key and re-pin the identity (TOFU downgrade).
            existingPeerKeys.signingPublicKey = env.persistedSigningPublicKey(peerID)
        }
        let hasSignature = packet.signature != nil
        let signatureValid: Bool
        if hasSignature {
            signatureValid = env.verifySignature(packet, announcement.signingPublicKey)
            if !signatureValid {
                SecureLogger.warning("⚠️ Signature verification for announce failed \(peerID.id.prefix(8))", category: .security)
            }
        } else {
            signatureValid = false
        }
        let trustDecision = BLEAnnounceTrustPolicy.evaluate(
            hasSignature: hasSignature,
            signatureValid: signatureValid,
            existingNoisePublicKey: existingPeerKeys.noisePublicKey,
            announcedNoisePublicKey: announcement.noisePublicKey,
            existingSigningPublicKey: existingPeerKeys.signingPublicKey,
            authenticatedSigningPublicKey: env.authenticatedSigningPublicKey(
                announcement.noisePublicKey
            ),
            announcedSigningPublicKey: announcement.signingPublicKey
        )
        if case .reject(.keyMismatch) = trustDecision {
            SecureLogger.warning("⚠️ Announce key mismatch for \(peerID.id.prefix(8))… — keeping unverified", category: .security)
        }
        if case .reject(.signingKeyMismatch) = trustDecision {
            SecureLogger.warning("🚨 Announce signing-key mismatch for \(peerID.id.prefix(8))… — refusing to replace pinned signing key (possible impersonation attempt)", category: .security)
        }
        if case .reject(.authenticatedSigningKeyMismatch) = trustDecision {
            SecureLogger.warning(
                "⚠️ Announce signing-key replacement rejected for Noise-authenticated peer \(peerID.id.prefix(8))…",
                category: .security
            )
        }
        var verifiedAnnounce = trustDecision.isVerified

        var isNewPeer = false
        var isReconnectedPeer = false
        let directLinkState = env.linkState(peerID)
        let isDirectAnnounce = packet.ttl == env.messageTTL
        // A "direct" announce arriving on a link that another peer already
        // owns is either a rotation heal or a replay with its TTL restored;
        // both are ambiguous, so only the rebind (which containment-checks
        // the claimed identity) may promote it — never this shortcut.
        //
        // Known limitation: denying the shortcut cannot prevent forged
        // presence outright. A rebind that passes the containment checks
        // promotes the claimed peer to connected — it must, or a legitimate
        // rotation on an open link would read as disconnected — so a replay
        // that wins the rebind (absent victim, cooldown clear) still forges
        // presence. That residue is presence display only: DMs stay gated on
        // canDeliverSecurely (no Noise session means retain + courier, see
        // MessageRouter.sendPrivate). What this check buys: the ambiguous
        // announce alone never flips presence — forging requires winning the
        // containment-checked rebind (never steals an identity that owns a
        // live link; at most one rebind per link per cooldown window).
        let linkBoundToOtherPeer = isDirectAnnounce && env.linkBoundToOtherPeer(packet, peerID)

        env.withRegistryBarrier {
            let hasPeripheralConnection = directLinkState.hasPeripheral
            let hasCentralSubscription = directLinkState.hasCentral

            // Require verified announce; ignore otherwise (no backward compatibility)
            if !verifiedAnnounce {
                SecureLogger.warning("❌ Ignoring unverified announce from \(peerID.id.prefix(8))…", category: .security)
                // Reset flags to prevent post-barrier code from acting on unverified announces
                isNewPeer = false
                isReconnectedPeer = false
                return
            }

            // The registry re-checks the signing-key pin inside the barrier.
            // The pre-barrier trust check reads the registry outside the
            // barrier, so this closes the race where two announces for the
            // same peer are evaluated concurrently.
            guard let update = env.upsertVerifiedAnnounce(
                peerID,
                announcement,
                hasPeripheralConnection || hasCentralSubscription || (isDirectAnnounce && !linkBoundToOtherPeer),
                now
            ) else {
                SecureLogger.warning("🚨 Registry refused announce for \(peerID.id.prefix(8))… — signing key differs from pinned key", category: .security)
                verifiedAnnounce = false
                isNewPeer = false
                isReconnectedPeer = false
                return
            }
            isNewPeer = update.isNewPeer
            isReconnectedPeer = update.wasDisconnected

            // Log connection status only for direct connectivity changes; debounce to reduce spam
            if isDirectAnnounce || hasPeripheralConnection || hasCentralSubscription {
                let now = env.now()
                if update.isNewPeer {
                    SecureLogger.debug("🆕 New peer: \(announcement.nickname)", category: .session)
                } else if update.wasDisconnected {
                    if env.shouldEmitReconnectLog(peerID, now) {
                        SecureLogger.debug("🔄 Peer \(announcement.nickname) reconnected", category: .session)
                    }
                } else if let previousNickname = update.previousNickname, previousNickname != announcement.nickname {
                    SecureLogger.debug("🔄 Peer \(peerID.id.prefix(8))… changed nickname: \(previousNickname) -> \(announcement.nickname)", category: .session)
                }
            }
        }

        // Update topology with verified neighbor claims (only for authenticated announces)
        if verifiedAnnounce, let neighbors = announcement.directNeighbors {
            env.updateTopology(peerID, neighbors)
        }

        // Persist cryptographic identity and signing key for robust offline
        // verification — only for verified announces. Persisting unverified
        // announces would let an attacker who replays a victim's noisePublicKey
        // overwrite the victim's stored signing key/nickname (identity poisoning).
        if verifiedAnnounce {
            env.persistIdentity(announcement)
        }

        let announceBackID = "announce-back-\(peerID)"
        let shouldSendBack = !env.dedupContains(announceBackID)
        if shouldSendBack {
            env.dedupMarkProcessed(announceBackID)
        }
        let responsePlan = BLEAnnounceResponsePolicy.plan(
            isDirectAnnounce: isDirectAnnounce,
            isNewPeer: isNewPeer,
            isReconnectedPeer: isReconnectedPeer,
            shouldSendAnnounceBack: shouldSendBack
        )

        // Only notify of connection for new or reconnected peers when it is a
        // direct announce; the list update always follows in the same hop.
        env.deliverAnnounceUIEvents(
            peerID,
            responsePlan.shouldNotifyPeerConnected,
            responsePlan.shouldNotifyPeerConnected && responsePlan.shouldScheduleInitialSync
        )

        // Track for sync (include our own and others' announces)
        env.trackPacketSeen(packet)

        if responsePlan.shouldSendAnnounceBack {
            // Reciprocate announce for bidirectional discovery
            // Force send to ensure the peer receives our announce
            env.sendAnnounceBack()
        }

        // Afterglow: on first-seen peers, schedule a short re-announce to push presence one more hop
        if responsePlan.shouldScheduleAfterglow {
            let delay = Double.random(in: 0.3...0.6)
            env.scheduleAfterglow(delay)
        }

        return BLEAnnounceHandlingResult(
            peerID: peerID,
            announcement: announcement,
            isDirectAnnounce: isDirectAnnounce,
            isVerified: verifiedAnnounce
        )
    }
}
