#pragma once

#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/web_server_base/web_server_base.h"
#include <nvs.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdio>
#include <mutex>
#include <vector>

namespace esphome::jk_bms_selector {

namespace espbt = esphome::esp32_ble_tracker;

struct SeenDevice {
  uint64_t address;
  esp_ble_addr_type_t address_type;
  std::string mac;
  std::string name;
  int rssi;
  uint32_t seen_at;
  bool likely_jk;
};

struct BmsSlot {
  ble_client::BLEClient *client{nullptr};
  uint64_t address{0};
  esp_ble_addr_type_t type{BLE_ADDR_TYPE_PUBLIC};
  bool pending{false};
  bool connected{false};
};

class JkBmsSelector : public Component, public espbt::ESPBTDeviceListener, public AsyncWebHandler {
 public:
  void set_ble_client(ble_client::BLEClient *client) { slots_[0].client = client; }
  void set_ble_client_2(ble_client::BLEClient *client) { slots_[1].client = client; }
  float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH - 1.0f; }
  void setup() override;
  void loop() override;
  void dump_config() override;
  bool parse_device(const espbt::ESPBTDevice &device) override;
  bool canHandle(AsyncWebServerRequest *request) const override;
  void handleRequest(AsyncWebServerRequest *request) override;

 protected:
  static bool parse_mac_(const std::string &mac, uint64_t &value);
  static std::string format_mac_(uint64_t address);
  bool save_(size_t slot, uint64_t address, esp_ble_addr_type_t type);
  bool apply_(BmsSlot &slot);
  std::string page_();
  static std::string escape_(const std::string &value);
  std::mutex mutex_;
  std::vector<SeenDevice> devices_;
  std::array<BmsSlot, 2> slots_{};
  // Keep the original keys so a normal OTA preserves the first BMS.
  static constexpr const char *ADDRESS_KEYS[2] = {"address", "address2"};
  static constexpr const char *TYPE_KEYS[2] = {"addr_type", "addr_type2"};
};

inline void JkBmsSelector::setup() {
  nvs_handle_t handle;
  if (nvs_open("jk_bms_select", NVS_READONLY, &handle) == ESP_OK) {
    for (size_t i = 0; i < slots_.size(); i++) {
      uint64_t address = 0;
      uint8_t type = BLE_ADDR_TYPE_PUBLIC;
      if (nvs_get_u64(handle, ADDRESS_KEYS[i], &address) == ESP_OK) {
        nvs_get_u8(handle, TYPE_KEYS[i], &type);
        if (address <= 0xFFFFFFFFFFFFULL && type <= 3 && (i == 0 || address != slots_[0].address)) {
          slots_[i].address = address;
          slots_[i].type = static_cast<esp_ble_addr_type_t>(type);
        }
      }
    }
    nvs_close(handle);
  }
  for (auto &slot : slots_) slot.pending = !apply_(slot);
  web_server_base::global_web_server_base->add_handler(this);
}

inline void JkBmsSelector::loop() {
  std::lock_guard<std::mutex> lock(mutex_);
  for (auto &slot : slots_) {
    if (slot.pending && apply_(slot)) slot.pending = false;
    slot.connected = slot.client != nullptr && slot.client->connected();
  }
}

inline bool JkBmsSelector::apply_(BmsSlot &slot) {
  if (slot.client == nullptr) return true;
  const auto state = slot.client->state();
  // GATT registration must finish first. A disconnect in INIT would set a
  // deferred disconnect flag and needlessly cancel the next connection.
  if (state == espbt::ClientState::INIT) return false;
  if (state != espbt::ClientState::IDLE) {
    // Keep the original remote address until all old GATT events are handled.
    // CONNECTING only schedules a disconnect; changing its address now would
    // strand that client and stop the shared tracker from scanning either BMS.
    slot.client->set_enabled(false);
    return false;
  }
  slot.client->set_address(slot.address);
  slot.client->set_remote_addr_type(slot.type);
  slot.client->set_enabled(slot.address != 0);
  slot.connected = false;
  ESP_LOGI("jk_bms_selector", "BMS %u: %s", static_cast<unsigned>(&slot - slots_.data() + 1),
           slot.address ? slot.client->address_str() : "not selected");
  return true;
}

inline bool JkBmsSelector::save_(size_t slot, uint64_t address, esp_ble_addr_type_t type) {
  nvs_handle_t handle;
  if (nvs_open("jk_bms_select", NVS_READWRITE, &handle) != ESP_OK) return false;
  esp_err_t err = nvs_set_u64(handle, ADDRESS_KEYS[slot], address);
  if (err == ESP_OK) err = nvs_set_u8(handle, TYPE_KEYS[slot], static_cast<uint8_t>(type));
  if (err == ESP_OK) err = nvs_commit(handle);
  nvs_close(handle);
  return err == ESP_OK;
}

