#include "odinroom.h"
#include "odinmedia.h"
#include "odincipher.h"
#include "utilities.h"
#include <iostream>
#include <vector>
#include <cstring>
#include <cstdio>

using namespace std;



// Required, otherwise an unknown symbol error comes up
Napi::FunctionReference* OdinRoomWrapper::constructor;
struct OdinConnectionPool* OdinRoomWrapper::_pool = nullptr;
std::map<uint64_t, OdinRoomWrapper*> OdinRoomWrapper::_roomsMap;
std::mutex OdinRoomWrapper::_roomsMapMutex;

// Shared SDK initialization flag (also used by OdinClient)
bool g_odinSdkInitialized = false;

// Static counter for active rooms - used for SDK lifecycle management
static std::atomic<int> g_activeRoomCount{0};
static std::mutex g_sdkLifecycleMutex;

/**
 * Increments the active room count and initializes the SDK if this is the first room.
 * Thread-safe for concurrent room creation.
 */
void OdinRoomWrapper::IncrementRoomCount() {
    std::lock_guard<std::mutex> lock(g_sdkLifecycleMutex);
    int prevCount = g_activeRoomCount.fetch_add(1);
    
    if (prevCount == 0 && !g_odinSdkInitialized) {
        // First room - initialize the SDK
        OdinError initRc = odin_initialize(ODIN_VERSION);
        if (!odin_is_error(initRc)) {
            g_odinSdkInitialized = true;
        }
    }
}

/**
 * Decrements the active room count and shuts down the SDK if this was the last room.
 * Thread-safe for concurrent room destruction.
 */
void OdinRoomWrapper::DecrementRoomCount() {
    std::lock_guard<std::mutex> lock(g_sdkLifecycleMutex);
    int newCount = g_activeRoomCount.fetch_sub(1) - 1;
    
    if (newCount == 0 && g_odinSdkInitialized) {
        // Last room closed - shutdown the SDK
        // First free the connection pool if it exists
        if (_pool) {
            odin_connection_pool_free(_pool);
            _pool = nullptr;
        }
        odin_shutdown();
        g_odinSdkInitialized = false;
    }
}

struct OdinConnectionPool* OdinRoomWrapper::GetConnectionPool() {
    if (!_pool) {
        // Auto-initialize ODIN SDK if not already done
        if (!g_odinSdkInitialized) {
            OdinError initRc = odin_initialize(ODIN_VERSION);
            if (!odin_is_error(initRc)) {
                g_odinSdkInitialized = true;
            } else {
                // Don't print error for "already initialized" - this is expected
                // when OdinClient was created first
            }
        }
        
        OdinConnectionPoolSettings settings;
        memset(&settings, 0, sizeof(settings));
        settings.on_datagram = OdinRoomWrapper::OnDatagram;
        settings.on_rpc = OdinRoomWrapper::OnRPC;
        settings.user_data = nullptr;
        
        OdinError rc = odin_connection_pool_create(settings, &_pool);
        if (odin_is_error(rc)) {
            printf("Odin NodeJS Addon: Error creating connection pool: %d. Did you forget to create OdinClient first?\n", rc);
        }
    }
    return _pool;
}

void OdinRoomWrapper::OnDatagram(uint64_t room_ref, uint16_t media_id, const uint8_t *bytes, uint32_t bytes_length, void *user_data) {
    std::lock_guard<std::mutex> lock(_roomsMapMutex);
    auto it = _roomsMap.find(room_ref);
    if (it != _roomsMap.end()) {
        it->second->HandleDatagramInternal(media_id, bytes, bytes_length);
    }
}

void OdinRoomWrapper::OnRPC(uint64_t room_ref, const uint8_t *bytes, uint32_t bytes_length, void *user_data) {
    std::lock_guard<std::mutex> lock(_roomsMapMutex);
    auto it = _roomsMap.find(room_ref);
    if (it != _roomsMap.end()) {
        it->second->HandleRPCInternal(bytes, bytes_length);
    }
}

