#include "crNet.h"

#include <curl/curl.h>
#include <libwebsockets.h>

#include "crStruct.h"

struct ByteBuf
{
    uint8_t* data = nullptr;
    int32_t  size = 0;
    int32_t  cap  = 0;

    void EnsureCap( int32_t need )
    {
        if( need <= cap )
            return;

        int32_t newCap = ( cap > 0 ) ? cap : 256;
        while( newCap < need )
            newCap *= 2;

        data = static_cast<uint8_t*>( SDL_realloc( data, static_cast<size_t>( newCap ) ) );
        cap  = newCap;
    }

    void Append( const void* src, int32_t len )
    {
        EnsureCap( size + len );
        SDL_memcpy( data + size, src, static_cast<size_t>( len ) );
        size += ( len );
    }

    void Release()
    {
        SDL_free( data );
        data = nullptr;
        size = 0;
        cap  = 0;
    }
};

struct HttpRequest
{
    int32_t     id         = crNet::INVALID_ID;
    CURL*       easy       = nullptr;
    curl_slist* headerList = nullptr;
    ByteBuf     body;
};

enum class EWsState : uint8_t
{
    CONNECTING = 0,
    OPEN,
    CLOSING,
};

struct WsSocket
{
    int32_t  id              = crNet::INVALID_ID;
    lws*     wsi             = nullptr;
    EWsState state           = EWsState::CONNECTING;
    bool     userClose       = false;
    bool     recvBinary      = false;
    int32_t  closeCode       = 0;
    uint64_t connectDeadline = 0;      // SDL_GetTicks() ms
    int32_t  sendHead        = 0;
    ByteBuf  send;                     // records: [int32 len][uint8 binary][payload]
    ByteBuf  recv;
};

static void LwsLog( int level, const char* line )
{
    SDL_LogWarn( CR_LOG_CATEGORY_NET, "crNet lws(%d): %s", level, line );
}

struct crNetBackend
{
    crNet*       net   = nullptr;
    CURLM*       multi = nullptr;
    lws_context* ws    = nullptr;

    crArray<HttpRequest*> requests{ 8 };
    crArray<WsSocket*>    sockets{ 2 };

    ByteBuf scratch;   // LWS_PRE-padded staging for lws_write

    // lws does not document whether it copies the CA bytes, so this outlives the context
    void*  caPem    = nullptr;
    size_t caPemLen = 0;

    static size_t HttpWrite( char* data, size_t size, size_t nmemb, void* user )
    {
        HttpRequest* req = static_cast<HttpRequest*>( user );
        const size_t total = size * nmemb;
        req->body.Append( data, static_cast<int32_t>( total ) );
        return total;
    }

    static int WsCallback( lws* wsi, lws_callback_reasons reason, void* user, void* in, size_t len )
    {
        crNetBackend* be = static_cast<crNetBackend*>( lws_context_user( lws_get_context( wsi ) ) );
        WsSocket*     s  = static_cast<WsSocket*>( user );

        switch( reason )
        {
            case LWS_CALLBACK_CLIENT_ESTABLISHED:
            {
                s->state = EWsState::OPEN;
                be->net->PushEvent( s->id, ENetEventType::WS_OPEN, 0, nullptr, 0 );
                break;
            }
            case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
            {
                if( s != nullptr )
                {
                    const char* err = ( in != nullptr ) ? static_cast<const char*>( in ) : "connection error";
                    be->net->PushEvent( s->id, ENetEventType::WS_CLOSED, 0, err, static_cast<int32_t>( SDL_strlen( err ) ) );
                    be->RemoveSocket( s );
                }
                break;
            }
            case LWS_CALLBACK_CLIENT_RECEIVE:
            {
                if( s->recv.size == 0 )
                    s->recvBinary = ( lws_frame_is_binary( wsi ) != 0 );

                s->recv.Append( in, static_cast<int32_t>( len ) );

                if( lws_is_final_fragment( wsi ) && ( lws_remaining_packet_payload( wsi ) == 0 ) )
                {
                    const ENetEventType type = s->recvBinary ? ENetEventType::WS_BINARY : ENetEventType::WS_TEXT;
                    be->net->PushEvent( s->id, type, 0, s->recv.data, s->recv.size );
                    s->recv.size = 0;
                }
                break;
            }
            case LWS_CALLBACK_CLIENT_WRITEABLE:
            {
                if( s->userClose )
                {
                    lws_close_reason( wsi, LWS_CLOSE_STATUS_NORMAL, nullptr, 0 );
                    return -1;
                }

                if( s->sendHead < s->send.size )
                {
                    int32_t msgLen = 0;
                    SDL_memcpy( &msgLen, s->send.data + s->sendHead, sizeof( msgLen ) );
                    const uint8_t  isBinary = s->send.data[ s->sendHead + 4 ];
                    const uint8_t* payload  = s->send.data + s->sendHead + 5;

                    be->scratch.EnsureCap( LWS_PRE + msgLen );
                    SDL_memcpy( be->scratch.data + LWS_PRE, payload, static_cast<size_t>( msgLen ) );

                    const lws_write_protocol proto   = ( isBinary != 0 ) ? LWS_WRITE_BINARY : LWS_WRITE_TEXT;
                    const int32_t            written = lws_write( wsi, be->scratch.data + LWS_PRE, static_cast<size_t>( msgLen ), proto );
                    if( written < msgLen )
                        return -1;

                    s->sendHead += ( 5 + msgLen );
                    if( s->sendHead >= s->send.size )
                    {
                        s->sendHead  = 0;
                        s->send.size = 0;
                    }
                    else
                        lws_callback_on_writable( wsi );
                }
                break;
            }
            case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE:
            {
                if( ( s != nullptr ) && ( len >= 2 ) )
                {
                    const uint8_t* code = static_cast<const uint8_t*>( in );
                    s->closeCode = ( code[ 0 ] << 8 ) | code[ 1 ];
                }
                break;
            }
            case LWS_CALLBACK_CLIENT_CLOSED:
            {
                if( s != nullptr )
                {
                    be->net->PushEvent( s->id, ENetEventType::WS_CLOSED, s->closeCode, nullptr, 0 );
                    be->RemoveSocket( s );
                }
                break;
            }
            default:
                break;
        }

        return 0;
    }

