/*******************************************************************************
Copyright (c) 2014, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#include <iostream>
#include <algorithm>
#include <chrono>
#include <memory>
#include <vector>
#include <atomic>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <map>

#include <unistd.h>
#include <signal.h>
#include <sys/event.h>
#include <sys/types.h>
#include <sys/select.h>
#include <fcntl.h>
#include <errno.h>
#include <cstring>

#include "config.h"
#include "socket.h"
#include "exception.h"
#include "eventapi.h"
#include "connection.h"
#include "../posix/error.h"

namespace netplus {

    static std::vector<socket*> SSOCKETS;
    static std::unordered_map<int, std::shared_ptr<con>> CONNECTIONS;

    static std::shared_mutex POLL_HANDLER_MUTEX;

    // ------------------------------------------------------------
    // Utility: safe close (no deadlock, no double close)
    // ------------------------------------------------------------
    static void safeCloseSocket(std::shared_ptr<con>& c) {
        if (!c) return;
        if (!c->slots[0].csock) return;

        try {
            c->slots[0].csock->close();
        } catch (...) {}
        // Reset to nullptr so concurrent guards (KqueueArmGuard)
        // detect the closed state and skip re-arming.
        c->slots[0].csock.reset();
    }

    // ------------------------------------------------------------
    // pollapi interface
    // ------------------------------------------------------------
    class pollapi {
    public:
        pollapi(eventapi* evcon, int timeout) : evconnection(evcon), _Timeout(timeout) {}
        virtual ~pollapi() = default;

        enum EventHandlerStatus { EVWAIT=0, EVIN=1, EVOUT=2, EVUP=3, EVERR=4, EVCON=-1 };

        virtual void initEventHandler()=0;
        virtual const char* getpolltype()=0;
        virtual int pollState(int pos)=0;
        virtual void setpollEventsFd(int fd, int events)=0;

        virtual int waitEventHandler(int timeout)=0;
        virtual void ConnectEventHandler(int tid, ULONG_PTR args)=0;
        virtual void IoEventHandler(int fd, int events, const int tid, ULONG_PTR args)=0;
        virtual void CloseEventHandler(int fd, int tid, ULONG_PTR args)=0;

    protected:
        eventapi* evconnection;
        int _Timeout;
    };

    // Internal event flags (mapped from kqueue filters)
    #define KQUEUE_EV_READ   1
    #define KQUEUE_EV_WRITE  2
    #define KQUEUE_EV_ERROR  4
    #define KQUEUE_EV_EOF    8

    // Batch kevent changes to reduce syscall overhead
    class KeventBatch {
    public:
        static constexpr int MAX_CHANGES = 256;
        
        KeventBatch(int kqfd) : _kqfd(kqfd) {}
        
        ~KeventBatch() {
            flush();
        }
        
        void add_change(int fd, int filter, int flags) {
            if (_count >= MAX_CHANGES)
                flush();
            EV_SET(&_changes[_count], fd, filter, flags, 0, 0, nullptr);
            _count++;
        }
        
        void flush() {
            if (_count > 0) {
                // Use nevents=0 so kevent() submits all changes but
                // does NOT return (and consume) any ready events.
                // EV_ONESHOT filters would be silently lost if a ready
                // event were consumed here and then ignored.
                kevent(_kqfd, _changes, _count, nullptr, 0, nullptr);
                _count = 0;
            }
        }

        // Submit any pending changes AND wait for events in a
        // single kevent() call.  This halves the number of
        // syscalls in the hot path compared to flush()+kevent().
        int submit_and_wait(struct kevent* out, int maxevents,
                            struct timespec* timeout) {
            struct kevent* ch  = _count > 0 ? _changes : nullptr;
            int            nch = _count;
            _count = 0;
            return kevent(_kqfd, ch, nch, out, maxevents, timeout);
        }
        
    private:
        int _kqfd;
        struct kevent _changes[MAX_CHANGES];
        int _count = 0;
    };

    // RAII Rearm guard - handles event re-enabling for EV_ONESHOT
    //
    // CRITICAL: Arms only ONE filter at a time — either WRITE (when
    // data is pending) or READ (idle / waiting for next request).
    //
    // Unlike epoll (which combines EPOLLIN|EPOLLOUT into one event
    // delivered to one thread), kqueue fires EVFILT_READ and
    // EVFILT_WRITE as independent events that can be delivered to
    // DIFFERENT threads.  If both filters are armed simultaneously,
    // two threads may process the same fd concurrently.  When one
    // thread closes the connection while the other's guard still
    // tries to re-arm, the stale change corrupts the batch
    // (kevent returns -1, dropping all later changes).
    //
    // Single-filter approach prevents this race entirely and
    // matches epoll's one-event-per-arm semantic.
    class KqueueArmGuard {
    public:
        KqueueArmGuard(int fd, std::shared_ptr<con> c, KeventBatch* batch)
            : _fd(fd), _con(std::move(c)), _batch(batch) {}

        ~KqueueArmGuard() {
            if (!_armed) return;
            if (!_batch || !_con) return;
            try {
                if (!_con->slots[0].csock) return;

                bool needWrite = _con->SendData.pos < _con->SendData.size()
                              || _con->slots[0].csock->hasPendingWrite()
                              || _con->slots[0].ResponsePending.exchange(false, std::memory_order_relaxed);

                if (needWrite) {
                    // Data pending → arm WRITE only.
                    // When WRITE fires, Step 4 sends the data.
                    // Step 1 also does unconditional prime_recv so
                    // any incoming data is processed in the same cycle.
                    _batch->add_change(_fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT);
                } else {
                    // Idle → arm READ for next request / data.
                    _batch->add_change(_fd, EVFILT_READ, EV_ADD | EV_ONESHOT);
                }
                // Flush immediately so other threads blocked in
                // kevent() see the re-armed fd right away — matches
                // epoll's immediate epoll_ctl() in EpollArmGuard.
                _batch->flush();
            } catch (...) {
                // Connection was closed concurrently — skip re-arm
            }
        }

        void disarm() { _armed = false; }

    private:
        bool _armed = true;
        int _fd;
        std::shared_ptr<con> _con;
        KeventBatch* _batch;
    };

    // ------------------------------------------------------------
    // poll implementation (kqueue)
    // ------------------------------------------------------------
    class poll : public pollapi {
    public:
        poll(socket* serversocket, eventapi* evcon, int kqfd, int timeout)
            : pollapi(evcon, timeout),
              _kqfd(kqfd),
              _Events(new struct kevent[serversocket->getMaxconnections()]),
              _ServerSocket(serversocket),
              _EventCount(0),
              _batch(kqfd)
        {
            {
                std::unique_lock<std::shared_mutex> lk(POLL_HANDLER_MUTEX);
                SSOCKETS.emplace_back(_ServerSocket);
            }

            for (int i = 0; i < _ServerSocket->getMaxconnections(); ++i) {
                _Events[i].ident  = 0;
                _Events[i].filter = 0;
                _Events[i].flags  = 0;
                _Events[i].fflags = 0;
                _Events[i].data   = 0;
                _Events[i].udata  = nullptr;
            }
        }

        const char* getpolltype() override { return "KQUEUE"; }
        void initEventHandler() override {}

        int getEventFlags(int pos) {
            if (pos >= _EventCount) return 0;
            int flags = 0;
            if (_Events[pos].filter == EVFILT_READ)
                flags |= KQUEUE_EV_READ;
            if (_Events[pos].filter == EVFILT_WRITE)
                flags |= KQUEUE_EV_WRITE;
            if (_Events[pos].flags & EV_ERROR)
                flags |= KQUEUE_EV_ERROR;
            if (_Events[pos].flags & EV_EOF)
                flags |= KQUEUE_EV_EOF;
            return flags;
        }

        int getEventFd(int pos) {
            if (pos >= _EventCount) return -1;
            return static_cast<int>(_Events[pos].ident);
        }

        std::shared_ptr<con> getConByFd(int fd) {
            std::shared_lock<std::shared_mutex> lk(POLL_HANDLER_MUTEX);
            auto it = CONNECTIONS.find(fd);
            if (it == CONNECTIONS.end()) return nullptr;
            return it->second;
        }

        void setpollEventsFd(int fd, int events) override {
            // Use EV_ONESHOT for reliability + explicit re-arming
            // Batching reduces syscall overhead
            
            if (events == 0) {
                // Disable all events
                _batch.add_change(fd, EVFILT_READ, EV_DELETE);
                _batch.add_change(fd, EVFILT_WRITE, EV_DELETE);
                return;
            }

            // Add/enable READ filter (one-shot, re-arm on demand)
            if (events & KQUEUE_EV_READ) {
                _batch.add_change(fd, EVFILT_READ, EV_ADD | EV_ONESHOT);
            } else {
                _batch.add_change(fd, EVFILT_READ, EV_DELETE);
            }

            // Add/enable WRITE filter (one-shot, re-arm on demand)
            if (events & KQUEUE_EV_WRITE) {
                _batch.add_change(fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT);
            } else {
                _batch.add_change(fd, EVFILT_WRITE, EV_DELETE);
            }
        }

        int pollState(int pos) override {
            if (pos >= _EventCount) return -1;
            if (static_cast<int>(_Events[pos].ident) == _ServerSocket->fd())
                return EVCON;
            return getEventFlags(pos);
        }

        // Flush any pending batch entries (re-arms, new fds)
        // so other threads sharing this kqueue fd can see them
        // immediately.  Called after each event in the loop.
        void flushPendingChanges() { _batch.flush(); }

        int waitEventHandler(int timeout) override {
            struct timespec ts{};
            struct timespec* pts = nullptr;

            if (timeout >= 0) {
                ts.tv_sec  = timeout / 1000;
                ts.tv_nsec = (timeout % 1000) * 1000000;
                pts = &ts;
            }

            // Submit any pending changes AND wait for events in
            // one syscall (half the kernel transitions).
            int evn = _batch.submit_and_wait(
                          _Events.get(),
                          _ServerSocket->getMaxconnections(), pts);

            if (evn < 0) {
                if (errno == EINTR) return 0;
                NetException e;
                char str[255];
                strerror_r_netplus(errno, str, 255);
                e[NetException::Error] << "waitEventHandler: kevent failed: " << str;
                throw e;
            }

            _EventCount = evn;
            return evn;
        }

        // ------------------------------------------------------------
        // Accept new connection
        // ------------------------------------------------------------
        void ConnectEventHandler(const int tid, ULONG_PTR args) override {
            try {
                std::shared_ptr<con> ccon;
                evconnection->CreateConnection(ccon);
                if (!ccon) return;

                if (_ServerSocket->_Type == sockettype::TCP)
                    ccon->slots[0].csock = std::make_unique<tcp>(-1);
                else if (_ServerSocket->_Type == sockettype::SSL) {
                    ssl* srv = static_cast<ssl*>(_ServerSocket);
                    // Pass the certificate bundle map (SNI will select appropriate cert per connection)
                    ccon->slots[0].csock = std::make_unique<ssl>(*srv->_tls.cert_map, -1);
                } else if (_ServerSocket->_Type == sockettype::QUIC) {
                    // QUIC: All child connections share the parent's UDP socket.
                    // accept() receives packets and either routes to existing children
                    // or creates new ones (owned by parent). We don't register QUIC
                    // children in kqueue/CONNECTIONS since they share the parent fd.
                    do {
                        std::unique_ptr<socket> quic_csock;
                        _ServerSocket->accept(quic_csock, true);
                        // Drain remaining packets from the socket
                        fd_set readfds;
                        FD_ZERO(&readfds);
                        FD_SET(_ServerSocket->fd(), &readfds);
                        timeval tv = {0, 0};
                        if (select(_ServerSocket->fd() + 1, &readfds, nullptr, nullptr, &tv) <= 0)
                            break;
                    } while (true);
                    return;
                }

                _ServerSocket->accept(ccon->slots[0].csock, true);
                if (!ccon || !ccon->slots[0].csock) return;

                ccon->slots[0].csock->setNonBlock();
                int fd = ccon->slots[0].csock->fd();

                // insert into connection map
                {
                    std::unique_lock<std::shared_mutex> lk(POLL_HANDLER_MUTEX);
                    CONNECTIONS.emplace(fd, ccon);
                }

                // Add to kqueue (EV_ONESHOT with batched re-arming)
                _batch.add_change(fd, EVFILT_READ, EV_ADD | EV_ONESHOT);

                evconnection->ConnectEvent(*ccon, tid, args);

                // If ConnectEvent queued data (e.g. HTTP/2 SETTINGS),
                // enable WRITE to flush immediately
                if (!ccon->SendData.empty() || ccon->slots[0].csock->hasPendingWrite()) {
                    _batch.add_change(fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT);
                }

                // Flush immediately so the new connection is active
                // in kqueue for all threads — matches epoll's
                // immediate epoll_ctl(EPOLL_CTL_ADD) behavior.
                _batch.flush();

            } catch (NetException& e) {
                if (e.getErrorType() == NetException::Note)
                    return;
                throw;
            }
        }

        // ------------------------------------------------------------
        // IO handling (unified for TCP/SSL/QUIC)
        // ------------------------------------------------------------
        void IoEventHandler(int fd, int events, const int tid, ULONG_PTR args) override {
            std::shared_ptr<con> c = getConByFd(fd);
            if (!c || !c->slots[0].csock) {
                return;
            }

            KqueueArmGuard rearm(fd, c, &_batch);
            bool needClose = false;
            // Pre-allocate buffer outside the lock to avoid
            // repeated 16 KB heap allocs in the inner loops.
            buffer recvBuf(BLOCKSIZE);

            {
                std::lock_guard<std::recursive_mutex> lk(c->event_mutex);
                if (!c->slots[0].csock) return;

                try {
                    // Peer closed or error?
                    if (events & (KQUEUE_EV_ERROR | KQUEUE_EV_EOF)) {
                        // EV_ERROR is always fatal
                        if (events & KQUEUE_EV_ERROR) {
                            needClose = true;
                        } else if ((events & KQUEUE_EV_EOF) && !(events & KQUEUE_EV_READ)) {
                            // EOF on write filter means peer closed
                            needClose = true;
                        }
                        // EOF on read filter: we still try to read remaining data below
                    }

                    if (!needClose) {
                        // Step 1: Buffer transport data (non-blocking).
                        // Always attempt — even on WRITE events the peer
                        // may have sent data while we were write-blocked.
                        // Avoids an extra kevent round-trip for keep-alive
                        // and TLS handshake phases.
                        bool gotRawData = false;
                        {
                            recvBuf.size = BLOCKSIZE;
                            try {
                                size_t n = c->slots[0].csock->prime_recv(recvBuf, 0);
                                if (n > 0) gotRawData = true;
                            } catch (NetException& e) {
                                if (e.getErrorType() != NetException::Note) throw;
                            }
                        }

                        // Step 2: Handshake (if not done)
                        // Keep driving the handshake state machine until:
                        // - Handshake completes, or
                        // - We need to wait for more data (Note exception)
                        while (!c->slots[0].csock->getHandshakeDone() && !needClose) {
                            // First flush any pending writes
                            if (c->slots[0].csock->hasPendingWrite()) {
                                try {
                                    c->slots[0].csock->flush_out();
                                } catch (NetException& e) {
                                    if (e.getErrorType() == NetException::Note) {
                                        // Guard auto-detects hasPendingWrite and arms WRITE
                                        return;
                                    }
                                    throw;
                                }
                            }

                            // Drive the handshake state machine
                            try {
                                c->slots[0].csock->handshake_after_accept();
                            } catch (NetException& e) {
                                if (e.getErrorType() != NetException::Note) throw;
                                // Note means need more data or write blocked - exit loop
                                break;
                            }
                        }

                        // Re-arm if handshake still in progress
                        // Guard auto-detects READ + WRITE needs
                        if (!c->slots[0].csock->getHandshakeDone())
                            return;

                        // Post-handshake: if the socket has a framing
                        // callback, invoke it with stream_id=0 to produce
                        // initial connection data (e.g. HTTP/2 SETTINGS).
                        if (auto *sslsock = dynamic_cast<ssl*>(c->slots[0].csock.get())) {
                            const auto &fcb = sslsock->getFramingCallback();
                            if (fcb && c->SendData.empty()) {
                                std::string init = fcb(sslsock->getSelectedAlpn(), 0, "", "");
                                if (!init.empty()) {
                                    c->SendData.append(init.data(), init.size());
                                }
                            }
                        }

                        // Step 3: Application data (handshake done)
                        // Drain all available TLS records to avoid stalling
                        // when multiple records arrive in a single TCP segment.
                        // Edge-triggered: drain completely before returning
                        bool hasAppData = gotRawData ||
                                          c->slots[0].csock->hasBufferedData();
                        while (hasAppData && c->slots[0].csock->getHandshakeDone() && !needClose) {
                            recvBuf.size = BLOCKSIZE;
                            size_t rcv = 0;
                            bool wouldBlock = false;

                            try {
                                rcv = c->slots[0].csock->recvData(recvBuf, 0);
                            } catch (NetException& e) {
                                if (e.getErrorType() == NetException::Note) {
                                    wouldBlock = true;
                                } else {
                                    throw;
                                }
                            }

                            if (!wouldBlock && rcv == 0) {
                                // True EOF — peer closed the connection
                                needClose = true;
                            } else if (rcv > 0) {
                                c->RecvData.append(recvBuf.data.buf, rcv);
                                evconnection->RequestEvent(*c, tid, args);
                            }

                            if (wouldBlock) {
                                // The TLS layer may hold a partial record
                                // but can't decrypt it yet.  Pull more raw
                                // TCP data so the record can be completed
                                // without waiting for another kqueue event.
                                bool gotMore = false;
                                try {
                                    recvBuf.size = BLOCKSIZE;
                                    size_t pr = c->slots[0].csock->prime_recv(recvBuf, 0);
                                    if (pr > 0) gotMore = true;
                                } catch (NetException&) {
                                    // EAGAIN / would-block — no more kernel data
                                }
                                if (!gotMore) break;
                                continue;  // retry recvData with the new raw bytes
                            }

                            // Continue only if there are more buffered records
                            if (rcv == 0 || !c->slots[0].csock->hasBufferedData())
                                break;
                        }

                        // Step 4: Flush and send
                        // Always attempt to send if there is data queued
                        if ((c->SendData.pos < c->SendData.size() || c->slots[0].csock->hasPendingWrite()) && !needClose) {
                            if (c->slots[0].csock->hasPendingWrite()) {
                                try {
                                    c->slots[0].csock->flush_out();
                                } catch (NetException& e) {
                                    if (e.getErrorType() == NetException::Note) {
                                        // Guard auto-detects hasPendingWrite and arms WRITE
                                        return;
                                    }
                                    throw;
                                }
                            }

                            // Send loop: drain all available data in one event
                            // Using pos avoids O(n) erase-from-front on every chunk
                            bool sendBlockedWouldBlock = false;
                            while (c->SendData.pos < c->SendData.size() && !sendBlockedWouldBlock && !needClose) {
                                size_t remaining = c->SendData.size() - c->SendData.pos;
                                size_t sendlen = std::min((size_t)BLOCKSIZE, remaining);
                                buffer out(c->SendData.data() + c->SendData.pos, sendlen);

                                size_t consumed = 0;
                                try {
                                    consumed = c->slots[0].csock->sendData(out, 0);
                                    // Advance pos immediately after sendData —
                                    // the plaintext is already encrypted and
                                    // queued in the TLS send_queue.  If the
                                    // flush_out below throws EAGAIN we must not
                                    // re-encrypt this chunk on the next cycle.
                                    if (consumed > 0) {
                                        c->SendData.pos += consumed;
                                        if (c->SendData.pos >= c->SendData.size()) {
                                            c->SendData.clear();
                                            c->SendData.pos = 0;
                                        } else if (c->SendData.pos > BLOCKSIZE * 4) {
                                            c->SendData.erase(c->SendData.begin(), c->SendData.begin() + c->SendData.pos);
                                            c->SendData.pos = 0;
                                        }
                                    } else {
                                        sendBlockedWouldBlock = true;
                                        break;
                                    }
                                    if (c->slots[0].csock->hasPendingWrite())
                                        c->slots[0].csock->flush_out();
                                } catch (NetException& e) {
                                    if (e.getErrorType() == NetException::Note) {
                                        sendBlockedWouldBlock = true;
                                        break;
                                    }
                                    throw;
                                }
                            }

                            // Send blocked — guard auto-detects and arms WRITE
                            if ((sendBlockedWouldBlock || c->slots[0].csock->hasPendingWrite())
                                && c->SendData.pos < c->SendData.size()) {
                                return;
                            }

                            if (c->SendData.pos >= c->SendData.size() && !c->slots[0].csock->hasPendingWrite()) {
                                c->SendData.clear();
                                c->SendData.pos = 0;
                                evconnection->ResponseEvent(*c, tid, args);
                            }
                        }
                    }

                } catch (NetException& e) {
                    if (e.getErrorType() == NetException::Note) {
                        // Guard auto-detects READ + WRITE needs
                        return;
                    }
                    needClose = true;
                } catch (...) {
                    needClose = true;
                }
            } // unlock event_mutex

            if (needClose) {
                rearm.disarm();
                CloseEventHandler(fd, tid, args);
            }
        }

        // ------------------------------------------------------------
        // Close handler (safe + no deadlock)
        // ------------------------------------------------------------
        void CloseEventHandler(int fd, const int tid, ULONG_PTR args) override {
            std::shared_ptr<con> ccon;

            // take out of map first
            {
                std::unique_lock<std::shared_mutex> lock(POLL_HANDLER_MUTEX);
                auto it = CONNECTIONS.find(fd);
                if (it == CONNECTIONS.end())
                    return;
                ccon = std::move(it->second);
                CONNECTIONS.erase(it);
            }

            if (!ccon) return;

            // now lock per connection
            {
                std::lock_guard<std::recursive_mutex> lk(ccon->event_mutex);

                // close() automatically removes all kqueue filters for
                // this fd.  Do NOT add EV_DELETE to the batch — those
                // changes would execute after close() invalidates the
                // fd, producing ERR#9 and potentially dropping later
                // batch entries for other connections.
                safeCloseSocket(ccon);
            }

            try {
                evconnection->DisconnectEvent(*ccon, tid, args);
            } catch (...) {
                // ignore user callback failures
            }
        }

    private:
        int _kqfd;
        std::unique_ptr<struct kevent[]> _Events;
        socket* _ServerSocket;
        int _EventCount;
        KeventBatch _batch;
    };

    // ------------------------------------------------------------
    // Thread worker
    // ------------------------------------------------------------
    class EventWorkerArgs {
    public:
        int timeout;
        eventapi* event;
        std::map<int,socket*> ssocket;
    };

    class EventWorker {
    public:
        EventWorker(int tid, ULONG_PTR args, EventWorkerArgs* eargs) {
            std::vector<std::thread> local_threads;

            for(auto& kv : eargs->ssocket){
                local_threads.emplace_back([&, tid, args, kv] {
                    try {
                        poll pollptr(kv.second, eargs->event, kv.first, eargs->timeout);

                        while (event::Running) {
                            try {
                                int wait = pollptr.waitEventHandler(eargs->timeout);

                                if (wait == 0) {
                                    // kevent() already blocked for the full
                                    // timeout duration — no additional sleep
                                    // needed (matches epoll behavior).
                                    continue;
                                }

                                for (int i = 0; i < wait; ++i) {
                                    int evtype = pollptr.pollState(i);

                                    if (evtype == pollapi::EVCON) {
                                        // If the listen socket reports EV_EOF
                                        // (e.g. after SIGINT shutdown()), skip
                                        // accept — the socket is being closed.
                                        int flags = pollptr.getEventFlags(i);
                                        if (flags & KQUEUE_EV_EOF)
                                            continue;
                                        pollptr.ConnectEventHandler(tid, args);
                                    } else {
                                        int fd = pollptr.getEventFd(i);
                                        int flags = pollptr.getEventFlags(i);
                                        pollptr.IoEventHandler(fd, flags, tid, args);
                                    }

                                    // Flush re-arms immediately after each
                                    // event so other threads (and this
                                    // thread's next kevent) see the filter
                                    // right away — matches epoll's per-event
                                    // epoll_ctl(MOD) latency.
                                    pollptr.flushPendingChanges();
                                }
                            } catch (NetException& e) {
                                std::cerr << "Poll Error: " << e.what() << std::endl;
                                usleep(10000);  // 10ms sleep on error to reduce CPU thrashing
                            }
                        }
                    } catch (NetException& e) {
                        std::cerr << "Worker thread " << tid << " exited with exception: " << e.what() << std::endl;
                    } catch (...) {
                        std::cerr << "Worker thread " << tid << " exited with unknown exception." << std::endl;
                    }
                });
            }

            for (auto& t : local_threads)
                t.join();
        }
    };

    struct ThreadHelperArgs {
        EventWorkerArgs* eargs;
        ULONG_PTR args;
        int tid;
    };

    static void* worker_wrapper(void* data) {
        ThreadHelperArgs* helper_args = reinterpret_cast<ThreadHelperArgs*>(data);

        try {
            EventWorker(helper_args->tid, helper_args->args, helper_args->eargs);
        } catch (NetException& e) {
            std::cerr << "Thread " << helper_args->tid << " exited with exception: " << e.what() << std::endl;
        } catch (...) {
            std::cerr << "Thread " << helper_args->tid << " exited with unknown exception." << std::endl;
        }

        delete helper_args;
        return nullptr;
    }

    // ------------------------------------------------------------
    // Signal handler — MUST be async-signal-safe.
    //
    // Acquiring mutexes here deadlocks if the signal is
    // delivered to a thread that already holds the lock.
    // Only set the atomic flag; cleanup happens in
    // runEventloop() after all threads have joined.
    // ------------------------------------------------------------
    static void signalHandler(int signum) {
        if (signum != SIGINT)
            return;
        event::Running = false;
    }

    // Called from runEventloop() AFTER all worker threads have
    // joined — safe to acquire any lock.
    static void cleanupConnections() {
        std::vector<socket*> servers;
        std::vector<std::shared_ptr<con>> conns;

        {
            std::unique_lock<std::shared_mutex> lk(POLL_HANDLER_MUTEX);

            servers = SSOCKETS;
            for (auto& kv : CONNECTIONS)
                conns.push_back(kv.second);

            CONNECTIONS.clear();
        }

        for (auto* s : servers) {
            try { s->close(); } catch (...) {}
        }

        for (auto& c : conns) {
            if (!c) continue;
            std::lock_guard<std::recursive_mutex> lk(c->event_mutex);
            safeCloseSocket(c);
        }
    }

} // namespace netplus

// ============================================================
// event class implementation (outside namespace)
// ============================================================
netplus::event::event(std::vector<netplus::socket*> serversockets, int timeout)
    : _ServerSockets(serversockets), _Timeout(timeout)
{
    if (_ServerSockets.empty()) {
        NetException e;
        e[NetException::Critical] << "server socket empty!";
        throw e;
    }

    for(auto* s : _ServerSockets) {
        if (!s) {
            continue;
        }
        s->bind();
        s->setNonBlock();
        s->listen();
        s->setTimeout(timeout);
    }

    threads = sysconf(_SC_NPROCESSORS_ONLN);
}

netplus::event::event(const netplus::event& src) : _ServerSockets(src._ServerSockets), _Timeout(src._Timeout) {}
netplus::event::~event() {}

void netplus::event::runEventloop(ULONG_PTR args) {
    signal(SIGINT, netplus::signalHandler);
    signal(SIGPIPE, SIG_IGN);

    netplus::EventWorkerArgs eargs;

MAINWORKERLOOP:
    for(auto ss : _ServerSockets) {
        _pollFD = kqueue();
        if (_pollFD < 0) {
            NetException e;
            e[NetException::Critical] << "runEventloop: can't create kqueue";
            throw e;
        }

        struct kevent setevent;
        EV_SET(&setevent, ss->fd(), EVFILT_READ, EV_ADD, 0, 0, nullptr);

        if (kevent(_pollFD, &setevent, 1, nullptr, 0, nullptr) < 0) {
            NetException e;
            e[NetException::Critical] << "runEventloop: kevent ADD server failed";
            throw e;
        }

        eargs.ssocket[_pollFD] = ss;
    }

    eargs.timeout = _Timeout;
    eargs.event = this;

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, THREAD_STACK_SIZE);

    std::vector<pthread_t> threadpool;
    threadpool.reserve(threads);

    for (int i = 0; i < threads; ++i) {
        ThreadHelperArgs* helper_args = new ThreadHelperArgs{ &eargs, args, i };

        pthread_t tid;
        if (pthread_create(&tid, &attr, netplus::worker_wrapper, helper_args) != 0) {
            delete helper_args;
            NetException e;
            e[NetException::Critical] << "runEventloop: pthread_create failed";
            throw e;
        }
        threadpool.push_back(tid);
    }

    pthread_attr_destroy(&attr);

    for (auto& t : threadpool)
        pthread_join(t, nullptr);

    // All worker threads have exited — safe to clean up.
    netplus::cleanupConnections();

    // Close all kqueue FDs (not just the last one)
    for (auto& kv : eargs.ssocket)
        close(kv.first);
    eargs.ssocket.clear();

    if (netplus::event::Restart) {
        netplus::event::Restart = false;
        goto MAINWORKERLOOP;
    }
}

// ================================================================
// socketwait – kqueue-based implementation
// ================================================================
netplus::socketwait::socketwait() {
    _handle = ::kqueue();
    if (_handle < 0) {
        NetException e;
        e[NetException::Error] << "socketwait: kqueue() failed: " << strerror(errno);
        throw e;
    }
}

netplus::socketwait::~socketwait() {
    if (_handle >= 0) ::close(_handle);
}

bool netplus::socketwait::waitRead(socket &sock, int timeout_ms) {
    SOCKET fd = sock.fd();
    struct kevent change;
    EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, nullptr);
    struct kevent out;
    struct timespec ts_buf;
    struct timespec* tsp = nullptr;
    if (timeout_ms >= 0) {
        ts_buf.tv_sec  = timeout_ms / 1000;
        ts_buf.tv_nsec = (timeout_ms % 1000) * 1000000L;
        tsp = &ts_buf;
    }
    int r = ::kevent(_handle, &change, 1, &out, 1, tsp);
    return r > 0;
}

bool netplus::socketwait::waitWrite(socket &sock, int timeout_ms) {
    SOCKET fd = sock.fd();
    struct kevent change;
    EV_SET(&change, fd, EVFILT_WRITE, EV_ADD | EV_ONESHOT, 0, 0, nullptr);
    struct kevent out;
    struct timespec ts_buf;
    struct timespec* tsp = nullptr;
    if (timeout_ms >= 0) {
        ts_buf.tv_sec  = timeout_ms / 1000;
        ts_buf.tv_nsec = (timeout_ms % 1000) * 1000000L;
        tsp = &ts_buf;
    }
    int r = ::kevent(_handle, &change, 1, &out, 1, tsp);
    return r > 0;
}

bool netplus::socketwait::isDisconnected(socket &sock) {
    SOCKET fd = sock.fd();
    struct kevent change;
    EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, nullptr);
    struct kevent out;
    struct timespec ts = {0, 0};
    int r = ::kevent(_handle, &change, 1, &out, 1, &ts);
    if (r > 0 && (out.flags & EV_EOF)) return true;
    return false;
}