Napi::Value OdinRoomWrapper::RoomId(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (!_room) return env.Null();
    char name[256];
    uint32_t len = sizeof(name);
    OdinError err = odin_room_get_name(_room, name, &len);
    if (odin_is_error(err)) return env.Null();
    return Napi::String::New(env, name);
}

/**
 * Internal handler for audio datagrams received from the ODIN server.
 * Called from the static OnDatagram callback on the connection pool's thread.
 * 
 * Thread Safety: Checks _started before accessing decoders to prevent
 * operations on freed decoders during shutdown.
 */
void OdinRoomWrapper::HandleDatagramInternal(uint16_t media_id, const uint8_t *bytes, uint32_t bytes_length) {
    // Early exit if room is closing - decoders may be freed
    if (!_started) return;
    
    std::lock_guard<std::mutex> lock(_decodersMutex);
    
    // Double-check after acquiring lock
    if (!_started) return;
    
    auto it = _decoders.find(media_id);
    if (it == _decoders.end()) {
        OdinDecoder* decoder = nullptr;
        OdinError rc = odin_decoder_create(media_id, 48000, true, &decoder);
        if (!odin_is_error(rc)) {
            _decoders[media_id] = decoder;
            it = _decoders.find(media_id);
        }
    }
    
    if (it != _decoders.end()) {
        odin_decoder_push(it->second, bytes, bytes_length);
    }
}

/**
 * Internal handler for RPC messages received from the ODIN server.
 * Called from the static OnRPC callback on the connection pool's thread.
 * 
 * Thread Safety: Checks _started before calling callbacks to prevent
 * calling released ThreadSafeFunctions during shutdown.
 */
void OdinRoomWrapper::HandleRPCInternal(const uint8_t *bytes, uint32_t bytes_length) {
    // Early exit if room is closing - callbacks may be released
    if (!_started) return;
    
    // Just pass the raw RPC bytes to JavaScript
    // The JS layer can handle parsing MessagePack format
    
    // Create a copy of the data to pass to the callback
    std::vector<uint8_t>* data = new std::vector<uint8_t>(bytes, bytes + bytes_length);
    
    // Get pointer to pending counter for ref-counting
    std::atomic<int>* pendingPtr = &_pendingCallbacks;
    
    auto callback = [pendingPtr](Napi::Env env, Napi::Function jsCallback, void* value) {
        std::vector<uint8_t>* rpcData = (std::vector<uint8_t>*)value;
        try {
            auto buffer = Napi::ArrayBuffer::New(env, rpcData->size());
            std::copy(rpcData->begin(), rpcData->end(), (uint8_t*)buffer.Data());
            jsCallback.Call({buffer});
        } catch (...) {
            // Exception in RPC callback - silently ignore
        }
        delete rpcData;
        
        // Decrement pending count AFTER callback completes
        if (pendingPtr) {
            pendingPtr->fetch_sub(1);
        }
    };
    
    // Emit to the generic event listener (with safety check)
    if (_started && _eventListener) {
        std::vector<uint8_t>* dataCopy = new std::vector<uint8_t>(*data);
        _pendingCallbacks.fetch_add(1);  // Increment before NonBlockingCall
        _eventListener.NonBlockingCall(dataCopy, callback);
    }
    
    // Emit to specific event listeners (with safety check for each)
    // Note: We iterate over a copy of the keys to avoid issues if the map changes
    if (_started) {
        for (auto& pair : _eventListeners) {
            if (!_started) break;  // Early exit if closing during iteration
            std::vector<uint8_t>* dataCopy = new std::vector<uint8_t>(*data);
            _pendingCallbacks.fetch_add(1);  // Increment before NonBlockingCall
            pair.second.NonBlockingCall(dataCopy, callback);
        }
    }
    
    delete data;
}