    int32_t StartRequest( crNet* owner, const char* url, bool isPost, const char* contentType,
                          const void* body, int32_t bodySize, const char* const* headers, int32_t headerCount )
    {
        CURL* easy = curl_easy_init();
        if( easy == nullptr )
            return crNet::INVALID_ID;

        HttpRequest* req = new HttpRequest();
        req->id   = ++owner->_idCounter;
        req->easy = easy;

        curl_blob caBlob = { caPem, caPemLen, CURL_BLOB_COPY };

        curl_easy_setopt( easy, CURLOPT_URL, url );
        curl_easy_setopt( easy, CURLOPT_NOSIGNAL, 1L );
        curl_easy_setopt( easy, CURLOPT_CAINFO_BLOB, &caBlob );
        curl_easy_setopt( easy, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>( crNet::HTTP_CONNECT_TIMEOUT_MS ) );
        curl_easy_setopt( easy, CURLOPT_TIMEOUT_MS, static_cast<long>( crNet::HTTP_TOTAL_TIMEOUT_MS ) );
        curl_easy_setopt( easy, CURLOPT_WRITEFUNCTION, HttpWrite );
        curl_easy_setopt( easy, CURLOPT_WRITEDATA, req );
        curl_easy_setopt( easy, CURLOPT_PRIVATE, req );

        if( contentType != nullptr )
        {
            char line[ 256 ];
            SDL_snprintf( line, sizeof( line ), "Content-Type: %s", contentType );
            req->headerList = curl_slist_append( req->headerList, line );
        }
        for( int32_t i = 0; i < headerCount; ++i )
            req->headerList = curl_slist_append( req->headerList, headers[ i ] );
        if( req->headerList != nullptr )
            curl_easy_setopt( easy, CURLOPT_HTTPHEADER, req->headerList );

        if( isPost )
        {
            // size first — COPYPOSTFIELDS reads it; empty body must not fall back to the read-callback path
            curl_easy_setopt( easy, CURLOPT_POSTFIELDSIZE, static_cast<long>( bodySize ) );
            curl_easy_setopt( easy, CURLOPT_COPYPOSTFIELDS, ( body != nullptr ) ? body : "" );
        }

        curl_multi_add_handle( multi, easy );
        requests.Add( req );
        return req->id;
    }

