package me.proxylink.plugin.web;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import me.proxylink.plugin.ConfigFileEditor;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

public final class WebWizardServer implements AutoCloseable {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    private static final HexFormat HEX = HexFormat.of().withLowerCase();
    private static final Pattern CONFIG_KEY = Pattern.compile("[A-Za-z0-9._-]+");
    private static final int MAX_FORM_BYTES = 512 * 1024;
    private static final String SESSION_COOKIE = "dsp2p_wizard";

    private final Path configPath;
    private final Logger logger;
    private final Supplier<String> reloadAction;
    private final HttpServer server;
    private final ExecutorService executor;
    private final String loginKey;
    private final String sessionToken;
    private final URI uri;
    private final AtomicBoolean running = new AtomicBoolean(true);

    private WebWizardServer(
            Path configPath,
            Logger logger,
            Supplier<String> reloadAction,
            HttpServer server,
            ExecutorService executor,
            String loginKey,
            String sessionToken,
            URI uri
    ) {
        this.configPath = configPath;
        this.logger = logger;
        this.reloadAction = reloadAction;
        this.server = server;
        this.executor = executor;
        this.loginKey = loginKey;
        this.sessionToken = sessionToken;
        this.uri = uri;
    }

    public static WebWizardServer start(Path configPath, Logger logger, Supplier<String> reloadAction) throws IOException {
        WizardSettings settings = WizardSettings.load(configPath);
        IOException lastFailure = null;
        int firstPort = settings.port();
        int attempts = firstPort == 0 ? 1 : 31;

        for (int attempt = 0; attempt < attempts; attempt++) {
            int port = firstPort == 0 ? 0 : firstPort + attempt;
            try {
                return bind(configPath, logger, reloadAction, settings.bindHost(), port);
            } catch (BindException e) {
                lastFailure = e;
            }
        }

        throw lastFailure == null ? new IOException("Unable to bind web wizard") : lastFailure;
    }

    public URI uri() {
        return uri;
    }

    public boolean isRunning() {
        return running.get();
    }

    @Override
    public void close() {
        if (!running.compareAndSet(true, false)) {
            return;
        }
        server.stop(0);
        executor.shutdownNow();
        logger.info("DirtSimpleP2P Web Wizard stopped");
    }

