#include "meter_driver.h"
#include "config.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>

// Dedicated reader task pumps UART continuously; keep timeout short so parser can
// recover quickly from broken frames.
static constexpr uint32_t METER_FRAME_TIMEOUT_MS = 3000;
static constexpr size_t METER_FRAME_MAX = 512;

enum class MeterRxState : uint8_t {
  WaitStart = 0,
  InFrame = 1
};

static MeterRxState g_rx_state = MeterRxState::WaitStart;
static char g_frame_buf[METER_FRAME_MAX + 1];
static size_t g_frame_len = 0;
static uint32_t g_last_rx_ms = 0;
static uint32_t g_bytes_rx = 0;
static uint32_t g_frames_ok = 0;
static uint32_t g_frames_parse_fail = 0;
static uint32_t g_rx_overflow = 0;
static uint32_t g_rx_timeout = 0;
static uint32_t g_last_log_ms = 0;
static uint32_t g_last_good_frame_ms = 0;
static constexpr uint32_t METER_FIXED_FRAC_MAX_DIV = 10000;

void meter_init() {
#ifdef ARDUINO_ARCH_ESP32
  // Buffer enough serial data to survive long LoRa blocking sections.
  Serial2.setRxBufferSize(8192);
#endif
  Serial2.begin(9600, SERIAL_7E1, PIN_METER_RX, -1);
}

enum class ObisField : uint8_t {
  None = 0,
  Energy = 1,
  TotalPower = 2,
  Phase1 = 3,
  Phase2 = 4,
  Phase3 = 5,
  MeterSeconds = 6
};

static ObisField detect_obis_field(const char *line) {
  if (!line) {
    return ObisField::None;
  }
  const char *p = line;
  while (*p == ' ' || *p == '\t') {
    ++p;
  }
  if (strncmp(p, "1-0:1.8.0", 9) == 0) {
    return ObisField::Energy;
  }
  if (strncmp(p, "1-0:16.7.0", 10) == 0) {
    return ObisField::TotalPower;
  }
  if (strncmp(p, "1-0:36.7.0", 10) == 0) {
    return ObisField::Phase1;
  }
  if (strncmp(p, "1-0:56.7.0", 10) == 0) {
    return ObisField::Phase2;
  }
  if (strncmp(p, "1-0:76.7.0", 10) == 0) {
    return ObisField::Phase3;
  }
  if (strncmp(p, "0-0:96.8.0*255", 14) == 0) {
    return ObisField::MeterSeconds;
  }
  return ObisField::None;
}

static bool parse_decimal_fixed(const char *start, const char *end, float &out_value) {
  if (!start || !end || end <= start) {
    return false;
  }

  const char *cur = start;
  bool started = false;
  bool negative = false;
  bool in_fraction = false;
  bool saw_digit = false;
  uint64_t int_part = 0;
  uint32_t frac_part = 0;
  uint32_t frac_div = 1;

  while (cur < end) {
    char c = *cur++;
    if (!started) {
      if (c == '+' || c == '-') {
        started = true;
        negative = (c == '-');
        continue;
      }
      if (c >= '0' && c <= '9') {
        started = true;
        saw_digit = true;
        int_part = static_cast<uint64_t>(c - '0');
        continue;
      }
      if (c == '.' || c == ',') {
        started = true;
        in_fraction = true;
        continue;
      }
      continue;
    }

    if (c >= '0' && c <= '9') {
      saw_digit = true;
      uint32_t digit = static_cast<uint32_t>(c - '0');
      if (!in_fraction) {
        if (int_part <= (UINT64_MAX - digit) / 10ULL) {
          int_part = int_part * 10ULL + digit;
        }
      } else if (frac_div < METER_FIXED_FRAC_MAX_DIV) {
        frac_part = frac_part * 10U + digit;
        frac_div *= 10U;
      }
      continue;
    }

    if ((c == '.' || c == ',') && !in_fraction) {
      in_fraction = true;
      continue;
    }

    break;
  }

  if (!saw_digit) {
    return false;
  }
  double value = static_cast<double>(int_part);
  if (frac_div > 1U) {
    value += static_cast<double>(frac_part) / static_cast<double>(frac_div);
  }
  if (negative) {
    value = -value;
  }
  out_value = static_cast<float>(value);
  return true;
}

