#!/usr/bin/env node
/**
 * Simple HTTP server that responds 200 with a test message including client IP.
 *
 * Behavior:
 *   - GET returns HTTP/1.1 200 with a message including the requester IP
 *   - HEAD returns the same headers as GET but no body
 *   - Displays incoming X-* headers when present
 *   - If User-Agent contains "curl" or "wget" (case-insensitive), the server
 *     responds with Content-Type: text/plain; otherwise Content-Type: text/html.
 *   - --pem specifies a PEM bundle file (cert chain + private key). When provided,
 *     the server listens on both HTTP (--port) and HTTPS (--tls-port).
 *     Without --pem the server listens on plain HTTP only.
 *   - --look controls which HTML variant is returned for non-CLI agents:
 *       * basic     - plain HTML with no external references
 *       * nice      - includes Google Font "Noto Sans" (default)
 *       * bootstrap - Bootstrap 5 layout
 *       * tailwind  - Tailwind CSS via @tailwindcss/browser@latest
 *   - The URL query parameter "look" (e.g. /?look=nice) overrides --look for
 *     that request only. Values are case-insensitive and must be basic,nice,bootstrap,tailwind.
 */

import http from "node:http";
import https from "node:https";
import fs from "node:fs";
import { URL } from "node:url";

const VALID_LOOKS = new Set(["basic", "nice", "bootstrap", "tailwind"]);

const PLAIN_TEMPLATE =
  "Hello, This is a test HTTP server.\n\n" +
  "Your request came from {ip}.\n\n" +
  "{proxy_headers_block}" +
  "Have a nice day!\n";

const HTML_BASIC = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Test HTTP server</title>
  </head>
  <body>
    <h1>Hello,</h1>
    <p>This is a test HTTP server.</p>
    <p>Your request came from {ip_html}.</p>
    {proxy_headers_html}
    <p>Have a nice day!</p>
  </body>
</html>
`;

const HTML_NICE = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test HTTP server</title>
    <!-- Google Font: Noto Sans -->
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet">
    <style>
      body { font-family: "Noto Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 2rem; background: #f6f7fb; }
      .card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); max-width: 800px; margin: 2rem auto; }
      h1 { margin-top: 0; }
    </style>
  </head>
  <body>
    <div class="card">
      <h1>Hello,</h1>
      <p>This is a test HTTP server.</p>
      <p>Your request came from {ip_html}.</p>
      {proxy_headers_html}
      <p>Have a nice day!</p>
    </div>
  </body>
</html>
`;

const HTML_BOOTSTRAP = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test HTTP server</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
  </head>
  <body class="bg-light">
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
      <div class="container">
        <a class="navbar-brand" href="#">Test HTTP server</a>
      </div>
    </nav>

    <main class="container">
      <div class="row justify-content-center">
        <div class="col-md-8">
          <div class="card shadow-sm">
            <div class="card-body">
              <h1 class="card-title">Hello,</h1>
              <p class="card-text">This is a test HTTP server.</p>
              <p class="card-text">Your request came from {ip_html}.</p>
              {proxy_headers_html}
              <hr>
              <p class="mb-0">Have a nice day!</p>
            </div>
          </div>
        </div>
      </div>
    </main>

    <footer class="text-center mt-4 mb-4">
      <small class="text-muted">Simple status page for firewall/testing</small>
    </footer>
  </body>
</html>
`;

const HTML_TAILWIND = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Test HTTP server</title>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@latest"></script>
  </head>
  <body class="min-h-screen bg-slate-100 text-slate-900">
    <div class="mx-auto max-w-3xl px-4 py-10 sm:py-16">
      <div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-lg">
        <div class="bg-slate-900 px-6 py-4 text-slate-100">
          <h1 class="text-xl font-semibold">Test HTTP server</h1>
          <p class="mt-1 text-sm text-slate-300">Simple status page for firewall/testing</p>
        </div>
        <main class="space-y-4 px-6 py-6">
          <p class="text-2xl font-bold">Hello,</p>
          <p>This is a test HTTP server.</p>
          <p>Your request came from {ip_html}.</p>
          {proxy_headers_html}
          <p class="pt-2">Have a nice day!</p>
        </main>
      </div>
    </div>
  </body>
</html>
`;