    int32_t StartWs( crNet* owner, const char* url )
    {
        char uri[ 512 ];
        if( SDL_strlcpy( uri, url, sizeof( uri ) ) >= sizeof( uri ) )
            return crNet::INVALID_ID;

        const char* prot = nullptr;
        const char* ads  = nullptr;
        const char* path = nullptr;
        int         port = 0;
        if( lws_parse_uri( uri, &prot, &ads, &port, &path ) != 0 )
        {
            SDL_LogError( CR_LOG_CATEGORY_NET, "crNet: bad ws url '%s'", url );
            return crNet::INVALID_ID;
        }

        char fullPath[ 512 ];
        fullPath[ 0 ] = '/';
        SDL_strlcpy( fullPath + 1, path, sizeof( fullPath ) - 1 );

        const bool ssl = ( SDL_strcmp( prot, "wss" ) == 0 ) || ( SDL_strcmp( prot, "https" ) == 0 );

        WsSocket* s = new WsSocket();
        const int32_t id   = ++owner->_idCounter;
        s->id              = id;
        s->connectDeadline = SDL_GetTicks() + crNet::WS_CONNECT_TIMEOUT_MS;
        sockets.Add( s );   // before connect — the error callback can fire synchronously

        lws_client_connect_info info = {};
        info.context             = ws;
        info.address             = ads;
        info.port                = port;
        info.path                = fullPath;
        info.host                = ads;
        info.origin              = ads;
        info.ssl_connection      = ssl ? LCCSCF_USE_SSL : 0;
        info.local_protocol_name = "crnet";
        info.userdata            = s;
        info.pwsi                = &s->wsi;

        if( lws_client_connect_via_info( &info ) == nullptr )
        {
            if( SocketIndex( s ) >= 0 )   // the callback may already have reported + removed it
            {
                const char* err = "connect failed";
                net->PushEvent( id, ENetEventType::WS_CLOSED, 0, err, static_cast<int32_t>( SDL_strlen( err ) ) );
                RemoveSocket( s );
            }
        }
        return id;
    }

    WsSocket* SocketById( int32_t id )
    {
        for( int32_t i = 0; i < sockets.Size(); ++i )
        {
            if( sockets.At( i )->id == id )
                return sockets.At( i );
        }
        return nullptr;
    }

    int32_t SocketIndex( WsSocket* s )
    {
        for( int32_t i = 0; i < sockets.Size(); ++i )
        {
            if( sockets.At( i ) == s )
                return i;
        }
        return -1;
    }

    bool Send( int32_t id, const void* data, int32_t size, bool binary )
    {
        WsSocket* s = SocketById( id );
        if( ( s == nullptr ) || ( s->state != EWsState::OPEN ) )
            return false;

        const uint8_t bin = binary ? 1 : 0;
        s->send.Append( &size, sizeof( int32_t ) );
        s->send.Append( &bin, 1 );
        s->send.Append( data, size );
        lws_callback_on_writable( s->wsi );
        return true;
    }

    void RemoveSocket( WsSocket* s )
    {
        const int32_t index = SocketIndex( s );
        if( index >= 0 )
        {
            sockets.At( index ) = sockets.At( sockets.Size() - 1 );
            sockets.Pop();
        }
        s->send.Release();
        s->recv.Release();
        delete s;
    }

    void FinishRequest( CURL* easy, CURLcode result )
    {
        char* priv = nullptr;
        curl_easy_getinfo( easy, CURLINFO_PRIVATE, &priv );
        HttpRequest* req = reinterpret_cast<HttpRequest*>( priv );
        if( req == nullptr )
            return;

        if( result == CURLE_OK )
        {
            long status = 0;
            curl_easy_getinfo( easy, CURLINFO_RESPONSE_CODE, &status );
            const ENetEventType type = ( status < 400 ) ? ENetEventType::HTTP_RESULT : ENetEventType::HTTP_FAILED;
            net->PushEvent( req->id, type, static_cast<int32_t>( status ), req->body.data, req->body.size );
        }
        else
        {
            const char* err = curl_easy_strerror( result );
            net->PushEvent( req->id, ENetEventType::HTTP_FAILED, 0, err, static_cast<int32_t>( SDL_strlen( err ) ) );
        }

        for( int32_t i = 0; i < requests.Size(); ++i )
        {
            if( requests.At( i ) == req )
            {
                requests.At( i ) = requests.At( requests.Size() - 1 );
                requests.Pop();
                break;
            }
        }
        curl_multi_remove_handle( multi, easy );
        curl_easy_cleanup( easy );
        curl_slist_free_all( req->headerList );
        req->body.Release();
        delete req;
    }
};