inline bool JkBmsSelector::parse_device(const espbt::ESPBTDevice &device) {
  SeenDevice item{device.address_uint64(), device.get_address_type(), device.address_str(), device.get_name(),
                  device.get_rssi(), millis(), false};
  for (const auto &uuid : device.get_service_uuids())
    if (uuid.contains(0xE0, 0xFF)) item.likely_jk = true;
  if (item.name.rfind("JK", 0) == 0 || item.name.rfind("603", 0) == 0 || item.name.rfind("606", 0) == 0)
    item.likely_jk = true;
  std::lock_guard<std::mutex> lock(mutex_);
  auto it = std::find_if(devices_.begin(), devices_.end(), [&](const SeenDevice &v) { return v.address == item.address; });
  if (it == devices_.end()) {
    if (devices_.size() >= 40) {
      it = std::min_element(devices_.begin(), devices_.end(), [](const SeenDevice &a, const SeenDevice &b) {
        return static_cast<uint32_t>(millis() - a.seen_at) > static_cast<uint32_t>(millis() - b.seen_at);
      });
      *it = item;
    } else {
      devices_.push_back(item);
    }
  } else {
    *it = item;
  }
  return false;
}

inline bool JkBmsSelector::parse_mac_(const std::string &mac, uint64_t &value) {
  if (mac.size() != 17) return false;
  for (size_t i = 0; i < mac.size(); i++) {
    if (i % 3 == 2 ? mac[i] != ':' : !std::isxdigit(static_cast<unsigned char>(mac[i]))) return false;
  }
  unsigned int b[6];
  if (std::sscanf(mac.c_str(), "%2x:%2x:%2x:%2x:%2x:%2x", &b[0], &b[1], &b[2], &b[3], &b[4], &b[5]) != 6)
    return false;
  value = 0;
  for (auto byte : b) value = (value << 8) | byte;
  return value != 0;
}

inline std::string JkBmsSelector::format_mac_(uint64_t address) {
  char buffer[18];
  std::snprintf(buffer, sizeof(buffer), "%02X:%02X:%02X:%02X:%02X:%02X",
                static_cast<unsigned>(address >> 40 & 255), static_cast<unsigned>(address >> 32 & 255),
                static_cast<unsigned>(address >> 24 & 255), static_cast<unsigned>(address >> 16 & 255),
                static_cast<unsigned>(address >> 8 & 255), static_cast<unsigned>(address & 255));
  return buffer;
}

inline std::string JkBmsSelector::escape_(const std::string &value) {
  std::string out;
  for (char c : value) {
    if (c == '&') out += "&amp;";
    else if (c == '<') out += "&lt;";
    else if (c == '>') out += "&gt;";
    else if (c == '\"') out += "&quot;";
    else out += c;
  }
  return out;
}

inline std::string JkBmsSelector::page_() {
  std::vector<SeenDevice> copy;
  std::array<BmsSlot, 2> slots;
  {
    std::lock_guard<std::mutex> lock(mutex_);
    copy = devices_;
    slots = slots_;
  }
  std::sort(copy.begin(), copy.end(), [](const SeenDevice &a, const SeenDevice &b) {
    if (a.likely_jk != b.likely_jk) return a.likely_jk > b.likely_jk;
    return a.rssi > b.rssi;
  });
  std::string html = R"HTML(<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>JK BMS setup</title><style>body{font:16px system-ui;max-width:850px;margin:30px auto;padding:0 14px;background:#111;color:#eee}a{color:#7cc4ff}.card{padding:16px;margin:10px 0;background:#222;border-radius:12px}button{padding:9px 14px;border:0;border-radius:8px;background:#1888e8;color:white;font-weight:600}button:disabled{background:#555}form{display:inline-block;margin:8px 8px 0 0}.jk{border:1px solid #28a745}.muted{color:#aaa;font-size:14px}</style><h1>Two JK BMS units</h1><p><a href="/">← All readings</a> · refreshes every 8 seconds</p><p>Each BMS connects independently. An offline BMS stays saved.</p>)HTML";
  for (size_t i = 0; i < slots.size(); i++) {
    const auto &slot = slots[i];
    const std::string number = std::to_string(i + 1);
    html += "<div class=card><b>BMS " + number + (i == 0 ? " · original" : " · new") + "</b><br>";
    html += slot.address ? format_mac_(slot.address) : "Not selected";
    html += "<br><span class=muted>";
    html += slot.pending ? "Updating connection" : (slot.connected ? "BLE connected" : "No BLE connection");
    html += "</span>";
    if (slot.address)
      html += "<form method=post action=/bms/clear><input type=hidden name=slot value=" + number + "><button>Clear BMS " + number + "</button></form>";
    html += "</div>";
  }
  if (copy.empty()) html += "<div class=card>Scanning Bluetooth… Power on the BMS and close its phone app if the connection is busy.</div>";
  size_t rendered = 0;
  for (const auto &d : copy) {
    const uint32_t age = (millis() - d.seen_at) / 1000;
    if (age > 60) continue;
    // Leave heap for both BLE links and the native API when phones fill the cache.
    if (rendered == 12) {
      html += "<p class=muted>Showing the first 12 devices. Likely JK BMS units are listed first.</p>";
      break;
    }
    rendered++;
    html += "<div class=\"card" + std::string(d.likely_jk ? " jk" : "") + "\"><b>";
    html += escape_(d.name.empty() ? "Unnamed" : d.name) + "</b><br><span class=muted>" + d.mac + " · RSSI " + std::to_string(d.rssi);
    html += " · " + std::to_string(age) + " seconds ago";
    if (d.likely_jk) html += " · likely JK BMS";
    html += "</span><br>";
    for (size_t i = 0; i < slots.size(); i++) {
      const std::string number = std::to_string(i + 1);
      const bool used_elsewhere = slots[1 - i].address == d.address;
      html += "<form method=post action=/bms/select><input type=hidden name=slot value=" + number;
      html += "><input type=hidden name=mac value=\"" + d.mac + "\"><input type=hidden name=type value=\"" + std::to_string(static_cast<int>(d.address_type)) + "\"><button";
      if (used_elsewhere || slots[i].address == d.address) html += " disabled";
      html += ">" + std::string(slots[i].address == d.address ? "Selected: BMS " : "Connect as BMS ") + number + "</button></form>";
    }
    html += "</div>";
  }
  html += "<script>setTimeout(()=>location.reload(),8000)</script></html>";
  return html;
}

