import Foundation

enum SearchPacketChannel: Hashable, Sendable {
    case ui(UUID)
    case userInfo
    case unscoped

    nonisolated static func == (lhs: SearchPacketChannel, rhs: SearchPacketChannel) -> Bool {
        switch (lhs, rhs) {
        case let (.ui(left), .ui(right)):
            return left == right
        case (.userInfo, .userInfo), (.unscoped, .unscoped):
            return true
        default:
            return false
        }
    }

    nonisolated func hash(into hasher: inout Hasher) {
        switch self {
        case let .ui(id):
            hasher.combine(0)
            hasher.combine(id)
        case .userInfo:
            hasher.combine(1)
        case .unscoped:
            hasher.combine(2)
        }
    }
}

/// Deterministic router for PacketSearch responses.
///
/// Outgoing search requests enqueue their logical channel in FIFO order.
/// Incoming responses are routed to the first pending channel, while `unscoped`
/// handlers remain compatibility listeners that receive all routed responses.
final class SearchPacketRouter {

    private struct HandlerEntry {
        let channel: SearchPacketChannel
        let handler: (PacketSearch) -> Void
    }

    private let lock = NSLock()
    private var handlers: [UUID: HandlerEntry] = [:]
    private var pendingChannels: [SearchPacketChannel] = []

    @discardableResult
    func addHandler(
        channel: SearchPacketChannel = .unscoped,
        _ handler: @escaping (PacketSearch) -> Void
    ) -> UUID {
        let id = UUID()
        lock.lock()
        handlers[id] = HandlerEntry(channel: channel, handler: handler)
        lock.unlock()
        return id
    }

    func removeHandler(_ id: UUID) {
        lock.lock()
        handlers.removeValue(forKey: id)
        lock.unlock()
    }

    func enqueueOutgoingRequest(channel: SearchPacketChannel) {
        lock.lock()
        pendingChannels.append(channel)
        lock.unlock()
    }

    func resetPending() {
        lock.lock()
        pendingChannels.removeAll()
        lock.unlock()
    }

    /// Routes one incoming response to the next pending channel (FIFO).
    /// Returns the routed channel for observability/logging.
    @discardableResult
    func dispatchIncomingResponse(_ packet: PacketSearch) -> SearchPacketChannel {
        let routedChannel: SearchPacketChannel
        let callbacks: [(PacketSearch) -> Void]

        lock.lock()
        if pendingChannels.isEmpty {
            routedChannel = .unscoped
        } else {
            routedChannel = pendingChannels.removeFirst()
        }

        callbacks = handlers.values
            .filter { $0.channel == .unscoped || $0.channel == routedChannel }
            .map(\.handler)
        lock.unlock()

        for callback in callbacks {
            callback(packet)
        }

        return routedChannel
    }

    // MARK: - Test support

    var pendingCount: Int {
        lock.lock()
        let count = pendingChannels.count
        lock.unlock()
        return count
    }
}