OdinRoomWrapper::OdinRoomWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap<OdinRoomWrapper>(info) {
    Napi::Env env = info.Env();
    if (info.Length() != 1 || !info[0].IsString()) {
        Napi::TypeError::New(env, "Provide a token to create a room").ThrowAsJavaScriptException();
        return;
    }
    
    // Increment room count - initializes SDK if this is the first room
    IncrementRoomCount();
    
    _token = info[0].ToString().Utf8Value();
    _room = nullptr;
    _cipher = nullptr;
    _joined = false;
    _started = false;
    _pendingCallbacks = 0;
    _ownPeerId = 0;
    // Initialize position to origin and scale to 1.0 (default)
    _position[0] = 0.0f;
    _position[1] = 0.0f;
    _position[2] = 0.0f;
    _positionScale = 1.0f;
    

}

/**
 * Releases resources when the JavaScript object is garbage collected.
 * 
 * This ensures resources are freed even if close() wasn't called explicitly.
 * Since Close() now handles all cleanup, Finalize() just needs to:
 * 1. Stop the thread and free room (if not already done)
 * 2. Free any remaining decoders
 * 3. Decrement room count for SDK lifecycle management
 */
void OdinRoomWrapper::Finalize(Napi::Env env) {
    // Stop audio thread if still running
    _started = false;
    if (_nativeThread.joinable()) {
        _nativeThread.join();
    }
    
    // Free decoders if not already done by Close()
    {
        std::lock_guard<std::mutex> lock(_decodersMutex);
        for (auto& pair : _decoders) {
            odin_decoder_free(pair.second);
        }
        _decoders.clear();
    }
    
    // Clear media-to-peer mapping
    {
        std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
        _mediaToPeer.clear();
    }
    
    // Free room if not already done by Close()
    if (_room) {
        {
            std::lock_guard<std::mutex> lock(_roomsMapMutex);
            _roomsMap.erase(odin_room_get_ref(_room));
        }
        odin_room_free(_room);
        _room = nullptr;
    }
    
    // Clear cipher reference
    _cipher = nullptr;
    
    // Decrement room count - shuts down SDK if this was the last room
    DecrementRoomCount();
}

