#include "modbus_tcp.hpp"
#include "modbus_exception.hpp"
#include "transport_error.hpp"
#include "log_macros.hpp"

#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <thread>


namespace {
constexpr std::uint8_t MODBUS_UNIT_ID = 1;
constexpr int SOCKET_TIMEOUT_SEC = 2;
constexpr int WATCHDOG_SEC = 5;
constexpr int MAX_RETRY = 5;
}

// ------------------------------------------------------------
// Modbus exception → TransportError
// ------------------------------------------------------------

static TransportError map_exception(ModbusException ex)
{
    switch (ex) {
    case ModbusException::Acknowledge:
        return TransportError::InProgress;
    case ModbusException::SlaveDeviceBusy:
        return TransportError::Busy;
    default:
        return TransportError::RemoteError;
    }
}

// ------------------------------------------------------------
// ctor / dtor
// ------------------------------------------------------------

ModbusTCP::ModbusTCP()
    : sock_(-1),
      transaction_id_(0),
      state_(TransportState::Disconnected),
      last_error_(TransportError::None),
      exception_(ModbusException::IllegalFunction),
      retry_count_(0)
{
}

ModbusTCP::~ModbusTCP()
{
    disconnect();
}

// ------------------------------------------------------------
// connection
// ------------------------------------------------------------

