#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
#include <EEPROM.h>
#include <time.h>
#include <FastLED.h>
#include <ArduinoOTA.h>

// ------------------- Projektname -------------------
#define PROJECT_NAME "HollowClock"

// ------------------- Hardware -------------------
#define EN_PIN D0
#define DIR_PIN D4
#define STEP_PIN D3
#define LED_PIN D6
#define ANALOG_PIN A0
#define NUM_LEDS 60
CRGB strip[NUM_LEDS];

// ------------------- WLAN -------------------
const char* ssid = "DarkNet";
const char* password = "7DDBD0A6BC123";

// ------------------- Uhrparameter (variabel) -------------------
uint32_t stepsPerRev = 122870UL;
const uint8_t TICKS_PER_HOUR = 10;
const unsigned long STEP_TRIGGER_INTERVAL_MS = 6000;

const uint16_t NORMAL_STEP_INTERVAL_US = 10000;
const uint16_t FAST_STEP_INTERVAL_US = 300;
uint16_t currentStepInterval = NORMAL_STEP_INTERVAL_US;

// ------------------- EEPROM -------------------
const int EEPROM_SIZE = 512;
const int ADDR_CURRENT_STEP = 0;
const int ADDR_BRIGHTNESS = 20;
const int ADDR_NIGHTDIM = 24;
const int ADDR_COLOR = 28;
const int ADDR_MODE = 32;
const int ADDR_NIGHT_BRIGHTNESS = 36;
const int ADDR_NIGHT_START = 40;
const int ADDR_NIGHT_END = 44;
const int ADDR_STEPS_PER_REV = 48;
const int ADDR_RAINBOW_COLOR = 52;
const int ADDR_AUTO_SYNC_MINUTE = 56;
const int ADDR_SENSOR_TRIGGER_VALUE = 60;
const int ADDR_SENSOR_TRIGGER_MODE = 64;
const int ADDR_AUTO_SYNC_SECOND = 68;  // NEU: Sekunde für Auto-Sync

// ------------------- Globale Variablen -------------------
ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater;

uint64_t currentStep = 0;
bool moving = false;
uint64_t moveStepsRemaining = 0;
uint64_t moveTargetStep = 0;
unsigned long lastStepMicros = 0;
unsigned long lastStepTrigger = 0;

bool syncMovePending = false;
unsigned long syncMoveStartTime = 0;
uint64_t syncMoveStartStep = 0;

uint8_t brightness = 128;
uint8_t nightBrightness = 20;
bool nightDim = true;
uint8_t stripColor = 96;
bool rainbowColor = false;

uint8_t nightStartHour = 22;
uint8_t nightEndHour = 6;

enum LedMode { MODE_SECOND_TICK = 0,
               MODE_RUNNING,
               MODE_FILL };
LedMode ledMode = MODE_SECOND_TICK;

unsigned long lastLEDTime = 0;
int lightCount = 0;
bool lightUpDown = true;
uint8_t randomRainbow = 0;
uint8_t targetBrightness = 128;
uint8_t currentBrightness = 128;
unsigned long lastBrightnessUpdate = 0;

bool forceLedRefresh = false;
char ntpTimeStr[20] = "noch keine Sync";

unsigned long lastNTPSync = 0;
const unsigned long NTP_RESYNC_INTERVAL = 6UL * 3600UL * 1000UL;

// Median Filter für Analogwert
const int MEDIAN_SAMPLES = 21;
int analogBuffer[MEDIAN_SAMPLES] = { 0 };
int analogBufferIndex = 0;

// Auto-Sync bei Sensor-Trigger
int sensorTriggerValue = 1024;
bool sensorTriggerAbove = true;
const int SENSOR_TOLERANCE = 5;
bool lastSensorTriggered = false;
unsigned long lastAutoSync = 0;
const unsigned long AUTO_SYNC_COOLDOWN = 300000;
unsigned long sensorTriggerStart = 0;
const unsigned long SENSOR_STABLE_TIME = 1000;
bool sensorEnteredLog = false;
bool cooldownLogShown = false;  // NEU: Verhindert Log-Spam
uint8_t autoSyncMinute = 32;
uint8_t autoSyncSecond = 0;  // NEU: Auto-Sync Sekunde

// Log-System
const int MAX_LOG_ENTRIES = 50;
String logBuffer[MAX_LOG_ENTRIES];
int logIndex = 0;

// ---------- Forward declarations ----------
bool getESP8266Time(struct tm* timeinfo);
bool timeToGermanLocal(time_t t, struct tm* out);