Napi::Object OdinRoomWrapper::Init(Napi::Env env, Napi::Object exports) {
    Napi::Function func = DefineClass(env, "OdinRoom", {
        InstanceMethod<&OdinRoomWrapper::Join>("join", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::UpdatePeerUserData>("updateOwnUserData", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::UpdatePosition>("updatePosition", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SetPositionScale>("setPositionScale", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SendMessage>("sendMessage", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SendRpc>("sendRpc", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SetEventListener>("setEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::AddEventListener>("addEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::RemoveEventListener>("removeEventListener", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::Close>("close", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::CreateAudioStream>("createAudioStream", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SetCipher>("setCipher", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::RegisterMediaPeer>("registerMediaPeer", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::UnregisterMedia>("unregisterMedia", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::SetOwnPeerId>("setOwnPeerId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::GetConnectionStats>("getConnectionStats", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::GetConnectionId>("getConnectionId", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceMethod<&OdinRoomWrapper::GetJitterStats>("getJitterStats", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceAccessor("ownPeerId", &OdinRoomWrapper::GetOwnPeerId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        InstanceAccessor("id", &OdinRoomWrapper::RoomId, nullptr, static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
        StaticMethod<&OdinRoomWrapper::CreateNewItem>("CreateNewItem", static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
    });

    constructor = new Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    exports.Set("OdinRoom", func);
    // REMOVED: env.SetInstanceData<Napi::FunctionReference>(constructor);
    return exports;
}

/**
 * Updates the 3D position of the local peer. This position is used for spatial audio
 * and server-side culling. If called before join(), the position will be used during
 * room creation. If called after join(), this is a no-op as the current SDK doesn't
 * support runtime position updates (position is only set during room creation).
 *
 * @param info[0] x coordinate (number)
 * @param info[1] y coordinate (number)
 * @param info[2] z coordinate (number)
 */
void OdinRoomWrapper::UpdatePosition(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    // Validate arguments: we need exactly 3 numbers (x, y, z)
    if (info.Length() < 3) {
        Napi::TypeError::New(env, "updatePosition requires 3 arguments: x, y, z").ThrowAsJavaScriptException();
        return;
    }
    
    if (!info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
        Napi::TypeError::New(env, "updatePosition arguments must be numbers").ThrowAsJavaScriptException();
        return;
    }
    
    // Store the position values (will be used during join/room creation)
    // Note: The ODIN SDK 1.8.2+ only supports setting position during odin_room_create_ex,
    // so this stores the values for use before join() is called.
    _position[0] = info[0].As<Napi::Number>().FloatValue();
    _position[1] = info[1].As<Napi::Number>().FloatValue();
    _position[2] = info[2].As<Napi::Number>().FloatValue();
}

/**
 * Sets the scaling factor for position coordinates. This scale is applied to the
 * position values when they are used during room creation. Peers are visible to each
 * other within a unit circle of radius 1.0, so the scale should be set such that
 * the maximum distance between peers remains <= 1.0.
 *
 * @param info[0] scale factor (number)
 */
void OdinRoomWrapper::SetPositionScale(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    // Validate arguments: we need exactly 1 number (scale)
    if (info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "setPositionScale requires a scale value (number)").ThrowAsJavaScriptException();
        return;
    }
    
    // Store the scale value (will be applied to position during join/room creation)
    _positionScale = info[0].As<Napi::Number>().FloatValue();
}

void OdinRoomWrapper::Join(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() < 1) {
        Napi::TypeError::New(env, "Gateway required").ThrowAsJavaScriptException();
        return;
    }
    std::string url = info[0].ToString().Utf8Value();
    const uint8_t* userData = nullptr;
    size_t userDataLen = 0;
    if (info.Length() > 1 && info[1].IsTypedArray()) {
        Napi::Uint8Array data = info[1].As<Napi::Uint8Array>();
        userData = data.Data();
        userDataLen = data.ByteLength();
    }
    struct OdinConnectionPool* pool = GetConnectionPool();
    // Apply position scale to the stored position coordinates
    float scaledPosition[3] = {
        _position[0] / _positionScale,
        _position[1] / _positionScale,
        _position[2] / _positionScale
    };
    OdinError error = odin_room_create_ex(pool, url.c_str(), _token.c_str(), nullptr, userData, userDataLen, scaledPosition, _cipher, &_room);
    if (odin_is_error(error)) {
        OdinUtilities::ThrowNapiException(env, error, "Failed to join room");
        return;
    }
    {
        std::lock_guard<std::mutex> lock(_roomsMapMutex);
        _roomsMap[odin_room_get_ref(_room)] = this;
    }
    _joined = true;
    _started = true;
    _nativeThread = std::thread(&OdinRoomWrapper::HandleAudioData, this);
}

/**
 * Closes the room connection and frees all associated resources.
 * 
 * This method performs a complete cleanup in the correct order to prevent:
 * - Memory leaks (decoders, media-to-peer mappings)
 * - Dangling pointers (cipher reference)
 * - Race conditions (thread-callback synchronization)
 * 
 * CRITICAL: The cleanup sequence must:
 * 1. Remove from room map FIRST (stops connection pool from routing new callbacks)
 * 2. Close native room (stops connection pool activity)
 * 3. Signal thread to stop and wait for it
 * 4. THEN release callbacks (now safe - no more incoming calls)
 * 5. Free remaining resources
 */
void OdinRoomWrapper::Close(const Napi::CallbackInfo &info) {
    if (!_room || !_joined) {
        return;
    }
    
    uint64_t roomRef = odin_room_get_ref(_room);
    // Step 1: Remove from global room map FIRST
    // This prevents the connection pool from routing any new callbacks to us
    {
        std::lock_guard<std::mutex> lock(_roomsMapMutex);
        _roomsMap.erase(roomRef);
    }
    
    // Step 2: Close the native room - this stops the connection pool
    // After this call, OnDatagram and OnRPC won't be called for this room
    odin_room_close(_room);
    
    // Step 3: Signal the audio thread to stop
    _started = false;
    
    // Step 4: Wait for audio processing thread to finish
    // The thread will see _started=false and exit
    if (_nativeThread.joinable()) {
        _nativeThread.join();
    }
    
    
    // Step 4.5: Wait for pending NonBlockingCall callbacks to complete
    // NonBlockingCall schedules work asynchronously on the Node.js event loop.
    // Even after the audio thread exits, there may be callbacks already queued
    // that haven't executed yet. We must wait for these to complete before
    // releasing the ThreadSafeFunctions.
    // 
    // We use ref-counting: _pendingCallbacks is incremented before NonBlockingCall
    // and decremented in the callback after execution. We wait for it to reach 0.
    {
        int waitMs = 0;
        const int maxWaitMs = 500;  // Maximum wait time to prevent hangs
        while (_pendingCallbacks.load() > 0 && waitMs < maxWaitMs) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            waitMs += 10;
        }
        // If we still have pending callbacks after timeout, they will execute
        // after release but we've done our best. In practice this shouldn't happen.
    }
    
    // Step 5: NOW it's safe to release ThreadSafeFunctions
    // No more connection pool callbacks, no more audio thread activity,
    // and all pending NonBlockingCall callbacks have completed
    if (_eventListener) {
        _eventListener.Release();
    }
    if (_audioDataReceivedEventListener) {
        _audioDataReceivedEventListener.Release();
    }
    for (auto& pair : _eventListeners) {
        pair.second.Release();
    }
    _eventListeners.clear();
    
    // Step 6: Free all decoders to prevent memory leaks
    {
        std::lock_guard<std::mutex> lock(_decodersMutex);
        for (auto& pair : _decoders) {
            odin_decoder_free(pair.second);
        }
        _decoders.clear();
    }
    
    // Step 7: Clear media-to-peer mapping
    {
        std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
        _mediaToPeer.clear();
    }
    
    // Step 8: Clear cipher reference (we don't own it, just null the pointer)
    _cipher = nullptr;
    
    // Step 9: Free the native room
    odin_room_free(_room);
    _room = nullptr;
    _joined = false;
    
}

Napi::Value OdinRoomWrapper::CreateNewItem(const Napi::CallbackInfo& info) {
    return constructor->New({ info[0] });
}

Napi::Object OdinRoomWrapper::NewInstance(Napi::Value arg) {
    return constructor->New({ arg });
}

void OdinRoomWrapper::UpdatePeerUserData(const Napi::CallbackInfo &info) {}

void OdinRoomWrapper::SendMessage(const Napi::CallbackInfo &info) {}

/**
 * Sends a raw RPC message (MessagePack format) to the ODIN server.
 * This is used for commands like StartMedia, StopMedia, etc.
 * 
 * @param info[0] Uint8Array containing the MessagePack-encoded RPC message
 */
void OdinRoomWrapper::SendRpc(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    if (!_room || !_joined) {
        Napi::Error::New(env, "Room not joined").ThrowAsJavaScriptException();
        return;
    }
    
    if (info.Length() < 1 || !info[0].IsTypedArray()) {
        Napi::TypeError::New(env, "Uint8Array required as RPC data").ThrowAsJavaScriptException();
        return;
    }
    
    Napi::Uint8Array rpcData = info[0].As<Napi::Uint8Array>();
    
    OdinError rc = odin_room_send_rpc(_room, rpcData.Data(), static_cast<uint32_t>(rpcData.ByteLength()));
    if (odin_is_error(rc)) {
        OdinUtilities::ThrowNapiException(env, rc, "Failed to send RPC");
    }
}

void OdinRoomWrapper::SetEventListener(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() != 1 || !info[0].IsFunction()) {
        Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
        return;
    }
    _eventListener = Napi::ThreadSafeFunction::New(env, info[0].As<Napi::Function>(), "Callback", 0, 1);
}

void OdinRoomWrapper::AddEventListener(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() != 2 || !info[0].IsString() || !info[1].IsFunction()) {
        Napi::TypeError::New(env, "Event Name and Callback expected").ThrowAsJavaScriptException();
        return;
    }
    std::string eventName = info[0].As<Napi::String>().Utf8Value();
    if (eventName == "AudioDataReceived") {
       _audioDataReceivedEventListener = Napi::ThreadSafeFunction::New(env, info[1].As<Napi::Function>(), "AudioDataReceivedCallback", 0, 1);
    } else {
       _eventListeners[eventName] = Napi::ThreadSafeFunction::New(env, info[1].As<Napi::Function>(), eventName + "Callback", 0, 1);
    }
}

void OdinRoomWrapper::RemoveEventListener(const Napi::CallbackInfo &info) {}

/**
 * Background thread function that processes incoming audio from decoders.
 * 
 * Runs continuously while _started is true, popping decoded audio from 
 * each decoder and sending it to JavaScript via ThreadSafeFunction callbacks.
 * 
 * Safety: Checks _started multiple times per iteration to allow quick exit
 * when Close() is called. Uses NonBlockingCall to avoid deadlocks during shutdown.
 * 
 * Ref-counting: Increments _pendingCallbacks before each NonBlockingCall and
 * decrements after the callback executes. Close() waits for this count to reach
 * zero before releasing ThreadSafeFunctions.
 */
void OdinRoomWrapper::HandleAudioData() {
    while (_started) {
        // Early exit check at loop start
        if (!_started) break;
        
        {
            std::lock_guard<std::mutex> lock(_decodersMutex);
            for (auto& pair : _decoders) {
                // Check again before processing each decoder
                if (!_started) break;
                
                OdinDecoder* decoder = pair.second;
                bool silent = false; 
                OdinError rc = odin_decoder_pop(decoder, _audioSamplesBuffer, 1920, &silent);
                if (rc == ODIN_ERROR_SUCCESS && !silent) {
                     // Double-check _started before calling callback
                     // This prevents calling a released ThreadSafeFunction
                     if (_started && _audioDataReceivedEventListener) {
                         AudioSamples* samples = new AudioSamples();
                         samples->MediaId = pair.first;
                         // Look up peer ID from media-to-peer mapping
                         {
                             std::lock_guard<std::mutex> peerLock(_mediaToPeerMutex);
                             auto peerIt = _mediaToPeer.find(pair.first);
                             samples->PeerId = (peerIt != _mediaToPeer.end()) ? peerIt->second : 0;
                         }
                         samples->SetSamples(_audioSamplesBuffer, 1920);
                         
                         // Store pointer to pending counter in samples for callback to decrement
                         samples->PendingCounterPtr = &_pendingCallbacks;
                         
                         // Increment pending count BEFORE NonBlockingCall
                         _pendingCallbacks.fetch_add(1);
                         
                         // Callback that decrements counter after execution
                         // NOTE: Adding try-catch and logging to diagnose crashes
                         auto callback = [](Napi::Env env, Napi::Function jsCallback, void* value) {
                            AudioSamples* samples = (AudioSamples*)value;
                            
                            // Get the counter pointer before we delete samples
                            std::atomic<int>* pendingPtr = samples->PendingCounterPtr;
                            
                            try {
                                // Execute callback
                                Napi::Object obj = Napi::Object::New(env);
                                obj.Set("mediaId", samples->MediaId);
                                obj.Set("peerId", (double)samples->PeerId); 
                                // Must use Copy(), not New(). New() wraps the pointer without copying.
                                // Since we delete samples below, New() would leave JS holding a dangling pointer.
                                obj.Set("samples16", Napi::Buffer<short>::Copy(env, samples->Data, samples->Len));
                                obj.Set("samples32", Napi::Buffer<float>::Copy(env, samples->OriginalData, samples->Len));
                                jsCallback.Call({obj});
                            } catch (...) {
                                // Exception in audio callback - silently ignore
                            }
                            
                            delete samples;
                            
                            // Decrement pending count AFTER callback completes
                            if (pendingPtr) {
                                pendingPtr->fetch_sub(1);
                            }
                         };
                         
                         // Use NonBlockingCall to avoid potential deadlocks during shutdown
                         _audioDataReceivedEventListener.NonBlockingCall(samples, callback);
                     }
                }
            }
        }
        // Sleep must match the decoder frame size (1920 stereo samples @ 48kHz = 20ms).
        // A shorter sleep (e.g. 10ms) polls faster than audio arrives, causing the
        // decoder's jitter buffer to produce PLC (Packet Loss Concealment) frames that
        // stretch and distort the audio stream.
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }
}

Napi::Value OdinRoomWrapper::CreateAudioStream(const Napi::CallbackInfo &info) {
    std::vector<napi_value> args;
    args.push_back(this->Value()); 
    for (size_t i = 0; i < info.Length(); i++) args.push_back(info[i]);
    return OdinMediaWrapper::NewInstance(args);
}

void OdinRoomWrapper::SetCipher(const Napi::CallbackInfo &info) {
     Napi::Env env = info.Env();
     if (info.Length() < 1 || !info[0].IsObject()) {
         Napi::TypeError::New(env, "OdinCipher instance expected").ThrowAsJavaScriptException();
         return;
     }
     OdinCipherWrapper* cipherWrapper = Napi::ObjectWrap<OdinCipherWrapper>::Unwrap(info[0].As<Napi::Object>());
     _cipher = cipherWrapper->GetCipher();
     
     // Transfer ownership: the room now owns the cipher and will free it.
     // The wrapper must not try to free it again in its destructor.
     cipherWrapper->TransferOwnership();
}

Napi::Value OdinRoomWrapper::GetOwnPeerId(const Napi::CallbackInfo &info) {
    return Napi::Number::New(info.Env(), static_cast<double>(_ownPeerId));
}

void OdinRoomWrapper::SetOwnPeerId(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Peer ID (number) required").ThrowAsJavaScriptException();
        return;
    }
    _ownPeerId = static_cast<uint64_t>(info[0].As<Napi::Number>().DoubleValue());
}

void OdinRoomWrapper::RegisterMediaPeer(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
        Napi::TypeError::New(env, "Media ID and Peer ID (numbers) required").ThrowAsJavaScriptException();
        return;
    }
    uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
    uint64_t peerId = static_cast<uint64_t>(info[1].As<Napi::Number>().DoubleValue());
    
    std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
    _mediaToPeer[mediaId] = peerId;
}

void OdinRoomWrapper::UnregisterMedia(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    if (info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
        return;
    }
    uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
    
    std::lock_guard<std::mutex> lock(_mediaToPeerMutex);
    _mediaToPeer.erase(mediaId);
}

/**
 * Retrieves detailed connection statistics for the room.
 * Returns an object containing UDP transmission/reception stats, packet loss, RTT, etc.
 * 
 * @returns {Object} Connection statistics:
 *   - udpTxDatagrams: Number of outgoing UDP datagrams
 *   - udpTxBytes: Total bytes sent
 *   - udpTxLoss: Packet loss percentage for outgoing datagrams
 *   - udpRxDatagrams: Number of incoming UDP datagrams
 *   - udpRxBytes: Total bytes received
 *   - udpRxLoss: Packet loss percentage for incoming datagrams
 *   - cwnd: Current congestion window size
 *   - congestionEvents: Number of congestion events
 *   - rtt: Round-trip time in milliseconds
 */
Napi::Value OdinRoomWrapper::GetConnectionStats(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    if (!_room || !_joined) {
        return env.Null();
    }
    
    OdinConnectionStats stats;
    memset(&stats, 0, sizeof(stats));
    
    OdinError rc = odin_room_get_connection_stats(_room, &stats);
    if (odin_is_error(rc)) {
        OdinUtilities::ThrowNapiException(env, rc, "Failed to get connection stats");
        return env.Null();
    }
    
    // Build the JavaScript object with all stats
    Napi::Object result = Napi::Object::New(env);
    result.Set("udpTxDatagrams", Napi::Number::New(env, static_cast<double>(stats.udp_tx_datagrams)));
    result.Set("udpTxBytes", Napi::Number::New(env, static_cast<double>(stats.udp_tx_bytes)));
    result.Set("udpTxLoss", Napi::Number::New(env, stats.udp_tx_loss));
    result.Set("udpRxDatagrams", Napi::Number::New(env, static_cast<double>(stats.udp_rx_datagrams)));
    result.Set("udpRxBytes", Napi::Number::New(env, static_cast<double>(stats.udp_rx_bytes)));
    result.Set("udpRxLoss", Napi::Number::New(env, stats.udp_rx_loss));
    result.Set("cwnd", Napi::Number::New(env, static_cast<double>(stats.cwnd)));
    result.Set("congestionEvents", Napi::Number::New(env, static_cast<double>(stats.congestion_events)));
    result.Set("rtt", Napi::Number::New(env, stats.rtt));
    
    return result;
}

/**
 * Retrieves the underlying connection identifier for the room.
 * Returns 0 if no valid connection exists.
 * 
 * @returns {number} The connection ID, or 0 if not connected
 */
Napi::Value OdinRoomWrapper::GetConnectionId(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    if (!_room) {
        return Napi::Number::New(env, 0);
    }
    
    uint64_t connectionId = odin_room_get_connection_id(_room);
    return Napi::Number::New(env, static_cast<double>(connectionId));
}

/**
 * Retrieves jitter buffer statistics for a specific decoder (media stream).
 * Useful for debugging audio quality issues.
 * 
 * @param info[0] Media ID (number) - The media stream to get stats for
 * @returns {Object|null} Jitter statistics, or null if decoder not found:
 *   - packetsTotal: Total packets seen by jitter buffer
 *   - packetsBuffered: Packets currently in buffer
 *   - packetsProcessed: Packets successfully processed
 *   - packetsArrivedTooEarly: Packets dropped (arrived too early)
 *   - packetsArrivedTooLate: Packets dropped (arrived too late)
 *   - packetsDropped: Packets dropped due to buffer reset
 *   - packetsInvalid: Invalid packets received
 *   - packetsRepeated: Duplicate packets received
 *   - packetsLost: Packets lost during transmission
 */
Napi::Value OdinRoomWrapper::GetJitterStats(const Napi::CallbackInfo &info) {
    Napi::Env env = info.Env();
    
    if (info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Media ID (number) required").ThrowAsJavaScriptException();
        return env.Null();
    }
    
    uint16_t mediaId = static_cast<uint16_t>(info[0].As<Napi::Number>().Uint32Value());
    
    // Find the decoder for this media ID
    std::lock_guard<std::mutex> lock(_decodersMutex);
    auto it = _decoders.find(mediaId);
    if (it == _decoders.end()) {
        // No decoder for this media ID yet - return null
        return env.Null();
    }
    
    OdinDecoderJitterStats stats;
    memset(&stats, 0, sizeof(stats));
    
    OdinError rc = odin_decoder_get_jitter_stats(it->second, &stats);
    if (odin_is_error(rc)) {
        OdinUtilities::ThrowNapiException(env, rc, "Failed to get jitter stats");
        return env.Null();
    }
    
    // Build the JavaScript object with all jitter stats
    Napi::Object result = Napi::Object::New(env);
    result.Set("packetsTotal", Napi::Number::New(env, stats.packets_total));
    result.Set("packetsBuffered", Napi::Number::New(env, stats.packets_buffered));
    result.Set("packetsProcessed", Napi::Number::New(env, stats.packets_processed));
    result.Set("packetsArrivedTooEarly", Napi::Number::New(env, stats.packets_arrived_too_early));
    result.Set("packetsArrivedTooLate", Napi::Number::New(env, stats.packets_arrived_too_late));
    result.Set("packetsDropped", Napi::Number::New(env, stats.packets_dropped));
    result.Set("packetsInvalid", Napi::Number::New(env, stats.packets_invalid));
    result.Set("packetsRepeated", Napi::Number::New(env, stats.packets_repeated));
    result.Set("packetsLost", Napi::Number::New(env, stats.packets_lost));
    
    return result;
}