bool crNet::Init()
{
    if( curl_global_init( CURL_GLOBAL_DEFAULT ) != 0 )
        return false;

    crNetBackend* be = new crNetBackend();
    be->net   = this;
    be->multi = curl_multi_init();
    be->caPem = SDL_LoadFile( CA_BUNDLE_PATH, &be->caPemLen );

    lws_set_log_level( LLL_ERR | LLL_WARN, LwsLog );

    static const lws_protocols PROTOCOLS[] =
    {
        { "crnet", crNetBackend::WsCallback, 0, 16384, 0, nullptr, 0 },
        { nullptr, nullptr, 0, 0, 0, nullptr, 0 },
    };

    lws_context_creation_info info = {};
    info.port                    = CONTEXT_PORT_NO_LISTEN;
    info.protocols               = PROTOCOLS;
    info.options                 = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
    info.user                    = be;
    info.client_ssl_ca_mem       = be->caPem;
    info.client_ssl_ca_mem_len   = static_cast<unsigned int>( be->caPemLen );
    be->ws = ( be->caPem != nullptr ) ? lws_create_context( &info ) : nullptr;

    if( ( be->multi == nullptr ) || ( be->ws == nullptr ) )
    {
        SDL_LogError( CR_LOG_CATEGORY_NET, "crNet: init failed( multi:%p, ws:%p, ca:%p )",
                 static_cast<void*>( be->multi ), static_cast<void*>( be->ws ), be->caPem );
        if( be->ws != nullptr )
            lws_context_destroy( be->ws );
        if( be->multi != nullptr )
            curl_multi_cleanup( be->multi );
        SDL_free( be->caPem );
        delete be;
        curl_global_cleanup();
        return false;
    }

    _backend = be;

    const curl_version_info_data* curlInfo = curl_version_info( CURLVERSION_NOW );
    SDL_LogTrace( CR_LOG_CATEGORY_NET, "crNet: initted( curl:%s, ssl:%s, lws:%s, ca:%" SDL_PRIu64 " bytes )",
                  curlInfo->version, curlInfo->ssl_version, lws_get_library_version(),
                  static_cast<uint64_t>( be->caPemLen ) );
    return true;
}

void crNet::Cleanup()
{
    if( _backend == nullptr )
        return;
    crNetBackend* be = _backend;

    for( int32_t i = 0; i < be->requests.Size(); ++i )
    {
        HttpRequest* req = be->requests.At( i );
        curl_multi_remove_handle( be->multi, req->easy );
        curl_easy_cleanup( req->easy );
        curl_slist_free_all( req->headerList );
        req->body.Release();
        delete req;
    }
    be->requests.Clear();
    curl_multi_cleanup( be->multi );
    curl_global_cleanup();

    lws_context_destroy( be->ws );   // fires the close callbacks — sockets remove themselves
    while( be->sockets.Size() > 0 )
        be->RemoveSocket( be->sockets.At( 0 ) );

    be->scratch.Release();
    SDL_free( be->caPem );   // after lws_context_destroy: the context may still reference it
    delete be;
    _backend = nullptr;

    ClearAllEvents();
}

void crNet::Update()
{
    if( _backend == nullptr )
        return;
    crNetBackend* be = _backend;

    int running = 0;
    curl_multi_perform( be->multi, &running );
    for( ;; )
    {
        int      queued = 0;
        CURLMsg* msg    = curl_multi_info_read( be->multi, &queued );
        if( msg == nullptr )
            break;
        if( msg->msg == CURLMSG_DONE )
            be->FinishRequest( msg->easy_handle, msg->data.result );
    }

    const uint64_t now = SDL_GetTicks();
    for( int32_t i = 0; i < be->sockets.Size(); ++i )
    {
        WsSocket* s = be->sockets.At( i );
        if( ( s->state == EWsState::CONNECTING ) && ( now >= s->connectDeadline ) )
        {
            s->state = EWsState::CLOSING;
            lws_set_timeout( s->wsi, PENDING_TIMEOUT_CLOSE_SEND, LWS_TO_KILL_ASYNC );
        }
    }

    lws_service( be->ws, -1 );   // negative = single non-blocking pass (>= 0 blocks — verified in plat source)
}

int32_t crNet::Get( const char* url, const char* const* headers, int32_t headerCount )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartRequest( this, url, false, nullptr, nullptr, 0, headers, headerCount );
}

int32_t crNet::Post( const char* url, const char* contentType, const void* body, int32_t bodySize,
                     const char* const* headers, int32_t headerCount )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartRequest( this, url, true, contentType, body, bodySize, headers, headerCount );
}

int32_t crNet::WsConnect( const char* url )
{
    if( _backend == nullptr )
        return INVALID_ID;
    return _backend->StartWs( this, url );
}

bool crNet::WsSendText( int32_t id, const char* text )
{
    if( _backend == nullptr )
        return false;
    return _backend->Send( id, text, static_cast<int32_t>( SDL_strlen( text ) ), false );
}

bool crNet::WsSendBinary( int32_t id, const void* data, int32_t size )
{
    if( _backend == nullptr )
        return false;
    return _backend->Send( id, data, size, true );
}

void crNet::WsClose( int32_t id )
{
    if( _backend == nullptr )
        return;
    crNetBackend* be = _backend;

    WsSocket* s = be->SocketById( id );
    if( s == nullptr )
        return;

    if( s->state == EWsState::CONNECTING )
    {
        s->state = EWsState::CLOSING;
        lws_set_timeout( s->wsi, PENDING_TIMEOUT_CLOSE_SEND, LWS_TO_KILL_ASYNC );
        return;
    }
    if( s->state == EWsState::OPEN )
    {
        s->state     = EWsState::CLOSING;
        s->userClose = true;
        s->closeCode = 1000;
        lws_callback_on_writable( s->wsi );
    }
}