bool ModbusTCP::connect(const char* host, std::uint16_t port)
{
    
    host_ = host;
    port_ = port;

    LOG_INFO("ModbusTCP: Connecting start: " + host_ + ":" + std::to_string(port_));

    sock_ = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sock_ < 0)
        return false;

    timeval tv{};
    tv.tv_sec = SOCKET_TIMEOUT_SEC;
    tv.tv_usec = 0;
    ::setsockopt(sock_, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    ::setsockopt(sock_, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    ::inet_pton(AF_INET, host, &addr.sin_addr);

    if (::connect(sock_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        LOG_ERROR("ModbusTCP: Connecting failed: " + host_ + ":" + std::to_string(port_));
        disconnect();
        return false;
    }

    state_ = TransportState::Connected;
    retry_count_ = 0;
    last_ok_ = std::chrono::steady_clock::now();
    last_error_ = TransportError::None;
    return true;
}

void ModbusTCP::disconnect()
{
    LOG_INFO("ModbusTCP: Disconnecting: " + host_ + ":" + std::to_string(port_));
    
    if (sock_ >= 0) {
        ::close(sock_);
        sock_ = -1;
    }
    state_ = TransportState::Disconnected;
}

bool ModbusTCP::is_connected() const
{
    return state_ == TransportState::Connected;
}

// ------------------------------------------------------------
// TCP fragmentation safe recv
// ------------------------------------------------------------

bool ModbusTCP::recv_all(void* buf, std::size_t len)
{
    std::uint8_t* p = static_cast<std::uint8_t*>(buf);
    std::size_t total = 0;

    while (total < len) {
        ssize_t n = ::recv(sock_, p + total, len - total, 0);
        if (n <= 0)
            return false;
        total += static_cast<std::size_t>(n);
    }
    return true;
}

// ------------------------------------------------------------
// status to log string helper
// ------------------------------------------------------------

static const char* to_string(TransportState s) {
    switch (s) {
    case TransportState::Disconnected: return "Disconnected";
    case TransportState::Connected:    return "Connected";
    case TransportState::Reconnecting: return "Reconnecting";
    case TransportState::Fault:        return "Fault";
    default: return "Unknown";
    }
}


// ------------------------------------------------------------
// Read Holding Registers (0x03)
// ------------------------------------------------------------

bool ModbusTCP::read_registers(std::uint16_t addr,
                               std::uint16_t count,
                               std::vector<std::uint16_t>& out)
{
    if (!can_io()) {
        last_error_ = TransportError::Disconnected;
        return false;
    }

    if (count == 0) {
        last_error_ = TransportError::ProtocolError;
        return false;
    }

    // MBAP(7) + PDU(5)
    std::uint8_t req[12]{};

    req[0] = transaction_id_ >> 8;
    req[1] = transaction_id_ & 0xFF;
    req[2] = 0x00;
    req[3] = 0x00;
    req[4] = 0x00;
    req[5] = 0x06;
    req[6] = MODBUS_UNIT_ID;
    req[7] = 0x03;
    req[8] = addr >> 8;
    req[9] = addr & 0xFF;
    req[10] = count >> 8;
    req[11] = count & 0xFF;

    if (::send(sock_, req, sizeof(req), 0) < 0) {
        last_error_ = TransportError::Disconnected;
        return false;
    }

    std::uint8_t header[9];
    if (!recv_all(header, 9)) {
        last_error_ = TransportError::Timeout;
        return false;
    }

    std::uint8_t func = header[7];
    if (func & 0x80) {
        exception_ = static_cast<ModbusException>(header[8]);
        last_error_ = map_exception(exception_);
        return false;
    }

    std::uint8_t byte_count = header[8];
    std::vector<std::uint8_t> payload(byte_count);

    if (!recv_all(payload.data(), byte_count)) {
        last_error_ = TransportError::Timeout;
        return false;
    }

    if (byte_count != count * 2) {
        last_error_ = TransportError::ProtocolError;
        return false;
    }

    out.clear();
    out.reserve(count);
    for (std::size_t i = 0; i < count; ++i) {
        out.push_back((payload[i * 2] << 8) | payload[i * 2 + 1]);
    }

    transaction_id_++;
    last_ok_ = std::chrono::steady_clock::now();
    last_error_ = TransportError::None;
    return true;
}

// ------------------------------------------------------------
// Write Multiple Registers (0x10)
// ------------------------------------------------------------

bool ModbusTCP::write_registers(std::uint16_t addr,
                                const std::vector<std::uint16_t>& values)
{
    if (!can_io()) {
        last_error_ = TransportError::Disconnected;
        return false;
    }

    if (values.empty()) {
        last_error_ = TransportError::ProtocolError;
        return false;
    }

    std::size_t count = values.size();
    std::size_t data_len = count * 2;
    std::size_t pdu_len = 6 + data_len;
    std::size_t length = 1 + pdu_len;

    std::vector<std::uint8_t> req(7 + pdu_len);

    req[0] = transaction_id_ >> 8;
    req[1] = transaction_id_ & 0xFF;
    req[2] = 0x00;
    req[3] = 0x00;
    req[4] = length >> 8;
    req[5] = length & 0xFF;
    req[6] = MODBUS_UNIT_ID;
    req[7] = 0x10;
    req[8] = addr >> 8;
    req[9] = addr & 0xFF;
    req[10] = count >> 8;
    req[11] = count & 0xFF;
    req[12] = static_cast<std::uint8_t>(data_len);

    for (std::size_t i = 0; i < count; ++i) {
        req[13 + i * 2] = values[i] >> 8;
        req[14 + i * 2] = values[i] & 0xFF;
    }

    if (::send(sock_, req.data(), req.size(), 0) < 0) {
        last_error_ = TransportError::Disconnected;
        return false;
    }

    std::uint8_t resp[12];
    if (!recv_all(resp, sizeof(resp))) {
        last_error_ = TransportError::Timeout;
        return false;
    }

    std::uint8_t func = resp[7];
    if (func & 0x80) {
        exception_ = static_cast<ModbusException>(resp[8]);
        last_error_ = map_exception(exception_);
        return false;
    }

    if (func != 0x10) {
        last_error_ = TransportError::ProtocolError;
        return false;
    }

    transaction_id_++;
    last_ok_ = std::chrono::steady_clock::now();
    last_error_ = TransportError::None;
    return true;
}

// ------------------------------------------------------------
// watchdog / reconnect / log state changes(added)
// ------------------------------------------------------------

void ModbusTCP::poll()
{
    using namespace std::chrono;

    auto now = steady_clock::now();
    auto prev = state_;

    switch (state_) {

    case TransportState::Connected: {
        auto idle = duration_cast<seconds>(now - last_ok_);

        if (idle.count() > WATCHDOG_SEC) {
            last_error_ = TransportError::Timeout;
            state_ = TransportState::Reconnecting;
        }
        break;
    }

    case TransportState::Disconnected:
    case TransportState::Reconnecting:
        reconnect();
        break;

    case TransportState::Fault: {
        // FAULT_RESET_SEC 후 자동 리셋 → Disconnected로 복귀해 재시도
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
            std::chrono::steady_clock::now() - fault_since_).count();
        if (elapsed >= FAULT_RESET_SEC) {
            LOG_WARN("[Transport] Fault 상태 리셋 — 재접속 시도");
            retry_count_ = 0;
            state_ = TransportState::Disconnected;
        }
        break;
    }
    }

    if (prev != state_) {
        LOG_WARN(std::string("state: ") + 
        to_string(prev) + " -> " + 
        to_string(state_));   
    }
}

// ------------------------------------------------------------
// reset procedure
// ------------------------------------------------------------

bool ModbusTCP::reconnect()
{
    LOG_DEBUG("reconnect attempt " + std::to_string(retry_count_));

    if (retry_count_++ >= MAX_RETRY) {
        state_ = TransportState::Fault;
        fault_since_ = std::chrono::steady_clock::now();
        LOG_ERROR("reconnect failed, entering Fault (will retry in " +
                  std::to_string(FAULT_RESET_SEC) + "s)");
        return false;
    }

    disconnect();
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    return connect(host_.c_str(), port_);
}

// ------------------------------------------------------------
// reset procedure
// ------------------------------------------------------------

void ModbusTCP::reset()
{
    disconnect();

    transaction_id_ = 0;
    retry_count_ = 0;
    last_error_ = TransportError::None;
    exception_ = ModbusException::IllegalFunction; // 의미 없는 초기값 OK

    state_ = TransportState::Disconnected;
}

// ------------------------------------------------------------
// read/write allowed check
// ------------------------------------------------------------

inline bool ModbusTCP::can_io() const
{
    return state_ == TransportState::Connected;
}





