#include "udp_client.h"

#include <arpa/inet.h>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <map>
#include <netdb.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <unistd.h>

UDPClient::UDPClient(std::string host, uint16_t port,
                     std::string multicast_group, uint16_t data_port)
    : host_(std::move(host))
    , port_(port)
    , multicast_group_(std::move(multicast_group))
    , data_port_(data_port)
{}

UDPClient::~UDPClient() { stop(); }

void UDPClient::start() {
    running_ = true;
    thread_ = std::thread(&UDPClient::run, this);
}

void UDPClient::stop() {
    running_ = false;
    if (thread_.joinable()) thread_.join();
}

UDPClient::Stats UDPClient::stats() const {
    Stats s;
    s.packets_rx   = rx_packets_.load(std::memory_order_relaxed);
    s.packets_lost = seq_gaps_.load(std::memory_order_relaxed);
    s.bytes_rx     = rx_bytes_.load(std::memory_order_relaxed);
    s.connected    = connected_.load(std::memory_order_relaxed);
    return s;
}

// ─── Fragmentation reassembly ────────────────────────────────────────

namespace {

struct FragSet {
    uint32_t total = 0;
    uint32_t count = 0;
    std::vector<std::vector<uint8_t>> parts;
    std::chrono::steady_clock::time_point created;
};

using ReassemblyMap = std::map<uint64_t, FragSet>;

std::vector<uint8_t> reassemble(ReassemblyMap& m,
                                const UDPSPacketHeader& hdr,
                                const uint8_t* payload, size_t plen)
{
    if (hdr.total_fragments == 1 && hdr.fragment_idx == 0)
        return std::vector<uint8_t>(payload, payload + plen);

    uint64_t key = (uint64_t(hdr.counter) << 8) | hdr.type;
    auto& fs = m[key];
    if (fs.parts.empty()) {
        fs.total   = hdr.total_fragments;
        fs.parts.resize(fs.total);
        fs.created = std::chrono::steady_clock::now();
    }
    if (hdr.fragment_idx < fs.total && fs.parts[hdr.fragment_idx].empty()) {
        fs.parts[hdr.fragment_idx].assign(payload, payload + plen);
        ++fs.count;
    }
    if (fs.count < fs.total) return {};

    size_t sz = 0;
    for (auto& p : fs.parts) sz += p.size();
    std::vector<uint8_t> out;
    out.reserve(sz);
    for (auto& p : fs.parts) out.insert(out.end(), p.begin(), p.end());
    m.erase(key);
    return out;
}

// Drop fragments older than 2 seconds to avoid unbounded map growth
void gc_fragments(ReassemblyMap& m) {
    auto now  = std::chrono::steady_clock::now();
    auto it   = m.begin();
    while (it != m.end()) {
        if (std::chrono::duration<double>(now - it->second.created).count() > 2.0)
            it = m.erase(it);
        else
            ++it;
    }
}

} // anonymous namespace

// ─── Ingest thread ───────────────────────────────────────────────────

void UDPClient::run() {
    if (!multicast_group_.empty())
        run_multicast();
    else
        run_unicast();
}

