/*******************************************************************************
* Copyright (c) 2026, 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.
*******************************************************************************/

#pragma once

#include <vector>
#include <deque>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <chrono>

namespace netplus {

// Minimal fixed-size worker pool for offloading tasks that don't belong on
// whatever thread submits them (e.g. a caller-supplied callback that may
// block for a while, run from code that also needs its thread free for
// other work). Not a general scheduler: no priorities, no per-task futures,
// no work-stealing — just a shared FIFO queue drained by a fixed set of
// worker threads. Tasks are run in submission order across the pool, but
// nothing here orders tasks *relative to each other* beyond that — callers
// needing per-key ordering (e.g. "this connection's callbacks must run in
// sequence") need their own serialization on top, since two tasks for the
// same key could otherwise be picked up by two different idle workers at
// once.
class ThreadPool {
public:
    explicit ThreadPool(size_t num_threads) {
        _workers.reserve(num_threads);
        for (size_t i = 0; i < num_threads; ++i) {
            _workers.emplace_back([this] { workerLoop(); });
        }
    }

    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lk(_mtx);
            _stopping = true;
        }
        _cv.notify_all();

        // A worker can be stuck inside a submitted task that never returns
        // (e.g. a callback blocked on an unresponsive network peer with no
        // timeout). A plain join() would then hang process shutdown
        // forever, since this pool is a process-lifetime singleton
        // (quicDispatchPool()) destructed from global teardown. Give
        // workers a bounded grace period to drain; past that, detach the
        // stragglers so the process can still exit — the OS reclaims a
        // detached thread when the process terminates.
        static constexpr auto SHUTDOWN_GRACE = std::chrono::seconds(5);
        {
            std::unique_lock<std::mutex> lk(_mtx);
            _done_cv.wait_for(lk, SHUTDOWN_GRACE,
                               [this] { return _finished >= _workers.size(); });
        }

        bool all_finished = _finished >= _workers.size();
        for (auto& t : _workers) {
            if (!t.joinable()) continue;
            if (all_finished) t.join();
            else t.detach();
        }
    }

    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

    // Queues a task for execution on some worker thread. Safe to call from
    // any thread, including from within a task running on this same pool.
    void submit(std::function<void()> task) {
        {
            std::lock_guard<std::mutex> lk(_mtx);
            _tasks.push_back(std::move(task));
        }
        _cv.notify_one();
    }

private:
    void workerLoop() {
        for (;;) {
            std::function<void()> task;
            {
                std::unique_lock<std::mutex> lk(_mtx);
                _cv.wait(lk, [this] { return _stopping || !_tasks.empty(); });
                if (_tasks.empty()) {
                    // Only reachable via _stopping — drain fully before exiting
                    // (queued work still runs; shutdown just stops accepting
                    // the wait once there's nothing left).
                    ++_finished;
                    lk.unlock();
                    _done_cv.notify_all();
                    return;
                }
                task = std::move(_tasks.front());
                _tasks.pop_front();
            }
            // A task throwing must not crash the whole pool (and, since
            // nothing else on this thread's call stack will catch it,
            // the whole process) — matches the epoll worker loop's own
            // per-event try/catch (src/event/epoll.cpp), which is what
            // used to protect a callback like this before it ran here.
            try {
                task();
            } catch (...) {
            }
        }
    }

    std::vector<std::thread> _workers;
    std::deque<std::function<void()>> _tasks;
    std::mutex _mtx;
    std::condition_variable _cv;
    bool _stopping = false;

    // Shutdown-hang guard: counts workers that have returned from
    // workerLoop(), so the destructor can bound how long it waits before
    // giving up on a worker stuck in a never-returning task.
    size_t _finished = 0;
    std::condition_variable _done_cv;
};

} // namespace netplus
