#ifndef LIBTORRENT_TORRENT_RUNTIME_SOCKET_MANAGER_H
#define LIBTORRENT_TORRENT_RUNTIME_SOCKET_MANAGER_H

#include <array>
#include <atomic>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <torrent/common.h>

// A socket that is closed by the kernel while neitehr ready nor write polling will be considered
// uninterested by the kernel, and possibly reused.
//
// Those sockets must be marked as 'inactive' in the socket manager, and when entering active state
// it must check the socket manager if the socket is still valid.
//
// File and other socket types that do not unexpectedly get closed by the kernel do not need to be
// managed.

namespace torrent::runtime {

SocketManager* socket_manager() LIBTORRENT_EXPORT;

// Categories of allocated sockets within the global pool.
//
// When a category's max is non-zero, open operations additionally check that the category's
// current usage has not reached its limit.
enum class socket_manager_category_t : uint32_t {
  category_generic,   // peer connections, uncategorized
  category_http,      // HTTP/curl
  category_internal,  // DHT, UDP tracker, thread interrupt, BT listen
  category_rpc,       // SCGI/RPC
  category_files      // open files (mmap, etc.)
};

using enum socket_manager_category_t;

struct SocketInfo {
  // TODO: Replace with Event*, and add thread to PollEvent?
  // TODO: Event should contain the owning thread, and not be included here.
  // TODO: Fd not needed here.

  int               fd{-1};
  system::Event*    event{};
  system::Thread*   thread{};
  int               flags{};

  socket_manager_category_t category{};
};

class LIBTORRENT_EXPORT SocketManager {
public:
  static constexpr uint32_t category_count     = 5;
  static constexpr uint32_t category_max_alloc = 1000000;
  static constexpr int      flag_inactive = (1 << 0);

  using category_t = socket_manager_category_t;

  uint32_t            size();
  uint32_t            max_size();

  uint32_t            category_managed_size(category_t category);
  uint32_t            category_max_size(category_t category);

  uint32_t            category_min_allocation(category_t category);
  uint32_t            category_max_allocation(category_t category);

  void                set_category_min_allocation(category_t category, uint32_t min_alloc);
  void                set_category_max_allocation(category_t category, uint32_t max_alloc);

  void                set_max_size_and_adjust(uint32_t max_open);

  void                adjust_allocation();

  void                add_unmanaged_socket();
  void                remove_unmanaged_socket();

  bool                can_open_socket(category_t category);

  // TODO: Rename / change _or_throw to be more specific about what we throw, as it currently calls
  // internal errors. DHT server should probably throw resource_error instead? Or retry. (this
  // however hides errors for normal users)
  //
  // TODO: Finish implementation of reuse logic.

  // To avoid reuse race conditions, these calls must also add the Event to read/write polling.

  // Throw internal_error on conflicts, as the caller isn't expecting conflicts.
  void                open_event_or_throw(system::Event* event, category_t category, std::function<void ()> func);

  // Try to reuse existing socket, if that fails call cleanup.
  //
  // This is used for listen accept and other cases where the caller doesn't mind ignoring failures.
  bool                open_event_or_cleanup(system::Event* event, category_t category, std::function<void ()> func, std::function<void (bool)> cleanup);

  void                close_event_or_throw(system::Event* event, std::function<void ()> func);

  // Event already opened the socket, just register it.
  void                register_event_or_throw(system::Event* event, category_t category, std::function<void ()> func);
  void                unregister_event_or_throw(system::Event* event, std::function<void ()> func);

  // The func must close event_from and the pointer must remain valid.
  system::Event*      transfer_event(system::Event* event_from, std::function<system::Event* ()> func);

  bool                execute_if_not_present(int fd, std::function<void ()> func);

  [[nodiscard]] bool  mark_event_active_or_fail(system::Event* event);
  void                mark_event_inactive(system::Event* event, std::function<void ()> func);
  [[nodiscard]] bool  mark_stream_event_inactive(system::Event* event, std::function<void ()> func, std::function<void ()> on_reuse);

  // The lock is held while the callback is called, so use Thread::callback().
  void                subscribe_to_changes(void* target, const std::function<void()>& callback);
  void                unsubscribe_from_changes(void* target);

protected:
  friend class torrent::RuntimeManager;

  SocketManager();
  ~SocketManager();

  auto                lock_guard() { return std::lock_guard(m_mutex); }

private:
  using socket_map       = std::unordered_map<int, SocketInfo>;
  using category_list    = std::array<std::atomic<uint32_t>, category_count>;
  using subscriber_list  = std::vector<std::pair<void*, std::function<void()>>>;

  auto&               managed_size_unsafe(category_t category);
  auto&               max_size_unsafe(category_t category);

  void                adjust_allocation_unsafe(uint32_t max_open);

  void                notify_changes_unsafe() const;

  void                account_new_socket_unsafe(socket_map::iterator itr, category_t category);
  void                account_remove_socket_unsafe(socket_map::iterator itr);

  bool                can_open_socket_unsafe(category_t category);

  bool                handle_reused_socket(socket_map::iterator itr);

  std::atomic<uint32_t> m_managed_size{};
  std::atomic<uint32_t> m_unmanaged_size{};
  std::atomic<uint32_t> m_max_size{};

  category_list         m_category_managed_size{};
  category_list         m_category_max_size{};

  category_list         m_category_min_alloc{};
  category_list         m_category_max_alloc{};

  align_cacheline std::mutex m_mutex;

  subscriber_list     m_change_subscribers;

  socket_map          m_socket_map;
};

inline auto& SocketManager::managed_size_unsafe(category_t category) { return m_category_managed_size[static_cast<uint32_t>(category)]; }
inline auto& SocketManager::max_size_unsafe(category_t category)     { return m_category_max_size[static_cast<uint32_t>(category)]; }


} // namespace torrent::runtime

#endif // LIBTORRENT_TORRENT_RUNTIME_SOCKET_MANAGER_H