void UDPClient::run_unicast() {
    constexpr double SILENCE_SEC  = 5.0;
    constexpr double CONNECT_RETRY= 1.0;   // re-send CONNECT if no CONFIG yet
    constexpr double RETRY_SEC    = 2.0;   // wait before full reconnect

    constexpr size_t BUF_SZ = 65536;
    uint8_t buf[BUF_SZ];

    while (running_) {
        // ── Resolve server address ────────────────────────────────────
        addrinfo hints{};
        hints.ai_family   = AF_INET;
        hints.ai_socktype = SOCK_DGRAM;
        char port_str[8];
        std::snprintf(port_str, sizeof(port_str), "%u", port_);

        addrinfo* res = nullptr;
        if (getaddrinfo(host_.c_str(), port_str, &hints, &res) != 0 || !res) {
            std::fprintf(stderr, "[udp_client %s:%u] cannot resolve host\n",
                         host_.c_str(), port_);
            std::this_thread::sleep_for(std::chrono::duration<double>(RETRY_SEC));
            continue;
        }
        sockaddr_in server{};
        std::memcpy(&server, res->ai_addr, sizeof(server));
        freeaddrinfo(res);

        // ── Open socket ───────────────────────────────────────────────
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock < 0) {
            std::fprintf(stderr, "[udp_client %s:%u] socket: %s\n",
                         host_.c_str(), port_, std::strerror(errno));
            std::this_thread::sleep_for(std::chrono::duration<double>(RETRY_SEC));
            continue;
        }

        // Large receive buffer
        {
            int rcv = 8 * 1024 * 1024;
            setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcv, sizeof(rcv));
        }

        // Bind to any local port so we have a stable source address
        {
            sockaddr_in local{};
            local.sin_family      = AF_INET;
            local.sin_port        = 0;          // OS picks port
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(sock, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0) {
                std::fprintf(stderr, "[udp_client %s:%u] bind: %s\n",
                             host_.c_str(), port_, std::strerror(errno));
                close(sock);
                std::this_thread::sleep_for(std::chrono::duration<double>(RETRY_SEC));
                continue;
            }
        }

        // ── Send CONNECT to server ────────────────────────────────────
        {
            auto pkt = build_connect();
            sendto(sock, pkt.data(), pkt.size(), 0,
                   reinterpret_cast<sockaddr*>(&server), sizeof(server));
            std::fprintf(stderr, "[udp_client %s:%u] CONNECT sent\n",
                         host_.c_str(), port_);
        }

        // ── Receive loop (poll-based, non-blocking recv) ──────────────
        ReassemblyMap fragments;
        std::vector<SignalDef> signal_defs;
        uint32_t last_counter  = 0;
        bool     seen_first    = false;
        bool     config_rx     = false;
        using clock = std::chrono::steady_clock;
        auto     last_rx       = clock::now();
        auto     last_connect  = clock::now();
        uint32_t gc_counter    = 0;

        while (running_) {
            // poll() with 200 ms timeout — keeps reconnect logic responsive
            pollfd pfd{sock, POLLIN, 0};
            int    ready = poll(&pfd, 1, 200 /*ms*/);

            if (!running_) break;

            if (ready < 0) {
                if (errno == EINTR) continue;
                std::fprintf(stderr, "[udp_client %s:%u] poll: %s\n",
                             host_.c_str(), port_, std::strerror(errno));
                break;
            }

            auto now = clock::now();
            double elapsed = std::chrono::duration<double>(now - last_rx).count();

            if (ready == 0) {
                // Timeout — check timers
                if (elapsed > SILENCE_SEC) {
                    std::fprintf(stderr,
                        "[udp_client %s:%u] silence timeout, reconnecting\n",
                        host_.c_str(), port_);
                    connected_ = false;
                    break;
                }
                // If CONFIG not yet received, re-send CONNECT periodically
                if (!config_rx) {
                    double since_connect = std::chrono::duration<double>(
                        now - last_connect).count();
                    if (since_connect >= CONNECT_RETRY) {
                        auto pkt = build_connect();
                        sendto(sock, pkt.data(), pkt.size(), 0,
                               reinterpret_cast<sockaddr*>(&server), sizeof(server));
                        last_connect = now;
                    }
                }
                continue;
            }

            // ── Read one datagram ─────────────────────────────────────
            ssize_t n = recvfrom(sock, buf, BUF_SZ, 0, nullptr, nullptr);
            if (n < 0) {
                if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
                    continue;
                std::fprintf(stderr, "[udp_client %s:%u] recv: %s\n",
                             host_.c_str(), port_, std::strerror(errno));
                break;
            }

            last_rx = clock::now();
            rx_packets_++;
            rx_bytes_ += static_cast<uint64_t>(n);

            UDPSPacketHeader hdr;
            if (!parse_header(buf, static_cast<size_t>(n), hdr)) continue;

            size_t        plen   = static_cast<size_t>(n) - UDPS_HEADER_SIZE;
            const uint8_t* payload = buf + UDPS_HEADER_SIZE;

            auto complete = reassemble(fragments, hdr, payload, plen);
            if (complete.empty()) continue;

            // Periodic GC of stale fragment sets
            if (++gc_counter % 1000 == 0) gc_fragments(fragments);

            switch (hdr.type) {
            case UDPS_TYPE_CONFIG:
                signal_defs = parse_config(complete.data(), complete.size());
                config_rx   = true;
                connected_  = true;
                std::fprintf(stderr, "[udp_client %s:%u] CONFIG: %zu signals\n",
                             host_.c_str(), port_, signal_defs.size());
                if (on_config) on_config(signal_defs);
                break;

            case UDPS_TYPE_DATA: {
                if (seen_first && hdr.counter != last_counter + 1)
                    seq_gaps_ += hdr.counter - last_counter - 1;
                seen_first   = true;
                last_counter = hdr.counter;
                if (signal_defs.empty()) break;
                auto dp = parse_data(complete.data(), complete.size(), signal_defs);
                if (on_data) on_data(dp, signal_defs);
                break;
            }

            case UDPS_TYPE_DISCONNECT:
                std::fprintf(stderr, "[udp_client %s:%u] server DISCONNECT\n",
                             host_.c_str(), port_);
                connected_ = false;
                goto reconnect;

            default: break;
            }
        }

        reconnect:
        {
            auto pkt = build_disconnect();
            sendto(sock, pkt.data(), pkt.size(), 0,
                   reinterpret_cast<sockaddr*>(&server), sizeof(server));
        }
        close(sock);

        if (running_)
            std::this_thread::sleep_for(std::chrono::duration<double>(RETRY_SEC));
    }

    std::fprintf(stderr, "[udp_client %s:%u] stopped\n", host_.c_str(), port_);
}