// ------------------- Hilfsfunktionen -------------------
void addLog(String message) {
  struct tm timeinfo;
  String timestamp = "";
  if (getESP8266Time(&timeinfo)) {
    char buf[20];
    snprintf(buf, sizeof(buf), "[%02d:%02d:%02d] ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    timestamp = String(buf);
  }
  logBuffer[logIndex] = timestamp + message;
  logIndex = (logIndex + 1) % MAX_LOG_ENTRIES;
  Serial.println(message);
}

String getLogHTML() {
  String html = "";
  for (int i = 0; i < MAX_LOG_ENTRIES; i++) {
    int idx = (logIndex - 1 - i + MAX_LOG_ENTRIES) % MAX_LOG_ENTRIES;
    if (logBuffer[idx].length() > 0) {
      html += logBuffer[idx] + "\n";
    }
  }
  return html;
}

// ------------------- Basisfunktionen -------------------
void enableDriver() {
  digitalWrite(EN_PIN, LOW);
}
void disableDriver() {
  digitalWrite(EN_PIN, HIGH);
}

void saveSettings() {
  EEPROM.put(ADDR_BRIGHTNESS, brightness);
  EEPROM.put(ADDR_NIGHTDIM, nightDim);
  EEPROM.put(ADDR_COLOR, stripColor);
  EEPROM.put(ADDR_MODE, ledMode);
  EEPROM.put(ADDR_NIGHT_BRIGHTNESS, nightBrightness);
  EEPROM.put(ADDR_NIGHT_START, nightStartHour);
  EEPROM.put(ADDR_NIGHT_END, nightEndHour);
  EEPROM.put(ADDR_STEPS_PER_REV, stepsPerRev);
  EEPROM.put(ADDR_RAINBOW_COLOR, rainbowColor);
  EEPROM.put(ADDR_AUTO_SYNC_MINUTE, autoSyncMinute);
  EEPROM.put(ADDR_AUTO_SYNC_SECOND, autoSyncSecond);  // NEU
  EEPROM.put(ADDR_SENSOR_TRIGGER_VALUE, sensorTriggerValue);
  EEPROM.put(ADDR_SENSOR_TRIGGER_MODE, sensorTriggerAbove);
  EEPROM.commit();
}

void loadSettings() {
  EEPROM.get(ADDR_BRIGHTNESS, brightness);
  EEPROM.get(ADDR_NIGHTDIM, nightDim);
  EEPROM.get(ADDR_COLOR, stripColor);
  EEPROM.get(ADDR_MODE, ledMode);
  EEPROM.get(ADDR_NIGHT_BRIGHTNESS, nightBrightness);
  EEPROM.get(ADDR_NIGHT_START, nightStartHour);
  EEPROM.get(ADDR_NIGHT_END, nightEndHour);
  EEPROM.get(ADDR_STEPS_PER_REV, stepsPerRev);
  EEPROM.get(ADDR_RAINBOW_COLOR, rainbowColor);
  EEPROM.get(ADDR_AUTO_SYNC_MINUTE, autoSyncMinute);
  EEPROM.get(ADDR_AUTO_SYNC_SECOND, autoSyncSecond);  // NEU
  EEPROM.get(ADDR_SENSOR_TRIGGER_VALUE, sensorTriggerValue);
  EEPROM.get(ADDR_SENSOR_TRIGGER_MODE, sensorTriggerAbove);

  if (brightness > 255) brightness = 128;
  if (nightBrightness > 255) nightBrightness = 20;
  if (ledMode > MODE_FILL) ledMode = MODE_SECOND_TICK;
  if (nightStartHour > 23) nightStartHour = 22;
  if (nightEndHour > 23) nightEndHour = 6;
  if (stepsPerRev < 1000UL || stepsPerRev > 2000000UL) stepsPerRev = 122870UL;
  if (autoSyncMinute > 59) autoSyncMinute = 32;
  if (autoSyncSecond > 59) autoSyncSecond = 0;  // NEU
  if (sensorTriggerValue < 0 || sensorTriggerValue > 1024) sensorTriggerValue = 1024;
}

// ------------------- Zeit → Schritt-Umrechnung -------------------
uint64_t totalSteps12h() {
  return (uint64_t)stepsPerRev * 12ULL;
}

uint64_t timeToStepIndex(uint8_t hour, uint8_t minute, uint8_t second) {
  const uint32_t SECONDS_PER_REV = 3600UL;
  uint8_t hour12 = hour % 12;
  uint32_t totalSeconds = (uint32_t(hour12) * 3600UL) + (uint32_t(minute) * 60UL) + uint32_t(second);
  uint64_t num = (uint64_t)totalSeconds * (uint64_t)stepsPerRev + (SECONDS_PER_REV / 2);
  return (num / SECONDS_PER_REV) % totalSteps12h();
}

uint64_t calcForwardDelta(uint64_t fromStep, uint64_t toStep) {
  const uint64_t TOTAL = totalSteps12h();
  if (toStep >= fromStep) return toStep - fromStep;
  return (TOTAL - fromStep) + toStep;
}

// ------------------- Stepper (nicht-blockierend) -------------------
void startMove(uint64_t delta, bool fast = false, bool silent = false, bool manual = false) {
  if (delta == 0 || moving) return;
  enableDriver();
  delay(2);
  digitalWrite(DIR_PIN, LOW);
  currentStepInterval = fast ? FAST_STEP_INTERVAL_US : NORMAL_STEP_INTERVAL_US;
  moveStepsRemaining = delta;
  moveTargetStep = (currentStep + delta) % totalSteps12h();
  lastStepMicros = micros();
  moving = true;

  if (manual) {
    fill_solid(strip, NUM_LEDS, CRGB::Blue);
    FastLED.show();
  }

  if (!silent) {
    addLog("Bewegung gestartet: " + String((unsigned long)delta) + " Schritte (" + String(fast ? "schnell" : "normal") + ")");
  }
}

void handleStepper() {
  if (!moving) return;
  unsigned long now = micros();
  if ((now - lastStepMicros) >= currentStepInterval) {
    lastStepMicros = now;
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(5);
    digitalWrite(STEP_PIN, LOW);
    currentStep++;
    if (currentStep >= totalSteps12h()) currentStep -= totalSteps12h();

    if (--moveStepsRemaining == 0) {
      moving = false;
      disableDriver();

      if (syncMovePending) {
        unsigned long elapsedMs = millis() - syncMoveStartTime;
        unsigned long elapsedSeconds = elapsedMs / 1000;
        uint64_t catchupSteps = ((uint64_t)elapsedSeconds * (uint64_t)stepsPerRev) / 3600ULL;
        if (catchupSteps > 0) {
          addLog("Sync abgeschlossen, fahre " + String((unsigned long)catchupSteps) + " Schritte nach (" + String(elapsedSeconds) + "s vergangen)");
          syncMovePending = false;
          startMove(catchupSteps, true, false, false);
          return;
        } else {
          addLog("Sync abgeschlossen (keine Nachkorrektur nötig)");
          syncMovePending = false;
        }
      }
      if (currentStepInterval == FAST_STEP_INTERVAL_US) {
        FastLED.clear(true);
        forceLedRefresh = true;
      }
    }
  }
}

void handleNormalClockMove() {
  if (moving) return;
  unsigned long nowMs = millis();
  if (nowMs - lastStepTrigger < STEP_TRIGGER_INTERVAL_MS) return;
  lastStepTrigger = nowMs;

  uint64_t stepsPerTick = stepsPerRev / (60ULL * TICKS_PER_HOUR);
  if (stepsPerTick == 0) stepsPerTick = 1;

  currentStep += stepsPerTick;
  if (currentStep >= totalSteps12h()) currentStep -= totalSteps12h();

  startMove(stepsPerTick, false, true);
}

void checkAutoSync() {
  if (moving || syncMovePending) return;

  int analogValue = getFilteredAnalogValue();

  // Prüfung abhängig vom Modus
  bool sensorTriggered;
  if (sensorTriggerAbove) {
    sensorTriggered = (analogValue >= sensorTriggerValue - SENSOR_TOLERANCE && analogValue <= sensorTriggerValue + SENSOR_TOLERANCE);
  } else {
    sensorTriggered = (analogValue <= sensorTriggerValue + SENSOR_TOLERANCE && analogValue >= sensorTriggerValue - SENSOR_TOLERANCE);
  }

  if (sensorTriggered && !lastSensorTriggered && !sensorEnteredLog) {
    sensorEnteredLog = true;
    sensorTriggerStart = millis();
    addLog("Sensor erreicht (Wert: " + String(analogValue) + "), warte auf Stabilität...");
  }

  if (sensorTriggered && lastSensorTriggered) {
    unsigned long now = millis();
    if (now - sensorTriggerStart >= SENSOR_STABLE_TIME) {
      if (now - lastAutoSync > AUTO_SYNC_COOLDOWN) {
        struct tm timeinfo;
        if (getESP8266Time(&timeinfo)) {
          // Berechne Zeitdifferenz zur Sync-Zeit in Sekunden
          int targetTotalSec = autoSyncMinute * 60 + autoSyncSecond;
          int currentTotalSec = timeinfo.tm_min * 60 + timeinfo.tm_sec;
          
          int secDiff = currentTotalSec - targetTotalSec;
          if (secDiff < 0) secDiff += 3600;  // Wrap around für vorherige Stunde

          // Zeitfenster: ±13 Minuten = ±780 Sekunden
          if (secDiff <= 780 || secDiff >= 2820) {
            uint64_t sensorPosition = timeToStepIndex(timeinfo.tm_hour, autoSyncMinute, autoSyncSecond);
            uint64_t realStep = timeToStepIndex(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
            uint64_t delta = calcForwardDelta(sensorPosition, realStep);
            uint64_t maxDelta = stepsPerRev / 2;
            if (delta > 0 && delta < maxDelta) {
              currentStep = sensorPosition;
              syncMovePending = true;
              syncMoveStartTime = millis();
              syncMoveStartStep = currentStep;
              startMove(delta, true);
              lastAutoSync = now;
              cooldownLogShown = false;  // Reset für nächsten Cooldown
              char logMsg[140];
              snprintf(logMsg, sizeof(logMsg), "Auto-Sync: Sensor bei %02d:%02d, Ziel: %02d:%02d:%02d, Delta: %llu (%.1f Min)",
                       autoSyncMinute, autoSyncSecond, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
                       delta, (float)delta / (float)stepsPerRev * 60.0);
              addLog(String(logMsg));
              sensorEnteredLog = false;
            } else {
              addLog("Auto-Sync abgebrochen: Delta zu groß (" + String((unsigned long)delta) + " Schritte)");
              sensorEnteredLog = false;
            }
          } else {
            addLog("Auto-Sync übersprungen: Zeit " + String(timeinfo.tm_min) + ":" + String(timeinfo.tm_sec) + 
                   " nicht im Bereich " + String(autoSyncMinute) + ":" + String(autoSyncSecond) + " ±13min");
            sensorEnteredLog = false;
          }
        }
      } else {
        // Log nur einmal während Cooldown zeigen
        if (!cooldownLogShown) {
          addLog("Auto-Sync übersprungen: Cooldown aktiv");
          cooldownLogShown = true;
        }
      }
      sensorTriggerStart = 0;
    }
  }

  if (!sensorTriggered && lastSensorTriggered) {
    addLog("Sensor verlassen");
    cooldownLogShown = false;  // Reset wenn Sensor verlassen wird
  }

  lastSensorTriggered = sensorTriggered;
}

// ------------------- Zeit-Helper -------------------
int dayOfWeek(int y, int m, int d) {
  static int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
  if (m < 3) y -= 1;
  return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
}

int daysInMonth(int year, int month) {
  static const int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2) {
    bool leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
    return leap ? 29 : 28;
  }
  return mdays[month - 1];
}

int lastSundayOfMonth(int year, int month) {
  int lastDay = daysInMonth(year, month);
  int w = dayOfWeek(year, month, lastDay);
  return lastDay - w;
}

bool isCEST_UTC(const struct tm& utc_tm) {
  int y = utc_tm.tm_year + 1900;
  int m = utc_tm.tm_mon + 1;
  int d = utc_tm.tm_mday;
  int h = utc_tm.tm_hour;

  if (m > 3 && m < 10) return true;
  if (m < 3 || m > 10) return false;

  if (m == 3) {
    int ls = lastSundayOfMonth(y, 3);
    if (d > ls) return true;
    if (d < ls) return false;
    return (h >= 1);
  }

  if (m == 10) {
    int ls = lastSundayOfMonth(y, 10);
    if (d < ls) return true;
    if (d > ls) return false;
    return (h < 1);
  }

  return false;
}

bool getESP8266Time(struct tm* timeinfo) {
  time_t now = time(nullptr);
  if (now < 100000) return false;

  struct tm utc_tm;
  if (gmtime_r(&now, &utc_tm) == nullptr) return false;

  bool dst = isCEST_UTC(utc_tm);

  int32_t offset = 3600 + (dst ? 3600 : 0);

  time_t local = now + offset;
  struct tm* lt = gmtime(&local);
  if (lt == nullptr) return false;
  memcpy(timeinfo, lt, sizeof(struct tm));
  return true;
}

bool timeToGermanLocal(time_t t, struct tm* out) {
  struct tm utc_tm;
  if (gmtime_r(&t, &utc_tm) == nullptr) return false;
  bool dst = isCEST_UTC(utc_tm);
  int32_t offset = 3600 + (dst ? 3600 : 0);
  time_t local = t + offset;
  struct tm* lt = gmtime(&local);
  if (lt == nullptr) return false;
  memcpy(out, lt, sizeof(struct tm));
  return true;
}

// ------------------- LED -------------------
int mapLogicalToPhysical(int logicalIndex) {
  if (logicalIndex % 2 == 0)
    return NUM_LEDS - 1 - (logicalIndex / 2);
  else
    return (logicalIndex - 1) / 2;
}

void ledModeFillSimple() {
  static unsigned long lastChange = 0;
  static bool filling = true;
  static int index = 0;
  unsigned long now = millis();

  if (now - lastChange > 15) {
    lastChange = now;
    int physicalIndex = mapLogicalToPhysical(index);
    if (filling) {
      int hue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
      strip[physicalIndex].setHue(hue);
      index++;
      if (index >= NUM_LEDS) {
        index = 0;
        filling = false;
      }
    } else {
      strip[physicalIndex] = CRGB::Black;
      index++;
      if (index >= NUM_LEDS) {
        index = 0;
        filling = true;
        if (rainbowColor) randomRainbow = random8();
      }
    }
    FastLED.show();
  }
}

void updateLEDs() {
  if (moving && currentStepInterval == FAST_STEP_INTERVAL_US) return;

  struct tm timeinfo;
  if (!getESP8266Time(&timeinfo)) return;

  bool nightActive = nightDim && ((nightStartHour < nightEndHour && timeinfo.tm_hour >= nightStartHour && timeinfo.tm_hour < nightEndHour) || (nightStartHour > nightEndHour && (timeinfo.tm_hour >= nightStartHour || timeinfo.tm_hour < nightEndHour)));

  targetBrightness = nightActive ? nightBrightness : brightness;

  unsigned long now = millis();
  if (now - lastBrightnessUpdate > 50) {
    lastBrightnessUpdate = now;
    if (currentBrightness < targetBrightness) currentBrightness++;
    else if (currentBrightness > targetBrightness) currentBrightness--;
    FastLED.setBrightness(currentBrightness);
  }

  static int lastSecondDisplay = -1;
  int currentSecond = timeinfo.tm_sec;

  if (ledMode == MODE_SECOND_TICK) {
    if (forceLedRefresh || currentSecond != lastSecondDisplay) {
      forceLedRefresh = false;
      lastSecondDisplay = currentSecond;
      FastLED.clear();
      int index = map(currentSecond, 0, 59, 0, NUM_LEDS - 1);
      int hue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
      strip[mapLogicalToPhysical(index)].setHue(hue);
      FastLED.show();
      if (rainbowColor && currentSecond == 0) randomRainbow = random8();
    }
  } else if (ledMode == MODE_RUNNING) {
    if (now - lastLEDTime < 50) return;
    lastLEDTime = now;
    int index = lightUpDown ? lightCount : NUM_LEDS - 1 - lightCount;
    int hueValue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
    if (lightUpDown)
      strip[mapLogicalToPhysical(index)].setHue(hueValue);
    else
      strip[mapLogicalToPhysical(index)] = CRGB::Black;
    FastLED.show();
    lightCount++;
    if (lightCount >= NUM_LEDS) {
      lightCount = 0;
      lightUpDown = !lightUpDown;
      if (!lightUpDown && rainbowColor) randomRainbow = random8();
    }
  } else if (ledMode == MODE_FILL) {
    ledModeFillSimple();
  }
}

// ------------------- OTA (IDE) -------------------
void setupOTA() {
  ArduinoOTA.setHostname(PROJECT_NAME);
  ArduinoOTA.setPassword("espupdate");

  ArduinoOTA.onStart([]() {
    disableDriver();
    FastLED.clear(true);
    addLog("OTA Update gestartet");
  });
  ArduinoOTA.onEnd([]() {
    addLog("OTA Update abgeschlossen");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {});
  ArduinoOTA.onError([](ota_error_t error) {
    char buf[50];
    snprintf(buf, sizeof(buf), "OTA Fehler: %u", error);
    addLog(String(buf));
  });

  ArduinoOTA.begin();
  addLog("OTA (IDE) bereit");
}

// ------------------- Web UI -------------------
String htmlHeader() {
  return String("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'/>") + "<title>" + String(PROJECT_NAME) + "</title>"
                                                                                                                                                                               "<style>"
                                                                                                                                                                               "body{font-family:Segoe UI,Arial;background:#1e1e2f;color:#eee;margin:0;display:flex;justify-content:center;}"
                                                                                                                                                                               ".wrapper{max-width:720px;width:100%;padding:2em;}"
                                                                                                                                                                               "h1{color:#4fc3f7;margin-top:0;text-align:center}"
                                                                                                                                                                               ".card{background:#2b2b3c;padding:1em;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.4);margin-bottom:1.5em}"
                                                                                                                                                                               "form{display:block}"
                                                                                                                                                                               "button,input[type=submit]{background:#4fc3f7;color:#000;font-weight:600;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;transition:.2s}"
                                                                                                                                                                               "button:hover,input[type=submit]:hover{background:#81d4fa}"
                                                                                                                                                                               "input,select{width:100%;padding:8px;margin-top:6px;border-radius:6px;border:1px solid #555;background:#1e1e2f;color:#fff;box-sizing:border-box}"
                                                                                                                                                                               "label{display:block;margin-top:.8em}"
                                                                                                                                                                               "a{color:#4fc3f7}"
                                                                                                                                                                               "#infobox{display:none;background:#263238;border-left:4px solid #81d4fa;padding:10px;border-radius:6px;margin-bottom:1.2em;white-space:pre-wrap}"
                                                                                                                                                                               "#infobox.show{display:block}"
                                                                                                                                                                               "</style>"
                                                                                                                                                                               "</head><body><div class='wrapper'><h1>"
         + String(PROJECT_NAME) + "</h1>";
}

void handleRoot() {
  struct tm timeinfo;
  char timeStr[16] = "??:??:??";
  if (getESP8266Time(&timeinfo))
    snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);

  String html = htmlHeader();
  html += "<div id='infobox'></div>";

  html += "<div class='card'><h3>Status</h3>";
  html += "<b>Aktuelle Zeit:</b> <span id='curTime'>" + String(timeStr) + "</span><br>";
  html += "<b>NTP-Zeit (letzte Sync):</b> <span id='ntpTime'>--:--:--</span><br>";
  html += "<b>Zeitzone:</b> Europe/Berlin (CET/CEST)<br>";
  html += "<b>WLAN:</b> " + String(WiFi.status() == WL_CONNECTED ? "Verbunden" : "Getrennt") + "<br>";
  html += "<b>Analogwert:</b> <span id='analogVal'>--</span><br>";
  html += "<b>Auto-Sync Zeit:</b> " + String(autoSyncMinute) + ":" + String(autoSyncSecond) + "<br>";
  html += "<b>Sensor-Triggerwert:</b> " + String(sensorTriggerValue) + " (" + String(sensorTriggerAbove ? "über" : "unter") + ")<br>";
  html += "<b>OTA (IDE):</b> aktiv • <b>OTA (Web):</b> <a href='/update'>/update</a><br>";
  html += "<b>Steps/Rev:</b> " + String(stepsPerRev) + "</div>";

  html += "<div class='card'><h3>System-Log</h3>";
  html += "<pre id='logBox' style='max-height:200px;overflow-y:auto;background:#1e1e2f;padding:8px;border-radius:6px;margin:0'>";
  html += getLogHTML();
  html += "</pre></div>";

  html += "<div class='card'><h3>Manuell vorwärts bewegen</h3>"
          "<form onsubmit='return submitForm(this,\"/move\")'>"
          "<label>Schritte (1 .. 500000):</label><input name='steps' type='number' min='1' max='500000' value='200'>"
          "<label>Schnellmodus:</label><select name='fast'><option value='0'>Nein</option><option value='1' selected>Ja</option></select>"
          "<input type='submit' value='Start'>"
          "</form></div>";

  html += "<div class='card'><h3>Zeit / NTP</h3>"
          "<form onsubmit='return submitForm(this,\"/syncntp\")'>"
          "<input type='submit' value='Zeit mit NTP synchronisieren'>"
          "</form></div>";

  html += "<div class='card'><h3>Auto-Sync Einstellungen</h3>"
          "<form onsubmit='return submitForm(this,\"/setautosync\")'>"
          "<label>Sensor-Zeit (MM:SS):</label>"
          "<input name='ast' placeholder='z. B. 32:00' value='"
          + String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "'>"
          "<label>Sensor-Triggerwert (0-1024):</label>"
          "<input name='stv' type='number' min='0' max='1024' value='"
          + String(sensorTriggerValue) + "'>"
                                         "<label>Trigger-Modus:</label>"
                                         "<select name='stm'>"
          + String(sensorTriggerAbove ? "<option value='1' selected>Über dem Wert</option><option value='0'>Unter dem Wert</option>"
                                      : "<option value='1'>Über dem Wert</option><option value='0' selected>Unter dem Wert</option>")
          + "</select>"
            "<input type='submit' value='Speichern'>"
            "</form></div>";

  html += "<div class='card'><h3>Zeigerzeit setzen</h3>"
          "<form onsubmit='return submitForm(this,\"/setpos\")'>"
          "<label>Aktuelle Zeigerzeit (HH:MM):</label>"
          "<input name='t' placeholder='z. B. 12:34' required>"
          "<input type='submit' value='Zeiger auf aktuelle Zeit stellen'>"
          "</form></div>";

  html += "<div class='card'><h3>LED & Einstellungen</h3>"
          "<form onsubmit='return submitForm(this,\"/setled\")'>"
          "<label>Brightness:</label><input name='b' type='number' min='0' max='255' value='"
          + String(brightness) + "'>"
                                 "<label>Nacht-Brightness:</label><input name='nb' type='number' min='0' max='255' value='"
          + String(nightBrightness) + "'>"
                                      "<label>Nachtdimmen:</label><select name='n'>"
          + String(nightDim ? "<option value='1' selected>An</option><option value='0'>Aus</option>"
                            : "<option value='1'>An</option><option value='0' selected>Aus</option>")
          + "</select>"
            "<label>Nachtdimmung Start (Stunde):</label><input name='ns' type='number' min='0' max='23' value='"
          + String(nightStartHour) + "'>"
                                     "<label>Nachtdimmung Ende (Stunde):</label><input name='ne' type='number' min='0' max='23' value='"
          + String(nightEndHour) + "'>"
                                   "<label>Farbe Hue:</label><input name='c' type='number' min='0' max='255' value='"
          + String(stripColor) + "'>"
                                 "<label>Regenbogen:</label><select name='r'>"
          + String(rainbowColor ? "<option value='0'>Aus</option><option value='1' selected>An</option>"
                                : "<option value='0' selected>Aus</option><option value='1'>An</option>")
          + "</select>"
            "<label>LED-Modus:</label><select name='m'>"
          + String(ledMode == MODE_SECOND_TICK ? "<option value='0' selected>Sekundenzeiger</option>" : "<option value='0'>Sekundenzeiger</option>") + String(ledMode == MODE_RUNNING ? "<option value='1' selected>Lauflicht</option>" : "<option value='1'>Lauflicht</option>") + String(ledMode == MODE_FILL ? "<option value='2' selected>Sekunden-Füllung</option>" : "<option value='2'>Sekunden-Füllung</option>") + "</select>"
                                                                                                                                                                                                                                                                                                                                                                                                                            "<label>Steps per Revolution:</label><input name='spr' type='number' min='1000' max='2000000' value='"
          + String(stepsPerRev) + "'>"
                                  "<input type='submit' value='Speichern'>"
                                  "</form></div>";

  html +=
    "<iframe name='hiddenFrame' style='display:none'></iframe>"
    "<script>"
    "let hideTimer;"
    "function showInfo(msg){"
    "const box=document.getElementById('infobox');"
    "box.textContent=msg;box.classList.add('show');"
    "clearTimeout(hideTimer);"
    "hideTimer=setTimeout(()=>{box.classList.remove('show');},10000);"
    "}"
    "function submitForm(form,path){"
    "form.action=path;form.target='hiddenFrame';form.method='POST';"
    "const formData=new FormData(form);"
    "formData.append('response','1');"
    "setTimeout(()=>{"
    "fetch(path,{method:'POST',body:new URLSearchParams(formData)})"
    ".then(r=>r.text())"
    ".then(t=>{showInfo(t);updateAnalog();updateLog();updateTime();})"
    ".catch(e=>showInfo('Fehler: '+e));"
    "},100);"
    "return true;"
    "}"
    "async function updateAnalog(){"
    "try{const r=await fetch('/analog');document.getElementById('analogVal').innerText=await r.text();}catch(e){}"
    "}"
    "async function updateLog(){"
    "try{const r=await fetch('/log');document.getElementById('logBox').innerHTML=await r.text();}catch(e){}"
    "}"
    "async function updateTime(){"
    "try{const r=await fetch('/time');document.getElementById('curTime').innerText=await r.text();}catch(e){}"
    "try{const r2=await fetch('/ntptime');document.getElementById('ntpTime').innerText=await r2.text();}catch(e){}"
    "}"
    "setInterval(updateAnalog,1000);"
    "setInterval(updateLog,2000);"
    "setInterval(updateTime,1000);"
    "updateAnalog();updateLog();updateTime();"
    "</script>"
    "</body></html>";

  server.send(200, "text/html", html);
}

// ------------------- Web-Handler -------------------
int getFilteredAnalogValue() {
  analogBuffer[analogBufferIndex] = analogRead(ANALOG_PIN);
  analogBufferIndex = (analogBufferIndex + 1) % MEDIAN_SAMPLES;
  int sorted[MEDIAN_SAMPLES];
  for (int i = 0; i < MEDIAN_SAMPLES; i++) sorted[i] = analogBuffer[i];
  for (int i = 0; i < MEDIAN_SAMPLES - 1; i++) {
    for (int j = 0; j < MEDIAN_SAMPLES - i - 1; j++) {
      if (sorted[j] > sorted[j + 1]) {
        int t = sorted[j];
        sorted[j] = sorted[j + 1];
        sorted[j + 1] = t;
      }
    }
  }
  return sorted[MEDIAN_SAMPLES / 2];
}

void handleAnalog() {
  int val = getFilteredAnalogValue();
  server.send(200, "text/plain", String(val));
}

void handleTime() {
  struct tm timeinfo;
  if (!getESP8266Time(&timeinfo)) {
    server.send(200, "text/plain", "--:--:--");
    return;
  }
  char timeStr[16];
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
  server.send(200, "text/plain", String(timeStr));
}

void handleNtpTime() {
  if (lastNTPSync == 0) {
    server.send(200, "text/plain", "Noch kein Sync");
    return;
  }
  time_t now = time(nullptr);
  unsigned long diffSec = (millis() - lastNTPSync) / 1000;
  time_t syncTime = now - diffSec;

  struct tm ti;
  if (!timeToGermanLocal(syncTime, &ti)) {
    server.send(200, "text/plain", "Unbekannt");
    return;
  }
  char buf[32];
  snprintf(buf, sizeof(buf), "%02d:%02d:%02d", ti.tm_hour, ti.tm_min, ti.tm_sec);
  server.send(200, "text/plain", String(buf));
}

void handleLog() {
  server.send(200, "text/plain", getLogHTML());
}

void handleManualMove() {
  bool isResponse = server.hasArg("response");
  if (moving) {
    if (isResponse) server.send(200, "text/plain", "⚠ Bewegung läuft bereits.");
    else server.send(200, "text/html", "<html><body>OK</body></html>");
    return;
  }
  uint32_t steps = 200;
  bool fast = false;
  if (server.hasArg("steps")) {
    long s = server.arg("steps").toInt();
    if (s < 1) s = 1;
    if (s > 500000L) s = 500000L;
    steps = (uint32_t)s;
  }
  if (server.hasArg("fast")) fast = (server.arg("fast") == "1");
  startMove(steps, fast, false, true);
  String msg = "Vorwärtsbewegung gestartet\nSchritte: " + String(steps) + (fast ? " (schnell)" : " (normal)");
  if (isResponse) server.send(200, "text/plain", msg);
  else server.send(200, "text/html", "<html><body>OK</body></html>");
}

void handleSetPos() {
  bool isResponse = server.hasArg("response");
  if (moving) {
    if (isResponse) server.send(200, "text/plain", "⚠ Bewegung läuft bereits.");
    else server.send(200, "text/html", "<html><body>OK</body></html>");
    return;
  }
  String timestr = server.arg("t");
  int hh = 0, mm = 0;
  if (sscanf(timestr.c_str(), "%d:%d", &hh, &mm) != 2 || hh < 0 || hh > 23 || mm < 0 || mm > 59) {
    String msg = "✗ Ungültiges Zeitformat.\nBeispiel: 12:34";
    if (isResponse) server.send(200, "text/plain", msg);
    else server.send(200, "text/html", "<html><body>OK</body></html>");
    return;
  }
  currentStep = timeToStepIndex(hh, mm, 0);

  struct tm timeinfo;
  if (!getESP8266Time(&timeinfo)) {
    String msg = "✗ Konnte aktuelle Zeit nicht lesen.\n(prüfe NTP-Sync)";
    if (isResponse) server.send(200, "text/plain", msg);
    else server.send(200, "text/html", "<html><body>OK</body></html>");
    return;
  }
  uint64_t realStep = timeToStepIndex(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
  uint64_t delta = calcForwardDelta(currentStep, realStep);

  syncMovePending = true;
  syncMoveStartTime = millis();
  syncMoveStartStep = currentStep;
  startMove(delta, true);

  String msg = "Zeiger wird nachgestellt\nVon: " + String(hh) + ":" + String(mm) + "\nZu: " + String(timeinfo.tm_hour) + ":" + String(timeinfo.tm_min) + ":" + String(timeinfo.tm_sec) + "\n(Vergangene Zeit wird nachgefahren)";
  addLog("Manuelles Sync gestartet: " + String(hh) + ":" + String(mm) + " -> " + String(timeinfo.tm_hour) + ":" + String(timeinfo.tm_min));

  if (isResponse) server.send(200, "text/plain", msg);
  else server.send(200, "text/html", "<html><body>OK</body></html>");
}

void handleSetLED() {
  bool isResponse = server.hasArg("response");
  String msg = "";

  if (server.hasArg("b")) {
    brightness = constrain(server.arg("b").toInt(), 0, 255);
    msg += "Brightness: " + String(brightness) + "\n";
  }
  if (server.hasArg("nb")) {
    nightBrightness = constrain(server.arg("nb").toInt(), 0, 255);
    msg += "Nacht-Brightness: " + String(nightBrightness) + "\n";
  }
  if (server.hasArg("n")) {
    nightDim = (server.arg("n") == "1");
    msg += String("Nachtdimmen: ") + (nightDim ? "An" : "Aus") + "\n";
  }
  if (server.hasArg("ns")) {
    nightStartHour = constrain(server.arg("ns").toInt(), 0, 23);
    msg += "Nacht Start: " + String(nightStartHour) + "\n";
  }
  if (server.hasArg("ne")) {
    nightEndHour = constrain(server.arg("ne").toInt(), 0, 23);
    msg += "Nacht Ende: " + String(nightEndHour) + "\n";
  }
  if (server.hasArg("c")) {
    stripColor = constrain(server.arg("c").toInt(), 0, 255);
    msg += "Hue: " + String(stripColor) + "\n";
  }
  if (server.hasArg("r")) {
    rainbowColor = (server.arg("r") == "1");
    msg += String("Regenbogen: ") + (rainbowColor ? "An" : "Aus") + "\n";
  }
  if (server.hasArg("m")) {
    ledMode = (LedMode)constrain(server.arg("m").toInt(), 0, 2);
    forceLedRefresh = true;
    msg += "LED-Mode: " + String((int)ledMode) + "\n";
  }

  if (server.hasArg("spr")) {
    uint32_t newSpr = (uint32_t)server.arg("spr").toInt();
    if (newSpr < 1000UL) newSpr = 1000UL;
    if (newSpr > 2000000UL) newSpr = 2000000UL;
    if (newSpr != stepsPerRev) {
      stepsPerRev = newSpr;
      currentStep %= totalSteps12h();
      msg += "Steps/Rev aktualisiert: " + String(stepsPerRev) + "\n";
    }
  }

  saveSettings();

  if (msg.length() == 0) msg = "Keine Änderungen erkannt.";
  if (isResponse) server.send(200, "text/plain", msg);
  else server.send(200, "text/html", "<html><body>OK</body></html>");
}

void handleSetAutoSync() {
  bool isResponse = server.hasArg("response");
  String msg = "";

  // Parse MM:SS Format
  if (server.hasArg("ast")) {
    String timeStr = server.arg("ast");
    int mm = 0, ss = 0;
    if (sscanf(timeStr.c_str(), "%d:%d", &mm, &ss) == 2) {
      if (mm >= 0 && mm <= 59 && ss >= 0 && ss <= 59) {
        if (mm != autoSyncMinute || ss != autoSyncSecond) {
          autoSyncMinute = mm;
          autoSyncSecond = ss;
          msg += "Auto-Sync Zeit: " + String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "\n";
          addLog("Auto-Sync Zeit geändert auf: " + String(autoSyncMinute) + ":" + String(autoSyncSecond));
        }
      } else {
        msg += "✗ Ungültige Zeit (0-59:0-59)\n";
      }
    } else {
      msg += "✗ Ungültiges Format (MM:SS)\n";
    }
  }

  if (server.hasArg("stv")) {
    int newTrigger = constrain(server.arg("stv").toInt(), 0, 1024);
    if (newTrigger != sensorTriggerValue) {
      sensorTriggerValue = newTrigger;
      msg += "Sensor-Triggerwert: " + String(sensorTriggerValue) + "\n";
      addLog("Sensor-Triggerwert geändert auf: " + String(sensorTriggerValue));
    }
  }

  if (server.hasArg("stm")) {
    bool newMode = (server.arg("stm") == "1");
    if (newMode != sensorTriggerAbove) {
      sensorTriggerAbove = newMode;
      msg += "Trigger-Modus: " + String(sensorTriggerAbove ? "Über" : "Unter") + " dem Wert\n";
      addLog("Trigger-Modus geändert auf: " + String(sensorTriggerAbove ? "Über" : "Unter"));
    }
  }

  if (msg.length() > 0) {
    saveSettings();
    msg += "Zeitfenster: ±13 Minuten\nToleranz: ±" + String(SENSOR_TOLERANCE);
  } else {
    msg = "Keine Änderungen erkannt.";
  }

  if (isResponse) server.send(200, "text/plain", msg);
  else server.send(200, "text/html", "<html><body>OK</body></html>");
}

void handleSyncNTP() {
  bool isResponse = server.hasArg("response");
  syncTimeFromNTP();
  String msg = "NTP-Sync ausgelöst.\nLetzte Sync: " + String(ntpTimeStr);
  if (isResponse) server.send(200, "text/plain", msg);
  else server.send(200, "text/html", "<html><body>OK</body></html>");
}

void handleDebugTime() {
  String out = "";
  time_t now = time(nullptr);
  out += "time_t (raw): " + String((unsigned long)now) + "\n";

  struct tm gm;
  if (gmtime_r(&now, &gm) != nullptr) {
    char buf[64];
    snprintf(buf, sizeof(buf), "UTC: %04d-%02d-%02d %02d:%02d:%02d\n",
             gm.tm_year + 1900, gm.tm_mon + 1, gm.tm_mday, gm.tm_hour, gm.tm_min, gm.tm_sec);
    out += String(buf);
  } else {
    out += "UTC: (gmtime_r failed)\n";
  }

  struct tm lt;
  if (timeToGermanLocal(now, &lt)) {
    char buf2[64];
    snprintf(buf2, sizeof(buf2), "Local (DE): %04d-%02d-%02d %02d:%02d:%02d\n",
             lt.tm_year + 1900, lt.tm_mon + 1, lt.tm_mday, lt.tm_hour, lt.tm_min, lt.tm_sec);
    out += String(buf2);
  } else {
    out += "Local (DE): (error)\n";
  }

  out += "lastNTPSync (ms): " + String(lastNTPSync) + " (ntpTimeStr: " + String(ntpTimeStr) + ")\n";

  server.send(200, "text/plain", out);
}

// ------------------- NTP -------------------
void syncTimeFromNTP() {
  if (WiFi.status() != WL_CONNECTED) {
    strncpy(ntpTimeStr, "Kein WLAN", sizeof(ntpTimeStr));
    addLog("NTP Sync fehlgeschlagen: Kein WLAN");
    return;
  }

  addLog("Starte NTP-Sync (UTC holen)...");
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");

  const int tries = 30;
  for (int i = 0; i < tries; ++i) {
    time_t now = time(nullptr);
    if (now > 100000) {
      struct tm timeinfo;
      if (getESP8266Time(&timeinfo)) {
        snprintf(ntpTimeStr, sizeof(ntpTimeStr), "%02d:%02d:%02d",
                 timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
        addLog("NTP Sync erfolgreich (Europe/Berlin)");
        lastNTPSync = millis();
        return;
      }
    }
    delay(500);
  }

  strncpy(ntpTimeStr, "Sync fehlgeschlagen", sizeof(ntpTimeStr));
  addLog("NTP Sync fehlgeschlagen: Timeout");
}

// ------------------- Setup -------------------
void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(PROJECT_NAME);

  pinMode(EN_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);
  pinMode(STEP_PIN, OUTPUT);
  disableDriver();
  digitalWrite(DIR_PIN, LOW);

  FastLED.addLeds<NEOPIXEL, LED_PIN>(strip, NUM_LEDS);
  FastLED.clear(true);

  EEPROM.begin(EEPROM_SIZE);
  loadSettings();

  addLog("System gestartet");

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  addLog("Verbinde mit WLAN...");
  for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    addLog("WLAN verbunden: " + WiFi.localIP().toString());
    syncTimeFromNTP();
  } else {
    addLog("WLAN fehlgeschlagen, starte AP-Modus");
    WiFi.mode(WIFI_AP);
    WiFi.softAP(PROJECT_NAME);
    addLog("AP gestartet: " + WiFi.softAPIP().toString());
  }

  setupOTA();

  httpUpdater.setup(&server, "/update", "admin", "espupdate");
  addLog("Web-OTA bereit unter /update");

  server.on("/", HTTP_GET, handleRoot);
  server.on("/analog", HTTP_GET, handleAnalog);
  server.on("/time", HTTP_GET, handleTime);
  server.on("/log", HTTP_GET, handleLog);
  server.on("/move", HTTP_POST, handleManualMove);
  server.on("/setpos", HTTP_POST, handleSetPos);
  server.on("/setled", HTTP_POST, handleSetLED);
  server.on("/syncntp", HTTP_POST, handleSyncNTP);
  server.on("/setautosync", HTTP_POST, handleSetAutoSync);
  server.on("/ntptime", HTTP_GET, handleNtpTime);
  server.on("/debugtime", HTTP_GET, handleDebugTime);

  server.begin();
  addLog("Webserver gestartet");
}

// ------------------- Loop -------------------
void loop() {
  server.handleClient();
  handleStepper();
  handleNormalClockMove();
  checkAutoSync();
  updateLEDs();

  ArduinoOTA.handle();

  if (WiFi.status() == WL_CONNECTED && (millis() - lastNTPSync > NTP_RESYNC_INTERVAL))
    syncTimeFromNTP();
}
