// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest

/** \file */

#pragma once

#include <cstdint>
#include <string>
#include <vector>

#include <everest/io/event/unique_fd.hpp>
#include <everest/io/udp/endpoint.hpp>

namespace everest::lib::io::socket {

/**
 * @brief Open a UDP socket in server mode
 * @param[in] port The port to listen to
 * @param[in] device Optional interface name (e.g. "eth0"). When non-empty the socket is bound
 * to that device via SO_BINDTODEVICE. Requires CAP_NET_RAW or root.
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_udp_server_socket(std::uint16_t port, std::string const& device = {});

/**
 * @brief Open a UDP socket in client mode
 * @param[in] host The host to connect to
 * @param[in] port The port to listen to
 * @param[in] device Optional interface name (e.g. "eth0"). When non-empty the socket is bound
 * to that device via SO_BINDTODEVICE before connect. Requires CAP_NET_RAW or root.
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_udp_client_socket(std::string const& host, std::uint16_t port, std::string const& device = {});

/**
 * @brief Open an unconnected UDP datagram socket (IPv4 or IPv6) for a target.
 * @details Socket family is taken from @p target. SOCK_DGRAM, non-blocking,
 * bound to the family wildcard address on an ephemeral port. For a multicast
 * @p target the multicast egress interface is pinned (IP_MULTICAST_IF /
 * IPV6_MULTICAST_IF) to @p iface (falling back to the interface carried by
 * @p target); SO_BINDTODEVICE is applied best-effort for extra link
 * confinement (tolerated to fail unprivileged). Performs neither ::connect()
 * nor any multicast group join, so a unicast reply from an address other than
 * the configured target is delivered on this same fd by destination address
 * and port.
 * @param[in] target Destination address; selects the socket family.
 * @param[in] iface Optional interface name. Empty uses @p target's iface hint.
 * @return The managed file descriptor of the socket.
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_udp_unconnected_socket(udp::endpoint const& target, std::string const& iface = {});

/**
 * @brief Open a dual-stack (IPv6 + IPv4-mapped) UDP server socket.
 * @details AF_INET6/SOCK_DGRAM, IPV6_V6ONLY=0, non-blocking, SO_REUSEADDR,
 * bound to [::]:@p port so it receives both native IPv6 and IPv4-mapped
 * traffic. @p device (optional) is applied best-effort via
 * bind_socket_to_device (SO_BINDTODEVICE → IPV6_UNICAST_IF); the wildcard
 * bind is always kept. If IPv6 is unavailable (socket(AF_INET6) fails with
 * EAFNOSUPPORT) it falls back to AF_INET bound to 0.0.0.0:@p port.
 * @param[in] port Local UDP port; 0 picks an ephemeral port.
 * @param[in] device Optional interface name. Empty = no binding.
 * @return The managed file descriptor of the socket.
 * @throws std::runtime_error on any failure other than the EAFNOSUPPORT
 * v6→v4 fallback.
 */
event::unique_fd open_udp_dualstack_server_socket(std::uint16_t port, std::string const& device = {});

/**
 * @brief Open a UDP socket with <a href="https://man7.org/linux/man-pages/man7/ip.7.html">multicast</a>
 * enabled
 * @details Description
 * @param[in] multicast_group The multicast group to join
 * @param[in] port Port to listen to.
 * @param[in] interface_address The IP address of the interface
 * @param[in] listen_address IP adress to listen for incoming traffic
 * @param[in] reuse_address 'True' if reuse of address is required
 * @param[in] reuse_port 'True' if reuse of port is required
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_udp_multicast_socket(std::string const& multicast_group, std::uint16_t port,
                                           std::string interface_address, std::string listen_address,
                                           bool reuse_address, bool reuse_port);

/**
 * @brief Open a UDP socket for
 * <a href="https://datatracker.ietf.org/doc/html/rfc6762">Multicast DNS</a>
 * @details This creates a UDP socket for mDNS discovery. The socket is bound
 * to the specified interface and messages are only send on that interface.
 * @param[in] interface_name The name of interface
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_mdns_socket(std::string const& interface_name);

/**
 * @brief Open a TCP socket in client mode
 * @param[in] host The host to connect to
 * @param[in] port The port to listen to
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_tcp_socket(const std::string& host, std::uint16_t port, const std::string& device = {});

/**
 * @brief Open a TCP socket in client mode
 * @param[in] host The host to connect to
 * @param[in] port The port to listen to.
 * @param[in] timeout_ms Timeout for the operation in ms
 * @param[in] device Optional interface name (e.g. "eth0"). When non-empty the socket is bound
 * to that device via SO_BINDTODEVICE before connect. If the caller lacks CAP_NET_RAW, falls back
 * to source-IP bind using the interface's IPv4 address (no privilege needed).
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_tcp_socket_with_timeout(const std::string& host, std::uint16_t port, unsigned int timeout_ms,
                                              const std::string& device = {});

/**
 * @brief Bind a socket to a specific network interface.
 * @details Tries SO_BINDTODEVICE first (needs CAP_NET_RAW). On EPERM/EACCES, falls back to:
 *   - IP_UNICAST_IF (AF_INET) or IPV6_UNICAST_IF (AF_INET6) to restrict the outgoing
 *     interface for unicast packets — no privilege required, works for both v4 and v6
 *     client sockets.
 *   - As a last resort for AF_INET, a source-IP bind() to the interface's IPv4 address.
 * The IPV6 source-IP fallback is not implemented; IPv6 sockets must succeed via
 * SO_BINDTODEVICE or IPV6_UNICAST_IF. No-op when @p device is empty.
 * @param[in] fd Open socket file descriptor
 * @param[in] device Interface name (e.g. "eth0"). Empty string is a no-op.
 * @throws std::runtime_error if all viable strategies fail.
 */
void bind_socket_to_device(int fd, std::string const& device);

#ifndef EVEREST_NO_PACKET_IGNORE_OUTGOING
/**
 * @brief Open a raw socket in promiscuous mode
 * @param[in] if_name The name of the interface
 * @return The managed file descriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_raw_promiscuous_socket(std::string const& if_name);
#endif

/**
 * @brief Enable <a href="https://man7.org/linux/man-pages/man7/tcp.7.html">TCP_NODELAY</a> on a socket
 * @details   If set, disable the Nagle algorithm.  This means that
 *            segments are always sent as soon as possible, even if there
 *            is only a small amount of data.  When not set, data is
 *            buffered until there is a sufficient amount to send out,
 *            thereby avoiding the frequent sending of small packets,
 *            which results in poor utilization of the network.  This
 *            option is overridden by TCP_CORK; however, setting this
 *            option forces an explicit flush of pending output, even if
 *            TCP_CORK is currently set.
 * @param[in] fd Filedescriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
void enable_tcp_no_delay(int fd);

/**
 * @brief Set the port <a href="https://man7.org/linux/man-pages/man3/errno.3.html">non blocking</a>
 * @details Operations on this socket will not block but instead return an error EAGAIN or EWOULDBLOCK
 * @param[in] fd Filedescriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
void set_non_blocking(int fd);

/**
 * @brief Set the <a href="https://man7.org/linux/man-pages/man7/socket.7.html">keepalive</a> option.
 * @details   Enable sending of keep-alive messages on connection-oriented sockets.
 * @param[in] fd Description
 * @param[in] enable enabled if 'true'
 * @throws std::runtime_error if the operation fails.
 */
void set_keepalive(int fd, bool enable);

/**
 * @brief Set the parameters for <a href="https://man7.org/linux/man-pages/man7/tcp.7.html">TCP keepalive</a>
 * @details This implicitly calls @p set_keepalive
 * @param[in] fd Description
 * @param[in] count  The maximum number of keepalive probes TCP should send
 *                   before dropping the connection.
 * @param[in] idle_s The time (in seconds) the connection needs to remain idle
 *                   before TCP starts sending keepalive probes.
 * @param[in] intval_s The time (in seconds) between individual keepalive probes.
 * @throws std::runtime_error if the operation fails.
 */
void set_tcp_keepalive(int fd, uint32_t count, uint32_t idle_s, uint32_t intval_s);

/**
 * @brief Sets the maximum transmission timeout befor error
 * @details
 * Increasing user timeouts allows a TCP connection to survive
 * extended periods without end-to-end connectivity.
 * Decreasing user timeouts allows applications to "fail
 * fast", if so desired.  Otherwise, failure may take up to 20
 * minutes with the current system defaults in a normal WAN environment.
 * @param[in] fd Description
 * @param[in] to_ms timeout in milliseconds
 * @throws std::runtime_error if the operation fails.
 */
void set_tcp_user_timeout(int fd, uint32_t to_ms);

/**
 * @brief Set socket send buffer to minimum value
 * @details
 * This transforms ENOBUFS to EAGAIN and allows to wait for
 * the socket with select/poll/epoll to be really ready to write
 * This is especially helpfull for
 * <a href="https://rtime.felk.cvut.cz/can/socketcan-qdisc-final.pdf">socket_can</a>.
 * @param[in] fd Filedescriptor of the socket
 * @throws std::runtime_error if the operation fails.
 */
void set_socket_send_buffer_to_min(int fd);

/**
 * @brief Get the pending errors from a socket
 * @details Gets <a href="https://man7.org/linux/man-pages/man7/socket.7.html">SO_ERROR</a>
 * with <a href="https://man7.org/linux/man-pages/man2/getsockopt.2.html">getsockopt</a>
 * @param[in] fd Description
 * @return Description
 */
int get_pending_error(int fd);

/**
 * @brief Checks if a TCP socket is still connected and operational without consuming any incoming data.
 *
 * @details By peeking into the socket's receive buffer the connection status is determinded.
 * The function is guaranteed to be non blocking and not removing any data from the buffer
 * by using `MSG_PEEK` and `MSG_DONTWAIT` flags.
 * @param fd The file descriptor of the TCP socket to check.
 * @return `true` if the socket is considered alive, `false` otherwise.
 */
bool is_tcp_socket_alive(int fd);

/**
 * @brief Brings a network device up (activates it).
 * @param sock_fd The file descriptor of a control socket (AF_INET, SOCK_DGRAM).
 * @param dev_name The name of the network device (e.g., "tap0", "eth0").
 * @throws std::runtime_error if the operation fails.
 */
void bring_device_up(int sock_fd, std::string const& dev_name);

/**
 * @brief Sets the IP address for a network device.
 * @param sock_fd The file descriptor of a control socket (AF_INET, SOCK_DGRAM).
 * @param dev_name The name of the network device.
 * @param ip_address_str The IP address string (e.g., "192.168.1.1").
 * @throws std::runtime_error if the operation fails or IP address is invalid.
 */
void set_ip_address(int sock_fd, std::string const& dev_name, std::string const& ip_address_str);

/**
 * @brief Sets the netmask for a network device.
 * @param sock_fd The file descriptor of a control socket (AF_INET, SOCK_DGRAM).
 * @param dev_name The name of the network device.
 * @param netmask_str The netmask string (e.g., "255.255.255.0").
 * @throws std::runtime_error if the operation fails or netmask is invalid.
 */
void set_netmask(int sock_fd, std::string const& dev_name, std::string const& netmask_str);

/**
 * @brief Sets the MTU (Maximum Transmission Unit) for a network device.
 * @param sock_fd The file descriptor of a control socket (AF_INET, SOCK_DGRAM).
 * @param dev_name The name of the network device.
 * @param mtu The MTU value to set.
 * @throws std::runtime_error if the operation fails.
 */
void set_mtu(int sock_fd, std::string const& dev_name, int mtu);

/**
 * @brief Creates a new TAP device with a *specific* desired name.
 * This function opens /dev/net/tun and performs the TUNSETIFF ioctl.
 * It will fail if the desired_device_name is empty or already in use.
 *
 * @param desired_device_name The exact name for the TAP device (e.g., "mytap0").
 * Must NOT be empty.
 * @return The managed filedescriptor for the device.
 * @throws std::runtime_error if operation fails or if desired_device_name is empty.
 */
event::unique_fd create_tap_device(std::string const& desired_device_name);

/**
 * @brief Opens a control socket for network interface configuration.
 * This socket is used to issue SIOC* ioctl commands.
 * @return An event::unique_fd for the opened control socket.
 * @throws std::runtime_error if the operation fails.
 */
event::unique_fd open_control_socket();

/**
 * @brief Configures a previously created TAP device with IP, netmask, MTU, and non-blocking mode.
 * This function now internally creates and manages the control socket.
 *
 * @param tap_fd The file descriptor of the TAP device.
 * @param dev_name The actual name of the TAP device.
 * @param ip The IP address to assign (e.g., "192.168.1.1").
 * @param netmask The netmask to assign (e.g., "255.255.255.0").
 * @param mtu The MTU value to set (defaults to 1500).
 * @return bool on success, false otherwise
 */
bool configure_tap_device_properties(int tap_fd, std::string const& dev_name, std::string const& ip,
                                     std::string const& netmask, int mtu = 1500);

/**
 * @brief Get the flags of a socket
 * @param[in] fd The file descriptor of a socket
 * @return The flags of the socket
 * @throws std::runtime_error if the flags cannot be read
 */
int get_socket_flags(int fd);

/**
 * @brief Set the flags of a socket
 * @param[in] fd The file descirptor of a socket
 * @param[in] flags The flags to be set
 * @throws std::runtime_error if the flags cannot be set
 */
void set_socket_flags(int fd, int flags);

/**
 * @brief <a href="https://man7.org/linux/man-pages/man2/poll.2.html">poll</a> the socket until timeout
 * @details No special care is taken in the error case to check if poll was interrupted.
 * @param[in] fd The file descirptor of a socket
 * @param[in] timeout_ms The timeout for poll
 */
int poll_for_timeout_once(int fd, uint32_t timeout_ms);

/**
 * @brief <a href="https://man7.org/linux/man-pages/man2/poll.2.html">poll</a> the socket until timeout
 * @details The function checks in the error case if poll was interrupted. If it was, poll is called again with the
 * remaining portion of the timeout
 * @param[in] fd The file descirptor of a socket
 * @param[in] timeout_ms The timeout for poll
 */
int poll_until_timeout(int fd, uint32_t timeout_ms);

/**
 * @brief Connect with timeout
 * @details This function adds a timeout to
 * <a href="https://man7.org/linux/man-pages/man2/connect.2.html">connect</a>.
 * The behavior of the original function is mimicked as closely as possible by
 * forwarding the original 'errno' extended with a possible 'ETIMEDOUT'
 * @param[in] fd The file descriptor of a socket to be connected
 * @param[in] addr Address information
 * @param[in] addrlen The length of addr
 * @param[in] timeout_ms Timeout in milliseconds
 * @return '0' on success, '-1' on error. In the error case, errno is set accordingly
 */
int connect_with_timeout(int fd, const struct sockaddr* addr, uint32_t addrlen, unsigned int timeout_ms);

/**
 * @brief Set <a href="https://man7.org/linux/man-pages/man7/socket.7.html">reuse address<a>
 * flag on socket
 * @param[in] fd The filedescriptor of a socket.
 */
void set_reuse_address(int fd);

/**
 * @brief Set <a href="https://man7.org/linux/man-pages/man7/socket.7.html">reuse port<a>
 * flag on socket
 * @param[in] fd The filedescriptor of a socket.
 */
void set_reuse_port(int fd);

/**
 * @brief Uses <a href="https://man7.org/linux/man-pages/man2/bind.2.html">bind</a> to
 * to adding an address and port to the socket.
 * @details Description
 * @param[in] fd The filedescriptor of the socket
 * @param[in] ip IP address
 * @param[in] port Port
 */
void bind_socket_ip4(int fd, std::string const& ip, std::uint16_t port);

/**
 * @brief Enable <a href="https://man7.org/linux/man-pages/man7/ip.7.html">multicast</a> for a UDP socket
 * @details This sets the local device for the multicast socket and joins the multicast group
 * @param[in] fd The filedescriptor of the socket
 * @param[in] multicast_ip The IP of the multicast group to be joined.
 * @param[in] interface_ip The IP of the local interface
 */
void set_udp_multicast(int fd, std::string const& multicast_ip, std::string const& interface_ip);

/**
 * @brief Convert string representation of an IP to numeric representation
 * @param[in] ip The string representation
 * @return The numeric representation
 */
std::uint32_t ip_to_s_addr(std::string const& ip);

struct if_info {
    std::string name;
    std::string ipv4;
};

/**
 * @brief Get the address of the supplied interface
 * @param[in] name The name of the interface
 * @return The IP adress of the interface
 * @throws std::runtime_error if the operation fails.
 */
std::string get_interface_address(std::string const& name);

/**
 * @brief Get all interfaces and their adresses
 * @return The list of interfaces and adresses.
 * @throws std::runtime_error if the operation fails.
 */
std::vector<if_info> get_all_interaces();

} // namespace everest::lib::io::socket