inline bool JkBmsSelector::canHandle(AsyncWebServerRequest *request) const {
  char buffer[AsyncWebServerRequest::URL_BUF_SIZE];
  auto url = request->url_to(buffer);
  return url == "/bms" || url == "/bms/select" || url == "/bms/clear";
}

inline void JkBmsSelector::handleRequest(AsyncWebServerRequest *request) {
  char buffer[AsyncWebServerRequest::URL_BUF_SIZE];
  const std::string url(request->url_to(buffer).c_str());
  if (request->method() == HTTP_POST && (url == "/bms/select" || url == "/bms/clear")) {
    const std::string slot_arg = request->arg("slot");
    // An old single-BMS form without a slot still selects the original slot.
    const size_t index = slot_arg == "2" ? 1 : 0;
    if ((slot_arg.empty() && url == "/bms/clear") ||
        (!slot_arg.empty() && slot_arg != "1" && slot_arg != "2") || slots_[index].client == nullptr) {
      request->send(409, "text/plain; charset=utf-8", "Invalid BMS slot");
      return;
    }
    uint64_t address = 0;
    esp_ble_addr_type_t type = BLE_ADDR_TYPE_PUBLIC;
    if (url == "/bms/select") {
      const std::string type_arg = request->arg("type");
      if (!parse_mac_(request->arg("mac"), address) || type_arg.size() != 1 || type_arg[0] < '0' || type_arg[0] > '3') {
        request->send(409, "text/plain; charset=utf-8", "Invalid BLE address or address type");
        return;
      }
      type = static_cast<esp_ble_addr_type_t>(type_arg[0] - '0');
    }
    int error_code = 0;
    const char *error_message = nullptr;
    {
      std::lock_guard<std::mutex> lock(mutex_);
      if (address != 0 && slots_[1 - index].address == address) {
        error_code = 409;
        error_message = "This BMS is already assigned to the other slot";
      } else if (!save_(index, address, type)) {
        error_code = 500;
        error_message = "Could not save selection";
      } else {
        slots_[index].address = address;
        slots_[index].type = type;
        slots_[index].pending = true;
      }
    }
    if (error_code != 0) {
      request->send(error_code, "text/plain; charset=utf-8", error_message);
      return;
    }
    request->redirect("/bms");
    return;
  }
  if (request->method() != HTTP_GET || url != "/bms") {
    request->send(404, "text/plain", "Not found");
    return;
  }
  const std::string body = page_();
  request->send(200, "text/html; charset=utf-8", body.c_str());
}

inline void JkBmsSelector::dump_config() {
  for (size_t i = 0; i < slots_.size(); i++)
    ESP_LOGCONFIG("jk_bms_selector", "BMS %u: %s", static_cast<unsigned>(i + 1),
                  slots_[i].address ? format_mac_(slots_[i].address).c_str() : "none");
}

}  // namespace esphome::jk_bms_selector
