import { randomUUID } from "node:crypto";
import { createServer as createHttpServer } from "node:http";
import { createServer as createTcpServer } from "node:net";

import {
  ARUSNAVI_B2_MODEL_PROFILE,
  inspectUnverifiedInitialBytes,
} from "../../../packages/arusnavi-b2-adapter/src/index.mjs";

export function createDeviceGatewayRuntime(options = {}) {
  const config = normalizeConfig(options);
  const sessions = new Map();
  let totalAccepted = 0;
  let totalRejected = 0;
  let totalEvidence = 0;

  const tcpServer = createTcpServer((socket) => {
    if (sessions.size >= config.maxConcurrentSessions) {
      totalRejected += 1;
      socket.destroy();
      return;
    }

    const sessionRef = `session:${randomUUID()}`;
    const session = {
      sessionRef,
      bytes: [],
      byteLength: 0,
      evidenceRecorded: false,
    };
    sessions.set(socket, session);
    totalAccepted += 1;
    socket.setNoDelay(true);
    socket.setTimeout(config.sessionTimeoutMs);

    socket.on("data", (chunk) => {
      if (session.evidenceRecorded) return;
      session.byteLength += chunk.length;
      if (session.byteLength > config.maxInitialBytes) {
        totalRejected += 1;
        socket.destroy();
        return;
      }
      session.bytes.push(chunk);
      const evidence = inspectUnverifiedInitialBytes(
        Buffer.concat(session.bytes, session.byteLength),
      );
      session.evidenceRecorded = true;
      totalEvidence += 1;
      config.onEvidence?.({
        sessionRef,
        evidence,
      });
      // Until exact official framing is implemented, the gateway never sends
      // acknowledgement or command bytes and never guesses an identifier.
      socket.end();
    });
    socket.on("timeout", () => socket.destroy());
    socket.on("close", () => sessions.delete(socket));
    socket.on("error", () => sessions.delete(socket));
  });

  const healthServer = createHttpServer((request, response) => {
    response.setHeader("Content-Type", "application/json; charset=utf-8");
    response.setHeader("Cache-Control", "no-store");
    response.setHeader("X-Content-Type-Options", "nosniff");
    if (request.method !== "GET" || request.url !== "/healthz") {
      response.statusCode = 404;
      return response.end('{"ok":false,"error":"device_gateway_route_not_found"}\n');
    }
    response.statusCode = 200;
    return response.end(`${JSON.stringify({
      ok: true,
      service: "nodedc-device-gateway",
      protocolProfile: ARUSNAVI_B2_MODEL_PROFILE.profileRef,
      framing: ARUSNAVI_B2_MODEL_PROFILE.framing.status,
      tcpListener: config.listenEnabled ? "internal-test-only" : "disabled",
      publicIngress: "disabled",
      commandTransport: "disabled",
      sessions: {
        active: sessions.size,
        accepted: totalAccepted,
        rejected: totalRejected,
        evidenceRecorded: totalEvidence,
      },
    })}\n`);
  });

  return {
    async start() {
      await listen(healthServer, config.healthPort, config.healthHost);
      if (config.listenEnabled) {
        await listen(tcpServer, config.tcpPort, config.tcpHost);
      }
      return {
        healthAddress: healthServer.address(),
        tcpAddress: config.listenEnabled ? tcpServer.address() : null,
      };
    },
    async stop() {
      for (const socket of sessions.keys()) socket.destroy();
      await Promise.all([
        closeServer(healthServer),
        config.listenEnabled ? closeServer(tcpServer) : Promise.resolve(),
      ]);
    },
    status() {
      return {
        activeSessions: sessions.size,
        totalAccepted,
        totalRejected,
        totalEvidence,
        commandTransport: "disabled",
        publicIngress: "disabled",
      };
    },
  };
}

function normalizeConfig(input) {
  const listenEnabled = input.listenEnabled === true;
  const maxInitialBytes = parseInteger(
    input.maxInitialBytes,
    ARUSNAVI_B2_MODEL_PROFILE.framing.maxInitialBytes,
    1,
    ARUSNAVI_B2_MODEL_PROFILE.framing.maxInitialBytes,
    "device_gateway_initial_bytes_invalid",
  );
  return {
    listenEnabled,
    healthHost: normalizeHealthHost(input.healthHost, "127.0.0.1"),
    healthPort: parseInteger(
      input.healthPort,
      18121,
      0,
      65535,
      "device_gateway_health_port_invalid",
    ),
    tcpHost: normalizeTcpHost(input.tcpHost, "127.0.0.1"),
    tcpPort: parseInteger(
      input.tcpPort,
      9921,
      0,
      65535,
      "device_gateway_tcp_port_invalid",
    ),
    maxInitialBytes,
    maxConcurrentSessions: parseInteger(
      input.maxConcurrentSessions,
      100,
      1,
      10000,
      "device_gateway_session_limit_invalid",
    ),
    sessionTimeoutMs: parseInteger(
      input.sessionTimeoutMs,
      10000,
      100,
      60000,
      "device_gateway_session_timeout_invalid",
    ),
    onEvidence: typeof input.onEvidence === "function"
      ? input.onEvidence
      : undefined,
  };
}

function normalizeHealthHost(value, fallback) {
  const normalized = String(value || fallback).trim();
  if (!["127.0.0.1", "::1", "0.0.0.0", "::"].includes(normalized)) {
    throw new TypeError("device_gateway_health_host_invalid");
  }
  return normalized;
}

function normalizeTcpHost(value, fallback) {
  const normalized = String(value || fallback).trim();
  if (!["127.0.0.1", "::1"].includes(normalized)) {
    throw new TypeError("device_gateway_baseline_loopback_only");
  }
  return normalized;
}

function parseInteger(value, fallback, minimum, maximum, errorCode) {
  const parsed = Number(value ?? fallback);
  if (
    !Number.isSafeInteger(parsed)
    || parsed < minimum
    || parsed > maximum
  ) {
    throw new TypeError(errorCode);
  }
  return parsed;
}

function listen(server, port, host) {
  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(port, host, resolve);
  });
}

function closeServer(server) {
  if (!server.listening) return Promise.resolve();
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}