static bool parse_obis_ascii_payload_value(const char *line, float &out_value) {
  const char *lparen = strchr(line, '(');
  if (!lparen) {
    return false;
  }
  const char *end = lparen + 1;
  while (*end && *end != ')' && *end != '*') {
    ++end;
  }
  if (end <= lparen + 1) {
    return false;
  }
  return parse_decimal_fixed(lparen + 1, end, out_value);
}

static bool parse_obis_ascii_unit_scale(const char *line, float &value) {
  const char *lparen = strchr(line, '(');
  if (!lparen) {
    return false;
  }
  const char *asterisk = strchr(lparen, '*');
  if (!asterisk) {
    return false;
  }
  const char *end = strchr(asterisk, ')');
  if (!end) {
    return false;
  }
  char unit_buf[8];
  size_t ulen = 0;
  for (const char *c = asterisk + 1; c < end && ulen + 1 < sizeof(unit_buf); ++c) {
    if (*c == ' ') {
      continue;
    }
    unit_buf[ulen++] = *c;
  }
  unit_buf[ulen] = '\0';
  if (ulen == 0) {
    return false;
  }
  if (strcmp(unit_buf, "Wh") == 0) {
    value *= 0.001f;
    return true;
  }
  return false;
}

static int8_t hex_nibble(char c) {
  if (c >= '0' && c <= '9') {
    return static_cast<int8_t>(c - '0');
  }
  if (c >= 'A' && c <= 'F') {
    return static_cast<int8_t>(10 + (c - 'A'));
  }
  if (c >= 'a' && c <= 'f') {
    return static_cast<int8_t>(10 + (c - 'a'));
  }
  return -1;
}

static bool parse_obis_hex_payload_u32(const char *line, uint32_t &out_value) {
  const char *lparen = strchr(line, '(');
  if (!lparen) {
    return false;
  }
  const char *cur = lparen + 1;
  uint32_t value = 0;
  size_t n = 0;
  while (*cur && *cur != ')' && *cur != '*') {
    int8_t nib = hex_nibble(*cur++);
    if (nib < 0) {
      if (n == 0) {
        continue;
      }
      break;
    }
    if (n >= 8) {
      return false;
    }
    value = (value << 4) | static_cast<uint32_t>(nib);
    n++;
  }
  if (n == 0) {
    return false;
  }
  out_value = value;
  return true;
}

static void meter_debug_log() {
  if (!SERIAL_DEBUG_MODE) {
    return;
  }
  uint32_t now_ms = millis();
  if (now_ms - g_last_log_ms < 60000) {
    return;
  }
  g_last_log_ms = now_ms;
  Serial.printf("meter: ok=%lu parse_fail=%lu overflow=%lu timeout=%lu bytes=%lu\n",
                static_cast<unsigned long>(g_frames_ok),
                static_cast<unsigned long>(g_frames_parse_fail),
                static_cast<unsigned long>(g_rx_overflow),
                static_cast<unsigned long>(g_rx_timeout),
                static_cast<unsigned long>(g_bytes_rx));
}

void meter_get_stats(MeterDriverStats &out) {
  out.frames_ok = g_frames_ok;
  out.frames_parse_fail = g_frames_parse_fail;
  out.rx_overflow = g_rx_overflow;
  out.rx_timeout = g_rx_timeout;
  out.bytes_rx = g_bytes_rx;
  out.last_rx_ms = g_last_rx_ms;
  out.last_good_frame_ms = g_last_good_frame_ms;
}

bool meter_poll_frame(const char *&frame, size_t &len) {
  frame = nullptr;
  len = 0;
  uint32_t now_ms = millis();

  if (g_rx_state == MeterRxState::InFrame && (now_ms - g_last_rx_ms > METER_FRAME_TIMEOUT_MS)) {
    g_rx_timeout++;
    g_rx_state = MeterRxState::WaitStart;
    g_frame_len = 0;
  }

  while (Serial2.available()) {
    char c = static_cast<char>(Serial2.read());
    g_bytes_rx++;
    g_last_rx_ms = now_ms;

    if (g_rx_state == MeterRxState::WaitStart) {
      if (c == '/') {
        g_rx_state = MeterRxState::InFrame;
        g_frame_len = 0;
        g_frame_buf[g_frame_len++] = c;
      }
      continue;
    }

    // Fast resync if a new telegram starts before current frame completed.
    if (c == '/') {
      g_frame_len = 0;
      g_frame_buf[g_frame_len++] = c;
      continue;
    }

    if (g_frame_len + 1 >= sizeof(g_frame_buf)) {
      g_rx_overflow++;
      g_rx_state = MeterRxState::WaitStart;
      g_frame_len = 0;
      continue;
    }

    g_frame_buf[g_frame_len++] = c;
    if (c == '!') {
      g_frame_buf[g_frame_len] = '\0';
      frame = g_frame_buf;
      len = g_frame_len;
      g_rx_state = MeterRxState::WaitStart;
      g_frame_len = 0;
      meter_debug_log();
      return true;
    }
  }

  meter_debug_log();
  return false;
}