    private static WebWizardServer bind(
            Path configPath,
            Logger logger,
            Supplier<String> reloadAction,
            String bindHost,
            int port
    ) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(bindHost, port), 0);
        ExecutorService executor = Executors.newThreadPerTaskExecutor(
                Thread.ofVirtual().name("proxylink-webwizard-", 1).factory()
        );
        String loginKey = randomHex(18);
        String sessionToken = randomHex(32);
        URI uri = URI.create("http://" + uriHost(bindHost) + ":" + server.getAddress().getPort() + "/");
        WebWizardServer webWizard = new WebWizardServer(
                configPath,
                logger,
                reloadAction,
                server,
                executor,
                loginKey,
                sessionToken,
                uri
        );

        server.createContext("/", webWizard::handleIndex);
        server.createContext("/login", webWizard::handleLogin);
        server.createContext("/logout", webWizard::handleLogout);
        server.createContext("/save", webWizard::handleSave);
        server.setExecutor(executor);
        server.start();

        logger.warning("DirtSimpleP2P Web Wizard started at " + uri);
        logger.warning("Please sign in with this key: " + loginKey);
        logger.warning("The Web Wizard can edit config.properties. Stop it with /dsp2p webwizard when finished.");
        return webWizard;
    }

    private void handleIndex(HttpExchange exchange) throws IOException {
        try {
            addNoStoreHeaders(exchange);
            if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
                sendText(exchange, 405, "Method not allowed");
                return;
            }

            if (!isAuthenticated(exchange)) {
                sendHtml(exchange, 200, loginPage(""));
                return;
            }

            sendHtml(exchange, 200, wizardPage(""));
        } catch (Exception e) {
            handleError(exchange, e);
        }
    }

    private void handleLogin(HttpExchange exchange) throws IOException {
        try {
            addNoStoreHeaders(exchange);
            if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
                sendText(exchange, 405, "Method not allowed");
                return;
            }

            Map<String, List<String>> form = readForm(exchange);
            if (!loginKey.equals(first(form, "key"))) {
                sendHtml(exchange, 403, loginPage("That key did not match. Check the server console and try again."));
                return;
            }

            exchange.getResponseHeaders().add("Set-Cookie",
                    SESSION_COOKIE + "=" + sessionToken + "; Path=/; HttpOnly; SameSite=Strict");
            redirect(exchange, "/");
        } catch (Exception e) {
            handleError(exchange, e);
        }
    }

    private void handleLogout(HttpExchange exchange) throws IOException {
        try {
            addNoStoreHeaders(exchange);
            exchange.getResponseHeaders().add("Set-Cookie",
                    SESSION_COOKIE + "=deleted; Path=/; Max-Age=0; HttpOnly; SameSite=Strict");
            redirect(exchange, "/");
        } catch (Exception e) {
            handleError(exchange, e);
        }
    }

    private void handleSave(HttpExchange exchange) throws IOException {
        try {
            addNoStoreHeaders(exchange);
            if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
                sendText(exchange, 405, "Method not allowed");
                return;
            }
            if (!isAuthenticated(exchange)) {
                sendHtml(exchange, 401, loginPage("Sign in again before saving changes."));
                return;
            }

            Map<String, List<String>> form = readForm(exchange);
            String action = first(form, "action");
            String message;

            if ("reloadOnly".equals(action)) {
                message = reloadAction.get();
            } else {
                saveWizardForm(form);
                if ("generateToken".equals(action)) {
                    String token = randomHex(32);
                    ConfigFileEditor.setProperty(configPath, "tunnel.authToken", token);
                    message = "Generated and saved a new tunnel.authToken. Copy it to the other DirtSimpleP2P nodes.";
                } else {
                    message = "Saved config.properties.";
                }

                if ("saveReload".equals(action)) {
                    message = message + " " + reloadAction.get();
                }
            }

            sendHtml(exchange, 200, wizardPage(message));
        } catch (Exception e) {
            handleError(exchange, e);
        }
    }

    private void saveWizardForm(Map<String, List<String>> form) throws IOException {
        List<String> frontendNames = namesFrom(first(form, "frontends"));
        String newFrontend = first(form, "newFrontendName").trim();
        if (!newFrontend.isBlank()) {
            validateConfigKey(newFrontend);
            if (!frontendNames.contains(newFrontend)) {
                frontendNames.add(newFrontend);
            }
        }

        List<String> routeNames = namesFrom(first(form, "routes"));
        String newRoute = first(form, "newRouteName").trim();
        if (!newRoute.isBlank()) {
            validateConfigKey(newRoute);
            if (!routeNames.contains(newRoute)) {
                routeNames.add(newRoute);
            }
        }

        saveKey(form, "role");
        saveKey(form, "node.name");
        saveKey(form, "tunnel.listenHost");
        saveKey(form, "tunnel.listenPort");
        saveKey(form, "tunnel.authToken");
        saveKey(form, "tunnel.connectTimeoutMillis");
        saveKey(form, "tunnel.heartbeatIntervalMillis");
        saveKey(form, "tunnel.heartbeatTimeoutMillis");
        saveKey(form, "tunnel.heartbeatMissesBeforeDisconnect");
        saveKey(form, "tunnel.reconnectInitialMillis");
        saveKey(form, "tunnel.reconnectMaxMillis");
        saveBoolean(form, "tunnel.tls.enabled");
        saveBoolean(form, "tunnel.tls.allowInsecure");
        saveBoolean(form, "tunnel.tls.autoGenerate");
        saveKey(form, "tunnel.tls.keyStore");
        saveKey(form, "tunnel.tls.keyStorePassword");
        saveBoolean(form, "tunnel.tls.requireClientAuth");
        saveBoolean(form, "tunnel.tls.trustOnFirstUse");
        saveKey(form, "webwizard.bindHost");
        saveKey(form, "webwizard.port");

        ConfigFileEditor.setProperty(configPath, "frontends", String.join(",", frontendNames));
        for (String frontend : frontendNames) {
            saveNamed(form, "frontend." + frontend + ".connectHost");
            saveNamed(form, "frontend." + frontend + ".connectPort");
            saveNamed(form, "frontend." + frontend + ".tls.pinnedCertificateSha256");
        }

        ConfigFileEditor.setProperty(configPath, "routes", String.join(",", routeNames));
        for (String route : routeNames) {
            saveNamed(form, "route." + route + ".frontendBindHost");
            saveNamed(form, "route." + route + ".frontendBindPort");
            saveNamed(form, "route." + route + ".backendNode");
            saveNamed(form, "route." + route + ".backendTargetHost");
            saveNamed(form, "route." + route + ".backendTargetPort");
        }

        for (String key : form.getOrDefault("rawKeys", List.of())) {
            validateConfigKey(key);
            ConfigFileEditor.setProperty(configPath, key, first(form, "raw." + key));
        }

        String newRawKey = first(form, "newRawKey").trim();
        if (!newRawKey.isBlank()) {
            validateConfigKey(newRawKey);
            ConfigFileEditor.setProperty(configPath, newRawKey, first(form, "newRawValue"));
        }
    }

    private void saveKey(Map<String, List<String>> form, String key) throws IOException {
        ConfigFileEditor.setProperty(configPath, key, first(form, key));
    }

    private void saveNamed(Map<String, List<String>> form, String key) throws IOException {
        ConfigFileEditor.setProperty(configPath, key, first(form, key));
    }

    private void saveBoolean(Map<String, List<String>> form, String key) throws IOException {
        ConfigFileEditor.setProperty(configPath, key, Boolean.toString(form.containsKey(key)));
    }

    private String loginPage(String message) {
        return page("Sign In", """
                <section class="login-shell">
                  <div class="brand-mark">DS</div>
                  <h1>DirtSimpleP2P Control Center</h1>
                  <p>Enter the temporary setup key printed in the server console.</p>
                  %s
                  <form method="post" action="/login" class="stack">
                    <label>Login key<input name="key" autocomplete="off" autofocus></label>
                    <button type="submit">Sign In</button>
                  </form>
                </section>
                """.formatted(alert(message)));
    }

    private String wizardPage(String message) throws IOException {
        Properties properties = loadProperties(configPath);
        Set<String> guidedKeys = new HashSet<>();
        String role = value(properties, "role", "backend");
        List<String> frontendNames = namesFrom(value(properties, "frontends", defaultFrontends(properties)));
        List<String> routeNames = namesFrom(value(properties, "routes", "minecraft"));

        StringBuilder frontends = new StringBuilder();
        for (String frontend : frontendNames) {
            guidedKeys.add("frontend." + frontend + ".connectHost");
            guidedKeys.add("frontend." + frontend + ".connectPort");
            guidedKeys.add("frontend." + frontend + ".tls.pinnedCertificateSha256");
            frontends.append("""
                    <div class="mini-card">
                      <div class="mini-title">%s</div>
                      <div class="field-grid">
                        %s
                        %s
                        %s
                      </div>
                    </div>
                    """.formatted(
                    html(frontend),
                    textInput(properties, "frontend." + frontend + ".connectHost", "Proxy IP or domain", "proxy.example.com"),
                    textInput(properties, "frontend." + frontend + ".connectPort", "Tunnel port", "24445"),
                    textInput(properties, "frontend." + frontend + ".tls.pinnedCertificateSha256", "TLS pin", "")
            ));
        }

        StringBuilder routes = new StringBuilder();
        for (String route : routeNames) {
            guidedKeys.add("route." + route + ".frontendBindHost");
            guidedKeys.add("route." + route + ".frontendBindPort");
            guidedKeys.add("route." + route + ".backendNode");
            guidedKeys.add("route." + route + ".backendTargetHost");
            guidedKeys.add("route." + route + ".backendTargetPort");
            routes.append("""
                    <div class="mini-card">
                      <div class="mini-title">%s</div>
                      <div class="field-grid">
                        %s
                        %s
                        %s
                        %s
                        %s
                      </div>
                    </div>
                    """.formatted(
                    html(route),
                    textInput(properties, "route." + route + ".frontendBindHost", "Proxy local bind host", "127.0.0.1"),
                    textInput(properties, "route." + route + ".frontendBindPort", "Proxy local bind port", "25566"),
                    textInput(properties, "route." + route + ".backendNode", "Backend node name", "paper-backend"),
                    textInput(properties, "route." + route + ".backendTargetHost", "Backend target host", "127.0.0.1"),
                    textInput(properties, "route." + route + ".backendTargetPort", "Backend Minecraft port", "25565")
            ));
        }

        guidedKeys.addAll(List.of(
                "role",
                "node.name",
                "tunnel.listenHost",
                "tunnel.listenPort",
                "tunnel.authToken",
                "tunnel.connectTimeoutMillis",
                "tunnel.heartbeatIntervalMillis",
                "tunnel.heartbeatTimeoutMillis",
                "tunnel.heartbeatMissesBeforeDisconnect",
                "tunnel.reconnectInitialMillis",
                "tunnel.reconnectMaxMillis",
                "tunnel.tls.enabled",
                "tunnel.tls.allowInsecure",
                "tunnel.tls.autoGenerate",
                "tunnel.tls.keyStore",
                "tunnel.tls.keyStorePassword",
                "tunnel.tls.requireClientAuth",
                "tunnel.tls.trustOnFirstUse",
                "webwizard.bindHost",
                "webwizard.port",
                "frontends",
                "routes"
        ));

        String advancedRows = advancedRows(properties, guidedKeys);

        return page("Setup Wizard", """
                <section class="hero">
                  <div>
                    <div class="eyebrow">Configuration Wizard</div>
                    <h1>DirtSimpleP2P Control Center</h1>
                    <p>Configure the tunnel with guided fields, save safely, then reload DirtSimpleP2P without restarting the Minecraft server.</p>
                  </div>
                  <div class="status-card">
                    <span>Config file</span>
                    <strong>%s</strong>
                    <small>Wizard URL: %s</small>
                  </div>
                </section>
                %s
                <form method="post" action="/save">
                  <section class="layout">
                    <aside class="steps">
                      <a href="#identity">1. Identity</a>
                      <a href="#proxy">2. Proxy</a>
                      <a href="#backend">3. Backend</a>
                      <a href="#routes">4. Routes</a>
                      <a href="#security">5. Security</a>
                      <a href="#reliability">6. Reliability</a>
                      <a href="#advanced">7. Advanced</a>
                    </aside>
                    <div class="cards">
                      <section class="panel" id="identity">
                        <div class="section-head"><span>1</span><div><h2>Server Identity</h2><p>Choose which side this plugin is running on.</p></div></div>
                        <div class="field-grid">
                          %s
                          %s
                        </div>
                      </section>
                      <section class="panel" id="proxy">
                        <div class="section-head"><span>2</span><div><h2>Proxy Listener</h2><p>Used on BungeeCord or Waterfall. Backend nodes connect to this public tunnel port.</p></div></div>
                        <div class="field-grid">
                          %s
                          %s
                        </div>
                      </section>
                      <section class="panel" id="backend">
                        <div class="section-head"><span>3</span><div><h2>Backend Connections</h2><p>Used on Paper or Spigot. Add every public proxy this backend should connect to.</p></div></div>
                        %s
                        <div class="add-row">
                          <label>New proxy name<input name="newFrontendName" placeholder="proxy2"></label>
                        </div>
                        %s
                      </section>
                      <section class="panel" id="routes">
                        <div class="section-head"><span>4</span><div><h2>Minecraft Routes</h2><p>Map Bungee local ports to named backend nodes and backend Minecraft ports.</p></div></div>
                        %s
                        <div class="add-row">
                          <label>New route name<input name="newRouteName" placeholder="lobby"></label>
                        </div>
                        %s
                      </section>
                      <section class="panel" id="security">
                        <div class="section-head"><span>5</span><div><h2>Security</h2><p>Use a shared secret token. Enable TLS for real deployments.</p></div></div>
                        <div class="field-grid">
                          %s
                          %s
                          %s
                          %s
                          %s
                          %s
                          %s
                          %s
                        </div>
                      </section>
                      <section class="panel" id="reliability">
                        <div class="section-head"><span>6</span><div><h2>Reliability</h2><p>Fast reconnect defaults are already tuned for most Minecraft networks.</p></div></div>
                        <div class="field-grid">
                          %s
                          %s
                          %s
                          %s
                          %s
                        </div>
                      </section>
                      <section class="panel" id="wizard">
                        <div class="section-head"><span>7</span><div><h2>Web Wizard Access</h2><p>Keep this bound to localhost unless you are putting it behind your own secure tunnel.</p></div></div>
                        <div class="field-grid">
                          %s
                          %s
                        </div>
                      </section>
                      <section class="panel" id="advanced">
                        <div class="section-head"><span>8</span><div><h2>Advanced Raw Settings</h2><p>Extra config keys are preserved here for power users.</p></div></div>
                        %s
                        <div class="field-grid">
                          <label>New key<input name="newRawKey" placeholder="example.setting"></label>
                          <label>New value<input name="newRawValue"></label>
                        </div>
                      </section>
                      <section class="action-bar">
                        <button type="submit" name="action" value="save">Save Config</button>
                        <button type="submit" name="action" value="saveReload">Save + Reload DirtSimpleP2P</button>
                        <button type="submit" name="action" value="reloadOnly" class="secondary">Reload Current Config</button>
                        <button type="submit" name="action" value="generateToken" class="secondary">Generate New Token</button>
                        <a href="/logout">Sign out</a>
                      </section>
                    </div>
                  </section>
                </form>
                """.formatted(
                html(configPath.toString()),
                html(uri.toString()),
                alert(message),
                roleSelect(role),
                textInput(properties, "node.name", "Node name", role.equals("frontend") ? "bungee-frontend" : "paper-backend"),
                textInput(properties, "tunnel.listenHost", "Tunnel listen host", "0.0.0.0"),
                textInput(properties, "tunnel.listenPort", "Tunnel listen port", "24445"),
                hiddenText("frontends", String.join(",", frontendNames)),
                frontends,
                hiddenText("routes", String.join(",", routeNames)),
                routes,
                textInput(properties, "tunnel.authToken", "Shared tunnel token", "copy the same token to every node"),
                checkbox(properties, "tunnel.tls.enabled", "Enable TLS"),
                checkbox(properties, "tunnel.tls.allowInsecure", "Allow insecure mode"),
                checkbox(properties, "tunnel.tls.autoGenerate", "Auto-generate frontend certificate"),
                textInput(properties, "tunnel.tls.keyStore", "Frontend TLS keystore", "certs/frontend.p12"),
                textInput(properties, "tunnel.tls.keyStorePassword", "Keystore password", ""),
                checkbox(properties, "tunnel.tls.requireClientAuth", "Require TLS client certificates"),
                checkbox(properties, "tunnel.tls.trustOnFirstUse", "Trust first TLS certificate"),
                textInput(properties, "tunnel.connectTimeoutMillis", "Connect timeout", "5000"),
                textInput(properties, "tunnel.heartbeatIntervalMillis", "Heartbeat interval", "2000"),
                textInput(properties, "tunnel.heartbeatTimeoutMillis", "Heartbeat timeout", "8000"),
                textInput(properties, "tunnel.reconnectInitialMillis", "First reconnect delay", "250"),
                textInput(properties, "tunnel.reconnectMaxMillis", "Max reconnect delay", "10000"),
                textInput(properties, "webwizard.bindHost", "Wizard bind host", "127.0.0.1"),
                textInput(properties, "webwizard.port", "Wizard port", "8765"),
                advancedRows
        ));
    }

    private static String roleSelect(String role) {
        return """
                <label>Mode
                  <select name="role">
                    <option value="frontend"%s>Proxy / BungeeCord / Waterfall</option>
                    <option value="backend"%s>Backend / Paper / Spigot</option>
                  </select>
                </label>
                """.formatted(selected(role, "frontend"), selected(role, "backend"));
    }

    private static String textInput(Properties properties, String key, String label, String placeholder) {
        return """
                <label>%s<input name="%s" value="%s" placeholder="%s"></label>
                """.formatted(html(label), attr(key), attr(value(properties, key, "")), attr(placeholder));
    }

    private static String hiddenText(String key, String value) {
        return "<input type=\"hidden\" name=\"" + attr(key) + "\" value=\"" + attr(value) + "\">";
    }

    private static String checkbox(Properties properties, String key, String label) {
        String checked = Boolean.parseBoolean(value(properties, key, "false")) ? " checked" : "";
        return """
                <label class="check"><input type="checkbox" name="%s" value="true"%s><span>%s</span></label>
                """.formatted(attr(key), checked, html(label));
    }

    private static String advancedRows(Properties properties, Set<String> guidedKeys) {
        List<String> keys = new ArrayList<>(properties.stringPropertyNames());
        keys.removeIf(guidedKeys::contains);
        keys.sort(Comparator.naturalOrder());
        if (keys.isEmpty()) {
            return "<p class=\"muted\">No extra settings found.</p>";
        }

        StringBuilder rows = new StringBuilder("<div class=\"raw-grid\">");
        for (String key : keys) {
            rows.append("<label><code>").append(html(key)).append("</code>")
                    .append("<input type=\"hidden\" name=\"rawKeys\" value=\"").append(attr(key)).append("\">")
                    .append("<input name=\"raw.").append(attr(key)).append("\" value=\"")
                    .append(attr(properties.getProperty(key, ""))).append("\"></label>");
        }
        rows.append("</div>");
        return rows.toString();
    }

    private static String page(String title, String body) {
        return """
                <!doctype html>
                <html lang="en">
                <head>
                  <meta charset="utf-8">
                  <meta name="viewport" content="width=device-width, initial-scale=1">
                  <title>DirtSimpleP2P - %s</title>
                  <style>
                    :root {
                      color-scheme: light;
                      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
                      --ink: #162033;
                      --muted: #637086;
                      --line: #d9e0eb;
                      --panel: #ffffff;
                      --soft: #f5f7fb;
                      --blue: #1f66d1;
                      --teal: #15847a;
                      --gold: #a86c12;
                    }
                    body { margin: 0; min-height: 100vh; color: var(--ink); background: linear-gradient(180deg, #edf3fb 0, #f8fafc 280px); }
                    main { width: min(1180px, calc(100%% - 32px)); margin: 28px auto 44px; }
                    .hero { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; align-items: stretch; margin-bottom: 18px; }
                    .hero, .panel, .login-shell, .action-bar { background: rgba(255,255,255,.94); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 18px 40px rgba(22,32,51,.10); }
                    .hero { padding: 26px; }
                    .eyebrow { color: var(--teal); font-weight: 800; text-transform: uppercase; letter-spacing: .08em; font-size: 12px; margin-bottom: 8px; }
                    h1, h2 { margin: 0; letter-spacing: 0; }
                    h1 { font-size: 32px; line-height: 1.1; }
                    h2 { font-size: 20px; }
                    p { color: var(--muted); line-height: 1.5; }
                    .status-card { display: grid; gap: 8px; align-content: center; background: #132238; color: white; border-radius: 8px; padding: 18px; min-width: 0; }
                    .status-card span, .status-card small { color: #c5d2e4; }
                    .status-card strong, .status-card small { overflow-wrap: anywhere; }
                    .layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 18px; align-items: start; }
                    .steps { position: sticky; top: 16px; display: grid; gap: 8px; background: #132238; border-radius: 8px; padding: 12px; }
                    .steps a { color: #d8e5f6; text-decoration: none; padding: 10px 12px; border-radius: 6px; font-weight: 750; }
                    .steps a:hover { background: rgba(255,255,255,.10); }
                    .cards { display: grid; gap: 16px; min-width: 0; }
                    .panel { padding: 20px; }
                    .section-head { display: flex; gap: 12px; align-items: start; margin-bottom: 16px; }
                    .section-head > span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 8px; color: white; background: var(--blue); font-weight: 850; flex: 0 0 auto; }
                    .section-head p { margin: 4px 0 0; }
                    .field-grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 14px; }
                    .raw-grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; margin-bottom: 16px; }
                    .mini-card { border: 1px solid var(--line); background: var(--soft); border-radius: 8px; padding: 14px; margin: 12px 0; }
                    .mini-title { font-weight: 850; margin-bottom: 12px; }
                    .add-row { margin-top: 12px; max-width: 340px; }
                    label { display: grid; gap: 6px; color: #253149; font-weight: 750; }
                    input, select { width: 100%%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #c7d1df; border-radius: 6px; background: white; color: var(--ink); font: inherit; }
                    input:focus, select:focus { outline: 3px solid rgba(31,102,209,.18); border-color: var(--blue); }
                    .check { display: flex; align-items: center; gap: 10px; min-height: 42px; border: 1px solid #c7d1df; border-radius: 6px; padding: 0 12px; background: white; }
                    .check input { width: 18px; height: 18px; }
                    .alert { padding: 13px 15px; border-radius: 8px; margin: 14px 0; border: 1px solid #e3c16c; background: #fff8dd; color: #51370a; font-weight: 700; }
                    .muted { color: var(--muted); }
                    .action-bar { position: sticky; bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; padding: 14px; }
                    button { border: 0; border-radius: 6px; padding: 11px 15px; font: inherit; font-weight: 850; background: var(--blue); color: white; cursor: pointer; }
                    button.secondary { background: #39465c; }
                    .action-bar a, .login-shell a { color: var(--blue); font-weight: 800; text-decoration: none; }
                    code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color: #33415c; }
                    .login-shell { max-width: 450px; margin: 76px auto; padding: 28px; }
                    .brand-mark { width: 46px; height: 46px; display: grid; place-items: center; background: #132238; color: white; border-radius: 8px; font-weight: 900; margin-bottom: 18px; }
                    .stack { display: grid; gap: 14px; }
                    @media (max-width: 880px) {
                      main { width: min(100%% - 20px, 1180px); margin: 10px auto 24px; }
                      .hero { grid-template-columns: 1fr; padding: 18px; }
                      .layout { grid-template-columns: 1fr; }
                      .steps { position: static; grid-template-columns: repeat(2, minmax(0, 1fr)); }
                      .field-grid, .raw-grid { grid-template-columns: 1fr; }
                      h1 { font-size: 26px; }
                    }
                  </style>
                </head>
                <body><main>%s</main></body>
                </html>
                """.formatted(html(title), body);
    }

    private boolean isAuthenticated(HttpExchange exchange) {
        String cookieHeader = exchange.getRequestHeaders().getFirst("Cookie");
        if (cookieHeader == null || cookieHeader.isBlank()) {
            return false;
        }
        for (String cookie : cookieHeader.split(";")) {
            String trimmed = cookie.trim();
            int equals = trimmed.indexOf('=');
            if (equals <= 0) {
                continue;
            }
            String name = trimmed.substring(0, equals);
            String value = trimmed.substring(equals + 1);
            if (SESSION_COOKIE.equals(name) && sessionToken.equals(value)) {
                return true;
            }
        }
        return false;
    }

    private static Map<String, List<String>> readForm(HttpExchange exchange) throws IOException {
        byte[] body;
        try (InputStream input = exchange.getRequestBody()) {
            body = input.readNBytes(MAX_FORM_BYTES + 1);
        }
        if (body.length > MAX_FORM_BYTES) {
            throw new IOException("Form body is too large");
        }

        Map<String, List<String>> values = new LinkedHashMap<>();
        String encoded = new String(body, StandardCharsets.UTF_8);
        if (encoded.isBlank()) {
            return values;
        }

        for (String pair : encoded.split("&")) {
            if (pair.isBlank()) {
                continue;
            }
            int equals = pair.indexOf('=');
            String key = equals < 0 ? pair : pair.substring(0, equals);
            String value = equals < 0 ? "" : pair.substring(equals + 1);
            values.computeIfAbsent(urlDecode(key), ignored -> new ArrayList<>()).add(urlDecode(value));
        }
        return values;
    }

    private static Properties loadProperties(Path configPath) throws IOException {
        Properties properties = new Properties();
        try (InputStream input = Files.newInputStream(configPath)) {
            properties.load(input);
        }
        return properties;
    }

    private static void validateConfigKey(String key) {
        if (!CONFIG_KEY.matcher(key).matches()) {
            throw new IllegalArgumentException("Invalid config name: " + key);
        }
    }

    private static List<String> namesFrom(String raw) {
        List<String> names = new ArrayList<>();
        for (String part : raw.split(",")) {
            String name = part.trim();
            if (!name.isBlank() && !names.contains(name)) {
                validateConfigKey(name);
                names.add(name);
            }
        }
        return names;
    }

    private static String defaultFrontends(Properties properties) {
        String host = value(properties, "tunnel.connectHost", "");
        String port = value(properties, "tunnel.connectPort", "");
        if (!host.isBlank() || !port.isBlank()) {
            return "default";
        }
        return "proxy1";
    }

    private static String selected(String actual, String expected) {
        return expected.equalsIgnoreCase(actual) ? " selected" : "";
    }

    private static String first(Map<String, List<String>> form, String key) {
        List<String> values = form.get(key);
        return values == null || values.isEmpty() ? "" : values.get(0);
    }

    private static String value(Properties properties, String key, String defaultValue) {
        return properties.getProperty(key, defaultValue).trim();
    }

    private static String alert(String message) {
        if (message == null || message.isBlank()) {
            return "";
        }
        return "<div class=\"alert\">" + html(message) + "</div>";
    }

    private static String randomHex(int bytes) {
        byte[] data = new byte[bytes];
        SECURE_RANDOM.nextBytes(data);
        return HEX.formatHex(data);
    }

    private static String urlDecode(String value) {
        return URLDecoder.decode(value, StandardCharsets.UTF_8);
    }

    private static String html(String value) {
        return value == null ? "" : value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;");
    }

    private static String attr(String value) {
        return html(value);
    }

    private static void addNoStoreHeaders(HttpExchange exchange) {
        exchange.getResponseHeaders().set("Cache-Control", "no-store");
        exchange.getResponseHeaders().set("X-Content-Type-Options", "nosniff");
    }

    private static void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException {
        byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
        exchange.sendResponseHeaders(statusCode, bytes.length);
        try (OutputStream output = exchange.getResponseBody()) {
            output.write(bytes);
        }
    }

    private static void sendText(HttpExchange exchange, int statusCode, String text) throws IOException {
        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(statusCode, bytes.length);
        try (OutputStream output = exchange.getResponseBody()) {
            output.write(bytes);
        }
    }

    private static void redirect(HttpExchange exchange, String location) throws IOException {
        exchange.getResponseHeaders().set("Location", location);
        exchange.sendResponseHeaders(303, -1);
        exchange.close();
    }

    private void handleError(HttpExchange exchange, Exception e) throws IOException {
        logger.log(Level.WARNING, "Web Wizard request failed", e);
        if (exchange.getResponseCode() == -1) {
            sendHtml(exchange, 500, page("Error", "<section class=\"login-shell\"><h1>Request failed</h1><p>"
                    + html(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())
                    + "</p></section>"));
        }
    }

    private static String uriHost(String host) {
        if (host.contains(":") && !host.startsWith("[")) {
            return "[" + host + "]";
        }
        return host;
    }

    private record WizardSettings(String bindHost, int port) {
        private static WizardSettings load(Path configPath) throws IOException {
            Properties properties = loadProperties(configPath);
            String bindHost = properties.getProperty("webwizard.bindHost", "127.0.0.1").trim();
            if (bindHost.isBlank()) {
                bindHost = "127.0.0.1";
            }
            int port = intValue(properties.getProperty("webwizard.port"), 8765);
            if (port < 0 || port > 65535) {
                port = 8765;
            }
            return new WizardSettings(bindHost, port);
        }

        private static int intValue(String raw, int defaultValue) {
            if (raw == null || raw.isBlank()) {
                return defaultValue;
            }
            try {
                return Integer.parseInt(raw.trim());
            } catch (NumberFormatException ignored) {
                return defaultValue;
            }
        }
    }
}
