void updateDisplay();

void updateMinHeap() { lastFreeHeap = ESP.getFreeHeap(); if (lastFreeHeap < minFreeHeap) minFreeHeap = lastFreeHeap; }

void logHealth(const char* reason) {
  updateMinHeap();
  Serial.print(F("[")); Serial.print(millis()/1000); Serial.print(F("s] ")); Serial.print(reason);
  Serial.print(F(" | heap=")); Serial.print(lastFreeHeap); Serial.print(F(" min=")); Serial.print(minFreeHeap);
  Serial.print(F(" | WiFi=")); Serial.print(WiFi.isConnected() ? F("OK") : F("--"));
  if (WiFi.isConnected()) { Serial.print(F(" ")); Serial.print(WiFi.localIP()); Serial.print(F(" RSSI=")); Serial.print(WiFi.RSSI()); }
  Serial.print(F(" | MQTT=")); Serial.println(mqtt.connected() ? F("OK") : F("--"));
}
void logHealthTimed() { if (millis() - lastLog > 30000) { lastLog = millis(); logHealth("status"); } }

static String trimCopy(const String& s) { String t = s; t.trim(); return t; }
static int toTrackIndex(int trackNo) { if (trackNo < 1 || trackNo > cfg.trackCount || trackNo > MAX_TRACKS) return -1; return trackNo - 1; }

static bool parseRowLine(const char* line,
                         String& fromStation, int& fromTrack,
                         String& toStation,   int& toTrack,
                         String& depTime,     String& arrTime,
                         String& train) {
  char buf[192];
  size_t n = strlcpy(buf, line, sizeof(buf));
  if (n >= sizeof(buf)) return false;

  const int EXPECT = 7;
  const char* parts[EXPECT] = {0};
  int count = 0;
  char* saveptr = nullptr;
  char* tok = strtok_r(buf, ";", &saveptr);
  while (tok && count < EXPECT) {
    parts[count++] = tok;
    tok = strtok_r(nullptr, ";", &saveptr);
  }
  if (count != EXPECT) return false;

  fromStation = trimCopy(String(parts[0]));
  fromTrack   = atoi(parts[1]);
  toStation   = trimCopy(String(parts[2]));
  toTrack     = atoi(parts[3]);
  depTime     = trimCopy(String(parts[4]));
  arrTime     = trimCopy(String(parts[5]));
  train       = trimCopy(String(parts[6]));
  return fromTrack > 0 && toTrack > 0;
}

void clearTrack(uint8_t idx) {
  trackTrain[idx] = ".";
  trackFrom[idx]  = ".";
  trackTime[idx]  = ".";
}

void subscribeArrivalTopics() {
  mqtt.subscribe(cfg.rowTopic);
  Serial.print(F("Subscribed: ")); Serial.println(cfg.rowTopic);
}

void setupMqtt() {
  if (cfg.mqttTls) {
    secureClient.setInsecure();
    secureClient.setBufferSizes(512, 512);
    mqtt.setClient(secureClient);
  } else mqtt.setClient(plainClient);
  mqtt.setBufferSize(384);
  mqtt.setServer(cfg.mqttHost, cfg.mqttPort);
  mqtt.setCallback([](char* topic, byte* payload, unsigned int length){
    if (String(topic) != String(cfg.rowTopic)) return;

    char msg[192];
    unsigned int L = (length < sizeof(msg)-1) ? length : sizeof(msg)-1;
    memcpy(msg, payload, L);
    msg[L] = '\0';

    lastPayloadTopic = String(topic);
    lastPayloadValue = String(msg);

    String fromSt, toSt, dep, arr, train;
    int fromTr = 0, toTr = 0;
    if (!parseRowLine(msg, fromSt, fromTr, toSt, toTr, dep, arr, train)) {
      lastMqttStatus = "row parse failed";
      Serial.println(F("Row parse failed"));
      return;
    }

    String home = String(cfg.homeStation); home.trim();
    bool toIsHome   = toSt.equalsIgnoreCase(home);
    bool fromIsHome = fromSt.equalsIgnoreCase(home);
    if (!(toIsHome || fromIsHome)) return;

    // V4-logikken: ankomst vises på destinationssporet. Når toget forlader hjemmestationen,
    // ryddes samme destinationsspor igen.
    int idx = toTrackIndex(toTr);
    if (idx < 0) { lastMqttStatus = "track out of range"; Serial.println(F("Track out of range")); return; }

    if (toIsHome) {
      trackTrain[idx] = train;
      trackFrom[idx]  = fromSt;
      trackTime[idx]  = arr;
    } else if (fromIsHome) {
      clearTrack(idx);
    }

    lastMqttStatus = "last row ok";
    updateDisplay();
  });
}

void connectMqttNonBlocking() {
  if (!WiFi.isConnected() || mqtt.connected() || millis() - lastMqttAttempt < 5000) return;
  lastMqttAttempt = millis();
  String id = String(cfg.hostname) + "-" + String(ESP.getChipId(), HEX);
  bool ok = strlen(cfg.mqttUser) > 0 ? mqtt.connect(id.c_str(), cfg.mqttUser, cfg.mqttPassword) : mqtt.connect(id.c_str());
  if (ok) { subscribeArrivalTopics(); lastMqttStatus = "connected"; updateDisplay(); logHealth("mqtt connected"); }
  else { lastMqttStatus = "connect failed rc=" + String(mqtt.state()); logHealth("mqtt failed"); }
}

void connectWifiOrPortal() {
  if (strlen(cfg.wifiSsid) == 0) portalMode = true;
  else {
    WiFi.mode(WIFI_STA); WiFi.hostname(cfg.hostname); WiFi.begin(cfg.wifiSsid, cfg.wifiPassword);
    unsigned long start = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) delay(250);
    portalMode = WiFi.status() != WL_CONNECTED;
  }
  if (portalMode) {
    logHealth("wifi portal");
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP("Ankomsttavle");
    dnsServer.start(53, "*", WiFi.softAPIP());
  } else logHealth("wifi connected");
}

String clipCol(const String& s, uint8_t len) { if (s.length() <= len) return s; return s.substring(0, len); }

void updateDisplay() {
  if (millis() - lastDisplayUpdate < 250 && lastDisplayUpdate != 0) return;
  lastDisplayUpdate = millis();
  display.clearDisplay();
  display.setTextColor(WHITE); display.setTextSize(1);
  display.setCursor(0,0); display.println(F("--- Ankomsttavle ---"));
  display.setCursor(0,8); display.println(F("Sp Tog     Fra  Tid"));
  for (uint8_t i=0; i<cfg.trackCount && i<MAX_TRACKS; i++) {
    int y = 16 + (i * 8);
    display.setCursor(0, y); display.print(i + 1);
    display.setCursor(18, y); display.print(clipCol(trackTrain[i], 7));
    display.setCursor(66, y); display.print(clipCol(trackFrom[i], 4));
    display.setCursor(96, y); display.print(clipCol(trackTime[i], 5));
  }
  display.display();
}
