export interface LocalServiceRecord {
  id: string;
  createdAt: string;
  [key: string]: unknown;
}
export interface LocalServicesState {
  database: Map<string, unknown>;
  cache: Map<string, { value: unknown; expiresAt?: number }>;
  mail: LocalServiceRecord[];
  sms: LocalServiceRecord[];
  webhooks: LocalServiceRecord[];
  storage: Map<string, Uint8Array>;
  queue: LocalServiceRecord[];
  cron: LocalServiceRecord[];
  auth: Map<string, LocalServiceRecord>;
  metrics: LocalServiceRecord[];
}
export function createLocalServicesState(): LocalServicesState {
  return {
    database: new Map(),
    cache: new Map(),
    mail: [],
    sms: [],
    webhooks: [],
    storage: new Map(),
    queue: [],
    cron: [],
    auth: new Map(),
    metrics: [],
  };
}
const json = (value: unknown, status = 200, origin = "https://localhost:3000") =>
  Response.json(value, {
    status,
    headers: {
      "cache-control": "no-store",
      "access-control-allow-origin": origin,
      "x-content-type-options": "nosniff",
    },
  });
async function boundedJson(
  request: Request,
  maximum = 256 * 1024,
): Promise<Record<string, unknown>> {
  const text = await request.text();
  if (new TextEncoder().encode(text).byteLength > maximum)
    throw new RangeError("payload-too-large");
  const value = JSON.parse(text) as unknown;
  if (!value || typeof value !== "object" || Array.isArray(value))
    throw new TypeError("object-required");
  return value as Record<string, unknown>;
}
function record(value: Record<string, unknown>): LocalServiceRecord {
  return { id: crypto.randomUUID(), createdAt: new Date().toISOString(), ...value };
}
export function createLocalServicesHandler(
  state = createLocalServicesState(),
  options: { origin?: string } = {},
) {
  const origin = options.origin ?? "https://localhost:3000";
  return async (request: Request): Promise<Response> => {
    const url = new URL(request.url);
    const path = url.pathname;
    if (request.method === "OPTIONS")
      return new Response(null, {
        status: 204,
        headers: {
          "access-control-allow-origin": origin,
          "access-control-allow-methods": "GET,POST,PUT,DELETE",
          "access-control-allow-headers": "content-type",
        },
      });
    if (path === "/healthz" || path === "/readyz")
      return json({ status: "up", service: "wrnexus-local-services" }, 200, origin);
    if (path === "/" || path === "/__services")
      return new Response(
        `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>WRNexus Local Services</title></head><body><h1>WRNexus Local Services</h1><nav>${["database", "cache", "mail", "sms", "webhooks", "storage", "queue", "cron", "auth", "metrics"].map((name) => `<a href="/${name}">${name}</a> `).join("")}</nav><p>Use the JSON endpoints to inspect or inject local development events.</p></body></html>`,
        {
          headers: {
            "content-type": "text/html; charset=utf-8",
            "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
          },
        },
      );
    try {
      if (path === "/database") {
        if (request.method === "GET") return json(Object.fromEntries(state.database), 200, origin);
        const body = await boundedJson(request);
        if (typeof body.key !== "string") return json({ error: "key is required" }, 400);
        state.database.set(body.key, body.value);
        return json({ key: body.key, value: body.value }, 201, origin);
      }
      if (path === "/cache") {
        if (request.method === "GET") {
          const now = Date.now();
          for (const [key, value] of state.cache)
            if (value.expiresAt && value.expiresAt <= now) state.cache.delete(key);
          return json(Object.fromEntries(state.cache), 200, origin);
        }
        const body = await boundedJson(request);
        if (typeof body.key !== "string") return json({ error: "key is required" }, 400);
        const ttlMs = typeof body.ttlMs === "number" ? Math.max(0, body.ttlMs) : undefined;
        state.cache.set(body.key, {
          value: body.value,
          ...(ttlMs ? { expiresAt: Date.now() + ttlMs } : {}),
        });
        return json({ stored: true }, 201, origin);
      }
      for (const [name, values] of [
        ["mail", state.mail],
        ["sms", state.sms],
        ["webhooks", state.webhooks],
        ["queue", state.queue],
        ["cron", state.cron],
        ["metrics", state.metrics],
      ] as const)
        if (path === `/${name}`) {
          if (request.method === "GET") return json(values, 200, origin);
          const value = record(await boundedJson(request));
          values.unshift(value);
          if (values.length > 500) values.length = 500;
          return json(value, 202, origin);
        }
      if (path === "/storage") {
        if (request.method === "GET")
          return json(
            [...state.storage.entries()].map(([key, value]) => ({ key, bytes: value.byteLength })),
            200,
            origin,
          );
        const key = url.searchParams.get("key");
        if (!key || key.includes("..") || key.length > 256)
          return json({ error: "safe key is required" }, 400);
        const bytes = new Uint8Array(await request.arrayBuffer());
        if (bytes.byteLength > 10 * 1024 * 1024) return json({ error: "object too large" }, 413);
        state.storage.set(key, bytes);
        return json({ key, bytes: bytes.byteLength }, 201, origin);
      }
      if (path.startsWith("/storage/") && request.method === "GET") {
        const key = decodeURIComponent(path.slice(9));
        const value = state.storage.get(key);
        return value
          ? new Response(Uint8Array.from(value), {
              headers: {
                "content-type": "application/octet-stream",
                "content-disposition": "attachment",
                "x-content-type-options": "nosniff",
              },
            })
          : json({ error: "not found" }, 404);
      }
      if (path === "/auth") {
        if (request.method === "GET") return json([...state.auth.values()], 200, origin);
        const value = record(await boundedJson(request));
        if (typeof value.email !== "string") return json({ error: "email is required" }, 400);
        state.auth.set(value.id, value);
        return json({ user: value, accessToken: `local_${value.id}` }, 201, origin);
      }
      return json({ error: "not found" }, 404);
    } catch (error) {
      if (error instanceof RangeError) return json({ error: error.message }, 413);
      return json({ error: error instanceof Error ? error.message : "invalid request" }, 400);
    }
  };
}

