/**
 * node:http adapter — bridge a WinterCG `fetch(request) => Response` handler
 * onto a Node HTTP server, with no external dependencies. Converts a Node
 * `IncomingMessage` into a web `Request` and writes a web `Response` back into a
 * `ServerResponse` (preserving multiple `Set-Cookie` headers).
 *
 * Caveat: the production handler uses Bun-native APIs (Bun.file for assets,
 * Bun.serve for websockets, Bun.SQL / bun:sqlite for the database), so running
 * the FULL app under plain Node needs Bun-compatible globals. This adapter is
 * for WinterCG hosts and for embedding the handler behind an existing
 * `node:http` server; the Request/Response conversion itself is fully portable.
 */

import type { IncomingMessage, ServerResponse, Server } from "node:http";

export type FetchHandler = (req: Request) => Response | undefined | Promise<Response | undefined>;

/** Convert a Node IncomingMessage into a web Request (buffers the body). */
export async function toRequest(
  req: IncomingMessage,
  opts: { origin?: string } = {},
): Promise<Request> {
  const method = req.method ?? "GET";
  const host = req.headers.host ?? "localhost";
  const proto = (asString(req.headers["x-forwarded-proto"]) ?? "http").split(",")[0]!.trim();
  const origin = opts.origin ?? `${proto}://${host}`;
  const url = new URL(req.url ?? "/", origin);

  const headers = new Headers();
  for (const [key, value] of Object.entries(req.headers)) {
    if (value === undefined) continue;
    if (Array.isArray(value)) for (const v of value) headers.append(key, v);
    else headers.set(key, value);
  }

  const hasBody = method !== "GET" && method !== "HEAD";
  const body = hasBody ? ((await readBody(req)) as BodyInit) : undefined;
  return new Request(url, { method, headers, body });
}

function asString(value: string | string[] | undefined): string | undefined {
  return Array.isArray(value) ? value[0] : value;
}

function readBody(req: IncomingMessage): Promise<Uint8Array> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on("data", (c: Buffer) => chunks.push(c));
    req.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
    req.on("error", reject);
  });
}

/** Write a web Response into a Node ServerResponse. */
export async function writeResponse(res: ServerResponse, response: Response): Promise<void> {
  const headers: Record<string, string | string[]> = {};
  response.headers.forEach((value, key) => {
    if (key.toLowerCase() !== "set-cookie") headers[key] = value;
  });
  // Multiple Set-Cookie headers must stay separate (Headers.forEach joins them).
  const getSetCookie = (response.headers as { getSetCookie?: () => string[] }).getSetCookie;
  const cookies = typeof getSetCookie === "function" ? getSetCookie.call(response.headers) : [];
  if (cookies.length) headers["set-cookie"] = cookies;

  res.writeHead(response.status, headers);

  if (response.body) {
    const reader = response.body.getReader();
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      res.write(value);
    }
  } else {
    const buf = new Uint8Array(await response.arrayBuffer());
    if (buf.length) res.write(buf);
  }
  res.end();
}

/** A `node:http` request listener that dispatches to a fetch handler. */
export function nodeListener(handler: FetchHandler, opts: { origin?: string } = {}) {
  return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
    try {
      const response = await handler(await toRequest(req, opts));
      if (!response) {
        // A missing response means the handler expected a protocol upgrade
        // (e.g. a WebSocket), which this HTTP adapter does not perform.
        res.writeHead(426, { "content-type": "text/plain" });
        res.end("Upgrade Required");
        return;
      }
      await writeResponse(res, response);
    } catch (err) {
      if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain" });
      res.end("Internal Server Error");
      console.error("[wrnexus] node adapter error:", err);
    }
  };
}

/** Create and start a `node:http` server for a fetch handler. */
export async function serveNode(
  handler: FetchHandler,
  opts: { port?: number; hostname?: string } = {},
): Promise<Server> {
  const { createServer } = await import("node:http");
  const server = createServer(nodeListener(handler, {}));
  const port = opts.port ?? 3000;
  server.listen(port, opts.hostname ?? "0.0.0.0");
  console.log(`WrNexus (node adapter) listening on http://localhost:${port}`);
  return server;
}