function escapeHtml(value) {
  return String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\"/g, "&quot;")
    .replace(/'/g, "&#x27;");
}

function isCliAgent(userAgent) {
  if (!userAgent) {
    return false;
  }
  const ua = userAgent.toLowerCase();
  return ua.includes("curl") || ua.includes("wget");
}

function getRequestLook(req) {
  const url = new URL(req.url || "/", "http://localhost");
  const look = (url.searchParams.get("look") || "").trim().toLowerCase();
  if (VALID_LOOKS.has(look)) {
    return look;
  }
  return null;
}

function collectXHeaders(req) {
  const xHeaders = [];
  for (const [name, value] of Object.entries(req.headers)) {
    if (!name.toLowerCase().startsWith("x-")) {
      continue;
    }
    if (Array.isArray(value)) {
      xHeaders.push([name, value.join(", ")]);
    } else if (value !== undefined) {
      xHeaders.push([name, value]);
    }
  }
  xHeaders.sort((a, b) => a[0].localeCompare(b[0], "en", { sensitivity: "base" }));
  return xHeaders;
}

function proxyMarkup(xHeaders) {
  if (xHeaders.length === 0) {
    return { proxyHeadersBlock: "", proxyHeadersHtml: "" };
  }

  const plainLines = xHeaders.map(([name, value]) => `  - ${name}: ${value}`);
  const proxyHeadersBlock =
    "Reverse proxy condition: detected via X-* headers.\n" +
    "X-* headers:\n" +
    plainLines.join("\n") +
    "\n\n";

  const htmlItems = xHeaders
    .map(([name, value]) => `<li><code>${escapeHtml(name)}</code>: ${escapeHtml(value)}</li>`)
    .join("");
  const proxyHeadersHtml =
    "<p>Reverse proxy condition: <strong>detected via X-* headers</strong>.</p>" +
    `<p>X-* headers:</p><ul>${htmlItems}</ul>`;

  return { proxyHeadersBlock, proxyHeadersHtml };
}

function htmlForLook(look, ipHtml, proxyHeadersHtml) {
  const template =
    look === "basic"
      ? HTML_BASIC
      : look === "nice"
        ? HTML_NICE
        : look === "bootstrap"
          ? HTML_BOOTSTRAP
          : HTML_TAILWIND;

  return template
    .replace("{ip_html}", ipHtml)
    .replace("{proxy_headers_html}", proxyHeadersHtml);
}

function makeBodyAndType(req, clientIp, defaultLook) {
  const userAgent = req.headers["user-agent"] || "";
  const xHeaders = collectXHeaders(req);
  const { proxyHeadersBlock, proxyHeadersHtml } = proxyMarkup(xHeaders);

  let xff = (req.headers["x-forwarded-for"] || "").split(",")[0].trim();
  // strip port: [::1]:port -> ::1, 1.2.3.4:port -> 1.2.3.4
  if (xff.startsWith("[")) {
    xff = xff.slice(1, xff.includes("]") ? xff.indexOf("]") : xff.length);
  } else if ((xff.match(/:/g) || []).length === 1) {
    xff = xff.split(":")[0];
  }
  let ipText, ipHtml;
  if (xff) {
    ipText = `${xff} (forwarded by proxy ${clientIp})`;
    ipHtml = `<strong>${escapeHtml(xff)}</strong> (forwarded by proxy <strong>${escapeHtml(clientIp)}</strong>)`;
  } else {
    ipText = clientIp;
    ipHtml = `<strong>${escapeHtml(clientIp)}</strong>`;
  }

  if (isCliAgent(userAgent)) {
    const body = PLAIN_TEMPLATE
      .replace("{ip}", ipText)
      .replace("{proxy_headers_block}", proxyHeadersBlock);
    return { body: Buffer.from(body, "utf8"), contentType: "text/plain; charset=utf-8" };
  }

  const requestLook = getRequestLook(req);
  const look = requestLook || defaultLook;
  const html = htmlForLook(look, ipHtml, proxyHeadersHtml);
  return { body: Buffer.from(html, "utf8"), contentType: "text/html; charset=utf-8" };
}

function getClientIp(req) {
  return req.socket?.remoteAddress || "unknown";
}

function parseArgs(argv) {
  let bind = "0.0.0.0";
  let port = 8080;
  let tlsPort = 8443;
  let look = "nice";
  let pem = null;

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];

    if (arg === "-b" || arg === "--bind") {
      i += 1;
      if (i >= argv.length) {
        throw new Error(`${arg} requires a value`);
      }
      bind = argv[i];
      continue;
    }

    if (arg === "-p" || arg === "--port") {
      i += 1;
      if (i >= argv.length) {
        throw new Error(`${arg} requires a value`);
      }
      const parsedPort = Number.parseInt(argv[i], 10);
      if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
        throw new Error(`Invalid port: ${argv[i]}`);
      }
      port = parsedPort;
      continue;
    }

    if (arg === "--pem") {
      i += 1;
      if (i >= argv.length) {
        throw new Error(`${arg} requires a value`);
      }
      pem = argv[i];
      continue;
    }

    if (arg === "--tls-port") {
      i += 1;
      if (i >= argv.length) {
        throw new Error(`${arg} requires a value`);
      }
      const parsedTlsPort = Number.parseInt(argv[i], 10);
      if (!Number.isInteger(parsedTlsPort) || parsedTlsPort < 1 || parsedTlsPort > 65535) {
        throw new Error(`Invalid tls-port: ${argv[i]}`);
      }
      tlsPort = parsedTlsPort;
      continue;
    }

    if (arg === "--look") {
      i += 1;
      if (i >= argv.length) {
        throw new Error(`${arg} requires a value`);
      }
      const value = String(argv[i]).trim().toLowerCase();
      if (!VALID_LOOKS.has(value)) {
        throw new Error(`Invalid look: ${argv[i]} (expected one of: basic, nice, bootstrap, tailwind)`);
      }
      look = value;
      continue;
    }

    if (arg === "-h" || arg === "--help") {
      const help = [
        "Usage:",
        "  node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--tls-port PORT] [--pem PEMFILE] [--look basic|nice|bootstrap|tailwind]",
        "",
        "Defaults:",
        "  --bind 0.0.0.0",
        "  --port 8080",
        "  --tls-port 8443",
        "  --pem  (none, plain HTTP only)",
        "  --look nice",
      ].join("\n");
      console.log(help);
      process.exit(0);
    }

    throw new Error(`Unknown argument: ${arg}`);
  }

  return { bind, port, tlsPort, look, pem };
}