bool meter_parse_frame(const char *frame, size_t len, MeterData &data) {
  if (!frame || len == 0) {
    return false;
  }
  bool got_any = false;
  bool energy_ok = false;
  bool total_p_ok = false;
  bool p1_ok = false;
  bool p2_ok = false;
  bool p3_ok = false;
  char line[128];
  size_t line_len = 0;

  for (size_t i = 0; i < len; ++i) {
    char c = frame[i];
    if (c == '\r') {
      continue;
    }
    if (c == '!') {
      if (line_len + 1 < sizeof(line)) {
        line[line_len++] = c;
      }
      line[line_len] = '\0';
      data.valid = energy_ok || total_p_ok || p1_ok || p2_ok || p3_ok;
      if (data.valid) {
        g_frames_ok++;
        g_last_good_frame_ms = millis();
      } else {
        g_frames_parse_fail++;
      }
      return data.valid;
    }
    if (c == '\n') {
      line[line_len] = '\0';
      if (line[0] == '!') {
        data.valid = energy_ok || total_p_ok || p1_ok || p2_ok || p3_ok;
        if (data.valid) {
          g_frames_ok++;
          g_last_good_frame_ms = millis();
        } else {
          g_frames_parse_fail++;
        }
        return data.valid;
      }

      ObisField field = detect_obis_field(line);
      float value = NAN;
      uint32_t meter_seconds = 0;
      switch (field) {
        case ObisField::Energy:
          if (parse_obis_ascii_payload_value(line, value)) {
            parse_obis_ascii_unit_scale(line, value);
            data.energy_total_kwh = value;
            energy_ok = true;
            got_any = true;
          }
          break;
        case ObisField::TotalPower:
          if (parse_obis_ascii_payload_value(line, value)) {
            data.total_power_w = value;
            total_p_ok = true;
            got_any = true;
          }
          break;
        case ObisField::Phase1:
          if (parse_obis_ascii_payload_value(line, value)) {
            data.phase_power_w[0] = value;
            p1_ok = true;
            got_any = true;
          }
          break;
        case ObisField::Phase2:
          if (parse_obis_ascii_payload_value(line, value)) {
            data.phase_power_w[1] = value;
            p2_ok = true;
            got_any = true;
          }
          break;
        case ObisField::Phase3:
          if (parse_obis_ascii_payload_value(line, value)) {
            data.phase_power_w[2] = value;
            p3_ok = true;
            got_any = true;
          }
          break;
        case ObisField::MeterSeconds:
          if (parse_obis_hex_payload_u32(line, meter_seconds)) {
            data.meter_seconds = meter_seconds;
            data.meter_seconds_valid = true;
          }
          break;
        default:
          break;
      }

      if (energy_ok && total_p_ok && p1_ok && p2_ok && p3_ok && data.meter_seconds_valid) {
        data.valid = true;
        g_frames_ok++;
        g_last_good_frame_ms = millis();
        return true;
      }

      line_len = 0;
      continue;
    }
    if (line_len + 1 < sizeof(line)) {
      line[line_len++] = c;
    }
  }

  data.valid = got_any;
  if (data.valid) {
    g_frames_ok++;
    g_last_good_frame_ms = millis();
  } else {
    g_frames_parse_fail++;
  }
  return data.valid;
}

bool meter_read(MeterData &data) {
  data.meter_seconds = 0;
  data.meter_seconds_valid = false;
  data.energy_total_kwh = NAN;
  data.total_power_w = NAN;
  data.phase_power_w[0] = NAN;
  data.phase_power_w[1] = NAN;
  data.phase_power_w[2] = NAN;
  data.valid = false;

  const char *frame = nullptr;
  size_t len = 0;
  if (!meter_poll_frame(frame, len)) {
    return false;
  }
  return meter_parse_frame(frame, len, data);
}
