#pragma once
#include "esphome/core/defines.h"

#ifdef USE_SOCKET_IMPL_LWIP_TCP

#include <array>
#include <cerrno>
#include <cstring>
#include <memory>
#include <span>

#include "esphome/core/helpers.h"
#include "headers.h"
#include "lwip/ip.h"
#include "lwip/netif.h"
#include "lwip/opt.h"
#include "lwip/tcp.h"

namespace esphome::socket {

// Forward declaration
class LWIPRawImpl;

/// Non-virtual common base for LWIP raw TCP sockets.
/// Provides shared fields and methods for both connected and listening sockets.
/// No virtual methods — pure code sharing.
class LWIPRawCommon {
 public:
  LWIPRawCommon(sa_family_t family, struct tcp_pcb *pcb) : pcb_(pcb), family_(family) {}
  ~LWIPRawCommon();
  LWIPRawCommon(const LWIPRawCommon &) = delete;
  LWIPRawCommon &operator=(const LWIPRawCommon &) = delete;

  int bind(const struct sockaddr *name, socklen_t addrlen);
  int close();
  int shutdown(int how);

  int getpeername(struct sockaddr *name, socklen_t *addrlen);
  int getsockname(struct sockaddr *name, socklen_t *addrlen);

  /// Format peer address into a fixed-size buffer (no heap allocation)
  size_t getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf);
  /// Format local address into a fixed-size buffer (no heap allocation)
  size_t getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf);

  int getsockopt(int level, int optname, void *optval, socklen_t *optlen);
  int setsockopt(int level, int optname, const void *optval, socklen_t optlen);

  int get_fd() const { return -1; }

 protected:
  int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen);

  // Member ordering optimized to minimize padding on 32-bit systems
  struct tcp_pcb *pcb_;
  // don't use lwip nodelay flag, it sometimes causes reconnect
  // instead use it for determining whether to call lwip_output
  bool nodelay_ = false;
  sa_family_t family_ = 0;
  uint8_t recv_timeout_cs_ = 0;  // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
};

/// Connected socket implementation for LWIP raw TCP.
/// No virtual methods — callers always use the concrete type.
class LWIPRawImpl : public LWIPRawCommon {
 public:
  using LWIPRawCommon::LWIPRawCommon;
  ~LWIPRawImpl();

  void init(struct pbuf *initial_rx = nullptr, bool initial_rx_closed = false);

  // Non-listening sockets return error
  std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *, socklen_t *) {
    errno = EINVAL;
    return nullptr;
  }
  std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
    return this->accept(addr, addrlen);
  }
  // Regular sockets can't be converted to listening - this shouldn't happen
  // as listen() should only be called on sockets created for listening
  int listen(int) {
    errno = EOPNOTSUPP;
    return -1;
  }
  ssize_t read(void *buf, size_t len);
  ssize_t readv(const struct iovec *iov, int iovcnt);
  ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t write(const void *buf, size_t len);
  ssize_t writev(const struct iovec *iov, int iovcnt);
  ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
    // return ::sendto(fd_, buf, len, flags, to, tolen);
    errno = ENOSYS;
    return -1;
  }
  // Check if the socket has buffered data ready to read.
  // See the ready() contract in socket.h — callers must drain or track remaining data.
  // Intentionally unlocked — this is a polling check called every loop iteration.
  // A stale read at worst delays processing by one loop tick; the actual I/O in
  // read() holds the lwip lock and re-checks properly. See esphome#10681.
  bool ready() const { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }

  // No lock needed — only called during setup before callbacks are registered.
  // A stale pcb_ read is benign (returns ECONNRESET, which the caller handles).
  int setblocking(bool blocking) {
    if (this->pcb_ == nullptr) {
      errno = ECONNRESET;
      return -1;
    }
    // Raw TCP doesn't use a blocking flag directly. Blocking behavior
    // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay().
    return 0;
  }
  int loop() { return 0; }

  err_t recv_fn(struct pbuf *pb, err_t err);

  static void s_err_fn(void *arg, err_t err);
  static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);

 protected:
  // True when the socket could receive data but none has arrived yet.
  // Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
  // all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
  // an unnecessary wait (stale true) or skips it (stale false), and the
  // authoritative recheck happens under LWIP_LOCK afterward.
  bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
  void wait_for_data_();
  ssize_t read_locked_(void *buf, size_t len);
  ssize_t internal_write_(const void *buf, size_t len);
  int internal_output_();

  pbuf *rx_buf_ = nullptr;
  size_t rx_buf_offset_ = 0;
  bool rx_closed_ = false;
};

/// Listening socket implementation for LWIP raw TCP.
/// Separate from LWIPRawImpl — no virtual dispatch needed.
class LWIPRawListenImpl : public LWIPRawCommon {
 public:
  using LWIPRawCommon::LWIPRawCommon;
  ~LWIPRawListenImpl();

  void init();

  // Intentionally unlocked — polling check, see LWIPRawImpl::ready() comment.
  bool ready() const { return this->accepted_socket_count_ > 0; }

  std::unique_ptr<LWIPRawImpl> accept(struct sockaddr *addr, socklen_t *addrlen);
  std::unique_ptr<LWIPRawImpl> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
    return this->accept(addr, addrlen);
  }
  int listen(int backlog);

  // Listening sockets don't do I/O
  ssize_t read(void *, size_t) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t write(const void *, size_t) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t readv(const struct iovec *, int) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t writev(const struct iovec *, int) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t recvfrom(void *, size_t, sockaddr *, socklen_t *) {
    errno = ENOTSUP;
    return -1;
  }
  ssize_t sendto(const void *, size_t, int, const struct sockaddr *, socklen_t) {
    errno = ENOTSUP;
    return -1;
  }
  int setblocking(bool) { return 0; }
  int loop() { return 0; }

  static void s_err_fn(void *arg, err_t err);

 private:
  err_t accept_fn_(struct tcp_pcb *newpcb, err_t err);
  static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err);

  // Temporary callbacks for queued PCBs (between accept_fn_ and accept())
  static void s_queued_err_fn(void *arg, err_t err);
  static err_t s_queued_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);

  // Accept queue entry — stores a raw tcp_pcb and any data received while queued.
  // lwip's default tcp_recv_null handler drops data and ACKs it, so we must register
  // a temporary recv callback to buffer any data that arrives between accept_fn_
  // (which stores the PCB) and accept() (which creates the LWIPRawImpl).
  struct QueuedPcb {
    struct tcp_pcb *pcb{nullptr};
    struct pbuf *rx_buf{nullptr};  // Data received while queued (before accept() picks it up)
    bool rx_closed{false};         // Remote sent FIN while queued
  };

  // Accept queue — stores raw tcp_pcb entries instead of heap-allocated LWIPRawImpl objects.
  // LWIPRawImpl creation is deferred to the main-loop accept() call. This avoids:
  // - Heap allocation in the accept callback (unsafe from IRQ context on RP2040)
  // - Dangling LWIPRawImpl if the connection errors before accept() picks it up
  // 2 slots is plenty since the main loop drains the queue every iteration.
  static constexpr size_t MAX_ACCEPTED_SOCKETS = 2;
  std::array<QueuedPcb, MAX_ACCEPTED_SOCKETS> accepted_pcbs_{};
  uint8_t accepted_socket_count_ = 0;  // Number of entries currently in queue
};

}  // namespace esphome::socket

#endif  // USE_SOCKET_IMPL_LWIP_TCP