function run(bind, port, tlsPort, look, pem) {
  let isShuttingDown = false;
  const servers = [];

  function createRequestHandler(req, res) {
    const method = req.method || "GET";

    if (method !== "GET" && method !== "HEAD") {
      res.writeHead(405, { "Content-Length": "0" });
      res.end();
      return;
    }

    const clientIp = getClientIp(req);
    const { body, contentType } = makeBodyAndType(req, clientIp, look);

    res.writeHead(200, {
      "Content-Type": contentType,
      "Content-Length": String(body.length),
    });

    if (method === "HEAD") {
      res.end();
      return;
    }

    res.end(body);
  }

  function setupServer(server, scheme, listenPort) {
    server.on("request", (req) => {
      const clientIp = getClientIp(req);
      const now = new Date().toUTCString();
      const method = req.method || "GET";
      const path = req.url || "/";
      process.stderr.write(`${clientIp} - - [${now}] "${method} ${path}"\n`);
    });

    server.on("error", (err) => {
      process.stderr.write(`Server error (${scheme}): ${err.message}\n`);
      process.exitCode = 1;
    });

    server.listen(listenPort, bind, () => {
      console.log(`Serving on ${scheme}://${bind}:${listenPort} (default look=${look})`);
    });

    servers.push(server);
  }

  // Create HTTP server
  const httpServer = http.createServer(createRequestHandler);
  setupServer(httpServer, "http", port);

  // Create HTTPS server if PEM is provided
  if (pem) {
    let pemContent;
    try {
      pemContent = fs.readFileSync(pem);
    } catch (err) {
      throw new Error(`Cannot read PEM file '${pem}': ${err.message}`);
    }
    const httpsServer = https.createServer({ cert: pemContent, key: pemContent }, createRequestHandler);
    setupServer(httpsServer, "https", tlsPort);
  }

  function handleShutdownSignal() {
    if (isShuttingDown) {
      return;
    }
    isShuttingDown = true;
    console.log("\nShutting down server");
    let closed = 0;
    servers.forEach((server) => {
      server.close(() => {
        closed += 1;
        if (closed === servers.length) {
          process.exit(0);
        }
      });
    });
  }

  process.on("SIGINT", handleShutdownSignal);
  process.on("SIGTERM", handleShutdownSignal);
}

try {
  const { bind, port, tlsPort, look, pem } = parseArgs(process.argv.slice(2));
  run(bind, port, tlsPort, look, pem);
} catch (err) {
  process.stderr.write(`${err.message}\n`);
  process.exit(2);
}
