#include "io/io_module_service.hpp"

#include "storage/io_module_store.hpp"
#include "util/string_util.hpp"

#include <algorithm>
#include <arpa/inet.h>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <netdb.h>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>

namespace lm {

namespace {

constexpr int kDefaultModbusPort = 502;
constexpr int kPortCount = 4;

bool parseBool(const nlohmann::json& v, bool fallback)
{
  if (v.is_boolean())
    return v.get<bool>();
  if (v.is_string())
  {
    const std::string s = StringUtil::toLower(v.get<std::string>());
    return s == "on" || s == "true" || s == "1";
  }
  if (v.is_number())
    return v.get<int>() != 0;
  return fallback;
}

int parsePort(const nlohmann::json& params, const std::string& module_type)
{
  int port = 0;
  if (params.contains("output") && params["output"].is_number_integer())
    port = params["output"].get<int>();
  else if (params.contains("pin") && params["pin"].is_number_integer())
    port = params["pin"].get<int>();
  else if (params.contains("input") && params["input"].is_number_integer())
    port = params["input"].get<int>();
  if (!IoModuleService::validPortForType(module_type, port))
    port = IoModuleService::defaultPortForType(module_type);
  return port;
}

}  // namespace

IoModuleService::IoModuleService(IoModuleStore& store) : store_(store) {}

bool IoModuleService::validPortForType(const std::string& module_type, int port)
{
  if (module_type == "wise")
    return port >= 0 && port <= 3;
  return port >= 1 && port <= 4;
}

int IoModuleService::defaultPortForType(const std::string& module_type)
{
  return module_type == "wise" ? 0 : 1;
}

bool IoModuleService::testConnection(const std::string& ip_address, int port, std::string& err) const
{
  const std::string ip = StringUtil::trimCopy(ip_address);
  if (ip.empty())
  {
    err = "ip_address is required";
    return false;
  }
  if (port <= 0)
    port = kDefaultModbusPort;

  addrinfo hints{};
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  addrinfo* res = nullptr;
  const std::string port_str = std::to_string(port);
  if (getaddrinfo(ip.c_str(), port_str.c_str(), &hints, &res) != 0)
  {
    err = "cannot resolve host";
    return false;
  }

  bool ok = false;
  for (addrinfo* p = res; p; p = p->ai_next)
  {
    const int fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (fd < 0)
      continue;
    const int flags = fcntl(fd, F_GETFL, 0);
    if (flags >= 0)
      fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    const int rc = ::connect(fd, p->ai_addr, p->ai_addrlen);
    if (rc == 0)
    {
      ok = true;
      close(fd);
      break;
    }
    if (errno == EINPROGRESS)
    {
      fd_set wfds;
      FD_ZERO(&wfds);
      FD_SET(fd, &wfds);
      timeval tv{};
      tv.tv_sec = 2;
      tv.tv_usec = 0;
      if (select(fd + 1, nullptr, &wfds, nullptr, &tv) > 0)
      {
        int so_error = 0;
        socklen_t len = sizeof(so_error);
        getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &len);
        ok = (so_error == 0);
      }
    }
    close(fd);
    if (ok)
      break;
  }
  freeaddrinfo(res);
  if (!ok)
    err = "connection failed";
  return ok;
}

std::optional<nlohmann::json> IoModuleService::resolveModule(const std::string& ref) const
{
  const std::string needle = StringUtil::trimCopy(ref);
  if (needle.empty())
    return std::nullopt;
  if (auto by_id = store_.find(needle))
    return by_id;
  return store_.findByName(needle);
}

bool IoModuleService::connect(const std::string& module_id, std::string& err)
{
  const auto mod = store_.find(module_id);
  if (!mod)
  {
    err = "io module not found";
    return false;
  }
  std::string test_err;
  if (!testConnection(mod->value("ip_address", ""), kDefaultModbusPort, test_err))
  {
    err = "connection test failed: " + test_err;
    return false;
  }

  std::lock_guard<std::mutex> lock(mu_);
  connected_[module_id] = true;
  ensureSimStateUnlocked(module_id, mod->value("module_type", "bluetooth"));

  std::string persist_err;
  if (!store_.setConnected(module_id, true, persist_err))
  {
    err = persist_err;
    return false;
  }
  return true;
}

bool IoModuleService::disconnect(const std::string& module_id, std::string& err)
{
  if (!store_.find(module_id))
  {
    err = "io module not found";
    return false;
  }
  {
    std::lock_guard<std::mutex> lock(mu_);
    connected_[module_id] = false;
  }
  return store_.setConnected(module_id, false, err);
}

bool IoModuleService::isConnected(const std::string& module_id) const
{
  std::lock_guard<std::mutex> lock(mu_);
  const auto it = connected_.find(module_id);
  if (it != connected_.end())
    return it->second;
  const auto mod = store_.find(module_id);
  return mod && mod->value("connected", false);
}

