#include "NetSession.h"

#include <enet/enet.h>

#include <SDL3/SDL.h>

#include <cstring>

namespace {
constexpr uint8_t kChannelReliable = 0;

static bool netLogVerboseEnabled() {
    // Set environment variable / hint: SPACETRIS_NET_LOG=1
    const char* v = SDL_GetHint("SPACETRIS_NET_LOG");
    return v && v[0] == '1';
}

template <typename T>
static void append(std::vector<uint8_t>& out, const T& value) {
    const uint8_t* p = reinterpret_cast<const uint8_t*>(&value);
    out.insert(out.end(), p, p + sizeof(T));
}

template <typename T>
static bool read(const uint8_t* data, size_t size, size_t& off, T& out) {
    if (off + sizeof(T) > size) return false;
    std::memcpy(&out, data + off, sizeof(T));
    off += sizeof(T);
    return true;
}
}

NetSession::NetSession() = default;

NetSession::~NetSession() {
    shutdown();
}

bool NetSession::ensureEnetInitialized() {
    static bool s_inited = false;
    if (s_inited) return true;
    if (enet_initialize() != 0) {
        setError("enet_initialize failed");
        m_state = ConnState::Error;
        return false;
    }
    s_inited = true;
    return true;
}

void NetSession::setError(const std::string& msg) {
    m_lastError = msg;
}

bool NetSession::host(const std::string& bindHost, uint16_t port) {
    shutdown();
    if (!ensureEnetInitialized()) return false;

    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] host(bind='%s', port=%u)", bindHost.c_str(), (unsigned)port);

    ENetAddress address{};
    address.host = ENET_HOST_ANY;
    address.port = port;

    if (!bindHost.empty() && bindHost != "0.0.0.0") {
        if (enet_address_set_host(&address, bindHost.c_str()) != 0) {
            setError("enet_address_set_host (bind) failed");
            m_state = ConnState::Error;
            return false;
        }
    }

    // 1 peer, 2 channels (reserve extra)
    m_host = enet_host_create(&address, 1, 2, 0, 0);
    if (!m_host) {
        setError("enet_host_create (host) failed");
        m_state = ConnState::Error;
        return false;
    }

    m_mode = Mode::Host;
    m_state = ConnState::Connecting;
    return true;
}

bool NetSession::join(const std::string& hostNameOrIp, uint16_t port) {
    shutdown();
    if (!ensureEnetInitialized()) return false;

    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] join(remote='%s', port=%u)", hostNameOrIp.c_str(), (unsigned)port);

    m_host = enet_host_create(nullptr, 1, 2, 0, 0);
    if (!m_host) {
        setError("enet_host_create (client) failed");
        m_state = ConnState::Error;
        return false;
    }

    ENetAddress address{};
    if (enet_address_set_host(&address, hostNameOrIp.c_str()) != 0) {
        setError("enet_address_set_host failed");
        m_state = ConnState::Error;
        return false;
    }
    address.port = port;

    m_peer = enet_host_connect(m_host, &address, 2, 0);
    if (!m_peer) {
        setError("enet_host_connect failed");
        m_state = ConnState::Error;
        return false;
    }

    m_mode = Mode::Client;
    m_state = ConnState::Connecting;
    return true;
}

void NetSession::shutdown() {
    if (m_host || m_peer) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] shutdown(mode=%d state=%d)", (int)m_mode, (int)m_state);
    }

    m_remoteInputs.clear();
    m_remoteHashes.clear();
    m_receivedHandshake.reset();

    m_inputsSent = 0;
    m_inputsReceived = 0;
    m_hashesSent = 0;
    m_hashesReceived = 0;
    m_handshakesSent = 0;
    m_handshakesReceived = 0;
    m_lastRecvInputTick = 0xFFFFFFFFu;
    m_lastRecvHashTick = 0xFFFFFFFFu;
    m_lastStatsLogMs = 0;

    if (m_peer) {
        enet_peer_disconnect(m_peer, 0);
        m_peer = nullptr;
    }

    if (m_host) {
        enet_host_destroy(m_host);
        m_host = nullptr;
    }

    m_mode = Mode::None;
    m_state = ConnState::Disconnected;
    m_lastError.clear();
}

void NetSession::poll(uint32_t timeoutMs) {
    if (!m_host) return;

    ENetEvent event{};
    while (enet_host_service(m_host, &event, static_cast<enet_uint32>(timeoutMs)) > 0) {
        switch (event.type) {
            case ENET_EVENT_TYPE_CONNECT:
                m_peer = event.peer;
                m_state = ConnState::Connected;
                SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] connected (mode=%d)", (int)m_mode);
                break;
            case ENET_EVENT_TYPE_RECEIVE:
                if (event.packet) {
                    handlePacket(event.packet->data, event.packet->dataLength);
                    enet_packet_destroy(event.packet);
                }
                break;
            case ENET_EVENT_TYPE_DISCONNECT:
                m_peer = nullptr;
                m_state = ConnState::Disconnected;
                SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[NET] disconnected");
                break;
            case ENET_EVENT_TYPE_NONE:
            default:
                break;
        }

        // After first event, do non-blocking passes.
        timeoutMs = 0;
    }

    // Rate-limited stats log (opt-in)
    if (netLogVerboseEnabled()) {
        const uint32_t nowMs = SDL_GetTicks();
        if (m_lastStatsLogMs == 0) m_lastStatsLogMs = nowMs;
        if (nowMs - m_lastStatsLogMs >= 1000u) {
            m_lastStatsLogMs = nowMs;
            SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
                        "[NET] stats: sent(in=%u hash=%u hs=%u) recv(in=%u hash=%u hs=%u) lastRecv(inTick=%u hashTick=%u) state=%d",
                        m_inputsSent,
                        m_hashesSent,
                        m_handshakesSent,
                        m_inputsReceived,
                        m_hashesReceived,
                        m_handshakesReceived,
                        m_lastRecvInputTick,
                        m_lastRecvHashTick,
                        (int)m_state);
        }
    }
}