export interface LocalCertificate {
  cert: string;
  key: string;
  certFile: string;
  keyFile: string;
  reused: boolean;
}

/** Generate and cache a localhost-only development certificate without requiring OpenSSL. */
export async function ensureLocalCertificate(appRoot: string): Promise<LocalCertificate> {
  const directory = join(resolve(appRoot), ".wrnexus", "certificates");
  const certFile = join(directory, "localhost.pem");
  const keyFile = join(directory, "localhost-key.pem");
  if (existsSync(certFile) && existsSync(keyFile)) {
    try {
      const cert = readFileSync(certFile, "utf8");
      const key = readFileSync(keyFile, "utf8");
      const certificate = new X509Certificate(cert);
      if (Date.parse(certificate.validTo) > Date.now() + 7 * 24 * 60 * 60 * 1000) {
        return { cert, key, certFile, keyFile, reused: true };
      }
    } catch {
      // Replace invalid or expired development material below.
    }
  }
  const now = new Date();
  const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
  const generated = await generate([{ name: "commonName", value: "localhost" }], {
    algorithm: "sha256",
    keyType: "ec",
    curve: "P-256",
    notBeforeDate: new Date(now.getTime() - 60_000),
    notAfterDate: expires,
    extensions: [
      { name: "basicConstraints", cA: false, critical: true },
      { name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
      { name: "extKeyUsage", serverAuth: true },
      {
        name: "subjectAltName",
        altNames: [
          { type: 2, value: "localhost" },
          { type: 2, value: "*.localhost" },
          { type: 7, ip: "127.0.0.1" },
          { type: 7, ip: "::1" },
        ],
      },
    ],
  });
  mkdirSync(directory, { recursive: true });
  writeFileSync(certFile, generated.cert, { encoding: "utf8", mode: 0o600 });
  writeFileSync(keyFile, generated.private, { encoding: "utf8", mode: 0o600 });
  try {
    chmodSync(certFile, 0o600);
    chmodSync(keyFile, 0o600);
  } catch {
    // Windows ACLs are inherited from the private workspace directory.
  }
  return {
    cert: generated.cert,
    key: generated.private,
    certFile,
    keyFile,
    reused: false,
  };
}

export async function startLocalServices(
  options: {
    appRoot?: string;
    port?: number;
    hostname?: string;
    https?: boolean;
    origin?: string;
    certificate?: LocalCertificate;
  } = {},
) {
  const port = options.port ?? 3099;
  const hostname = options.hostname ?? "127.0.0.1";
  const secure = options.https !== false;
  const tls = secure
    ? (options.certificate ?? (await ensureLocalCertificate(options.appRoot ?? ".")))
    : undefined;
  const server = Bun.serve({
    port,
    hostname,
    fetch: createLocalServicesHandler(undefined, { origin: options.origin }),
    ...(tls ? { tls: { cert: tls.cert, key: tls.key } } : {}),
  });
  console.log(
    `  ▸ Local services: ${secure ? "https" : "http"}://${hostname}:${server.port}/__services`,
  );
  if (tls) console.log(`    certificate: ${tls.certFile} (trust locally to remove warnings)`);
  console.log("    database cache mail sms webhooks storage queue cron auth metrics");
  return server;
}
import { X509Certificate } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { generate } from "selfsigned";