// ─── Multicast ingest thread ──────────────────────────────────────────

/** Returns true if the session ran normally (even if the server disconnected),
 *  false if a fatal socket error occurred before data could be received. */
bool UDPClient::run_multicast_session() {
    constexpr double SILENCE_SEC = 5.0;
    constexpr size_t BUF_SZ = 65536;
    uint8_t buf[BUF_SZ];

    // ── Resolve server address ────────────────────────────────────────
    addrinfo hints{};
    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    char port_str[8];
    std::snprintf(port_str, sizeof(port_str), "%u", port_);
    addrinfo* res = nullptr;
    if (getaddrinfo(host_.c_str(), port_str, &hints, &res) != 0 || !res) {
        std::fprintf(stderr, "[udp_client %s:%u] cannot resolve host\n",
                     host_.c_str(), port_);
        return false;
    }
    sockaddr_in server{};
    std::memcpy(&server, res->ai_addr, sizeof(server));
    freeaddrinfo(res);

    // ── Open and connect TCP control socket ───────────────────────────
    int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (tcp_sock < 0) return false;
    {
        int one = 1;
        setsockopt(tcp_sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
    }
    if (connect(tcp_sock, reinterpret_cast<sockaddr*>(&server), sizeof(server)) < 0) {
        std::fprintf(stderr, "[udp_client %s:%u] tcp connect: %s\n",
                     host_.c_str(), port_, std::strerror(errno));
        close(tcp_sock);
        return false;
    }

    // RAII wrapper so tcp_sock is always closed on return
    struct TcpGuard {
        int& fd;
        std::function<void()> on_close;
        ~TcpGuard() {
            if (on_close) on_close();
            close(fd);
        }
    } tcp_guard{tcp_sock, [&]() {
        auto pkt = build_disconnect();
        send(tcp_sock, pkt.data(), pkt.size(), 0);
    }};

    // ── Send CONNECT via TCP ──────────────────────────────────────────
    {
        auto pkt = build_connect();
        if (send(tcp_sock, pkt.data(), pkt.size(), 0) < 0) {
            std::fprintf(stderr, "[udp_client %s:%u] tcp send CONNECT: %s\n",
                         host_.c_str(), port_, std::strerror(errno));
            return false;
        }
        std::fprintf(stderr, "[udp_client %s:%u] tcp: CONNECT sent\n",
                     host_.c_str(), port_);
    }

    // ── Read CONFIG header via TCP ────────────────────────────────────
    uint8_t hdr_buf[UDPS_HEADER_SIZE];
    {
        size_t total = 0;
        while (total < UDPS_HEADER_SIZE) {
            ssize_t n = recv(tcp_sock, hdr_buf + total, UDPS_HEADER_SIZE - total, 0);
            if (n <= 0) return false;
            total += static_cast<size_t>(n);
        }
    }
    UDPSPacketHeader cfg_hdr{};
    if (!parse_header(hdr_buf, UDPS_HEADER_SIZE, cfg_hdr) ||
        cfg_hdr.type != UDPS_TYPE_CONFIG) {
        std::fprintf(stderr, "[udp_client %s:%u] expected CONFIG header\n",
                     host_.c_str(), port_);
        return false;
    }

    // ── Read CONFIG payload via TCP ───────────────────────────────────
    std::vector<uint8_t> cfg_payload(cfg_hdr.payload_bytes);
    {
        size_t total = 0;
        while (total < cfg_hdr.payload_bytes) {
            ssize_t n = recv(tcp_sock, cfg_payload.data() + total,
                             cfg_hdr.payload_bytes - total, 0);
            if (n <= 0) return false;
            total += static_cast<size_t>(n);
        }
    }
    auto signal_defs = parse_config(cfg_payload.data(), cfg_payload.size());
    std::fprintf(stderr, "[udp_client %s:%u] tcp: CONFIG (%zu signals)\n",
                 host_.c_str(), port_, signal_defs.size());
    connected_ = true;
    if (on_config) on_config(signal_defs);

    // ── Determine multicast data port ─────────────────────────────────
    uint16_t mcast_port = (data_port_ > 0) ? data_port_
                                            : static_cast<uint16_t>(port_ + 1u);

    // ── Open UDP socket and join multicast group ──────────────────────
    int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock < 0) return false;

    // RAII for udp_sock
    struct UdpGuard {
        int& fd;
        std::string& mgroup;
        ~UdpGuard() {
            ip_mreq mreq{};
            mreq.imr_multiaddr.s_addr = inet_addr(mgroup.c_str());
            mreq.imr_interface.s_addr = INADDR_ANY;
            setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
            close(fd);
        }
    } udp_guard{udp_sock, multicast_group_};

    {
        int reuse = 1;
        setsockopt(udp_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
        int rcv = 8 * 1024 * 1024;
        setsockopt(udp_sock, SOL_SOCKET, SO_RCVBUF, &rcv, sizeof(rcv));
    }
    {
        sockaddr_in mcast_addr{};
        mcast_addr.sin_family      = AF_INET;
        mcast_addr.sin_port        = htons(mcast_port);
        mcast_addr.sin_addr.s_addr = INADDR_ANY;
        if (bind(udp_sock, reinterpret_cast<sockaddr*>(&mcast_addr),
                 sizeof(mcast_addr)) < 0) {
            std::fprintf(stderr, "[udp_client %s:%u] udp bind: %s\n",
                         host_.c_str(), port_, std::strerror(errno));
            return false;
        }
    }
    {
        ip_mreq mreq{};
        mreq.imr_multiaddr.s_addr = inet_addr(multicast_group_.c_str());
        mreq.imr_interface.s_addr = INADDR_ANY;
        if (setsockopt(udp_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
                       &mreq, sizeof(mreq)) < 0) {
            std::fprintf(stderr, "[udp_client %s:%u] IP_ADD_MEMBERSHIP(%s): %s\n",
                         host_.c_str(), port_,
                         multicast_group_.c_str(), std::strerror(errno));
            return false;
        }
    }
    std::fprintf(stderr, "[udp_client %s:%u] joined multicast %s:%u\n",
                 host_.c_str(), port_, multicast_group_.c_str(), mcast_port);

    // ── Main DATA loop ────────────────────────────────────────────────
    ReassemblyMap fragments;
    uint32_t last_counter = 0;
    bool     seen_first   = false;
    uint32_t gc_counter   = 0;
    using clock = std::chrono::steady_clock;
    auto last_rx = clock::now();

    while (running_) {
        pollfd pfds[2];
        pfds[0] = {udp_sock, POLLIN, 0};
        pfds[1] = {tcp_sock, POLLIN, 0};
        int ready = poll(pfds, 2, 200 /*ms*/);

        if (!running_) break;
        if (ready < 0) {
            if (errno == EINTR) continue;
            break;
        }

        // TCP event: DISCONNECT or closed connection
        if (pfds[1].revents & (POLLIN | POLLHUP | POLLERR)) {
            ssize_t n = recv(tcp_sock, buf, BUF_SZ, MSG_DONTWAIT);
            if (n <= 0) {
                std::fprintf(stderr, "[udp_client %s:%u] tcp control closed\n",
                             host_.c_str(), port_);
                break;
            }
            UDPSPacketHeader ctrl_hdr{};
            if (parse_header(buf, static_cast<size_t>(n), ctrl_hdr) &&
                ctrl_hdr.type == UDPS_TYPE_DISCONNECT) {
                std::fprintf(stderr, "[udp_client %s:%u] server DISCONNECT\n",
                             host_.c_str(), port_);
                break;
            }
        }

        // Silence timeout on UDP DATA
        {
            auto now = clock::now();
            double elapsed = std::chrono::duration<double>(now - last_rx).count();
            if (!(pfds[0].revents & POLLIN)) {
                if (elapsed > SILENCE_SEC) {
                    std::fprintf(stderr,
                        "[udp_client %s:%u] multicast silence timeout\n",
                        host_.c_str(), port_);
                    break;
                }
                continue;
            }
        }

        ssize_t n = recvfrom(udp_sock, buf, BUF_SZ, 0, nullptr, nullptr);
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
            std::fprintf(stderr, "[udp_client %s:%u] udp recv: %s\n",
                         host_.c_str(), port_, std::strerror(errno));
            break;
        }

        last_rx = clock::now();
        rx_packets_++;
        rx_bytes_ += static_cast<uint64_t>(n);

        UDPSPacketHeader hdr{};
        if (!parse_header(buf, static_cast<size_t>(n), hdr)) continue;

        size_t         plen    = static_cast<size_t>(n) - UDPS_HEADER_SIZE;
        const uint8_t* payload = buf + UDPS_HEADER_SIZE;

        auto complete = reassemble(fragments, hdr, payload, plen);
        if (complete.empty()) continue;

        if (++gc_counter % 1000 == 0) gc_fragments(fragments);

        if (hdr.type == UDPS_TYPE_DATA) {
            if (seen_first && hdr.counter != last_counter + 1)
                seq_gaps_ += hdr.counter - last_counter - 1;
            seen_first   = true;
            last_counter = hdr.counter;
            if (!signal_defs.empty()) {
                auto dp = parse_data(complete.data(), complete.size(), signal_defs);
                if (on_data) on_data(dp, signal_defs);
            }
        }
    }

    connected_ = false;
    return true;
}

void UDPClient::run_multicast() {
    constexpr double RETRY_SEC = 2.0;
    while (running_) {
        connected_ = false;
        run_multicast_session();
        connected_ = false;
        if (running_)
            std::this_thread::sleep_for(std::chrono::duration<double>(RETRY_SEC));
    }
    std::fprintf(stderr, "[udp_client %s:%u] multicast stopped\n",
                 host_.c_str(), port_);
}