bool NetSession::sendBytesReliable(const void* data, size_t size) {
    if (!m_peer) return false;
    ENetPacket* packet = enet_packet_create(data, size, ENET_PACKET_FLAG_RELIABLE);
    if (!packet) return false;
    if (enet_peer_send(m_peer, kChannelReliable, packet) != 0) {
        enet_packet_destroy(packet);
        return false;
    }
    // Let the caller decide flush cadence; but for tiny control packets, flushing is cheap.
    enet_host_flush(m_host);
    return true;
}

bool NetSession::sendHandshake(const Handshake& hs) {
    if (m_mode != Mode::Host) return false;
    m_handshakesSent++;
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendHandshake(seed=%u startTick=%u startLevel=%u)", hs.rngSeed, hs.startTick, (unsigned)hs.startLevel);
    std::vector<uint8_t> buf;
    buf.reserve(1 + sizeof(uint32_t) * 2 + sizeof(uint8_t));
    buf.push_back(static_cast<uint8_t>(MsgType::Handshake));
    append(buf, hs.rngSeed);
    append(buf, hs.startTick);
    append(buf, hs.startLevel);
    return sendBytesReliable(buf.data(), buf.size());
}

std::optional<NetSession::Handshake> NetSession::takeReceivedHandshake() {
    auto out = m_receivedHandshake;
    m_receivedHandshake.reset();
    return out;
}

bool NetSession::sendLocalInput(uint32_t tick, uint8_t buttons) {
    m_inputsSent++;
    if (netLogVerboseEnabled() && (tick % 60u) == 0u) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendInput(tick=%u buttons=0x%02X)", tick, (unsigned)buttons);
    }
    std::vector<uint8_t> buf;
    buf.reserve(1 + sizeof(uint32_t) + sizeof(uint8_t));
    buf.push_back(static_cast<uint8_t>(MsgType::Input));
    append(buf, tick);
    append(buf, buttons);
    return sendBytesReliable(buf.data(), buf.size());
}

std::optional<uint8_t> NetSession::getRemoteButtons(uint32_t tick) const {
    auto it = m_remoteInputs.find(tick);
    if (it == m_remoteInputs.end()) return std::nullopt;
    return it->second;
}

bool NetSession::sendStateHash(uint32_t tick, uint64_t hash) {
    m_hashesSent++;
    if (netLogVerboseEnabled()) {
        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendHash(tick=%u hash=%llu)", tick, (unsigned long long)hash);
    }
    std::vector<uint8_t> buf;
    buf.reserve(1 + sizeof(uint32_t) + sizeof(uint64_t));
    buf.push_back(static_cast<uint8_t>(MsgType::Hash));
    append(buf, tick);
    append(buf, hash);
    return sendBytesReliable(buf.data(), buf.size());
}

std::optional<uint64_t> NetSession::takeRemoteHash(uint32_t tick) {
    auto it = m_remoteHashes.find(tick);
    if (it == m_remoteHashes.end()) return std::nullopt;
    uint64_t v = it->second;
    m_remoteHashes.erase(it);
    return v;
}

void NetSession::handlePacket(const uint8_t* data, size_t size) {
    if (!data || size < 1) return;
    size_t off = 0;
    uint8_t typeByte = 0;
    if (!read(data, size, off, typeByte)) return;

    MsgType t = static_cast<MsgType>(typeByte);
    switch (t) {
        case MsgType::Handshake: {
            Handshake hs{};
            if (!read(data, size, off, hs.rngSeed)) return;
            if (!read(data, size, off, hs.startTick)) return;
            if (!read(data, size, off, hs.startLevel)) return;
            m_receivedHandshake = hs;
            m_handshakesReceived++;
            SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvHandshake(seed=%u startTick=%u startLevel=%u)", hs.rngSeed, hs.startTick, (unsigned)hs.startLevel);
            break;
        }
        case MsgType::Input: {
            uint32_t tick = 0;
            uint8_t buttons = 0;
            if (!read(data, size, off, tick)) return;
            if (!read(data, size, off, buttons)) return;
            m_remoteInputs[tick] = buttons;
            m_inputsReceived++;
            m_lastRecvInputTick = tick;
            if (netLogVerboseEnabled() && (tick % 60u) == 0u) {
                SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvInput(tick=%u buttons=0x%02X)", tick, (unsigned)buttons);
            }
            break;
        }
        case MsgType::Hash: {
            uint32_t tick = 0;
            uint64_t hash = 0;
            if (!read(data, size, off, tick)) return;
            if (!read(data, size, off, hash)) return;
            m_remoteHashes[tick] = hash;
            m_hashesReceived++;
            m_lastRecvHashTick = tick;
            if (netLogVerboseEnabled()) {
                SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvHash(tick=%u hash=%llu)", tick, (unsigned long long)hash);
            }
            break;
        }
        default:
            break;
    }
}
