#include "EggDuino.h"
#include <ArduinoJson.h>
#include <DNSServer.h>
#include <string.h>

namespace
{
	const char *kConfigPath = "/config.json";
	const size_t kConfigJsonCapacity = 4096;
	const byte kDnsPort = 53;
	const unsigned long kWifiReconnectIntervalMs = 10000;
	using ConfigJsonDocument = StaticJsonDocument<kConfigJsonCapacity>;

	WebServer server(80);
	DNSServer dnsServer;
	bool configStoreReady = false;
	bool apModeActive = false;
	bool staReconnectEnabled = false;
	bool staConnectionKnown = false;
	unsigned long lastStaReconnectAttemptMs = 0;
	ConfigJsonDocument g_configJsonScratch;

	ConfigJsonDocument &configJsonScratch()
	{
		// Keep the large config JSON buffer off ESP32 Arduino's 8 KB loopTask stack.
		g_configJsonScratch.clear();
		return g_configJsonScratch;
	}

	void redirectToRoot()
	{
		server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString() + "/", true);
		server.send(302, "text/plain", "");
	}

	bool isIntType(const ConfigParameter &param)
	{
		return strcmp(param.type, "int") == 0;
	}

	int *asIntPtr(ConfigParameter &param)
	{
		return static_cast<int *>(param.value);
	}

	String *asStringPtr(ConfigParameter &param)
	{
		return static_cast<String *>(param.value);
	}

	ConfigParameter *findParameter(const String &key)
	{
		for (size_t i = 0; i < configParameterCount; ++i)
		{
			if (key.equals(configParameters[i].key))
			{
				return &configParameters[i];
			}
		}
		return nullptr;
	}

	void applyDefaults()
	{
		for (size_t i = 0; i < configParameterCount; ++i)
		{
			if (isIntType(configParameters[i]))
			{
				*asIntPtr(configParameters[i]) = configParameters[i].defaultValue;
			}
			else
			{
				const char *fallback = configParameters[i].defaultText == nullptr ? "" : configParameters[i].defaultText;
				*asStringPtr(configParameters[i]) = String(fallback);
			}
		}
	}

	void handleRoot()
	{
		static const char kPage[] PROGMEM = R"HTML(
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EggDuino Konfiguration</title>
<style>
body { font-family: "Segoe UI", sans-serif; margin: 20px; background: #f3f6fb; color: #1a1a1a; }
main { max-width: 760px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
h1 { margin-top: 0; font-size: 1.35rem; }
label { display: block; margin: 14px 0 6px; font-weight: 600; }
input[type='number'], input[type='text'], input[type='password'] { width: 100%; padding: 10px; border: 1px solid #c7d2e5; border-radius: 8px; box-sizing: border-box; }
button { margin-top: 18px; border: 0; background: #0b5ed7; color: white; padding: 10px 14px; border-radius: 8px; cursor: pointer; }
#status { margin-top: 12px; min-height: 1.2em; }
#log { margin-top: 20px; border: 1px solid #d6dfef; border-radius: 8px; background: #0f172a; color: #d2e3ff; padding: 10px; height: 220px; overflow-y: auto; white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.9rem; }
#logTitle { margin-top: 24px; margin-bottom: 8px; font-weight: 700; }
#downloadPrintLog { display: inline-block; margin-top: 8px; color: #0b5ed7; text-decoration: none; font-weight: 600; }
</style>
</head>
<body>
<main>
<h1>EggDuino Parameter</h1>
<form id="cfgForm"></form>
<button id="saveBtn" type="button">Speichern</button>
<div id="status"></div>
<div id="logTitle">Logs</div>
<a id="downloadPrintLog" href="/api/print-log.txt" download="incoming-print-log.txt">Incoming print log herunterladen</a>
<div id="log"></div>
</main>
<script>
let lastSeq = 0;

async function loadConfig() {
  const resp = await fetch('/api/config', { cache: 'no-store' });
  if (!resp.ok) throw new Error('Konfiguration konnte nicht geladen werden');
  return resp.json();
}

function renderForm(config) {
  const form = document.getElementById('cfgForm');
  form.innerHTML = '';
  (config.parameters || []).forEach(p => {
    const label = document.createElement('label');
    label.textContent = p.description || p.key;
    const input = document.createElement('input');
    const paramType = p.type || 'int';
    input.type = paramType === 'int' ? 'number' : paramType;
    if (paramType === 'password') {
      input.value = '';
      if (p.hasValue) {
        input.placeholder = '(gespeichert)';
      }
    } else {
      input.value = p.value ?? '';
    }
    input.dataset.key = p.key;
    input.dataset.ptype = paramType;
    input.dataset.secret = p.secret ? '1' : '0';
    input.dataset.touched = '0';
    input.addEventListener('input', () => {
      input.dataset.touched = '1';
    });
    form.appendChild(label);
    form.appendChild(input);
  });
}

async function saveConfig() {
  const status = document.getElementById('status');
  const inputs = [...document.querySelectorAll('input[data-key]')];
  const payload = {
    parameters: inputs
      .map(i => {
        const type = i.dataset.ptype || 'int';
        const secret = i.dataset.secret === '1';
        if (type === 'int') {
          return { key: i.dataset.key, value: Number(i.value) };
        }
        if (secret && i.dataset.touched !== '1') {
          return null;
        }
        return { key: i.dataset.key, value: i.value };
      })
      .filter(Boolean)
  };
  const resp = await fetch('/api/config', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(text || 'Speichern fehlgeschlagen');
  }
  const saved = await resp.json();
  renderForm(saved);
  status.textContent = 'Gespeichert';
}

async function pollLogs() {
  const box = document.getElementById('log');
  const resp = await fetch('/api/logs?since=' + lastSeq);
  if (!resp.ok) {
    return;
  }
  const payload = await resp.json();
  (payload.logs || []).forEach(entry => {
    box.textContent += entry.text + '\n';
  });
  if (typeof payload.lastSeq === 'number') {
    lastSeq = payload.lastSeq;
  }
  box.scrollTop = box.scrollHeight;
}

(async function init() {
  const status = document.getElementById('status');
  try {
    const cfg = await loadConfig();
    renderForm(cfg);
    status.textContent = 'Bereit';
  } catch (e) {
    status.textContent = e.message;
  }
  document.getElementById('saveBtn').addEventListener('click', async () => {
    try {
      await saveConfig();
    } catch (e) {
      status.textContent = e.message;
    }
  });
  pollLogs();
  setInterval(pollLogs, 800);
})();
</script>
</body>
</html>
)HTML";

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

	void handleGetConfig()
	{
		if (!configStoreReady && !initConfigStore())
		{
			server.send(500, "text/plain", "Config storage not available");
			return;
		}
		server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
		server.sendHeader("Pragma", "no-cache");
		server.sendHeader("Expires", "0");
		server.send(200, "application/json", buildConfigJson());
	}

	void handlePostConfig()
	{
		if (!configStoreReady && !initConfigStore())
		{
			server.send(500, "text/plain", "Config storage not available");
			return;
		}

		if (!server.hasArg("plain"))
		{
			server.send(400, "text/plain", "Missing JSON body");
			return;
		}

		String error;
		if (!applyConfigJson(server.arg("plain"), error))
		{
			Log(String("Config JSON fehlerhaft: ") + error);
			server.send(400, "text/plain", error);
			return;
		}

		if (!saveConfigToFile())
		{
			Log("Config konnte nicht gespeichert werden");
			server.send(500, "text/plain", "Could not save config");
			return;
		}

		server.send(200, "application/json", buildConfigJson());
	}

	void handleGetLogs()
	{
		uint32_t since = 0;
		if (server.hasArg("since"))
		{
			since = static_cast<uint32_t>(server.arg("since").toInt());
		}
		server.send(200, "application/json", buildLogsJson(since));
	}

	void handleDownloadPrintLog()
	{
		server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
		server.sendHeader("Pragma", "no-cache");
		server.sendHeader("Expires", "0");
		server.sendHeader("Content-Disposition", "attachment; filename=\"incoming-print-log.txt\"");
		server.send(200, "text/plain; charset=utf-8", buildIncomingPrintLogText());
	}

	void handleNotFound()
	{
		if (apModeActive)
		{
			redirectToRoot();
			return;
		}
		if (server.uri().startsWith("/api/"))
		{
			server.send(404, "text/plain", "API endpoint not found");
			return;
		}
		handleRoot();
	}

	void handleWifiReconnect()
	{
		if (!staReconnectEnabled || apModeActive)
		{
			return;
		}

		const wl_status_t wifiStatus = WiFi.status();
		if (wifiStatus == WL_CONNECTED)
		{
			if (!staConnectionKnown)
			{
				staConnectionKnown = true;
				Log(String("WLAN verbunden: ") + WiFi.localIP().toString());
			}
			return;
		}

		if (staConnectionKnown)
		{
			staConnectionKnown = false;
			Log("WLAN Verbindung verloren");
		}

		const unsigned long now = millis();
		if (now - lastStaReconnectAttemptMs < kWifiReconnectIntervalMs)
		{
			return;
		}

		lastStaReconnectAttemptMs = now;
		if (!WiFi.reconnect())
		{
			WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());
		}
		Log("WLAN Reconnect versucht");
	}
} // namespace

ConfigParameter configParameters[] = {
	{"int", "penUpPos", &g_iPenUpPos, "Pen Up Position", 40, "", false},
	{"int", "penDownPos", &g_iPenDownPos, "Pen Down Position", 10, "", false},
	{"int", "rotStepPin", &g_iRotStepPin, "Rotational Stepper Step Pin", kDefaultRotStepPin, "", false},
	{"int", "rotDirPin", &g_iRotDirPin, "Rotational Stepper Direction Pin", kDefaultRotDirPin, "", false},
	{"int", "rotEnablePin", &g_iRotEnablePin, "Rotational Stepper Enable Pin", kDefaultRotEnablePin, "", false},
	{"int", "penStepPin", &g_iPenStepPin, "Pen Stepper Step Pin", kDefaultPenStepPin, "", false},
	{"int", "penDirPin", &g_iPenDirPin, "Pen Stepper Direction Pin", kDefaultPenDirPin, "", false},
	{"int", "penEnablePin", &g_iPenEnablePin, "Pen Stepper Enable Pin", kDefaultPenEnablePin, "", false},
	{"int", "rotMicrostep", &g_iRotMicrostep, "Rotational Stepper Microsteps", kDefaultRotMicrostep, "", false},
	{"int", "penMicrostep", &g_iPenMicrostep, "Pen Stepper Microsteps", kDefaultPenMicrostep, "", false},
	{"int", "iMaxAcclSpeed", &g_iMaxAcclSpeed, "max. Stepper acceleration speed", 10000, "", false},
	{"int", "servoPin", &g_iServoPin, "Servo Pin", kDefaultServoPin, "", false},
	{"text", "Name", &g_sHostname, "Name", 0, "EggDuino", false},
	{"text", "wifiSsid", &g_sWifiSsid, "WLAN SSID", 0, "", false},
	{"password", "wifiPassword", &g_sWifiPassword, "WLAN Passwort", 0, "", true},
};

const size_t configParameterCount = sizeof(configParameters) / sizeof(configParameters[0]);

bool initConfigStore()
{
	if (!SPIFFS.begin(true))
	{
		configStoreReady = false;
		return false;
	}
	configStoreReady = loadConfigFromFile();
	return configStoreReady;
}

bool loadConfigFromFile()
{
	applyDefaults();

	File file = SPIFFS.open(kConfigPath, "r");
	if (!file)
	{
		Log("config.json fehlt, defaults werden gespeichert");
		return saveConfigToFile();
	}

	ConfigJsonDocument &doc = configJsonScratch();
	DeserializationError err = deserializeJson(doc, file);
	file.close();
	if (err)
	{
		Log(String("config.json ist ungueltig (") + err.c_str() + "), defaults werden gespeichert");
		return saveConfigToFile();
	}

	JsonArray params = doc["parameters"].as<JsonArray>();
	for (JsonObject item : params)
	{
		const char *key = item["key"];
		if (key == nullptr)
		{
			continue;
		}
		ConfigParameter *param = findParameter(String(key));
		if (param == nullptr)
		{
			continue;
		}
		if (item.containsKey("value"))
		{
			if (isIntType(*param))
			{
				*asIntPtr(*param) = item["value"].as<int>();
			}
			else
			{
				*asStringPtr(*param) = item["value"].as<String>();
			}
		}
		if (item.containsKey("description"))
		{
			param->description = item["description"].as<String>();
		}
	}
	updateStepCorrectionFactors();
	Log(String("Config geladen: penUpPos=") + g_iPenUpPos + ", penDownPos=" + g_iPenDownPos);

	return true;
}

bool saveConfigToFile()
{
	File file = SPIFFS.open(kConfigPath, "w");
	if (!file)
	{
		Log("SPIFFS open write failed");
		return false;
	}

	ConfigJsonDocument &doc = configJsonScratch();
	JsonArray params = doc.createNestedArray("parameters");
	for (size_t i = 0; i < configParameterCount; ++i)
	{
		JsonObject item = params.createNestedObject();
		item["key"] = configParameters[i].key;
		item["type"] = configParameters[i].type;
		item["secret"] = configParameters[i].secret;
		if (isIntType(configParameters[i]))
		{
			item["value"] = *asIntPtr(configParameters[i]);
		}
		else
		{
			item["value"] = *asStringPtr(configParameters[i]);
		}
		item["description"] = configParameters[i].description;
	}
	if (doc.overflowed())
	{
		Log("Config JSON Dokument zu klein beim Speichern");
		file.close();
		return false;
	}

	bool ok = serializeJsonPretty(doc, file) > 0;
	file.flush();
	file.close();
	if (!ok)
	{
		Log("serializeJsonPretty failed");
	}
	return ok;
}

String buildConfigJson()
{
	ConfigJsonDocument &doc = configJsonScratch();
	JsonArray params = doc.createNestedArray("parameters");
	for (size_t i = 0; i < configParameterCount; ++i)
	{
		JsonObject item = params.createNestedObject();
		item["key"] = configParameters[i].key;
		item["type"] = configParameters[i].type;
		item["secret"] = configParameters[i].secret;
		if (isIntType(configParameters[i]))
		{
			item["value"] = *asIntPtr(configParameters[i]);
		}
		else if (configParameters[i].secret)
		{
			item["value"] = "";
			item["hasValue"] = !asStringPtr(configParameters[i])->isEmpty();
		}
		else
		{
			item["value"] = *asStringPtr(configParameters[i]);
		}
		item["description"] = configParameters[i].description;
	}
	if (doc.overflowed())
	{
		Log("Config JSON Dokument zu klein beim Lesen");
		return String("{\"parameters\":[]}");
	}

	String output;
	serializeJson(doc, output);
	return output;
}

bool applyConfigJson(const String &payload, String &errorMessage)
{
	ConfigJsonDocument &doc = configJsonScratch();
	DeserializationError err = deserializeJson(doc, payload);
	if (err)
	{
		errorMessage = "Invalid JSON payload";
		return false;
	}

	JsonArray params = doc["parameters"].as<JsonArray>();
	if (params.isNull())
	{
		errorMessage = "JSON must contain 'parameters' array";
		return false;
	}

	for (JsonObject item : params)
	{
		const char *key = item["key"];
		if (key == nullptr)
		{
			continue;
		}

		ConfigParameter *param = findParameter(String(key));
		if (param == nullptr)
		{
			continue;
		}

		if (!item.containsKey("value"))
		{
			continue;
		}
		if (isIntType(*param))
		{
			*asIntPtr(*param) = item["value"].as<int>();
		}
		else
		{
			*asStringPtr(*param) = item["value"].as<String>();
		}
		if (item.containsKey("description"))
		{
			param->description = item["description"].as<String>();
		}
	}
	updateStepCorrectionFactors();

	return true;
}

void startWebInterface()
{
	initConfigStore();
	bool staConnected = false;
	apModeActive = false;
	staReconnectEnabled = !g_sWifiSsid.isEmpty();
	staConnectionKnown = false;
	lastStaReconnectAttemptMs = millis();
	dnsServer.stop();
	char deviceName[32] = {0};
	buildDeviceName(deviceName, sizeof(deviceName));
	String hostName = String(deviceName);
	if (hostName.isEmpty())
	{
		hostName = "EggBot_UNKNOWN";
	}
	g_sHostname = hostName;

	if (!g_sWifiSsid.isEmpty())
	{
		WiFi.mode(WIFI_STA);
		WiFi.setAutoReconnect(true);
		WiFi.setHostname(hostName.c_str());
		WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str());

		const unsigned long connectStart = millis();
		const unsigned long connectTimeoutMs = 20000;
		Serial.println(String("Trying to connect to Wifi: ") + g_sWifiSsid.c_str() + g_sWifiPassword.c_str());
		while (WiFi.status() != WL_CONNECTED && millis() - connectStart < connectTimeoutMs)
		{
			delay(250);
		}
		staConnected = (WiFi.status() == WL_CONNECTED);
		staConnectionKnown = staConnected;
		if (staConnected)
		{
			Serial.println(String("Connected: http://") + WiFi.localIP().toString());
		}
		else
		{
			Serial.println("WLAN Verbindung fehlgeschlagen");
		}

		server.on("/", HTTP_GET, handleRoot);
		server.on("/api/config", HTTP_GET, handleGetConfig);
		server.on("/api/config", HTTP_POST, handlePostConfig);
		server.on("/api/logs", HTTP_GET, handleGetLogs);
		server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
		server.onNotFound(handleNotFound);
		server.begin();
		startWifiProtocolInterface();
	}
	if (!staConnected)
	{
		WiFi.mode(WIFI_AP);
#ifdef ESP32
		WiFi.softAPsetHostname(hostName.c_str());
#endif
		if (WiFi.softAP(hostName.c_str()))
		{
			apModeActive = true;
			dnsServer.start(kDnsPort, "*", WiFi.softAPIP());
			Serial.println(String("AP aktiv: ") + hostName + " / http://" + WiFi.softAPIP().toString() + " (Name: " + hostName + ")");
		}
		else
		{
			Serial.println("AP konnte nicht gestartet werden");
		}
	}

	server.on("/", HTTP_GET, handleRoot);
	server.on("/api/config", HTTP_GET, handleGetConfig);
	server.on("/api/config", HTTP_POST, handlePostConfig);
	server.on("/api/logs", HTTP_GET, handleGetLogs);
	server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog);
	server.on("/generate_204", HTTP_GET, redirectToRoot);
	server.on("/gen_204", HTTP_GET, redirectToRoot);
	server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot);
	server.on("/connecttest.txt", HTTP_GET, redirectToRoot);
	server.on("/ncsi.txt", HTTP_GET, redirectToRoot);
	server.on("/fwlink", HTTP_GET, redirectToRoot);
	server.onNotFound(handleNotFound);
	server.begin();
}

void handleWebInterface()
{
	handleWifiReconnect();

	if (apModeActive)
	{
		dnsServer.processNextRequest();
	}
	server.handleClient();
}
