import { createReadStream } from "node:fs";
import { access, stat } from "node:fs/promises";
import { createServer } from "node:http";
import { extname, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawn } from "node:child_process";
import { Readable } from "node:stream";

const scriptDirectory = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = resolve(scriptDirectory, "..");
const clientRoot = resolve(projectRoot, "dist", "client");
const workerPath = resolve(projectRoot, "dist", "server", "index.js");
const noOpen = process.argv.includes("--no-open");
const portArgument = process.argv.find((argument) => argument.startsWith("--port="));
const requestedPort = Number(portArgument?.slice("--port=".length) ?? process.env.PORT ?? 4173);

if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
  console.error("Invalid local server port.");
  process.exit(64);
}

try {
  await Promise.all([access(workerPath), access(clientRoot)]);
} catch {
  console.error("The prebuilt calculator files are missing. Please extract the complete ZIP again.");
  process.exit(66);
}

const { default: calculatorWorker } = await import(pathToFileURL(workerPath).href);

if (!calculatorWorker || typeof calculatorWorker.fetch !== "function") {
  console.error("The prebuilt calculator entry point is invalid. Please download the package again.");
  process.exit(65);
}

const contentTypes = new Map([
  [".css", "text/css; charset=utf-8"],
  [".html", "text/html; charset=utf-8"],
  [".ico", "image/x-icon"],
  [".js", "text/javascript; charset=utf-8"],
  [".json", "application/json; charset=utf-8"],
  [".map", "application/json; charset=utf-8"],
  [".png", "image/png"],
  [".svg", "image/svg+xml"],
  [".webp", "image/webp"],
  [".woff", "font/woff"],
  [".woff2", "font/woff2"],
]);

function resolveClientAsset(pathname) {
  let decodedPath;
  try {
    decodedPath = decodeURIComponent(pathname);
  } catch {
    return null;
  }
  const relativePath = decodedPath.replace(/^[/\\]+/, "");
  const candidate = resolve(clientRoot, relativePath);
  if (candidate !== clientRoot && !candidate.startsWith(`${clientRoot}${sep}`)) {
    return null;
  }
  return candidate;
}

async function fetchAsset(request) {
  const assetUrl = new URL(request.url);
  const assetPath = resolveClientAsset(assetUrl.pathname);
  if (!assetPath) {
    return new Response("Not Found", { status: 404 });
  }
  try {
    const metadata = await stat(assetPath);
    if (!metadata.isFile()) {
      return new Response("Not Found", { status: 404 });
    }
    const headers = new Headers({
      "Content-Length": String(metadata.size),
      "Content-Type": contentTypes.get(extname(assetPath).toLowerCase()) ?? "application/octet-stream",
    });
    if (assetUrl.pathname.includes("/assets/")) {
      headers.set("Cache-Control", "public, max-age=31536000, immutable");
    }
    return new Response(Readable.toWeb(createReadStream(assetPath)), { headers });
  } catch {
    return new Response("Not Found", { status: 404 });
  }
}

function toWebRequest(nodeRequest, port) {
  const headers = new Headers();
  for (let index = 0; index < nodeRequest.rawHeaders.length; index += 2) {
    headers.append(nodeRequest.rawHeaders[index], nodeRequest.rawHeaders[index + 1]);
  }
  const method = nodeRequest.method ?? "GET";
  const init = { method, headers };
  if (method !== "GET" && method !== "HEAD") {
    init.body = Readable.toWeb(nodeRequest);
    init.duplex = "half";
  }
  return new Request(`http://127.0.0.1:${port}${nodeRequest.url ?? "/"}`, init);
}

function applyResponseHeaders(nodeResponse, webResponse) {
  for (const [name, value] of webResponse.headers) {
    if (name.toLowerCase() !== "set-cookie") {
      nodeResponse.setHeader(name, value);
    }
  }
  const cookies = webResponse.headers.getSetCookie?.() ?? [];
  if (cookies.length > 0) {
    nodeResponse.setHeader("set-cookie", cookies);
  }
}

function openBrowser(url) {
  const command = process.platform === "win32"
    ? { executable: "cmd", arguments: ["/d", "/s", "/c", "start", "", url] }
    : process.platform === "darwin"
      ? { executable: "open", arguments: [url] }
      : { executable: "xdg-open", arguments: [url] };
  try {
    spawn(command.executable, command.arguments, {
      detached: true,
      stdio: "ignore",
      windowsHide: true,
    }).unref();
  } catch {
    // The URL remains visible in the console when no desktop opener exists.
  }
}

const pendingTasks = new Set();
const executionContext = {
  passThroughOnException() {},
  waitUntil(task) {
    const pendingTask = Promise.resolve(task).finally(() => pendingTasks.delete(pendingTask));
    pendingTasks.add(pendingTask);
  },
};

const server = createServer(async (nodeRequest, nodeResponse) => {
  try {
    const address = server.address();
    const port = typeof address === "object" && address ? address.port : requestedPort;
    const request = toWebRequest(nodeRequest, port);
    const directAssetResponse = await fetchAsset(request);
    const response = directAssetResponse.status !== 404
      ? directAssetResponse
      : await calculatorWorker.fetch(
          request,
          { ASSETS: { fetch: fetchAsset } },
          executionContext,
        );
    nodeResponse.statusCode = response.status;
    nodeResponse.statusMessage = response.statusText;
    applyResponseHeaders(nodeResponse, response);
    if (!response.body || nodeRequest.method === "HEAD") {
      nodeResponse.end();
      return;
    }
    Readable.fromWeb(response.body).pipe(nodeResponse);
  } catch (error) {
    console.error(error);
    if (!nodeResponse.headersSent) {
      nodeResponse.statusCode = 500;
      nodeResponse.setHeader("Content-Type", "text/plain; charset=utf-8");
    }
    nodeResponse.end("The calculator could not render this request.");
  }
});

function listen(port, remainingAttempts = 20) {
  server.once("error", (error) => {
    if (error.code === "EADDRINUSE" && remainingAttempts > 0 && port !== 0) {
      listen(port + 1, remainingAttempts - 1);
      return;
    }
    console.error(error);
    process.exitCode = 1;
  });
  server.listen(port, "127.0.0.1", () => {
    const address = server.address();
    const activePort = typeof address === "object" && address ? address.port : port;
    const url = `http://127.0.0.1:${activePort}/`;
    console.log(`Factorio Production Calculator is running at ${url}`);
    console.log("Keep this window open while using the calculator. Press Ctrl+C to stop it.");
    if (!noOpen) {
      openBrowser(url);
    }
  });
}

for (const signal of ["SIGINT", "SIGTERM"]) {
  process.on(signal, () => {
    server.close(async () => {
      await Promise.allSettled([...pendingTasks]);
      process.exit(0);
    });
  });
}

listen(requestedPort);