void IoModuleService::ensureSimStateUnlocked(const std::string& module_id, const std::string& module_type)
{
  if (outputs_.find(module_id) == outputs_.end())
    outputs_[module_id] = std::vector<bool>(kPortCount, false);
  if (inputs_.find(module_id) == inputs_.end())
    inputs_[module_id] = std::vector<bool>(kPortCount, false);
  (void)module_type;
}

bool IoModuleService::requireConnectedUnlocked(const std::string& module_id, std::string& err) const
{
  const auto it = connected_.find(module_id);
  const bool connected = (it != connected_.end()) ? it->second : false;
  if (!connected)
  {
    const auto mod = store_.find(module_id);
    if (!mod || !mod->value("connected", false))
    {
      err = "io module is not connected";
      return false;
    }
  }
  return true;
}

bool IoModuleService::setOutput(const std::string& module_id, int port, bool on, std::string& err)
{
  const auto mod = store_.find(module_id);
  if (!mod)
  {
    err = "io module not found";
    return false;
  }
  const std::string type = mod->value("module_type", "bluetooth");
  if (!validPortForType(type, port))
  {
    err = "invalid output port for module type";
    return false;
  }
  {
    std::lock_guard<std::mutex> lock(mu_);
    if (!requireConnectedUnlocked(module_id, err))
      return false;
    ensureSimStateUnlocked(module_id, type);
    const int idx = std::clamp(port, 0, kPortCount - 1);
    outputs_[module_id][static_cast<size_t>(idx)] = on;
    inputs_[module_id][static_cast<size_t>(idx)] = on;
  }
  return true;
}

bool IoModuleService::setOutputTimed(const std::string& module_id,
                                     int port,
                                     bool on,
                                     int timeout_ms,
                                     std::string& err)
{
  if (!setOutput(module_id, port, on, err))
    return false;
  if (on && timeout_ms > 0)
  {
    std::thread([this, module_id, port, timeout_ms]() {
      std::this_thread::sleep_for(std::chrono::milliseconds(timeout_ms));
      std::string ignore;
      setOutput(module_id, port, false, ignore);
    }).detach();
  }
  return true;
}

bool IoModuleService::getInput(const std::string& module_id, int port, bool& value, std::string& err) const
{
  const auto mod = store_.find(module_id);
  if (!mod)
  {
    err = "io module not found";
    return false;
  }
  const std::string type = mod->value("module_type", "bluetooth");
  if (!validPortForType(type, port))
  {
    err = "invalid input port for module type";
    return false;
  }
  std::lock_guard<std::mutex> lock(mu_);
  if (!requireConnectedUnlocked(module_id, err))
    return false;
  const auto it = inputs_.find(module_id);
  if (it == inputs_.end())
  {
    value = false;
    return true;
  }
  const int idx = std::clamp(port, 0, kPortCount - 1);
  value = it->second[static_cast<size_t>(idx)];
  return true;
}

bool IoModuleService::waitInput(const std::string& module_id,
                                int port,
                                bool expected,
                                int timeout_ms,
                                std::string& err)
{
  const int step = 100;
  for (int elapsed = 0; elapsed <= timeout_ms; elapsed += step)
  {
    bool value = false;
    if (!getInput(module_id, port, value, err))
      return false;
    if (value == expected)
      return true;
    std::this_thread::sleep_for(std::chrono::milliseconds(step));
  }
  err = "wait for input timed out";
  return false;
}

bool IoModuleService::applyZoneSettings(const nlohmann::json& zone, std::string& err)
{
  const std::string module_ref = zone.value("io_module", "");
  if (module_ref.empty())
  {
    err = "io_module is required";
    return false;
  }
  const auto mod = resolveModule(module_ref);
  if (!mod)
  {
    err = "unknown io module: " + module_ref;
    return false;
  }
  const std::string module_id = mod->value("id", "");
  if (!isConnected(module_id))
  {
    if (!connect(module_id, err))
      return false;
  }

  nlohmann::json params = zone;
  const std::string type = mod->value("module_type", "bluetooth");
  const int port = parsePort(params, type);
  const bool on = true;
  if (!setOutput(module_id, port, on, err))
    return false;

  if (zone.contains("plc_register") && zone["plc_register"].is_number())
  {
    // PLC register side-effect is logged only in simulated runtime for now.
  }
  return true;
}

nlohmann::json IoModuleService::runtimeSnapshot(const std::string& module_id) const
{
  nlohmann::json out = {{"connected", isConnected(module_id)}, {"outputs", nlohmann::json::array()}, {"inputs", nlohmann::json::array()}};
  std::lock_guard<std::mutex> lock(mu_);
  const auto oit = outputs_.find(module_id);
  if (oit != outputs_.end())
  {
    for (bool v : oit->second)
      out["outputs"].push_back(v);
  }
  const auto iit = inputs_.find(module_id);
  if (iit != inputs_.end())
  {
    for (bool v : iit->second)
      out["inputs"].push_back(v);
  }
  return out;
}

}  // namespace lm
